import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useHttpService } from '../services/HttpService.tsx'; import { environment } from '../environments/environment.ts'; /** * The results this practitioner ordered that have come back and nobody has acknowledged. * * WHY IT IS NOT THE LAB QUEUE. That one is org-wide and needs `diagnostics: view`, a permission a * practitioner reviewing their own patient's result has no reason to hold and which the brief keeps * for the lab and imaging work rather than for the order. This route is scoped on the SERVER to the * caller's own requester reference, so there is nothing to filter here and nothing to widen by asking. * * `undefined` UNTIL THE FIRST ANSWER ARRIVES, NEVER `[]`. An empty array would let the card print * "Nothing waiting on you" over a list that has not loaded, and that sentence is the one output of * this whole card that claims something rather than reporting it. * * A FAILED POLL LEAVES THE LAST GOOD ANSWER ALONE, for the same reason the drafts poll beside it does: * a refresh that blanked the row mid-visit would replace a true count with a loading state every time * the network hiccupped. */ export interface ResultAwaitingReview { orderId?: string; kind?: 'laboratory' | 'imaging'; test?: string; priority?: string; authoredOn?: string; patientId?: string; patientDisplay?: string; } /** * What a caller gets back when we asked and could not find out. * * A THIRD STATE, BECAUSE TWO WERE NOT ENOUGH. `undefined` means the answer has not arrived yet and * the caller should wait; `[]` means we asked and there are none. A request that cannot complete is * neither: waiting for it is waiting for ever, and calling it empty asserts a clinician has no * results when nobody managed to look. */ export const RESULTS_UNREADABLE = 'unreadable'; export type ResultsAwaitingReview = ResultAwaitingReview[] | typeof RESULTS_UNREADABLE | undefined; /** The same ten minutes the owed-work card's other sources poll on. */ const REFETCH_MS = 10 * 60 * 1000; export const useResultsAwaitingReview = (enabled: boolean): { results: ResultsAwaitingReview; partial: boolean } => { const { get } = useHttpService(); const [results, setResults] = useState(undefined); // The server stopped reading before its last page: the rows it sent are real, the list is not whole. const [partial, setPartial] = useState(false); const mountedRef = useRef(true); useEffect(() => () => { mountedRef.current = false; }, []); const load = useCallback(() => { // Not a practitioner: an empty list is the HONEST answer rather than a guess. Only a // practitioner can have ordered anything, so there is nothing for this caller by construction - // unlike a failed fetch below, where we asked and did not find out. The two look identical from // outside and must not be treated the same: this one may contribute to an all-clear. if (!enabled) { setResults([]); setPartial(false); return; } void get<{ orders?: ResultAwaitingReview[]; truncated?: boolean }>( `${environment.fhirBaseApi}/diagnostic-order/awaiting-review`, ) .then((response) => { if (!mountedRef.current) return; setResults(response?.orders ?? []); setPartial(response?.truncated === true); }) // MEASURED IN THE BROWSER, and it is why this branch exists at all. The route was called // before it was deployed, the gateway answered the preflight with a 403 carrying no CORS // headers, and the browser reported a bare fetch failure with no status to read. Swallowing // that left this source at `undefined` for ever - which withheld the card's all-clear // permanently and made the whole "Waiting on you" card disappear. // // IT DOES NOT CLEAR A GOOD ANSWER. A later failed poll leaves the last real list standing, // the same posture the drafts poll beside it takes: a network hiccup must not replace a // true count with anything. .catch(() => { if (mountedRef.current) { setResults((current) => (Array.isArray(current) ? current : RESULTS_UNREADABLE)); } }); }, [enabled, get]); useEffect(() => { load(); const refetch = window.setInterval(load, REFETCH_MS); return () => window.clearInterval(refetch); }, [load]); return useMemo(() => ({ results, partial }), [results, partial]); };