import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import type { Attachment } from "fhir/r4"; import { AlertCircle } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../../components/ui/dialog.tsx"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Button } from "../../../components/ui/button.tsx"; import { Input } from "../../../components/ui/input.tsx"; import { Label } from "../../../components/ui/label.tsx"; import { useDiagnosticCatalogue } from "../../../hooks/useDiagnosticCatalogue.ts"; import { useRecordDiagnosticResult } from "../../../hooks/useRecordDiagnosticResult.ts"; import { blockedByPending, entryOperationFor, entryFailureIsSettled, entryIsCompleted, entryReadIdentityMatches, readEntryRecovery, resultEntryRecovery, type PendingAttempt, type EntryRecovery } from "../../../utils/resultEntryOperation.ts"; import type { EnteredResult } from "../../../hooks/useDiagnosticResult.ts"; import { useHttpService } from "../../../services/HttpService.tsx"; import { environment } from "../../../environments/environment.ts"; import { NO_LONGER_PERMITTED, useAdmission } from "../../../hooks/useAdmission.ts"; import { ActWithdrawn } from "./ActWithdrawn.tsx"; import type { RequestAdmission } from "../../../services/HttpService.tsx"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import LabReportField from "./RecordDiagnosticResult/LabReportField.tsx"; import "./EnterTestResultDialog.css"; import { measurementsFromCatalogue } from "./RecordDiagnosticResult/measurementsFromCatalogue.ts"; import { useStaffPermissionServiceContext } from "../../../services"; import { writeThroughViewer, viewerEpoch } from "../../../utils/writeThroughHold"; import { SaveRetiredError, isSaveRetired, isSaveTimedOut, useSaveScope, type SaveOperation } from "../../../utils/saveOperation"; // Saved as preliminary for this test alone; release is a separate act. export interface TestToResult { orderId: string; test?: string; testCode?: string; testCodeSystem?: string; patient?: string; detail?: string; inPanel?: boolean; } interface Props { order?: TestToResult; onClose: () => void; onSaved: (state?: string) => void; withdrawn?: boolean; } interface ManualMeasurement { name?: string; value: string; unit: string; } const SAVE_FAILED = "Could not save. Your entries are retained; the report and test status have not changed."; const UNCONFIRMED_RESULT = "The result could not be confirmed. Check the saved result before trying again."; const RESULT_NOT_SENT = "The save timed out before the result was sent. Your entries are retained. Try again."; const CHECK_FAILED = "The saved result could not be confirmed. Save remains unavailable. Check the saved result again."; // Retain only an unresolved order key, never a draft or a retired opening's callbacks. // A positive read permits review; it does not permit another preliminary POST. const unconfirmedOrders = { has: (key: string) => Boolean(resultEntryRecovery.read(key)), get: resultEntryRecovery.read }; type ReadResult = ReturnType["get"]; const SavedResultCheck = ({ checking, onCheck, label = "Check saved result" }: { checking: boolean; onCheck: (read: ReadResult) => Promise; label?: string }) => { const { get } = useHttpService(); return ; }; const EnterTestResultDialog = ({ order, onClose, onSaved, withdrawn }: Props) => { const { canManage, permissionsSettled } = useStaffPermissionServiceContext(); const mayEnterNow = () => permissionsSettled && canManage("diagnostics"); const admission = useRef(mayEnterNow); admission.current = mayEnterNow; const mayEnter = mayEnterNow(); const permitted = useAdmission(mayEnter && !withdrawn); const activeSave = useRef<{ operation: SaveOperation; sent: boolean }>(); const { orderables, loading, error, reload } = useDiagnosticCatalogue(); const recordResult = useRecordDiagnosticResult(); const entry = useRef<{ operationId: string; key: string; requestKey?: string; supersedes?: string }>(); const defined = useMemo( () => measurementsFromCatalogue(order?.test, "laboratory", orderables, order?.testCode, order?.testCodeSystem), [order?.test, order?.testCode, orderables, order?.testCodeSystem], ); const [values, setValues] = useState>({}); // Without a catalogue definition each measurement is named, valued and given a unit as reported. const [manual, setManual] = useState([{ value: "", unit: "" }]); const [report, setReport] = useState(); const [sourceAssertion, setSourceAssertion] = useState<'measured-here' | 'from-another-record'>(); const [busy, setBusy] = useState(false); const [attaching, setAttaching] = useState(false); const [failure, setFailure] = useState(); const recoveryKey = JSON.stringify([writeThroughViewer() ?? viewerEpoch(), order?.orderId]); const uncertain = useSyncExternalStore(resultEntryRecovery.subscribe, () => unconfirmedOrders.has(recoveryKey)); const [takeover, setTakeover] = useState<{ recovery: EntryRecovery; held: PendingAttempt | undefined }>(); const reviewedRecovery = useRef(takeover); reviewedRecovery.current = takeover; const [checking, setChecking] = useState(false); const [checkedResult, setCheckedResult] = useState(); const checkedOwner = useRef(); // Fixed once the catalogue has answered, so a later read never swaps the rows being entered. const [settled, setSettled] = useState<{ fields: NonNullable }>(); const saves = useSaveScope(mayEnter && order?.orderId ? JSON.stringify([order.orderId, order.patient]) : undefined, () => { setBusy(false); setAttaching(false); setFailure(undefined); setChecking(false); setCheckedResult(undefined); checkedOwner.current = undefined; entry.current = undefined; setTakeover(undefined); setReport(undefined); setSourceAssertion(undefined); setValues({}); setManual([{ value: "", unit: "" }]); setSettled(undefined); }); useEffect(() => { if (!settled && !loading && !error) setSettled({ fields: defined ?? [] }); }, [settled, loading, error, defined]); useEffect(() => { if (withdrawn && activeSave.current && !activeSave.current.sent && activeSave.current.operation.belongs()) { activeSave.current.operation.retire(); setBusy(false); setFailure(NO_LONGER_PERMITTED); } }, [withdrawn]); if (!order) return null; if (!mayEnter) { return ( { if (!open) onClose(); }}> Enter this test's result {order.test ?? "Test not named"} {permissionsSettled ? "Your role at this practice cannot change Lab and imaging work. If you need it, ask a practice administrator." : "Confirming your access…"} ); } const fields = settled?.fields ?? []; const catalogued = fields.length > 0; const manualEntry = Boolean(settled) && !catalogued; const nameOf = (row: ManualMeasurement, at: number) => (row.name ?? (at === 0 ? order.test ?? "" : "")).trim(); const entries = catalogued ? fields.map((field, at) => ({ ...(field.code ? { code: field.code } : {}), name: field.name, value: (values[at] ?? "").trim(), unit: field.unit })) : !manualEntry ? [] : manual.map((row, at) => ({ name: nameOf(row, at), value: row.value.trim(), unit: row.unit.trim() || undefined })); const complete = entries.filter((entry) => entry.name && entry.value); // A manual row somebody typed into is finished or removed, never dropped on Save. Untouched rows, // including the first row's default name alone, are ignored. A unit is optional. const typedInto = (row: ManualMeasurement, at: number) => Boolean(row.value.trim() || row.unit.trim() || (row.name?.trim() && !(at === 0 && row.name.trim() === (order.test ?? "").trim()))); const unfinished = !manualEntry ? [] : manual.map((row, at) => { if (!typedInto(row, at)) return undefined; if (!entries[at].name) return "Name this measurement, or remove it."; return entries[at].value ? undefined : "Enter this measurement's value, or remove it."; }); const held = unfinished.some(Boolean); // A panel left part-entered says so; the blank components are unreported, not normal. const missing = catalogued && complete.length ? entries.filter((entry) => !entry.value).map((entry) => entry.name) : []; const setRow = (at: number, patch: Partial) => setManual((rows) => rows.map((row, index) => (index === at ? { ...row, ...patch } : row))); const filed = report?.url ? { key: report.url, contentType: report.contentType ?? "application/pdf", ...(report.title ? { title: report.title } : {}) } : undefined; const requestKey = JSON.stringify([order.orderId, takeover?.recovery.basis ?? 'none', complete, filed ?? null, sourceAssertion ?? null]); const continuesOwn = Boolean(takeover && entry.current?.operationId === takeover.recovery.operationId && entry.current.requestKey === requestKey && takeover.held?.operationId === takeover.recovery.operationId && unconfirmedOrders.get(recoveryKey) === takeover.held); const canResumeStored = takeover?.recovery.canResumeStored === true; const submit = async (resumeStored = false) => { if (resumeStored !== canResumeStored || !admission.current() || withdrawn || busy || checking || attaching || (!resumeStored && held) || (unconfirmedOrders.has(recoveryKey) && !takeover)) return; if (takeover) { try { checkedOwner.current?.admit(); } catch { setTakeover(undefined); return; } if (!checkedOwner.current?.belongs()) return; } const save = saves.start(); const pending = Symbol(); if (!resumeStored && !continuesOwn) { entry.current = { ...entryOperationFor(entry.current, JSON.stringify([requestKey, takeover?.recovery ?? null])), requestKey, ...(takeover ? { supersedes: takeover.recovery.operationId } : {}) }; } const supersedes = entry.current?.supersedes; const operationId = resumeStored ? takeover?.recovery.operationId : entry.current?.operationId; if (!operationId) return; const attempt: PendingAttempt = { token: pending, operationId }; let sent = false; const active = { operation: save, sent: false }; activeSave.current = active; const settle = () => { resultEntryRecovery.settle(recoveryKey, attempt); }; const admit: RequestAdmission = (phase) => { save.admit(); permitted(phase); if (!admission.current()) { save.retire(); throw new SaveRetiredError(); } if (!sent && reviewedRecovery.current !== takeover) throw new SaveRetiredError(); if (!sent && takeover) { checkedOwner.current?.admit(); if (!checkedOwner.current?.belongs()) throw new SaveRetiredError(); } if (phase !== 'answer' && blockedByPending(unconfirmedOrders.get(recoveryKey), pending, operationId) && !(takeover && unconfirmedOrders.get(recoveryKey) === takeover.held)) { setBusy(false); save.retire(); throw new SaveRetiredError(); } }; setBusy(true); setFailure(undefined); try { const saved = await save.step(() => recordResult(resumeStored ? { orderId: order.orderId, operationId, basis: takeover!.recovery.basis, resumeStored: true } : { orderId: order.orderId, operationId, basis: takeover?.recovery.basis ?? "none", ...(supersedes ? { supersedes } : {}), observations: complete, report: filed, ...(sourceAssertion ? { sourceAssertion } : {}) }, { ...save.transport, admit, onSend: () => { admit(); // Cancellation after fetch admission cannot prove that the server rolled this write back. const reserved = takeover ? resultEntryRecovery.takeOver(recoveryKey, takeover.held, attempt) : resultEntryRecovery.reserve(recoveryKey, attempt); if (!reserved) throw new SaveRetiredError(); sent = true; active.sent = true; if (resumeStored) setTakeover(undefined); }, }).catch(error => { if (save.belongs() && admission.current() && entryFailureIsSettled(error)) settle(); throw error; }), true); admit('answer'); if (!entryIsCompleted(saved, order.orderId, operationId)) { throw Object.assign(new Error(UNCONFIRMED_RESULT), { name: 'UnreadableWriteAnswerError' }); } settle(); onSaved(saved.state); admit('answer'); onClose(); } catch (error) { if (isSaveRetired(error) || !save.belongs() || !admission.current()) return; console.error("Error saving a test result:", error); const unsentTimeout = isSaveTimedOut(error) && !sent; const unknown = sent && !entryFailureIsSettled(error); if (!unknown) settle(); setFailure(unknown ? UNCONFIRMED_RESULT : unsentTimeout ? RESULT_NOT_SENT : isSaveTimedOut(error) ? error.message : relayServerFailure(error, SAVE_FAILED)); } finally { if (save.belongs()) setBusy(false); if (activeSave.current === active) activeSave.current = undefined; save.retire(); } }; const checkSavedResult = async (get: ReadResult) => { if (busy || checking || !admission.current() || withdrawn) return; const check = saves.start(); const heldAttempt = unconfirmedOrders.get(recoveryKey); let confirmed = false; const admit = () => { check.admit(); permitted(); if (!admission.current()) { check.retire(); throw new SaveRetiredError(); } }; setChecking(true); try { const read = await check.step(() => get( `${environment.fhirBaseApi}/diagnostic-order/${encodeURIComponent(order.orderId)}/result?operationKind=entry`, { ...check.transport, admit }, )); admit(); if (unconfirmedOrders.get(recoveryKey) !== heldAttempt) throw new SaveRetiredError(); if (!entryReadIdentityMatches(read, order.orderId)) throw new Error(CHECK_FAILED); const settlement = Array.isArray(read?.entrySettlements) && heldAttempt ? read.entrySettlements.find(item => item.operationId === heldAttempt.operationId && ['completed', 'superseded', 'refused'].includes(item.outcome)) : undefined; if (settlement) { resultEntryRecovery.settle(recoveryKey, heldAttempt!); setTakeover(undefined); entry.current = undefined; setFailure(settlement.outcome === 'completed' ? 'The earlier entry completed. Return to the order to review its saved result.' : 'The earlier entry has ended. Review the current order before entering a new result.'); } // An order that never took a report answers with its settlements alone; that answer ends the held entry. if (settlement && read?.entryRecovery === undefined && !read?.entryOperation && read?.status === undefined && read?.measurements === undefined) return; const recovery = readEntryRecovery(read?.entryRecovery); if (recovery) { confirmed = true; checkedOwner.current = check; setTakeover({ recovery, held: unconfirmedOrders.get(recoveryKey) }); setFailure(recovery.canResumeStored ? 'An earlier entry is saved and still needs to finish. Resume that saved entry without sending your current measurements.' : 'An earlier entry is still unfinished. Review your measurements, then explicitly take over that entry.'); return; } if (read?.entryOperation) throw new Error(CHECK_FAILED); // An absent or malformed read cannot establish what the earlier write did. if (!read || !["preliminary", "final"].includes(read.status ?? "") || !Array.isArray(read.measurements) || !read.measurements.every(measurement => measurement && typeof measurement === "object" && [measurement.name, measurement.value, measurement.unit].every(value => value === undefined || typeof value === "string"))) throw new Error(CHECK_FAILED); confirmed = true; checkedOwner.current = check; setCheckedResult(read); setFailure(undefined); } catch (error) { if (isSaveRetired(error) || !check.belongs() || !admission.current()) return; setFailure(uncertain ? CHECK_FAILED : "The saved entry could not be checked. Try checking again before resuming it."); } finally { if (check.belongs()) setChecking(false); if (!confirmed) check.retire(); } }; if (checkedResult) { const returnToOrders = () => { const owner = checkedOwner.current; if (!owner?.belongs() || !admission.current()) return; onSaved(); if (owner.belongs() && admission.current()) onClose(); owner.retire(); }; return ( { if (!open) returnToOrders(); }}> Saved result for this test {[order.patient ?? "Patient not named", order.test ?? "Test not named"].join(" · ")}

