'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 { MESSAGE_MAX_CHARS } from '@/lib/screen/contract'; import { translateToEnglish } from '@/lib/translation/service'; export type ActionResult = { ok: true; value: T } | { ok: false; error: string }; /** * Saves a reusable phrase. * * Templates are translated like any other message, but once rather than at * every use: that is most of the point of having them. */ export async function saveTemplate(input: { id?: string; textFr: string; sortOrder: number; }): Promise> { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const textFr = input.textFr.trim(); if (textFr.length === 0) { return { ok: false, error: 'Le modèle ne peut pas être vide.' }; } if (textFr.length > MESSAGE_MAX_CHARS) { return { ok: false, error: `Le modèle dépasse ${MESSAGE_MAX_CHARS} caractères et ne tiendrait pas sur l’écran.`, }; } const existing = input.id ? await prisma.messageTemplate.findUnique({ where: { id: input.id } }) : null; // The French text is the identity of a template: two identical phrases are // one phrase, and a duplicate would only ever be half-corrected later. const clash = await prisma.messageTemplate.findUnique({ where: { textFr } }); if (clash && clash.id !== existing?.id) { return { ok: false, error: 'Ce modèle existe déjà.' }; } const textChanged = existing ? existing.textFr !== textFr : true; const data = { textFr, sortOrder: input.sortOrder, ...(textChanged ? { textEn: null, translationStatus: 'PENDING' as const } : {}), }; const saved = existing ? await prisma.messageTemplate.update({ where: { id: existing.id }, data }) : await prisma.messageTemplate.create({ data: { ...data, createdBy: gate.value.email } }); await recordAudit({ userEmail: gate.value.email, action: existing ? 'template.update' : 'template.create', entity: 'MessageTemplate', entityId: saved.id, diff: diffOf(existing ? { texte: existing.textFr } : null, { texte: textFr }), }); revalidateAll(); return { ok: true, value: { id: saved.id } }; } export async function translateTemplate(id: string): Promise> { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const template = await prisma.messageTemplate.findUnique({ where: { id } }); if (!template) { return { ok: false, error: 'Ce modèle a été supprimé.' }; } if (template.translationStatus === 'MANUAL') { return { ok: false, error: 'La traduction a été saisie à la main; elle n’est pas écrasée.' }; } const outcome = await translateToEnglish(template.textFr); if (!outcome.ok) { await prisma.messageTemplate.update({ where: { id }, data: { translationStatus: 'ERROR' } }); revalidateAll(); return { ok: false, error: outcome.error }; } await prisma.messageTemplate.update({ where: { id }, data: { textEn: outcome.text, translationStatus: 'DONE' }, }); revalidateAll(); return { ok: true, value: { textEn: outcome.text } }; } export async function setTemplateTranslation(id: string, textEn: string): Promise { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const trimmed = textEn.trim(); const existing = await prisma.messageTemplate.findUnique({ where: { id } }); if (!existing) { return { ok: false, error: 'Ce modèle a été supprimé.' }; } await prisma.messageTemplate.update({ where: { id }, data: { textEn: trimmed || null, translationStatus: trimmed ? 'MANUAL' : 'PENDING' }, }); await recordAudit({ userEmail: gate.value.email, action: 'template.translate_manual', entity: 'MessageTemplate', entityId: id, diff: diffOf({ anglais: existing.textEn }, { anglais: trimmed || null }), }); revalidateAll(); return { ok: true, value: undefined }; } export async function deleteTemplate(id: string): Promise { const gate = await requireAdmin(); if (!gate.ok) { return { ok: false, error: gate.error }; } const existing = await prisma.messageTemplate.findUnique({ where: { id } }); if (!existing) { return { ok: false, error: 'Ce modèle a déjà été supprimé.' }; } await prisma.messageTemplate.delete({ where: { id } }); await recordAudit({ userEmail: gate.value.email, action: 'template.delete', entity: 'MessageTemplate', entityId: id, diff: diffOf({ texte: existing.textFr }, null), }); revalidateAll(); return { ok: true, value: undefined }; } function revalidateAll(): void { // Templates appear in the pickers on every page that composes a message. revalidatePath('/admin/modeles'); revalidatePath('/admin/exceptions'); revalidatePath('/admin/vacances'); revalidatePath('/admin/messages'); }