import { useEffect, useState, useCallback, useRef } from 'react'; import { usePatientServiceContext, useCareTeamServiceContext, useAuthServiceContext } from '../services'; import { careTeamScopeAdmitted, useCareTeamReadScope } from './useCareTeamReadScope'; import { useScopeLifetime } from './useScopeLifetime'; import { patientCacheGuard } from '../utils/cacheOwner'; import { describeFetchFailure } from '../utils/fetchFailureMessage'; interface UseFhirCareTeamsOptions { autoLoad?: boolean; errorMessage?: string; forceReload?: boolean; } interface UseFhirCareTeamsReturn { loading: boolean; error: string | undefined; /** This patient's read has answered, one way or the other. Until then an empty list is unknown. */ answered: boolean; loadCareTeams: () => Promise; clearError: () => void; careTeamsBundle: ReturnType['careTeamsBundle']; careTeamsList: ReturnType['careTeamsList']; } // What this hook last heard, and in which scope lifetime. A load answers only the lifetime it began in. type LoadState = { generation: number; loading: boolean; error?: string }; export const useFhirCareTeams = (options: UseFhirCareTeamsOptions = {}): UseFhirCareTeamsReturn => { const { autoLoad = true, errorMessage = "Failed to load care teams", forceReload = false } = options; const { patientState } = usePatientServiceContext(); const { currentOrganization } = useAuthServiceContext(); const { fetchCareTeamsWithPolling, careTeamsBundle, careTeamsList, ownedScope } = useCareTeamServiceContext(); const scope = useCareTeamReadScope(patientState?.id); const { generation, isLive } = useScopeLifetime(scope); const latestLoad = useRef(0); const [state, setState] = useState({ generation, loading: false }); // Bound to the scope lifetime it was made in: the patient, practice and viewer of this render. Called // after that lifetime has ended - even back on the same scope - it starts nothing. const loadCareTeams = useCallback(async () => { if (!patientState || !careTeamScopeAdmitted(scope) || !isLive(generation)) { return; } const load = ++latestLoad.current; // A load that is no longer the newest, or whose lifetime has ended, changes nothing. const current = () => load === latestLoad.current && isLive(generation); setState({ generation, loading: true }); try { await fetchCareTeamsWithPolling(patientState, currentOrganization, false, scope, { isLive: () => isLive(generation) }); if (current()) setState({ generation, loading: false }); } catch (err) { console.error('Failed to load care teams:', err); if (current()) setState({ generation, loading: false, error: describeFetchFailure(err, errorMessage) }); } }, [patientState, currentOrganization, scope, generation, isLive, fetchCareTeamsWithPolling, errorMessage]); const clearError = useCallback(() => { setState((prev) => ({ ...prev, error: undefined })); }, []); useEffect(() => { if (!autoLoad) return; if (!patientState) return; // A list published for ANY OTHER scope - another patient, viewer or practice - is not a reason not to // ask. NOT a refetch on every navigation: same scope, bundle loaded, no request. if (!careTeamsBundle || ownedScope !== scope || forceReload) { loadCareTeams(); } }, [autoLoad, patientState, careTeamsBundle, ownedScope, scope, forceReload, loadCareTeams]); // The shared list can still be another scope's, and this hook's state the last scope's, until the new read // answers; neither is shown for the scope this render is for. const guard = patientCacheGuard(ownedScope, patientState ? scope : undefined); const own = state.generation === generation ? state : { generation, loading: false }; const error = own.error; return { loading: guard.loading(own.loading, Boolean(error)), error, answered: Boolean(error) || guard.flag(Boolean(careTeamsBundle)), loadCareTeams, clearError, careTeamsBundle: guard.bundle(careTeamsBundle, null), careTeamsList: guard.list(careTeamsList) }; };