import { useCallback, useEffect, useState } from 'react'; import type { Encounter } from 'fhir/r4'; import { useAuthServiceContext, useEncounterServiceContext, useFhirServiceContext } from '../../../services'; import { usePatientNamesByIds } from '../../../hooks/useAppointmentPatientNames'; import type { QueueItem } from './encounterLifecycle'; import { openVisitsFromEarlierDays } from './openVisits'; /** * - THE VISITS THIS CLINICIAN NEVER CLOSED, fetched once and kept off the day's critical path. * * WHY IT IS ITS OWN READ rather than part of `useClinicianDay`: that hook answers *"who is here today"* * and is what the board waits on. This one answers *"what did I leave open"*, it is not urgent, and a * slow or refused answer to it must never delay or empty the day. So it fails to an EMPTY LIST with the * failure recorded - a clinician whose day loads is better served than one staring at a spinner because * a six-week-old visit could not be counted. * * IT ASKS FOR THE STATUSES, NOT THE DATES. The server has no "before today" search and inventing one * would be a second definition of the practice's own midnight; the boundary is applied here, by the pure * rule next door, against the clock the rest of the board already uses. */ export interface OpenVisits { visits: QueueItem[]; /** Distinct from "none" - a refused or failed read must never render as "you have nothing open". */ failed: boolean; refresh: () => void; } export const useOpenVisits = (now: Date, refreshToken?: unknown): OpenVisits => { const { currentUser } = useAuthServiceContext(); const { fetchMyOpenEncounters } = useEncounterServiceContext(); const { unpackBundle } = useFhirServiceContext(); const [encounters, setEncounters] = useState([]); const [failed, setFailed] = useState(false); const [tick, setTick] = useState(0); const practitionerId = currentUser?.fhir?.resourceType === 'Practitioner' ? currentUser.fhir.id : undefined; useEffect(() => { if (!practitionerId) { setEncounters([]); return; } let cancelled = false; void (async () => { try { const bundle = await fetchMyOpenEncounters(practitionerId); if (cancelled) return; setEncounters(unpackBundle(bundle) as Encounter[]); setFailed(false); } catch (error) { if (cancelled) return; // Kept as a STATE rather than swallowed: the panel says it could not check, which is a // different sentence from "nothing is open" and the only honest one here. console.warn('Could not read this clinician\'s open visits', error); setEncounters([]); setFailed(true); } })(); return () => { cancelled = true; }; }, [practitionerId, fetchMyOpenEncounters, unpackBundle, tick, refreshToken]); // The names come from the shared resolver every other surface uses - never a bare id in front of a // clinician, and never a second way of turning a patient id into a person. const patientIds = encounters .map((encounter) => encounter.subject?.reference?.split('/').pop()) .filter((id): id is string => Boolean(id)); const patientNames = usePatientNamesByIds(patientIds); // The resolver's own shape: an empty string means "resolved, and the record carries no usable name", // which must stay distinct from "not looked up yet" rather than both collapsing to a blank row. const namesById: Record = Object.fromEntries( Object.entries(patientNames.names).map(([id, name]) => [id, name || undefined]), ); const refresh = useCallback(() => setTick((value) => value + 1), []); return { visits: openVisitsFromEarlierDays(encounters, now, namesById), failed, refresh, }; };