'use client'; import { useMemo, useState, useTransition } from 'react'; import { MAX_SLOTS_PER_DAY } from '@/lib/schedule/validate'; import { changedDays, DAY_NAMES_FR, DAY_SHORT_FR, duplicateToOpenDays, validateWeek, type WeekDraft, } from '@/lib/schedule/week'; import { saveWeek } from './actions'; type Props = { initial: WeekDraft; canEdit: boolean; }; /** * The reference week, as one row per day. * * 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); const [saved, setSaved] = useState(initial); const [pending, startTransition] = useTransition(); const [message, setMessage] = useState<{ kind: 'ok' | 'error'; text: string } | null>(null); const [serverErrors, setServerErrors] = useState>({}); const errors = useMemo(() => validateWeek(week), [week]); const pendingChanges = useMemo(() => changedDays(saved, week), [saved, week]); const blocked = Object.keys(errors).length > 0; 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 ? { ...day, slots: day.slots.map((slot, position) => position === index ? { ...slot, [field]: value } : slot, ), } : day, ), ); } function submit() { touch(); startTransition(async () => { const result = await saveWeek(week); if (result.ok) { setSaved(week); const count = result.changed.length; setMessage({ kind: 'ok', text: count === 0 ? 'Aucun changement à enregistrer.' : `Enregistré — ${count} jour${count > 1 ? 's' : ''} modifié${count > 1 ? 's' : ''}.`, }); } else { setServerErrors(result.dayErrors ?? {}); setMessage({ kind: 'error', text: result.error }); } }); } return (
{!canEdit ? (

Compte en lecture seule : consultation uniquement.

) : null}
{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-t border-[var(--line)]' : ''} ${ dayErrors.length > 0 ? 'bg-[var(--danger-tint)]' : '' }`} >
{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 ? ( ) : null}
{dayErrors.length > 0 ? (
    {dayErrors.map((error) => (
  • {error}
  • ))}
) : null}
); })}
{canEdit ? (
{pendingChanges.length > 0 ? ( ) : null}

{blocked ? 'Corrigez les erreurs signalées.' : pendingChanges.length === 0 ? 'Aucune modification.' : `Sera modifié : ${pendingChanges.map((d) => DAY_SHORT_FR[d]).join(', ')}.`}

) : null} {message ? (

{message.text}

) : null}
); }