"use client"; import { ArrowLeftRight, CalendarDays, FileDown, type LucideIcon, ListTodo, Mic, Network, NotebookPen, Pencil, Pill, Send, Trash2, UserRound, } from "lucide-react"; import { type ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Sparkline } from "@/components/chat/sparkline"; import { AttachmentsSection } from "@/components/patients/patient-files"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { type ActivityEntityType, type ActivityEntry, listPatientActivity, } from "@/lib/activity"; import { printPatientSummary } from "@/lib/patient-pdf"; 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 type { Appointment } from "@/lib/appointments"; import { formatMoney, type Invoice, invoiceTotal, } from "@/lib/invoices"; import type { AllergySeverity, LabFlag, Patient, Trend } from "@/lib/patients"; import type { Prescription } from "@/lib/prescriptions"; import { listProviders, type Provider, specialtyLabel } from "@/lib/staff"; import { cn } from "@/lib/utils"; // A record "file" surfaced both in the graph and the sheet's clickable list. type RecordFile = { id: string; kind: "problem" | "visit"; title: string; sub: string; rows: { label: string; value: string }[]; }; type BadgeVariant = | "default" | "secondary" | "destructive" | "outline" | "success" | "info" | "warning"; const severityVariant: Record = { mild: "outline", moderate: "secondary", severe: "destructive", }; const labFlagVariant: Record = { normal: "outline", low: "secondary", high: "secondary", critical: "destructive", }; const statusVariant: Record = { active: "success", inpatient: "info", discharged: "outline", }; function Section({ title, children }: { title: string; children: ReactNode }) { return (

{title}

{children}
); } function Stat({ label, value }: { label: string; value: ReactNode }) { return (
{label} {value}
); } function Row({ label, value }: { label: ReactNode; value: ReactNode }) { return (
{label} {value}
); } function TrendBlock({ trend }: { trend: Trend }) { if (trend.points.length === 0) return null; return (
{trend.label} {trend.points.at(-1)} {trend.unit}
); } // Which icon marks each kind of audited change on the timeline. const historyIcon: Record = { appointment: CalendarDays, note: NotebookPen, patient: UserRound, prescription: Pill, task: ListTodo, }; // The patient's record history: every audited add/change on this chart, newest // first. Reuses the clinic activity log scoped to this file number, laid out as // a vertical timeline — who made the change, what happened, and when. function RecordHistory({ fileNumber }: { fileNumber: string }) { const { t } = useTranslation(); const [entries, setEntries] = useState(null); const [error, setError] = useState(false); useEffect(() => { let active = true; listPatientActivity(fileNumber) .then((e) => active && setEntries(e)) .catch(() => active && setError(true)); return () => { active = false; }; }, [fileNumber]); return (
{error ? (

{t("patientCard.history.loadError")}

) : entries === null ? (

{t("patients.loading")}

) : entries.length === 0 ? (

{t("patientCard.history.empty")}

) : ( // A plain vertical rail so EVERY audited change renders in full — the // icon column draws a connector that stretches to the next entry, and // the flex layout mirrors correctly under RTL.
    {entries.map((e, i) => { const Icon = historyIcon[e.entityType] ?? Pencil; const isLast = i === entries.length - 1; return (
  1. {!isLast && (

    {e.actorName}

    {e.action}

  2. ); })}
)}
); } // Full patient record laid out vertically for the side Sheet — plain full-width // sections (no fixed-width cards, no nested click-to-expand dialogs). export function PatientDetail({ patient, onEdit, onScribe, onWalletPush, onTransfer, onDelete, onOpenGraph, prescriptions, appointments, invoices, }: { patient: Patient; onEdit?: () => void; // Opens the ambient AI visit scribe (record/transcribe → draft note). onScribe?: () => void; // Pushes the record to the patient's wallet (only when wallet-linked). onWalletPush?: () => void; onTransfer?: () => void; onDelete?: () => void; // Pops the record graph out into its own dialog (closing this sheet). onOpenGraph?: () => void; prescriptions?: Prescription[]; appointments?: Appointment[]; invoices?: Invoice[]; }) { const { t } = useTranslation(); const sex = t(`patientCard.sex.${patient.sex}`); const idLine = `${patient.age} · ${sex} · MRN ${patient.fileNumber}`; // The record "file" opened in a detail dialog from the records list. const [openFile, setOpenFile] = useState(null); // Resolve the responsible clinician's specialty (set by an admin in Care // Team) to show alongside the primary-care provider. const [providers, setProviders] = useState([]); useEffect(() => { if (!patient.primaryProviderId) return; let active = true; listProviders() .then((p) => active && setProviders(p)) .catch(() => {}); return () => { active = false; }; }, [patient.primaryProviderId]); const providerSpecialty = specialtyLabel( t, providers.find((p) => p.userId === patient.primaryProviderId)?.specialty, ); // The same problems + visits the graph plots, as a clickable list. const files: RecordFile[] = [ ...patient.problems.map((p, i) => ({ id: `prob-${i}`, kind: "problem" as const, title: p.label, sub: p.since, rows: [{ label: t("patientCard.graph.fields.since"), value: p.since }], })), ...patient.encounters.map((e, i) => ({ id: `enc-${i}`, kind: "visit" as const, title: e.type, sub: e.date, rows: [ { label: t("patientCard.graph.fields.date"), value: e.date }, { label: t("patientCard.graph.fields.provider"), value: e.provider }, { label: t("patientCard.graph.fields.summary"), value: e.summary }, ].filter((r) => r.value), })), ]; return (
{/* Identity — full width so the name never gets squeezed by the actions. */}
{patient.initials}
{patient.name} {t(`patients.status.${patient.status}`)}
{idLine} {patient.alerts.length > 0 && (
{patient.alerts.map((alert) => ( {alert} ))}
)}
{/* Actions — their own wrapping row beneath the identity. */}
{onTransfer && ( )} {onScribe && ( )} {onEdit && ( )} {onWalletPush && ( )} {onDelete && ( )}

