import { useCallback, useEffect, useRef, useState } from "react"; import { AlertCircle } from "lucide-react"; import { Card, CardContent } from "../../../components/ui/card.tsx"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Badge } from "../../../components/ui/badge.tsx"; import { Button } from "../../../components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../components/ui/dialog.tsx"; import { Input } from "../../../components/ui/input.tsx"; import { Label } from "../../../components/ui/label.tsx"; import { useAuthServiceContext } from "../../../services"; import { useImagingReadHandoff, type ReadRequest } from "../../../hooks/useImagingReadHandoff.ts"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import { formatDate, formatDateTime } from "../../../utils/dateUtils"; import { useSaveScope, type SaveOperation } from "../../../utils/saveOperation"; import { viewerEpoch } from "../../../utils/writeThroughHold"; import { SEP } from "../DiagnosticOrdersPage/ImagingWorklist.tsx"; // STUDIES OTHER PRACTICES HAVE ASKED THIS ONE TO READ. The patient is shown as the sender recorded them, // so the reader can check the name before accepting; accepting puts the study on "To read". const LOAD_FAILED = "The studies other practices sent to be read could not be loaded."; const PARTIAL = "Not every study sent to be read could be listed, so there may be more than shown."; const NOT_REFRESHED = "The list could not be refreshed, so what is shown may be out of date."; /** One send of a request: a later send of the same request is a new attempt, and is offered again. */ const attemptOf = (request: ReadRequest) => `${request.requestId}|${request.sentAt ?? ""}`; type Shown = { owner: string; requests: ReadRequest[]; truncated: boolean; current: boolean }; /** Attempts answered here, by viewer and practice: kept by the page, so a remount while it reloads forgets none. */ export type AnsweredAttempts = Readonly>; const ReadRequests = ({ onAccepted, answered, onAnswered }: { onAccepted: (readerOrderId?: string) => void; answered: AnsweredAttempts; onAnswered: (scopeKey: string, attempt: string) => void; }) => { const { listReadRequests, accept, decline } = useImagingReadHandoff(); const { wpOrganization } = useAuthServiceContext() as { wpOrganization?: { current?: { id?: string } } }; const practiceId = wpOrganization?.current?.id; // Whose list this is, as rendered now: another practice, viewer or later opening never sees it, not for one render. const opening = useRef<{ practiceId?: string; n: number }>({ practiceId, n: 0 }); if (opening.current.practiceId !== practiceId) opening.current = { practiceId, n: opening.current.n + 1 }; const scopeKey = `${viewerEpoch()}|${practiceId ?? ""}`; const owner = `${scopeKey}|${opening.current.n}`; const scope = useSaveScope(practiceId); const [shown, setShown] = useState(); const [failure, setFailure] = useState<{ owner: string }>(); const [reading, setReading] = useState<{ owner: string; seq: number }>(); const readSeq = useRef(0); const [actionError, setActionError] = useState<{ owner: string; message: string }>(); const [busyFor, setBusyFor] = useState<{ owner: string; requestId: string }>(); const [decliningFor, setDecliningFor] = useState<{ owner: string; request: ReadRequest }>(); const [reason, setReason] = useState(""); const [declineError, setDeclineError] = useState(); const sendFor = (operation: SaveOperation) => ({ ...operation.transport, organizationId: practiceId }); const load = useCallback(async () => { const operation = scope.start(); const seq = ++readSeq.current; const forOwner = owner; const ours = () => operation.belongs() && seq === readSeq.current; setReading({ owner: forOwner, seq }); try { const answer = await listReadRequests(sendFor(operation)); if (!ours()) return; setShown({ owner: forOwner, requests: answer.requests, truncated: answer.truncated, current: true }); setFailure(undefined); } catch (error) { if (!ours()) return; console.error("Error loading read requests:", error); // Any failure, a missing route or an unreachable server included, is a list not read, never an empty one. setFailure({ owner: forOwner }); setShown((known) => (known && known.owner === forOwner ? { ...known, current: false } : known)); } finally { setReading((now) => (now?.seq === seq ? undefined : now)); } // eslint-disable-next-line react-hooks/exhaustive-deps -- owner and practice are read as rendered, through the scope }, [listReadRequests, scope, owner, practiceId]); useEffect(() => { void load(); }, [load]); const onAccept = async (request: ReadRequest) => { if (!answerable(request)) return; const operation = scope.start(); const forOwner = owner; const forScope = scopeKey; const mine = { owner, requestId: request.requestId }; setBusyFor(mine); setActionError(undefined); let accepted: { readerOrderId?: string } | undefined; try { accepted = await accept(request.requestId, sendFor(operation)); } catch (error) { console.error("Error accepting the read request:", error); if (operation.belongs()) setActionError({ owner: forOwner, message: relayServerFailure(error, "The study could not be accepted. Please try again.") }); if (operation.belongs()) setBusyFor((now) => (now === mine ? undefined : now)); return; } if (!operation.belongs()) return; // Accepted: that is the answer, whatever the list says next. Its row is not offered again, and it is never resent. onAnswered(forScope, attemptOf(request)); onAccepted(accepted?.readerOrderId); setBusyFor((now) => (now === mine ? undefined : now)); await load(); }; const onDecline = async () => { if (!declining || !answerable(declining)) return; const request = declining; const operation = scope.start(); const forScope = scopeKey; const mine = { owner, requestId: request.requestId }; setBusyFor(mine); setDeclineError(undefined); try { await decline(request.requestId, reason.trim(), sendFor(operation)); } catch (error) { console.error("Error declining the read request:", error); if (operation.belongs()) { setDeclineError(relayServerFailure(error, "The study could not be declined. Please try again.")); setBusyFor((now) => (now === mine ? undefined : now)); } return; } if (!operation.belongs()) return; onAnswered(forScope, attemptOf(request)); setDecliningFor(undefined); setBusyFor((now) => (now === mine ? undefined : now)); await load(); }; // Busy only for an answer started under the owner shown now: a late one from another practice frees nothing here. const busy = busyFor?.owner === owner ? busyFor.requestId : undefined; // A decline opened for another practice or viewer is not offered here. const opened = decliningFor?.owner === owner ? decliningFor.request : undefined; const list = shown?.owner === owner ? shown : undefined; const failed = failure?.owner === owner; const loading = reading?.owner === owner; const done = new Set(answered[scopeKey] ?? []); const rows = (list?.requests ?? []).filter((request) => !done.has(attemptOf(request))); // Answerable only while a current read still lists this exact attempt: an open dialog is held to the same rule. function answerable(request: ReadRequest) { return Boolean(list?.current) && rows.some((row) => attemptOf(row) === attemptOf(request)); } // A dialog whose attempt a current read no longer lists is closed; one over a list not refreshed waits. const declining = opened && (!list?.current || answerable(opened)) ? opened : undefined; const closedByRead = Boolean(opened && !declining); useEffect(() => { if (closedByRead) setDecliningFor(undefined); }, [closedByRead]); // Only an answer read now, whole, with no newer read pending, may stand for the full list. const whole = Boolean(list?.current && !list.truncated && !loading); const stale = Boolean(list && !list.current); // Drawn only when a whole, current answer says there is nothing to answer: most practices are never sent a study. if (whole && !rows.length && !failed && !loading && !(actionError?.owner === owner)) return null; return (

