import { useRef, useState, useSyncExternalStore } from "react"; import { TriangleAlert } from "lucide-react"; import { ArchiveConfirmModal } from "../../../components/ArchiveConfirmModal/ArchiveConfirmModal"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Button } from "../../../components/ui/button.tsx"; import { useWithdrawDiagnosticOrder } from "../../../hooks/useWithdrawDiagnosticOrder.ts"; import type { DiagnosticOrderQueueRow } from "../../../hooks/useDiagnosticOrderQueue.ts"; import type { ServiceRequest } from "fhir/r4"; import { readArchiveDetail, type ArchiveReason } from "../../../utils/archivedResource"; import { isSaveRetired, outcomeUnknown, useSaveScope, type SaveOperation } from "../../../utils/saveOperation"; import { mayHaveBeenWritten, relayServerFailure } from "../../../utils/fetchFailureMessage"; import { hasLostAnswer, noteLostAnswer, onLostAnswerChange, onViewerChange, settleLostAnswers, startRead, viewerEpoch, writeThroughViewer, } from "../../../utils/writeThroughHold"; const UNCONFIRMED = "The withdrawal could not be confirmed. The list is being checked; if the order is still offered, send the same withdrawal again."; const STILL_CHECKING = "The list is still being checked for the earlier withdrawal. It is sent again only once the check shows the order still offered."; const SETTLED_ELSEWHERE = "The list no longer offers this order for withdrawal, so nothing more was sent. Check the order before doing anything else."; /** One withdrawal as it was admitted, for the order and viewer it was sent for. `sent`: its request was seen leaving. */ interface Attempt { reason: ArchiveReason; note?: string; sent: boolean; orderId: string; viewer: string | null; test?: string } // Withdrawals not yet answered, by the viewer who sent them (person, acted-as link, practice) and the order, which names // its patient. Kept past this dialog and across viewer changes: the same withdrawal is what is sent again, and until a // read begun after the loss answers, nothing more is sent for that order. const heldListeners = new Set<() => void>(); // Tells the list's notice whenever an attempt is held or let go. class Attempts extends Map { set(hold: string, attempt: Attempt) { super.set(hold, attempt); heldListeners.forEach((listener) => listener()); return this; } delete(hold: string) { const had = super.delete(hold); if (had) heldListeners.forEach((listener) => listener()); return had; } } const unresolved = new Attempts(); // Held orders a read after the loss no longer offered: the withdrawal landed or the order moved on, so nothing is resent. const settledElsewhere = new Set(); const holdOf = (viewer: string | null, orderId: string) => `diagnostic-order-withdrawal ${viewer ?? ""} ${orderId}`; const answered = (hold: string) => { unresolved.delete(hold); settleLostAnswers([hold], startRead()); }; // The server's own word that the withdrawal may have landed, or landed without its record yet: not a refusal. const UNSETTLED_CODES = new Set(["WITHDRAWAL_UNCONFIRMED", "WITHDRAWAL_UNRECORDED"]); const unsettledByServer = (failure: unknown) => UNSETTLED_CODES.has(String((failure as { body?: { code?: unknown } } | null)?.body?.code)); // A refusal from the server is an answer: nothing of the withdrawal landed. const refused = (failure: unknown) => typeof (failure as { status?: unknown } | null)?.status === "number" && !mayHaveBeenWritten(failure) && !unsettledByServer(failure); const listedIn = (reread: unknown): DiagnosticOrderQueueRow[] | undefined => reread && typeof reread === "object" && Array.isArray((reread as { listed?: unknown }).listed) ? (reread as { listed: DiagnosticOrderQueueRow[] }).listed : undefined; const orderIn = (reread: unknown, orderId: string): ServiceRequest | undefined => { const order = reread && typeof reread === "object" ? (reread as { order?: ServiceRequest }).order : undefined; return order?.resourceType === "ServiceRequest" && order.id === orderId ? order : undefined; }; // How the order read by id was withdrawn, when it was. const withdrawalOn = (order: ServiceRequest) => (order.status === "revoked" || order.status === "entered-in-error" ? readArchiveDetail(order) : undefined); // The ordering clinician withdraws an order from Order details: an untouched one, or one sent to an Akashic laboratory // that has not accepted it. An opening belongs to the viewer it was opened for; once they change it never sends again. export const WithdrawQueueOrderDialog = ({ row, onClose, onSettled, }: { row?: DiagnosticOrderQueueRow; onClose: () => void; /** Re-read the queue, and the order by id when the read leaves it out. A `{ listed, order }` answer is judged; false means nothing was read. */ onSettled: (orderId?: string) => unknown; }) => { const withdrawOrder = useWithdrawDiagnosticOrder(); const scope = useSaveScope(row?.orderId, () => setSubmitting(false)); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(); const epoch = useSyncExternalStore(onViewerChange, viewerEpoch); const [shown, setShown] = useState<{ orderId?: string; epoch: number }>({ epoch }); // The press under way in this dialog, from its first check to its answer: one at a time. const pressing = useRef(); const reading = useRef(new Map>()); const orderId = row?.orderId; if (orderId !== shown.orderId) { setShown({ orderId, epoch }); setError(undefined); } const current = orderId !== shown.orderId || shown.epoch === epoch; const hold = orderId ? holdOf(writeThroughViewer(), orderId) : undefined; const held = useSyncExternalStore(onLostAnswerChange, () => Boolean(hold && hasLostAnswer(hold))); const kept = hold ? unresolved.get(hold) : undefined; const sent = row?.state === "sent"; // Rereads the queue for a held order, one read at a time, and settles the hold only on an answer for the same viewer // read after the loss. A withdrawal seen leaving is sent again only when that read lists the order still offered. const check = (heldFor: string, forOrder: string): Promise => { const inFlight = reading.current.get(heldFor); if (inFlight) return inFlight; const since = viewerEpoch(); const started = startRead(); const judged = (async () => { let reread: unknown; try { reread = await onSettled(forOrder); } catch { return false; } if (reread === false || viewerEpoch() !== since) return false; const now = listedIn(reread)?.find((each) => each.orderId === forOrder); const attempt = unresolved.get(heldFor); const order = orderIn(reread, forOrder); const landed = !now && order ? withdrawalOn(order) : undefined; // The order carries this very withdrawal: it landed, and sending it again only completes its record. It stays // held, and on the list's notice, until that completion is answered. if (landed && attempt && landed.reason === attempt.reason && (landed.note ?? "") === (attempt.note ?? "").trim()) { return true; } if ((now && !now.withdrawable) || landed) { unresolved.delete(heldFor); settledElsewhere.add(heldFor); settleLostAnswers([heldFor], started); if (holdOf(writeThroughViewer(), forOrder) === heldFor) setError(SETTLED_ELSEWHERE); return false; } if (unresolved.get(heldFor)?.sent !== false && !now?.withdrawable) return false; settleLostAnswers([heldFor], started); return true; })().finally(() => reading.current.delete(heldFor)); reading.current.set(heldFor, judged); return judged; }; const submit = async (reason: ArchiveReason, note?: string) => { if (!orderId || !hold || !current || viewerEpoch() !== shown.epoch || pressing.current?.belongs()) return; if (settledElsewhere.has(hold)) { setError(SETTLED_ELSEWHERE); return; } // Owned from here: closing the order, opening another or a change of viewer retires the whole press. const operation = scope.start(); pressing.current = operation; const attempt: Attempt = unresolved.get(hold) ?? { reason, ...(note ? { note } : {}), sent: false, orderId, viewer: writeThroughViewer(), ...(row?.test ? { test: row.test } : {}) }; let leftAt = false; try { if (hasLostAnswer(hold)) { setError(STILL_CHECKING); if (!(await check(hold, orderId)) || !operation.belongs()) return; } setSubmitting(true); setError(undefined); const transport = { ...operation.transport, onSend: () => { operation.transport.onSend?.(); leftAt = true; unresolved.set(hold, { ...attempt, sent: true }); noteLostAnswer(hold); }, }; await operation.step(() => withdrawOrder(orderId, attempt.reason, attempt.note, transport).then( (answer) => { answered(hold); return answer; }, (failure: unknown) => { if (refused(failure)) answered(hold); else if (!leftAt && (outcomeUnknown(failure) || unsettledByServer(failure))) { unresolved.set(hold, attempt); noteLostAnswer(hold); } throw failure; }, ), true); if (!operation.belongs()) return; void onSettled(); onClose(); } catch (failure) { if (isSaveRetired(failure) || !operation.belongs()) return; const unknown = hasLostAnswer(hold); setError(unknown ? UNCONFIRMED : relayServerFailure(failure, "That order could not be withdrawn. Nothing changed.")); if (unknown) void check(hold, orderId); else void onSettled(); } finally { operation.retire(); if (pressing.current === operation) pressing.current = undefined; setSubmitting(false); } }; return ( { if (!open && !submitting) onClose(); }} resourceLabel={row?.test ?? "this order"} title={`Withdraw ${row?.test ?? "this order"}?`} description={sent ? `${row?.akashicRoute?.laboratory ?? "The laboratory"} has not accepted it yet. Withdrawing stops them accepting it; it stays on the list marked Withdrawn.` : "It stays on this patient's list marked Withdrawn and comes off the practice's lab or imaging worklist."} confirmLabel="Withdraw order" submittingLabel="Withdrawing..." cancelLabel="Keep the order" submitting={submitting} error={error ?? (held && !submitting ? UNCONFIRMED : undefined)} initialReason={kept?.reason} initialNote={kept?.note} onConfirm={(reason, note) => void submit(reason, note)} /> ); }; export default WithdrawQueueOrderDialog; // Sent withdrawals of this viewer still unconfirmed for orders the list no longer shows, as one string so it is stable. const heldSnapshot = (shownIds: string): string => { const viewer = writeThroughViewer(); const shown = new Set(shownIds.split(" ")); return [...unresolved.entries()] .filter(([hold, attempt]) => attempt.viewer === viewer && attempt.sent && hasLostAnswer(hold) && !shown.has(attempt.orderId)) .map(([, attempt]) => attempt.orderId).sort().join(" "); }; const onHeldChange = (listener: () => void) => { heldListeners.add(listener); const stopLost = onLostAnswerChange(listener); const stopViewer = onViewerChange(listener); return () => { heldListeners.delete(listener); stopLost(); stopViewer(); }; }; /** * The door back to a withdrawal whose outcome is still unknown once its order has left the list: Check withdrawal * reopens it as it was sent, and the dialog finishes it only from what the order itself shows. */ export const UnconfirmedWithdrawals = ({ rows, onCheck }: { rows: DiagnosticOrderQueueRow[]; onCheck: (row: DiagnosticOrderQueueRow) => void; }) => { const shownIds = rows.map((each) => each.orderId).filter(Boolean).join(" "); const held = useSyncExternalStore(onHeldChange, () => heldSnapshot(shownIds)); if (!held) return null; const viewer = writeThroughViewer(); return ( {held.split(" ").map((orderId) => { const attempt = unresolved.get(holdOf(viewer, orderId)); const test = attempt?.test ?? "an order"; return (

A withdrawal of {test} could not be confirmed, and the order is no longer on this list.{" "}

); })}
); };