// Internal (non-generated) shared validation for Shift segments — used by both createShift and // createShifts so the two commands can never drift on what makes a shift valid. export const SHIFT_TYPES = ["NORMAL", "SPLIT", "THROUGH", "ON_CALL", "SUPPORT"] as const; export type ShiftType = (typeof SHIFT_TYPES)[number]; export interface ShiftSegmentInput { plannedStartAt: Date; plannedEndAt: Date; breakMinutes: number; } export interface ResolvedSegment { plannedStartAt: Date; plannedEndAt: Date; breakMinutes: number; } const MINUTE_MS = 60_000; export function resolvePatternSegment( date: Date, seg: { startTime: number; endTime: number; spansMidnight: boolean; breakMinutes: number; }, ): ResolvedSegment { const dayStart = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); const plannedStartAt = new Date(dayStart.getTime() + seg.startTime * MINUTE_MS); const endDayStart = seg.spansMidnight ? new Date(dayStart.getTime() + 24 * 60 * MINUTE_MS) : dayStart; const plannedEndAt = new Date(endDayStart.getTime() + seg.endTime * MINUTE_MS); return { plannedStartAt, plannedEndAt, breakMinutes: seg.breakMinutes }; } export function validateSegments( segments: ResolvedSegment[], ): { ok: true } | { ok: false; error: "GAP" | "BREAK" } { const sorted = [...segments].sort( (a, b) => a.plannedStartAt.getTime() - b.plannedStartAt.getTime(), ); for (const seg of sorted) { const grossSpanMinutes = (seg.plannedEndAt.getTime() - seg.plannedStartAt.getTime()) / MINUTE_MS; if (seg.breakMinutes < 0 || seg.breakMinutes >= grossSpanMinutes) { return { ok: false, error: "BREAK" }; } } for (let i = 1; i < sorted.length; i++) { if (sorted[i].plannedStartAt.getTime() <= sorted[i - 1].plannedEndAt.getTime()) { return { ok: false, error: "GAP" }; } } return { ok: true }; } export function isShiftTypeConsistent(shiftType: ShiftType, segmentCount: number): boolean { switch (shiftType) { case "SPLIT": return segmentCount >= 2; case "SUPPORT": return segmentCount >= 1; case "NORMAL": case "THROUGH": case "ON_CALL": return segmentCount === 1; default: return false; } } export function sortSegments(segments: ResolvedSegment[]): ResolvedSegment[] { return [...segments].sort((a, b) => a.plannedStartAt.getTime() - b.plannedStartAt.getTime()); }