Source

components/prototype/MemoTabbedList.jsx

import { Building, ChevronRight, MapPin } from 'lucide-react';
import { useMemo, useState } from 'react';

/**
 * A tabbed list component for displaying and filtering memos.
 *
 * @param {object} props - The component props.
 * @param {object|Array} props.data - The memo data, either an array or an object with tab keys.
 * @param {function} [props.onMemoClick] - Callback function when a memo card is clicked.
 **/
const MemoTabbedList = ({ data, onMemoClick }) => {
  const tabMap = useMemo(() => {
    if (!data) return {};
    if (Array.isArray(data)) {
      const flat = data.map((m) => ({ ...m, ...m.latest_snapshot }));
      return { All: flat };
    }
    const firstKey = Object.keys(data)[0] || 'default';
    return data[firstKey] || {};
  }, [data]);

  const tabNames = Object.keys(tabMap);
  const [activeTab, setActiveTab] = useState(tabNames[0] || '');

  const allMemos = tabMap[activeTab] || [];

  const unique = (arr) => [...new Set(arr.filter(Boolean))];
  const wardOptions = unique(allMemos.map((m) => m.latest_snapshot.current_ward_name));
  const blockOptions = unique(allMemos.map((m) => m.latest_snapshot.current_block_name));
  const floorOptions = unique(allMemos.map((m) => m.latest_snapshot.current_floor));
  const deptOptions = unique(allMemos.map((m) => m.latest_snapshot.department));

  const [searchInput, setSearchInput] = useState('');
  const [wardInput, setWardInput] = useState('');
  const [blockInput, setBlockInput] = useState('');
  const [floorInput, setFloorInput] = useState('');
  const [deptInput, setDeptInput] = useState('');
  const [dateInput, setDateInput] = useState('');

  const [search, setSearch] = useState('');
  const [wardFilter, setWardFilter] = useState('');
  const [blockFilter, setBlockFilter] = useState('');
  const [floorFilter, setFloorFilter] = useState('');
  const [deptFilter, setDeptFilter] = useState('');
  const [dateFilter, setDateFilter] = useState('');

  const [filtersVisible, setFiltersVisible] = useState(false);

  const applyFilters = () => {
    setSearch(searchInput);
    setWardFilter(wardInput);
    setBlockFilter(blockInput);
    setFloorFilter(floorInput);
    setDeptFilter(deptInput);
    setDateFilter(dateInput);
  };

  const resetFilters = () => {
    setSearchInput('');
    setWardInput('');
    setBlockInput('');
    setFloorInput('');
    setDeptInput('');
    setDateInput('');
    setSearch('');
    setWardFilter('');
    setBlockFilter('');
    setFloorFilter('');
    setDeptFilter('');
    setDateFilter('');
  };

  const filtered = allMemos.filter((m) => {
    const dateOnly = (d) => new Date(d).toISOString().split('T')[0];
    const textOk = search ? (m.latest_snapshot.complaint || '').toLowerCase().includes(search.toLowerCase()) : true;
    const wardOk = wardFilter ? m.latest_snapshot.current_ward_name === wardFilter : true;
    const deptOk = deptFilter ? m.latest_snapshot.department === deptFilter : true;
    const blockOk = blockFilter ? m.latest_snapshot.current_block_name === blockFilter : true;
    const floorOk = floorFilter ? String(m.latest_snapshot.current_floor) === String(floorFilter) : true;
    const dateOk = dateFilter ? dateOnly(m.created_at) === dateFilter : true;
    return textOk && wardOk && deptOk && blockOk && floorOk && dateOk;
  });

  const formatDate = (dateString) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  };

  const getStatusColor = (taggedRoleName) => {
    const colors = {
      'nursing superintendent': 'bg-blue-100 text-blue-800',
      doctor: 'bg-green-100 text-green-800',
      admin: 'bg-purple-100 text-purple-800',
      default: 'bg-gray-100 text-gray-800',
    };
    return colors[taggedRoleName?.toLowerCase()] || colors.default;
  };

  function getDisplayStatusColor(status) {
    switch (status.toLowerCase()) {
      case 'completed':
        return 'bg-green-100 text-green-800';
      case 'attended':
        return 'bg-blue-100 text-blue-800';
      case 'incomplete':
        return 'bg-red-100 text-red-800';
      case 'tagged':
        return 'bg-yellow-100 text-yellow-800';
      default:
        return 'bg-gray-100 text-gray-700'; // for 'pending' or unknown
    }
  }


  return (
    <div className="space-y-4">
      {/* Tabs */}
      <div className="flex gap-2 flex-wrap">
        {tabNames.map((tab) => (
          <button
            key={tab}
            onClick={() => setActiveTab(tab)}
            className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${tab === activeTab ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
              }`}
          >
            {tab}
          </button>
        ))}
      </div>

      {/* Filter Toggle Button */}
      <div>
        <button
          className="bg-blue-100 text-blue-700 px-4 py-2 rounded-md text-sm mb-2"
          onClick={() => setFiltersVisible((prev) => !prev)}
        >
          {filtersVisible ? 'Hide Filters' : 'Memo Filter'}
        </button>
      </div>

      {/* Filters */}
      {filtersVisible && (
        <div className="bg-gray-50 border rounded-md p-4 space-y-3">
          <div className="grid grid-cols-1 md:grid-cols-6 gap-3">
            <input
              type="text"
              placeholder="Search complaint"
              value={searchInput}
              onChange={(e) => setSearchInput(e.target.value)}
              className="border rounded-md px-3 py-2 text-sm w-full"
            />
            <select value={wardInput} onChange={(e) => setWardInput(e.target.value)} className="border rounded-md px-3 py-2 text-sm w-full">
              <option value="">All Wards</option>
              {wardOptions.map((w) => <option key={w} value={w}>{w}</option>)}
            </select>
            <select value={deptInput} onChange={(e) => setDeptInput(e.target.value)} className="border rounded-md px-3 py-2 text-sm w-full">
              <option value="">All Departments</option>
              {deptOptions.map((d) => <option key={d} value={d}>{d}</option>)}
            </select>
            <select value={blockInput} onChange={(e) => setBlockInput(e.target.value)} className="border rounded-md px-3 py-2 text-sm w-full">
              <option value="">All Blocks</option>
              {blockOptions.map((b) => <option key={b} value={b}>{b}</option>)}
            </select>
            <select value={floorInput} onChange={(e) => setFloorInput(e.target.value)} className="border rounded-md px-3 py-2 text-sm w-full">
              <option value="">All Floors</option>
              {floorOptions.map((f) => <option key={f} value={f}>{f}</option>)}
            </select>
            <input
              type="date"
              value={dateInput}
              onChange={(e) => setDateInput(e.target.value)}
              className="border rounded-md px-3 py-2 text-sm w-full"
            />
          </div>

          <div className="flex gap-2 justify-end">
            <button
              onClick={resetFilters}
              className="text-gray-600 border border-gray-300 px-4 py-2 rounded-md text-sm hover:bg-gray-100"
            >
              Reset Filters
            </button>
            <button
              onClick={applyFilters}
              className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm"
            >
              Apply Filters
            </button>
          </div>
        </div>
      )}

      {/* Memo Cards */}
      <div className="space-y-3 max-h-[500px] overflow-y-auto pr-1">
        {filtered.length === 0 ? (
          <p className="text-sm text-gray-500">No memos match your criteria.</p>
        ) : (
          filtered.map((memo) => {
            const snapshot = memo.latest_snapshot;
            const status = snapshot.display_status ||snapshot.status || 'pending';

            return (
              <div
                key={memo.memoId}
                className="bg-white rounded-lg shadow-sm border border-gray-200 p-3 active:bg-gray-50 transition-all cursor-pointer hover:shadow-md"
                onClick={() => onMemoClick?.(memo)}
              >
                <div className="flex justify-between items-center mb-2">
                  <div className="flex items-center gap-2">
                    <span className={`text-xs px-2 py-1 rounded-full font-medium ${getStatusColor(snapshot.tagged_role_name)}`}>
                      {snapshot.tagged_role_name || 'Unassigned'}
                    </span>
                    <span className="text-xs text-gray-500">{formatDate(memo.created_at)}</span>
                  </div>
                  <ChevronRight className="w-4 h-4 text-gray-400" />
                </div>

                <h3 className="font-medium text-gray-900 mb-2 line-clamp-2">
                  {snapshot.complaint || 'No complaint description available'}
                </h3>

                {/* Status badge */}
                <div className="mb-2">
                  <span className={`inline-block text-xs px-2 py-1 rounded-full font-semibold capitalize ${getDisplayStatusColor(status)}`}>
                    {status}
                  </span>
                </div>

                <div className="flex items-center gap-4 text-sm text-gray-600">
                  <span className="flex items-center gap-1">
                    <Building className="w-3 h-3" />
                    {snapshot.department}
                  </span>
                  <span className="flex items-center gap-1">
                    <MapPin className="w-3 h-3" />
                    {snapshot.current_ward_name}
                  </span>
                </div>
              </div>
            );
          })
        )}
      </div>

    </div>
  );
};

export default MemoTabbedList;