feat: validate opening slots and format dates in French and English
Slot validation reports every problem at once rather than the first, so someone fixing a day's hours on a phone in the shop is not sent round the loop three times. It covers the format, the ordering, overlaps and the three-slot ceiling. Slots that merely touch are accepted: 13:00 closing and 13:00 reopening is pointless but not contradictory, and refusing it would only annoy whoever typed it. Month and day names are spelled out rather than taken from Intl. The screen text feeds a content hash that decides whether the e-ink panel redraws at all, so it has to be byte-stable and must not shift because a container image ships different ICU locale data. English uses the twelve-hour clock, as the payload contract specifies: "Closes at 6:30 pm". Noon and midnight are covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user