feat: Oberflaechenbausteine, Anwendungsrahmen und Konventionen
Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native <select>. Android und iOS oeffnen dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit Screenreader und Tastatur funktioniert es ohne Zutun. - Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari beim Hineintippen das Formular heran. <Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll. <Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung stattfindet. Er steht an jeder Stelle mit Paragraphenbezug. Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich hin?" wird schon im Menue beantwortet. Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf. docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt: Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern, Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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.
|
||||
*/
|
||||
|
||||
/** Leere Zeichenketten aus einem Formular 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()
|
||||
|
||||
/** HTML-Datumsfelder liefern "" oder "2026-01-15". */
|
||||
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.',
|
||||
})
|
||||
.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
|
||||
}
|
||||
Reference in New Issue
Block a user