'use server' import { revalidatePath } from 'next/cache' import { z } from 'zod' import { db, nichtGeloescht } from '@/lib/db' import { erfordereZugang, ZugriffVerweigert } from '@/lib/auth/sitzung' import { protokollieren, aenderungErmitteln } from '@/lib/audit' import { bestellungsabsatzFehlt } from '@/lib/schreiben/platzhalter' /** * Server Actions fuer eigene Schreibvorlagen. * * Die Warnung beim fehlenden Bestellungsabsatz ist zweistufig: Der erste * Speicherversuch ohne ihn kommt mit einer Warnung zurueck; erst die * ausdrueckliche Bestaetigung ("ohneBestellungsabsatz") speichert. So * laesst sich der Absatz weglassen - aber nur wissentlich. Ohne den * Nachweis der Bestellung antwortet keine Versicherung, und das faellt * erst nach vier Wochen auf. */ export type VorlagenZustand = { fehler?: string /** Die Bestellungsabsatz-Warnung - Speichern erst nach Bestaetigung. */ warnung?: string erfolg?: string felder?: Record } function zugriffsfehler(ausnahme: unknown): VorlagenZustand | null { if (ausnahme instanceof ZugriffVerweigert) return { fehler: ausnahme.message } return null } const vorlageSchema = z.object({ betreuteId: z.string().min(1), vorlageId: z.string().trim().default(''), name: z.string().trim().min(1, 'Bitte einen Namen vergeben.').max(120), betreff: z.string().trim().max(200).default(''), text: z.string().trim().min(10, 'Der Text ist zu kurz für eine Vorlage.').max(50_000), ohneBestellungsabsatz: z.string().default(''), }) export async function vorlageSpeichern( _zustand: VorlagenZustand, daten: FormData, ): Promise { const gelesen = vorlageSchema.safeParse({ betreuteId: daten.get('betreuteId'), vorlageId: daten.get('vorlageId') ?? '', name: daten.get('name'), betreff: daten.get('betreff') ?? '', text: daten.get('text'), ohneBestellungsabsatz: daten.get('ohneBestellungsabsatz') ?? '', }) if (!gelesen.success) { const felder = gelesen.error.flatten().fieldErrors return { fehler: 'Bitte prüfe die Eingaben.', felder: Object.fromEntries( Object.entries(felder).map(([name, meldungen]) => [name, meldungen?.[0] ?? '']), ), } } const werte = gelesen.data const bestaetigt = werte.ohneBestellungsabsatz === 'ja' try { const { benutzer } = await erfordereZugang(werte.betreuteId, 'DOKUMENTE', 'schreiben') // Die zweistufige Warnung: erst warnen, dann - bestaetigt - speichern. if (bestellungsabsatzFehlt(werte.text) && !bestaetigt) { return { warnung: 'In dieser Vorlage fehlt der Bestellungsabsatz — der Nachweis, dass du zur Vertretung ' + 'befugt bist. Ohne ihn antworten Versicherungen und Behörden mit „aus ' + 'datenschutzrechtlichen Gründen keine Auskunft", und das fällt erst nach Wochen auf. ' + 'Füge {{bestellungsabsatz}} ein — oder speichere bewusst ohne.', } } if (werte.vorlageId) { const vorhanden = await db.schreibvorlage.findFirst({ where: { id: werte.vorlageId, betreuteId: werte.betreuteId, ...nichtGeloescht }, }) if (!vorhanden) return { fehler: 'Diese Vorlage gibt es nicht (mehr).' } const nachher = await db.schreibvorlage.update({ where: { id: vorhanden.id }, data: { name: werte.name, betreff: werte.betreff, text: werte.text, ohneBestellungsabsatz: bestaetigt, }, }) await protokollieren({ aktion: 'GEAENDERT', entitaet: 'Schreibvorlage', entitaetId: nachher.id, bezeichnung: nachher.name, betreuteId: werte.betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, aenderung: aenderungErmitteln( { name: vorhanden.name, betreff: vorhanden.betreff, text: vorhanden.text }, { name: nachher.name, betreff: nachher.betreff, text: nachher.text }, ), }) } else { const neu = await db.schreibvorlage.create({ data: { betreuteId: werte.betreuteId, name: werte.name, betreff: werte.betreff, text: werte.text, ohneBestellungsabsatz: bestaetigt, }, }) await protokollieren({ aktion: 'ERSTELLT', entitaet: 'Schreibvorlage', entitaetId: neu.id, bezeichnung: neu.name, betreuteId: werte.betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, }) } } catch (ausnahme) { const verweigert = zugriffsfehler(ausnahme) if (verweigert) return verweigert console.error('[vorlagen] Speichern fehlgeschlagen:', ausnahme) return { fehler: 'Die Vorlage konnte nicht gespeichert werden.' } } revalidatePath('/einstellungen/vorlagen') return { erfolg: `„${werte.name}“ ist gespeichert.` } } /** Papierkorb: geloeschtAm setzen, 30 Tage rueckholbar wie ueberall. */ export async function vorlageLoeschen( _zustand: VorlagenZustand, daten: FormData, ): Promise { const betreuteId = String(daten.get('betreuteId') ?? '') const vorlageId = String(daten.get('vorlageId') ?? '') try { const { benutzer } = await erfordereZugang(betreuteId, 'DOKUMENTE', 'schreiben') const vorlage = await db.schreibvorlage.findFirst({ where: { id: vorlageId, betreuteId, ...nichtGeloescht }, select: { id: true, name: true }, }) if (!vorlage) return { fehler: 'Diese Vorlage gibt es nicht (mehr).' } await db.schreibvorlage.update({ where: { id: vorlage.id }, data: { geloeschtAm: new Date() }, }) await protokollieren({ aktion: 'GELOESCHT', entitaet: 'Schreibvorlage', entitaetId: vorlage.id, bezeichnung: vorlage.name, betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, }) } catch (ausnahme) { const verweigert = zugriffsfehler(ausnahme) if (verweigert) return verweigert console.error('[vorlagen] Löschen fehlgeschlagen:', ausnahme) return { fehler: 'Die Vorlage konnte nicht gelöscht werden.' } } revalidatePath('/einstellungen/vorlagen') return { erfolg: 'In den Papierkorb gelegt — 30 Tage rückholbar.' } }