Ein namentlich adressiertes Schreiben landet bei der Person, die den Vorgang kennt, statt in der Poststelle. Am Vorgang laesst sich jetzt ein Kontakt aus dem Verzeichnis als Ansprechpartner benennen; die Sachstandsanfrage redet ihn an, und sein Name steht ueber der Behoerde. Die Sachstandsanfrage konnte das schon - sie bekam den Namen nur nie uebergeben. entwurf.ts lud einen der verknuepften Kontakte und verwarf ihn wieder. Wer angeredet wird, ist jetzt eine Entscheidung und kein Zufall. Aufbau: - Migration: ansprechpartnerId am Vorgang (SetNull - ein Vorgang darf nicht daran haengen, dass es den Kontakt noch gibt) und anrede am Kontakt. - Die Anrede als reine Funktion in kontakte/anrede.ts. Geraten wird nie: ohne hinterlegte Anrede wird mit vollem Namen angeredet. Ein leerer Name mit gesetzter Anrede faellt auf die allgemeine Anrede zurueck - "Sehr geehrte Frau ," darf nie herausgehen. - Der Wechsel des Ansprechpartners steht mit NAMEN im Protokoll. Zwei cuids nebeneinander waeren protokolliert, aber nicht nachvollziehbar, und nachvollziehbar war die Anforderung. - Der Ansprechpartner wird gegen die Akte geprueft, bevor er gespeichert wird - sein Name steht anschliessend im Briefkopf. Nebenbei behoben: Der Schreiben-Editor verwarf die empfaengerAnschrift aus dem Entwurf, sein State startete immer leer. Jetzt kommt die Anschrift des Ansprechpartners im Formular an. Geprueft: neun Einheitstests fuer die Anrede, fuenf End-to-End-Tests von der Auswahl bis zum Protokolleintrag. Refs #9
247 lines
7.4 KiB
TypeScript
247 lines
7.4 KiB
TypeScript
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()
|
|
|
|
/** Ein Verweis auf einen anderen Datensatz - leer heisst "keiner". */
|
|
const optionaleId = z
|
|
.string()
|
|
.trim()
|
|
.transform((wert) => (wert === '' ? null : wert))
|
|
.nullable()
|
|
.default(null)
|
|
|
|
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<VorgangKategorie, string> = {
|
|
GERICHT: 'Betreuungsgericht',
|
|
SOZIALHILFE: 'Sozialhilfe',
|
|
IMMOBILIE: 'Immobilie',
|
|
VERSICHERUNG: 'Versicherung',
|
|
HEIM_PFLEGE: 'Heim & Pflege',
|
|
VERTRAEGE: 'Verträge',
|
|
STEUERN: 'Steuern',
|
|
SONSTIGES: 'Sonstiges',
|
|
}
|
|
|
|
export const PRIORITAET_BESCHRIFTUNG: Record<number, string> = {
|
|
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),
|
|
ansprechpartnerId: optionaleId,
|
|
verantwortlich: optionalerText(120),
|
|
naechsterSchritt: optionalerText(300),
|
|
faelligAm: optionalesDatum,
|
|
letzterKontaktAm: optionalesDatum,
|
|
genehmigungspflichtig: z.coerce.boolean().default(false),
|
|
})
|
|
|
|
export type VorgangEingabe = z.infer<typeof VorgangSchema>
|
|
|
|
/**
|
|
* 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<typeof VorgangAnlegenSchema>
|
|
|
|
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<typeof GenehmigungSchema>
|
|
|
|
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<string, string>
|
|
werte?: Record<string, string>
|
|
/** 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<string, unknown> {
|
|
const roh: Record<string, unknown> = {}
|
|
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<string, string> {
|
|
const flach = fehler.flatten().fieldErrors
|
|
const felder: Record<string, string> = {}
|
|
for (const [name, meldungen] of Object.entries(flach)) {
|
|
const erste = meldungen?.[0]
|
|
if (erste) felder[name] = erste
|
|
}
|
|
return felder
|
|
}
|