"use client"; import { Check, FlaskConical, Plus, Search } from "lucide-react"; import { type FormEvent, type ReactNode, useEffect, useMemo, useState, } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { type LabFlag, type Patient, appendLabs, listPatients, } from "@/lib/patients"; import { type Priority, type Task, listTasks, updateTask } from "@/lib/tasks"; import { notify } from "@/lib/toast"; import { cn } from "@/lib/utils"; const priorityVariant: Record = { high: "destructive", medium: "secondary", low: "outline", }; const LAB_FLAGS: LabFlag[] = ["normal", "low", "high", "critical"]; // Patient dates are stored as formatted strings (e.g. "Jun 02, 2026") — match // the format used by the patient form. const today = () => new Date().toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric", }); function Field({ label, children }: { label: string; children: ReactNode }) { return ( ); } const controlClass = "h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm text-foreground outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30"; function CheckButton({ done, onClick, label, }: { done: boolean; onClick: () => void; label: string; }) { return ( ); } // Dialog for submitting an analysis result: pick a patient, then enter the // test name, value, flag and date. Posts to the lab-only append endpoint. function AddResultDialog({ open, onOpenChange, patients, onAdded, }: { open: boolean; onOpenChange: (open: boolean) => void; patients: Patient[]; onAdded: () => void; }) { const { t } = useTranslation(); const [patient, setPatient] = useState(null); const [patientQuery, setPatientQuery] = useState(""); const [name, setName] = useState(""); const [value, setValue] = useState(""); const [flag, setFlag] = useState("normal"); const [takenAt, setTakenAt] = useState(today()); const [saving, setSaving] = useState(false); const reset = () => { setPatient(null); setPatientQuery(""); setName(""); setValue(""); setFlag("normal"); setTakenAt(today()); setSaving(false); }; const search = patientQuery.trim().toLowerCase(); const matches = useMemo(() => { const base = search ? patients.filter( (p) => p.name.toLowerCase().includes(search) || p.fileNumber.includes(search), ) : patients; return base.slice(0, 6); }, [patients, search]); const submit = async (event: FormEvent) => { event.preventDefault(); if (!patient) { notify.error( t("lab.addResult.needPatientTitle"), t("lab.addResult.needPatientBody"), ); return; } if (!name.trim() || !value.trim()) { notify.error( t("lab.addResult.needFieldsTitle"), t("lab.addResult.needFieldsBody"), ); return; } setSaving(true); try { await appendLabs(patient.fileNumber, [ { name: name.trim(), value: value.trim(), flag, takenAt: takenAt.trim() || today(), }, ]); notify.success( t("lab.addResult.addedTitle"), t("lab.addResult.addedBody", { test: name.trim(), name: patient.name }), ); reset(); onOpenChange(false); onAdded(); } catch { setSaving(false); notify.error( t("lab.addResult.failedTitle"), t("lab.addResult.failedBody"), ); } }; return ( { onOpenChange(o); if (!o) reset(); }} open={open} > {t("lab.addResult.title")} {t("lab.addResult.description")}
{patient ? (
{patient.initials}
{patient.name} #{patient.fileNumber}
) : (
{t("lab.addResult.patient")}
setPatientQuery(event.target.value)} placeholder={t("lab.addResult.patientPlaceholder")} value={patientQuery} />
{matches.map((p) => ( ))} {matches.length === 0 && (

{t("lab.addResult.noPatients")}

)}
)}
setName(event.target.value)} placeholder={t("lab.addResult.testPlaceholder")} value={name} /> setValue(event.target.value)} placeholder={t("lab.addResult.valuePlaceholder")} value={value} />
setTakenAt(event.target.value)} value={takenAt} />
}> {t("lab.addResult.cancel")}
); } // The lab department home: the lab's task queue plus the "add result" flow // for submitting a patient's analyses to their record. export function LabView() { const { t } = useTranslation(); const [tasks, setTasks] = useState([]); const [patients, setPatients] = useState([]); const [addOpen, setAddOpen] = useState(false); useEffect(() => { let active = true; listTasks() .then((data) => { if (active) setTasks(data); }) .catch(() => { /* api-client redirects on 401; otherwise leave the list empty */ }); listPatients() .then((data) => { if (active) setPatients(data); }) .catch(() => { /* same */ }); return () => { active = false; }; }, []); // The lab work queue. The backend already scopes visibility; the client // filter keeps the page focused for admins (who see every task). const queue = useMemo( () => tasks.filter((task) => task.assigneeRole === "lab"), [tasks], ); // Optimistically flip done, then persist; roll back on failure. const toggle = async (id: string) => { const current = tasks.find((task) => task.id === id); if (!current) return; const next = !current.done; setTasks((prev) => prev.map((row) => (row.id === id ? { ...row, done: next } : row)), ); try { await updateTask(id, { done: next }); } catch { setTasks((prev) => prev.map((row) => (row.id === id ? { ...row, done: current.done } : row)), ); notify.error( t("lab.toast.updateFailedTitle"), t("lab.toast.updateFailedBody"), ); } }; return (

{t("lab.title")}

{t("lab.subtitle")}

{t("lab.queue.title")}

{t("lab.queue.description")}

{queue.length === 0 ? (

{t("lab.queue.empty")}

) : ( queue.map((task) => (
toggle(task.id)} />
{task.title} {task.due} {task.createdByName ? ` · ${t("tasks.list.byCreator", { name: task.createdByName })}` : ""}
{t(`tasks.priority.${task.priority}`)}
)) )}
{ /* the patient record is reloaded on next lookup; nothing to refresh here */ }} onOpenChange={setAddOpen} open={addOpen} patients={patients} />
); }