Files
vliaudatandClaude Opus 5 eb23eb650b feat: edit the reference week from the admin
The first administration page, built for the actual use case: a phone
held in one hand behind the counter. One card per day, native time
inputs so the platform keyboard does the work, and the consequences
shown before the save rather than after — the footer names which days
are about to change, and the button stays disabled until something
actually has.

"Duplicate onto the other open days" leaves closed days closed. Someone
copying Tuesday's hours means "the days I open, I open like this", not
"open seven days a week".

Validation runs in the browser for the feedback and again in the action
before the write: the client is a convenience, not a guarantee, and this
is the schedule the shop window shows. A day being closed drops its
leftover slots rather than failing on them.

A save that changes nothing writes nothing — no rows, no audit entry,
and so no needless panel redraw. Reordering slots does not count as a
change. The audit diff stores one readable line per day in French, so
the log can be read without cross-referencing the schema.

The editing helpers are pure and tested, and the write path is tested
against a real database including the read-only refusal.

Test files now run sequentially: the integration files share one
database and each truncates it, so parallel files raced. The suite takes
six seconds; giving every file its own database would buy nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 18:57:46 +02:00

155 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
changedDays,
duplicateToOpenDays,
hasErrors,
normaliseWeek,
toDraft,
validateWeek,
WEEK_ORDER,
type WeekDraft,
} from './week';
const FULL = [{ open: '10:00', close: '18:30' }];
const SPLIT = [
{ open: '10:00', close: '13:00' },
{ open: '14:00', close: '18:30' },
];
function week(overrides: Partial<Record<number, Partial<WeekDraft[number]>>> = {}): WeekDraft {
return WEEK_ORDER.map((dayOfWeek) => ({
dayOfWeek,
isClosed: dayOfWeek === 0 || dayOfWeek === 1,
slots: dayOfWeek === 0 || dayOfWeek === 1 ? [] : FULL.map((slot) => ({ ...slot })),
...overrides[dayOfWeek],
}));
}
describe('toDraft', () => {
it('returns the week Monday first', () => {
expect(toDraft([]).map((day) => day.dayOfWeek)).toEqual([1, 2, 3, 4, 5, 6, 0]);
});
it('fills in a day the database does not have, as closed', () => {
const draft = toDraft([{ dayOfWeek: 2, isClosed: false, slots: FULL }]);
expect(draft.find((day) => day.dayOfWeek === 2)?.slots).toEqual(FULL);
expect(draft.find((day) => day.dayOfWeek === 5)).toMatchObject({ isClosed: true, slots: [] });
});
it('puts slots in chronological order whatever the stored order', () => {
const draft = toDraft([{ dayOfWeek: 3, isClosed: false, slots: [SPLIT[1]!, SPLIT[0]!] }]);
expect(draft.find((day) => day.dayOfWeek === 3)?.slots).toEqual(SPLIT);
});
});
describe('duplicateToOpenDays', () => {
it('copies the source day onto the other open days', () => {
const source = week({ 2: { slots: SPLIT } });
const result = duplicateToOpenDays(source, 2);
for (const dayOfWeek of [3, 4, 5, 6]) {
expect(result.find((day) => day.dayOfWeek === dayOfWeek)?.slots).toEqual(SPLIT);
}
});
it('leaves closed days closed', () => {
// "The days I open, I open like this" — not "open seven days a week".
const result = duplicateToOpenDays(week({ 2: { slots: SPLIT } }), 2);
expect(result.find((day) => day.dayOfWeek === 1)).toMatchObject({
isClosed: true,
slots: [],
});
});
it('does nothing when the source day is itself closed', () => {
const source = week();
expect(duplicateToOpenDays(source, 1)).toEqual(source);
});
it('does nothing when the source day does not exist', () => {
const source = week();
expect(duplicateToOpenDays(source, 42)).toEqual(source);
});
it('copies by value, so later edits do not leak between days', () => {
const result = duplicateToOpenDays(week({ 2: { slots: SPLIT } }), 2);
const tuesday = result.find((day) => day.dayOfWeek === 2)!;
const wednesday = result.find((day) => day.dayOfWeek === 3)!;
expect(wednesday.slots).not.toBe(tuesday.slots);
expect(wednesday.slots[0]).not.toBe(tuesday.slots[0]);
});
});
describe('validateWeek', () => {
it('accepts a sound week', () => {
expect(hasErrors(validateWeek(week()))).toBe(false);
});
it('ignores the slots of a closed day', () => {
// Leftovers from before the day was closed must not block the save.
const draft = week({ 2: { isClosed: true, slots: [{ open: '18:00', close: '10:00' }] } });
expect(hasErrors(validateWeek(draft))).toBe(false);
});
it('refuses an open day with no hours', () => {
const draft = week({ 2: { isClosed: false, slots: [] } });
expect(validateWeek(draft)[2]).toContain('Un jour ouvert doit avoir au moins une plage horaire.');
});
it('reports the problem against the day it belongs to', () => {
const draft = week({ 4: { slots: [{ open: '18:30', close: '10:00' }] } });
const errors = validateWeek(draft);
expect(errors[4]?.length).toBeGreaterThan(0);
expect(errors[2]).toBeUndefined();
});
it('catches overlapping slots', () => {
const draft = week({
3: {
slots: [
{ open: '10:00', close: '14:00' },
{ open: '13:00', close: '18:30' },
],
},
});
expect(errors(validateWeek(draft), 3)).toMatch(/chevauchent/);
});
});
function errors(all: Record<number, string[]>, day: number): string {
return (all[day] ?? []).join(' ');
}
describe('normaliseWeek', () => {
it('strips the slots of a closed day', () => {
const draft = week({ 2: { isClosed: true, slots: FULL } });
expect(normaliseWeek(draft).find((day) => day.dayOfWeek === 2)?.slots).toEqual([]);
});
it('orders the slots of an open day', () => {
const draft = week({ 3: { slots: [SPLIT[1]!, SPLIT[0]!] } });
expect(normaliseWeek(draft).find((day) => day.dayOfWeek === 3)?.slots).toEqual(SPLIT);
});
});
describe('changedDays', () => {
it('reports nothing when the week is untouched', () => {
expect(changedDays(week(), week())).toEqual([]);
});
it('ignores a reordering that changes nothing', () => {
const before = week({ 3: { slots: SPLIT } });
const after = week({ 3: { slots: [SPLIT[1]!, SPLIT[0]!] } });
expect(changedDays(before, after)).toEqual([]);
});
it('names a day whose hours moved', () => {
expect(changedDays(week(), week({ 4: { slots: SPLIT } }))).toEqual([4]);
});
it('names a day that opened or closed', () => {
expect(changedDays(week(), week({ 1: { isClosed: false, slots: FULL } }))).toEqual([1]);
});
});