import { useCallback, useEffect, useRef, useState } from 'react'; import type { HealthcareService, Organization } from 'fhir/r4'; import { useAuthServiceContext } from '../services/AuthService.tsx'; import { useHealthcareServiceServiceContext } from '../services/HealthcareServiceService.tsx'; import { environment } from '../environments/environment.ts'; import { onViewerChange } from '../utils/writeThroughHold'; import { isWholeCatalogue, serviceCatalogueBasis, type ServiceCatalogueBasis } from '../utils/booking/serviceCatalogueBasis'; import { beginRead, evidenceFor, evidenceScope, onEvidenceChange, settleFromSearch, type ScopeEvidence } from '../utils/booking/serviceCatalogueEvidence'; import { useFhirHealthcareServiceBroadcast } from './useFhirHealthcareServiceBroadcast'; const READ_BACK_ATTEMPTS = 5; const READ_BACK_DELAY_MS = 3000; export type ServiceCatalogue = ServiceCatalogueBasis & { retry: () => Promise }; /** * The practice's services as booking may use them: read whole, with what this tab knows beyond the search applied. * A change no search has yet agreed with is read again a bounded number of times, then said as unverified with Retry. */ export const useServiceCatalogueBasis = (): ServiceCatalogue => { const { wpOrganization } = useAuthServiceContext(); const organization = wpOrganization.current as Organization | undefined; const practiceId = organization?.id; const { healthcareServicesBundle, healthcareServicesList, fetchHealthcareServicesWithPolling } = useHealthcareServiceServiceContext(); useFhirHealthcareServiceBroadcast(); // This opening: a read begun in it settles evidence only while it is still the one shown. const opening = useRef({}); useEffect(() => { opening.current = {}; return () => { opening.current = {}; }; }, [practiceId]); const [, setTick] = useState(0); const [failed, setFailed] = useState(false); const [spent, setSpent] = useState(false); const rereads = useRef(0); // Each owned completion moves the reread cycle on; the read in flight, if any, holds the next one back. const [completed, setCompleted] = useState(0); const inFlight = useRef(null); useEffect(() => onEvidenceChange(() => setTick((n) => n + 1)), []); useEffect(() => onViewerChange(() => { opening.current = {}; setTick((n) => n + 1); }), []); const scope = practiceId ? evidenceScope(practiceId, environment.fhirBaseApi) : undefined; let evidence: ScopeEvidence | 'unavailable' = 'unavailable'; if (scope) { try { evidence = evidenceFor(scope); } catch { evidence = 'unavailable'; } } const read = useCallback(async (force: boolean) => { if (!organization?.id) return; const here = opening.current; let started; try { started = beginRead(evidenceScope(organization.id, environment.fhirBaseApi), here); } catch { setTick((n) => n + 1); return; } inFlight.current = here; try { const bundle = await fetchHealthcareServicesWithPolling(organization, force); if (here !== opening.current) return; setFailed(false); // A partial answer settles nothing: an absence in it is not a deletion. if (bundle && isWholeCatalogue(bundle)) settleFromSearch(started, here, (bundle.entry ?? []).map((each) => each.resource as HealthcareService)); } catch (error) { if (here !== opening.current) return; console.error('Failed to load healthcare services:', error); setFailed(true); } finally { if (inFlight.current === here) inFlight.current = null; if (here === opening.current) setCompleted((n) => n + 1); } }, [organization, fetchHealthcareServicesWithPolling]); useEffect(() => { rereads.current = 0; setSpent(false); setFailed(false); // A cached catalogue serves an opening with nothing to settle; evidence to settle needs a read begun after it. let pending = true; try { pending = Object.values(evidenceFor(evidenceScope(practiceId ?? '', environment.fhirBaseApi)).services).some((each) => each.observation); } catch { pending = true; } void read(pending); }, [read, practiceId]); const basis = practiceId ? serviceCatalogueBasis({ practiceId, services: healthcareServicesList, bundle: healthcareServicesBundle, failed, evidence }) : { state: 'checking' as const, services: [] as HealthcareService[], unconfirmed: [] as string[] }; // A change no search has agreed with yet: read again, a bounded number of times, then stop and say so. const waiting = basis.state === 'checking' && Boolean(healthcareServicesBundle) && !spent; useEffect(() => { if (!waiting || inFlight.current === opening.current) return; if (rereads.current >= READ_BACK_ATTEMPTS) { setSpent(true); return; } const timer = setTimeout(() => { rereads.current += 1; void read(true); }, READ_BACK_DELAY_MS); return () => clearTimeout(timer); }, [waiting, read, completed]); const retry = useCallback(async () => { rereads.current = 0; setSpent(false); await read(true); }, [read]); const shown: ServiceCatalogueBasis = spent && basis.state === 'checking' ? { ...basis, state: 'unavailable', services: [] } : basis; return { ...shown, retry }; };