Zwei Fehler, beide erst im Betrieb sichtbar. 1. Schritt 3 der Ersteinrichtung meldete "Bitte pruefe die markierten Felder" - und markierte keines. Man kam dort nicht weiter. Das kurze Formular schickt neun Felder, das vollstaendige unter Stammdaten alle. Beide gehen durch dasselbe Schema, und die optionalen Felder waren zwar nullable, aber nicht optional: Ein Feld, das gar nicht abgeschickt wurde, ist undefined, nicht null. Beanstandet wurden also zwei Dutzend Felder, die auf dem Bildschirm nie standen. Damit das nicht wiederkommt, unterscheidet die Aktion jetzt zwischen Eingabefehler und Formular-Schema-Bruch: Beanstandet die Pruefung ein Feld, das nicht abgeschickt wurde, sagt sie das ausdruecklich und schreibt es ins Serverlog, statt es dem Nutzer als Eingabefehler unterzuschieben, den er nirgends finden kann. 2. Eine betreute Person liess sich nicht endgueltig loeschen. Postgres setzt beim Loeschen den Verweis im Audit-Log auf NULL, und genau dieses UPDATE hat der Unveraenderlichkeits-Trigger abgewiesen. Damit scheiterte auch das Leeren des Papierkorbs nach 30 Tagen. Der Trigger erlaubt jetzt genau eine Ausnahme: das Nullen der beiden Fremdschluessel. Jede andere Aenderung bleibt verboten, auch das Setzen eines Fremdschluessels auf einen anderen Wert. Der Nachweis bleibt vollstaendig - es verschwindet nur der Verweis auf einen Datensatz, den es nicht mehr gibt. Geprueft: sieben neue Tests gegen die tatsaechlichen Formularinhalte, der Ablauf einmal komplett im Browser bis zum Dashboard, und der Trigger gegen eine laufende Datenbank - Loeschen gelingt, Faelschen scheitert. Etappe: 8 Phase: Haertung
189 lines
6.6 KiB
TypeScript
189 lines
6.6 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
/**
|
|
* Eingabepruefung fuer die Stammdaten der betreuten Person.
|
|
*
|
|
* Die Pruefung laeuft IMMER serverseitig - das Formular im Browser ist nur
|
|
* Bequemlichkeit. Alle Meldungen sind auf Deutsch und sagen, was zu tun
|
|
* ist, nicht was falsch war.
|
|
*/
|
|
|
|
/**
|
|
* Optionales Textfeld.
|
|
*
|
|
* Drei Eingaben muessen zu null werden:
|
|
* "" das Feld war da, blieb aber leer
|
|
* null ausdruecklich geleert
|
|
* undefined das Feld war im Formular ueberhaupt nicht dabei
|
|
*
|
|
* Der letzte Fall ist der wichtige: Das kurze Formular der Ersteinrichtung
|
|
* schickt nur eine Handvoll Felder, das vollstaendige unter Stammdaten
|
|
* alle. Beide gehen durch dasselbe Schema. Ohne .optional() wuerde die
|
|
* Ersteinrichtung an zwei Dutzend Feldern scheitern, die gar nicht auf dem
|
|
* Bildschirm stehen - und die Meldung liesse sich keinem Feld zuordnen.
|
|
*/
|
|
const optionalerText = (max = 200) =>
|
|
z
|
|
.string()
|
|
.trim()
|
|
.max(max, `Bitte höchstens ${max} Zeichen.`)
|
|
.optional()
|
|
.nullable()
|
|
.transform((wert) => (wert === undefined || wert === '' ? null : wert))
|
|
|
|
/** HTML-Datumsfelder liefern "" oder "2026-01-15" - oder gar nichts. */
|
|
const optionalesDatum = z
|
|
.string()
|
|
.trim()
|
|
.optional()
|
|
.nullable()
|
|
.transform((wert) => (wert === undefined || wert === '' ? null : wert))
|
|
.refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), {
|
|
message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.',
|
|
})
|
|
.transform((wert) => (wert === null ? null : new Date(`${wert}T00:00:00.000Z`)))
|
|
|
|
const pflichtDatum = z
|
|
.string()
|
|
.trim()
|
|
.min(1, 'Bitte ein Datum angeben.')
|
|
.refine((wert) => /^\d{4}-\d{2}-\d{2}$/.test(wert), {
|
|
message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.',
|
|
})
|
|
.transform((wert) => new Date(`${wert}T00:00:00.000Z`))
|
|
|
|
export const AUFGABENKREISE = [
|
|
'VERMOEGENSSORGE',
|
|
'GESUNDHEITSSORGE',
|
|
'AUFENTHALTSBESTIMMUNG',
|
|
'WOHNUNGSANGELEGENHEITEN',
|
|
'BEHOERDENANGELEGENHEITEN',
|
|
'POST_UND_FERNMELDEVERKEHR',
|
|
'RECHTS_ANTRAGS_UND_BEHOERDENVERTRETUNG',
|
|
'ENTSCHEIDUNG_UEBER_FREIHEITSENTZIEHENDE_MASSNAHMEN',
|
|
'SONSTIGES',
|
|
] as const
|
|
|
|
export const AUFGABENKREIS_BESCHRIFTUNG: Record<(typeof AUFGABENKREISE)[number], string> = {
|
|
VERMOEGENSSORGE: 'Vermögenssorge',
|
|
GESUNDHEITSSORGE: 'Gesundheitssorge',
|
|
AUFENTHALTSBESTIMMUNG: 'Aufenthaltsbestimmung',
|
|
WOHNUNGSANGELEGENHEITEN: 'Wohnungsangelegenheiten',
|
|
BEHOERDENANGELEGENHEITEN: 'Behördenangelegenheiten',
|
|
POST_UND_FERNMELDEVERKEHR: 'Post- und Fernmeldeverkehr',
|
|
RECHTS_ANTRAGS_UND_BEHOERDENVERTRETUNG: 'Rechts-, Antrags- und Behördenvertretung',
|
|
ENTSCHEIDUNG_UEBER_FREIHEITSENTZIEHENDE_MASSNAHMEN:
|
|
'Entscheidung über freiheitsentziehende Maßnahmen',
|
|
SONSTIGES: 'Sonstiges',
|
|
}
|
|
|
|
export const BetreuteSchema = z.object({
|
|
// --- Person ---------------------------------------------------------------
|
|
anrede: optionalerText(20),
|
|
vorname: z.string().trim().min(1, 'Bitte den Vornamen angeben.').max(100),
|
|
nachname: z.string().trim().min(1, 'Bitte den Nachnamen angeben.').max(100),
|
|
geburtsname: optionalerText(100),
|
|
geburtsdatum: pflichtDatum.refine((datum) => datum <= new Date(), {
|
|
message: 'Das Geburtsdatum kann nicht in der Zukunft liegen.',
|
|
}),
|
|
geburtsort: optionalerText(100),
|
|
|
|
// --- Heim -----------------------------------------------------------------
|
|
heimName: optionalerText(150),
|
|
heimStrasse: optionalerText(150),
|
|
heimPlz: optionalerText(10),
|
|
heimOrt: optionalerText(100),
|
|
heimZimmer: optionalerText(50),
|
|
heimTelefon: optionalerText(50),
|
|
heimEinzugAm: optionalesDatum,
|
|
|
|
fruehereStrasse: optionalerText(150),
|
|
frueherePlz: optionalerText(10),
|
|
fruehererOrt: optionalerText(100),
|
|
|
|
// --- Gesundheit (verschluesselt gespeichert) ------------------------------
|
|
krankenkasse: optionalerText(150),
|
|
krankenversichertennummer: optionalerText(50),
|
|
pflegekasse: optionalerText(150),
|
|
pflegegrad: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(1, 'Der Pflegegrad liegt zwischen 1 und 5.')
|
|
.max(5, 'Der Pflegegrad liegt zwischen 1 und 5.')
|
|
.nullable()
|
|
.catch(null),
|
|
pflegegradSeit: optionalesDatum,
|
|
pflegegradBescheidVom: optionalesDatum,
|
|
schwerbehinderungGdB: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(20, 'Ein Grad der Behinderung wird ab 20 festgestellt.')
|
|
.max(100, 'Der Grad der Behinderung geht bis 100.')
|
|
.nullable()
|
|
.catch(null),
|
|
schwerbehinderungMerkzeichen: optionalerText(50),
|
|
diagnosen: optionalerText(4000),
|
|
ermittelterWille: optionalerText(4000),
|
|
patientenverfuegungVorhanden: z.coerce.boolean().default(false),
|
|
vorsorgevollmachtVorhanden: z.coerce.boolean().default(false),
|
|
|
|
// --- Betreuungsgericht ----------------------------------------------------
|
|
betreuungsgericht: optionalerText(150),
|
|
aktenzeichenGericht: optionalerText(80),
|
|
bestellungAm: optionalesDatum,
|
|
bestellungBis: optionalesDatum,
|
|
aufgabenkreise: z.array(z.enum(AUFGABENKREISE)).default([]),
|
|
aufgabenkreiseFreitext: optionalerText(500),
|
|
befreiterBetreuer: z.coerce.boolean().default(true),
|
|
ehrenamtlich: z.coerce.boolean().default(true),
|
|
|
|
// --- Sozialleistungstraeger ----------------------------------------------
|
|
aktenzeichenBezirk: optionalerText(80),
|
|
aktenzeichenSozialamt: optionalerText(80),
|
|
rentenversicherungsnummer: optionalerText(30),
|
|
steuerId: optionalerText(20),
|
|
|
|
notizen: optionalerText(4000),
|
|
})
|
|
|
|
export type BetreuteEingabe = z.infer<typeof BetreuteSchema>
|
|
|
|
/**
|
|
* Diese Felder wandern verschluesselt in die Datenbank. Der Name ohne
|
|
* Suffix ist der Klartext im Formular, mit Suffix der Umschlag.
|
|
*/
|
|
export const BETREUTE_GEHEIME_FELDER = [
|
|
'krankenversichertennummer',
|
|
'diagnosen',
|
|
'ermittelterWille',
|
|
'rentenversicherungsnummer',
|
|
'steuerId',
|
|
'notizen',
|
|
] as const
|
|
|
|
/**
|
|
* Liest ein FormData-Objekt in die Struktur, die das Schema erwartet.
|
|
* Kontrollkaestchen liefern "on" oder gar nichts - beides wird hier
|
|
* in einen echten Wahrheitswert uebersetzt.
|
|
*/
|
|
export function formularLesen(formular: FormData): Record<string, unknown> {
|
|
const roh: Record<string, unknown> = {}
|
|
for (const [name, wert] of formular.entries()) {
|
|
if (name === 'aufgabenkreise') continue
|
|
roh[name] = wert
|
|
}
|
|
|
|
roh.aufgabenkreise = formular.getAll('aufgabenkreise').map(String)
|
|
roh.patientenverfuegungVorhanden = formular.get('patientenverfuegungVorhanden') === 'on'
|
|
roh.vorsorgevollmachtVorhanden = formular.get('vorsorgevollmachtVorhanden') === 'on'
|
|
roh.befreiterBetreuer = formular.get('befreiterBetreuer') === 'on'
|
|
roh.ehrenamtlich = formular.get('ehrenamtlich') === 'on'
|
|
|
|
// Leere Zahlenfelder duerfen nicht als "" ins Schema.
|
|
for (const feld of ['pflegegrad', 'schwerbehinderungGdB']) {
|
|
if (roh[feld] === '') roh[feld] = null
|
|
}
|
|
|
|
return roh
|
|
}
|