export interface BookingBlock { scheduleId: string; practitionerRoleId: string; locationRef?: { reference?: string; display?: string }; serviceRef?: { id: string; display: string }; } export interface AvailabilityWindow { start: string; end: string; } export interface TaggedAvailabilityWindow extends AvailabilityWindow { block: BookingBlock; } export interface UnionAvailability { events: TaggedAvailabilityWindow[]; resolveBlockForSlot: (dateStr: string, startTimeHHmm: string) => BookingBlock | undefined; } const pad = (n: number) => String(n).padStart(2, '0'); const localIso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; // Every reader keys a window by the date it starts on, so a window that runs past local midnight is cut // there: otherwise the later day is never offered (a block read in another zone's clock often crosses it). function splitAtLocalMidnight(window: AvailabilityWindow): AvailabilityWindow[] { const end = new Date(window.end); let start = new Date(window.start); const pieces: AvailabilityWindow[] = []; for (;;) { const nextMidnight = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 1); if (!(nextMidnight < end)) break; pieces.push({ start: pieces.length ? localIso(start) : window.start, end: localIso(nextMidnight) }); start = nextMidnight; } pieces.push({ start: pieces.length ? localIso(start) : window.start, end: window.end }); return pieces; } export function buildUnionAvailability( perBlock: { block: BookingBlock; events: AvailabilityWindow[] }[], ): UnionAvailability { // Flatten all events from all blocks, tagging each with its source block const events: TaggedAvailabilityWindow[] = perBlock.flatMap(({ block, events: blockEvents }) => blockEvents.flatMap(splitAtLocalMidnight).map((e) => ({ ...e, block, })), ); // Resolver function that finds the block whose window contains the given slot const resolveBlockForSlot = (dateStr: string, startTimeHHmm: string): BookingBlock | undefined => { // Build the slot instant in ISO format (timezone-naive local time) const slotInstant = new Date(`${dateStr}T${startTimeHHmm}:00`); // Find the tagged event where slot >= start && slot < end const matchingEvent = events.find((e) => { const windowStart = new Date(e.start); const windowEnd = new Date(e.end); return slotInstant >= windowStart && slotInstant < windowEnd; }); return matchingEvent?.block; }; return { events, resolveBlockForSlot, }; }