/** * Closure periods. * * A vacation period is a source of truth, never expanded into exception rows: * the resolver reads it directly. That is why editing one can never orphan * anything, and why overlapping periods are worth refusing rather than * silently merging — two rows covering the same day would both be "in effect" * with no way to say which. */ import { civilDaysBetween, compareCivil, type CivilDate } from './civil'; /** Longer than a year is a data-entry accident, not a closure. */ export const MAX_PERIOD_DAYS = 366; export type Period = { startDate: CivilDate; endDate: CivilDate; }; export type NamedPeriod = Period & { id: string; labelFr: string }; /** Inclusive on both ends, as the shop would read it off a note on the door. */ export function countDays(period: Period): number { return civilDaysBetween(period.startDate, period.endDate) + 1; } export function periodsOverlap(a: Period, b: Period): boolean { return compareCivil(a.startDate, b.endDate) <= 0 && compareCivil(b.startDate, a.endDate) <= 0; } /** The first existing period a candidate would collide with, if any. */ export function findOverlap(candidate: Period, existing: readonly NamedPeriod[]): NamedPeriod | null { return existing.find((period) => periodsOverlap(candidate, period)) ?? null; } export function validatePeriod(input: { startDate: CivilDate; endDate: CivilDate; labelFr: string; }): string[] { const errors: string[] = []; if (!input.startDate || !input.endDate) { errors.push('Indiquez une date de début et une date de fin.'); return errors; } if (compareCivil(input.startDate, input.endDate) > 0) { errors.push('La date de fin précède la date de début.'); } else if (countDays(input) > MAX_PERIOD_DAYS) { errors.push(`Une période ne peut pas dépasser ${MAX_PERIOD_DAYS} jours.`); } if (input.labelFr.trim().length === 0) { errors.push('Donnez un libellé à cette période (il s’affiche sur l’écran).'); } return errors; } /** Periods still to come or in progress, soonest first. */ export function upcomingPeriods(periods: readonly T[], today: CivilDate): T[] { return periods .filter((period) => compareCivil(period.endDate, today) >= 0) .sort((a, b) => compareCivil(a.startDate, b.startDate)); }