diff --git a/app/admin/horaires/WeekEditor.tsx b/app/admin/horaires/WeekEditor.tsx index 5be8671..7c0d3f0 100644 --- a/app/admin/horaires/WeekEditor.tsx +++ b/app/admin/horaires/WeekEditor.tsx @@ -6,7 +6,6 @@ import { MAX_SLOTS_PER_DAY } from '@/lib/schedule/validate'; import { changedDays, DAY_NAMES_FR, - DAY_SHORT_FR, duplicateToOpenDays, validateWeek, type WeekDraft, @@ -20,11 +19,11 @@ type Props = { }; /** - * The reference week, as one row per day. + * The reference week. * - * 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. + * 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. */ export default function WeekEditor({ initial, canEdit }: Props) { const [week, setWeek] = useState(initial); @@ -37,20 +36,15 @@ export default function WeekEditor({ initial, canEdit }: Props) { const pendingChanges = useMemo(() => changedDays(saved, week), [saved, week]); const blocked = Object.keys(errors).length > 0; - function touch() { + function update(dayOfWeek: number, patch: Partial) { 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 @@ -63,21 +57,36 @@ 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() { - touch(); + setMessage(null); + setServerErrors({}); startTransition(async () => { const result = await saveWeek(week); if (result.ok) { setSaved(week); - const count = result.changed.length; setMessage({ kind: 'ok', text: - count === 0 + result.changed.length === 0 ? 'Aucun changement à enregistrer.' - : `Enregistré — ${count} jour${count > 1 ? 's' : ''} modifié${count > 1 ? 's' : ''}.`, + : `Horaires enregistrés (${result.changed.length} jour${result.changed.length > 1 ? 's' : ''} modifié${result.changed.length > 1 ? 's' : ''}).`, }); } else { setServerErrors(result.dayErrors ?? {}); @@ -87,166 +96,138 @@ export default function WeekEditor({ initial, canEdit }: Props) { } return ( -
+
{!canEdit ? ( -

- Compte en lecture seule : consultation uniquement. +

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

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

{DAY_NAMES_FR[day.dayOfWeek]}

- - 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 ? ( -
+
@@ -256,20 +237,23 @@ export default function WeekEditor({ initial, canEdit }: Props) { type="button" onClick={() => { setWeek(saved); - touch(); + setMessage(null); + setServerErrors({}); }} className="text-sm text-[var(--ink-muted)] underline-offset-2 hover:underline" > - Annuler + Annuler les modifications ) : null}

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

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

+

Horaires fixes

-

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

+ 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.

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