112 lines
3.3 KiB
TypeScript
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));
|
|
}
|