/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; type ChangeStatus = "blocked" | "unblocked"; const slotCatalog = [ "09:00-10:00", "10:00-11:00", "11:00-12:00", "13:00-14:00", "14:00-15:00", "15:00-16:00", "16:00-17:00", ] as const; type SlotId = (typeof slotCatalog)[number]; type SlotInput = string | { start?: string; end?: string }; type ParticipantInput = Partial & { slots?: SlotInput[]; }; interface ParticipantAvailability { name: string; slots: SlotId[]; } interface AvailabilityChange { slot: SlotId; status: ChangeStatus; } interface ModifyAvailabilityEvent { slot?: SlotInput; action?: "block" | "unblock" | "toggle"; } interface CalendarAvailabilityArgs { participants: Default< ParticipantInput[], typeof defaultParticipants >; blocked: Default; } const slotOrder = new Map( slotCatalog.map((slot, index) => [slot, index]), ); const slotSet = new Set(slotCatalog); const emptySlotFallback: readonly SlotId[] = [] as const; const defaultParticipants: ParticipantAvailability[] = [ { name: "Alex Rivera", slots: ["09:00-10:00", "13:00-14:00", "15:00-16:00"], }, { name: "Blair Chen", slots: ["10:00-11:00", "13:00-14:00", "15:00-16:00"], }, { name: "Casey Morgan", slots: ["13:00-14:00", "15:00-16:00", "16:00-17:00"], }, ]; const parseSlotString = (value: string): SlotId | null => { const trimmed = value.trim(); const match = trimmed.match( /^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/, ); if (!match) return null; const startHour = Number(match[1]); const endHour = Number(match[3]); if (!Number.isFinite(startHour) || !Number.isFinite(endHour)) { return null; } const start = `${String(startHour).padStart(2, "0")}:${match[2]}`; const end = `${String(endHour).padStart(2, "0")}:${match[4]}`; const candidate = `${start}-${end}`; return slotSet.has(candidate) ? candidate as SlotId : null; }; const normalizeSlot = (input: SlotInput | undefined): SlotId | null => { if (typeof input === "string") { return parseSlotString(input); } if (input && typeof input === "object") { const start = typeof input.start === "string" ? input.start : ""; const end = typeof input.end === "string" ? input.end : ""; return parseSlotString(`${start}-${end}`); } return null; }; const sanitizeSlotList = ( value: unknown, fallback: readonly SlotId[], ): SlotId[] => { const entries = Array.isArray(value) ? value : []; const sanitized: SlotId[] = []; for (const entry of entries) { const slot = normalizeSlot(entry as SlotInput); if (!slot || sanitized.includes(slot)) continue; sanitized.push(slot); } if (sanitized.length === 0) { return [...fallback]; } sanitized.sort((left, right) => { const leftIndex = slotOrder.get(left) ?? slotCatalog.length; const rightIndex = slotOrder.get(right) ?? slotCatalog.length; return leftIndex - rightIndex; }); return sanitized; }; const sanitizeName = ( value: unknown, fallback: string, index: number, ): string => { if (typeof value !== "string") { return fallback || `Participant ${index + 1}`; } const trimmed = value.trim(); if (trimmed.length === 0) { return fallback || `Participant ${index + 1}`; } return trimmed.slice(0, 48); }; const cloneDefaults = (): ParticipantAvailability[] => { return defaultParticipants.map((entry) => ({ name: entry.name, slots: [...entry.slots], })); }; const sanitizeParticipants = ( value: readonly ParticipantInput[] | undefined, ): ParticipantAvailability[] => { if (!Array.isArray(value) || value.length === 0) { return cloneDefaults(); } const sanitized: ParticipantAvailability[] = []; let index = 0; for (const entry of value) { const fallback = defaultParticipants[index] ?? defaultParticipants[0]; const name = sanitizeName( entry?.name, fallback?.name ?? `Participant ${index + 1}`, index, ); const fallbackSlots = fallback ? fallback.slots : slotCatalog; const slots = sanitizeSlotList(entry?.slots, fallbackSlots); sanitized.push({ name, slots }); index += 1; } return sanitized.length > 0 ? sanitized : cloneDefaults(); }; const sanitizeBlocked = (value: unknown): SlotId[] => { return sanitizeSlotList(value, emptySlotFallback); }; const computeSharedAvailability = ( participants: readonly ParticipantAvailability[], blocked: readonly SlotId[], ): SlotId[] => { if (participants.length === 0) { return []; } const shared = new Set(participants[0]?.slots ?? []); for (let index = 1; index < participants.length; index += 1) { const entry = participants[index]; const slots = new Set(entry?.slots ?? []); for (const slot of [...shared]) { if (!slots.has(slot)) { shared.delete(slot); } } } const blockedSet = new Set(blocked); const result: SlotId[] = []; for (const slot of slotCatalog) { if (shared.has(slot) && !blockedSet.has(slot)) { result.push(slot); } } return result; }; const recordHistoryEntry = ( history: Cell, entry: string, ): void => { const current = history.get(); const list = Array.isArray(current) ? [...current] : []; list.push(entry); history.set(list.slice(-12)); }; const modifySharedAvailability = handler( ( event: ModifyAvailabilityEvent | undefined, context: { blocked: Cell; history: Cell; latestChange: Cell; }, ) => { const slot = normalizeSlot(event?.slot); if (!slot) { return; } const action = event?.action ?? "block"; const existing = sanitizeBlocked(context.blocked.get()); const blockedSet = new Set(existing); let status: ChangeStatus | null = null; if (action === "block") { if (!blockedSet.has(slot)) { blockedSet.add(slot); status = "blocked"; } } else if (action === "unblock") { if (blockedSet.delete(slot)) { status = "unblocked"; } } else { if (blockedSet.has(slot)) { blockedSet.delete(slot); status = "unblocked"; } else { blockedSet.add(slot); status = "blocked"; } } if (!status) { return; } const next = slotCatalog.filter((value) => blockedSet.has(value)); context.blocked.set([...next]); recordHistoryEntry(context.history, `${status} ${slot}`); const change: AvailabilityChange = { slot, status }; context.latestChange.set(change); }, ); /** * Pattern computing shared availability windows across participants while * reacting to block edits in real time for offline scheduling flows. */ export const calendarAvailabilityPattern = recipe( "Calendar Availability Pattern", ({ participants, blocked }) => { const history = cell([]); const latestChange = cell(null); const participantsView = lift(sanitizeParticipants)(participants); const blockedView = lift(sanitizeBlocked)(blocked); const sharedAvailability = lift( ( input: { participants: ParticipantAvailability[]; blocked: SlotId[] }, ): SlotId[] => { return computeSharedAvailability(input.participants, input.blocked); }, )({ participants: participantsView, blocked: blockedView }); const sharedLabel = lift((slots: readonly SlotId[]) => { if (!Array.isArray(slots) || slots.length === 0) { return "none"; } return slots.join(", "); })(sharedAvailability); const nextAvailableSlot = lift((slots: readonly SlotId[]) => { return Array.isArray(slots) && slots.length > 0 ? slots[0] : "none"; })(sharedAvailability); const freeSlotCount = lift((slots: readonly SlotId[]) => { return Array.isArray(slots) ? slots.length : 0; })(sharedAvailability); const historyView = lift((entries: string[] | undefined) => { return Array.isArray(entries) ? [...entries] : []; })(history); const latestChangeView = lift( (entry: AvailabilityChange | null | undefined) => { return entry ? { ...entry } : null; }, )(latestChange); const sharedSummary = str`Shared slots: ${sharedLabel}`; const nextSlotSummary = str`Next slot: ${nextAvailableSlot}`; const updateAvailability = modifySharedAvailability({ blocked, history, latestChange, }); return { participants, participantsView, blocked, blockedView, sharedAvailability, sharedLabel, sharedSummary, nextAvailableSlot, nextSlotSummary, freeSlotCount, actionHistory: historyView, latestChange: latestChangeView, controls: { updateAvailability, }, }; }, );