import { useEffect, useRef, useState, useCallback } from 'react'; import { usePatientServiceContext, useTaskServiceContext } from '../services'; import { describeFetchFailure } from '../utils/fetchFailureMessage'; import { mayMissRecentWrite } from '../utils/recentWritesUnresolved'; import { onViewerChange } from '../utils/writeThroughHold'; interface UseFhirTasksOptions { autoLoad?: boolean; errorMessage?: string; forceReload?: boolean; } interface UseFhirTasksReturn { loading: boolean; error: string | undefined; loadTasks: () => Promise; clearError: () => void; /** This patient's read has answered, one way or the other. Until then an empty list is unknown. */ answered: boolean; /** The read could be asked at all: there is a patient to ask about. */ attempted: boolean; /** This patient's list answered but may be missing a recent change: its rows are shown, never as complete or empty. */ mayBeIncomplete: boolean; /** Reads this patient's list again, never from the cache. */ recheck: () => Promise; tasksBundle: ReturnType['tasksBundle']; tasksList: ReturnType['tasksList']; } export const useFhirTasks = (options: UseFhirTasksOptions = {}): UseFhirTasksReturn => { const { autoLoad = true, errorMessage = "Failed to load tasks", forceReload = false } = options; const { patientState } = usePatientServiceContext(); const { cachedForPatientId, fetchTasksWithPolling, tasksBundle, tasksList } = useTaskServiceContext(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); // The read this hook's loading and error belong to. A newer read, another patient or another viewer retires the // older one, whose end then changes nothing here. const current = useRef(0); const retire = useCallback(() => { current.current += 1; setLoading(false); setError(undefined); }, []); useEffect(retire, [patientState?.id, retire]); useEffect(() => onViewerChange(retire), [retire]); const read = useCallback(async (fresh: boolean) => { if (!patientState) { return; } const request = ++current.current; const owns = () => current.current === request; setLoading(true); setError(undefined); try { await fetchTasksWithPolling(patientState, fresh); } catch (err) { if (owns()) { console.error('Failed to load tasks:', err); setError(describeFetchFailure(err, errorMessage)); } } finally { if (owns()) setLoading(false); } }, [patientState, fetchTasksWithPolling, errorMessage]); const loadTasks = useCallback(() => read(false), [read]); const recheck = useCallback(() => read(true), [read]); 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 (!tasksBundle || cachedForPatientId !== patientState.id || forceReload) { loadTasks(); } }, [autoLoad, patientState, tasksBundle, cachedForPatientId, forceReload, loadTasks]); const listIsThisPatients = Boolean(tasksBundle) && cachedForPatientId === patientState?.id; return { loading, error, loadTasks, recheck, clearError, answered: Boolean(error) || listIsThisPatients, mayBeIncomplete: !error && listIsThisPatients && mayMissRecentWrite(tasksBundle), attempted: Boolean(patientState), tasksBundle, tasksList }; };