import { useCallback, useState } from 'react'; import type { Appointment } from 'fhir/r4'; import type { HealthcareService } from 'fhir/r4'; import { useAppointmentServiceContext, useAuthServiceContext, useHealthcareServiceServiceContext, usePractitionerServiceContext, useScheduleServiceContext, } from '../services'; import { useStaffBookabilityContext } from './useStaffBookabilityContext'; import { environment } from '../environments/environment.ts'; import { serviceCatalogueBasis } from '../utils/booking/serviceCatalogueBasis'; import { evidenceFor, evidenceScope, type ScopeEvidence } from '../utils/booking/serviceCatalogueEvidence'; import { reassignInputsFrom } from '../utils/booking/reassignContext'; import { planReassign, type ReassignPlan } from '../utils/booking/reassignFeasibility'; import { actorReferencesForStaff } from '../utils/staffAppointmentImpact'; import type { ReassignTarget } from '../utils/booking/executeReassign'; /** * - work out what a chosen colleague could actually take. * * It runs ONLY when a target is picked, never up front. Assessing every practitioner in the practice * would mean fetching every schedule and every calendar to answer a question the admin has not asked yet - * and a deactivation dialog that pauses to load the whole org is one people learn to click through. */ export type ReassignPlanState = | { status: 'idle' } | { status: 'loading' } /** ⚠️ A failed look is NOT an empty plan — see the dialog's gate for why that distinction is load-bearing. */ | { status: 'error'; message: string } | { status: 'ready'; plan: ReassignPlan; target: ReassignTarget }; export function useReassignPlan(affected: ReadonlyArray) { const { practitionersList } = usePractitionerServiceContext(); const { fetchSchedules } = useScheduleServiceContext(); const { fetchAppointmentsForActors } = useAppointmentServiceContext(); const { wpOrganization } = useAuthServiceContext(); // The SERVICE CONTEXT, not `useFhirHealthcareServices` - that hook auto-loads on mount, and this one // is mounted on the staff profile screen. `useStaffBookabilityContext` makes the same point about // itself: *"the catalog is an ARGUMENT, not a hook call. Pulling useFhirHealthcareServices in here // would add a FETCH to every screen that mounts this."* The catalog is fetched when a colleague is // actually picked, and not before. const { fetchHealthcareServicesWithPolling, healthcareServicesList } = useHealthcareServiceServiceContext(); // The assembled context, never hand-wired accessors utils/booking/reassignContext.ts explains why: // calling the availability helper the wrong way yields ZERO open hours with no error, which would // report every appointment unmovable and read as a colleague who simply does not work those days. const bookability = useStaffBookabilityContext({ practitioners: practitionersList, healthcareServicesList }); const [state, setState] = useState({ status: 'idle' }); const clear = useCallback(() => setState({ status: 'idle' }), []); const assess = useCallback(async (targetPractitionerId: string) => { const entry = practitionersList.find((item) => item.practitioner.id === targetPractitionerId); // Only an ACTIVE role can take a booking - handing visits to someone who is themselves deactivated // would move the defect rather than fix it. const role = entry?.roles.find((candidate) => candidate.active !== false); if (!entry?.practitioner.id || !role?.id) { setState({ status: 'error', message: 'That staff member has no active role at this organization.' }); return; } setState({ status: 'loading' }); try { const [scheduleBundle, theirCalendar, catalogBundle] = await Promise.all([ fetchSchedules(entry.practitioner), fetchAppointmentsForActors(actorReferencesForStaff(entry.practitioner, entry.roles)), // AWAITED, and the result is read from the BUNDLE rather than from the context's state. // The state has not re-rendered yet at this point, so trusting it would hand the service // check an empty catalog - which refuses every service and reports a perfectly capable // colleague as unable to take anything. wpOrganization.current ? fetchHealthcareServicesWithPolling(wpOrganization.current as never) : Promise.resolve(undefined), ]); const targetSchedules = (scheduleBundle.entry ?? []) .map((bundleEntry) => bundleEntry.resource) .filter((schedule): schedule is NonNullable => Boolean(schedule)); const catalog: HealthcareService[] = (catalogBundle?.entry ?? []) .map((bundleEntry) => bundleEntry.resource as HealthcareService | undefined) .filter((service): service is HealthcareService => Boolean(service)); // Only a verified catalogue plans anything: a deleted or unconfirmed service is never a place to move a visit. const practiceId = wpOrganization.current?.id ?? ''; let evidence: ScopeEvidence | 'unavailable'; try { evidence = evidenceFor(evidenceScope(practiceId, environment.fhirBaseApi)); } catch { evidence = 'unavailable'; } const basis = serviceCatalogueBasis({ practiceId, services: catalog, bundle: catalogBundle, failed: !catalogBundle, evidence }); if (basis.state !== 'complete') { setState({ status: 'error', message: 'We could not verify the practice\'s services, so nothing was planned. Try again.' }); return; } const { accessors, matchContext } = reassignInputsFrom(bookability, basis.services); const plan = planReassign(affected, { targetSchedules, targetAppointments: theirCalendar.appointments, accessors, matchContext, // No `location` here on purpose - the feasibility engine derives it from each appointment, // so a colleague who does not serve that site is refused without this caller remembering. criteria: { practitionerId: entry.practitioner.id }, }); setState({ status: 'ready', plan, target: { practitionerId: entry.practitioner.id, practitionerRoleId: role.id }, }); } catch (error: any) { console.error('Failed to work out what could be reassigned:', error); setState({ status: 'error', message: 'We could not check that colleague\'s calendar.', }); } }, [ practitionersList, fetchSchedules, fetchAppointmentsForActors, bookability, healthcareServicesList, affected, wpOrganization, fetchHealthcareServicesWithPolling, ]); return { state, assess, clear }; }