/** * Validation of opening slots. * * Returns the list of problems in French, ready to show next to the field that * caused them. It returns every problem rather than the first, so someone * fixing a day's hours on a phone is not sent round the loop three times. */ import { minutesOfTime } from './civil'; import type { Slot } from './types'; /** A shop day realistically has a morning, an afternoon, and at most one more. */ export const MAX_SLOTS_PER_DAY = 3; const TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/; export function validateSlots(slots: Slot[]): string[] { const errors: string[] = []; if (slots.length > MAX_SLOTS_PER_DAY) { errors.push(`Trois plages horaires au maximum par jour (${slots.length} fournies).`); } const wellFormed: Slot[] = []; for (const slot of slots) { let malformed = false; for (const time of [slot.open, slot.close]) { if (!TIME_PATTERN.test(time)) { errors.push(`Heure invalide : « ${time} ». Format attendu : HH:mm.`); malformed = true; } } if (malformed) { continue; } if (minutesOfTime(slot.close) <= minutesOfTime(slot.open)) { errors.push( `L'heure de fermeture doit suivre l'heure d'ouverture : ${slot.open} – ${slot.close}.`, ); continue; } wellFormed.push(slot); } // Overlaps are checked in chronological order, so the message names the // slots the way the user reads them rather than the way they were typed. const ordered = [...wellFormed].sort((a, b) => minutesOfTime(a.open) - minutesOfTime(b.open)); for (let index = 1; index < ordered.length; index += 1) { const previous = ordered[index - 1]; const current = ordered[index]; if (!previous || !current) { continue; } if (minutesOfTime(current.open) < minutesOfTime(previous.close)) { errors.push( `Les plages ${previous.open} – ${previous.close} et ${current.open} – ${current.close} se chevauchent.`, ); } } return errors; }