Source

components/memos/MemoInfo.jsx

import React, { useState } from 'react';
import { User, MapPin, FileText, Phone, Building2, SquareAsterisk, Hospital } from 'lucide-react';
import PhoneLink from './PhoneLink';
import { useAuth } from '../../contexts/AuthContext';
import toast from 'react-hot-toast';
import { RotateCcw } from 'lucide-react';

/**
 * Displays detailed information about a memo, including request details, user information,
 * and progress through its timeline. It groups information into sections using cards.
 * Allows refreshing of OTPs for creators and approvers.
 * Provides a progress bar showing the completion status based on approved timeline entries.
 */


// Map field key to {label, section, icon} (location fields merged into Request Info)
const fieldMap = {
  complaint: { label: 'Complaint', section: 'Request Info', icon: FileText },
  department: { label: 'Department', section: 'Request Info', icon: Building2 },
  tagged_role_name: { label: 'Tagged Role', section: 'Request Info', icon: User },

  role_name: { label: 'User Role', section: 'User Info', icon: User },
  phone_number: { label: 'Phone', section: 'User Info', icon: Phone },
  user_role_name: { label: 'User Role', section: 'User Info', icon: User },
  institution_id: { label: 'Name', section: 'User Info', icon: Building2 },
  attendee_otp: { label: 'Attendee OTP', section: 'User Info', icon: SquareAsterisk },
  completion_otp: { label: 'Completion OTP', section: 'User Info', icon: SquareAsterisk },


  current_block_name: { label: 'Block', section: 'Request Info', icon: MapPin },
  current_ward_name: { label: 'Ward', section: 'Request Info', icon: MapPin },
  current_floor: { label: 'Floor', section: 'Request Info', icon: MapPin },
};

// Section icons
const sectionIcons = {
  'Request Info': FileText,
  'User Info': User,

};

const MemoInfo = ({ info, data, memoId }) => {

  // no accordion, just cards
  const { user, apiService, hospital } = useAuth();
  const [expanded, setExpanded] = useState(false);
  const [timelineData, setTimelineData] = useState(data);
  const [memoInfo, setMemoInfo] = useState(info);

  const handleRefreshOtp = async () => {
    try {
      const result = await apiService.refreshOTP(hospital.id, memoId); // Replace with actual API call
      if (result.attendee_otp || result.completion_otp) {
        console.log(result);
        setMemoInfo(prev => ({
          ...prev,
          attendee_otp: result.attendee_otp ? result.attendee_otp: prev.attendee_otp,
          completion_otp: result.completion_otp ? result.completion_otp: prev.completion_otp,
        }));
        toast.success("OTP refreshed");
      }
    } catch (error) {
      toast.error("could not refresh OTP");
      console.error("Failed to refresh OTP:", error);
    }
  };


  if (!memoInfo) {
    return (
      <div className="flex items-center justify-center p-8 text-gray-500">
        <div className="text-center">
          <FileText className="w-12 h-12 mx-auto mb-3 text-gray-300" />
          <p className="text-sm">No information available</p>
        </div>
      </div>
    );
  }

  // Group fields by section
  const sections = {};
  Object.entries(fieldMap).forEach(([key, meta]) => {
    if (memoInfo[key] !== undefined && memoInfo[key] !== null && memoInfo[key] !== '') {
      sections[meta.section] = sections[meta.section] || [];
      sections[meta.section].push({
        key,
        label: meta.label,
        value: memoInfo[key],
        icon: meta.icon
      });
    }
  });


  const formatValue = (value, key) => {
    // Special formatting for phone numbers
    if (key === 'phone_number' && typeof value === 'string') {
      return (
        <PhoneLink phone={value} />
      );
    }

    if ((key === 'attendee_otp' || key === 'completion_otp') && (user.is_creator || user.is_approver)) {
      return (
        <div className="flex items-center gap-2">
          <span className="font-mono">{value}</span>
          { key==="attendee_otp" && 
          <button
            onClick={handleRefreshOtp}
            title="Refresh OTP"
            className="inline-flex items-center gap-1 px-3 py-1.5 bg-transparent text-blue-600 border border-blue-300 text-sm rounded-md transition"
          >
            <RotateCcw className="w-4 h-4" />
            <span>Refresh</span>
          </button>}


        </div>
      );
    }

    // Truncate long text on mobile with expand option
    if (typeof value === 'string' && value.length > 100) {
      return (
        <div className="group">
          {
            expanded ?
              <span className="block">
                {value.substring(0, 80)}...
                <button className="text-blue-600 text-xs ml-1 underline" onClick={() => setExpanded(false)}>more</button>
              </span> :
              <span className="sm:block">
                {value}
                <button className="text-blue-600 text-xs ml-1 underline" onClick={() => setExpanded(true)}>less</button>
              </span>
          }
        </div>
      );
    }

    return value;
  };

  return (
    <>

      {/* Overall status */}
      <div className="mt-6 pt-4 border-t border-gray-200">
        <div className="flex items-center justify-between">
          <span className="text-sm text-gray-600">Overall Progress</span>
          <div className="flex items-center gap-4">
            <span className="text-sm font-medium text-green-600">
              {timelineData.filter(item => item.approved).length} approved
            </span>
            <span className="text-sm font-medium">
              {timelineData.filter(item => item.approved).length} of {timelineData.length} completed
            </span>
          </div>
        </div>
        <div className="mt-2 w-full bg-gray-200 rounded-full h-2 overflow-hidden">
          <div
            className="bg-green-600 transition-all duration-300 h-full"
            style={{
              width: `${(timelineData.filter(item => item.approved).length / timelineData.length) * 100}%`
            }}
          ></div>
        </div>
      </div>
      <hr className="my-6 border-gray-200" />
      {/* memo detail start */}
      <div className="grid gap-6 md:grid-cols-2 px-4 sm:px-0 pb-6">
        {Object.entries(sections).map(([sectionTitle, fields]) => {
          const SectionIcon = sectionIcons[sectionTitle] || FileText;

          return (
            <div key={sectionTitle} className="bg-white rounded-lg shadow-sm border border-gray-200">
              <div className="px-4 py-3 sm:px-6 sm:py-4 border-b border-gray-200 flex items-center space-x-3 bg-gray-50">
                <div className="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
                  <SectionIcon className="w-4 h-4 text-blue-600" />
                </div>
                <h3 className="text-base sm:text-lg font-semibold text-gray-800">
                  {sectionTitle}
                </h3>
              </div>
              <div className="p-4 sm:p-6">
                <div className="space-y-4 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-x-6 sm:gap-y-4">
                  {fields.map(({ key, label, value, icon: FieldIcon }) => (
                    <div key={key}>
                      <div className="flex items-center space-x-2 mb-1.5">
                        <FieldIcon className="w-4 h-4 text-gray-500" />
                        <dt className="text-sm font-medium text-gray-600">{label}</dt>
                      </div>
                      <dd className="text-sm text-gray-900 break-words pl-6">
                        {formatValue(value, key)}
                      </dd>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          );
        })}

        {Object.keys(sections).length === 0 && (
          <div className="bg-gray-50 rounded-lg p-8 text-center sm:col-span-2">
            <FileText className="w-12 h-12 mx-auto mb-3 text-gray-300" />
            <p className="text-gray-500 text-sm">No details available to display</p>
          </div>
        )}
      </div>
    </>
  );
};

export default MemoInfo;