feat: Sozialhilfe-Modul mit Nachforderungs-Tracker und Fristpruefung
Etappe 5. Bildet das laufende Verfahren beim Bezirk Oberfranken ab. Rechtsbehelfsfrist: § 187 Abs. 1, § 188 Abs. 2 und 3 BGB, danach § 64 Abs. 3 SGG - hier verschieben Wochenende und Feiertag das Fristende auf den naechsten Werktag. Das ist genau umgekehrt zu den Betreuerfristen, wo bewusst nicht verschoben wird, und deshalb liegt die Feiertagsliste getrennt. Zwei bayerische Sonderfaelle, die haeufig falsch gemacht werden: Mariae Himmelfahrt verschiebt NICHT (Art. 1 Abs. 1 S. 2 BayFTG - der Feiertag gilt nur in ueberwiegend katholischen Gemeinden, Bayreuth ist keine), und der Buss- und Bettag ist in Bayern seit 1995 kein Feiertag mehr. Beide erzeugen stattdessen einen ausdruecklichen Hinweis, damit man die Frist im Zweifel selbst nachrechnet. Der Rechenweg wird als Schrittliste ausgegeben. Heimkosten: Der Dashboard-Agent hatte einen fachlichen Widerspruch gefunden - der einrichtungseinheitliche Eigenanteil auf der Heimrechnung ist bereits um die Leistung der Pflegekasse gekuerzt. Wird sie zusaetzlich als Deckung erfasst, entsteht eine Doppelzaehlung von ueber tausend Euro im Monat. Das Modul rechnet jetzt ausdruecklich nach der Lesart EIGENANTEIL_NETTO, fuehrt die Pflegekassenleistung nur nachrichtlich mit und warnt, wenn sie doch belegt ist. Die Oberflaeche sagt auf einer eigenen Karte, was hier gegen was gerechnet wird. Die Schonvermoegens-Bewertung liefert Argumentationshilfen zum Uebernehmen, keine Automatik - die Einordnung obliegt dem Traeger und gegebenenfalls dem Sozialgericht. Das steht an jeder dieser Karten. Alle jaehrlich veraenderlichen Betraege stehen an einer Stelle mit Stand-Datum und Prueftermin. Die Bekleidungspauschale bewusst ohne Zahl: sie ist regional und steht im Bescheid. 46 Tests. Etappe: 5 Phase: Behoerde
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
import type { VermoegensArt, VertragArt } from '@prisma/client'
|
||||
|
||||
/**
|
||||
* Menschenlesbare Namen der Vermögens- und Vertragsarten.
|
||||
*
|
||||
* Sie stehen hier, weil die Bewertungsansicht sie braucht und die Module
|
||||
* "Vermögen & Kasse" und "Verträge" ihre eigenen Beschriftungen führen.
|
||||
* Sobald dort eine gemeinsame Quelle existiert, wandert diese Datei
|
||||
* dorthin - bis dahin lieber eine kleine Doppelung als ein Modul, das auf
|
||||
* ein anderes wartet.
|
||||
*/
|
||||
|
||||
export const VERMOEGENSART_BESCHRIFTUNG: Record<VermoegensArt, string> = {
|
||||
KONTO: 'Konto',
|
||||
IMMOBILIE: 'Immobilie',
|
||||
VERSICHERUNG: 'Versicherung',
|
||||
FAHRZEUG: 'Fahrzeug',
|
||||
WERTGEGENSTAND: 'Wertgegenstand',
|
||||
FORDERUNG: 'Forderung',
|
||||
VERBINDLICHKEIT: 'Verbindlichkeit',
|
||||
SONSTIGES: 'Sonstiges',
|
||||
}
|
||||
|
||||
export const VERTRAGSART_BESCHRIFTUNG: Record<VertragArt, string> = {
|
||||
LEBENSVERSICHERUNG: 'Lebensversicherung',
|
||||
STERBEGELDVERSICHERUNG: 'Sterbegeldversicherung',
|
||||
BESTATTUNGSVORSORGE: 'Bestattungsvorsorge',
|
||||
RENTENVERSICHERUNG_PRIVAT: 'Private Rentenversicherung',
|
||||
UNFALLVERSICHERUNG: 'Unfallversicherung',
|
||||
HAFTPFLICHT: 'Haftpflichtversicherung',
|
||||
HAUSRAT: 'Hausratversicherung',
|
||||
WOHNGEBAEUDE: 'Wohngebäudeversicherung',
|
||||
KRANKENZUSATZ: 'Krankenzusatzversicherung',
|
||||
PFLEGEZUSATZ: 'Pflegezusatzversicherung',
|
||||
RECHTSSCHUTZ: 'Rechtsschutzversicherung',
|
||||
STERBEKASSE: 'Sterbekasse',
|
||||
STROM: 'Stromvertrag',
|
||||
GAS: 'Gasvertrag',
|
||||
WASSER: 'Wasserversorgung',
|
||||
TELEFON_INTERNET: 'Telefon und Internet',
|
||||
RUNDFUNKBEITRAG: 'Rundfunkbeitrag',
|
||||
MITGLIEDSCHAFT: 'Mitgliedschaft',
|
||||
ABONNEMENT: 'Abonnement',
|
||||
SONSTIGES: 'Sonstiges',
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertragsarten, die im Sozialhilfeverfahren überhaupt Vermögen darstellen
|
||||
* können. Ein Stromvertrag hat keinen Rückkaufswert - er gehört nicht in
|
||||
* die Bewertungsansicht und würde sie nur zumüllen.
|
||||
*/
|
||||
export const VERMOEGENSRELEVANTE_VERTRAGSARTEN: readonly VertragArt[] = [
|
||||
'LEBENSVERSICHERUNG',
|
||||
'STERBEGELDVERSICHERUNG',
|
||||
'BESTATTUNGSVORSORGE',
|
||||
'RENTENVERSICHERUNG_PRIVAT',
|
||||
'PFLEGEZUSATZ',
|
||||
'KRANKENZUSATZ',
|
||||
'STERBEKASSE',
|
||||
'UNFALLVERSICHERUNG',
|
||||
] as const
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Eingelesene Beträge in Cent umrechnen.
|
||||
*
|
||||
* Die Anwendung rechnet ausschließlich mit BigInt-Cent. Der Weg vom
|
||||
* Formularfeld dorthin ist die einzige Stelle, an der eine Zeichenkette in
|
||||
* eine Zahl wird - deshalb steht er hier und nicht verstreut in den
|
||||
* Server Actions.
|
||||
*
|
||||
* Bewusst NICHT über parseFloat: 1234,56 als Fließkomma und dann mal 100
|
||||
* ergibt 123455.99999999999. In der Rechnungslegung gegenüber dem Gericht
|
||||
* hat ein solcher Cent nichts verloren.
|
||||
*/
|
||||
|
||||
/** "1.234,56", "1234,56", "1234.56", "1234" -> 123456n. */
|
||||
export function betragZuCent(eingabe: string | null | undefined): bigint | null {
|
||||
if (eingabe === null || eingabe === undefined) return null
|
||||
|
||||
let text = String(eingabe).trim()
|
||||
if (text === '') return null
|
||||
|
||||
text = text.replace(/\s/g, '').replace(/€/g, '').replace(/EUR/gi, '')
|
||||
|
||||
let negativ = false
|
||||
if (text.startsWith('-')) {
|
||||
negativ = true
|
||||
text = text.slice(1)
|
||||
} else if (text.startsWith('+')) {
|
||||
text = text.slice(1)
|
||||
}
|
||||
|
||||
// Deutsche Schreibweise: Punkt trennt Tausender, Komma die Cent.
|
||||
// Englische Schreibweise mit Punkt als Dezimaltrenner wird ebenfalls
|
||||
// angenommen, solange sie eindeutig ist (genau ein Punkt, ein bis zwei
|
||||
// Nachkommastellen, kein Komma).
|
||||
if (text.includes(',')) {
|
||||
text = text.replace(/\./g, '').replace(',', '.')
|
||||
} else {
|
||||
const punkte = text.split('.').length - 1
|
||||
if (punkte > 1) {
|
||||
text = text.replace(/\./g, '')
|
||||
} else if (punkte === 1) {
|
||||
const nachkomma = text.split('.')[1] ?? ''
|
||||
// "1.234" ist ein Tausenderpunkt, "1.23" ein Dezimalpunkt.
|
||||
if (nachkomma.length === 3) text = text.replace('.', '')
|
||||
}
|
||||
}
|
||||
|
||||
const treffer = /^(\d+)(?:\.(\d{1,2}))?$/.exec(text)
|
||||
if (!treffer) return null
|
||||
|
||||
const euro = BigInt(treffer[1]!)
|
||||
const centText = (treffer[2] ?? '').padEnd(2, '0')
|
||||
const cent = BigInt(centText)
|
||||
|
||||
const gesamt = euro * 100n + cent
|
||||
return negativ ? -gesamt : gesamt
|
||||
}
|
||||
|
||||
/** Gegenrichtung: 123456n -> "1234,56" für die Vorbelegung von Feldern. */
|
||||
export function centZuEingabe(cent: bigint | null | undefined): string {
|
||||
if (cent === null || cent === undefined) return ''
|
||||
const negativ = cent < 0n
|
||||
const betrag = negativ ? -cent : cent
|
||||
const euro = betrag / 100n
|
||||
const rest = betrag % 100n
|
||||
return `${negativ ? '-' : ''}${euro},${rest.toString().padStart(2, '0')}`
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import {
|
||||
LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE,
|
||||
LEISTUNGSZUSCHLAG_STAFFEL,
|
||||
LEISTUNGSZUSCHLAG_STAND,
|
||||
} from '@/lib/pflege/konstanten'
|
||||
import { anteilCent, stufeFuerMonate, verweildauerMonate } from '@/lib/pflege/leistungszuschlag'
|
||||
import {
|
||||
ABWESENHEIT_KARENZTAGE,
|
||||
ABWESENHEIT_MINDERUNG_PROZENT,
|
||||
ABWESENHEIT_PLATZFREIHALTUNG_TAGE,
|
||||
LEISTUNGSZUSCHLAG_STAFFEL,
|
||||
PFLEGEKASSE_RECHTSGRUNDLAGE,
|
||||
PFLEGEKASSE_VOLLSTATIONAER_CENT,
|
||||
type Pflegegrad,
|
||||
@@ -12,78 +17,106 @@ import {
|
||||
* Heimkosten-Gegenüberstellung: Was kostet der Monat, wer zahlt was, und
|
||||
* was bleibt offen?
|
||||
*
|
||||
* Die Heimrechnung besteht aus vier Bausteinen, von denen nur der erste
|
||||
* pflegebedingt ist:
|
||||
* =========================================================================
|
||||
* DIE LESART — bitte vor jeder Änderung lesen
|
||||
* =========================================================================
|
||||
*
|
||||
* 1. einrichtungseinheitlicher Eigenanteil (EEE) - der pflegebedingte
|
||||
* Anteil, in jedem Pflegegrad gleich hoch. Nur auf ihn wirkt der
|
||||
* Leistungszuschlag nach § 43c SGB XI.
|
||||
* 2. Unterkunft und Verpflegung - trägt die betreute Person selbst,
|
||||
* hilfsweise die Sozialhilfe.
|
||||
* 3. Investitionskosten - Gebäude, Instandhaltung. Laufen auch bei
|
||||
* Abwesenheit weiter.
|
||||
* 4. Ausbildungsumlage - Umlage zur Finanzierung der Pflegeausbildung.
|
||||
* Eine Heimrechnung rechnet in dieser Reihenfolge:
|
||||
*
|
||||
* Dazu kommen Zusatzleistungen (Friseur, besondere Komfortleistungen), die
|
||||
* der Träger in aller Regel nicht übernimmt.
|
||||
* Pflegesatz (pflegebedingter Aufwand, brutto)
|
||||
* − Leistung der Pflegekasse § 43 Abs. 2 SGB XI
|
||||
* = einrichtungseinheitlicher Eigenanteil (EEE)
|
||||
* − Leistungszuschlag § 43c SGB XI
|
||||
* = zu zahlender pflegebedingter Anteil
|
||||
* + Unterkunft und Verpflegung
|
||||
* + Investitionskosten
|
||||
* + Ausbildungsumlage
|
||||
* + Zusatzleistungen
|
||||
* = Rechnungsbetrag
|
||||
*
|
||||
* Der EEE ist damit DEFINITIONSGEMÄSS schon um die Pauschale der
|
||||
* Pflegekasse gekürzt. Wer ihn als Kosten ansetzt UND die Pauschale
|
||||
* zusätzlich als Deckung, zählt dasselbe Geld zweimal und weist eine
|
||||
* Überdeckung aus, die es nicht gibt. Genau darauf hat der Dashboard-Agent
|
||||
* hingewiesen (docs/schema-wuensche/dashboard.md, Punkt 1).
|
||||
*
|
||||
* ENTSCHEIDUNG DIESES MODULS: Lesart EIGENANTEIL_NETTO.
|
||||
*
|
||||
* `pflegeEigenanteilCent` ist der einrichtungseinheitliche Eigenanteil, so
|
||||
* wie er auf der Heimrechnung steht — bereits um § 43 Abs. 2 SGB XI
|
||||
* gekürzt, aber noch nicht um den Leistungszuschlag. Das entspricht dem
|
||||
* Feldnamen, dem Kommentar im Schema und dem, was man tatsächlich abtippt.
|
||||
*
|
||||
* Daraus folgt:
|
||||
* - `pflegekasseCent` wird NICHT als Deckung gezählt. Der Betrag steckt
|
||||
* schon im Eigenanteil. Er wird nachrichtlich mitgeführt und angezeigt,
|
||||
* damit man ihn nicht für verschwunden hält.
|
||||
* - `leistungszuschlagCent` wird SEHR WOHL als Deckung gezählt: Er
|
||||
* mindert den EEE erst danach und steht auf der Rechnung als eigene
|
||||
* Abzugsposition.
|
||||
*
|
||||
* Wer stattdessen den Bruttosatz erfassen will, ruft mit der Lesart
|
||||
* PFLEGESATZ_BRUTTO auf — dann zählt die Pauschale als Deckung. Bei
|
||||
* konsistenter Erfassung führen beide Lesarten zur selben Deckungslücke;
|
||||
* ein Test hält das fest. Was die Rechnung nicht verträgt, ist die
|
||||
* Mischung — und genau davor warnt `heimkostenAuswerten()`.
|
||||
*
|
||||
* Alle Beträge BigInt in Cent. Kein Fließkomma, nirgends.
|
||||
*/
|
||||
|
||||
export type Kostenlesart = 'EIGENANTEIL_NETTO' | 'PFLEGESATZ_BRUTTO'
|
||||
|
||||
/** Die Lesart, mit der dieses Modul rechnet, solange nichts anderes gesagt wird. */
|
||||
export const KOSTENLESART_STANDARD: Kostenlesart = 'EIGENANTEIL_NETTO'
|
||||
|
||||
export const KOSTENLESART_BESCHRIFTUNG: Record<Kostenlesart, string> = {
|
||||
EIGENANTEIL_NETTO: 'Eigenanteil laut Heimrechnung',
|
||||
PFLEGESATZ_BRUTTO: 'Pflegesatz brutto',
|
||||
}
|
||||
|
||||
export const KOSTENLESART_ERKLAERUNG: Record<Kostenlesart, string> = {
|
||||
EIGENANTEIL_NETTO:
|
||||
'Als Kosten steht der einrichtungseinheitliche Eigenanteil — er ist bereits um die ' +
|
||||
'Pauschale der Pflegekasse nach § 43 Abs. 2 SGB XI gekürzt. Deshalb zählt die Pauschale ' +
|
||||
'hier nicht noch einmal als Deckung, sie wird nur nachrichtlich ausgewiesen. Der ' +
|
||||
'Leistungszuschlag nach § 43c SGB XI zählt dagegen als Deckung, weil er den Eigenanteil ' +
|
||||
'erst danach mindert und auf der Rechnung als eigene Abzugsposition steht.',
|
||||
PFLEGESATZ_BRUTTO:
|
||||
'Als Kosten steht der volle pflegebedingte Aufwand vor Abzug der Pflegekassenleistung. ' +
|
||||
'Pauschale und Leistungszuschlag zählen dann beide als Deckung.',
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Leistungszuschlag, § 43c SGB XI
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// Die Staffel steht im Pflegemodul (src/lib/pflege/konstanten.ts) und wird
|
||||
// hier nur benutzt. Zwei Kopien derselben Prozentsätze wären genau die
|
||||
// Sorte Fehler, die bei der nächsten Gesetzesänderung an einer Stelle
|
||||
// nachgezogen wird und an der anderen nicht.
|
||||
|
||||
/**
|
||||
* Prozentsatz des Leistungszuschlags nach der Dauer der
|
||||
* Leistungsinanspruchnahme.
|
||||
*
|
||||
* Gezählt werden VOLLENDETE Monate in vollstationärer Pflege. Nach zwölf
|
||||
* vollen Monaten - also im dreizehnten - greift die zweite Stufe, weil
|
||||
* § 43c SGB XI von "mehr als 12 Monaten" spricht.
|
||||
*/
|
||||
export { LEISTUNGSZUSCHLAG_STAFFEL, LEISTUNGSZUSCHLAG_STAND, LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE }
|
||||
export { verweildauerMonate }
|
||||
|
||||
/** Prozentsatz nach vollendeten Monaten in der Einrichtung. */
|
||||
export function leistungszuschlagProzent(vollendeteMonate: number): number {
|
||||
const monate = Math.max(0, Math.floor(vollendeteMonate))
|
||||
let prozent = LEISTUNGSZUSCHLAG_STAFFEL[0]!.prozent
|
||||
for (const stufe of LEISTUNGSZUSCHLAG_STAFFEL) {
|
||||
if (monate >= stufe.abVollendetenMonaten) prozent = stufe.prozent
|
||||
}
|
||||
return prozent
|
||||
return stufeFuerMonate(Math.max(0, Math.floor(vollendeteMonate))).prozent
|
||||
}
|
||||
|
||||
/** Beschriftung der Stufe, für die Anzeige neben der Zahl. */
|
||||
export function leistungszuschlagStufe(vollendeteMonate: number): string {
|
||||
const prozent = leistungszuschlagProzent(vollendeteMonate)
|
||||
return LEISTUNGSZUSCHLAG_STAFFEL.find((s) => s.prozent === prozent)?.beschriftung ?? ''
|
||||
return stufeFuerMonate(Math.max(0, Math.floor(vollendeteMonate))).bezeichnung
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Zuschlag in Cent - immer nur auf den pflegebedingten Eigenanteil.
|
||||
*
|
||||
* Gerundet wird kaufmännisch auf ganze Cent. Bei ganzen Prozentsätzen und
|
||||
* Cent-Beträgen ist das eine Rundung von höchstens einem Cent, aber sie
|
||||
* gehört ausgeschrieben, damit niemand später über einen Cent Differenz
|
||||
* zur Heimrechnung rätselt.
|
||||
* Der Zuschlag in Cent — immer nur auf den pflegebedingten Eigenanteil,
|
||||
* nie auf Unterkunft, Investitionskosten oder Ausbildungsumlage.
|
||||
*/
|
||||
export function leistungszuschlagCent(
|
||||
pflegeEigenanteilCent: bigint,
|
||||
vollendeteMonate: number,
|
||||
): bigint {
|
||||
const prozent = BigInt(leistungszuschlagProzent(vollendeteMonate))
|
||||
const roh = pflegeEigenanteilCent * prozent
|
||||
return (roh + 50n) / 100n
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollendete Monate im Heim zwischen Einzug und Stichtag. Bricht die
|
||||
* Zählung nicht auf Tage herunter - § 43c SGB XI stellt auf Monate ab.
|
||||
*/
|
||||
export function verweildauerMonate(einzugAm: Date, stichtag: Date): number {
|
||||
const jahre = stichtag.getUTCFullYear() - einzugAm.getUTCFullYear()
|
||||
const monate = stichtag.getUTCMonth() - einzugAm.getUTCMonth()
|
||||
let gesamt = jahre * 12 + monate
|
||||
if (stichtag.getUTCDate() < einzugAm.getUTCDate()) gesamt -= 1
|
||||
return Math.max(0, gesamt)
|
||||
return anteilCent(pflegeEigenanteilCent, leistungszuschlagProzent(vollendeteMonate))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -129,7 +162,7 @@ export type Abwesenheitsschaetzung = {
|
||||
*
|
||||
* Gerundet wird abwärts (BigInt-Division schneidet ab). Die geschätzte
|
||||
* Minderung fällt damit im Zweifel einen Cent zu klein und die
|
||||
* Deckungslücke einen Cent zu groß aus - die vorsichtigere Richtung.
|
||||
* Deckungslücke einen Cent zu groß aus — die vorsichtigere Richtung.
|
||||
*/
|
||||
export function abwesenheitSchaetzen(eingabe: {
|
||||
abwesenheitstage: number
|
||||
@@ -204,6 +237,7 @@ export type HeimkostenEingabe = {
|
||||
export type HeimkostenErgebnis = {
|
||||
jahr: number
|
||||
monat: number
|
||||
lesart: Kostenlesart
|
||||
|
||||
heimkostenCent: bigint
|
||||
/** Heimkosten plus Barbetrag und Bekleidungspauschale. */
|
||||
@@ -211,9 +245,16 @@ export type HeimkostenErgebnis = {
|
||||
deckungCent: bigint
|
||||
verbleibendCent: bigint
|
||||
|
||||
/**
|
||||
* Leistung der Pflegekasse, die unter der Lesart EIGENANTEIL_NETTO nicht
|
||||
* als Deckung zählt, weil sie schon im Eigenanteil steckt. Nur für die
|
||||
* Anzeige — sie wird nirgends aufaddiert.
|
||||
*/
|
||||
pflegekasseNachrichtlichCent: bigint
|
||||
|
||||
/**
|
||||
* Positiv = ungedeckt, negativ = Überdeckung. Für die Anzeige stehen
|
||||
* daneben deckungsluecke Cent und ueberdeckungCent, jeweils >= 0.
|
||||
* daneben deckungslueckeCent und ueberdeckungCent, jeweils >= 0.
|
||||
*/
|
||||
saldoCent: bigint
|
||||
deckungslueckeCent: bigint
|
||||
@@ -232,11 +273,16 @@ export type HeimkostenErgebnis = {
|
||||
*
|
||||
* Bedarfsbegriff: Zum Bedarf zählen nicht nur die Heimkosten, sondern auch
|
||||
* der Barbetrag nach § 27b Abs. 2 SGB XII und die Bekleidungspauschale.
|
||||
* Beides muss der betreuten Person tatsächlich verbleiben - wer es aus der
|
||||
* Beides muss der betreuten Person tatsächlich verbleiben — wer es aus der
|
||||
* Rechnung herauslässt, hält die Deckung für besser, als sie ist, und
|
||||
* plündert am Monatsende das Taschengeld.
|
||||
*
|
||||
* Zur Behandlung von `pflegekasseCent` siehe den Kopf dieser Datei.
|
||||
*/
|
||||
export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgebnis {
|
||||
export function heimkostenAuswerten(
|
||||
eingabe: HeimkostenEingabe,
|
||||
lesart: Kostenlesart = KOSTENLESART_STANDARD,
|
||||
): HeimkostenErgebnis {
|
||||
const heimkostenCent =
|
||||
eingabe.pflegeEigenanteilCent +
|
||||
eingabe.unterkunftVerpflegungCent +
|
||||
@@ -247,8 +293,12 @@ export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgeb
|
||||
const verbleibendCent = eingabe.barbetragCent + eingabe.bekleidungspauschaleCent
|
||||
const gesamtbedarfCent = heimkostenCent + verbleibendCent
|
||||
|
||||
const nettoLesart = lesart === 'EIGENANTEIL_NETTO'
|
||||
const pflegekasseAlsDeckungCent = nettoLesart ? 0n : eingabe.pflegekasseCent
|
||||
const pflegekasseNachrichtlichCent = nettoLesart ? eingabe.pflegekasseCent : 0n
|
||||
|
||||
const deckungCent =
|
||||
eingabe.pflegekasseCent +
|
||||
pflegekasseAlsDeckungCent +
|
||||
eingabe.leistungszuschlagCent +
|
||||
eingabe.renteCent +
|
||||
eingabe.sonstigeEinkuenfteCent +
|
||||
@@ -267,6 +317,15 @@ export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgeb
|
||||
const nachAbwesenheit = saldoCent - abwesenheit.geschaetzteMinderungCent
|
||||
|
||||
const hinweise: string[] = []
|
||||
|
||||
if (nettoLesart && eingabe.pflegekasseCent > 0n) {
|
||||
hinweise.push(
|
||||
'Hier steht eine Leistung der Pflegekasse als Deckung — der einrichtungseinheitliche ' +
|
||||
'Eigenanteil ist aber schon um sie gekürzt (§ 43 Abs. 2 SGB XI). Der Betrag wird ' +
|
||||
'deshalb nicht mitgezählt; sonst stünde dasselbe Geld zweimal in der Rechnung. Trag ' +
|
||||
'als Kosten den Eigenanteil von der Heimrechnung ein und lass dieses Feld auf 0.',
|
||||
)
|
||||
}
|
||||
if (eingabe.zusatzleistungenCent > 0n) {
|
||||
hinweise.push(
|
||||
'Zusatzleistungen (Friseur, Komfortleistungen) übernimmt der Träger in aller Regel ' +
|
||||
@@ -297,10 +356,12 @@ export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgeb
|
||||
return {
|
||||
jahr: eingabe.jahr,
|
||||
monat: eingabe.monat,
|
||||
lesart,
|
||||
heimkostenCent,
|
||||
gesamtbedarfCent,
|
||||
deckungCent,
|
||||
verbleibendCent,
|
||||
pflegekasseNachrichtlichCent,
|
||||
saldoCent,
|
||||
deckungslueckeCent: saldoCent > 0n ? saldoCent : 0n,
|
||||
ueberdeckungCent: saldoCent < 0n ? -saldoCent : 0n,
|
||||
@@ -317,7 +378,7 @@ export type Jahresverlauf = {
|
||||
summeBedarfCent: bigint
|
||||
summeDeckungCent: bigint
|
||||
summeDeckungslueckeCent: bigint
|
||||
/** Monat mit der größten Lücke - dort lohnt der Blick zuerst. */
|
||||
/** Monat mit der größten Lücke — dort lohnt der Blick zuerst. */
|
||||
groessteLuecke: HeimkostenErgebnis | null
|
||||
}
|
||||
|
||||
|
||||
+237
-256
@@ -1,256 +1,237 @@
|
||||
/**
|
||||
* Beträge, Staffeln und Grenzwerte des Sozialhilfeverfahrens.
|
||||
*
|
||||
* ACHTUNG - EINZIGE STELLE.
|
||||
* Alles, was sich jährlich (oder mit dem nächsten Gesetzespaket) ändert,
|
||||
* steht hier und nirgendwo sonst. Jede Konstante trägt ihr Stand-Datum und
|
||||
* die Fundstelle. Wer einen Wert im Fachcode findet, hat einen Fehler
|
||||
* gefunden.
|
||||
*
|
||||
* Und der wichtigste Satz vorweg: Diese Zahlen sind Arbeitsgrundlage für
|
||||
* die eigene Vorbereitung, nicht die Rechtslage. Maßgeblich ist immer der
|
||||
* Bescheid des Trägers. Vor jedem Antrag und zu jedem Jahreswechsel
|
||||
* gehören die Werte hier nachgeprüft.
|
||||
*/
|
||||
|
||||
/** Wann wurden die Werte in dieser Datei zuletzt geprüft? */
|
||||
export const KONSTANTEN_STAND = '2025-01-01'
|
||||
|
||||
/**
|
||||
* Nächster Termin, zu dem die Werte hier durchzusehen sind. Die Oberfläche
|
||||
* blendet ab diesem Datum einen Prüfhinweis ein.
|
||||
*/
|
||||
export const KONSTANTEN_NAECHSTE_PRUEFUNG = '2027-01-01'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Regelbedarf - Bezugsgröße für Barbetrag und Zuzahlungsbefreiung
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Regelbedarfsstufe 1 nach der Regelbedarfsstufen-Fortschreibungsverordnung.
|
||||
* Stand 01.01.2025: 563,00 EUR monatlich (unverändert gegenüber 2024, die
|
||||
* Fortschreibung ergab rechnerisch keine Erhöhung).
|
||||
*
|
||||
* Aus diesem Wert leiten sich unten der Barbetrag und die Belastungsgrenze
|
||||
* der Zuzahlungsbefreiung ab - deshalb steht er hier oben und wird nicht
|
||||
* doppelt gepflegt.
|
||||
*/
|
||||
export const REGELBEDARFSSTUFE_1_CENT = 56_300n
|
||||
|
||||
/**
|
||||
* Barbetrag zur persönlichen Verfügung ("Taschengeld") für Menschen in
|
||||
* Einrichtungen, § 27b Abs. 2 SGB XII: mindestens 27 % der
|
||||
* Regelbedarfsstufe 1.
|
||||
*
|
||||
* 27 % von 563,00 EUR = 152,01 EUR monatlich (Stand 01.01.2025).
|
||||
*
|
||||
* "Mindestens": Der Träger darf mehr festsetzen, und einzelne Bezirke tun
|
||||
* das auch. Steht im Bescheid ein höherer Betrag, gilt der Bescheid.
|
||||
*/
|
||||
export const BARBETRAG_ANTEIL_PROZENT = 27
|
||||
export const BARBETRAG_CENT = (REGELBEDARFSSTUFE_1_CENT * BigInt(BARBETRAG_ANTEIL_PROZENT)) / 100n
|
||||
|
||||
/**
|
||||
* Bekleidungspauschale.
|
||||
*
|
||||
* Bewusst ohne Zahl. Es gibt keinen bundeseinheitlichen Betrag - die
|
||||
* Bekleidungsbeihilfe nach § 27b Abs. 1 SGB XII setzt der jeweilige Träger
|
||||
* fest, der Bezirk Oberfranken zahlt sie üblicherweise als Pauschale je
|
||||
* Quartal oder Jahr. Der Betrag ist dem Bewilligungsbescheid zu entnehmen
|
||||
* und im Heimkostenmonat einzutragen. Eine erfundene Zahl wäre hier
|
||||
* schädlicher als gar keine.
|
||||
*/
|
||||
export const BEKLEIDUNGSPAUSCHALE_CENT: bigint | null = null
|
||||
export const BEKLEIDUNGSPAUSCHALE_HINWEIS =
|
||||
'Die Höhe der Bekleidungspauschale legt der Träger fest (§ 27b Abs. 1 SGB XII). ' +
|
||||
'Übernimm den Betrag aus dem Bewilligungsbescheid, er ist regional unterschiedlich.'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Leistungen der Pflegekasse, § 43 SGB XI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export type Pflegegrad = 1 | 2 | 3 | 4 | 5
|
||||
|
||||
/**
|
||||
* Monatliche Pauschalen der Pflegekasse bei vollstationärer Pflege,
|
||||
* § 43 Abs. 2 SGB XI. Stand 01.01.2025 (Erhöhung um 4,5 % durch das
|
||||
* Pflegeunterstützungs- und -entlastungsgesetz).
|
||||
*
|
||||
* Pflegegrad 1 ist der Sonderfall: § 43 Abs. 2 SGB XI greift dort nicht.
|
||||
* Wer mit Pflegegrad 1 vollstationär gepflegt wird, erhält nach
|
||||
* § 43 Abs. 3 SGB XI einen Zuschuss von 125 EUR monatlich - deshalb steht
|
||||
* er hier mit ebendiesem Betrag, aber unter eigener Rechtsgrundlage.
|
||||
*
|
||||
* Die nächste turnusmäßige Anpassung nach § 30 SGB XI ist gesetzlich für
|
||||
* 2028 vorgesehen. Trotzdem: zum Jahreswechsel nachsehen.
|
||||
*/
|
||||
export const PFLEGEKASSE_VOLLSTATIONAER_CENT: Record<Pflegegrad, bigint> = {
|
||||
1: 12_500n, // § 43 Abs. 3 SGB XI - Zuschuss, keine Pauschale
|
||||
2: 80_500n,
|
||||
3: 131_900n,
|
||||
4: 185_500n,
|
||||
5: 209_600n,
|
||||
}
|
||||
|
||||
export const PFLEGEKASSE_RECHTSGRUNDLAGE: Record<Pflegegrad, string> = {
|
||||
1: '§ 43 Abs. 3 SGB XI',
|
||||
2: '§ 43 Abs. 2 SGB XI',
|
||||
3: '§ 43 Abs. 2 SGB XI',
|
||||
4: '§ 43 Abs. 2 SGB XI',
|
||||
5: '§ 43 Abs. 2 SGB XI',
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Leistungszuschlag, § 43c SGB XI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Der Leistungszuschlag mindert den *pflegebedingten* Eigenanteil
|
||||
* (einrichtungseinheitlicher Eigenanteil, EEE) - nicht Unterkunft und
|
||||
* Verpflegung, nicht die Investitionskosten und nicht die
|
||||
* Ausbildungsumlage. Wer ihn auf die ganze Heimrechnung anwendet, rechnet
|
||||
* die Deckungslücke zu klein.
|
||||
*
|
||||
* Die Staffel richtet sich nach der Dauer der Leistungsinanspruchnahme in
|
||||
* vollstationärer Pflege. Stand 01.01.2024 (angehoben durch das PUEG,
|
||||
* vorher 5/25/45/70 %).
|
||||
*
|
||||
* Lesart der Stufen: § 43c SGB XI spricht von "mehr als 12 Monaten". Die
|
||||
* höhere Stufe greift also, sobald die genannte Monatszahl *vollendet*
|
||||
* ist - nach 12 vollen Monaten, also im 13. Monat, gelten 30 %.
|
||||
*/
|
||||
export type Leistungszuschlagstufe = {
|
||||
/** Ab wie vielen vollendeten Monaten im Heim gilt diese Stufe? */
|
||||
abVollendetenMonaten: number
|
||||
prozent: number
|
||||
beschriftung: string
|
||||
}
|
||||
|
||||
export const LEISTUNGSZUSCHLAG_STAFFEL: readonly Leistungszuschlagstufe[] = [
|
||||
{ abVollendetenMonaten: 0, prozent: 15, beschriftung: 'bis 12 Monate' },
|
||||
{ abVollendetenMonaten: 12, prozent: 30, beschriftung: 'mehr als 12 Monate' },
|
||||
{ abVollendetenMonaten: 24, prozent: 50, beschriftung: 'mehr als 24 Monate' },
|
||||
{ abVollendetenMonaten: 36, prozent: 75, beschriftung: 'mehr als 36 Monate' },
|
||||
] as const
|
||||
|
||||
export const LEISTUNGSZUSCHLAG_STAND = '2024-01-01'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Abwesenheit (Krankenhaus, Reha, Urlaub)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Abwesenheitstage mindern die Heimrechnung nicht ab dem ersten Tag.
|
||||
*
|
||||
* Die Rahmenverträge nach § 75 SGB XI regeln das landesweit: Die ersten
|
||||
* drei Abwesenheitstage sind voll zu zahlen, erst ab dem vierten
|
||||
* zusammenhängenden Tag wird das Entgelt gemindert - in Bayern
|
||||
* üblicherweise um 25 % von Pflegesatz sowie Unterkunft und Verpflegung.
|
||||
* Die Investitionskosten laufen weiter, der Platz wird ja freigehalten
|
||||
* (§ 87a Abs. 1 SGB XI, Platzfreihaltung bis 42 Tage im Kalenderjahr).
|
||||
*
|
||||
* Weil die Prozentsätze im Rahmenvertrag und im Heimvertrag stehen und
|
||||
* nicht im Gesetz, rechnet die Anwendung damit nur eine *Schätzung*. Was
|
||||
* gilt, steht in der Heimrechnung.
|
||||
*/
|
||||
export const ABWESENHEIT_KARENZTAGE = 3
|
||||
export const ABWESENHEIT_MINDERUNG_PROZENT = 25
|
||||
export const ABWESENHEIT_PLATZFREIHALTUNG_TAGE = 42
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Vermögen, § 90 SGB XII
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Vermögensfreibetrag ("kleinere Barbeträge"), § 90 Abs. 2 Nr. 9 SGB XII
|
||||
* i.V.m. § 1 Abs. 1 Nr. 1 lit. b der Verordnung zur Durchführung des
|
||||
* § 90 Abs. 2 Nr. 9 SGB XII: 10.000 EUR.
|
||||
*
|
||||
* Stand 01.01.2023 (bis dahin 5.000 EUR). Für jede Person, die von der
|
||||
* leistungsberechtigten Person überwiegend unterhalten wird, kommen
|
||||
* weitere 500 EUR hinzu (§ 1 Abs. 2 der Verordnung).
|
||||
*/
|
||||
export const VERMOEGENSFREIBETRAG_CENT = 1_000_000n
|
||||
export const VERMOEGENSFREIBETRAG_JE_UNTERHALTENE_PERSON_CENT = 50_000n
|
||||
export const VERMOEGENSFREIBETRAG_STAND = '2023-01-01'
|
||||
|
||||
/**
|
||||
* Rückforderung einer Schenkung wegen Verarmung, § 528 BGB.
|
||||
* Der Anspruch ist ausgeschlossen, wenn zum Zeitpunkt des Eintritts der
|
||||
* Bedürftigkeit seit der Leistung des geschenkten Gegenstands zehn Jahre
|
||||
* verstrichen sind (§ 529 Abs. 1 BGB). Deshalb fragt der Träger nach
|
||||
* notariellen Übertragungen der letzten zehn Jahre.
|
||||
*/
|
||||
export const SCHENKUNG_RUECKFORDERUNG_JAHRE = 10
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rechtsbehelf
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Regelfall: ein Monat ab Bekanntgabe, § 84 Abs. 1 Satz 1 SGG. */
|
||||
export const WIDERSPRUCHSFRIST_MONATE_REGEL = 1
|
||||
|
||||
/**
|
||||
* Fehlt die Rechtsbehelfsbelehrung oder ist sie unrichtig erteilt, beträgt
|
||||
* die Frist ein Jahr seit Bekanntgabe (§ 66 Abs. 2 Satz 1 SGG).
|
||||
* Das wird ausdrücklich ausgewählt und nie automatisch angenommen.
|
||||
*/
|
||||
export const WIDERSPRUCHSFRIST_MONATE_OHNE_BELEHRUNG = 12
|
||||
|
||||
/**
|
||||
* Zugangsvermutung bei Übermittlung durch die Post im Inland:
|
||||
* bekanntgegeben am dritten Tag nach der Aufgabe zur Post
|
||||
* (§ 37 Abs. 2 Satz 1 SGB X).
|
||||
*/
|
||||
export const ZUGANGSVERMUTUNG_TAGE = 3
|
||||
|
||||
/** Vorwarnstufen für die Frist "Widerspruch möglich bis" (Tage vorher). */
|
||||
export const WIDERSPRUCH_VORWARNUNG_TAGE = [21, 14, 7, 3, 1]
|
||||
|
||||
/** Vorwarnstufen für eine vom Träger gesetzte Nachforderungsfrist. */
|
||||
export const NACHFORDERUNG_VORWARNUNG_TAGE = [14, 7, 3, 1]
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Weitere Leistungen
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Bayerisches Landespflegegeld: 1.000 EUR im Jahr, ab Pflegegrad 2,
|
||||
* Antrag bis 31.12. des jeweiligen Jahres (Bayerisches
|
||||
* Landespflegegeldgesetz - BayLPflGG).
|
||||
*
|
||||
* Der Betrag steht im Haushalt des Freistaats und wurde politisch schon
|
||||
* mehrfach in Frage gestellt. Vor der Antragstellung die aktuelle Höhe
|
||||
* beim Landesamt für Pflege prüfen.
|
||||
*/
|
||||
export const LANDESPFLEGEGELD_CENT = 100_000n
|
||||
export const LANDESPFLEGEGELD_AB_PFLEGEGRAD = 2
|
||||
export const LANDESPFLEGEGELD_ANTRAGSSCHLUSS = '31.12.'
|
||||
|
||||
/**
|
||||
* Zuzahlungsbefreiung, § 62 SGB V: Die Belastungsgrenze liegt bei 2 % der
|
||||
* jährlichen Bruttoeinnahmen zum Lebensunterhalt, bei schwerwiegend
|
||||
* chronisch Kranken bei 1 %.
|
||||
*
|
||||
* Für Menschen, die Leistungen nach dem SGB XII in einer Einrichtung
|
||||
* erhalten, ist als Bruttoeinnahme der Regelbedarf der Regelbedarfsstufe 1
|
||||
* anzusetzen (§ 62 Abs. 2 Satz 5 und 6 SGB V) - nicht die tatsächliche
|
||||
* Rente. Das macht die Befreiung meist schon im Januar erreichbar.
|
||||
*
|
||||
* 2 % von 12 x 563,00 EUR = 135,12 EUR; 1 % = 67,56 EUR (Stand 2025).
|
||||
*/
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_PROZENT = 2
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_PROZENT = 1
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_CENT =
|
||||
(REGELBEDARFSSTUFE_1_CENT * 12n * BigInt(ZUZAHLUNG_BELASTUNGSGRENZE_PROZENT)) / 100n
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_CENT =
|
||||
(REGELBEDARFSSTUFE_1_CENT * 12n * BigInt(ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_PROZENT)) / 100n
|
||||
|
||||
/**
|
||||
* Rundfunkbeitrag je Wohnung, § 8 Rundfunkfinanzierungsstaatsvertrag:
|
||||
* 18,36 EUR monatlich, unverändert seit 01.08.2021. Die Ermäßigung auf ein
|
||||
* Drittel (§ 4 Abs. 2 RBStV, Merkzeichen RF) beträgt 6,12 EUR.
|
||||
*/
|
||||
export const RUNDFUNKBEITRAG_CENT = 1_836n
|
||||
export const RUNDFUNKBEITRAG_ERMAESSIGT_CENT = 612n
|
||||
export const RUNDFUNKBEITRAG_STAND = '2021-08-01'
|
||||
/**
|
||||
* Beträge, Staffeln und Grenzwerte des Sozialhilfeverfahrens.
|
||||
*
|
||||
* ACHTUNG - EINZIGE STELLE.
|
||||
* Alles, was sich jährlich (oder mit dem nächsten Gesetzespaket) ändert,
|
||||
* steht hier und nirgendwo sonst. Jede Konstante trägt ihr Stand-Datum und
|
||||
* die Fundstelle. Wer einen Wert im Fachcode findet, hat einen Fehler
|
||||
* gefunden.
|
||||
*
|
||||
* Und der wichtigste Satz vorweg: Diese Zahlen sind Arbeitsgrundlage für
|
||||
* die eigene Vorbereitung, nicht die Rechtslage. Maßgeblich ist immer der
|
||||
* Bescheid des Trägers. Vor jedem Antrag und zu jedem Jahreswechsel
|
||||
* gehören die Werte hier nachgeprüft.
|
||||
*/
|
||||
|
||||
/** Wann wurden die Werte in dieser Datei zuletzt geprüft? */
|
||||
export const KONSTANTEN_STAND = '2025-01-01'
|
||||
|
||||
/**
|
||||
* Nächster Termin, zu dem die Werte hier durchzusehen sind. Die Oberfläche
|
||||
* blendet ab diesem Datum einen Prüfhinweis ein.
|
||||
*/
|
||||
export const KONSTANTEN_NAECHSTE_PRUEFUNG = '2027-01-01'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Regelbedarf - Bezugsgröße für Barbetrag und Zuzahlungsbefreiung
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Regelbedarfsstufe 1 nach der Regelbedarfsstufen-Fortschreibungsverordnung.
|
||||
* Stand 01.01.2025: 563,00 EUR monatlich (unverändert gegenüber 2024, die
|
||||
* Fortschreibung ergab rechnerisch keine Erhöhung).
|
||||
*
|
||||
* Aus diesem Wert leiten sich unten der Barbetrag und die Belastungsgrenze
|
||||
* der Zuzahlungsbefreiung ab - deshalb steht er hier oben und wird nicht
|
||||
* doppelt gepflegt.
|
||||
*/
|
||||
export const REGELBEDARFSSTUFE_1_CENT = 56_300n
|
||||
|
||||
/**
|
||||
* Barbetrag zur persönlichen Verfügung ("Taschengeld") für Menschen in
|
||||
* Einrichtungen, § 27b Abs. 2 SGB XII: mindestens 27 % der
|
||||
* Regelbedarfsstufe 1.
|
||||
*
|
||||
* 27 % von 563,00 EUR = 152,01 EUR monatlich (Stand 01.01.2025).
|
||||
*
|
||||
* "Mindestens": Der Träger darf mehr festsetzen, und einzelne Bezirke tun
|
||||
* das auch. Steht im Bescheid ein höherer Betrag, gilt der Bescheid.
|
||||
*/
|
||||
export const BARBETRAG_ANTEIL_PROZENT = 27
|
||||
export const BARBETRAG_CENT = (REGELBEDARFSSTUFE_1_CENT * BigInt(BARBETRAG_ANTEIL_PROZENT)) / 100n
|
||||
|
||||
/**
|
||||
* Bekleidungspauschale.
|
||||
*
|
||||
* Bewusst ohne Zahl. Es gibt keinen bundeseinheitlichen Betrag - die
|
||||
* Bekleidungsbeihilfe nach § 27b Abs. 1 SGB XII setzt der jeweilige Träger
|
||||
* fest, der Bezirk Oberfranken zahlt sie üblicherweise als Pauschale je
|
||||
* Quartal oder Jahr. Der Betrag ist dem Bewilligungsbescheid zu entnehmen
|
||||
* und im Heimkostenmonat einzutragen. Eine erfundene Zahl wäre hier
|
||||
* schädlicher als gar keine.
|
||||
*/
|
||||
export const BEKLEIDUNGSPAUSCHALE_CENT: bigint | null = null
|
||||
export const BEKLEIDUNGSPAUSCHALE_HINWEIS =
|
||||
'Die Höhe der Bekleidungspauschale legt der Träger fest (§ 27b Abs. 1 SGB XII). ' +
|
||||
'Übernimm den Betrag aus dem Bewilligungsbescheid, er ist regional unterschiedlich.'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Leistungen der Pflegekasse, § 43 SGB XI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export type Pflegegrad = 1 | 2 | 3 | 4 | 5
|
||||
|
||||
/**
|
||||
* Monatliche Pauschalen der Pflegekasse bei vollstationärer Pflege,
|
||||
* § 43 Abs. 2 SGB XI. Stand 01.01.2025 (Erhöhung um 4,5 % durch das
|
||||
* Pflegeunterstützungs- und -entlastungsgesetz).
|
||||
*
|
||||
* Pflegegrad 1 ist der Sonderfall: § 43 Abs. 2 SGB XI greift dort nicht.
|
||||
* Wer mit Pflegegrad 1 vollstationär gepflegt wird, erhält nach
|
||||
* § 43 Abs. 3 SGB XI einen Zuschuss von 125 EUR monatlich - deshalb steht
|
||||
* er hier mit ebendiesem Betrag, aber unter eigener Rechtsgrundlage.
|
||||
*
|
||||
* Die nächste turnusmäßige Anpassung nach § 30 SGB XI ist gesetzlich für
|
||||
* 2028 vorgesehen. Trotzdem: zum Jahreswechsel nachsehen.
|
||||
*/
|
||||
export const PFLEGEKASSE_VOLLSTATIONAER_CENT: Record<Pflegegrad, bigint> = {
|
||||
1: 12_500n, // § 43 Abs. 3 SGB XI - Zuschuss, keine Pauschale
|
||||
2: 80_500n,
|
||||
3: 131_900n,
|
||||
4: 185_500n,
|
||||
5: 209_600n,
|
||||
}
|
||||
|
||||
export const PFLEGEKASSE_RECHTSGRUNDLAGE: Record<Pflegegrad, string> = {
|
||||
1: '§ 43 Abs. 3 SGB XI',
|
||||
2: '§ 43 Abs. 2 SGB XI',
|
||||
3: '§ 43 Abs. 2 SGB XI',
|
||||
4: '§ 43 Abs. 2 SGB XI',
|
||||
5: '§ 43 Abs. 2 SGB XI',
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Leistungszuschlag, § 43c SGB XI
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// STEHT NICHT HIER. Die Staffel (15 / 30 / 50 / 75 Prozent nach
|
||||
// Verweildauer, Stand 01.01.2024) gehoert dem Pflegemodul und liegt in
|
||||
// src/lib/pflege/konstanten.ts. Dieses Modul benutzt sie ueber
|
||||
// src/lib/sozialhilfe/heimkosten.ts, kopiert sie aber nicht - zwei Kopien
|
||||
// derselben Prozentsaetze werden bei der naechsten Gesetzesaenderung
|
||||
// zuverlaessig nur an einer Stelle nachgezogen.
|
||||
//
|
||||
// Merksatz zur Anwendung: Der Zuschlag mindert ausschliesslich den
|
||||
// pflegebedingten Eigenanteil (EEE) - nicht Unterkunft und Verpflegung,
|
||||
// nicht die Investitionskosten, nicht die Ausbildungsumlage. Wer ihn auf
|
||||
// die ganze Heimrechnung anwendet, rechnet die Deckungsluecke zu klein.
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Abwesenheit (Krankenhaus, Reha, Urlaub)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Abwesenheitstage mindern die Heimrechnung nicht ab dem ersten Tag.
|
||||
*
|
||||
* Die Rahmenverträge nach § 75 SGB XI regeln das landesweit: Die ersten
|
||||
* drei Abwesenheitstage sind voll zu zahlen, erst ab dem vierten
|
||||
* zusammenhängenden Tag wird das Entgelt gemindert - in Bayern
|
||||
* üblicherweise um 25 % von Pflegesatz sowie Unterkunft und Verpflegung.
|
||||
* Die Investitionskosten laufen weiter, der Platz wird ja freigehalten
|
||||
* (§ 87a Abs. 1 SGB XI, Platzfreihaltung bis 42 Tage im Kalenderjahr).
|
||||
*
|
||||
* Weil die Prozentsätze im Rahmenvertrag und im Heimvertrag stehen und
|
||||
* nicht im Gesetz, rechnet die Anwendung damit nur eine *Schätzung*. Was
|
||||
* gilt, steht in der Heimrechnung.
|
||||
*/
|
||||
export const ABWESENHEIT_KARENZTAGE = 3
|
||||
export const ABWESENHEIT_MINDERUNG_PROZENT = 25
|
||||
export const ABWESENHEIT_PLATZFREIHALTUNG_TAGE = 42
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Vermögen, § 90 SGB XII
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Vermögensfreibetrag ("kleinere Barbeträge"), § 90 Abs. 2 Nr. 9 SGB XII
|
||||
* i.V.m. § 1 Abs. 1 Nr. 1 lit. b der Verordnung zur Durchführung des
|
||||
* § 90 Abs. 2 Nr. 9 SGB XII: 10.000 EUR.
|
||||
*
|
||||
* Stand 01.01.2023 (bis dahin 5.000 EUR). Für jede Person, die von der
|
||||
* leistungsberechtigten Person überwiegend unterhalten wird, kommen
|
||||
* weitere 500 EUR hinzu (§ 1 Abs. 2 der Verordnung).
|
||||
*/
|
||||
export const VERMOEGENSFREIBETRAG_CENT = 1_000_000n
|
||||
export const VERMOEGENSFREIBETRAG_JE_UNTERHALTENE_PERSON_CENT = 50_000n
|
||||
export const VERMOEGENSFREIBETRAG_STAND = '2023-01-01'
|
||||
|
||||
/**
|
||||
* Rückforderung einer Schenkung wegen Verarmung, § 528 BGB.
|
||||
* Der Anspruch ist ausgeschlossen, wenn zum Zeitpunkt des Eintritts der
|
||||
* Bedürftigkeit seit der Leistung des geschenkten Gegenstands zehn Jahre
|
||||
* verstrichen sind (§ 529 Abs. 1 BGB). Deshalb fragt der Träger nach
|
||||
* notariellen Übertragungen der letzten zehn Jahre.
|
||||
*/
|
||||
export const SCHENKUNG_RUECKFORDERUNG_JAHRE = 10
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rechtsbehelf
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Regelfall: ein Monat ab Bekanntgabe, § 84 Abs. 1 Satz 1 SGG. */
|
||||
export const WIDERSPRUCHSFRIST_MONATE_REGEL = 1
|
||||
|
||||
/**
|
||||
* Fehlt die Rechtsbehelfsbelehrung oder ist sie unrichtig erteilt, beträgt
|
||||
* die Frist ein Jahr seit Bekanntgabe (§ 66 Abs. 2 Satz 1 SGG).
|
||||
* Das wird ausdrücklich ausgewählt und nie automatisch angenommen.
|
||||
*/
|
||||
export const WIDERSPRUCHSFRIST_MONATE_OHNE_BELEHRUNG = 12
|
||||
|
||||
/**
|
||||
* Zugangsvermutung bei Übermittlung durch die Post im Inland:
|
||||
* bekanntgegeben am dritten Tag nach der Aufgabe zur Post
|
||||
* (§ 37 Abs. 2 Satz 1 SGB X).
|
||||
*/
|
||||
export const ZUGANGSVERMUTUNG_TAGE = 3
|
||||
|
||||
/** Vorwarnstufen für die Frist "Widerspruch möglich bis" (Tage vorher). */
|
||||
export const WIDERSPRUCH_VORWARNUNG_TAGE = [21, 14, 7, 3, 1]
|
||||
|
||||
/** Vorwarnstufen für eine vom Träger gesetzte Nachforderungsfrist. */
|
||||
export const NACHFORDERUNG_VORWARNUNG_TAGE = [14, 7, 3, 1]
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Weitere Leistungen
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Bayerisches Landespflegegeld: 1.000 EUR im Jahr, ab Pflegegrad 2,
|
||||
* Antrag bis 31.12. des jeweiligen Jahres (Bayerisches
|
||||
* Landespflegegeldgesetz - BayLPflGG).
|
||||
*
|
||||
* Der Betrag steht im Haushalt des Freistaats und wurde politisch schon
|
||||
* mehrfach in Frage gestellt. Vor der Antragstellung die aktuelle Höhe
|
||||
* beim Landesamt für Pflege prüfen.
|
||||
*/
|
||||
export const LANDESPFLEGEGELD_CENT = 100_000n
|
||||
export const LANDESPFLEGEGELD_AB_PFLEGEGRAD = 2
|
||||
export const LANDESPFLEGEGELD_ANTRAGSSCHLUSS = '31.12.'
|
||||
|
||||
/**
|
||||
* Zuzahlungsbefreiung, § 62 SGB V: Die Belastungsgrenze liegt bei 2 % der
|
||||
* jährlichen Bruttoeinnahmen zum Lebensunterhalt, bei schwerwiegend
|
||||
* chronisch Kranken bei 1 %.
|
||||
*
|
||||
* Für Menschen, die Leistungen nach dem SGB XII in einer Einrichtung
|
||||
* erhalten, ist als Bruttoeinnahme der Regelbedarf der Regelbedarfsstufe 1
|
||||
* anzusetzen (§ 62 Abs. 2 Satz 5 und 6 SGB V) - nicht die tatsächliche
|
||||
* Rente. Das macht die Befreiung meist schon im Januar erreichbar.
|
||||
*
|
||||
* 2 % von 12 x 563,00 EUR = 135,12 EUR; 1 % = 67,56 EUR (Stand 2025).
|
||||
*/
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_PROZENT = 2
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_PROZENT = 1
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_CENT =
|
||||
(REGELBEDARFSSTUFE_1_CENT * 12n * BigInt(ZUZAHLUNG_BELASTUNGSGRENZE_PROZENT)) / 100n
|
||||
export const ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_CENT =
|
||||
(REGELBEDARFSSTUFE_1_CENT * 12n * BigInt(ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_PROZENT)) / 100n
|
||||
|
||||
/**
|
||||
* Rundfunkbeitrag je Wohnung, § 8 Rundfunkfinanzierungsstaatsvertrag:
|
||||
* 18,36 EUR monatlich, unverändert seit 01.08.2021. Die Ermäßigung auf ein
|
||||
* Drittel (§ 4 Abs. 2 RBStV, Merkzeichen RF) beträgt 6,12 EUR.
|
||||
*/
|
||||
export const RUNDFUNKBEITRAG_CENT = 1_836n
|
||||
export const RUNDFUNKBEITRAG_ERMAESSIGT_CENT = 612n
|
||||
export const RUNDFUNKBEITRAG_STAND = '2021-08-01'
|
||||
|
||||
+437
-437
@@ -1,437 +1,437 @@
|
||||
import { z } from 'zod'
|
||||
import { betragZuCent } from '@/lib/sozialhilfe/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 = betragZuCent(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 = betragZuCent(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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user