import { isAsOfIncomplete } from '../utils/searchTruncation'; import { useEffect, useState, useCallback } from 'react'; import { usePatientServiceContext, useProcedureServiceContext } from '../services'; import { describeFetchFailure } from '../utils/fetchFailureMessage'; import { patientCacheGuard } from '../utils/cacheOwner'; interface UseFhirProceduresOptions { autoLoad?: boolean; errorMessage?: string; forceReload?: boolean; } interface UseFhirProceduresReturn { /** Served as of a cutoff and not in full: what it left out is unknown, never none. */ incomplete: boolean; loading: boolean; error: string | undefined; /** * - was the server actually asked? * * Every hook of this shape early-returns while `patientState` is unresolved, so `setLoading(true)` * never runs and the effect never fires. The caller is then holding `loading: false` - `error: * undefined` - an empty list - **indistinguishable from a load that succeeded and found nothing** - * and `ResourceListStatus` says *"No procedures found."* about a question nobody asked. * The hook is the only thing that knows which of the two it is in, so it reports it. */ attempted: boolean; loadProcedures: () => Promise; clearError: () => void; proceduresBundle: ReturnType['proceduresBundle']; proceduresList: ReturnType['proceduresList']; } export const useFhirProcedures = (options: UseFhirProceduresOptions = {}): UseFhirProceduresReturn => { const { autoLoad = true, errorMessage = "Failed to load procedures", forceReload = false } = options; const { patientState } = usePatientServiceContext(); const { fetchProceduresWithPolling, proceduresBundle, proceduresList, cachedForPatientId } = useProcedureServiceContext(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const loadProcedures = useCallback(async () => { if (!patientState) { return; } setLoading(true); setError(undefined); try { await fetchProceduresWithPolling(patientState); } catch (err) { console.error('Failed to load procedures:', err); setError(describeFetchFailure(err, errorMessage)); } finally { setLoading(false); } }, [patientState, fetchProceduresWithPolling, errorMessage]); const clearError = useCallback(() => { setError(undefined); }, []); useEffect(() => { if (!autoLoad) return; if (!patientState) return; // A bundle belonging to SOMEBODY ELSE is not a reason not to ask. This used to fire only when // there was no bundle at all, so moving between two charts left the previous patient's list in // place, with no request and nothing on screen to say it was not theirs. // NOT a refetch on every navigation: same patient, bundle loaded, no request. if (!proceduresBundle || cachedForPatientId !== patientState.id || forceReload) { loadProcedures(); } }, [autoLoad, patientState, proceduresBundle, cachedForPatientId, forceReload, loadProcedures]); // The cache outlives a change of patient: hand out only the patient asked about. const guard = patientCacheGuard(cachedForPatientId, patientState?.id); return { loading: guard.loading(loading, Boolean(error)), error, // Derived from the SAME condition every early return above tests, not tracked separately. A // second flag set by hand in the load path would go stale the first time somebody added a third // reason to bail, and it would go stale silently. attempted: Boolean(patientState), loadProcedures, clearError, // Served as of a cutoff and not in full: what it left out is unknown, never none. incomplete: isAsOfIncomplete(guard.bundle(proceduresBundle, null)), proceduresBundle: guard.bundle(proceduresBundle, null), proceduresList: guard.list(proceduresList) }; };