import { useCallback } from "react"; import { useHttpService, type RequestAdmission } from "../services/HttpService.tsx"; import { environment } from "../environments/environment.ts"; import type { OutsideRead, ReadElsewhere } from "./useDiagnosticOrderQueue.ts"; /** Another Akashic practice this one is connected to for reading its studies. */ export interface ImagingReader { readerId: string; reader?: string; } /** A study another practice has asked this one to read, not yet answered. */ export interface ReadRequest { requestId: string; from?: string; test?: string; modality?: string; accession?: string; patientName?: string; patientBirthDate?: string; /** Why it was ordered, in the orderer's words. */ question?: string; sentAt?: string; } /** The list as the server answered it: `truncated` means it could not list every study it was asked to read. */ export interface ReadRequestList { requests: ReadRequest[]; truncated: boolean; } /** Who a request is sent for: its cancellation, its admission, and the practice it is about. */ export interface ReadRequestScope { signal?: AbortSignal; admit?: RequestAdmission; organizationId?: string; } /** A list answer that is not a list of requests with ids, or whose completeness is not a boolean. */ export class ReadRequestListUnreadableError extends Error { constructor() { super("The studies sent to be read came back in a form that cannot be read."); this.name = "ReadRequestListUnreadableError"; } } const DISPLAY_FIELDS = ["from", "test", "modality", "accession", "patientName", "patientBirthDate", "question", "sentAt"] as const; // A usable request: an id, and every display field it carries is text. One that is not makes the list unreadable. const isRequest = (value: unknown): value is ReadRequest => { if (typeof value !== "object" || value === null) return false; const candidate = value as Record; return typeof candidate.requestId === "string" && candidate.requestId.trim().length > 0 && DISPLAY_FIELDS.every((field) => candidate[field] === undefined || typeof candidate[field] === "string"); }; const scoped = (scope: ReadRequestScope = {}) => ({ ...(scope.signal ? { signal: scope.signal } : {}), ...(scope.admit ? { admit: scope.admit } : {}), ...(scope.organizationId ? { organizationId: scope.organizationId } : {}), }); export const useImagingReadHandoff = () => { const { get, post, delete: del } = useHttpService(); const listReaders = useCallback(async () => { const response = await get<{ readers?: ImagingReader[] }>(`${environment.fhirClinicalApi}/imaging-reader`); return response?.readers ?? []; }, [get]); const sendForReading = useCallback( (orderId: string, readerId: string) => post<{ orderId: string; route: ReadElsewhere & { readerId: string } }>( `${environment.fhirClinicalApi}/diagnostic-order/${orderId}/read-elsewhere`, { readerId }, ), [post], ); // A reader that is not on Akashic: recorded, never delivered by this call. const sendForOutsideRead = useCallback( (orderId: string, outside: { reader: string; npi?: string; contact?: string; method: OutsideRead["method"]; note?: string }) => post<{ outsideRead: OutsideRead }>(`${environment.fhirClinicalApi}/diagnostic-order/${orderId}/read-outside`, outside), [post], ); const confirmOutsideRead = useCallback( (orderId: string, answer: { outcome: "received" | "not-received"; note?: string }) => post<{ outsideRead: OutsideRead }>(`${environment.fhirClinicalApi}/diagnostic-order/${orderId}/read-outside-confirmation`, answer), [post], ); // A missing completeness flag is the server's ordinary whole answer; anything else unreadable is no answer at all. const listReadRequests = useCallback(async (scope?: ReadRequestScope): Promise => { const response = await get<{ requests?: unknown; truncated?: unknown }>(`${environment.fhirClinicalApi}/read-request`, scoped(scope)); if (!response || typeof response !== "object" || !Array.isArray(response.requests) || !response.requests.every(isRequest) || (response.truncated !== undefined && typeof response.truncated !== "boolean")) { throw new ReadRequestListUnreadableError(); } return { requests: response.requests, truncated: response.truncated === true }; }, [get]); const accept = useCallback( (requestId: string, scope?: ReadRequestScope) => post<{ requestId?: string; readerOrderId?: string }>(`${environment.fhirClinicalApi}/read-request/${requestId}/accept`, {}, scoped(scope)), [post], ); const decline = useCallback( (requestId: string, reason: string, scope?: ReadRequestScope) => post(`${environment.fhirClinicalApi}/read-request/${requestId}/decline`, { reason }, scoped(scope)), [post], ); const addReader = useCallback( (npi: string) => post(`${environment.fhirClinicalApi}/imaging-reader`, { npi }), [post], ); const removeReader = useCallback( (readerId: string) => del(`${environment.fhirClinicalApi}/imaging-reader/${encodeURIComponent(readerId)}`), [del], ); return { listReaders, addReader, removeReader, sendForReading, sendForOutsideRead, confirmOutsideRead, listReadRequests, accept, decline }; };