{t("patientCard.graph.hint")}

{onOpenGraph && ( )}
{files.length === 0 ? (

{t("patientCard.graph.empty")}

) : (
{files.map((file) => ( ))}
)}

{t("patientCard.vitals.taken", { at: patient.vitals.takenAt })}

{patient.labs.length === 0 ? (

{t("patientCard.labs.empty")}

) : (
{patient.labs.map((lab) => ( {lab.value} {t(`patientCard.labFlag.${lab.flag}`)} } /> ))}
)}
{patient.medications.length === 0 ? (

{t("patientCard.medications.empty")}

) : (
{patient.medications.map((med) => ( ))}
)}
{patient.problems.length === 0 ? (

{t("patientCard.problems.empty")}

) : (
{patient.problems.map((problem) => ( ))}
)}
{patient.allergies.length === 0 ? (

{t("patientCard.allergies.none")}

) : (
{patient.allergies.map((allergy) => ( {allergy.substance} {" "} — {allergy.reaction} } value={ {t(`patientCard.severity.${allergy.severity}`)} } /> ))}
)}
{patient.encounters.length === 0 ? (

{t("patientCard.visits.empty")}

) : (
{patient.encounters.map((encounter) => (
{encounter.type} {encounter.date}
{encounter.summary} {encounter.provider}
))}
)}
{appointments && (
{appointments.length === 0 ? (

{t("patientCard.appointments.empty")}

) : (
{appointments.map((appt) => ( {appt.type} {t(`appointments.status.${appt.status}`)} } /> ))}
)}
)} {prescriptions && (
{prescriptions.length === 0 ? (

{t("patientCard.prescriptions.empty")}

) : (
{prescriptions.map((rx) => ( {`${rx.dose} · ${rx.frequency}`} {t(`prescriptions.status.${rx.status}`)} } /> ))}
)}
)} {invoices && (
{invoices.length === 0 ? (

{t("patientCard.invoices.empty")}

) : (
{invoices.map((inv) => ( {formatMoney(invoiceTotal(inv))} {t(`invoices.status.${inv.status}`)} } /> ))}
)}
)} { if (!o) setOpenFile(null); }} open={openFile !== null} > {openFile?.title} {openFile ? t(`patientCard.graph.kind.${openFile.kind}`) : ""} {openFile?.rows.map((row) => (
{row.label} {row.value}
))}
}> {t("patientCard.graph.close")}
); }