'use server'; import { revalidatePath } from 'next/cache'; import { diffOf, recordAudit } from '@/lib/audit'; import { requireAdmin } from '@/lib/auth/actions'; import { prisma } from '@/lib/db'; import type { CivilDate } from '@/lib/schedule/civil'; import { checkRange, expandRange, MAX_RANGE_DAYS } from '@/lib/schedule/exceptions'; import type { Slot } from '@/lib/schedule/types'; import { validateSlots } from '@/lib/schedule/validate'; export type ExceptionReason = 'TEMPORARY' | 'SPECIAL_EVENT'; export type ActionResult = { ok: true } | { ok: false; error: string }; export type ExceptionInput = { date: CivilDate; isClosed: boolean; slots: Slot[]; reason: ExceptionReason; noteFr: string | null; }; export async function saveException(input: ExceptionInput): Promise { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const slots = input.isClosed ? [] : input.slots; if (!input.isClosed) { const problems = validateSlots(slots); if (slots.length === 0) { problems.push('Une ouverture exceptionnelle doit avoir au moins une plage horaire.'); } if (problems.length > 0) { return { ok: false, error: problems.join(' ') }; } } const date = asDate(input.date); const existing = await prisma.scheduleException.findUnique({ where: { date } }); await prisma.scheduleException.upsert({ where: { date }, update: { isClosed: input.isClosed, slots: input.isClosed ? undefined : slots, reason: input.reason, noteFr: input.noteFr, // The French text changed, so any English translation is stale. noteEn: existing?.noteFr === input.noteFr ? undefined : null, source: 'MANUAL', }, create: { date, isClosed: input.isClosed, slots: input.isClosed ? undefined : slots, reason: input.reason, noteFr: input.noteFr, source: 'MANUAL', createdBy: gate.value.email, }, }); await recordAudit({ userEmail: gate.value.email, action: existing ? 'exception.update' : 'exception.create', entity: 'ScheduleException', entityId: input.date, diff: diffOf( existing ? { fermé: existing.isClosed, horaires: summarise(existing.slots as Slot[] | null), note: existing.noteFr } : null, { fermé: input.isClosed, horaires: summarise(slots), note: input.noteFr }, ), }); revalidateAll(); return { ok: true }; } export async function saveExceptionRange(input: { start: CivilDate; end: CivilDate; noteFr: string | null; }): Promise { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const problem = checkRange(input.start, input.end); if (problem === 'EMPTY') { return { ok: false, error: 'Indiquez une date de début et une date de fin.' }; } if (problem === 'REVERSED') { return { ok: false, error: 'La date de fin précède la date de début.' }; } if (problem === 'TOO_LONG') { return { ok: false, error: `Période trop longue (${MAX_RANGE_DAYS} jours au maximum). Utilisez les vacances pour une fermeture plus longue.`, }; } const dates = expandRange(input.start, input.end); await prisma.$transaction( dates.map((date) => prisma.scheduleException.upsert({ where: { date: asDate(date) }, update: { isClosed: true, slots: undefined, reason: 'TEMPORARY', noteFr: input.noteFr, noteEn: null, source: 'MANUAL' }, create: { date: asDate(date), isClosed: true, reason: 'TEMPORARY', noteFr: input.noteFr, source: 'MANUAL', createdBy: gate.value.email, }, }), ), ); await recordAudit({ userEmail: gate.value.email, action: 'exception.create_range', entity: 'ScheduleException', entityId: `${input.start}…${input.end}`, diff: { période: { before: null, after: `${dates.length} jours fermés` } }, }); revalidateAll(); return { ok: true }; } export async function deleteException(date: CivilDate): Promise { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const existing = await prisma.scheduleException.findUnique({ where: { date: asDate(date) } }); if (!existing) { return { ok: false, error: 'Cette exception a déjà été supprimée.' }; } await prisma.scheduleException.delete({ where: { date: asDate(date) } }); await recordAudit({ userEmail: gate.value.email, action: 'exception.delete', entity: 'ScheduleException', entityId: date, diff: diffOf( { fermé: existing.isClosed, horaires: summarise(existing.slots as Slot[] | null), note: existing.noteFr }, null, ), }); revalidateAll(); return { ok: true }; } function summarise(slots: Slot[] | null): string { if (!slots || slots.length === 0) { return 'Fermé'; } return slots.map((slot) => `${slot.open}–${slot.close}`).join(' · '); } function asDate(value: CivilDate): Date { return new Date(`${value}T00:00:00.000Z`); } function revalidateAll(): void { revalidatePath('/admin'); revalidatePath('/admin/exceptions'); }