'use client'; import { useMemo, useState, useTransition } from 'react'; import { MAX_SLOTS_PER_DAY } from '@/lib/schedule/validate'; import { changedDays, DAY_NAMES_FR, duplicateToOpenDays, validateWeek, type WeekDraft, } from '@/lib/schedule/week'; import { saveWeek } from './actions'; type Props = { initial: WeekDraft; canEdit: boolean; }; /** * The reference week. * * 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); 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 update(dayOfWeek: number, patch: Partial) { setMessage(null); setServerErrors({}); setWeek((current) => current.map((day) => (day.dayOfWeek === dayOfWeek ? { ...day, ...patch } : day)), ); } function updateSlot(dayOfWeek: number, index: number, field: 'open' | 'close', value: string) { setWeek((current) => current.map((day) => day.dayOfWeek === dayOfWeek ? { ...day, slots: day.slots.map((slot, position) => position === index ? { ...slot, [field]: value } : slot, ), } : 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({}); startTransition(async () => { const result = await saveWeek(week); if (result.ok) { setSaved(week); setMessage({ kind: 'ok', text: result.changed.length === 0 ? 'Aucun changement à enregistrer.' : `Horaires enregistrés (${result.changed.length} jour${result.changed.length > 1 ? 's' : ''} modifié${result.changed.length > 1 ? 's' : ''}).`, }); } else { setServerErrors(result.dayErrors ?? {}); setMessage({ kind: 'error', text: result.error }); } }); } return (
{!canEdit ? (

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

) : null}
{week.map((day) => { const dayErrors = [...(errors[day.dayOfWeek] ?? []), ...(serverErrors[day.dayOfWeek] ?? [])]; const isChanged = pendingChanges.includes(day.dayOfWeek); return (
0 ? 'border-[var(--danger)]' : 'border-[var(--line)]' }`} >

{DAY_NAMES_FR[day.dayOfWeek]}

{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 ? (
{pendingChanges.length > 0 ? ( ) : null}

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

) : null} {message ? (

{message.text}

) : null}
); }