189 lines
7.5 KiB
TypeScript
189 lines
7.5 KiB
TypeScript
import { useEffect, useState, useMemo } from 'react';
|
|
import { fetchSyncedPatients } from '../services/api';
|
|
import { UserCircle, Mail, Phone, CalendarClock, ShieldCheck, Search } from 'lucide-react';
|
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
import { Table } from '../components/Table';
|
|
import Pagination from '../components/Pagination';
|
|
|
|
const columnHelper = createColumnHelper<any>();
|
|
|
|
const columns = [
|
|
columnHelper.accessor('full_name', {
|
|
header: 'Patient',
|
|
size: 350,
|
|
cell: info => {
|
|
const patient = info.row.original;
|
|
return (
|
|
<div className="py-1">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-emerald-100/50 rounded-full flex items-center justify-center text-emerald-600 transition-colors duration-300">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<div className="font-semibold text-slate-800 text-[14px]">{patient.full_name}</div>
|
|
<div className="text-[12px] text-slate-400 font-mono mt-0.5">ID: {patient.hospital_patient_id}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}),
|
|
columnHelper.accessor('contact_number', {
|
|
header: 'Contact',
|
|
size: 200,
|
|
cell: info => (
|
|
<div className="flex items-center gap-2 text-[14px] text-slate-600">
|
|
<Phone className="w-4 h-4 text-emerald-500" />
|
|
{info.getValue() || 'N/A'}
|
|
</div>
|
|
)
|
|
}),
|
|
columnHelper.accessor('email', {
|
|
header: 'Email',
|
|
size: 250,
|
|
cell: info => (
|
|
<div className="flex items-center gap-2 text-[14px] text-slate-600">
|
|
<Mail className="w-4 h-4 text-emerald-500" />
|
|
{info.getValue() || 'N/A'}
|
|
</div>
|
|
)
|
|
}),
|
|
columnHelper.accessor('created_at', {
|
|
header: 'Synced At',
|
|
size: 200,
|
|
cell: info => (
|
|
<div className="flex items-center gap-2 text-[14px] text-slate-600">
|
|
<CalendarClock className="w-4 h-4 text-emerald-500" />
|
|
{new Date(info.getValue()).toLocaleString()}
|
|
</div>
|
|
)
|
|
})
|
|
];
|
|
|
|
export default function Patients() {
|
|
const [patients, setPatients] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
useEffect(() => {
|
|
fetchSyncedPatients()
|
|
.then(data => {
|
|
setPatients(data);
|
|
setLoading(false);
|
|
})
|
|
.catch(err => {
|
|
console.error('Error fetching synced patients:', err);
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
const filteredPatients = useMemo(() => {
|
|
const safePatients = Array.isArray(patients) ? patients : [];
|
|
if (!searchQuery.trim()) return safePatients;
|
|
const lowerQuery = searchQuery.toLowerCase();
|
|
return safePatients.filter(patient =>
|
|
patient && (
|
|
(patient.full_name && String(patient.full_name).toLowerCase().includes(lowerQuery)) ||
|
|
(patient.hospital_patient_id && String(patient.hospital_patient_id).toLowerCase().includes(lowerQuery)) ||
|
|
(patient.contact_number && String(patient.contact_number).toLowerCase().includes(lowerQuery)) ||
|
|
(patient.email && String(patient.email).toLowerCase().includes(lowerQuery))
|
|
)
|
|
);
|
|
}, [patients, searchQuery]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full min-h-[400px]">
|
|
<div className="w-10 h-10 border-4 border-slate-200 border-t-emerald-600 rounded-full animate-spin"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
const totalItems = filteredPatients.length;
|
|
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
|
|
const currentData = filteredPatients.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
|
|
|
return (
|
|
<div className="max-w-[1400px] mx-auto flex flex-col w-full p-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
<div className="flex flex-col md:flex-row justify-between items-center bg-white/50 backdrop-blur-xl p-6 rounded-2xl border border-white/20 shadow-sm mb-6 gap-4">
|
|
<div className="w-full md:w-auto">
|
|
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-500 bg-clip-text text-transparent">
|
|
Hospital Patient Directory
|
|
</h1>
|
|
<p className="text-gray-500 mt-1 flex items-center gap-2">
|
|
<ShieldCheck className="w-4 h-4 text-emerald-500" />
|
|
Securely Synced via AI Integration
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 w-full md:w-auto">
|
|
<div className="relative w-full md:w-80 group">
|
|
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none transition-colors text-slate-400 group-focus-within:text-emerald-500" style={{ paddingLeft: '16px' }}>
|
|
<Search size={18} strokeWidth={2} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => {
|
|
setSearchQuery(e.target.value);
|
|
setCurrentPage(1); // Reset page on search
|
|
}}
|
|
placeholder="Search by name, ID or contact..."
|
|
className="w-full py-2.5 bg-white border border-slate-200 hover:border-slate-300 rounded-xl focus:outline-none focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800 text-[14px]"
|
|
style={{ paddingLeft: '44px', paddingRight: '16px' }}
|
|
/>
|
|
</div>
|
|
<div className="text-sm font-medium px-4 py-2 bg-emerald-50 text-emerald-700 rounded-full border border-emerald-100 whitespace-nowrap">
|
|
{filteredPatients.length} Records
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-full flex flex-col bg-white border border-slate-200 rounded-2xl shadow-sm relative">
|
|
<div className="overflow-x-auto">
|
|
{(!Array.isArray(patients) || patients.length === 0) ? (
|
|
<div className="p-16 text-center text-slate-500 flex flex-col items-center min-h-[400px] justify-center">
|
|
<UserCircle className="w-16 h-16 mx-auto mb-6 opacity-20" />
|
|
<h3 className="text-xl font-medium text-slate-700 mb-2">No Patients Found</h3>
|
|
<p className="max-w-md mx-auto">No patients have been synced from the hospital yet.</p>
|
|
</div>
|
|
) : filteredPatients.length === 0 ? (
|
|
<div className="p-16 text-center text-slate-500 flex flex-col items-center min-h-[400px] justify-center">
|
|
<Search className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
|
<h3 className="text-xl font-medium text-slate-700 mb-2">No Matches Found</h3>
|
|
<p className="max-w-md mx-auto">No patients match your search "{searchQuery}".</p>
|
|
</div>
|
|
) : (
|
|
<Table
|
|
data={currentData}
|
|
columns={columns}
|
|
getRowId={(item) => item.id}
|
|
emptyMessage="No patients found"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{totalItems > 0 && (
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
totalItems={totalItems}
|
|
itemsPerPage={itemsPerPage}
|
|
itemsPerPageOptions={[10, 25, 50, 100]}
|
|
onPageChange={(page) => setCurrentPage(page)}
|
|
onItemsPerPageChange={(limit) => {
|
|
setItemsPerPage(limit);
|
|
setCurrentPage(1);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|