Files
ita-ito-horaires/lib/schedule/week.ts
T

112 lines
3.3 KiB
TypeScript

/**
* 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;
export const DAY_NAMES_FR: Record<number, string> = {
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<number, string[]>;
/** 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));
}