import React, { useState, useMemo, useEffect } from 'react';
/**
* @typedef {object} MemoData
* @property {string} complaint - The main complaint text of the memo.
* @property {string} department - The department related to the memo.
* @property {string} current_ward_name - The name of the current ward.
* @property {string} current_block_name - The name of the current block.
* Tailwind-only version of MemoExplorer (no Mantine).
*/
const MemoExplorerTW = ({ data }) => {
// Normalise
const normalised = useMemo(() => {
if (Array.isArray(data)) {
const flattened = data.map((item) => {
// If API returns latest_snapshot, merge it so filters can access complaint/department, etc.
if (item.latest_snapshot) {
/** @type {MemoData} */
return {
id: item.memoId || item.id,
...item.latest_snapshot,
original: item, // keep reference
};
}
return item;
});
return { default: { list: flattened } };
}
return data || {};
}, [data]);
/* Role & Tab state */
const roles = useMemo(() => Object.keys(normalised), [normalised]);
const [activeRole, setActiveRole] = useState(roles[0] || '');
const tabs = useMemo(() => (activeRole ? Object.keys(normalised[activeRole]) : []), [normalised, activeRole]);
const [activeTab, setActiveTab] = useState(tabs[0] || '');
// Keep in sync when data changes
useEffect(() => {
if (!roles.includes(activeRole)) setActiveRole(roles[0] || '');
}, [roles]);
useEffect(() => {
if (!tabs.includes(activeTab)) setActiveTab(tabs[0] || '');
}, [tabs]);
const currentMemos = useMemo(() => {
if (!activeRole || !activeTab) return [];
return normalised[activeRole][activeTab] || [];
}, [normalised, activeRole, activeTab]);
/* Filter state */
const [search, setSearch] = useState('');
const [wardFilter, setWardFilter] = useState('');
const [deptFilter, setDeptFilter] = useState('');
const wardOptions = useMemo(() => {
return [...new Set(currentMemos.map((m) => m.current_ward_name).filter(Boolean))];
}, [currentMemos]);
const deptOptions = useMemo(() => {
return [...new Set(currentMemos.map((m) => m.department).filter(Boolean))];
}, [currentMemos]);
/* Apply filters */
const filtered = useMemo(() => {
return currentMemos.filter((m) => {
const searchOk = search ? m.complaint?.toLowerCase().includes(search.toLowerCase()) : true;
const wardOk = wardFilter ? m.current_ward_name === wardFilter : true;
const deptOk = deptFilter ? m.department === deptFilter : true;
return searchOk && wardOk && deptOk;
});
}, [currentMemos, search, wardFilter, deptFilter]);
/* UI helpers */
const roleTabButton = (label, active, onClick) => (
<button
key={label}
onClick={onClick}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${active ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
>
{label}
</button>
);
return (
<div className="space-y-4">
{/* Role selector */}
{roles.length > 1 && (
<div className="flex gap-2 flex-wrap">
{roles.map((role) => roleTabButton(role, role === activeRole, () => setActiveRole(role)))}
</div>
)}
{/* Tab selector */}
{tabs.length > 1 && (
<div className="flex gap-2 flex-wrap">
{tabs.map((tab) => roleTabButton(tab, tab === activeTab, () => setActiveTab(tab)))}
</div>
)}
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-3">
<input
type="text"
placeholder="Search complaints..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1 border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<select
value={wardFilter}
onChange={(e) => setWardFilter(e.target.value)}
className="border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Wards</option>
{wardOptions.map((w) => (
<option key={w} value={w}>{w}</option>
))}
</select>
<select
value={deptFilter}
onChange={(e) => setDeptFilter(e.target.value)}
className="border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Departments</option>
{deptOptions.map((d) => (
<option key={d} value={d}>{d}</option>
))}
</select>
</div>
{/* List */}
<div className="max-h-[500px] overflow-y-auto space-y-3 pr-1">
{filtered.length === 0 ? (
<p className="text-sm text-gray-500">No memos match your criteria.</p>
) : (
filtered.map((m) => (
<div key={m.id} className="bg-white border border-gray-200 rounded-md p-3 shadow-sm">
<div className="flex justify-between items-start mb-1">
<h3 className="font-semibold text-gray-900 text-sm line-clamp-2 mr-2">{m.complaint}</h3>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full">{m.department}</span>
</div>
<p className="text-xs text-gray-500">Ward: {m.current_ward_name || '-'} | Block: {m.current_block_name || '-'}</p>
</div>
))
)}
</div>
</div>
);
};
export default MemoExplorerTW;
Source