"use client"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { AiBadge } from "@/components/ai-badge"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; import { RecordGraph } from "@/components/graph/record-graph"; import { PatientDetail } from "@/components/patients/patient-detail"; import { ScribeDialog } from "@/components/patients/scribe-dialog"; import { TransferPatientDialog } from "@/components/patients/transfer-patient-dialog"; import { WalletPushDialog } from "@/components/patients/wallet-push-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { Dialog, DialogDescription, DialogHeader, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import { Sheet, SheetHeader, SheetPanel, SheetPopup, SheetTitle, } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { type Appointment, listAppointments } from "@/lib/appointments"; import { type Invoice, listInvoices } from "@/lib/invoices"; import { deletePatient, getPatient, type Patient } from "@/lib/patients"; import { listPrescriptions, type Prescription } from "@/lib/prescriptions"; import { useAiAccess } from "@/lib/ai-policy"; import { hasClinicalAccess, useActiveRole } from "@/lib/roles"; import { notify } from "@/lib/toast"; import { getWalletLink } from "@/lib/wallet-updates"; type Status = "loading" | "ready" | "not-found"; function DetailSkeleton() { return (
{[0, 1, 2, 3].map((section) => (
{[0, 1, 2].map((row) => ( ))}
))}
); } // Right-side Sheet showing a patient's full record, laid out to fit the sheet // width (see PatientDetail). Opened from the Patients table instead of routing // into the AI chat. export function PatientDetailSheet({ fileNumber, open, onOpenChange, }: { fileNumber: string | null; open: boolean; onOpenChange: (open: boolean) => void; }) { const { t } = useTranslation(); const role = useActiveRole(); // Clinical roles can reassign a chart; show optimistically while role loads. const canTransfer = role == null || hasClinicalAccess(role); // Deleting a chart is destructive — only offer it once we know the role is // a full clinician (patient:delete), never optimistically. const canDelete = role != null && hasClinicalAccess(role); // The ambient scribe writes a clinical note, so it needs full clinical write // access AND the clinic's AI must be enabled for this member. const { allowed: aiAllowed } = useAiAccess(); const canScribe = role != null && hasClinicalAccess(role) && aiAllowed; const [patient, setPatient] = useState(null); const [status, setStatus] = useState("loading"); const [editOpen, setEditOpen] = useState(false); const [scribeOpen, setScribeOpen] = useState(false); const [walletPushOpen, setWalletPushOpen] = useState(false); // Set once we confirm this patient is linked to a wallet (permanent share). const [walletLinked, setWalletLinked] = useState(false); const [transferOpen, setTransferOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); // Graph popped out of the sheet into its own dialog (the sheet closes first). const [graphOpen, setGraphOpen] = useState(false); // Related records aggregated into the sheet for a 360° view. const [prescriptions, setPrescriptions] = useState([]); const [appointments, setAppointments] = useState([]); const [invoices, setInvoices] = useState([]); // Bumped on open so the editor remounts with the latest patient data. const [editKey, setEditKey] = useState(0); useEffect(() => { if (!open || !fileNumber) return; let active = true; setStatus("loading"); setPatient(null); setPrescriptions([]); setAppointments([]); setInvoices([]); getPatient(fileNumber) .then((data) => { if (!active) return; setPatient(data); setStatus(data ? "ready" : "not-found"); }) .catch(() => { if (active) setStatus("not-found"); }); // Pull related records in parallel; filter to this chart. Best-effort — a // missing permission (e.g. reception + prescriptions) just leaves it empty. const forFile = (fn: string) => fn === fileNumber; listPrescriptions() .then((rx) => active && setPrescriptions(rx.filter((r) => forFile(r.fileNumber)))) .catch(() => {}); listAppointments() .then((a) => active && setAppointments(a.filter((r) => forFile(r.fileNumber)))) .catch(() => {}); listInvoices() .then((i) => active && setInvoices(i.filter((r) => forFile(r.fileNumber)))) .catch(() => {}); return () => { active = false; }; }, [open, fileNumber]); // Whether this patient is wallet-linked (drives the "Push update" button). // Separate from the main load so it re-checks once the role resolves without // refetching the record. Only clinicians can push. useEffect(() => { setWalletLinked(false); if (!open || !fileNumber || !hasClinicalAccess(role)) return; let active = true; getWalletLink(fileNumber) .then(() => active && setWalletLinked(true)) .catch(() => {}); return () => { active = false; }; }, [open, fileNumber, role]); const remove = async () => { if (!patient) return; try { await deletePatient(patient.fileNumber); notify.success(t("patients.delete.doneTitle"), patient.name); onOpenChange(false); } catch { notify.error( t("patients.delete.failedTitle"), t("patients.delete.failedBody"), ); } }; const title = status === "ready" && patient ? patient.name : status === "not-found" ? t("patients.detail.notFound") : t("patients.detail.loading"); return ( <> {title} {status === "ready" && } {status === "loading" && } {status === "not-found" && (

{t("patients.detail.noPatientForFile", { number: fileNumber })}

)} {status === "ready" && patient && ( setConfirmOpen(true) : undefined} onEdit={() => { setEditKey((k) => k + 1); setEditOpen(true); }} onScribe={canScribe ? () => setScribeOpen(true) : undefined} onWalletPush={ walletLinked ? () => setWalletPushOpen(true) : undefined } onOpenGraph={() => { onOpenChange(false); setGraphOpen(true); }} onTransfer={ canTransfer ? () => setTransferOpen(true) : undefined } patient={patient} prescriptions={prescriptions} /> )}
{patient && ( setPatient(updated)} open={editOpen} patient={patient} /> )} {patient && ( setPatient(updated)} open={scribeOpen} patient={patient} /> )} {patient && ( )} {patient && ( setPatient(updated)} open={transferOpen} patient={patient} /> )} {patient && ( )} {patient && ( {t("patientCard.graph.title")} · {patient.name} {t("patientCard.graph.hint")} )} ); }