Ausschliesslich Zeilenenden. Nachgewiesen mit "git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich kein Zeichen Inhalt geaendert. Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit echten Aenderungen zu verstecken, macht die Historie an dieser Stelle unlesbar. So laesst er sich beim Durchsehen ueberspringen. Refs #26
438 lines
14 KiB
TypeScript
438 lines
14 KiB
TypeScript
import { z } from 'zod'
|
|
import { betragParsenWennMoeglich } from '@/lib/finanzen/betrag'
|
|
|
|
/**
|
|
* Eingabeprüfung für das Sozialhilfemodul.
|
|
*
|
|
* Geprüft wird immer serverseitig. Die Meldungen sagen, was zu tun ist -
|
|
* nicht, welche Regel verletzt wurde. Wer am Handy im Heim steht und eine
|
|
* Nachforderung erfasst, hat keine Lust auf "invalid input".
|
|
*/
|
|
|
|
const optionalerText = (max = 200) =>
|
|
z
|
|
.string()
|
|
.trim()
|
|
.max(max, `Bitte höchstens ${max} Zeichen.`)
|
|
.transform((wert) => (wert === '' ? null : wert))
|
|
.nullable()
|
|
.default(null)
|
|
|
|
const optionalesDatum = z
|
|
.string()
|
|
.trim()
|
|
.transform((wert) => (wert === '' ? null : wert))
|
|
.nullable()
|
|
.default(null)
|
|
.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`))
|
|
|
|
/**
|
|
* Geldbetrag aus einem Textfeld. Leer wird zu null, nicht zu 0 - "keine
|
|
* Angabe" und "null Euro" sind fachlich zweierlei.
|
|
*/
|
|
const optionalerBetrag = z
|
|
.string()
|
|
.trim()
|
|
.default('')
|
|
.transform((wert, ctx) => {
|
|
if (wert === '') return null
|
|
const cent = betragParsenWennMoeglich(wert)
|
|
if (cent === null) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Bitte einen Betrag wie 1.234,56 eingeben.',
|
|
})
|
|
return z.NEVER
|
|
}
|
|
return cent
|
|
})
|
|
|
|
/** Betrag, der in einer Gegenüberstellung stehen muss - leer heißt 0. */
|
|
const betragOderNull = z
|
|
.string()
|
|
.trim()
|
|
.default('')
|
|
.transform((wert, ctx) => {
|
|
if (wert === '') return 0n
|
|
const cent = betragParsenWennMoeglich(wert)
|
|
if (cent === null) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Bitte einen Betrag wie 1.234,56 eingeben.',
|
|
})
|
|
return z.NEVER
|
|
}
|
|
if (cent < 0n) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'In dieser Aufstellung stehen nur positive Beträge.',
|
|
})
|
|
return z.NEVER
|
|
}
|
|
return cent
|
|
})
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Antragsakte
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const AntragSchema = z
|
|
.object({
|
|
traeger: z
|
|
.string()
|
|
.trim()
|
|
.min(2, 'Bitte den Träger angeben, z.B. „Bezirk Oberfranken“.')
|
|
.max(150),
|
|
leistungsart: z
|
|
.string()
|
|
.trim()
|
|
.min(2, 'Bitte die Leistungsart angeben.')
|
|
.max(200),
|
|
aktenzeichen: optionalerText(80),
|
|
sachbearbeiter: optionalerText(150),
|
|
telefon: optionalerText(50),
|
|
email: z
|
|
.string()
|
|
.trim()
|
|
.max(200)
|
|
.transform((wert) => (wert === '' ? null : wert))
|
|
.nullable()
|
|
.default(null)
|
|
.refine((wert) => wert === null || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(wert), {
|
|
message: 'Bitte eine gültige E-Mail-Adresse angeben oder das Feld leer lassen.',
|
|
}),
|
|
antragAm: optionalesDatum,
|
|
eingangBeimTraegerAm: optionalesDatum,
|
|
bewilligtVon: optionalesDatum,
|
|
bewilligtBis: optionalesDatum,
|
|
notizen: optionalerText(4000),
|
|
})
|
|
.refine(
|
|
(daten) =>
|
|
!daten.bewilligtVon || !daten.bewilligtBis || daten.bewilligtVon <= daten.bewilligtBis,
|
|
{
|
|
message: 'Das Ende des Bewilligungszeitraums liegt vor dem Beginn.',
|
|
path: ['bewilligtBis'],
|
|
},
|
|
)
|
|
.refine(
|
|
(daten) =>
|
|
!daten.antragAm ||
|
|
!daten.eingangBeimTraegerAm ||
|
|
daten.antragAm <= daten.eingangBeimTraegerAm,
|
|
{
|
|
message: 'Der Eingang beim Träger kann nicht vor dem Antragsdatum liegen.',
|
|
path: ['eingangBeimTraegerAm'],
|
|
},
|
|
)
|
|
|
|
export type AntragEingabe = z.infer<typeof AntragSchema>
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Nachforderung
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const NACHFORDERUNG_STATUS_WERTE = [
|
|
'OFFEN',
|
|
'IN_BESCHAFFUNG',
|
|
'UEBERSANDT',
|
|
'ANERKANNT',
|
|
'NICHT_VORHANDEN',
|
|
] as const
|
|
|
|
export const NachforderungSchema = z
|
|
.object({
|
|
bezeichnung: z
|
|
.string()
|
|
.trim()
|
|
.min(2, 'Bitte benennen, welche Unterlage der Träger will.')
|
|
.max(200),
|
|
erlaeuterung: optionalerText(2000),
|
|
status: z.enum(NACHFORDERUNG_STATUS_WERTE).default('OFFEN'),
|
|
angefordertAm: optionalesDatum,
|
|
fristBis: optionalesDatum,
|
|
uebersandtAm: optionalesDatum,
|
|
begruendungNichtVorhanden: optionalerText(2000),
|
|
reihenfolge: z.coerce.number().int().min(0).max(10_000).default(0),
|
|
dokumentId: optionalerText(40),
|
|
/** Soll aus der Frist des Trägers eine Frist im Fristenmodul werden? */
|
|
fristAnlegen: z.coerce.boolean().default(false),
|
|
})
|
|
.refine(
|
|
(daten) =>
|
|
daten.status !== 'NICHT_VORHANDEN' ||
|
|
(daten.begruendungNichtVorhanden !== null &&
|
|
daten.begruendungNichtVorhanden.trim().length >= 5),
|
|
{
|
|
message:
|
|
'Bitte begründen, warum es die Unterlage nicht gibt. Die Begründung geht mit ins ' +
|
|
'Unterlagenpaket — ohne sie sieht der Träger nur eine Lücke.',
|
|
path: ['begruendungNichtVorhanden'],
|
|
},
|
|
)
|
|
.refine((daten) => daten.status !== 'UEBERSANDT' || daten.uebersandtAm !== null, {
|
|
message:
|
|
'Bitte das Übersendungsdatum angeben — es ist der Nachweis der Mitwirkung, wenn der ' +
|
|
'Träger später von fehlenden Unterlagen ausgeht.',
|
|
path: ['uebersandtAm'],
|
|
})
|
|
.refine((daten) => !daten.fristAnlegen || daten.fristBis !== null, {
|
|
message: 'Für eine Frist im Fristenmodul brauche ich ein Fristdatum.',
|
|
path: ['fristBis'],
|
|
})
|
|
|
|
export type NachforderungEingabe = z.infer<typeof NachforderungSchema>
|
|
|
|
export const NachforderungStatusSchema = z
|
|
.object({
|
|
nachforderungId: z.string().trim().min(1),
|
|
status: z.enum(NACHFORDERUNG_STATUS_WERTE),
|
|
begruendungNichtVorhanden: optionalerText(2000),
|
|
uebersandtAm: optionalesDatum,
|
|
})
|
|
.refine(
|
|
(daten) =>
|
|
daten.status !== 'NICHT_VORHANDEN' ||
|
|
(daten.begruendungNichtVorhanden !== null &&
|
|
daten.begruendungNichtVorhanden.trim().length >= 5),
|
|
{
|
|
message: 'Bitte begründen, warum es die Unterlage nicht gibt.',
|
|
path: ['begruendungNichtVorhanden'],
|
|
},
|
|
)
|
|
|
|
export const ChecklisteSchema = z.object({
|
|
antragId: z.string().trim().min(1),
|
|
schluessel: z.array(z.string().trim().min(1)).min(1, 'Bitte mindestens eine Position auswählen.'),
|
|
angefordertAm: optionalesDatum,
|
|
fristBis: optionalesDatum,
|
|
})
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Bescheid
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const BESCHEID_ART_WERTE = [
|
|
'BEWILLIGUNG',
|
|
'ABLEHNUNG',
|
|
'AENDERUNG',
|
|
'AUFHEBUNG',
|
|
'ERSTATTUNG',
|
|
'VORSCHUSS',
|
|
'WIDERSPRUCHSBESCHEID',
|
|
] as const
|
|
|
|
export const PRUEFSTATUS_WERTE = [
|
|
'UNGEPRUEFT',
|
|
'GEPRUEFT_OK',
|
|
'WIDERSPRUCH_EINGELEGT',
|
|
'WIDERSPRUCH_ERLEDIGT',
|
|
'FRIST_VERSTRICHEN',
|
|
] as const
|
|
|
|
export const BescheidSchema = z
|
|
.object({
|
|
art: z.enum(BESCHEID_ART_WERTE),
|
|
bescheidVom: pflichtDatum,
|
|
zugegangenAm: optionalesDatum,
|
|
zeitraumVon: optionalesDatum,
|
|
zeitraumBis: optionalesDatum,
|
|
betrag: optionalerBetrag,
|
|
/**
|
|
* 1 = Regelfall (§ 84 SGG), 12 = fehlende oder fehlerhafte
|
|
* Rechtsbehelfsbelehrung (§ 66 SGG). Wird ausgewählt, nie geraten.
|
|
*/
|
|
rechtsbehelfsfristMonate: z.coerce
|
|
.number()
|
|
.int()
|
|
.refine((wert) => wert === 1 || wert === 12, {
|
|
message: 'Es kommen nur ein Monat (Regelfall) oder ein Jahr (fehlende Belehrung) in Betracht.',
|
|
})
|
|
.default(1),
|
|
widerspruchEingelegtAm: optionalesDatum,
|
|
notizen: optionalerText(4000),
|
|
dokumentId: optionalerText(40),
|
|
/** Soll die Frist "Widerspruch möglich bis" angelegt werden? */
|
|
fristAnlegen: z.coerce.boolean().default(false),
|
|
})
|
|
.refine(
|
|
(daten) => !daten.zugegangenAm || daten.zugegangenAm >= daten.bescheidVom,
|
|
{
|
|
message:
|
|
'Der Bescheid kann nicht vor seinem Ausstellungsdatum zugegangen sein. Prüfe den ' +
|
|
'Poststempel.',
|
|
path: ['zugegangenAm'],
|
|
},
|
|
)
|
|
.refine(
|
|
(daten) => !daten.zeitraumVon || !daten.zeitraumBis || daten.zeitraumVon <= daten.zeitraumBis,
|
|
{ message: 'Das Ende des Zeitraums liegt vor dem Beginn.', path: ['zeitraumBis'] },
|
|
)
|
|
.refine((daten) => !daten.fristAnlegen || daten.zugegangenAm !== null, {
|
|
message:
|
|
'Ohne Zugangsdatum lässt sich die Widerspruchsfrist nicht berechnen — und damit auch ' +
|
|
'keine Frist anlegen.',
|
|
path: ['zugegangenAm'],
|
|
})
|
|
|
|
export type BescheidEingabe = z.infer<typeof BescheidSchema>
|
|
|
|
export const BescheidStatusSchema = z.object({
|
|
bescheidId: z.string().trim().min(1),
|
|
pruefStatus: z.enum(PRUEFSTATUS_WERTE),
|
|
widerspruchEingelegtAm: optionalesDatum,
|
|
})
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Vermögensbewertung
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const BEWERTUNG_WERTE = ['UNGEPRUEFT', 'VERWERTBAR', 'GESCHUETZT', 'STRITTIG'] as const
|
|
|
|
export const BewertungSchema = z
|
|
.object({
|
|
herkunft: z.enum(['VERMOEGEN', 'VERTRAG']),
|
|
id: z.string().trim().min(1),
|
|
bewertung: z.enum(BEWERTUNG_WERTE),
|
|
begruendung: optionalerText(6000),
|
|
})
|
|
.refine(
|
|
(daten) =>
|
|
daten.bewertung === 'UNGEPRUEFT' ||
|
|
(daten.begruendung !== null && daten.begruendung.trim().length >= 10),
|
|
{
|
|
message:
|
|
'Eine Bewertung ohne Begründung hilft im Widerspruchsverfahren nicht weiter. ' +
|
|
'Übernimm einen der Textbausteine und passe ihn an.',
|
|
path: ['begruendung'],
|
|
},
|
|
)
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Heimkosten
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const HeimkostenSchema = z.object({
|
|
jahr: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(2000, 'Das Jahr liegt außerhalb des sinnvollen Bereichs.')
|
|
.max(2100, 'Das Jahr liegt außerhalb des sinnvollen Bereichs.'),
|
|
monat: z.coerce.number().int().min(1, 'Bitte einen Monat wählen.').max(12),
|
|
|
|
pflegeEigenanteil: betragOderNull,
|
|
unterkunftVerpflegung: betragOderNull,
|
|
investitionskosten: betragOderNull,
|
|
ausbildungsumlage: betragOderNull,
|
|
zusatzleistungen: betragOderNull,
|
|
|
|
pflegekasse: betragOderNull,
|
|
leistungszuschlagProzent: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(0, 'Der Zuschlag liegt zwischen 0 und 100 Prozent.')
|
|
.max(100, 'Der Zuschlag liegt zwischen 0 und 100 Prozent.')
|
|
.default(0),
|
|
leistungszuschlag: betragOderNull,
|
|
rente: betragOderNull,
|
|
sonstigeEinkuenfte: betragOderNull,
|
|
sozialhilfe: betragOderNull,
|
|
eigenmittel: betragOderNull,
|
|
|
|
barbetrag: betragOderNull,
|
|
bekleidungspauschale: betragOderNull,
|
|
|
|
abwesenheitstage: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(0, 'Abwesenheitstage können nicht negativ sein.')
|
|
.max(31, 'Ein Monat hat höchstens 31 Tage.')
|
|
.default(0),
|
|
|
|
bemerkung: optionalerText(2000),
|
|
})
|
|
|
|
export type HeimkostenEingabeFormular = z.infer<typeof HeimkostenSchema>
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Weitere Leistungen
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const LEISTUNG_STATUS_WERTE = [
|
|
'NICHT_GEPRUEFT',
|
|
'PRUEFUNG_LAEUFT',
|
|
'BEANTRAGT',
|
|
'BEWILLIGT',
|
|
'ABGELEHNT',
|
|
'LAEUFT',
|
|
'NICHT_ZUTREFFEND',
|
|
] as const
|
|
|
|
export const LeistungSchema = z
|
|
.object({
|
|
leistungId: optionalerText(40),
|
|
katalogSchluessel: optionalerText(80),
|
|
bezeichnung: z.string().trim().min(2, 'Bitte die Leistung benennen.').max(200),
|
|
traeger: optionalerText(200),
|
|
rechtsgrundlage: optionalerText(200),
|
|
status: z.enum(LEISTUNG_STATUS_WERTE).default('NICHT_GEPRUEFT'),
|
|
beantragtAm: optionalesDatum,
|
|
bewilligtAm: optionalesDatum,
|
|
gueltigBis: optionalesDatum,
|
|
betrag: optionalerBetrag,
|
|
jaehrlichNeuBeantragen: z.coerce.boolean().default(false),
|
|
notizen: optionalerText(2000),
|
|
})
|
|
.refine((daten) => !daten.bewilligtAm || !daten.beantragtAm || daten.bewilligtAm >= daten.beantragtAm, {
|
|
message: 'Die Bewilligung kann nicht vor dem Antrag liegen.',
|
|
path: ['bewilligtAm'],
|
|
})
|
|
|
|
export const KatalogUebernehmenSchema = z.object({
|
|
schluessel: z.array(z.string().trim().min(1)).min(1, 'Bitte mindestens eine Leistung auswählen.'),
|
|
})
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Formularhilfen
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/** Kontrollkästchen liefern "on" oder gar nichts. */
|
|
export function kaestchen(formular: FormData, name: string): boolean {
|
|
const wert = formular.get(name)
|
|
return wert === 'on' || wert === 'true' || wert === '1'
|
|
}
|
|
|
|
/** Liest ein FormData flach in ein Objekt - ohne Mehrfachwerte. */
|
|
export function formularLesen(formular: FormData): Record<string, unknown> {
|
|
const roh: Record<string, unknown> = {}
|
|
for (const [name, wert] of formular.entries()) {
|
|
if (typeof wert === 'string') roh[name] = wert
|
|
}
|
|
return roh
|
|
}
|
|
|
|
/**
|
|
* Zod-Feldfehler in die flache Form bringen, die die Formulare erwarten.
|
|
*/
|
|
export function feldfehler(fehler: z.ZodError): Record<string, string> {
|
|
const flach = fehler.flatten().fieldErrors
|
|
const ergebnis: Record<string, string> = {}
|
|
for (const [feld, meldungen] of Object.entries(flach)) {
|
|
const erste = meldungen?.[0]
|
|
if (erste) ergebnis[feld] = erste
|
|
}
|
|
return ergebnis
|
|
}
|