import type { Schedule } from 'fhir/r4'; import type { BookabilityBlocker } from './bookabilityReasons'; // BAND 1 of bookability: is this schedule block live at all, independent of what is being booked? // // Extracted from PatientBookAppointment.tsx's `bookableSchedules` memo so there is ONE predicate rather than // an inline copy. Accessors are injected to keep this free of React context and FHIR fetching, matching the // pattern matchSchedules.ts already uses. // // MIRROR, NEVER A GATE - see bookabilityReasons.ts. export interface UsableAccessors { bookingFlags: (s: Schedule) => { allowBooking: boolean; allowPatientSelfBooking: boolean }; /** The owning PractitionerRole's `active`. `undefined` = no role link (pre-migration) and counts as active. */ roleActive: (s: Schedule) => boolean | undefined; /** * Does the owning PractitionerRole hold an ACTIVE clinician seat? * * The booking funnels already refuse a seatless role (`canBeBookedAsClinician`), but they applied that * test THEMSELVES, outside this resolver - so every surface that mirrors bookability from here reported a * seatless provider as ready while the picker would not offer them. That is the same two-paths-in-step-by- * convention drift this module's own header records paying for once, one axis over. * * NOT optional, deliberately. A caller that has not been told must not silently mean "seated": the seat * defaults to ABSENT everywhere else in this product precisely so it gates rather than suggests. */ roleHoldsClinicianSeat: (s: Schedule) => boolean; } export interface BandResult { ok: boolean; blockers: BookabilityBlocker[]; } export function evaluateScheduleUsable(schedule: Schedule, accessors: UsableAccessors): BandResult { const blockers: BookabilityBlocker[] = []; // A schedule with no id cannot be booked against, but that is a data defect rather than a configuration // gap - there is nothing a practice could go and fix - so it fails without producing a blocker to report. const hasId = Boolean(schedule.id); if (accessors.roleActive(schedule) === false) blockers.push('ROLE_INACTIVE'); if (!accessors.roleHoldsClinicianSeat(schedule)) blockers.push('NO_CLINICIAN_SEAT'); const flags = accessors.bookingFlags(schedule); if (!flags.allowBooking) blockers.push('BLOCK_BOOKING_DISABLED'); if (!flags.allowPatientSelfBooking) blockers.push('BLOCK_SELF_BOOKING_DISABLED'); return { ok: hasId && blockers.length === 0, blockers }; }