import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { AlertCircle, Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Button } from "../../../components/ui/button.tsx"; import { Label } from "../../../components/ui/label.tsx"; import { Tabs, TabsList, TabsTrigger } from "../../../components/ui/tabs.tsx"; import { useDiagnosticOrderQueue, type DiagnosticOrderQueueRow, } from "../../../hooks/useDiagnosticOrderQueue.ts"; import { alreadySigned, awaitingRead, awaitingSignature, outsideReportDueFrom, workspaceReadsStudies, } from "../../../utils/diagnosticReading.ts"; import { useAuthServiceContext, useStaffPermissionServiceContext } from "../../../services"; import { useHttpService } from "../../../services/HttpService.tsx"; import { environment } from "../../../environments/environment.ts"; import ReleaseResultDialog from "../DiagnosticOrdersPage/ReleaseResultDialog.tsx"; import AddAddendumDialog from "../DiagnosticOrdersPage/AddAddendumDialog.tsx"; import { ImagingGroup, SEP, bookedAt, elapsed, orderedMethod } from "../DiagnosticOrdersPage/ImagingWorklist.tsx"; import { isDiagnosticOrderState } from "../../../utils/diagnosticOrderQueue.ts"; import ReadRequests, { type AnsweredAttempts } from "./ReadRequests.tsx"; import "../BenchPage/BenchPage.css"; import "../DiagnosticOrdersPage/ImagingWorklist.css"; import "./ReadingPage.css"; // THE READING LIST: studies taken, and what the person reading them still owes each one. A section of // the Practitioner workspace, filtered from the same queue the technologists work, never re-fetched. // Unread, the reader's own drafts, and what went out today, each grouped by priority, oldest first. type View = "unread" | "mine" | "reported"; const MINUTE_MS = 60000; const isUrgent = (row: DiagnosticOrderQueueRow) => row.priority === "stat" || row.priority === "urgent"; const timeOf = (iso?: string) => (iso ? new Date(iso).getTime() : NaN); const minutesSince = (iso: string | undefined, now: number) => Number.isNaN(timeOf(iso)) ? undefined : Math.max(0, Math.floor((now - timeOf(iso)) / MINUTE_MS)); const today = (iso: string | undefined, now: number) => Boolean(iso) && new Date(iso as string).toDateString() === new Date(now).toDateString(); /** What each view waits on, for its age and its order: taken, written, or signed. */ const sinceFor = (view: View, row: DiagnosticOrderQueueRow) => view === "unread" ? row.performed?.at ?? row.authoredOn : view === "mine" ? row.report?.writtenAt : row.report?.releasedAt; const oldestFirst = (view: View) => (a: DiagnosticOrderQueueRow, b: DiagnosticOrderQueueRow) => (timeOf(sinceFor(view, a)) || 0) - (timeOf(sinceFor(view, b)) || 0); type Tone = "wait" | "go" | "done" | "bad" | "warn"; const TONE_CLASS: Record = { wait: "ak-row__state", go: "ak-row__state imaging-row__state--go", done: "ak-row__state ak-row__state--good", bad: "ak-row__state ak-row__state--bad", warn: "ak-row__state ak-row__state--warn", }; const standing = (view: View, row: DiagnosticOrderQueueRow): { word: string; tone: Tone } => { if (view === "reported") return { word: row.report?.addendedAt ? "Signed · addendum" : "Signed", tone: "done" }; if (view === "mine") return { word: "Draft · you", tone: "go" }; if (row.awaitingRelease) return { word: "Drafted", tone: "warn" }; if (outsideReportDueFrom(row)) return { word: "Report due", tone: "wait" }; if (row.openedBy?.byYou) return { word: "Open · you", tone: "go" }; if (row.openedBy) return { word: `Open · ${row.openedBy.name ?? "another reader"}`, tone: "warn" }; if (row.priority === "stat") return { word: "STAT", tone: "bad" }; if (row.priority === "urgent") return { word: "Urgent", tone: "bad" }; return { word: "Unread", tone: "wait" }; }; // Said as the technologist's worklist says it, so one study reads the same on both screens. const methodOf = (row: DiagnosticOrderQueueRow) => row.modality ?? orderedMethod(row.test); // Absent when the server could not count them, which is not the same as none. const priorsText = (priors?: number) => priors === undefined ? undefined : priors === 0 ? "no priors" : `${priors} prior${priors === 1 ? "" : "s"}`; const detail = (row: DiagnosticOrderQueueRow, now: number) => [ [row.patientDisplay ?? "Patient not named", row.patientAge].filter((part) => part !== undefined).join(", "), row.question?.trim() || undefined, priorsText(row.priors), row.readFor ? "from " + (row.readFor.from ?? "another practice") : undefined, // The sending practice's own number, beside this practice's accession: only when the queue carries one. row.theirOrderNumber ? "Sender’s order " + row.theirOrderNumber : undefined, outsideReportDueFrom(row) ? "report due from " + outsideReportDueFrom(row) + ", not on Akashic" : undefined, row.performed?.at ? "performed " + bookedAt(row.performed.at, now) : undefined, row.orderedBy ? "ordered by " + row.orderedBy : undefined, ].filter(Boolean).join(SEP); const ReadingRow = ({ view, row, action, now }: { view: View; row: DiagnosticOrderQueueRow; action?: React.ReactNode; now: number }) => { const { word, tone } = standing(view, row); const since = sinceFor(view, row); const age = view === "reported" ? bookedAt(since, now) : elapsed(minutesSince(since, now)); return (
{row.accession ?? "—"} {methodOf(row) ?? "—"} {row.test ?? "Study not named"} {detail(row, now)} {age ?? "—"} {word} {action}
); }; const ReadingPage = () => { const navigate = useNavigate(); const { post } = useHttpService(); const { rows, loading, error, reload, waitForOrder, markReleased } = useDiagnosticOrderQueue(); const { activeWorkspace, canManage } = useStaffPermissionServiceContext(); // The server takes a report only from a reader holding diagnostics, so nobody else is offered one; // reading is clinical work, so nobody switched into another person's account is offered it either. const { actingAs } = useAuthServiceContext(); const mayReport = !actingAs && workspaceReadsStudies(activeWorkspace) && canManage("diagnostics"); const [view, setView] = useState("unread"); const [modality, setModality] = useState(""); // Read requests answered here, kept above the panel, which is redrawn whenever this list reloads. const [answeredAttempts, setAnsweredAttempts] = useState({}); const [releasing, setReleasing] = useState(); const [adding, setAdding] = useState(); const now = Date.now(); const shown = useMemo(() => rows.filter((row) => !modality || methodOf(row) === modality), [rows, modality]); const modalities = useMemo( () => [...new Set(rows.filter((row) => row.kind === "imaging").map(methodOf).filter((each): each is string => Boolean(each)))].sort(), [rows], ); const signed = useMemo(() => shown.filter(alreadySigned), [shown]); const lists: Record = useMemo(() => ({ // A draft somebody else wrote is still unread by this reader, and still owed. unread: shown.filter((row) => awaitingRead(row) || (awaitingSignature(row) && !row.report?.writtenByYou)), mine: shown.filter((row) => awaitingSignature(row) && row.report?.writtenByYou), reported: signed.filter((row) => today(row.report?.releasedAt, now)), }), [shown, signed, now]); const earlier = signed.filter((row) => !today(row.report?.releasedAt, now)).sort(oldestFirst("reported")).reverse(); const reportedToday = lists.reported; const lastReported = [...reportedToday].sort(oldestFirst("reported")).pop()?.report?.releasedAt; const zero: Record = { unread: "Nothing is waiting on a report. " + (reportedToday.length ? reportedToday.length + (reportedToday.length === 1 ? " study was" : " studies were") + " reported today, the last at " + bookedAt(lastReported, now) + "." : "None has been reported today."), mine: "Nothing you have written is waiting to be signed.", reported: "Nothing has been reported today.", }; // Recorded on the way in so the next reader is warned; never awaited, so a slow or failed record // never keeps anybody from the study. const open = (row: DiagnosticOrderQueueRow) => { if (actingAs) return; Promise.resolve(post(`${environment.fhirClinicalApi}/diagnostic-order/${row.orderId}/opened`, {})) // Recording the open is best effort; the result opens either way. .catch(() => undefined); navigate(`/dashboard/diagnostic-orders/${row.orderId}/result`); }; const action = (row: DiagnosticOrderQueueRow) => { if (!mayReport) return null; if (view === "reported" || alreadySigned(row)) { return ; } const read = (className: string) => ( ); // Read at the outside practice; what is left here is typing in their report, so no opening is recorded. if (!row.awaitingRelease && outsideReportDueFrom(row)) { return ( ); } if (row.awaitingRelease) { return ( <> {read("btn btn-blue-ghost-sm")} ); } return read("btn btn-blue-sm"); }; const rowFor = (key: View) => (each: DiagnosticOrderQueueRow) => ( ); const current = [...lists[view]].sort(oldestFirst(view)); const urgent = current.filter(isUrgent); const routine = current.filter((each) => !isUrgent(each)); const TITLES: Record = { unread: "Unread", mine: "Mine", reported: "Reported today" }; return (

Reading

Studies waiting on a report, oldest first within each priority.

{error && ( {error} )} {!mayReport && !loading ? (

Reports are written and signed in the Practitioner workspace, by a reader who manages diagnostics.

) : null} {loading ? (

Loading studies…

) : ( <> {mayReport ? ( void (readerOrderId ? waitForOrder(readerOrderId) : reload())} answered={answeredAttempts} onAnswered={(scopeKey, attempt) => setAnsweredAttempts((now) => ({ ...now, [scopeKey]: [...(now[scopeKey] ?? []), attempt] }))} /> ) : null}
setView(next as View)}> Unread · {lists.unread.length} Mine · {lists.mine.length} Reported today · {lists.reported.length} {modalities.length > 1 ? ( ) : null}
{current.length === 0 ? ( {rowFor(view)} ) : ( <> {urgent.length ? {rowFor(view)} : null} {routine.length ? {rowFor(view)} : null} )} {/* Kept so an addendum to an older report is still reached from here. */} {view === "reported" && earlier.length ? ( {rowFor("reported")} ) : null} )}
setReleasing(undefined)} onReleased={(state, returned) => { if (releasing?.orderId) { markReleased(releasing.orderId, isDiagnosticOrderState(state) ? state : undefined, returned); } }} /> setAdding(undefined)} />
); }; export default ReadingPage;