import { useMemo } from 'react'; import type { HealthcareService, PractitionerRole, Schedule } from 'fhir/r4'; import type { PractitionerWithRoles } from '../services/PractitionerService'; import { getScheduleBookingFlags, useAuthServiceContext, usePractitionerRoleServiceContext, useScheduleServiceContext, } from '../services'; import { useFhirLocations } from './useFhirLocations.tsx'; import { onlyVirtual } from '../utils/roomLocation'; import { scheduleSiteFor } from '../utils/booking/physicalSite'; import { resolveOrgAppointmentPolicy } from '../config/appointmentPolicy'; import { buildStaffBookabilityContext, type StaffBookabilityInput, } from '../utils/booking/staffBookabilityContext'; import type { BookabilityContext } from '../utils/booking/resolveBookability'; import type { ServiceCatalogueBasis } from '../utils/booking/serviceCatalogueBasis'; // Assembles the staff-side BookabilityContext from the hooks that own the schedule helpers, so every staff // surface asks the bookability question the same way. Consumers stay one line. // // The catalog is an ARGUMENT, not a hook call. Pulling useFhirHealthcareServices in here would add a FETCH // to every screen that mounts this - including the staff list. Callers pass what they already hold; passing // nothing is safe and documented below. // // availabilityEvents MUST be called the way the patient flow calls it - // getScheduleAvailabilityEvents(schedule, transformFromFhirPractitionerRole(role)). Passing a raw // PractitionerRole instead of the re-transformed form shape yields ZERO availability, SILENTLY. export interface UseStaffBookabilityContextOptions { /** Used to resolve a schedule's owning PractitionerRole from its actor reference. */ practitioners: PractitionerWithRoles[]; /** * Optional. Without it, service NAMES fall back to the schedule actor's display and the org-default * self-bookable flag falls back to permissive (absent true) - the same behaviour the patient flow has * before its catalog loads. Widener detection under-reports rather than reporting something false. */ healthcareServicesList?: HealthcareService[]; /** * Optional. True only once healthcareServicesList is this practice's whole loaded catalogue; a block service * outside it then offers nothing. Leave it unset when the list may be partial or not loaded yet. */ healthcareServicesComplete?: boolean; /** * Optional. The verified catalogue: when given it replaces the two options above, so a service held back from it * offers nothing and readiness names the basis it stands on. */ serviceBasis?: ServiceCatalogueBasis; /** Optional. Only affects whether an all-locations widening is worth mentioning. */ locationCount?: number; /** Optional override, chiefly for tests. */ now?: Date; } export const useStaffBookabilityContext = ({ practitioners, healthcareServicesList, healthcareServicesComplete, serviceBasis, locationCount, now, }: UseStaffBookabilityContextOptions): BookabilityContext => { const { getScheduleAvailabilityEvents } = useScheduleServiceContext(); const { transformFromFhirPractitionerRole } = usePractitionerRoleServiceContext(); // - the two org-level telehealth facts, read once here so no call site can forget them. const { locationsList } = useFhirLocations(); const { currentOrganization } = useAuthServiceContext(); const roleForSchedule = useMemo(() => { const roleById = new Map(); for (const entry of practitioners) { for (const role of entry.roles) { if (role.id) roleById.set(role.id, role); } } return (schedule: Schedule): PractitionerRole | undefined => { const ref = (schedule.actor ?? []).find((a) => a.reference?.startsWith('PractitionerRole/')); const id = ref?.reference?.split('/').pop(); return id ? roleById.get(id) : undefined; }; }, [practitioners]); return useMemo(() => { const input: StaffBookabilityInput = { healthcareServicesList: serviceBasis ? serviceBasis.services : healthcareServicesList ?? [], catalogueComplete: serviceBasis ? serviceBasis.state === 'complete' : healthcareServicesComplete === true && healthcareServicesList !== undefined, ...(serviceBasis ? { serviceBasis: { state: serviceBasis.state, unconfirmed: serviceBasis.unconfirmed } } : {}), locationCount: locationCount ?? 0, roleForSchedule, bookingFlags: (s) => getScheduleBookingFlags(s), availabilityEvents: (s) => { const role = roleForSchedule(s); if (!role) return []; // - the PHYSICAL site, so a block is read in ITS site's clock rather than the // organization's. This hook decides whether a practitioner is BOOKABLE AT ALL, so a wrong clock // here does not just mis-label a time - it can move a window across a day boundary and report a // provider unbookable. `locationsList` is already held below for the telehealth facts, so this // costs no extra fetch. return getScheduleAvailabilityEvents( s, transformFromFhirPractitionerRole(role), scheduleSiteFor(s, role, locationsList), ); }, // Readiness asks "can ANY patient book this provider", so the new-patient exam gate is deliberately not // applied: a provider bookable by established patients IS bookable. Applying the gate here would report // every provider unbookable at an exam-policy org. examGate: { restrictToNewPatientExam: false }, now: now ?? new Date(), // - sourced HERE rather than passed by each caller, deliberately. This hook is the one // place a staff surface assembles a context; there are five call sites, and a per-caller option is // a thing four of them remember and one forgets, leaving that surface silently unable to see // telehealth. Both values fail closed on their own (no virtual location a video booking cannot be // expressed; absent policy off). virtualLocationId: onlyVirtual(locationsList ?? [])[0]?.id, telehealthEnabled: resolveOrgAppointmentPolicy(currentOrganization).telehealthEnabled, }; return buildStaffBookabilityContext(input); }, [ healthcareServicesList, healthcareServicesComplete, serviceBasis, locationCount, roleForSchedule, getScheduleAvailabilityEvents, transformFromFhirPractitionerRole, now, locationsList, currentOrganization, ]); };