/** * Editing helpers for the reference week. * * Pure, so the behaviour the shop owner actually touches — adding a lunch * break, copying one day onto the others — is tested without a browser. */ import { minutesOfTime } from './civil'; import type { Slot, WeeklyScheduleEntry } from './types'; import { validateSlots } from './validate'; /** Monday first: how the week is read, and how the screen draws it. */ export const WEEK_ORDER = [1, 2, 3, 4, 5, 6, 0] as const; /** Three letters, for the dense week table. */ export const DAY_SHORT_FR: Record = { 0: 'Dim', 1: 'Lun', 2: 'Mar', 3: 'Mer', 4: 'Jeu', 5: 'Ven', 6: 'Sam', }; export const DAY_NAMES_FR: Record = { 0: 'Dimanche', 1: 'Lundi', 2: 'Mardi', 3: 'Mercredi', 4: 'Jeudi', 5: 'Vendredi', 6: 'Samedi', }; export type WeekDraft = WeeklyScheduleEntry[]; /** A complete week, in display order, filling in any day the database lacks. */ export function toDraft(entries: readonly WeeklyScheduleEntry[]): WeekDraft { return WEEK_ORDER.map((dayOfWeek) => { const found = entries.find((entry) => entry.dayOfWeek === dayOfWeek); return { dayOfWeek, isClosed: found?.isClosed ?? true, slots: sortSlots(found?.slots ?? []), }; }); } /** * Copies one day's hours onto every other day that is currently open. * * Closed days stay closed: someone duplicating Tuesday's hours means "the days * I open, I open like this", not "open seven days a week". */ export function duplicateToOpenDays(week: WeekDraft, sourceDayOfWeek: number): WeekDraft { const source = week.find((day) => day.dayOfWeek === sourceDayOfWeek); if (!source || source.isClosed) { return week; } return week.map((day) => day.dayOfWeek === sourceDayOfWeek || day.isClosed ? day : { ...day, slots: source.slots.map((slot) => ({ ...slot })) }, ); } export type WeekErrors = Record; /** Validates every day, keyed by day so the message lands next to its row. */ export function validateWeek(week: WeekDraft): WeekErrors { const errors: WeekErrors = {}; for (const day of week) { if (day.isClosed) { continue; } const dayErrors = validateSlots(day.slots); if (day.slots.length === 0) { dayErrors.push('Un jour ouvert doit avoir au moins une plage horaire.'); } if (dayErrors.length > 0) { errors[day.dayOfWeek] = dayErrors; } } return errors; } export function hasErrors(errors: WeekErrors): boolean { return Object.keys(errors).length > 0; } /** * Cleans a draft for storage: closed days lose their slots, open days keep * them in chronological order. */ export function normaliseWeek(week: WeekDraft): WeekDraft { return week.map((day) => ({ dayOfWeek: day.dayOfWeek, isClosed: day.isClosed, slots: day.isClosed ? [] : sortSlots(day.slots), })); } /** Which days differ between two drafts, so the audit entry names only those. */ export function changedDays(before: WeekDraft, after: WeekDraft): number[] { return after .filter((day) => { const previous = before.find((entry) => entry.dayOfWeek === day.dayOfWeek); if (!previous) { return true; } return ( previous.isClosed !== day.isClosed || JSON.stringify(sortSlots(previous.slots)) !== JSON.stringify(sortSlots(day.slots)) ); }) .map((day) => day.dayOfWeek); } function sortSlots(slots: readonly Slot[]): Slot[] { return [...slots].sort((a, b) => minutesOfTime(a.open) - minutesOfTime(b.open)); }