"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 && (
)}
>
);
}