import { useEffect, useRef, useState } from "react";
import { clinicianApi } from "../../api/client";
import type { ContextRecord, PatientDetail } from "../../api/types";
import { contentTypeOf, fileProblem, guessType, RECORD_LIMITS, roomFor, titleFromFile } from "../../logic/records";
import { Count, EmptyState, SectionHeader } from "../kit";
import { useLocation } from "../router";
import { errorText, ErrorNote, Spinner, useLoad, useToast } from "../ui";
import { AddRecords, type Pending } from "./AddRecords";
import { EditRecord, PasteNote, RemoveRecord } from "./RecordDialogs";
import { RecordsTable } from "./RecordsTable";
import { SourceDrawer, type SourceRef } from "./SourceDrawer";
/** REC-1 to REC-4: the patient's records, and adding more from anywhere. */
export function RecordsPanel({ patient, onChanged }: { patient: PatientDetail; onChanged?: () => void }) {
const records = useLoad(() => clinicianApi.records(patient.id), [patient.id]);
const [pending, setPending] = useState<Pending[]>([]);
const [notice, setNotice] = useState("");
const [busy, setBusy] = useState(false);
const [paste, setPaste] = useState(false);
const [editing, setEditing] = useState<ContextRecord | null>(null);
const [removing, setRemoving] = useState<ContextRecord | null>(null);
const [source, setSource] = useState<SourceRef | null>(null);
const toast = useToast();
const chooseButton = useRef<HTMLButtonElement>(null);
const list = records.data?.records ?? [];
// "Add records" in the header lands here with ?add=1: bring the add panel into view and focus it.
const wantsAdd = useLocation().query.get("add") === "1";
useEffect(() => {
if (!wantsAdd) return;
chooseButton.current?.closest("[data-add-records]")?.scrollIntoView({ block: "start" });
chooseButton.current?.focus({ preventScroll: true });
}, [wantsAdd]);
// REC-7 and failed reads: confirm a held-back record, remove it, or read a failed file again.
const act = async (work: () => Promise<unknown>, done: string) => {
try {
await work();
records.reload(true);
onChanged?.();
toast.show(done);
} catch (e) {
toast.show(errorText(e), true);
}
};
const choose = (files: FileList | File[], origin: "upload" | "camera") => {
const all = [...files];
const { fits, message } = roomFor(list.length + pending.length, all.length);
setNotice(message ?? "");
setPending((p) => [
...p,
...all.slice(0, fits).map((file, i) => ({
key: `${Date.now()}-${i}-${file.name}`,
file,
text: null,
title: origin === "camera" ? "Photo of a record" : titleFromFile(file.name),
type: guessType(file.name),
place: "",
recordDate: "",
origin,
error: fileProblem(file),
})),
]);
};
const update = (key: string, change: Partial<Pending>) => setPending((p) => p.map((x) => (x.key === key ? { ...x, ...change } : x)));
const addAll = async () => {
setBusy(true);
const failed: Pending[] = [];
for (const item of pending) {
if (item.error) {
failed.push(item);
continue;
}
try {
let uploadId: string | null = null;
if (item.file) {
const ticket = await clinicianApi.requestUpload(patient.id, { fileName: item.file.name, contentType: contentTypeOf(item.file), sizeBytes: item.file.size });
await clinicianApi.uploadFile(ticket, item.file);
uploadId = ticket.uploadId;
}
await clinicianApi.addRecord(patient.id, {
uploadId,
text: item.text,
title: item.title.trim() || "Untitled record",
type: item.type,
place: item.place.trim() || null,
recordDate: item.recordDate || null,
origin: item.origin,
});
} catch (e) {
failed.push({ ...item, error: errorText(e) });
}
}
setPending(failed);
setBusy(false);
records.reload(true);
onChanged?.();
if (!failed.length) toast.show("Records added. Read them again on the Check tab.");
};
return (
<div className="flex flex-col gap-8">
{toast.node}
<section className="flex flex-col gap-3" aria-labelledby={`records-title-${patient.id}`}>
<SectionHeader
title={
<>
<span id={`records-title-${patient.id}`}>Records</span>
<Count>
{list.length} of {RECORD_LIMITS.perCheck}
</Count>
</>
}
/>
{records.loading && !records.data && <Spinner />}
<ErrorNote error={records.error} onRetry={() => records.reload()} />
{list.length > 0 && (
<RecordsTable
records={list}
patientFirstName={patient.firstName}
actions={{
open: (r) => setSource({ recordId: r.id, title: r.title, lines: [] }),
edit: setEditing,
remove: setRemoving,
confirm: (r) => void act(() => clinicianApi.confirmRecordPatient(r.id), "Confirmed. Read the records again on the Check tab."),
readAgain: (r) => void act(() => clinicianApi.readRecordAgain(r.id), "Reading it again."),
}}
/>
)}
{records.data && !list.length && <EmptyState title="No records yet." body="Add the patient's prescriptions, results and notes below." />}
</section>
<AddRecords
pending={pending}
notice={notice}
busy={busy}
chooseRef={chooseButton}
onChoose={choose}
onPaste={() => setPaste(true)}
onUpdate={update}
onRemove={(key) => setPending((all) => all.filter((x) => x.key !== key))}
onClear={() => setPending([])}
onAddAll={() => void addAll()}
/>
<PasteNote
open={paste}
onClose={() => setPaste(false)}
onAdd={(text, title) => {
setPaste(false);
setPending((p) => [...p, { key: `paste-${Date.now()}`, file: null, text, title, type: "clinic_note", place: "", recordDate: "", origin: "pasted", error: text.length > 12000 ? "This note is longer than 12,000 characters. Split it into smaller records." : null }]);
}}
/>
<EditRecord record={editing} onClose={() => setEditing(null)} onSaved={() => { setEditing(null); records.reload(true); }} />
<RemoveRecord
record={removing}
onClose={() => setRemoving(null)}
onRemove={(r) => {
setRemoving(null);
void act(() => clinicianApi.removeRecord(r.id), "Record removed. Its facts left review and the next check.");
}}
/>
<SourceDrawer source={source} onClose={() => setSource(null)} />
</div>
);
}
export function RecordsTab({ patient }: { patient: PatientDetail }) {
return <RecordsPanel patient={patient} />;
}