One card per day pushed the week to roughly 1100px, so checking a change meant scrolling back up past the day you had just edited. All seven days now fit on one phone screen. Each row carries the day, an open switch, its slots inline and its two actions. The per-row "duplicate onto the other open days" button was repeated five times in prose; it is now a short labelled control on the row itself, with the full sentence kept as its accessible name. Unsaved days are marked with a dot rather than a badge, and the footer still names which days are about to change — the point of the preview is that it is readable without leaving the week. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
123 lines
3.5 KiB
TypeScript
123 lines
3.5 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;
|
|
|
|
/** Three letters, for the dense week table. */
|
|
export const DAY_SHORT_FR: Record<number, string> = {
|
|
0: 'Dim',
|
|
1: 'Lun',
|
|
2: 'Mar',
|
|
3: 'Mer',
|
|
4: 'Jeu',
|
|
5: 'Ven',
|
|
6: 'Sam',
|
|
};
|
|
|
|
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));
|
|
}
|