import type { Schedule } from 'fhir/r4'; import type { BookabilityBlocker } from './bookabilityReasons'; import type { BandResult } from './scheduleUsable'; import { calendarDateInZone, viewerTimeZone } from '../practiceTimezone'; // BAND 3 of bookability: does this block actually produce open times? // // This is where "the provider is selectable but there are no times" lives - the most common real-world // complaint, and the one no existing check can see, because it was buried inline in handleScheduleChange // rather than being asked as a question. // // MIRROR, NEVER A GATE - see bookabilityReasons.ts. /** The minimum shape getScheduleAvailabilityEvents returns; kept structural so this imports no services. */ export interface AvailabilityEventLike { extendedProps?: { type?: string }; } export interface YieldsTimesAccessors { availabilityEvents: (s: Schedule) => AvailabilityEventLike[]; } // getScheduleAvailabilityEvents also returns 'not-available' (time-off) BACKGROUND events, which must never // count as bookable. This one filter is SHARED with the patient flow's handleScheduleChange so the two cannot // diverge - that shared use is the whole reason it is exported rather than inlined below. // // Generic so a caller holding a richer event type gets that same type back and needs no cast. export const availabilityWindowsFor = (events: T[]): T[] => events.filter((event) => event.extendedProps?.type === 'availability'); // A date-only end is a calendar day and stays open through it, as the slot generator reads it; parsed // as UTC midnight, a block read as ended on its own last day. const horizonEnded = (end: string, now: Date): boolean => /^\d{4}-\d{2}-\d{2}$/.test(end) ? end < calendarDateInZone(now, viewerTimeZone()) : new Date(end).getTime() < now.getTime(); export function evaluateScheduleYieldsTimes( schedule: Schedule, accessors: YieldsTimesAccessors, now: Date, ): BandResult { const blockers: BookabilityBlocker[] = []; if (availabilityWindowsFor(accessors.availabilityEvents(schedule)).length === 0) { blockers.push('BLOCK_NO_HOURS'); } const start = schedule.planningHorizon?.start; const end = schedule.planningHorizon?.end; if (!start || !end) { // A half-set horizon is reported as MISSING, not expired - "expired" would send someone off to extend a // date that was never set in the first place. blockers.push('BLOCK_NO_PLANNING_HORIZON'); } else if (horizonEnded(end, now)) { blockers.push('BLOCK_HORIZON_EXPIRED'); } return { ok: blockers.length === 0, blockers }; }