Files
AI_Pharmacy_Frontend/src/pages/Patients.tsx
T

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>
);
}