"use client"; import { Plus, Search, Smartphone } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { AiBadge } from "@/components/ai-badge"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; import { ImportFromWalletDialog } from "@/components/patients/import-from-wallet-dialog"; import { PatientDetailSheet } from "@/components/patients/patient-detail-sheet"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ListPagination } from "@/components/ui/list-pagination"; import { listPatients, type Patient } from "@/lib/patients"; // Rows shown per page on the patients table before paginating. const PAGE_SIZE = 10; type BadgeVariant = "success" | "info" | "outline"; // Colour the status for at-a-glance scanning: active patients read as success // (green), admitted inpatients as info (blue, draws the eye), and discharged as // a muted outline. const statusVariant: Record = { active: "success", inpatient: "info", discharged: "outline", }; export function PatientsView() { const { t } = useTranslation(); const [query, setQuery] = useState(""); const [addOpen, setAddOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); // Bumped on open so the create dialog remounts with a fresh file # / form. const [addKey, setAddKey] = useState(0); // The patient whose record is shown in the side Sheet. const [selected, setSelected] = useState(null); const [sheetOpen, setSheetOpen] = useState(false); const [allPatients, setAllPatients] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); useEffect(() => { let active = true; setLoading(true); listPatients() .then((data) => { if (!active) return; setAllPatients(data); setLoadError(null); }) .catch((err) => { if (!active) return; setLoadError( err instanceof Error ? err.message : t("patients.loadError") ); }) .finally(() => { if (active) setLoading(false); }); return () => { active = false; }; }, []); const q = query.trim().toLowerCase(); const patients = allPatients.filter( (p) => !q || p.name.toLowerCase().includes(q) || p.fileNumber.includes(q) ); // Client-side pagination over the filtered list (10/page). Searching resets to // the first page (done in the search handler); `page` is clamped at render so a // shrinking list (filter/refresh) never leaves us past the last page. const [page, setPage] = useState(1); const totalPages = Math.max(1, Math.ceil(patients.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const pageRows = patients.slice( (safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE ); const open = (fileNumber: string) => { setSelected(fileNumber); setSheetOpen(true); }; // Deep link from a notification: /patients?file= opens the record. const searchParams = useSearchParams(); const deepLinkFile = searchParams.get("file"); const openedDeepLink = useRef(null); useEffect(() => { if (!deepLinkFile || openedDeepLink.current === deepLinkFile) return; openedDeepLink.current = deepLinkFile; open(deepLinkFile); }, [deepLinkFile]); const refresh = () => { void listPatients() .then(setAllPatients) .catch(() => { /* keep the current list on a refresh error */ }); }; return (

{t("patients.title")}

{ setQuery(event.target.value); setPage(1); }} onKeyDown={(event) => { // Enter opens the top match's record, like picking it from the table. if (event.key === "Enter" && patients.length > 0) { event.preventDefault(); open(patients[0].fileNumber); } }} placeholder={t("patients.searchPlaceholder")} value={query} />
{loading ? ( ) : loadError ? ( ) : patients.length === 0 ? ( ) : ( pageRows.map((p) => ( open(p.fileNumber)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); open(p.fileNumber); } }} role="button" tabIndex={0} > )) )}
{t("patients.columns.name")} {t("patients.columns.mrn")} {t("patients.columns.ageSex")} {t("patients.columns.status")} {t("patients.columns.lastSeen")} {t("patients.columns.allergies")}
{t("patients.loading")}
{loadError}
{t("patients.empty")}
{p.name} {p.shareExpiresAt ? ( {t("patients.tempBadge")} ) : null} {p.fileNumber} {p.age} · {p.sex} {t(`patients.status.${p.status}`)} {p.encounters[0]?.date ?? "—"} {p.allergies.length || "—"}
{!loading && !loadError ? ( ) : null} { refresh(); open(fileNumber); }} onOpenChange={setAddOpen} open={addOpen} /> { refresh(); open(fileNumber); }} onOpenChange={setImportOpen} open={importOpen} /> { setSheetOpen(o); // Reflect any edits made in the Sheet back into the table. if (!o) refresh(); }} open={sheetOpen} />
); }