import { z } from 'zod' import type { VorgangKategorie, VorgangStatus } from '@prisma/client' import { GENEHMIGUNGSKETTE, SEITENAUSGANG } from '@/lib/vorgaenge/genehmigungskette' import { VORLAGEN_SCHLUESSEL } from '@/lib/vorgaenge/vorlagen' /** * Eingabeprüfung für Vorgänge, Aufgaben und Genehmigungen. * * Wie überall gilt: Die Prüfung läuft serverseitig, das Formular im * Browser ist nur Bequemlichkeit. Meldungen sagen, was zu tun ist. */ /** Leere Formularfelder sollen zu null werden, nicht zu "". */ const optionalerText = (max = 200) => z .string() .trim() .max(max, `Bitte höchstens ${max} Zeichen.`) .transform((wert) => (wert === '' ? null : wert)) .nullable() const optionalesDatum = z .string() .trim() .transform((wert) => (wert === '' ? null : wert)) .nullable() .refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), { message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.', }) // Mitternacht UTC: Faelligkeiten sind @db.Date, ohne Uhrzeit. Sonst // verschiebt die Zeitzone einen Termin um einen Tag. .transform((wert) => (wert === null ? null : new Date(`${wert}T00:00:00.000Z`))) export const VORGANG_KATEGORIEN = [ 'GERICHT', 'SOZIALHILFE', 'IMMOBILIE', 'VERSICHERUNG', 'HEIM_PFLEGE', 'VERTRAEGE', 'STEUERN', 'SONSTIGES', ] as const satisfies readonly VorgangKategorie[] export const VORGANG_STATUS = [ 'OFFEN', 'IN_ARBEIT', 'WARTET_AUF_DRITTE', 'WARTET_AUF_GENEHMIGUNG', 'ERLEDIGT', ] as const satisfies readonly VorgangStatus[] export const KATEGORIE_BESCHRIFTUNG: Record = { GERICHT: 'Betreuungsgericht', SOZIALHILFE: 'Sozialhilfe', IMMOBILIE: 'Immobilie', VERSICHERUNG: 'Versicherung', HEIM_PFLEGE: 'Heim & Pflege', VERTRAEGE: 'Verträge', STEUERN: 'Steuern', SONSTIGES: 'Sonstiges', } export const PRIORITAET_BESCHRIFTUNG: Record = { 1: 'Hoch', 2: 'Normal', 3: 'Niedrig', } // ----------------------------------------------------------------------------- // Vorgang // ----------------------------------------------------------------------------- export const VorgangSchema = z.object({ titel: z .string() .trim() .min(3, 'Bitte gib dem Vorgang einen Titel, an dem du ihn wiedererkennst.') .max(200, 'Bitte höchstens 200 Zeichen.'), kategorie: z.enum(VORGANG_KATEGORIEN, { errorMap: () => ({ message: 'Bitte eine Kategorie wählen.' }), }), status: z.enum(VORGANG_STATUS).default('OFFEN'), prioritaet: z.coerce .number() .int() .min(1, 'Die Priorität liegt zwischen 1 (hoch) und 3 (niedrig).') .max(3, 'Die Priorität liegt zwischen 1 (hoch) und 3 (niedrig).') .default(2), beschreibung: optionalerText(4000), gegenstelle: optionalerText(150), aktenzeichen: optionalerText(80), verantwortlich: optionalerText(120), naechsterSchritt: optionalerText(300), faelligAm: optionalesDatum, letzterKontaktAm: optionalesDatum, genehmigungspflichtig: z.coerce.boolean().default(false), }) export type VorgangEingabe = z.infer /** * Anlegen aus einer Vorlage. Der Schlüssel muss eine bekannte Vorlage sein * oder fehlen (leerer Vorgang) - erfundene Schlüssel würden sonst still * einen Vorgang ohne Aufgaben erzeugen. */ export const VorgangAnlegenSchema = VorgangSchema.extend({ vorlageSchluessel: z .string() .trim() .transform((wert) => (wert === '' ? null : wert)) .nullable() .refine((wert) => wert === null || VORLAGEN_SCHLUESSEL.includes(wert), { message: 'Diese Vorlage gibt es nicht.', }), }) export type VorgangAnlegenEingabe = z.infer export const VorgangStatusSchema = z.object({ vorgangId: z.string().min(1), status: z.enum(VORGANG_STATUS, { errorMap: () => ({ message: 'Diesen Status gibt es nicht.' }), }), }) // ----------------------------------------------------------------------------- // Aufgaben // ----------------------------------------------------------------------------- export const AufgabeSchema = z.object({ vorgangId: z.string().min(1), titel: z .string() .trim() .min(3, 'Bitte beschreibe den Handgriff in ein paar Worten.') .max(200, 'Bitte höchstens 200 Zeichen.'), beschreibung: optionalerText(2000), faelligAm: optionalesDatum, }) export const AufgabeAbhakenSchema = z.object({ aufgabeId: z.string().min(1), erledigt: z.coerce.boolean(), }) // ----------------------------------------------------------------------------- // Genehmigungen // ----------------------------------------------------------------------------- export const GENEHMIGUNG_STATUS_WERTE = [...GENEHMIGUNGSKETTE, SEITENAUSGANG] as [ string, ...string[], ] export const GenehmigungSchema = z.object({ vorgangId: z.string().min(1), titel: z .string() .trim() .min(3, 'Bitte benenne das Geschäft, das genehmigt werden soll.') .max(200, 'Bitte höchstens 200 Zeichen.'), rechtsgrundlage: z .string() .trim() .min(3, 'Bitte die Vorschrift angeben, z. B. „§ 1850 BGB“.') .max(120, 'Bitte höchstens 120 Zeichen.'), notizen: optionalerText(4000), mitteilungAm: optionalesDatum, antragAm: optionalesDatum, anhoerungAm: optionalesDatum, beschlussAm: optionalesDatum, rechtskraftAm: optionalesDatum, vollzugAm: optionalesDatum, }) export type GenehmigungEingabe = z.infer export const GenehmigungStatusSchema = z.object({ genehmigungId: z.string().min(1), status: z.enum(GENEHMIGUNG_STATUS_WERTE, { errorMap: () => ({ message: 'Diesen Status gibt es in der Genehmigungskette nicht.' }), }), /** Datum der neuen Stufe, sonst wird das heutige gesetzt. */ datum: optionalesDatum, }) export const GenehmigungBeschlussSchema = z.object({ genehmigungId: z.string().min(1), /** Leerer Wert löst die Verknüpfung wieder. */ dokumentId: z .string() .trim() .transform((wert) => (wert === '' ? null : wert)) .nullable(), }) // ----------------------------------------------------------------------------- /** Gemeinsamer Rückgabetyp aller Formular-Aktionen (useActionState). */ export type Formularzustand = { fehler?: string felder?: Record werte?: Record /** Gesetzt, wenn alles geklappt hat - die Oberfläche kann es melden. */ erfolg?: string } /** * Liest ein FormData-Objekt in die Struktur, die die Schemata erwarten. * Kontrollkästchen liefern "on" oder gar nichts. */ export function formularLesen( formular: FormData, kontrollkaestchen: string[] = [], ): Record { const roh: Record = {} for (const [name, wert] of formular.entries()) { if (kontrollkaestchen.includes(name)) continue roh[name] = wert } for (const name of kontrollkaestchen) { roh[name] = formular.get(name) === 'on' || formular.get(name) === 'true' } return roh } /** Zod-Fehler in die flache Feldkarte des Formularzustands übersetzen. */ export function felderAusFehler(fehler: z.ZodError): Record { const flach = fehler.flatten().fieldErrors const felder: Record = {} for (const [name, meldungen] of Object.entries(flach)) { const erste = meldungen?.[0] if (erste) felder[name] = erste } return felder }