'use server' import { redirect } from 'next/navigation' import { revalidatePath } from 'next/cache' import { z } from 'zod' import { db, nichtGeloescht } from '@/lib/db' import { sealen, oeffnen } from '@/lib/crypto/feld' import { aenderungErmitteln, protokollieren } from '@/lib/audit' import { erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung' import { BETREUTE_GEHEIME_FELDER, BetreuteSchema, formularLesen, } from '@/lib/validierung/betreute' /** * Stammdaten der betreuten Person anlegen und aendern. * * Das Bestellungsdatum ist hier das wichtigste Feld: aus ihm leiten sich * das Betreuungsjahr und damit fast alle gesetzlichen Fristen ab. Wird es * gesetzt oder geaendert, werden die Pflichtfristen neu abgeleitet. */ export type BetreuteZustand = { fehler?: string felder?: Record /** Rohwerte, damit ein langes Formular nach einem Fehler nicht leer ist. */ werte?: Record } /** Teilt die geprueften Daten in Klartextfelder und zu verschluesselnde. */ function aufteilen(daten: z.infer) { const geheim: Record = {} const offen: Record = {} for (const [name, wert] of Object.entries(daten)) { if ((BETREUTE_GEHEIME_FELDER as readonly string[]).includes(name)) { geheim[name] = wert as string | null } else { offen[name] = wert } } return { offen, verschluesselt: sealen('Betreute', geheim) } } function felderAusFehler(fehler: z.ZodError): Record { const flach = fehler.flatten().fieldErrors const ergebnis: Record = {} for (const [feld, meldungen] of Object.entries(flach)) { if (meldungen?.[0]) ergebnis[feld] = meldungen[0] } return ergebnis } function werteMerken(formular: FormData): Record { const werte: Record = {} for (const [name, wert] of formular.entries()) { if (typeof wert === 'string') werte[name] = wert } return werte } // ----------------------------------------------------------------------------- // Anlegen // ----------------------------------------------------------------------------- export async function betreuteAnlegen( _vorher: BetreuteZustand, formular: FormData, ): Promise { const benutzer = await erfordereBenutzer() // Nur Betreuer und Administratoren duerfen eine Betreuung anlegen. if (benutzer.rolle === 'MITLESER') { return { fehler: 'Als Mitleser kannst du keine Betreuung anlegen.' } } const gelesen = BetreuteSchema.safeParse(formularLesen(formular)) if (!gelesen.success) { return { fehler: 'Bitte prüfe die markierten Felder.', felder: felderAusFehler(gelesen.error), werte: werteMerken(formular), } } const { offen, verschluesselt } = aufteilen(gelesen.data) const betreute = await db.$transaction(async (tx) => { const angelegt = await tx.betreute.create({ data: { ...offen, ...verschluesselt } as never, }) // Der Anlegende bekommt sofort vollen Zugang - sonst saehe er die // gerade angelegte Person nicht. await tx.betreuungsZugang.create({ data: { userId: benutzer.id, betreuteId: angelegt.id, rolle: 'BETREUER' }, }) await protokollieren({ tx, aktion: 'ERSTELLT', entitaet: 'Betreute', entitaetId: angelegt.id, bezeichnung: `${angelegt.vorname} ${angelegt.nachname}`, betreuteId: angelegt.id, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, }) return angelegt }) // Aus dem Bestellungsdatum die gesetzlichen Pflichtfristen ableiten. await pflichtfristenAbleitenWennMoeglich(betreute.id) revalidatePath('/', 'layout') redirect('/heute') } // ----------------------------------------------------------------------------- // Aendern // ----------------------------------------------------------------------------- export async function betreuteAendern( _vorher: BetreuteZustand, formular: FormData, ): Promise { const betreuteId = String(formular.get('betreuteId') ?? '') if (!betreuteId) return { fehler: 'Es fehlt die Angabe, wer geändert werden soll.' } const { benutzer, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben') const gelesen = BetreuteSchema.safeParse(formularLesen(formular)) if (!gelesen.success) { return { fehler: 'Bitte prüfe die markierten Felder.', felder: felderAusFehler(gelesen.error), werte: werteMerken(formular), } } const { offen, verschluesselt } = aufteilen(gelesen.data) // Fuer das Protokoll wird der Klartext verglichen, nicht der Umschlag - // der aendert sich bei jedem Speichern, auch wenn der Inhalt gleich ist. const vorherOffen = oeffnen('Betreute', betreute as unknown as Record) const nachher = { ...offen, ...gelesen.data } const bestellungGeaendert = betreute.bestellungAm?.getTime() !== (gelesen.data.bestellungAm?.getTime() ?? undefined) await db.$transaction(async (tx) => { await tx.betreute.update({ where: { id: betreuteId }, data: { ...offen, ...verschluesselt } as never, }) await protokollieren({ tx, aktion: 'GEAENDERT', entitaet: 'Betreute', entitaetId: betreuteId, bezeichnung: `${gelesen.data.vorname} ${gelesen.data.nachname}`, betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, aenderung: aenderungErmitteln(vorherOffen, nachher), }) }) // Aendert sich das Bestellungsdatum, verschiebt sich das gesamte // Betreuungsjahr - die Pflichtfristen muessen neu abgeleitet werden. if (bestellungGeaendert) { await pflichtfristenAbleitenWennMoeglich(betreuteId) } revalidatePath('/', 'layout') redirect('/stammdaten') } /** * Ruft die Fristenableitung auf, sobald das Fristenmodul vorhanden ist. * * Der dynamische Import haelt die Stammdaten unabhaengig: fehlt das Modul * (frueher Bauzustand), wird die Ableitung uebersprungen, statt dass das * Speichern der Stammdaten scheitert. */ async function pflichtfristenAbleitenWennMoeglich(betreuteId: string): Promise { try { const modul = await import('@/lib/fristen/pflichtfristen') await modul.pflichtfristenSchreiben(betreuteId) } catch (fehler) { console.warn('[stammdaten] Pflichtfristen konnten nicht abgeleitet werden:', fehler) } } // ----------------------------------------------------------------------------- // Papierkorb // ----------------------------------------------------------------------------- export async function betreuteInPapierkorb(betreuteId: string): Promise { const { benutzer, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben') await db.betreute.update({ where: { id: betreuteId, ...nichtGeloescht }, data: { geloeschtAm: new Date() }, }) await protokollieren({ aktion: 'GELOESCHT', entitaet: 'Betreute', entitaetId: betreuteId, bezeichnung: `${betreute.vorname} ${betreute.nachname}`, betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, }) revalidatePath('/', 'layout') }