"use client"; import { ArrowRight, Pencil } from "lucide-react"; import { type ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogDescription, DialogHeader, DialogPanel, DialogPopup, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; import { Sparkline } from "@/components/chat/sparkline"; import { cn } from "@/lib/utils"; import type { AllergySeverity, LabFlag, Patient, Trend } from "@/lib/patients"; type BadgeVariant = | "default" | "secondary" | "destructive" | "outline" | "success" | "info" | "warning"; type PatientResultProps = { status: "loading" | "ready" | "not-found"; fileNumber: string; patient?: Patient; onPatientUpdated?: (patient: Patient) => void; // "row" = horizontal scroll (chat); "column" = full-width vertical stack // (the Patients detail Sheet). layout?: "row" | "column"; }; 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", }; // Fixed width so the cards sit in a horizontal scroll row instead of squashing, // plus a subtle clickable affordance (they open a detail dialog). Compact cards // size to their own (short) content — see `items-start` in PatientResult. const rowCard = "w-72 shrink-0 cursor-pointer gap-0 text-start outline-none transition hover:bg-accent/30 hover:ring-foreground/20 focus-visible:ring-2 focus-visible:ring-ring"; // Same footprint as `rowCard` but with no clickable affordance — used when a // card has nothing extra to reveal, so it shouldn't promise "Click for more". const rowCardStatic = "w-72 shrink-0 gap-0 text-start"; // COSS Card has no `size` variant; recreate the old compact ("sm") density by // tightening the inner section padding from p-6 → p-4 via data-slot selectors. const compactCard = "[&_[data-slot=card-header]]:p-4 [&_[data-slot=card-panel]]:px-4 [&_[data-slot=card-panel]]:pb-4"; function SectionLabel({ children }: { children: ReactNode }) { return (

{children}

); } function Stat({ label, value }: { label: string; value: ReactNode }) { return (
{label} {value}
); } function Row({ label, value }: { label: ReactNode; value: ReactNode }) { return (
{label} {value}
); } function Empty({ children }: { children: ReactNode }) { return

{children}

; } function TrendDetail({ trend }: { trend: Trend }) { const { t } = useTranslation(); if (trend.points.length === 0) { return {t("patientCard.trend.empty")}; } const min = Math.min(...trend.points); const max = Math.max(...trend.points); return (
{t("patientCard.trend.lastReadings", { label: trend.label, count: trend.points.length, })}
{t("patientCard.trend.latest")}{" "} {trend.points.at(-1)} {trend.unit} {t("patientCard.trend.min")}{" "} {min} {t("patientCard.trend.max")}{" "} {max}
); } function AlertBadges({ alerts }: { alerts: string[] }) { if (alerts.length === 0) { return null; } return (
{alerts.map((alert) => ( {alert} ))}
); } // A compact card that previews `children` and opens a roomier dialog of `detail` // on click. A muted "Click for more" footer signals the card is expandable. // When `expandable` is false (the card holds nothing beyond its preview), it // renders as a plain, non-clickable card with no footer — so empty sections // don't misleadingly promise more. function ExpandableCard({ title, description, detail, children, expandable = true, }: { title: ReactNode; description?: ReactNode; detail: ReactNode; children: ReactNode; expandable?: boolean; }) { const { t } = useTranslation(); if (!expandable) { return {children}; } return ( } > {children}
{t("patientCard.clickForMore")}
{title} {description ? {description} : null} {detail}
); } function SummaryCard({ patient, onEdit, }: { patient: Patient; onEdit?: () => void; }) { const { t } = useTranslation(); const sex = t(`patientCard.sex.${patient.sex}`); const statusLabel = t(`patients.status.${patient.status}`); const idLine = `${patient.age} · ${sex} · MRN ${patient.fileNumber}`; return (
{onEdit ? ( ) : null} } title={patient.name} >
{patient.initials}
{patient.name} {idLine}
{statusLabel}
); } function VitalsCard({ patient }: { patient: Patient }) { const { t } = useTranslation(); const { vitals } = patient; const vitalItems = [ { label: t("patientCard.vitals.bp"), value: vitals.bp }, { label: t("patientCard.vitals.hr"), value: vitals.hr }, { label: t("patientCard.vitals.temp"), value: vitals.temp }, { label: t("patientCard.vitals.spo2"), value: vitals.spo2 }, ]; const vitalsGrid = (gapY: string) => (
{vitalItems.map((item) => ( ))}
); const hasVitals = Boolean( vitals.bp || vitals.hr || vitals.temp || vitals.spo2 || patient.vitalsTrend.points.length, ); return ( {vitalsGrid("gap-y-3")} } title={t("patientCard.vitals.title")} > {t("patientCard.vitals.title")} {t("patientCard.vitals.taken", { at: vitals.takenAt })} ); } function LabValue({ value, flag }: { value: string; flag: LabFlag }) { const { t } = useTranslation(); return ( {value} {t(`patientCard.labFlag.${flag}`)} ); } function LabsCard({ patient }: { patient: Patient }) { const { t } = useTranslation(); return ( 0} detail={ patient.labs.length === 0 ? ( {t("patientCard.labs.empty")} ) : (
{patient.labs.map((lab) => (
{lab.name} {lab.takenAt}
))}
) } title={t("patientCard.labs.title")} > {t("patientCard.labs.title")} {t("patientCard.labs.asOf", { at: patient.labs[0]?.takenAt ?? "—" })}
); } function MedicationsCard({ patient }: { patient: Patient }) { const { t } = useTranslation(); const list = patient.medications.length === 0 ? ( {t("patientCard.medications.empty")} ) : (
{patient.medications.map((med) => ( ))}
); return ( 0} detail={list} title={t("patientCard.medications.title")} > {t("patientCard.medications.title")} {t("patientCard.medications.active", { count: patient.medications.length, })} ); } function ProblemsCard({ patient }: { patient: Patient }) { const { t } = useTranslation(); const list = patient.problems.length === 0 ? ( {t("patientCard.problems.empty")} ) : (
{patient.problems.map((problem) => ( ))}
); return ( 0} detail={list} title={t("patientCard.problems.title")} > {t("patientCard.problems.title")} {t("patientCard.problems.active", { count: patient.problems.length })} ); } function AllergiesList({ patient }: { patient: Patient }) { const { t } = useTranslation(); return (
{t("patientCard.allergies.sectionLabel")} {patient.allergies.length === 0 ? (

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

) : ( patient.allergies.map((allergy) => ( {allergy.substance} {" "} — {allergy.reaction} } value={ {t(`patientCard.severity.${allergy.severity}`)} } /> )) )}
); } function AllergiesCard({ patient }: { patient: Patient }) { const { t } = useTranslation(); return ( } expandable={patient.allergies.length > 0 || patient.alerts.length > 0} title={t("patientCard.allergies.title")} > {t("patientCard.allergies.title")} {patient.allergies.length === 0 ? t("patientCard.allergies.none") : t("patientCard.allergies.count", { count: patient.allergies.length, })} {patient.alerts.length > 0 ? ( ) : null} ); } function VisitsList({ patient }: { patient: Patient }) { const { t } = useTranslation(); if (patient.encounters.length === 0) { return {t("patientCard.visits.empty")}; } return (
{patient.encounters.map((encounter) => (
{encounter.type} {encounter.date}
{encounter.summary} {encounter.provider}
))}
); } function VisitsCard({ patient }: { patient: Patient }) { const { t } = useTranslation(); return ( 0} detail={} title={t("patientCard.visits.title")} > {t("patientCard.visits.title")} {t("patientCard.visits.recent", { count: patient.encounters.length })} ); } function LoadingCards() { return ( <>
{[0, 1, 2, 3].map((cell) => ( ))}
{[0, 1, 2, 3, 4, 5].map((card) => ( {[0, 1, 2].map((row) => ( ))} {card % 2 === 1 && } ))} ); } export function PatientResult({ status, fileNumber, patient, onPatientUpdated, layout = "row", }: PatientResultProps) { const { t } = useTranslation(); const [editOpen, setEditOpen] = useState(false); // Bumped on open so the editor remounts with the latest patient data. const [editKey, setEditKey] = useState(0); if (status === "not-found") { return (

{t("patientCard.notFound", { number: fileNumber })}

); } return (
{status === "loading" || !patient ? ( ) : ( <> { setEditKey((k) => k + 1); setEditOpen(true); }} patient={patient} /> onPatientUpdated?.(updated)} open={editOpen} patient={patient} /> )}
); }