// (re-cut) - is this person AWAY today, according to their SCHEDULES? // // **Spencer, 2026-08-14: deactivation means GONE, not away - absence is `Schedule.notAvailable`.** // That ruling was exact. This product stores a schedule's not-available periods in an Akashic extension // (`SCHEDULE_NOT_AVAILABLE_EXTENSION_URL`), two editors already write them - Manage Schedule and Edit // Availability - and the availability generator already skips any day they cover, so BOOKING has // honoured them all along. // // **The only thing missing was the readiness SENTENCE**, which is all this module supplies: HR saying // *"on leave, returns automatically"* rather than *"deactivated - this person has left the practice"*. // // IT TAKES PERIODS, NOT SCHEDULES, and that is deliberate rather than fussy. Reading the extension // lives in `getScheduleAvailability` (ScheduleService), which imports `environments/environment.ts` and // touches `window` at module scope - importing it here would make this module unloadable in a // node-environment test, which is exactly why `practitionerBookingReadiness.ts` never had one. // And re-parsing the extension here instead would be a second reader of one storage shape: the drift // this file's neighbours have been rewritten twice to remove. // Callers pass `schedules.flatMap((s) => getScheduleAvailability(s).notAvailable)`. /** The shape `getScheduleAvailability` returns - a FHIR `notAvailable` entry. */ export interface AbsencePeriod { description?: string; during?: { start?: string; end?: string }; } /** * AN OPEN-ENDED ABSENCE COUNTS. A practice records indefinite leave without knowing the return date, * and treating "no end" as "not away" reports somebody as available who is not - and on this surface * that means telling a practice their clinician is fine when nobody can book them. * A period with no START is ignored: it says nothing about when, and reading it as "always" would put * anyone with one malformed row permanently on leave. */ /** * A DATE-ONLY BOUND MEANS THE WHOLE OF THAT DAY, and `Date.parse` does not: it reads `2026-09-18` * as UTC midnight, so west of UTC the last day of leave stopped counting the evening BEFORE it. * Measured 2026-09-07 in the same family of defects as the booking picker's and the calendar's. * * A value with a time in it is an instant and is left alone - widening it would invent an absence. */ const boundInstant = (value: string, endOfDay: boolean): number => { if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { const [year, month, day] = value.split('-').map(Number); return new Date(year, month - 1, day + (endOfDay ? 1 : 0)).getTime(); } return Date.parse(value); }; export function periodCoversNow(during: AbsencePeriod['during'], now: Date): boolean { if (!during?.start) return false; const start = boundInstant(during.start, false); if (Number.isNaN(start) || start > now.getTime()) return false; if (!during.end) return true; // A date-only end becomes the start of the day AFTER it and is compared EXCLUSIVELY, which is what // makes the last day of leave count as leave. An end with a time in it keeps the inclusive // comparison it has always had - `end >= now` - so a period ending at this instant still covers it. const endIsWholeDay = /^\d{4}-\d{2}-\d{2}$/.test(during.end); const end = boundInstant(during.end, endIsWholeDay); // An unreadable end reads as STILL AWAY - the same direction as the rest of this file. Where we // cannot tell, we do not report an absent person as available. if (Number.isNaN(end)) return true; return endIsWholeDay ? end > now.getTime() : end >= now.getTime(); } /** * Whether any of these periods covers `now`. * * **ONE ABSENCE PER SCHEDULE BLOCK is the model Spencer chose, and it has a known edge:** a clinician * holding three blocks needs three entries, and missing one leaves that block bookable. This function * answers the READINESS question - *"is this person away?"* - across every block they hold, so the HR * line is right even when one block was missed. It does NOT make the missed block unbookable; only an * entry on that block does, and a "record across every block" helper is the fix for that, not this. */ export const isOnLeaveNow = (periods: readonly AbsencePeriod[] | undefined, now: Date): boolean => (periods ?? []).some((period) => periodCoversNow(period.during, now));