diff --git a/app/admin/horaires/WeekEditor.tsx b/app/admin/horaires/WeekEditor.tsx index 7c0d3f0..5be8671 100644 --- a/app/admin/horaires/WeekEditor.tsx +++ b/app/admin/horaires/WeekEditor.tsx @@ -6,6 +6,7 @@ import { MAX_SLOTS_PER_DAY } from '@/lib/schedule/validate'; import { changedDays, DAY_NAMES_FR, + DAY_SHORT_FR, duplicateToOpenDays, validateWeek, type WeekDraft, @@ -19,11 +20,11 @@ type Props = { }; /** - * The reference week. + * The reference week, as one row per day. * - * Built for 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 of a change shown before it is saved rather than after. + * Density is the point: all seven days have to be readable at a glance on a + * phone held behind the counter, so the whole week fits on one screen and a + * change can be checked without scrolling back up. */ export default function WeekEditor({ initial, canEdit }: Props) { const [week, setWeek] = useState(initial); @@ -36,15 +37,20 @@ export default function WeekEditor({ initial, canEdit }: Props) { const pendingChanges = useMemo(() => changedDays(saved, week), [saved, week]); const blocked = Object.keys(errors).length > 0; - function update(dayOfWeek: number, patch: Partial) { + function touch() { setMessage(null); setServerErrors({}); + } + + function update(dayOfWeek: number, patch: Partial) { + touch(); setWeek((current) => current.map((day) => (day.dayOfWeek === dayOfWeek ? { ...day, ...patch } : day)), ); } function updateSlot(dayOfWeek: number, index: number, field: 'open' | 'close', value: string) { + touch(); setWeek((current) => current.map((day) => day.dayOfWeek === dayOfWeek @@ -57,36 +63,21 @@ export default function WeekEditor({ initial, canEdit }: Props) { : day, ), ); - setMessage(null); - } - - function addSlot(dayOfWeek: number) { - update(dayOfWeek, { - slots: [ - ...(week.find((day) => day.dayOfWeek === dayOfWeek)?.slots ?? []), - { open: '14:00', close: '18:30' }, - ], - }); - } - - function removeSlot(dayOfWeek: number, index: number) { - const day = week.find((entry) => entry.dayOfWeek === dayOfWeek); - update(dayOfWeek, { slots: (day?.slots ?? []).filter((_, position) => position !== index) }); } function submit() { - setMessage(null); - setServerErrors({}); + touch(); startTransition(async () => { const result = await saveWeek(week); if (result.ok) { setSaved(week); + const count = result.changed.length; setMessage({ kind: 'ok', text: - result.changed.length === 0 + count === 0 ? 'Aucun changement à enregistrer.' - : `Horaires enregistrés (${result.changed.length} jour${result.changed.length > 1 ? 's' : ''} modifié${result.changed.length > 1 ? 's' : ''}).`, + : `Enregistré — ${count} jour${count > 1 ? 's' : ''} modifié${count > 1 ? 's' : ''}.`, }); } else { setServerErrors(result.dayErrors ?? {}); @@ -96,138 +87,166 @@ export default function WeekEditor({ initial, canEdit }: Props) { } return ( -
+
{!canEdit ? ( -

- Ce compte est en lecture seule : les horaires sont consultables mais pas modifiables. +

+ Compte en lecture seule : consultation uniquement.

) : null} -
- {week.map((day) => { - const dayErrors = [...(errors[day.dayOfWeek] ?? []), ...(serverErrors[day.dayOfWeek] ?? [])]; +
+ {week.map((day, position) => { + const dayErrors = [ + ...(errors[day.dayOfWeek] ?? []), + ...(serverErrors[day.dayOfWeek] ?? []), + ]; const isChanged = pendingChanges.includes(day.dayOfWeek); + const fullName = DAY_NAMES_FR[day.dayOfWeek] ?? ''; return ( -
0 ? 'border-[var(--danger)]' : 'border-[var(--line)]' + className={`${position > 0 ? 'border-t border-[var(--line)]' : ''} ${ + dayErrors.length > 0 ? 'bg-[var(--danger-tint)]' : '' }`} > -
-

{DAY_NAMES_FR[day.dayOfWeek]}

+
+ + {DAY_SHORT_FR[day.dayOfWeek]} + {fullName} + - + + update(day.dayOfWeek, { + isClosed: !event.target.checked, + slots: + event.target.checked && day.slots.length === 0 + ? [{ open: '10:00', close: '18:30' }] + : day.slots, + }) + } + /> + + {day.isClosed ? ( + Fermé + ) : ( +
+ {day.slots.map((slot, index) => ( + + + updateSlot(day.dayOfWeek, index, 'open', event.target.value) + } + className="rounded-[var(--radius-sm)] border border-[var(--line-strong)] bg-[var(--surface)] px-1.5 py-1 text-sm tabular-nums" + /> + + – + + + updateSlot(day.dayOfWeek, index, 'close', event.target.value) + } + className="rounded-[var(--radius-sm)] border border-[var(--line-strong)] bg-[var(--surface)] px-1.5 py-1 text-sm tabular-nums" + /> + {canEdit && day.slots.length > 1 ? ( + + ) : null} + + ))} +
+ )} + + {canEdit && !day.isClosed ? ( + + {day.slots.length < MAX_SLOTS_PER_DAY ? ( + + ) : null} + + + ) : null} {isChanged ? ( - - modifié - + ) : null}
- {day.isClosed ? ( -

Fermé toute la journée.

- ) : ( -
- {day.slots.map((slot, index) => ( -
- - updateSlot(day.dayOfWeek, index, 'open', event.target.value) - } - className="rounded-[var(--radius-sm)] border border-[var(--line-strong)] bg-[var(--surface)] px-2 py-1.5 text-base tabular-nums" - /> - - – - - - updateSlot(day.dayOfWeek, index, 'close', event.target.value) - } - className="rounded-[var(--radius-sm)] border border-[var(--line-strong)] bg-[var(--surface)] px-2 py-1.5 text-base tabular-nums" - /> - {canEdit && day.slots.length > 1 ? ( - - ) : null} -
- ))} - - {canEdit && day.slots.length < MAX_SLOTS_PER_DAY ? ( - - ) : null} - - {canEdit ? ( -
- -
- ) : null} -
- )} - {dayErrors.length > 0 ? ( -
    +
      {dayErrors.map((error) => (
    • {error}
    • ))}
    ) : null} -
+
); })}
{canEdit ? ( -
+
@@ -237,23 +256,20 @@ export default function WeekEditor({ initial, canEdit }: Props) { type="button" onClick={() => { setWeek(saved); - setMessage(null); - setServerErrors({}); + touch(); }} className="text-sm text-[var(--ink-muted)] underline-offset-2 hover:underline" > - Annuler les modifications + Annuler ) : null}

{blocked - ? 'Corrigez les erreurs ci-dessus.' + ? 'Corrigez les erreurs signalées.' : pendingChanges.length === 0 ? 'Aucune modification.' - : `${pendingChanges.length} jour${pendingChanges.length > 1 ? 's' : ''} sera modifié : ${pendingChanges - .map((dayOfWeek) => DAY_NAMES_FR[dayOfWeek]) - .join(', ')}.`} + : `Sera modifié : ${pendingChanges.map((d) => DAY_SHORT_FR[d]).join(', ')}.`}

) : null} @@ -261,7 +277,7 @@ export default function WeekEditor({ initial, canEdit }: Props) { {message ? (

+

Horaires fixes

-

- La semaine de référence. Les fermetures ponctuelles, les vacances et les jours fériés se - règlent ailleurs et priment sur ces horaires. +

+ Semaine de référence. Fermetures ponctuelles, vacances et jours fériés priment sur ces + horaires.

diff --git a/lib/schedule/week.ts b/lib/schedule/week.ts index 857b613..8853355 100644 --- a/lib/schedule/week.ts +++ b/lib/schedule/week.ts @@ -12,6 +12,17 @@ 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',