A {checkedResult.status} result is on file. Review what is saved before making any further changes.

{checkedResult.measurements.map((measurement, at) => (
{measurement.name ?? "Measurement not named"}
{[measurement.value ?? "Value not reported", measurement.unit].filter(Boolean).join(" ")}
))}
); } return ( { if (!open && !busy && !checking) onClose(); }}> Enter this test's result {[order.patient ?? "Patient not named", order.test ?? "Test not named"].join(" · ")} {order.detail ?

{order.detail}

: null} {failure || (uncertain && !busy) ? ( {failure ?? UNCONFIRMED_RESULT} ) : null}

This records your statement about where the result came from.

{!settled ? ( error && !loading ? ( {error} ) : (

Reading this test's measurements from your catalogue…

) ) : catalogued ? (
{fields.map((field, at) => (
setValues((prev) => ({ ...prev, [at]: event.target.value }))} />
))}
) : (

Your catalogue does not define this test's measurements. Enter each one as the laboratory reported it; nothing is filled in for you.

{manual.map((row, at) => (
setRow(at, { name: event.target.value })} />
setRow(at, { value: event.target.value })} />
setRow(at, { unit: event.target.value })} />
{manual.length > 1 ? ( ) : null} {unfinished[at] ?

{unfinished[at]}

: null}
))}
)} {missing.length ? (

{`Not entered: ${missing.join(", ")}. Saved without them, this result is partial.`}

) : null} admission.current() && !withdrawn} onAttachingChange={setAttaching} />

{`Saving is preliminary. It does not release this test${order.inPanel ? " or change the other tests in this panel" : ""}.`}

{withdrawn ? : null} {!busy ? : null}
); }; export default EnterTestResultDialog;