import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { AlertCircle, Loader2, TriangleAlert } from "lucide-react";
import { Alert, AlertDescription } from "../../../components/ui/alert.tsx";
import { RowPriority, RowStatus } from "../../../components/ui/row-meta.tsx";
import { Button } from "../../../components/ui/button.tsx";
import { Input } from "../../../components/ui/input.tsx";
import { Label } from "../../../components/ui/label.tsx";
import { useDiagnosticOrderQueue, type DiagnosticOrderQueueRow } from "../../../hooks/useDiagnosticOrderQueue.ts";
import SendOutDialog from "./SendOutDialog.tsx";
import { useSendOutDiagnosticOrder } from "../../../hooks/useSendOutDiagnosticOrder.ts";
import ReleaseResultDialog from "./ReleaseResultDialog.tsx";
import AddAddendumDialog from "./AddAddendumDialog.tsx";
import FaxResultDialog from "./FaxResultDialog.tsx";
import ReturnReportDialog from "./ReturnReportDialog.tsx";
import ScheduleStudyPage from "./ScheduleStudyPage.tsx";
import ArchiveConfirmModal from "../../../components/ArchiveConfirmModal/ArchiveConfirmModal";
import type { ArchiveReason } from "../../../utils/archivedResource";
import { describeDrill, diagnosticDrillFrom, matchesDrill } from "./diagnosticDrill";
import { relayServerFailure, ownFailureSentence } from "../../../utils/fetchFailureMessage";
import { useImagingUploadService } from "../../../services/ImagingUploadService.tsx";
import { useOptionalAuthServiceContext } from "../../../services/AuthService.tsx";
import { authSessionId } from "../../../services/authStorage";
import { SaveRetiredError, isSaveRetired, isSaveTimedOut, outcomeUnknown, useSaveScope } from "../../../utils/saveOperation";
import { useRowOwnedSaves, type RowSave } from "../../../utils/rowOwnedSave";
import EnterTestResultDialog from "./EnterTestResultDialog.tsx";
import UnreviewedResults from "./UnreviewedResults.tsx";
import RecordRefusalDialog from "./RecordRefusalDialog.tsx";
import WithdrawQueueOrderDialog, { UnconfirmedWithdrawals } from "./WithdrawQueueOrderDialog.tsx";
import RecollectionNeededDialog from "./RecollectionNeededDialog.tsx";
import { QueueOrderDialog, QueueResultDialog } from "./QueueOrderDialogs.tsx";
import { ImagingGroup, ImagingRow } from "./ImagingWorklist.tsx";
import UploadOrderImagesDialog from "./UploadOrderImagesDialog.tsx";
import SendForReadingDialog from "./SendForReadingDialog.tsx";
import OutsideReadArrivalDialog from "./OutsideReadArrivalDialog.tsx";
import { useImagingReadHandoff, type ImagingReader } from "../../../hooks/useImagingReadHandoff.ts";
import { useRecordOrderRefusal } from "../../../hooks/useRecordOrderRefusal.ts";
import { useStaffPermissionServiceContext } from "../../../services";
import {
isDiagnosticOrderState,
diagnosticsQueueLabel,
nextStatesFor,
lensFor,
type DiagnosticOrderState,
} from "../../../utils/diagnosticOrderQueue.ts";
import { deliveryLine, panelStanding, returnLine, sortQueue, stateLabel } from "./queueLines.ts";
import "../BenchPage/BenchPage.css";
// THE LAB QUEUE -- every order this practice is working, across patients.
//
// The practitioner places the order; staff carry it out. This screen is the EXECUTION half, and
// nothing on it places an order. Each row carries its one next step; everything else that can be
// done to an order opens with the order, so a row is never a strip of buttons.
const joined = (...parts: (string | undefined)[]) => parts.filter(Boolean).join(" · ");
const orderedOn = (iso?: string) => {
if (!iso) return undefined;
const at = new Date(iso);
return Number.isNaN(at.getTime()) ? undefined : `Ordered ${at.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;
};
const timeOf = (iso?: string) => {
if (!iso) return undefined;
const at = new Date(iso);
if (Number.isNaN(at.getTime())) return undefined;
const time = at.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
return `${at.toLocaleDateString("en-US", { month: "short", day: "numeric" })}, ${time}`;
};
/** Where a study stands in the department: booked where and when, who performed it, what images are filed. */
const imagingLines = (row: DiagnosticOrderQueueRow, importing: boolean): string[] => {
if (row.kind !== "imaging") return [];
const lines = [joined(row.modality, row.room, timeOf(row.scheduledFor))];
if (row.performed) {
lines.push(joined(`Performed ${timeOf(row.performed.at) ?? ""}`.trim(), row.performed.by ? `by ${row.performed.by}` : undefined));
}
// An uncounted study says nothing; only a count of zero is "no images".
if (row.images) {
const { studies, instances } = row.images;
lines.push(instances
? `${instances} ${instances === 1 ? "image" : "images"} in ${studies} ${studies === 1 ? "study" : "studies"}`
: importing ? "Images uploaded, still being filed" : "No images yet");
}
return lines.filter(Boolean);
};
/** Received here and not sent anywhere: the specimen is on this laboratory's bench. */
const onTheBench = (row: DiagnosticOrderQueueRow) =>
row.kind === "laboratory" && Boolean(row.accession) && !row.sentOut && !row.akashicRoute && !row.awaitingRelease;
// Refused at receipt and waiting for the replacement tube: the order is still collected, the tube is not.
const awaitingRedraw = (row: DiagnosticOrderQueueRow) =>
row.kind === "laboratory" && row.state === "collected" && !row.accession && row.rejection?.disposition === "redraw";
const isUrgent = (row: DiagnosticOrderQueueRow) => row.priority === "stat" || row.priority === "urgent";
const matches = (row: DiagnosticOrderQueueRow, query: string) =>
[row.patientDisplay, row.test, row.accession, row.theirOrderNumber, row.fromAkashic?.theirOrderId]
.some((value) => value?.toLowerCase().includes(query));
const panelLine = (of: number) => `Ordered with ${of - 1} other ${of === 2 ? "test" : "tests"} — one draw`;
const QueueRow = ({ row, panelOf, settled, importing, action }: {
row: DiagnosticOrderQueueRow;
panelOf: number;
settled: boolean;
importing: boolean;
action: React.ReactNode;
}) => (
{row.accession ? {row.accession} : null}
{row.test ?? "Test not named"}
{/* The patient, because whoever draws a specimen has to label the tube. Not a route into the record.
One draw serves the whole panel; the tests stay separate rows because they are worked separately. */}
{joined(row.patientDisplay ?? "Patient not named", orderedOn(row.authoredOn), panelOf > 1 ? panelLine(panelOf) : undefined)}
{/* State is a word, never a pill; released is never implied to be reviewed. */}
{awaitingRedraw(row) && !settled ? (
Recollection needed
) : row.refusal && !settled ? (
Needs attention · laboratory refused the order
) : {settled
? (row.state === "reviewed" ? "Reviewed" : "Not yet reviewed")
: row.awaitingRelease ? "Awaiting release" : onTheBench(row) ? "On the bench" : stateLabel(row.state)}}
{imagingLines(row, importing).map((line) => {line})}
{row.fromAkashic ? (
{joined(`From ${row.fromOutside ?? "the sending practice"}`, row.fromAkashic.orderedBy, `Sender’s order ${row.fromAkashic.theirOrderId}`)}
) : row.fromOutside ? (
{joined(`From ${row.fromOutside}`, row.theirOrderNumber ? `Sender’s order ${row.theirOrderNumber}` : undefined)}
) : null}
{deliveryLine(row) ? {deliveryLine(row)} : null}
{returnLine(row) ? {returnLine(row)} : null}
{/* Work stopped dead, and the state cannot say so: the order is still sent because it was. */}
{awaitingRedraw(row) ? (
{"Last specimen rejected: " + (row.rejection?.reason ?? "no reason recorded")}
) : null}
{row.refusal ? {row.refusal.reason ?? "The laboratory refused this order"} : null}
{action}
);
/** An imaging department works a study in the order it happens, so its open work is grouped by stage. */
const IMAGING_STAGES: { state: DiagnosticOrderState; title: string; empty: string }[] = [
{ state: "ordered", title: "To schedule", empty: "No study is waiting to be scheduled." },
{ state: "scheduled", title: "Scheduled", empty: "No study is scheduled." },
{ state: "performed", title: "Performed", empty: "No performed study is waiting on its result." },
];
/** The stage a row moves on to from the list itself, and the words for that move. */
const DIRECT_MOVE: Partial> = {
ordered: { to: "scheduled", label: "Schedule" },
scheduled: { to: "performed", label: "Mark performed" },
};
/** A section always states its count, so a zero reads as settled rather than as a failed load. */
const Group = ({ title, rows, empty, searching, children }: {
title: string;
rows: DiagnosticOrderQueueRow[];
empty: string;
searching: boolean;
children: (row: DiagnosticOrderQueueRow) => React.ReactNode;
}) => (
{title}
{`· ${rows.length}`}
{rows.length ?
{rows.map(children)}
: searching ? null :
{empty}
}
);
/** A performed, imaged, unreported study no reader has: it may go out, to a reader on Akashic or not. */
const sendsToBeRead = (row?: DiagnosticOrderQueueRow): boolean => {
if (!row?.orderId || row.kind !== "imaging" || row.state !== "performed") return false;
if ((row.images?.instances ?? 0) === 0 || row.resultOnFile || row.readFor) return false;
if (row.outsideRead && row.outsideRead.confirmation?.outcome !== "not-received") return false;
return !row.readElsewhere || Boolean(row.readElsewhere.declinedAt && !row.readElsewhere.returnedAt);
};
/** The one upload a discard is about (order, upload, job), only while its malware check refused it and nothing filed. */
// The upload a check is for: its order, upload and job (none yet when the start's answer was lost).
const checkKey = (row: DiagnosticOrderQueueRow): string | undefined => {
const upload = row.imageImport;
if (row.state !== "performed" || !row.orderId || upload?.state !== "pending") return undefined;
return [row.orderId, upload.uploadId, upload.jobId ?? ""].join("|");
};
const discardKey = (row: DiagnosticOrderQueueRow): string | undefined => {
const upload = row.imageImport;
const refused = upload?.state === "pending" && (upload.filingHeld === "blocked" || upload.filingHeld === "unscannable");
if (row.state !== "performed" || !row.orderId || row.images?.instances !== 0 || !refused || !upload?.jobId) return undefined;
return [row.orderId, upload.uploadId, upload.jobId].join("|");
};
const DiagnosticOrdersPage = () => {
const navigate = useNavigate();
const { rows, loading, error, advance, cancelStudy, reload, recountImages, markReleased, markReturned, markSentOut } = useDiagnosticOrderQueue();
const { activeWorkspace, canManage, permissionsSettled } = useStaffPermissionServiceContext();
const mayEnterLab = permissionsSettled && canManage("diagnostics");
const [busyOrderId, setBusyOrderId] = useState();
const [advanceError, setAdvanceError] = useState();
const [query, setQuery] = useState("");
const [viewing, setViewing] = useState();
const [viewingResult, setViewingResult] = useState();
const [viewingRejection, setViewingRejection] = useState();
const [sendingOut, setSendingOut] = useState();
const [releasing, setReleasing] = useState();
const [adding, setAdding] = useState();
const [faxing, setFaxing] = useState();
const [returning, setReturning] = useState();
const [entering, setEntering] = useState();
const [refusing, setRefusing] = useState();
const [withdrawing, setWithdrawing] = useState();
const [scheduling, setScheduling] = useState();
const [callingOff, setCallingOff] = useState();
const [callingOffBusy, setCallingOffBusy] = useState(false);
const [callOffError, setCallOffError] = useState();
const [uploading, setUploading] = useState();
// Orders whose upload finished here but whose images are not counted yet; they are not offered again.
const [importing, setImporting] = useState>(new Set());
const [checkingImport, setCheckingImport] = useState();
const [importCheck, setImportCheck] = useState<{ text: string; bad: boolean }>();
const { reconcileImport, resumeImport, discardImport } = useImagingUploadService();
// A discard is asked of one upload: its order, upload and job, in this practice and sign-in. A replacement upload,
// another practice or another person retires the question and any discard in flight, so neither answers it.
const wpOrganization = useOptionalAuthServiceContext()?.wpOrganization;
const practiceId = wpOrganization?.current?.id;
const [confirmingDiscard, setConfirmingDiscard] = useState();
const [discarding, setDiscarding] = useState();
const letGo = (key?: string) => {
setConfirmingDiscard((held) => (key === undefined || held === key ? undefined : held));
setDiscarding((held) => (key === undefined || held === key ? undefined : held));
};
const discardScope = useSaveScope((practiceId ?? "") + "|" + (authSessionId() ?? ""), () => letGo());
const discardableKeys = useMemo(
() => new Set(rows.map(discardKey).filter((key): key is string => Boolean(key))),
[rows],
);
const discardSaves = useRowOwnedSaves(discardScope, discardableKeys, !loading, letGo);
// The question ends when its upload leaves the list, before anything is pressed: shown again, it is asked afresh.
useLayoutEffect(() => {
setConfirmingDiscard((held) => (held && !discardableKeys.has(held) ? undefined : held));
}, [discardableKeys]);
// Retires an upload its malware check refused, so the study can be uploaded again; the row re-reads either way.
const discardUpload = async (row: DiagnosticOrderQueueRow) => {
const key = discardKey(row);
const orderId = row.orderId;
const jobId = row.imageImport?.jobId;
if (!key || !orderId || !jobId || confirmingDiscard !== key) return;
const save = discardSaves.start(key);
if (!save) return;
const pinned = practiceId;
const owned = {
admit: () => {
save.transport.admit();
if (wpOrganization?.current?.id !== pinned) throw new SaveRetiredError();
},
signal: save.transport.signal,
organizationId: pinned,
};
setDiscarding(key);
setImportCheck(undefined);
try {
const text = await save.step(() => discardImport(jobId, { orderId }, owned), true);
setImportCheck({ text, bad: false });
await save.step(() => recountImages(orderId, owned));
} catch (failure) {
if (isSaveRetired(failure) || !save.belongs()) return;
console.error("Error discarding an image upload:", failure);
setImportCheck({
text: isSaveTimedOut(failure) ? ownFailureSentence(failure) : relayServerFailure(failure, "The upload could not be discarded. Refresh the worklist to see where it stands."),
bad: true,
});
} finally {
if (save.belongs()) letGo(key);
}
};
// A check is asked in this practice and sign-in; a change retires it, so its answer and notice never land elsewhere.
const checkScope = useSaveScope((practiceId ?? "") + "|" + (authSessionId() ?? ""), () => {
setCheckingImport(undefined);
setImportCheck(undefined);
});
// A check belongs to its exact upload and job; one whose row shows another upload is retired for good.
const checkableKeys = useMemo(
() => new Set(rows.map(checkKey).filter((key): key is string => Boolean(key))),
[rows],
);
const checkSaves = useRowOwnedSaves(checkScope, checkableKeys, !loading, (key) => {
setCheckingImport((held) => (held !== undefined && key.startsWith(held + "|") ? undefined : held));
});
// The page has one notice and one busy mark: only the newest check publishes them.
const latestCheck = useRef();
// Files the study if its completion event was lost, or records the failure; either way the row re-reads.
const checkImport = async (row: DiagnosticOrderQueueRow) => {
const orderId = row.orderId;
const upload = row.imageImport;
const key = checkKey(row);
if (!orderId || !upload || !key) return;
const check = checkSaves.start(key);
if (!check) return;
latestCheck.current = check;
const latest = () => check.belongs() && latestCheck.current === check;
const pinned = practiceId;
const owned = {
admit: () => {
check.transport.admit();
if (wpOrganization?.current?.id !== pinned) throw new SaveRetiredError();
},
signal: check.transport.signal,
organizationId: pinned,
};
setCheckingImport(orderId);
setImportCheck(undefined);
try {
// Without a job id the start answer was lost; resuming on the same upload finds or starts that job.
const jobId = upload.jobId ?? await check.step(() => resumeImport(upload.uploadId, { orderId }, owned), true);
if (!jobId) throw new Error("Could not find the job for this upload. Try again.");
const outcome = await check.step(() => reconcileImport(jobId, { orderId }, owned), true);
if (!latest()) return;
// A study not provably this patient's: the upload stays as it is, and so does its Check again.
if (typeof outcome === "object" && "kind" in outcome) {
setImportCheck({ text: outcome.message, bad: true });
return;
}
if (outcome === "processing") setImportCheck({ text: (row.test ?? "The study") + " is still being processed. Check again later.", bad: false });
if (typeof outcome === "object") setImportCheck({ text: outcome.alreadyFiled, bad: true });
// The worklist read is the list's own, under its own owner; this check only admits around it.
await check.step(() => recountImages(orderId));
} catch (failure) {
if (isSaveRetired(failure) || !latest()) return;
console.error("Error checking an image upload:", failure);
setImportCheck({
text: isSaveTimedOut(failure) ? ownFailureSentence(failure) : relayServerFailure(failure, "The upload could not be checked. Refresh the worklist to see where it stands."),
bad: true,
});
} finally {
if (latest()) setCheckingImport(undefined);
}
};
const { markDealtWith } = useRecordOrderRefusal();
const { resumeTransmission } = useSendOutDiagnosticOrder();
// Each opening of the order dialog is its own owner for a retry pressed in it, as are the practice and the sign-in.
const [viewingOpening, setViewingOpening] = useState(0);
useLayoutEffect(() => setViewingOpening((opening) => opening + 1), [viewing]);
const [retrying, setRetrying] = useState();
const retryScope = useSaveScope(`${practiceId ?? ""}|${authSessionId() ?? ""}|${viewingOpening}`, () => setRetrying(undefined));
const lens = lensFor(activeWorkspace);
// The practices this one may send a study to for reading; with none, no study offers it.
const { listReaders } = useImagingReadHandoff();
const [readers, setReaders] = useState([]);
const [sendingToRead, setSendingToRead] = useState();
const [recordingArrival, setRecordingArrival] = useState();
useEffect(() => {
if (lens !== "imaging") return;
let live = true;
listReaders()
.then((found) => { if (live) setReaders(found); })
.catch((failure) => console.error("Error loading imaging readers:", failure));
return () => { live = false; };
}, [lens, listReaders]);
// The heading comes from the same place the rail reads, so the two can never name different queues.
const queueLabel = diagnosticsQueueLabel(activeWorkspace);
// An order of no known kind stays visible under both lenses; neither can claim it.
const [searchParams, setSearchParams] = useSearchParams();
const drill = useMemo(() => diagnosticDrillFrom(searchParams), [searchParams]);
const inLens = useMemo(
() => (lens ? rows.filter((row) => !row.kind || row.kind === lens) : rows)
.filter((row) => !drill || matchesDrill(row, drill)),
[rows, lens, drill],
);
const queue = useMemo(() => sortQueue(inLens), [inLens]);
// Counted across the whole queue: a panel whose tests are at different stages sits in both groups.
const panelSizes = useMemo(() => {
const sizes = new Map();
for (const row of queue) {
if (row.panelId) sizes.set(row.panelId, (sizes.get(row.panelId) ?? 0) + 1);
}
return sizes;
}, [queue]);
const panelOf = (row?: DiagnosticOrderQueueRow) => (row?.panelId ? panelSizes.get(row.panelId) ?? 1 : 1);
const siblingsOf = (row?: DiagnosticOrderQueueRow) => (row?.panelId
? queue.filter((other) => other.panelId === row.panelId && other.orderId !== row.orderId).map(panelStanding)
: []);
const searching = query.trim().toLowerCase();
const shown = searching ? queue.filter((row) => matches(row, searching)) : queue;
// Resulted and reviewed are the shared tail of both machines, so "is this still work" is asked by name.
const SETTLED: readonly string[] = ["resulted", "reviewed"];
const outstanding = shown.filter((row) => !row.state || !SETTLED.includes(row.state));
const settled = shown.filter((row) => !outstanding.includes(row));
// Opening a follow-on dialog closes the order it came from, so one dialog is on screen at a time.
const from = (open: (row: DiagnosticOrderQueueRow) => void) => (row: DiagnosticOrderQueueRow) => {
setViewing(undefined);
setViewingResult(undefined);
open(row);
};
const onEnter = (row: DiagnosticOrderQueueRow) => {
if (row.kind === "laboratory") {
if (!permissionsSettled || !canManage("diagnostics")) return;
setEntering(row);
}
else navigate(`/dashboard/diagnostic-orders/${row.orderId}/result`);
};
/** A refusal dealt with re-reads the queue: the answer to "is this still stopped" genuinely moved. */
const onRefusalDealtWith = async (row: DiagnosticOrderQueueRow) => {
if (!row.orderId) return;
try {
await markDealtWith(row.orderId);
setViewing(undefined);
await reload();
} catch (failure) {
console.error("Error clearing the laboratory refusal:", failure);
}
};
/**
* The same electronic send handed on again; the server says when it has started or can no longer go. It belongs to
* the order dialog it was pressed in, this practice and this sign-in: closing or reopening the dialog, or either
* changing, retires it, so nothing late reaches a later dialog and a retry not yet sent never goes.
*/
const onRetryTransmission = async (row: DiagnosticOrderQueueRow) => {
const orderId = row.orderId;
if (!orderId || retrying) return;
const operation = retryScope.start();
setRetrying(orderId);
setAdvanceError(undefined);
try {
const answer = await operation.step(() => resumeTransmission(orderId, operation.transport), true);
// An Akashic delivery is done once the laboratory has it; an electronic send once it is queued.
const stillOwed = row.deliveryOwed ? !answer?.akashic?.deliveredAt : answer?.transmission !== "queued";
setViewing(undefined);
setAdvanceError(!stillOwed ? undefined : row.deliveryOwed
? "The laboratory has still not received this order. Try again shortly."
: "The electronic send is still not confirmed. Try again shortly, or send the requisition with the specimen.");
void reload();
} catch (err) {
if (isSaveRetired(err) || !operation.belongs()) return;
console.error("Error retrying an electronic send:", err);
setAdvanceError(outcomeUnknown(err)
? `Whether the ${row.deliveryOwed ? "delivery" : "electronic send"} went through could not be confirmed. Reload to check before trying again.`
: isSaveTimedOut(err) ? ownFailureSentence(err) : relayServerFailure(err, `Could not retry the ${row.deliveryOwed ? "delivery" : "electronic send"}. Reload and check the order.`));
} finally {
if (operation.belongs()) setRetrying(undefined);
}
};
const onAdvance = async (row: DiagnosticOrderQueueRow, state: DiagnosticOrderState) => {
if (!row.orderId) return;
setBusyOrderId(row.orderId);
setAdvanceError(undefined);
try {
// Collection moves the order only; the accession is minted when the laboratory receives the tube.
await advance(row.orderId, state);
setViewing(undefined);
} catch (err) {
console.error("Error advancing a lab order:", err);
setAdvanceError("Could not update that order. It may have moved on already — reload and check.");
} finally {
setBusyOrderId(undefined);
}
};
/**
* One next step per row; everything else is inside the order. A collected specimen is sent out
* from here and resulted in-house at the bench; under imaging a study moves on to its next stage.
*/
const rowAction = (row: DiagnosticOrderQueueRow, isSettled: boolean) => {
if (isSettled) {
return ;
}
// An entered result is this test's next step, whatever its siblings are doing.
if (row.awaitingRelease && row.orderId && (row.kind !== "laboratory" || mayEnterLab)) {
return ;
}
if (onTheBench(row) && row.orderId && mayEnterLab) {
return ;
}
if (awaitingRedraw(row)) {
return ;
}
if (row.refusal) {
return ;
}
const moves = row.orderId ? nextStatesFor(row.kind, row.state) : [];
if (row.state === "collected" && !row.sentOut && !row.akashicRoute && moves.includes("sent")
&& (row.kind !== "laboratory" || mayEnterLab)) {
return ;
}
const move = lens === "imaging" && row.state ? DIRECT_MOVE[row.state] : undefined;
if (move && moves.includes(move.to)) {
return (
);
}
return ;
};
// A booked study never returns to the schedule page, so its row calls it off through the same confirmation.
const callOff = async (reason: ArchiveReason, note?: string) => {
if (!callingOff?.orderId) return;
setCallingOffBusy(true);
setCallOffError(undefined);
try {
await cancelStudy(callingOff.orderId, reason, note);
setCallingOff(undefined);
} catch (refusal) {
setCallOffError(relayServerFailure(refusal, "Could not cancel this study. It may have moved on already."));
} finally {
setCallingOffBusy(false);
}
};
/** The worklist move: booking opens over the worklist, the machine step is one press, and upload waits on a count. */
const imagingAction = (row: DiagnosticOrderQueueRow) => {
const moves = row.orderId ? nextStatesFor(row.kind, row.state) : [];
if (row.state === "ordered" && moves.includes("scheduled")) {
return ;
}
if (row.state === "scheduled" && moves.includes("performed")) {
return (
<>
>
);
}
// A finished import its malware check refused may be discarded, once asked twice; still checking never is.
if (discardKey(row)) {
const key = discardKey(row);
const busy = Boolean(key) && (discarding === key || checkingImport === row.orderId);
return key && confirmingDiscard === key && discardableKeys.has(key) ? (
<>
>
) : (
);
}
// The order remembers an upload across a reload: a pending one is checked, never uploaded twice.
if (row.state === "performed" && row.orderId && row.images?.instances === 0 && row.imageImport?.state === "pending") {
return (
);
}
// A failure the server confirmed outranks this page's own memory of having just uploaded.
const failed = row.imageImport?.state === "failed";
if (row.state === "performed" && row.orderId && row.images?.instances === 0 && row.imageImport?.state !== "pending" && (failed || !importing.has(row.orderId))) {
return (
);
}
// Going to an outside reader: nothing more to send until somebody records what they said.
if (row.state === "performed" && row.orderId && row.outsideRead && !row.outsideRead.confirmation && !row.resultOnFile) {
return ;
}
if (sendsToBeRead(row) && (readers.length || row.outsideRead)) {
return (
);
}
return ;
};
const imagingRow = (row: DiagnosticOrderQueueRow) => (
);
const queueRow = (settledRow: boolean) => (row: DiagnosticOrderQueueRow) => (
);
const staged = lens === "imaging";
const unstaged = staged ? outstanding.filter((row) => !IMAGING_STAGES.some((stage) => stage.state === row.state)) : outstanding;
// Scheduling is its own page, as imaging.html draws it; the worklist returns once it is saved or left.
if (scheduling?.orderId) {
return (
setScheduling(undefined)}
onSchedule={(orderId, schedule) => advance(orderId, "scheduled", schedule)}
onCancel={cancelStudy}
/>
);
}
return (
{queueLabel.heading}
{lens === "laboratory" ? (
) : null}
{queueLabel.subtitle}
{/* Above the queue, because results nobody has acknowledged are what should be dealt with first. */}
{drill ? (