{whole ? : null} Asked to read {list && !whole ? · Showing {rows.length} : null}

{actionError?.owner === owner && ( {actionError.message} )} {!list && loading && !failed ?

Checking for studies sent to be read…

: null} {list && loading ?

Checking for changes…

: null} {failed || (list && list.truncated) ? ( {failed ? (list ? NOT_REFRESHED : LOAD_FAILED) : PARTIAL}{" "} ) : null} {rows.map((request) => (
{request.accession ? {request.accession} : null} {request.test ?? "Study not named"} {request.modality ? {request.modality} : null}

{[ "From " + (request.from ?? "another practice"), (request.patientName ?? "Patient not named") + (request.patientBirthDate ? ", born " + formatDate(request.patientBirthDate) : ""), request.sentAt ? "Sent " + formatDateTime(request.sentAt) : undefined, ].filter(Boolean).join(SEP)}

{request.question ?

Why it was ordered: {request.question}

: null}
{/* A row from a list that could not be refreshed is not answered until a current read has it. */}
))}
{declining ? ( { if (!open && !busy) setDecliningFor(undefined); }}> Decline this study {[declining.test ?? "This study", "from " + (declining.from ?? "another practice")].join(SEP)}. They can send it elsewhere. {declineError && ( {declineError} )}
setReason(e.target.value)} />
{!answerable(declining) ? (

This study can be declined once the list has been read again.

) : null}
) : null}
); }; export default ReadRequests;