import { useChartRecordStanding, useFreshAfterReauthorization, type RecordAccessStanding, type RecordFreshness } from './useRecordAccessStanding'; import { isAsOfIncomplete, isServedAtCutoff } from '../utils/searchTruncation'; import { useEffect, useState, useCallback, useRef } from 'react'; import { usePatientServiceContext, useMedicationRequestServiceContext } from '../services'; import { describeFetchFailure, isRefusedRead } from '../utils/fetchFailureMessage'; import { patientCacheGuard } from '../utils/cacheOwner'; interface UseFhirMedicationRequestsOptions { /** The record standing to answer to; the open chart's when not given. */ standing?: RecordAccessStanding; autoLoad?: boolean; errorMessage?: string; forceReload?: boolean; } interface UseFhirMedicationRequestsReturn { /** After a reauthorization, whether this list has been read again under it. */ freshness: RecordFreshness; /** Served as of a cutoff and not in full: what it left out is unknown, never none. */ incomplete: boolean; loading: boolean; error: string | undefined; /** The last load was answered with a 403: refused, not lost. */ refused: boolean; /** The error and refusal are this patient's read, not an earlier patient's. */ failureIsThisPatients: boolean; /** The service's list is this patient's. Anything else is another patient's, never this one's data. */ listIsThisPatients: boolean; /** This patient's read has answered, one way or the other. Until then an empty list is unknown. */ answered: boolean; /** * WAS THE SERVER ACTUALLY ASKED? The effect early-returns when `patientState` is unresolved, so the * caller is left holding `loading: false` and `error: undefined` over an empty list - byte-identical to * a load that succeeded and found nothing. Only the hook knows which of the two it is in, so it reports * it. The four sibling hooks on the patient summary already did; this one did not, and that asymmetry * would have left medicines as the one section still able to claim None recorded about a question * nobody asked. */ attempted: boolean; loadMedicationRequests: () => Promise; clearError: () => void; medicationRequestsBundle: ReturnType['medicationRequestsBundle']; medicationRequestsList: ReturnType['medicationRequestsList']; } export const useFhirMedicationRequests = (options: UseFhirMedicationRequestsOptions = {}): UseFhirMedicationRequestsReturn => { const { autoLoad = true, errorMessage = "Failed to load medication requests", forceReload = false } = options; const { patientState } = usePatientServiceContext(); const { fetchMedicationRequestsWithPolling, listRead, medicationRequestsBundle, medicationRequestsList, cachedForPatientId } = useMedicationRequestServiceContext(); // After a reauthorization this list is read again past every cache; until then it is not taken for current. const chartStanding = useChartRecordStanding().standing; const freshness = useFreshAfterReauthorization(options.standing ?? chartStanding, () => ( patientState ? fetchMedicationRequestsWithPolling(patientState, true) : Promise.reject(new Error('No patient to read.'))), { resource: 'MedicationRequest', patientId: patientState?.id, listRead, servedAtCutoff: isServedAtCutoff(medicationRequestsBundle) }); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [refused, setRefused] = useState(false); const [failedFor, setFailedFor] = useState(); // The latest patient asked for; a slower read for an earlier patient must not write over it. const askingFor = useRef(undefined); const loadMedicationRequests = useCallback(async () => { if (!patientState) { return; } setLoading(true); setError(undefined); setRefused(false); askingFor.current = patientState.id; try { await fetchMedicationRequestsWithPolling(patientState); } catch (err) { if (askingFor.current !== patientState.id) return; console.error('Failed to load medication requests:', err); setFailedFor(patientState.id); setRefused(isRefusedRead(err)); setError(describeFetchFailure(err, errorMessage)); } finally { if (askingFor.current === patientState.id) setLoading(false); } }, [patientState, fetchMedicationRequestsWithPolling, errorMessage]); const clearError = useCallback(() => { setError(undefined); setRefused(false); }, []); 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 // medication 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 (!medicationRequestsBundle || cachedForPatientId !== patientState.id || forceReload) { loadMedicationRequests(); } }, [autoLoad, patientState, medicationRequestsBundle, cachedForPatientId, forceReload, loadMedicationRequests]); // The cache outlives a change of patient: hand out only the patient asked about. const guard = patientCacheGuard(cachedForPatientId, patientState?.id); return { freshness, loading: guard.loading(loading, Boolean(error) || refused), error, refused, failureIsThisPatients: failedFor !== undefined && failedFor === patientState?.id, listIsThisPatients: cachedForPatientId !== undefined && cachedForPatientId === patientState?.id, answered: Boolean(error) || (Boolean(medicationRequestsBundle) && cachedForPatientId === patientState?.id), // Derived from the SAME condition the early returns above test, never tracked separately: a // hand-set flag goes stale the first time somebody adds a third reason to bail, and silently. attempted: Boolean(patientState), loadMedicationRequests, clearError, // Served as of a cutoff and not in full: what it left out is unknown, never none. incomplete: isAsOfIncomplete(guard.bundle(medicationRequestsBundle, null)), medicationRequestsBundle: guard.bundle(medicationRequestsBundle, null), medicationRequestsList: guard.list(medicationRequestsList) }; };