import { useEffect, useRef, useState } from "react"; import type { ServiceRequest } 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 LabTestPicker, { type LabTestChoice } from "../../../../components/LabTestPicker/LabTestPicker"; import DiagnosisPicker from "../../../../components/DiagnosisPicker/DiagnosisPicker"; import type { IValueSet } from "../../../../services"; import { usePatientServiceContext } from "../../../../services"; import { usePlaceDiagnosticOrder, type DiagnosticOrderPriority } from "../../../../hooks/usePlaceDiagnosticOrder.ts"; import { usePlaceDiagnosticPanel } from "../../../../hooks/usePlaceDiagnosticPanel.ts"; import type { DiagnosticOrderKind } from "../../../../utils/diagnosticOrderQueue.ts"; import { relayServerFailure } from "../../../../utils/fetchFailureMessage"; import { isSaveRetired, isSaveTimedOut, outcomeUnknown, startSaveOperation, type SaveOperation } from "../../../../utils/saveOperation"; /** * A PRACTITIONER PLACES A LAB ORDER, and this is a practitioner surface. * * `POST /R4/ServiceRequest` allows `[PRACTITIONERS]` and carries no permission section, deliberately: * ordering a test is a clinical judgement, and a practice must not be able to hand it to somebody * else with a permission toggle. The `diagnostics` section governs the WORK that follows -- the * queue, collection, despatch, recording what came back -- and none of that is on this screen. * * TWO QUESTIONS, ASKED IN ORDER. What to order comes out of a search; why, and how urgently, come out * of the practitioner's head. Showing every field at once beside a search that has not been run gets * a worse answer to each -- the referral dialog next door records the same reasoning. * * SEVERAL TESTS ARE ONE ACT WHEN THEY ARE ORDERED TOGETHER. A panel goes to its own route, which * writes the parent that holds the components together; one test still goes to the single-order route, * because a group of one would put a panel heading on a single row for nothing. The screen decides by * counting, so nobody is asked which kind of act they are performing. * * WHY AND HOW URGENTLY ARE ASKED ONCE FOR THE WHOLE PANEL. One draw, one diagnosis, one urgency -- * asking three times would be three chances to disagree with yourself, and the server writes the * answer onto every component anyway because medical necessity is claimed per test. */ const KINDS: { value: DiagnosticOrderKind; label: string }[] = [ { value: "laboratory", label: "Lab test" }, { value: "imaging", label: "Imaging" }, ]; const PRIORITIES: { value: DiagnosticOrderPriority; label: string }[] = [ { value: "routine", label: "Routine" }, { value: "urgent", label: "Urgent" }, { value: "stat", label: "STAT" }, ]; /** * Two chosen tests are the same test when they name the same coded thing, or the same words when * neither is coded. An uncoded choice cannot be compared to a coded one, so it is not: they may be the * same test and the screen cannot know it. */ const sameTest = (a: LabTestChoice, b: LabTestChoice) => (a.coding && b.coding ? a.coding.system === b.coding.system && a.coding.code === b.coding.code : !a.coding && !b.coding && a.text.trim() === b.text.trim()); export const UNCONFIRMED_ORDER_MESSAGE = "We could not confirm this order was placed. It may already be on this patient's list, and a new " + "order can take a moment to appear there. Check before ordering again."; export type PlaceDiagnosticOrderDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; patientId: string; patientName?: string; /** Every order the act placed - one for a single test, several for a panel. */ onPlaced: (created: ServiceRequest[]) => void; /** Placing failed without an answer from the server, so the order may exist after all. */ onUnconfirmed: () => void; /** Whether this opening is still the one on screen: same viewer, practice and patient, not since reopened. */ owns?: () => boolean; }; export const PlaceDiagnosticOrderDialog = ({ open, onOpenChange, patientId, patientName, onPlaced, onUnconfirmed, owns = () => true, }: PlaceDiagnosticOrderDialogProps) => { const placeDiagnosticOrder = usePlaceDiagnosticOrder(); const placeDiagnosticPanel = usePlaceDiagnosticPanel(); /** * The visit this order came out of. `encounterState` is set in exactly two places: entering a * visit from the clinician queue, and the chart mounted inside an encounter route. * * Undefined is ordinary and correct -- an order placed from a chart opened directly did not come * out of a visit, and stamping one would be a lie the visit container would then repeat. */ const { encounterState } = usePatientServiceContext(); /** * WHICH KIND, ASKED FIRST, because it changes everything after it: the category the server * discriminates on, the state machine the order runs on, and the steps staff are later offered. * Defaulting silently would be how a chest X-ray ends up asking somebody to collect a specimen. */ const [kind, setKind] = useState(null); /** In the order they were chosen, which is the order the panel lists them in. */ const [chosen, setChosen] = useState([]); /** The picker is up: either nothing is chosen yet, or somebody asked to add another. */ const [adding, setAdding] = useState(false); const [panelName, setPanelName] = useState(""); const [priority, setPriority] = useState("routine"); const [reason, setReason] = useState(""); const [diagnosis, setDiagnosis] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // The placement in flight. It belongs to this opening: retired when the opening goes, so a held request // is never sent under whoever is on screen next. const placement = useRef(); useEffect(() => () => placement.current?.retire(), []); const reset = () => { setKind(null); setChosen([]); setAdding(false); setPanelName(""); setPriority("routine"); setReason(""); setDiagnosis(null); setError(null); }; // Chosen twice is chosen once. The picker can return the same test from a second search, and two // identical rows in a panel are two orders nobody can tell apart on the bench. const add = (test: LabTestChoice) => { setChosen((held) => (held.some((each) => sameTest(each, test)) ? held : [...held, test])); setAdding(false); }; const place = async () => { if (!chosen.length || !kind || busy) return; const operation = startSaveOperation(owns); placement.current = operation; setBusy(true); setError(null); try { // Bounded like any save: a preparation that never settles, or an answer past the deadline, ends as unconfirmed. const created = await operation.step(async () => chosen.length > 1 ? (await placeDiagnosticPanel({ patientId, kind, name: panelName, tests: chosen.map((test) => ({ text: test.text, coding: test.coding })), encounterId: encounterState?.id, priority, reason: reason.trim() || undefined, diagnosis: diagnosis ?? undefined, }, operation.transport)).orders ?? [] : [await placeDiagnosticOrder({ patientId, kind, testText: chosen[0].text, coding: chosen[0].coding, encounterId: encounterState?.id, priority, reason: reason.trim() || undefined, diagnosis: diagnosis ?? undefined, }, operation.transport)], true); if (!operation.belongs()) return; reset(); onOpenChange(false); onPlaced(created); } catch (failure: unknown) { if (isSaveRetired(failure) || !operation.belongs()) return; // NEVER CLOSES ON FAILURE. A practitioner who saw it vanish would believe the test was // ordered, and nobody downstream would draw a specimen for an order that does not exist. if (outcomeUnknown(failure)) { // Nothing answered, so the order may exist. Saying otherwise invites the second press // that makes a duplicate. setError(UNCONFIRMED_ORDER_MESSAGE); onUnconfirmed(); } else { setError(isSaveTimedOut(failure) ? failure.message : relayServerFailure(failure, "That order could not be placed. Nothing was ordered.")); } } finally { if (operation.belongs()) setBusy(false); // Settled: its deadline timer and signal are released, and nothing late can act for it. operation.retire(); } }; return ( // Not dismissed while a placement is unresolved: its outcome is told here, and a reopening is never its own. { if (!next && busy) return; if (!next) reset(); onOpenChange(next); }}> Order a test or study {patientName ? `For ${patientName}.` : "For this patient."}{" "} {encounterState?.id ? "This order will be recorded against the current visit." : "This order is not attached to a visit."} {error && ( {error} )} {!chosen.length || adding ? (
{KINDS.map((option) => ( ))}
{/* Only while adding to something already chosen. There is nothing to go back to before the first test, and a Cancel that looks like a Back is its own confusion. */} {chosen.length > 0 && ( )}
) : (
{/* WHICH KIND, STILL SAID AFTER THE CHOICE IS MADE. An ultrasound reached the laboratory bench once because nothing on this screen named the kind once a test was picked, and by this point the chooser above is locked -- so this line is the only thing saying it. One line rather than one per row: a panel goes as a single kind. */}

{kind === "imaging" ? "Imaging" : "Lab test"}

{chosen.map((test) => (

{test.text}

{/* Said plainly rather than left blank. An uncoded order is legitimate -- the lookup may be down -- but whoever reads it later should know a result cannot be matched back to it automatically. */}

{test.coding ? test.coding.code : "No code — matched by name only"}

))}
{/* ONLY WHEN THERE IS A PANEL TO NAME. One test is an order and has nothing to call itself; the name is what a chart shows over the group instead of counting it. */} {chosen.length > 1 && (
setPanelName(event.target.value)} />
)}
{PRIORITIES.map((option) => ( ))}
setReason(event.target.value)} />
)}
); }; export default PlaceDiagnosticOrderDialog;