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:
Florian Wilde
2026-08-22 06:22:05 +02:00
parent e7006c894e
commit d35e24f830
11 changed files with 2745 additions and 2537 deletions
+243
View File
@@ -0,0 +1,243 @@
# Schema-Wünsche aus Etappe 5 „Behörde“
Das Schema wurde nicht angefasst. Was beim Bauen des Sozialhilfemoduls
gefehlt hat, steht hier — nach Dringlichkeit sortiert, jeweils mit dem
Behelf, der jetzt an seiner Stelle steht.
---
## 1. `Heimkostenmonat`: Kostenseite und Deckungsseite überschneiden sich — wichtig
Dieselbe Sache wie Punkt 1 in `docs/schema-wuensche/dashboard.md`, hier aus
Sicht des Moduls, das die Gegenüberstellung baut.
**Das Problem.** `pflegeEigenanteilCent` heißt „einrichtungseinheitlicher
Eigenanteil". Der ist definitionsgemäß **schon um die Pauschale der
Pflegekasse nach § 43 Abs. 2 SGB XI gekürzt**. Steht in derselben Zeile
zusätzlich `pflegekasseCent` als Deckung, wird dasselbe Geld zweimal
gezählt — die Deckungslücke fällt um die Pauschale zu klein aus, bei
Pflegegrad 3 um 1.319 Euro im Monat.
**Was ich entschieden habe.** Dieses Modul rechnet in der Lesart
`EIGENANTEIL_NETTO` (siehe Kopfkommentar in
`src/lib/sozialhilfe/heimkosten.ts`):
```
Bedarf = pflegeEigenanteil (netto, wie auf der Heimrechnung)
+ unterkunftVerpflegung + investitionskosten
+ ausbildungsumlage + zusatzleistungen
+ barbetrag + bekleidungspauschale
Deckung = leistungszuschlag + rente + sonstigeEinkuenfte
+ sozialhilfe + eigenmittel
Deckungslücke = Bedarf − Deckung
```
`pflegekasseCent` zählt **nicht** als Deckung. Der Wert wird als
`pflegekasseNachrichtlichCent` mitgeführt und angezeigt; ist er ungleich
null, gibt `heimkostenAuswerten()` einen Hinweis aus, der die
Doppelzählung erklärt. Das Formular belegt das Feld nicht mehr vor.
`leistungszuschlagCent` zählt dagegen **sehr wohl** als Deckung: Der
Zuschlag nach § 43c SGB XI mindert den Eigenanteil erst danach und steht
auf der Heimrechnung als eigene Abzugsposition.
Die Alternative — durchgängig den Bruttosatz erfassen — ist als Lesart
`PFLEGESATZ_BRUTTO` implementiert und getestet: Bei konsistenter Erfassung
kommen beide Lesarten auf dieselbe Deckungslücke. Benutzt wird sie in der
Oberfläche nicht, weil das Schema keinen Ort hat, um die Wahl zu
speichern.
**Wunsch, in dieser Reihenfolge:**
1. Den Kommentar am Feld schärfen, damit die Lesart im Schema steht und
nicht nur hier:
```prisma
/// NETTO: der einrichtungseinheitliche Eigenanteil, wie er auf der
/// Heimrechnung steht - bereits um § 43 Abs. 2 SGB XI gekuerzt, noch
/// nicht um den Leistungszuschlag. pflegekasseCent zaehlt deshalb NICHT
/// als Deckung, sondern nur nachrichtlich.
pflegeEigenanteilCent BigInt @default(0)
```
2. Wenn beide Lesarten gebraucht werden, ein Feld dafür — sonst muss jedes
Modul raten, was in der Zeile steht:
```prisma
enum Kostenlesart {
EIGENANTEIL_NETTO
PFLEGESATZ_BRUTTO
}
lesart Kostenlesart @default(EIGENANTEIL_NETTO)
```
3. Alternativ ein `heimrechnungGesamtCent`, das schlicht den
Rechnungsbetrag trägt; die Aufteilung wäre dann nur noch informativ und
die Summe könnte gegen sie geprüft werden.
**Verabredung bis dahin**: Wer `Heimkostenmonat` schreibt — dieses Modul,
das Dashboard, ein Import — trägt als `pflegeEigenanteilCent` den Netto-
Eigenanteil ein und lässt `pflegekasseCent` auf 0.
---
## 2. `Nachforderung.checklistenSchluessel` — wichtig
```prisma
/// Stabiler Schluessel aus src/lib/sozialhilfe/checkliste.ts.
/// Verhindert Dubletten beim erneuten Uebernehmen der Standardunterlagen.
checklistenSchluessel String?
@@unique([antragId, checklistenSchluessel])
```
**Warum**: `ausCheckliste: Boolean` merkt sich nur *dass* eine Zeile aus der
Checkliste kam, nicht *welche* Position. Um beim zweiten Aufruf des Panels
nicht dieselbe Unterlage noch einmal anzulegen, vergleicht
`nochNichtUebernommen()` derzeit über die **Bezeichnung** (kleingeschrieben,
getrimmt). Das reicht, solange niemand die Bezeichnung ändert — tut er es,
entsteht beim nächsten Übernehmen eine Dublette.
**Behelf**: Vergleich über die Bezeichnung in
`src/lib/sozialhilfe/checkliste.ts` und in `checklisteUebernehmen()`.
---
## 3. Rückkaufswert am Vertrag — wichtig
```prisma
model Vertrag {
// ...
rueckkaufswertCent BigInt?
rueckkaufswertStichtag DateTime? @db.Date
}
```
**Warum**: Die Bewertungsansicht stellt Vermögensposten und Verträge
nebeneinander. Ein Vermögensposten trägt seinen Wert über `Wertstand`,
ein Vertrag hat gar kein Wertfeld — nur `beitragCent`, und das ist die
Prämie, nicht der Wert. Verträge erscheinen deshalb ohne Betrag, und die
Summe „verwertbar“ ist unvollständig, solange nicht zu jeder Versicherung
zusätzlich ein Vermögensposten angelegt wurde.
Für das Sozialhilfeverfahren ist genau dieser Wert der entscheidende: Der
Träger fragt nach Rückkaufswerten, nicht nach Versicherungssummen.
**Behelf**: Die Bewertungsansicht zeigt nur Verträge, die überhaupt einen
Wert haben können (`MIT_RUECKKAUFSWERT` aus
`src/lib/vertraege/konstanten.ts`), und markiert sie mit „Wert noch nicht
ermittelt“. Wer den Betrag braucht, legt zusätzlich einen
`Vermoegensposten` mit `vertragId` und einem `Wertstand` an — dann wird der
Vertrag in der Liste ausgeblendet, damit die Summe nicht doppelt zählt.
---
## 4. `Bescheid.rechtsbehelfsbelehrungVorhanden` — mittel
```prisma
/// Ausdrueckliche Einschaetzung, nicht aus der Fristlaenge abgeleitet.
rechtsbehelfsbelehrungVorhanden Boolean @default(true)
rechtsbehelfsbelehrungBemerkung String?
```
**Warum**: Ob die Jahresfrist des § 66 Abs. 2 SGG gilt, ist eine bewusste
Einschätzung mit Begründung („die Belehrung nennt die falsche Behörde“).
Derzeit wird sie aus `rechtsbehelfsfristMonate === 12` zurückgeschlossen —
das trägt, verliert aber die Begründung, und die bräuchte man genau dann,
wenn man sich im Widerspruchsverfahren darauf beruft.
**Behelf**: `belehrungZuMonaten()` in
`src/lib/sozialhilfe/rechtsbehelf.ts`. Die Begründung gehört bis auf
Weiteres in `notizenVerschluesselt`.
---
## 5. `Bescheid.widerspruchFristId` → `Frist` — mittel
```prisma
widerspruchFristId String? @unique
widerspruchFrist Frist? @relation(fields: [widerspruchFristId], references: [id], onDelete: SetNull)
```
**Warum**: Die angelegte Frist „Widerspruch möglich bis“ wird derzeit über
`Frist.systemSchluessel = "sozialhilfe-widerspruch-<bescheidId>"` gefunden.
Das funktioniert (`@@unique([betreuteId, systemSchluessel])` trägt es), ist
aber eine Verknüpfung über eine zusammengesetzte Zeichenkette. Ein echter
Fremdschlüssel wäre sauberer und ließe die Frist beim Löschen des Bescheids
mitziehen. Dasselbe gilt für
`"sozialhilfe-nachforderung-<nachforderungId>"`.
**Behelf**: `systemSchluessel` mit festem Präfix, `upsert` statt `create`.
---
## 6. `Heimkostenmonat` — Feld für die tatsächliche Zahlung — mittel
```prisma
/// Was tatsaechlich vom Konto ging - fuer den Abgleich mit dem Bescheid.
gezahltCent BigInt?
gezahltAm DateTime? @db.Date
rechnungsnummer String?
```
**Warum**: Die Gegenüberstellung rechnet Bedarf gegen Deckung. Was
tatsächlich abgebucht wurde, steht nirgends — und genau die Differenz
zwischen „laut Bescheid“ und „tatsächlich abgebucht“ ist der Anlass, beim
Träger nachzufragen.
**Behelf**: `bemerkung`.
---
## 7. `Heimkostenmonat.abwesenheitsminderungCent` — klein
```prisma
/// Tatsaechliche Minderung laut Heimrechnung, nicht die Schaetzung.
abwesenheitsminderungCent BigInt @default(0)
```
**Warum**: `abwesenheitstage` ist da, die Wirkung schätzt
`abwesenheitSchaetzen()` nach dem Rahmenvertrag (drei Karenztage, danach
25 % auf Pflegeanteil sowie Unterkunft und Verpflegung). Was die
Einrichtung wirklich abgezogen hat, steht auf der Rechnung und ließe sich
nicht festhalten.
**Behelf**: Die Schätzung wird ausdrücklich als Schätzung ausgewiesen und
neben die erfasste Lücke gestellt, nicht in sie hineingerechnet.
---
## 8. `Leistung.wiederholungsfristMonat/-tag` — klein
```prisma
/// Fuer jaehrlich zu wiederholende Antraege: Stichtag im Jahr.
antragsschlussMonat Int?
antragsschlussTag Int?
```
**Warum**: `jaehrlichNeuBeantragen: Boolean` sagt, *dass* wiederholt werden
muss, nicht *bis wann*. Beim Landespflegegeld ist der 31.12. eine
Ausschlussfrist — daraus ließe sich automatisch eine Frist ableiten,
statt sie von Hand anzulegen.
**Behelf**: `antragsschluss` als Text im Katalog
(`src/lib/sozialhilfe/leistungskatalog.ts`) und ein Hinweis in der
Oberfläche. Die Fristableitung übernimmt `pflichtfristenAbleiten()` mit
`landespflegegeld-<jahr>` (siehe `docs/SCHNITTSTELLEN.md`).
---
## 9. `Nachforderung.notizenVerschluesselt` — klein
**Warum**: `erlaeuterung` und `begruendungNichtVorhanden` liegen im
Klartext. Beides ist meist harmlos („Grundbuchauszug beim Amtsgericht
angefordert“), kann aber Gesundheits- oder Vermögensangaben enthalten
(„Rückkaufswert 12.480 Euro, Police verpfändet“). Wäre mindestens die
Begründung verschlüsselt, wäre das konsequenter zum Rest des Schemas.
**Behelf**: keiner. Die Felder werden benutzt, wie sie sind. Falls sie
verschlüsselt werden sollen, gehört `Nachforderung` in
`VERSCHLUESSELTE_FELDER` in `src/lib/crypto/feld.ts`.
@@ -1,149 +1,149 @@
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { Trash2 } from 'lucide-react' import { Trash2 } from 'lucide-react'
import { SeitenKopf } from '@/components/seiten-kopf' import { SeitenKopf } from '@/components/seiten-kopf'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { AktionMitRueckfrage } from '@/components/sozialhilfe/bausteine' import { AktionMitRueckfrage } from '@/components/sozialhilfe/bausteine'
import { BescheidFormular } from '@/components/sozialhilfe/bescheid-formular' import { BescheidFormular } from '@/components/sozialhilfe/bescheid-formular'
import { BescheidPruefung } from '@/components/sozialhilfe/bescheid-pruefung' import { BescheidPruefung } from '@/components/sozialhilfe/bescheid-pruefung'
import { oeffnen } from '@/lib/crypto/feld' import { oeffnen } from '@/lib/crypto/feld'
import { db, nichtGeloescht } from '@/lib/db' import { db, nichtGeloescht } from '@/lib/db'
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum' import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
import { BESCHEID_ART_BESCHRIFTUNG } from '@/lib/sozialhilfe/bescheide' import { BESCHEID_ART_BESCHRIFTUNG } from '@/lib/sozialhilfe/bescheide'
import { centZuEingabe } from '@/lib/sozialhilfe/betrag' import { betragFormatieren } from '@/lib/finanzen/betrag'
import { import {
fristAmpel, fristAmpel,
rechtsbehelfsfristBerechnen, rechtsbehelfsfristBerechnen,
zugangsvermutung, zugangsvermutung,
} from '@/lib/sozialhilfe/rechtsbehelf' } from '@/lib/sozialhilfe/rechtsbehelf'
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang' import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
import { bescheidLoeschen } from '@/server/actions/sozialhilfe' import { bescheidLoeschen } from '@/server/actions/sozialhilfe'
import { formatDatum } from '@/lib/utils' import { formatDatum } from '@/lib/utils'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
/** /**
* Bescheid-Prüfung. * Bescheid-Prüfung.
* *
* Der Rechenweg der Widerspruchsfrist steht ausgeschrieben da - die * Der Rechenweg der Widerspruchsfrist steht ausgeschrieben da - die
* Anwendung kann sich irren, und wer eine Frist verpasst, soll sehen * Anwendung kann sich irren, und wer eine Frist verpasst, soll sehen
* können, wo die Rechnung abgebogen ist. * können, wo die Rechnung abgebogen ist.
*/ */
export default async function BescheidSeite({ export default async function BescheidSeite({
params, params,
}: { }: {
params: Promise<{ antragId: string; bescheidId: string }> params: Promise<{ antragId: string; bescheidId: string }>
}) { }) {
const { antragId, bescheidId } = await params const { antragId, bescheidId } = await params
const { betreuteId, darfSchreiben } = await sozialhilfeKontext() const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
const bescheid = await db.bescheid.findFirst({ const bescheid = await db.bescheid.findFirst({
where: { id: bescheidId, antragId, antrag: { betreuteId }, ...nichtGeloescht }, where: { id: bescheidId, antragId, antrag: { betreuteId }, ...nichtGeloescht },
include: { antrag: { select: { id: true, traeger: true, aktenzeichen: true } } }, include: { antrag: { select: { id: true, traeger: true, aktenzeichen: true } } },
}) })
if (!bescheid) notFound() if (!bescheid) notFound()
const offen = oeffnen('Bescheid', bescheid) const offen = oeffnen('Bescheid', bescheid)
const heute = heuteAlsTag() const heute = heuteAlsTag()
const ergebnis = rechtsbehelfsfristBerechnen({ const ergebnis = rechtsbehelfsfristBerechnen({
zugegangenAm: bescheid.zugegangenAm, zugegangenAm: bescheid.zugegangenAm,
bescheidVom: bescheid.bescheidVom, bescheidVom: bescheid.bescheidVom,
fristMonate: bescheid.rechtsbehelfsfristMonate, fristMonate: bescheid.rechtsbehelfsfristMonate,
heute, heute,
}) })
const vorschlag = zugangsvermutung(bescheid.bescheidVom) const vorschlag = zugangsvermutung(bescheid.bescheidVom)
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<SeitenKopf <SeitenKopf
titel={`${BESCHEID_ART_BESCHRIFTUNG[bescheid.art]} vom ${formatDatum(bescheid.bescheidVom)}`} titel={`${BESCHEID_ART_BESCHRIFTUNG[bescheid.art]} vom ${formatDatum(bescheid.bescheidVom)}`}
beschreibung={ beschreibung={
<> <>
{bescheid.antrag.traeger} {bescheid.antrag.traeger}
{bescheid.antrag.aktenzeichen ? ` · Az. ${bescheid.antrag.aktenzeichen}` : ''} · Zugang{' '} {bescheid.antrag.aktenzeichen ? ` · Az. ${bescheid.antrag.aktenzeichen}` : ''} · Zugang{' '}
{formatDatum(bescheid.zugegangenAm)} {formatDatum(bescheid.zugegangenAm)}
</> </>
} }
pfad={[ pfad={[
{ titel: 'Start', href: '/heute' }, { titel: 'Start', href: '/heute' },
{ titel: 'Sozialhilfe', href: '/sozialhilfe' }, { titel: 'Sozialhilfe', href: '/sozialhilfe' },
{ titel: 'Antragsakte', href: `/sozialhilfe/${antragId}` }, { titel: 'Antragsakte', href: `/sozialhilfe/${antragId}` },
{ titel: 'Bescheid' }, { titel: 'Bescheid' },
]} ]}
/> />
<BescheidPruefung <BescheidPruefung
bescheidId={bescheid.id} bescheidId={bescheid.id}
pruefStatus={bescheid.pruefStatus} pruefStatus={bescheid.pruefStatus}
widerspruchEingelegtAm={ widerspruchEingelegtAm={
bescheid.widerspruchEingelegtAm ? alsIsoTag(bescheid.widerspruchEingelegtAm) : '' bescheid.widerspruchEingelegtAm ? alsIsoTag(bescheid.widerspruchEingelegtAm) : ''
} }
darfSchreiben={darfSchreiben} darfSchreiben={darfSchreiben}
rechenweg={{ rechenweg={{
berechenbar: ergebnis.berechenbar, berechenbar: ergebnis.berechenbar,
widerspruchMoeglichBis: ergebnis.widerspruchMoeglichBis widerspruchMoeglichBis: ergebnis.widerspruchMoeglichBis
? alsIsoTag(ergebnis.widerspruchMoeglichBis) ? alsIsoTag(ergebnis.widerspruchMoeglichBis)
: null, : null,
rechnerischesEnde: ergebnis.rechnerischesEnde rechnerischesEnde: ergebnis.rechnerischesEnde
? alsIsoTag(ergebnis.rechnerischesEnde) ? alsIsoTag(ergebnis.rechnerischesEnde)
: null, : null,
verschoben: ergebnis.verschoben, verschoben: ergebnis.verschoben,
tageVerbleibend: ergebnis.tageVerbleibend, tageVerbleibend: ergebnis.tageVerbleibend,
abgelaufen: ergebnis.abgelaufen, abgelaufen: ergebnis.abgelaufen,
ampel: fristAmpel(ergebnis), ampel: fristAmpel(ergebnis),
schritte: ergebnis.schritte, schritte: ergebnis.schritte,
hinweise: ergebnis.hinweise, hinweise: ergebnis.hinweise,
}} }}
/> />
{offen.notizen && ( {offen.notizen && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Notizen</CardTitle> <CardTitle>Notizen</CardTitle>
<CardDescription>Verschlüsselt gespeichert.</CardDescription> <CardDescription>Verschlüsselt gespeichert.</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="whitespace-pre-line text-sm">{offen.notizen}</p> <p className="whitespace-pre-line text-sm">{offen.notizen}</p>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{darfSchreiben && ( {darfSchreiben && (
<section className="space-y-4"> <section className="space-y-4">
<h2 className="text-lg font-semibold">Bescheid bearbeiten</h2> <h2 className="text-lg font-semibold">Bescheid bearbeiten</h2>
<BescheidFormular <BescheidFormular
antragId={antragId} antragId={antragId}
zugangsvorschlag={{ datum: alsIsoTag(vorschlag.datum), hinweis: vorschlag.hinweis }} zugangsvorschlag={{ datum: alsIsoTag(vorschlag.datum), hinweis: vorschlag.hinweis }}
werte={{ werte={{
id: bescheid.id, id: bescheid.id,
art: bescheid.art, art: bescheid.art,
bescheidVom: alsIsoTag(bescheid.bescheidVom), bescheidVom: alsIsoTag(bescheid.bescheidVom),
zugegangenAm: bescheid.zugegangenAm ? alsIsoTag(bescheid.zugegangenAm) : '', zugegangenAm: bescheid.zugegangenAm ? alsIsoTag(bescheid.zugegangenAm) : '',
zeitraumVon: bescheid.zeitraumVon ? alsIsoTag(bescheid.zeitraumVon) : '', zeitraumVon: bescheid.zeitraumVon ? alsIsoTag(bescheid.zeitraumVon) : '',
zeitraumBis: bescheid.zeitraumBis ? alsIsoTag(bescheid.zeitraumBis) : '', zeitraumBis: bescheid.zeitraumBis ? alsIsoTag(bescheid.zeitraumBis) : '',
betrag: centZuEingabe(bescheid.betragCent), betrag: bescheid.betragCent === null ? '' : betragFormatieren(bescheid.betragCent),
rechtsbehelfsfristMonate: bescheid.rechtsbehelfsfristMonate, rechtsbehelfsfristMonate: bescheid.rechtsbehelfsfristMonate,
widerspruchEingelegtAm: bescheid.widerspruchEingelegtAm widerspruchEingelegtAm: bescheid.widerspruchEingelegtAm
? alsIsoTag(bescheid.widerspruchEingelegtAm) ? alsIsoTag(bescheid.widerspruchEingelegtAm)
: '', : '',
notizen: offen.notizen ?? '', notizen: offen.notizen ?? '',
}} }}
/> />
<div className="border-t pt-6"> <div className="border-t pt-6">
<AktionMitRueckfrage <AktionMitRueckfrage
aktion={bescheidLoeschen.bind(null, bescheid.id)} aktion={bescheidLoeschen.bind(null, bescheid.id)}
frage="Diesen Bescheid in den Papierkorb legen? Er bleibt dort 30 Tage." frage="Diesen Bescheid in den Papierkorb legen? Er bleibt dort 30 Tage."
variant="outline" variant="outline"
size="default" size="default"
> >
<Trash2 aria-hidden="true" /> <Trash2 aria-hidden="true" />
Bescheid löschen Bescheid löschen
</AktionMitRueckfrage> </AktionMitRueckfrage>
</div> </div>
</section> </section>
)} )}
</div> </div>
) )
} }
+383 -318
View File
@@ -1,318 +1,383 @@
import Link from 'next/link' import Link from 'next/link'
import { SeitenKopf } from '@/components/seiten-kopf' import { SeitenKopf } from '@/components/seiten-kopf'
import { Rechtshinweis } from '@/components/rechtshinweis' import { Rechtshinweis } from '@/components/rechtshinweis'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { HeimkostenFormular } from '@/components/sozialhilfe/heimkosten-formular' import { HeimkostenFormular } from '@/components/sozialhilfe/heimkosten-formular'
import { HeimkostenTabelle, type MonatsZeile } from '@/components/sozialhilfe/heimkosten-tabelle' import { HeimkostenTabelle, type MonatsZeile } from '@/components/sozialhilfe/heimkosten-tabelle'
import { centZuEingabe } from '@/lib/sozialhilfe/betrag' import { betragFormatieren } from '@/lib/finanzen/betrag'
import { db, nichtGeloescht } from '@/lib/db' import { db, nichtGeloescht } from '@/lib/db'
import { heuteAlsTag } from '@/lib/fristen/datum' import { heuteAlsTag } from '@/lib/fristen/datum'
import { import {
BARBETRAG_CENT, BARBETRAG_CENT,
BEKLEIDUNGSPAUSCHALE_HINWEIS, BEKLEIDUNGSPAUSCHALE_HINWEIS,
KONSTANTEN_NAECHSTE_PRUEFUNG, KONSTANTEN_NAECHSTE_PRUEFUNG,
KONSTANTEN_STAND, KONSTANTEN_STAND,
} from '@/lib/sozialhilfe/konstanten' } from '@/lib/sozialhilfe/konstanten'
import { import {
heimkostenAuswerten, heimkostenAuswerten,
jahresverlaufBilden, jahresverlaufBilden,
leistungszuschlagProzent, KOSTENLESART_BESCHRIFTUNG,
leistungszuschlagStufe, KOSTENLESART_ERKLAERUNG,
monatsname, KOSTENLESART_STANDARD,
pflegekassePauschaleCent, leistungszuschlagProzent,
pflegekasseRechtsgrundlage, leistungszuschlagStufe,
verweildauerMonate, monatsname,
} from '@/lib/sozialhilfe/heimkosten' pflegekassePauschaleCent,
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang' pflegekasseRechtsgrundlage,
import { formatCent } from '@/lib/utils' verweildauerMonate,
import { Info, TrendingDown } from 'lucide-react' } from '@/lib/sozialhilfe/heimkosten'
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
export const dynamic = 'force-dynamic' import { formatCent } from '@/lib/utils'
import { Info, TrendingDown } from 'lucide-react'
/**
* Heimkosten-Gegenüberstellung. export const dynamic = 'force-dynamic'
*
* Die eine Zahl, um die es geht, ist die Deckungslücke: Was bleibt übrig, /**
* nachdem Pflegekasse, Rente und Sozialhilfe gezahlt haben? Sie steht je * Heimkosten-Gegenüberstellung.
* Monat und als Jahressumme da - denn eine Lücke von 180 Euro im Monat *
* sind über ein Jahr mehr als zweitausend. * Die eine Zahl, um die es geht, ist die Deckungslücke: Was bleibt übrig,
*/ * nachdem Pflegekasse, Rente und Sozialhilfe gezahlt haben? Sie steht je
export default async function HeimkostenSeite({ * Monat und als Jahressumme da - denn eine Lücke von 180 Euro im Monat
searchParams, * sind über ein Jahr mehr als zweitausend.
}: { */
searchParams: Promise<{ jahr?: string }> export default async function HeimkostenSeite({
}) { searchParams,
const { betreuteId, betreute } = await sozialhilfeKontext() }: {
const parameter = await searchParams searchParams: Promise<{ jahr?: string }>
}) {
const heute = heuteAlsTag() const { betreuteId, betreute } = await sozialhilfeKontext()
const gewaehltesJahr = Number.parseInt(parameter.jahr ?? '', 10) const parameter = await searchParams
const jahr = Number.isFinite(gewaehltesJahr) ? gewaehltesJahr : heute.getUTCFullYear()
const heute = heuteAlsTag()
const [monate, heimvertrag] = await Promise.all([ const gewaehltesJahr = Number.parseInt(parameter.jahr ?? '', 10)
db.heimkostenmonat.findMany({ const jahr = Number.isFinite(gewaehltesJahr) ? gewaehltesJahr : heute.getUTCFullYear()
where: { betreuteId, jahr },
orderBy: { monat: 'asc' }, const [monate, heimvertrag] = await Promise.all([
}), db.heimkostenmonat.findMany({
db.heimvertrag.findUnique({ where: { betreuteId } }), where: { betreuteId, jahr },
]) orderBy: { monat: 'asc' },
}),
const ergebnisse = monate.map((m) => db.heimvertrag.findUnique({ where: { betreuteId } }),
heimkostenAuswerten({ ])
jahr: m.jahr,
monat: m.monat, const ergebnisse = monate.map((m) =>
pflegeEigenanteilCent: m.pflegeEigenanteilCent, heimkostenAuswerten({
unterkunftVerpflegungCent: m.unterkunftVerpflegungCent, jahr: m.jahr,
investitionskostenCent: m.investitionskostenCent, monat: m.monat,
ausbildungsumlageCent: m.ausbildungsumlageCent, pflegeEigenanteilCent: m.pflegeEigenanteilCent,
zusatzleistungenCent: m.zusatzleistungenCent, unterkunftVerpflegungCent: m.unterkunftVerpflegungCent,
pflegekasseCent: m.pflegekasseCent, investitionskostenCent: m.investitionskostenCent,
leistungszuschlagCent: m.leistungszuschlagCent, ausbildungsumlageCent: m.ausbildungsumlageCent,
renteCent: m.renteCent, zusatzleistungenCent: m.zusatzleistungenCent,
sonstigeEinkuenfteCent: m.sonstigeEinkuenfteCent, pflegekasseCent: m.pflegekasseCent,
sozialhilfeCent: m.sozialhilfeCent, leistungszuschlagCent: m.leistungszuschlagCent,
eigenmittelCent: m.eigenmittelCent, renteCent: m.renteCent,
barbetragCent: m.barbetragCent, sonstigeEinkuenfteCent: m.sonstigeEinkuenfteCent,
bekleidungspauschaleCent: m.bekleidungspauschaleCent, sozialhilfeCent: m.sozialhilfeCent,
abwesenheitstage: m.abwesenheitstage, eigenmittelCent: m.eigenmittelCent,
}), barbetragCent: m.barbetragCent,
) bekleidungspauschaleCent: m.bekleidungspauschaleCent,
abwesenheitstage: m.abwesenheitstage,
const verlauf = jahresverlaufBilden(jahr, ergebnisse) }),
)
const zeilen: MonatsZeile[] = Array.from({ length: 12 }, (_, i) => {
const monat = i + 1 const verlauf = jahresverlaufBilden(jahr, ergebnisse)
const ergebnis = ergebnisse.find((e) => e.monat === monat)
const roh = monate.find((m) => m.monat === monat) const zeilen: MonatsZeile[] = Array.from({ length: 12 }, (_, i) => {
return { const monat = i + 1
jahr, const ergebnis = ergebnisse.find((e) => e.monat === monat)
monat, const roh = monate.find((m) => m.monat === monat)
heimkostenCent: ergebnis?.heimkostenCent ?? 0n, return {
gesamtbedarfCent: ergebnis?.gesamtbedarfCent ?? 0n, jahr,
deckungCent: ergebnis?.deckungCent ?? 0n, monat,
deckungslueckeCent: ergebnis?.deckungslueckeCent ?? 0n, heimkostenCent: ergebnis?.heimkostenCent ?? 0n,
ueberdeckungCent: ergebnis?.ueberdeckungCent ?? 0n, gesamtbedarfCent: ergebnis?.gesamtbedarfCent ?? 0n,
abwesenheitstage: roh?.abwesenheitstage ?? 0, deckungCent: ergebnis?.deckungCent ?? 0n,
erfasst: ergebnis !== undefined, deckungslueckeCent: ergebnis?.deckungslueckeCent ?? 0n,
} ueberdeckungCent: ergebnis?.ueberdeckungCent ?? 0n,
}) abwesenheitstage: roh?.abwesenheitstage ?? 0,
erfasst: ergebnis !== undefined,
// Vorschläge fürs Formular: Pauschale der Pflegekasse zum Pflegegrad }
// und der gesetzliche Mindest-Barbetrag. })
const pauschale = pflegekassePauschaleCent(betreute.pflegegrad)
const vorschlagPflegekasse = pauschale // Vorschläge fürs Formular: Pauschale der Pflegekasse zum Pflegegrad
? { // und der gesetzliche Mindest-Barbetrag.
betrag: centZuEingabe(pauschale), const pauschale = pflegekassePauschaleCent(betreute.pflegegrad)
text: `Pflegegrad ${betreute.pflegegrad}: ${formatCent(pauschale)} monatlich (${pflegekasseRechtsgrundlage(betreute.pflegegrad)}, Stand ${KONSTANTEN_STAND}).`,
} // Bewusst nur ein Hinweis, kein Vorschlagswert zum Uebernehmen: Unter der
: null // Lesart EIGENANTEIL_NETTO steckt die Pauschale schon im Eigenanteil.
// Wer sie hier zusaetzlich eintraegt, zaehlt dasselbe Geld zweimal.
const einzug = heimvertrag?.einzugAm ?? betreute.heimEinzugAm ?? null const hinweisPflegekasse = pauschale
const verweildauer = einzug ? verweildauerMonate(einzug, heute) : null ? `Pflegegrad ${betreute.pflegegrad}: ${formatCent(pauschale)} monatlich ` +
`(${pflegekasseRechtsgrundlage(betreute.pflegegrad)}, Stand ${KONSTANTEN_STAND}). ` +
const naechsterOffenerMonat = `Dieser Betrag ist im Eigenanteil oben bereits abgezogen — lass das Feld deshalb auf 0. ` +
zeilen.find((z) => !z.erfasst && z.monat <= (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 12)) `Es dient nur der Erinnerung, was die Pflegekasse direkt an die Einrichtung zahlt.`
?.monat ?? (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 1) : 'Pauschale nach § 43 SGB XI. Sie ist im Eigenanteil bereits abgezogen — lass das Feld ' +
'auf 0. Hinterlege den Pflegegrad in den Stammdaten, dann steht hier der Betrag.'
const jahre = Array.from({ length: 6 }, (_, i) => heute.getUTCFullYear() - 3 + i)
const einzug = heimvertrag?.einzugAm ?? betreute.heimEinzugAm ?? null
return ( const verweildauer = einzug ? verweildauerMonate(einzug, heute) : null
<div className="space-y-8">
<SeitenKopf const naechsterOffenerMonat =
titel={`Heimkosten ${jahr}`} zeilen.find((z) => !z.erfasst && z.monat <= (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 12))
beschreibung="Kosten gegen Deckung, Monat für Monat. Zum Bedarf zählen auch Barbetrag und Bekleidungspauschale — sie müssen der betreuten Person verbleiben." ?.monat ?? (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 1)
pfad={[
{ titel: 'Start', href: '/heute' }, const jahre = Array.from({ length: 6 }, (_, i) => heute.getUTCFullYear() - 3 + i)
{ titel: 'Sozialhilfe', href: '/sozialhilfe' },
{ titel: 'Heimkosten' }, return (
]} <div className="space-y-8">
aktionen={ <SeitenKopf
<nav aria-label="Jahr wählen" className="flex flex-wrap gap-1"> titel={`Heimkosten ${jahr}`}
{jahre.map((j) => ( beschreibung="Kosten gegen Deckung, Monat für Monat. Zum Bedarf zählen auch Barbetrag und Bekleidungspauschale — sie müssen der betreuten Person verbleiben."
<Link pfad={[
key={j} { titel: 'Start', href: '/heute' },
href={`/sozialhilfe/heimkosten?jahr=${j}`} { titel: 'Sozialhilfe', href: '/sozialhilfe' },
aria-current={j === jahr ? 'page' : undefined} { titel: 'Heimkosten' },
className={`inline-flex h-11 min-w-11 items-center justify-center rounded-md px-3 text-sm font-medium ${ ]}
j === jahr aktionen={
? 'bg-primary text-primary-foreground' <nav aria-label="Jahr wählen" className="flex flex-wrap gap-1">
: 'border border-input hover:bg-accent' {jahre.map((j) => (
}`} <Link
> key={j}
{j} href={`/sozialhilfe/heimkosten?jahr=${j}`}
</Link> aria-current={j === jahr ? 'page' : undefined}
))} className={`inline-flex h-11 min-w-11 items-center justify-center rounded-md px-3 text-sm font-medium ${
</nav> j === jahr
} ? 'bg-primary text-primary-foreground'
/> : 'border border-input hover:bg-accent'
}`}
<div className="grid gap-4 sm:grid-cols-3"> >
<Card> {j}
<CardContent className="pt-5"> </Link>
<p className="text-sm text-muted-foreground">Bedarf {jahr}</p> ))}
<p className="text-xl font-semibold tabular-nums"> </nav>
{formatCent(verlauf.summeBedarfCent)} }
</p> />
</CardContent>
</Card> <div className="grid gap-4 sm:grid-cols-3">
<Card> <Card>
<CardContent className="pt-5"> <CardContent className="pt-5">
<p className="text-sm text-muted-foreground">Deckung {jahr}</p> <p className="text-sm text-muted-foreground">Bedarf {jahr}</p>
<p className="text-xl font-semibold tabular-nums"> <p className="text-xl font-semibold tabular-nums">
{formatCent(verlauf.summeDeckungCent)} {formatCent(verlauf.summeBedarfCent)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card className={verlauf.summeDeckungslueckeCent > 0n ? 'border-ampel-rot/40' : undefined}> <Card>
<CardContent className="pt-5"> <CardContent className="pt-5">
<p className="text-sm text-muted-foreground">Deckungslücke {jahr}</p> <p className="text-sm text-muted-foreground">Deckung {jahr}</p>
<p <p className="text-xl font-semibold tabular-nums">
className={`text-xl font-semibold tabular-nums ${ {formatCent(verlauf.summeDeckungCent)}
verlauf.summeDeckungslueckeCent > 0n ? 'text-ampel-rot' : 'text-ampel-gruen' </p>
}`} </CardContent>
> </Card>
{formatCent(verlauf.summeDeckungslueckeCent)} <Card className={verlauf.summeDeckungslueckeCent > 0n ? 'border-ampel-rot/40' : undefined}>
</p> <CardContent className="pt-5">
</CardContent> <p className="text-sm text-muted-foreground">Deckungslücke {jahr}</p>
</Card> <p
</div> className={`text-xl font-semibold tabular-nums ${
verlauf.summeDeckungslueckeCent > 0n ? 'text-ampel-rot' : 'text-ampel-gruen'
{verlauf.groessteLuecke && ( }`}
<Alert variant="warnung"> >
<TrendingDown aria-hidden="true" /> {formatCent(verlauf.summeDeckungslueckeCent)}
<AlertTitle> </p>
Größte Lücke im {monatsname(verlauf.groessteLuecke.monat)}:{' '} </CardContent>
{formatCent(verlauf.groessteLuecke.deckungslueckeCent)} </Card>
</AlertTitle> </div>
<AlertDescription>
Prüfe, ob der Bescheid für diesen Monat den vollen Bedarf abdeckt. Weicht die Rechnung {verlauf.groessteLuecke && (
vom Bescheid ab, ist das der Anlass für eine Rückfrage beim Träger — und gegebenenfalls <Alert variant="warnung">
für einen Widerspruch. <TrendingDown aria-hidden="true" />
</AlertDescription> <AlertTitle>
</Alert> Größte Lücke im {monatsname(verlauf.groessteLuecke.monat)}:{' '}
)} {formatCent(verlauf.groessteLuecke.deckungslueckeCent)}
</AlertTitle>
<Card> <AlertDescription>
<CardHeader> Prüfe, ob der Bescheid für diesen Monat den vollen Bedarf abdeckt. Weicht die Rechnung
<CardTitle>Jahresverlauf</CardTitle> vom Bescheid ab, ist das der Anlass für eine Rückfrage beim Träger — und gegebenenfalls
<CardDescription> für einen Widerspruch.
Gesamtbedarf ist die Heimrechnung plus Barbetrag und Bekleidungspauschale. </AlertDescription>
</CardDescription> </Alert>
</CardHeader> )}
<CardContent>
<HeimkostenTabelle zeilen={zeilen} /> <Card>
</CardContent> <CardHeader>
</Card> <CardTitle>Was hier gegen was gerechnet wird</CardTitle>
<CardDescription>
{ergebnisse.some((e) => e.hinweise.length > 0) && ( Lesart: {KOSTENLESART_BESCHRIFTUNG[KOSTENLESART_STANDARD]}. Ohne diese Festlegung
<Card> lässt sich die Rechnung nicht mit der Heimrechnung vergleichen.
<CardHeader> </CardDescription>
<CardTitle>Was auffällt</CardTitle> </CardHeader>
</CardHeader> <CardContent className="space-y-4 text-sm">
<CardContent> <p>{KOSTENLESART_ERKLAERUNG[KOSTENLESART_STANDARD]}</p>
<ul className="space-y-3">
{ergebnisse.flatMap((e) => <div className="grid gap-4 sm:grid-cols-2">
e.hinweise.map((hinweis, i) => ( <div className="rounded-md border p-3">
<li key={`${e.monat}-${i}`} className="text-sm"> <p className="font-semibold">Bedarf</p>
<span className="font-medium">{monatsname(e.monat)}: </span> <ul className="mt-1 list-inside list-disc text-muted-foreground">
{hinweis} <li>einrichtungseinheitlicher Eigenanteil (bereits netto)</li>
</li> <li>Unterkunft und Verpflegung</li>
)), <li>Investitionskosten</li>
)} <li>Ausbildungsumlage</li>
</ul> <li>Zusatzleistungen</li>
</CardContent> <li>Barbetrag (§ 27b Abs. 2 SGB XII)</li>
</Card> <li>Bekleidungspauschale</li>
)} </ul>
</div>
{ergebnisse.some((e) => e.abwesenheit.abwesenheitstage > 0) && ( <div className="rounded-md border p-3">
<Card> <p className="font-semibold">Deckung</p>
<CardHeader> <ul className="mt-1 list-inside list-disc text-muted-foreground">
<CardTitle>Abwesenheitstage</CardTitle> <li>Leistungszuschlag (§ 43c SGB XI)</li>
<CardDescription> <li>Rente</li>
Was die Abwesenheit voraussichtlich bringt — eine Schätzung nach dem Rahmenvertrag, <li>sonstige Einkünfte</li>
nicht die Abrechnung. <li>Anteil der Sozialhilfe</li>
</CardDescription> <li>Eigenmittel</li>
</CardHeader> </ul>
<CardContent> <p className="mt-2 text-muted-foreground">
<ul className="space-y-3"> <span className="font-medium">Nicht dabei:</span> die Pauschale der Pflegekasse
{ergebnisse nach § 43 Abs. 2 SGB XI. Sie ist im Eigenanteil schon abgezogen und würde hier ein
.filter((e) => e.abwesenheit.abwesenheitstage > 0) zweites Mal zählen.
.map((e) => ( </p>
<li key={e.monat} className="text-sm"> </div>
<span className="font-medium">{monatsname(e.monat)}: </span> </div>
{e.abwesenheit.erklaerung} Geschätzte Minderung{' '}
<span className="font-medium tabular-nums"> <p className="text-muted-foreground">
{formatCent(e.abwesenheit.geschaetzteMinderungCent)} Barbetrag und Bekleidungspauschale stehen bewusst auf der Bedarfsseite: Sie müssen der
</span> betreuten Person tatsächlich verbleiben. Wer sie herauslässt, hält die Deckung für
, die Lücke sänke damit auf{' '} besser, als sie ist.
<span className="font-medium tabular-nums"> </p>
{formatCent(e.deckungslueckeNachAbwesenheitCent)}
</span> <Rechtshinweis rechtsgrundlage="§ 43 Abs. 2 SGB XI, § 43c SGB XI, § 27b SGB XII" kurz />
. </CardContent>
</li> </Card>
))}
</ul> <Card>
<Rechtshinweis className="mt-4" rechtsgrundlage="§ 87a Abs. 1 SGB XI, § 75 SGB XI" kurz /> <CardHeader>
</CardContent> <CardTitle>Jahresverlauf</CardTitle>
</Card> <CardDescription>
)} Gesamtbedarf ist die Heimrechnung plus Barbetrag und Bekleidungspauschale.
</CardDescription>
<Alert variant="hinweis"> </CardHeader>
<Info aria-hidden="true" /> <CardContent>
<AlertTitle>Woher die Vorschlagswerte kommen</AlertTitle> <HeimkostenTabelle zeilen={zeilen} />
<AlertDescription className="space-y-1"> </CardContent>
<p> </Card>
Die hinterlegten Beträge haben den Stand {KONSTANTEN_STAND} und sind bis spätestens{' '}
{KONSTANTEN_NAECHSTE_PRUEFUNG} nachzuprüfen. Maßgeblich ist immer die Heimrechnung und {ergebnisse.some((e) => e.hinweise.length > 0) && (
der Bescheid des Trägers. <Card>
</p> <CardHeader>
{verweildauer !== null && ( <CardTitle>Was auffällt</CardTitle>
<p> </CardHeader>
Verweildauer im Heim: {verweildauer}{' '} <CardContent>
{verweildauer === 1 ? 'vollendeter Monat' : 'vollendete Monate'} — Leistungszuschlag{' '} <ul className="space-y-3">
{leistungszuschlagProzent(verweildauer)} % ({leistungszuschlagStufe(verweildauer)}, {ergebnisse.flatMap((e) =>
§ 43c SGB XI). e.hinweise.map((hinweis, i) => (
</p> <li key={`${e.monat}-${i}`} className="text-sm">
)} <span className="font-medium">{monatsname(e.monat)}: </span>
<p>{BEKLEIDUNGSPAUSCHALE_HINWEIS}</p> {hinweis}
</AlertDescription> </li>
</Alert> )),
)}
<section className="space-y-4"> </ul>
<h2 className="text-lg font-semibold">Monat erfassen oder ändern</h2> </CardContent>
<HeimkostenFormular </Card>
jahre={jahre} )}
vorschlagPflegekasse={vorschlagPflegekasse}
vorschlagBarbetrag={{ {ergebnisse.some((e) => e.abwesenheit.abwesenheitstage > 0) && (
betrag: centZuEingabe(BARBETRAG_CENT), <Card>
text: `Mindestens ${formatCent(BARBETRAG_CENT)} (27 % der Regelbedarfsstufe 1, § 27b Abs. 2 SGB XII, Stand ${KONSTANTEN_STAND}). Steht im Bescheid mehr, gilt der Bescheid.`, <CardHeader>
}} <CardTitle>Abwesenheitstage</CardTitle>
werte={{ <CardDescription>
jahr, Was die Abwesenheit voraussichtlich bringt — eine Schätzung nach dem Rahmenvertrag,
monat: naechsterOffenerMonat, nicht die Abrechnung.
pflegeEigenanteil: centZuEingabe(heimvertrag?.pflegeEigenanteilCent ?? null), </CardDescription>
unterkunftVerpflegung: centZuEingabe(heimvertrag?.unterkunftVerpflegungCent ?? null), </CardHeader>
investitionskosten: centZuEingabe(heimvertrag?.investitionskostenCent ?? null), <CardContent>
ausbildungsumlage: centZuEingabe(heimvertrag?.ausbildungsumlageCent ?? null), <ul className="space-y-3">
zusatzleistungen: '', {ergebnisse
pflegekasse: pauschale ? centZuEingabe(pauschale) : '', .filter((e) => e.abwesenheit.abwesenheitstage > 0)
leistungszuschlagProzent: .map((e) => (
verweildauer !== null ? leistungszuschlagProzent(verweildauer) : 0, <li key={e.monat} className="text-sm">
leistungszuschlag: '', <span className="font-medium">{monatsname(e.monat)}: </span>
rente: '', {e.abwesenheit.erklaerung} Geschätzte Minderung{' '}
sonstigeEinkuenfte: '', <span className="font-medium tabular-nums">
sozialhilfe: '', {formatCent(e.abwesenheit.geschaetzteMinderungCent)}
eigenmittel: '', </span>
barbetrag: centZuEingabe(BARBETRAG_CENT), , die Lücke sänke damit auf{' '}
bekleidungspauschale: '', <span className="font-medium tabular-nums">
abwesenheitstage: 0, {formatCent(e.deckungslueckeNachAbwesenheitCent)}
bemerkung: '', </span>
}} .
/> </li>
</section> ))}
</ul>
<Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" /> <Rechtshinweis className="mt-4" rechtsgrundlage="§ 87a Abs. 1 SGB XI, § 75 SGB XI" kurz />
</div> </CardContent>
) </Card>
} )}
<Alert variant="hinweis">
<Info aria-hidden="true" />
<AlertTitle>Woher die Vorschlagswerte kommen</AlertTitle>
<AlertDescription className="space-y-1">
<p>
Die hinterlegten Beträge haben den Stand {KONSTANTEN_STAND} und sind bis spätestens{' '}
{KONSTANTEN_NAECHSTE_PRUEFUNG} nachzuprüfen. Maßgeblich ist immer die Heimrechnung und
der Bescheid des Trägers.
</p>
{verweildauer !== null && (
<p>
Verweildauer im Heim: {verweildauer}{' '}
{verweildauer === 1 ? 'vollendeter Monat' : 'vollendete Monate'} — Leistungszuschlag{' '}
{leistungszuschlagProzent(verweildauer)} % ({leistungszuschlagStufe(verweildauer)},
§ 43c SGB XI).
</p>
)}
<p>{BEKLEIDUNGSPAUSCHALE_HINWEIS}</p>
</AlertDescription>
</Alert>
<section className="space-y-4">
<h2 className="text-lg font-semibold">Monat erfassen oder ändern</h2>
<HeimkostenFormular
jahre={jahre}
hinweisPflegekasse={hinweisPflegekasse}
vorschlagBarbetrag={{
betrag: betragFormatieren(BARBETRAG_CENT),
text: `Mindestens ${formatCent(BARBETRAG_CENT)} (27 % der Regelbedarfsstufe 1, § 27b Abs. 2 SGB XII, Stand ${KONSTANTEN_STAND}). Steht im Bescheid mehr, gilt der Bescheid.`,
}}
werte={{
jahr,
monat: naechsterOffenerMonat,
pflegeEigenanteil: eingabefeld(heimvertrag?.pflegeEigenanteilCent ?? null),
unterkunftVerpflegung: eingabefeld(heimvertrag?.unterkunftVerpflegungCent ?? null),
investitionskosten: eingabefeld(heimvertrag?.investitionskostenCent ?? null),
ausbildungsumlage: eingabefeld(heimvertrag?.ausbildungsumlageCent ?? null),
zusatzleistungen: '',
// Absichtlich leer: siehe hinweisPflegekasse.
pflegekasse: '',
leistungszuschlagProzent:
verweildauer !== null ? leistungszuschlagProzent(verweildauer) : 0,
leistungszuschlag: '',
rente: '',
sonstigeEinkuenfte: '',
sozialhilfe: '',
eigenmittel: '',
barbetrag: betragFormatieren(BARBETRAG_CENT),
bekleidungspauschale: '',
abwesenheitstage: 0,
bemerkung: '',
}}
/>
</section>
<Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" />
</div>
)
}
/** Cent fuer ein Eingabefeld, null wird zu leer. */
function eingabefeld(cent: bigint | null): string {
return cent === null ? '' : betragFormatieren(cent)
}
+232 -229
View File
@@ -1,229 +1,232 @@
import { SeitenKopf } from '@/components/seiten-kopf' import { SeitenKopf } from '@/components/seiten-kopf'
import { Rechtshinweis } from '@/components/rechtshinweis' import { Rechtshinweis } from '@/components/rechtshinweis'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { Vermoegensliste, type ListenPosten } from '@/components/sozialhilfe/vermoegensliste' import { Vermoegensliste, type ListenPosten } from '@/components/sozialhilfe/vermoegensliste'
import { oeffnen } from '@/lib/crypto/feld' import { oeffnen } from '@/lib/crypto/feld'
import { centZuZahl, db, nichtGeloescht } from '@/lib/db' import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
import { alsIsoTag } from '@/lib/fristen/datum' import { alsIsoTag } from '@/lib/fristen/datum'
import { VERMOEGENSART_BESCHRIFTUNG, VERMOEGENSRELEVANTE_VERTRAGSARTEN, VERTRAGSART_BESCHRIFTUNG } from '@/lib/sozialhilfe/arten' import { VERMOEGENS_ART_BESCHRIFTUNG } from '@/lib/finanzen/kategorien'
import { BEWERTUNG_BESCHRIFTUNG } from '@/lib/sozialhilfe/schonvermoegen' import { MIT_RUECKKAUFSWERT, VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
import { freibetragVergleichen, uebersichtBilden } from '@/lib/sozialhilfe/vermoegensuebersicht' import { BEWERTUNG_BESCHRIFTUNG } from '@/lib/sozialhilfe/schonvermoegen'
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang' import { freibetragVergleichen, uebersichtBilden } from '@/lib/sozialhilfe/vermoegensuebersicht'
import { formatCent } from '@/lib/utils' import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
import { AlertCircle } from 'lucide-react' import { formatCent } from '@/lib/utils'
import { AlertCircle } from 'lucide-react'
export const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
/**
* Vermögens-Bewertungsansicht. /**
* * Vermögens-Bewertungsansicht.
* Sie zählt zusammen und stellt gegenüber - sie entscheidet nichts. Was *
* geschützt ist, sagt der Träger; bleibt man uneins, das Sozialgericht. * Sie zählt zusammen und stellt gegenüber - sie entscheidet nichts. Was
* Deshalb heißt die Spalte "Einschätzung" und nicht "Bewertung". * geschützt ist, sagt der Träger; bleibt man uneins, das Sozialgericht.
*/ * Deshalb heißt die Spalte "Einschätzung" und nicht "Bewertung".
export default async function VermoegensbewertungSeite() { */
const { betreuteId, darfSchreiben } = await sozialhilfeKontext() export default async function VermoegensbewertungSeite() {
const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
const [posten, vertraege] = await Promise.all([
db.vermoegensposten.findMany({ const [posten, vertraege] = await Promise.all([
where: { betreuteId, ...nichtGeloescht }, db.vermoegensposten.findMany({
include: { wertstaende: { orderBy: { stichtag: 'desc' }, take: 1 } }, where: { betreuteId, ...nichtGeloescht },
orderBy: { bezeichnung: 'asc' }, include: { wertstaende: { orderBy: { stichtag: 'desc' }, take: 1 } },
}), orderBy: { bezeichnung: 'asc' },
db.vertrag.findMany({ }),
where: { db.vertrag.findMany({
betreuteId, where: {
art: { in: [...VERMOEGENSRELEVANTE_VERTRAGSARTEN] }, betreuteId,
...nichtGeloescht, // Nur Vertraege, die ueberhaupt einen Wert darstellen koennen. Ein
}, // Stromvertrag hat keinen Rueckkaufswert und wuerde die Liste zumuellen.
orderBy: { bezeichnung: 'asc' }, art: { in: [...MIT_RUECKKAUFSWERT] },
}), ...nichtGeloescht,
]) },
orderBy: { bezeichnung: 'asc' },
// Verträge, die schon als Vermögensposten geführt werden, nicht doppelt }),
// zeigen - sonst wäre die Summe zweimal drin. ])
const bereitsAlsPosten = new Set(
posten.map((p) => p.vertragId).filter((id): id is string => id !== null), // Verträge, die schon als Vermögensposten geführt werden, nicht doppelt
) // zeigen - sonst wäre die Summe zweimal drin.
const bereitsAlsPosten = new Set(
const zeilen: ListenPosten[] = [ posten.map((p) => p.vertragId).filter((id): id is string => id !== null),
...posten.map((p) => { )
const geoeffnet = oeffnen('Vermoegensposten', p)
const stand = p.wertstaende[0] const zeilen: ListenPosten[] = [
return { ...posten.map((p) => {
id: p.id, const geoeffnet = oeffnen('Vermoegensposten', p)
bezeichnung: p.bezeichnung, const stand = p.wertstaende[0]
herkunft: 'VERMOEGEN' as const, return {
art: p.art, id: p.id,
artBeschriftung: VERMOEGENSART_BESCHRIFTUNG[p.art], bezeichnung: p.bezeichnung,
bewertung: p.bewertungSozialhilfe, herkunft: 'VERMOEGEN' as const,
wertCent: stand ? centZuZahl(stand.wertCent) : null, art: p.art,
wertStichtag: stand ? alsIsoTag(stand.stichtag) : null, artBeschriftung: VERMOEGENS_ART_BESCHRIFTUNG[p.art],
begruendung: geoeffnet.bewertungBegruendung ?? '', bewertung: p.bewertungSozialhilfe,
zusatz: stand ? stand.wertquelle : null, wertCent: stand ? centZuZahl(stand.wertCent) : null,
} wertStichtag: stand ? alsIsoTag(stand.stichtag) : null,
}), begruendung: geoeffnet.bewertungBegruendung ?? '',
...vertraege zusatz: stand ? stand.wertquelle : null,
.filter((v) => !bereitsAlsPosten.has(v.id)) }
.map((v) => { }),
const geoeffnet = oeffnen('Vertrag', v) ...vertraege
return { .filter((v) => !bereitsAlsPosten.has(v.id))
id: v.id, .map((v) => {
bezeichnung: v.bezeichnung, const geoeffnet = oeffnen('Vertrag', v)
herkunft: 'VERTRAG' as const, return {
art: v.art, id: v.id,
artBeschriftung: VERTRAGSART_BESCHRIFTUNG[v.art], bezeichnung: v.bezeichnung,
bewertung: v.bewertungSozialhilfe, herkunft: 'VERTRAG' as const,
// Der Rückkaufswert steht am Vertrag nicht als Betrag; er wird art: v.art,
// als Vermögensposten mit Wertstand geführt. Bis dahin: offen. artBeschriftung: VERTRAG_ART_BESCHRIFTUNG[v.art],
wertCent: null, bewertung: v.bewertungSozialhilfe,
wertStichtag: null, // Der Rückkaufswert steht am Vertrag nicht als Betrag; er wird
begruendung: geoeffnet.bewertungBegruendung ?? '', // als Vermögensposten mit Wertstand geführt. Bis dahin: offen.
zusatz: v.gesellschaft, wertCent: null,
} wertStichtag: null,
}), begruendung: geoeffnet.bewertungBegruendung ?? '',
] zusatz: v.gesellschaft,
}
const uebersicht = uebersichtBilden( }),
zeilen.map((z) => ({ ]
id: z.id,
bezeichnung: z.bezeichnung, const uebersicht = uebersichtBilden(
herkunft: z.herkunft, zeilen.map((z) => ({
artBeschriftung: z.artBeschriftung, id: z.id,
bewertung: z.bewertung, bezeichnung: z.bezeichnung,
wertCent: z.wertCent === null ? null : BigInt(z.wertCent), herkunft: z.herkunft,
wertStichtag: null, artBeschriftung: z.artBeschriftung,
begruendung: z.begruendung, bewertung: z.bewertung,
href: '#', wertCent: z.wertCent === null ? null : BigInt(z.wertCent),
})), wertStichtag: null,
) begruendung: z.begruendung,
href: '#',
const vergleich = freibetragVergleichen(uebersicht.jeBewertung.VERWERTBAR.summeCent) })),
)
return (
<div className="space-y-8"> const vergleich = freibetragVergleichen(uebersicht.jeBewertung.VERWERTBAR.summeCent)
<SeitenKopf
titel="Vermögen bewerten" return (
beschreibung="Jeder Posten bekommt eine Einschätzung und — das ist der wichtige Teil — eine Begründung. Ohne Begründung hilft die Einordnung im Widerspruchsverfahren nicht weiter." <div className="space-y-8">
pfad={[ <SeitenKopf
{ titel: 'Start', href: '/heute' }, titel="Vermögen bewerten"
{ titel: 'Sozialhilfe', href: '/sozialhilfe' }, beschreibung="Jeder Posten bekommt eine Einschätzung und — das ist der wichtige Teil — eine Begründung. Ohne Begründung hilft die Einordnung im Widerspruchsverfahren nicht weiter."
{ titel: 'Vermögen' }, pfad={[
]} { titel: 'Start', href: '/heute' },
/> { titel: 'Sozialhilfe', href: '/sozialhilfe' },
{ titel: 'Vermögen' },
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> ]}
<Summenkachel />
titel={BEWERTUNG_BESCHRIFTUNG.VERWERTBAR}
betrag={uebersicht.jeBewertung.VERWERTBAR.summeCent} <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
anzahl={uebersicht.jeBewertung.VERWERTBAR.anzahl} <Summenkachel
ohneWert={uebersicht.jeBewertung.VERWERTBAR.ohneWert} titel={BEWERTUNG_BESCHRIFTUNG.VERWERTBAR}
farbe="text-ampel-rot" betrag={uebersicht.jeBewertung.VERWERTBAR.summeCent}
/> anzahl={uebersicht.jeBewertung.VERWERTBAR.anzahl}
<Summenkachel ohneWert={uebersicht.jeBewertung.VERWERTBAR.ohneWert}
titel={BEWERTUNG_BESCHRIFTUNG.GESCHUETZT} farbe="text-ampel-rot"
betrag={uebersicht.jeBewertung.GESCHUETZT.summeCent} />
anzahl={uebersicht.jeBewertung.GESCHUETZT.anzahl} <Summenkachel
ohneWert={uebersicht.jeBewertung.GESCHUETZT.ohneWert} titel={BEWERTUNG_BESCHRIFTUNG.GESCHUETZT}
farbe="text-ampel-gruen" betrag={uebersicht.jeBewertung.GESCHUETZT.summeCent}
/> anzahl={uebersicht.jeBewertung.GESCHUETZT.anzahl}
<Summenkachel ohneWert={uebersicht.jeBewertung.GESCHUETZT.ohneWert}
titel={BEWERTUNG_BESCHRIFTUNG.STRITTIG} farbe="text-ampel-gruen"
betrag={uebersicht.jeBewertung.STRITTIG.summeCent} />
anzahl={uebersicht.jeBewertung.STRITTIG.anzahl} <Summenkachel
ohneWert={uebersicht.jeBewertung.STRITTIG.ohneWert} titel={BEWERTUNG_BESCHRIFTUNG.STRITTIG}
farbe="text-ampel-orange" betrag={uebersicht.jeBewertung.STRITTIG.summeCent}
/> anzahl={uebersicht.jeBewertung.STRITTIG.anzahl}
<Summenkachel ohneWert={uebersicht.jeBewertung.STRITTIG.ohneWert}
titel={BEWERTUNG_BESCHRIFTUNG.UNGEPRUEFT} farbe="text-ampel-orange"
betrag={uebersicht.jeBewertung.UNGEPRUEFT.summeCent} />
anzahl={uebersicht.jeBewertung.UNGEPRUEFT.anzahl} <Summenkachel
ohneWert={uebersicht.jeBewertung.UNGEPRUEFT.ohneWert} titel={BEWERTUNG_BESCHRIFTUNG.UNGEPRUEFT}
farbe="text-muted-foreground" betrag={uebersicht.jeBewertung.UNGEPRUEFT.summeCent}
/> anzahl={uebersicht.jeBewertung.UNGEPRUEFT.anzahl}
</div> ohneWert={uebersicht.jeBewertung.UNGEPRUEFT.ohneWert}
farbe="text-muted-foreground"
<Card> />
<CardContent className="space-y-2 pt-5"> </div>
<div className="flex flex-wrap items-baseline justify-between gap-3">
<span className="text-sm text-muted-foreground"> <Card>
Vermögensfreibetrag (§ 90 Abs. 2 Nr. 9 SGB XII) <CardContent className="space-y-2 pt-5">
</span> <div className="flex flex-wrap items-baseline justify-between gap-3">
<span className="text-lg font-semibold tabular-nums"> <span className="text-sm text-muted-foreground">
{formatCent(vergleich.freibetragCent)} Vermögensfreibetrag (§ 90 Abs. 2 Nr. 9 SGB XII)
</span> </span>
</div> <span className="text-lg font-semibold tabular-nums">
<div className="flex flex-wrap items-baseline justify-between gap-3"> {formatCent(vergleich.freibetragCent)}
<span className="text-sm text-muted-foreground">Über dem Freibetrag</span> </span>
<span </div>
className={`text-lg font-semibold tabular-nums ${ <div className="flex flex-wrap items-baseline justify-between gap-3">
vergleich.ausgeschoepft ? 'text-ampel-rot' : 'text-ampel-gruen' <span className="text-sm text-muted-foreground">Über dem Freibetrag</span>
}`} <span
> className={`text-lg font-semibold tabular-nums ${
{formatCent(vergleich.ueberFreibetragCent)} vergleich.ausgeschoepft ? 'text-ampel-rot' : 'text-ampel-gruen'
</span> }`}
</div> >
<p className="text-sm text-muted-foreground">{vergleich.hinweis}</p> {formatCent(vergleich.ueberFreibetragCent)}
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII" kurz /> </span>
</CardContent> </div>
</Card> <p className="text-sm text-muted-foreground">{vergleich.hinweis}</p>
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII" kurz />
{uebersicht.ungeprueft.length > 0 && ( </CardContent>
<Alert variant="warnung"> </Card>
<AlertCircle aria-hidden="true" />
<AlertTitle> {uebersicht.ungeprueft.length > 0 && (
{uebersicht.ungeprueft.length}{' '} <Alert variant="warnung">
{uebersicht.ungeprueft.length === 1 ? 'Posten ist' : 'Posten sind'} noch ungeprüft <AlertCircle aria-hidden="true" />
</AlertTitle> <AlertTitle>
<AlertDescription> {uebersicht.ungeprueft.length}{' '}
Vor dem Antrag sollte hier nichts mehr stehen — der Träger fragt sonst nach, und das {uebersicht.ungeprueft.length === 1 ? 'Posten ist' : 'Posten sind'} noch ungeprüft
kostet Wochen. </AlertTitle>
</AlertDescription> <AlertDescription>
</Alert> Vor dem Antrag sollte hier nichts mehr stehen — der Träger fragt sonst nach, und das
)} kostet Wochen.
</AlertDescription>
{uebersicht.ohneBegruendung.length > 0 && ( </Alert>
<Alert variant="warnung"> )}
<AlertCircle aria-hidden="true" />
<AlertTitle>Bewertung ohne Begründung</AlertTitle> {uebersicht.ohneBegruendung.length > 0 && (
<AlertDescription> <Alert variant="warnung">
{uebersicht.ohneBegruendung.map((p) => p.bezeichnung).join(', ')} — trag nach, worauf <AlertCircle aria-hidden="true" />
sich die Einschätzung stützt. Im Widerspruchsverfahren zählt nur, was begründet ist. <AlertTitle>Bewertung ohne Begründung</AlertTitle>
</AlertDescription> <AlertDescription>
</Alert> {uebersicht.ohneBegruendung.map((p) => p.bezeichnung).join(', ')} — trag nach, worauf
)} sich die Einschätzung stützt. Im Widerspruchsverfahren zählt nur, was begründet ist.
</AlertDescription>
<Vermoegensliste posten={zeilen} darfSchreiben={darfSchreiben} /> </Alert>
)}
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII, § 91 SGB XII" />
</div> <Vermoegensliste posten={zeilen} darfSchreiben={darfSchreiben} />
)
} <Rechtshinweis rechtsgrundlage="§ 90 SGB XII, § 91 SGB XII" />
</div>
function Summenkachel({ )
titel, }
betrag,
anzahl, function Summenkachel({
ohneWert, titel,
farbe, betrag,
}: { anzahl,
titel: string ohneWert,
betrag: bigint farbe,
anzahl: number }: {
ohneWert: number titel: string
farbe: string betrag: bigint
}) { anzahl: number
return ( ohneWert: number
<Card> farbe: string
<CardContent className="pt-5"> }) {
<p className="text-sm text-muted-foreground">{titel}</p> return (
<p className={`text-xl font-semibold tabular-nums ${farbe}`}>{formatCent(betrag)}</p> <Card>
<p className="mt-1 text-xs text-muted-foreground"> <CardContent className="pt-5">
{anzahl} {anzahl === 1 ? 'Posten' : 'Posten'} <p className="text-sm text-muted-foreground">{titel}</p>
{ohneWert > 0 && `, davon ${ohneWert} ohne Wert`} <p className={`text-xl font-semibold tabular-nums ${farbe}`}>{formatCent(betrag)}</p>
</p> <p className="mt-1 text-xs text-muted-foreground">
</CardContent> {anzahl} {anzahl === 1 ? 'Posten' : 'Posten'}
</Card> {ohneWert > 0 && `, davon ${ohneWert} ohne Wert`}
) </p>
} </CardContent>
</Card>
)
}
+310 -326
View File
@@ -1,326 +1,310 @@
'use client' 'use client'
import { useActionState, useState } from 'react' import { useActionState, useState } from 'react'
import { Feld, Feldgruppe } from '@/components/feld' import { Feld, Feldgruppe } from '@/components/feld'
import { Rechtshinweis } from '@/components/rechtshinweis' import { Rechtshinweis } from '@/components/rechtshinweis'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Select } from '@/components/ui/select' import { Select } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Absenden, Meldung } from '@/components/sozialhilfe/bausteine' import { Absenden, Meldung } from '@/components/sozialhilfe/bausteine'
import { MONATSNAMEN } from '@/lib/sozialhilfe/heimkosten' import { MONATSNAMEN } from '@/lib/sozialhilfe/heimkosten'
import { heimkostenSpeichern, type Formularzustand } from '@/server/actions/sozialhilfe' import { heimkostenSpeichern, type Formularzustand } from '@/server/actions/sozialhilfe'
export type HeimkostenWerte = { export type HeimkostenWerte = {
jahr: number jahr: number
monat: number monat: number
pflegeEigenanteil: string pflegeEigenanteil: string
unterkunftVerpflegung: string unterkunftVerpflegung: string
investitionskosten: string investitionskosten: string
ausbildungsumlage: string ausbildungsumlage: string
zusatzleistungen: string zusatzleistungen: string
pflegekasse: string pflegekasse: string
leistungszuschlagProzent: number leistungszuschlagProzent: number
leistungszuschlag: string leistungszuschlag: string
rente: string rente: string
sonstigeEinkuenfte: string sonstigeEinkuenfte: string
sozialhilfe: string sozialhilfe: string
eigenmittel: string eigenmittel: string
barbetrag: string barbetrag: string
bekleidungspauschale: string bekleidungspauschale: string
abwesenheitstage: number abwesenheitstage: number
bemerkung: string bemerkung: string
} }
/** /**
* Ein Monat der Gegenüberstellung. * Ein Monat der Gegenüberstellung.
* *
* Die Reihenfolge folgt der Heimrechnung: erst die vier Bausteine der * Die Reihenfolge folgt der Heimrechnung: erst die vier Bausteine der
* Kosten, dann die Deckungsquellen, dann das, was der betreuten Person * Kosten, dann die Deckungsquellen, dann das, was der betreuten Person
* verbleiben muss. Wer von oben nach unten abtippt, findet alles an der * verbleiben muss. Wer von oben nach unten abtippt, findet alles an der
* Stelle, an der es auf der Rechnung steht. * Stelle, an der es auf der Rechnung steht.
*/ */
export function HeimkostenFormular({ export function HeimkostenFormular({
werte, werte,
jahre, jahre,
vorschlagPflegekasse, hinweisPflegekasse,
vorschlagBarbetrag, vorschlagBarbetrag,
}: { }: {
werte: HeimkostenWerte werte: HeimkostenWerte
jahre: number[] jahre: number[]
/** Pauschale der Pflegekasse zum hinterlegten Pflegegrad, als "1319,00". */ /**
vorschlagPflegekasse: { betrag: string; text: string } | null * Erklaerung zum Feld "Leistung der Pflegekasse". Bewusst KEIN
vorschlagBarbetrag: { betrag: string; text: string } * Uebernehmen-Knopf: Unter der Lesart dieses Moduls ist der
}) { * Eigenanteil bereits um die Pauschale gekuerzt, das Feld gehoert
const [zustand, aktion] = useActionState<Formularzustand, FormData>(heimkostenSpeichern, {}) * deshalb auf 0. Siehe src/lib/sozialhilfe/heimkosten.ts.
const [pflegekasse, setPflegekasse] = useState(werte.pflegekasse) */
const [barbetrag, setBarbetrag] = useState(werte.barbetrag) hinweisPflegekasse: string
vorschlagBarbetrag: { betrag: string; text: string }
return ( }) {
<form action={aktion} className="space-y-6"> const [zustand, aktion] = useActionState<Formularzustand, FormData>(heimkostenSpeichern, {})
<Meldung zustand={zustand} /> const [barbetrag, setBarbetrag] = useState(werte.barbetrag)
<Card> return (
<CardHeader> <form action={aktion} className="space-y-6">
<CardTitle>Monat erfassen</CardTitle> <Meldung zustand={zustand} />
<CardDescription>
Beträge wie auf der Heimrechnung, z.B. 1.234,56. Ein bereits erfasster Monat wird <Card>
überschrieben. <CardHeader>
</CardDescription> <CardTitle>Monat erfassen</CardTitle>
</CardHeader> <CardDescription>
Beträge wie auf der Heimrechnung, z.B. 1.234,56. Ein bereits erfasster Monat wird
<CardContent className="space-y-6"> überschrieben.
<div className="grid gap-4 sm:grid-cols-2"> </CardDescription>
<Feld id="jahr" beschriftung="Jahr" pflicht fehler={zustand.felder?.jahr}> </CardHeader>
<Select name="jahr" defaultValue={String(werte.jahr)}>
{jahre.map((j) => ( <CardContent className="space-y-6">
<option key={j} value={j}> <div className="grid gap-4 sm:grid-cols-2">
{j} <Feld id="jahr" beschriftung="Jahr" pflicht fehler={zustand.felder?.jahr}>
</option> <Select name="jahr" defaultValue={String(werte.jahr)}>
))} {jahre.map((j) => (
</Select> <option key={j} value={j}>
</Feld> {j}
</option>
<Feld id="monat" beschriftung="Monat" pflicht fehler={zustand.felder?.monat}> ))}
<Select name="monat" defaultValue={String(werte.monat)}> </Select>
{MONATSNAMEN.map((name, i) => ( </Feld>
<option key={name} value={i + 1}>
{name} <Feld id="monat" beschriftung="Monat" pflicht fehler={zustand.felder?.monat}>
</option> <Select name="monat" defaultValue={String(werte.monat)}>
))} {MONATSNAMEN.map((name, i) => (
</Select> <option key={name} value={i + 1}>
</Feld> {name}
</div> </option>
))}
<Feldgruppe </Select>
titel="Kosten" </Feld>
beschreibung="Die vier Bausteine der Heimrechnung, dazu Zusatzleistungen." </div>
>
<Feld <Feldgruppe
id="pflegeEigenanteil" titel="Kosten"
beschriftung="Einrichtungseinheitlicher Eigenanteil" beschreibung="Die vier Bausteine der Heimrechnung, dazu Zusatzleistungen."
fehler={zustand.felder?.pflegeEigenanteil} >
erklaerung="Der pflegebedingte Anteil (EEE). In jedem Pflegegrad gleich hoch. Nur auf ihn wirkt der Leistungszuschlag." <Feld
> id="pflegeEigenanteil"
<Input beschriftung="Einrichtungseinheitlicher Eigenanteil"
name="pflegeEigenanteil" fehler={zustand.felder?.pflegeEigenanteil}
inputMode="decimal" erklaerung="Der pflegebedingte Anteil (EEE), genau wie er auf der Heimrechnung steht — also bereits abzüglich der Pauschale der Pflegekasse (§ 43 Abs. 2 SGB XI). In jedem Pflegegrad gleich hoch. Nur auf ihn wirkt der Leistungszuschlag."
defaultValue={werte.pflegeEigenanteil} >
/> <Input
</Feld> name="pflegeEigenanteil"
inputMode="decimal"
<Feld defaultValue={werte.pflegeEigenanteil}
id="unterkunftVerpflegung" />
beschriftung="Unterkunft und Verpflegung" </Feld>
fehler={zustand.felder?.unterkunftVerpflegung}
> <Feld
<Input id="unterkunftVerpflegung"
name="unterkunftVerpflegung" beschriftung="Unterkunft und Verpflegung"
inputMode="decimal" fehler={zustand.felder?.unterkunftVerpflegung}
defaultValue={werte.unterkunftVerpflegung} >
/> <Input
</Feld> name="unterkunftVerpflegung"
inputMode="decimal"
<Feld defaultValue={werte.unterkunftVerpflegung}
id="investitionskosten" />
beschriftung="Investitionskosten" </Feld>
fehler={zustand.felder?.investitionskosten}
erklaerung="Gebäude und Instandhaltung. Laufen auch bei Abwesenheit weiter." <Feld
> id="investitionskosten"
<Input beschriftung="Investitionskosten"
name="investitionskosten" fehler={zustand.felder?.investitionskosten}
inputMode="decimal" erklaerung="Gebäude und Instandhaltung. Laufen auch bei Abwesenheit weiter."
defaultValue={werte.investitionskosten} >
/> <Input
</Feld> name="investitionskosten"
inputMode="decimal"
<Feld defaultValue={werte.investitionskosten}
id="ausbildungsumlage" />
beschriftung="Ausbildungsumlage" </Feld>
fehler={zustand.felder?.ausbildungsumlage}
> <Feld
<Input id="ausbildungsumlage"
name="ausbildungsumlage" beschriftung="Ausbildungsumlage"
inputMode="decimal" fehler={zustand.felder?.ausbildungsumlage}
defaultValue={werte.ausbildungsumlage} >
/> <Input
</Feld> name="ausbildungsumlage"
inputMode="decimal"
<Feld defaultValue={werte.ausbildungsumlage}
id="zusatzleistungen" />
beschriftung="Zusatzleistungen" </Feld>
fehler={zustand.felder?.zusatzleistungen}
erklaerung="Friseur, Komfortleistungen. Der Träger übernimmt sie in aller Regel nicht — sie gehen vom Barbetrag ab." <Feld
> id="zusatzleistungen"
<Input beschriftung="Zusatzleistungen"
name="zusatzleistungen" fehler={zustand.felder?.zusatzleistungen}
inputMode="decimal" erklaerung="Friseur, Komfortleistungen. Der Träger übernimmt sie in aller Regel nicht — sie gehen vom Barbetrag ab."
defaultValue={werte.zusatzleistungen} >
/> <Input
</Feld> name="zusatzleistungen"
</Feldgruppe> inputMode="decimal"
defaultValue={werte.zusatzleistungen}
<Feldgruppe titel="Deckung" beschreibung="Wer zahlt welchen Teil?"> />
<Feld </Feld>
id="pflegekasse" </Feldgruppe>
beschriftung="Leistung der Pflegekasse"
fehler={zustand.felder?.pflegekasse} <Feldgruppe titel="Deckung" beschreibung="Wer zahlt welchen Teil?">
erklaerung={ <Feld
vorschlagPflegekasse ? ( id="pflegekasse"
<> beschriftung="Leistung der Pflegekasse (nachrichtlich)"
{vorschlagPflegekasse.text}{' '} fehler={zustand.felder?.pflegekasse}
<button erklaerung={hinweisPflegekasse}
type="button" >
onClick={() => setPflegekasse(vorschlagPflegekasse.betrag)} <Input name="pflegekasse" inputMode="decimal" defaultValue={werte.pflegekasse} />
className="font-medium text-primary underline-offset-4 hover:underline" </Feld>
>
Wert übernehmen <Feld
</button> id="leistungszuschlagProzent"
</> beschriftung="Leistungszuschlag in Prozent"
) : ( fehler={zustand.felder?.leistungszuschlagProzent}
'Pauschale nach § 43 SGB XI. Hinterlege den Pflegegrad in den Stammdaten, dann steht hier ein Vorschlag.' erklaerung="§ 43c SGB XI: 15 / 30 / 50 / 75 Prozent nach Verweildauer. Steht auf der Heimrechnung."
) >
} <Input
> name="leistungszuschlagProzent"
<Input type="number"
name="pflegekasse" min={0}
inputMode="decimal" max={100}
value={pflegekasse} defaultValue={werte.leistungszuschlagProzent}
onChange={(e) => setPflegekasse(e.target.value)} />
/> </Feld>
</Feld>
<Feld
<Feld id="leistungszuschlag"
id="leistungszuschlagProzent" beschriftung="Leistungszuschlag in Euro"
beschriftung="Leistungszuschlag in Prozent" fehler={zustand.felder?.leistungszuschlag}
fehler={zustand.felder?.leistungszuschlagProzent} erklaerung="Nur auf den pflegebedingten Eigenanteil, nicht auf die ganze Rechnung."
erklaerung="§ 43c SGB XI: 15 / 30 / 50 / 75 Prozent nach Verweildauer. Steht auf der Heimrechnung." >
> <Input
<Input name="leistungszuschlag"
name="leistungszuschlagProzent" inputMode="decimal"
type="number" defaultValue={werte.leistungszuschlag}
min={0} />
max={100} </Feld>
defaultValue={werte.leistungszuschlagProzent}
/> <Feld id="rente" beschriftung="Rente" fehler={zustand.felder?.rente}>
</Feld> <Input name="rente" inputMode="decimal" defaultValue={werte.rente} />
</Feld>
<Feld
id="leistungszuschlag" <Feld
beschriftung="Leistungszuschlag in Euro" id="sonstigeEinkuenfte"
fehler={zustand.felder?.leistungszuschlag} beschriftung="Sonstige Einkünfte"
erklaerung="Nur auf den pflegebedingten Eigenanteil, nicht auf die ganze Rechnung." fehler={zustand.felder?.sonstigeEinkuenfte}
> erklaerung="Betriebsrente, Mieteinnahmen, Zinsen."
<Input >
name="leistungszuschlag" <Input
inputMode="decimal" name="sonstigeEinkuenfte"
defaultValue={werte.leistungszuschlag} inputMode="decimal"
/> defaultValue={werte.sonstigeEinkuenfte}
</Feld> />
</Feld>
<Feld id="rente" beschriftung="Rente" fehler={zustand.felder?.rente}>
<Input name="rente" inputMode="decimal" defaultValue={werte.rente} /> <Feld
</Feld> id="sozialhilfe"
beschriftung="Anteil der Sozialhilfe"
<Feld fehler={zustand.felder?.sozialhilfe}
id="sonstigeEinkuenfte" erklaerung="Was der Bezirk laut Bescheid übernimmt."
beschriftung="Sonstige Einkünfte" >
fehler={zustand.felder?.sonstigeEinkuenfte} <Input name="sozialhilfe" inputMode="decimal" defaultValue={werte.sozialhilfe} />
erklaerung="Betriebsrente, Mieteinnahmen, Zinsen." </Feld>
>
<Input <Feld
name="sonstigeEinkuenfte" id="eigenmittel"
inputMode="decimal" beschriftung="Eigenmittel"
defaultValue={werte.sonstigeEinkuenfte} fehler={zustand.felder?.eigenmittel}
/> erklaerung="Was aus dem eigenen Vermögen zugeschossen wurde."
</Feld> >
<Input name="eigenmittel" inputMode="decimal" defaultValue={werte.eigenmittel} />
<Feld </Feld>
id="sozialhilfe" </Feldgruppe>
beschriftung="Anteil der Sozialhilfe"
fehler={zustand.felder?.sozialhilfe} <Feldgruppe
erklaerung="Was der Bezirk laut Bescheid übernimmt." titel="Was verbleiben muss"
> beschreibung="Barbetrag und Bekleidungspauschale sind Teil des Bedarfs, nicht Rest."
<Input name="sozialhilfe" inputMode="decimal" defaultValue={werte.sozialhilfe} /> >
</Feld> <Feld
id="barbetrag"
<Feld beschriftung="Barbetrag"
id="eigenmittel" fehler={zustand.felder?.barbetrag}
beschriftung="Eigenmittel" erklaerung={
fehler={zustand.felder?.eigenmittel} <>
erklaerung="Was aus dem eigenen Vermögen zugeschossen wurde." {vorschlagBarbetrag.text}{' '}
> <button
<Input name="eigenmittel" inputMode="decimal" defaultValue={werte.eigenmittel} /> type="button"
</Feld> onClick={() => setBarbetrag(vorschlagBarbetrag.betrag)}
</Feldgruppe> className="font-medium text-primary underline-offset-4 hover:underline"
>
<Feldgruppe Wert übernehmen
titel="Was verbleiben muss" </button>
beschreibung="Barbetrag und Bekleidungspauschale sind Teil des Bedarfs, nicht Rest." </>
> }
<Feld >
id="barbetrag" <Input
beschriftung="Barbetrag" name="barbetrag"
fehler={zustand.felder?.barbetrag} inputMode="decimal"
erklaerung={ value={barbetrag}
<> onChange={(e) => setBarbetrag(e.target.value)}
{vorschlagBarbetrag.text}{' '} />
<button </Feld>
type="button"
onClick={() => setBarbetrag(vorschlagBarbetrag.betrag)} <Feld
className="font-medium text-primary underline-offset-4 hover:underline" id="bekleidungspauschale"
> beschriftung="Bekleidungspauschale"
Wert übernehmen fehler={zustand.felder?.bekleidungspauschale}
</button> erklaerung="Höhe legt der Träger fest (§ 27b Abs. 1 SGB XII) — übernimm den Betrag aus dem Bescheid."
</> >
} <Input
> name="bekleidungspauschale"
<Input inputMode="decimal"
name="barbetrag" defaultValue={werte.bekleidungspauschale}
inputMode="decimal" />
value={barbetrag} </Feld>
onChange={(e) => setBarbetrag(e.target.value)}
/> <Feld
</Feld> id="abwesenheitstage"
beschriftung="Abwesenheitstage"
<Feld fehler={zustand.felder?.abwesenheitstage}
id="bekleidungspauschale" erklaerung="Krankenhaus, Reha, Urlaub. Die ersten drei Tage sind voll zu zahlen; ab dem vierten mindert sich das Entgelt nach dem Rahmenvertrag — die Anwendung schätzt das nur, maßgeblich ist die Heimrechnung."
beschriftung="Bekleidungspauschale" >
fehler={zustand.felder?.bekleidungspauschale} <Input
erklaerung="Höhe legt der Träger fest (§ 27b Abs. 1 SGB XII) — übernimm den Betrag aus dem Bescheid." name="abwesenheitstage"
> type="number"
<Input min={0}
name="bekleidungspauschale" max={31}
inputMode="decimal" defaultValue={werte.abwesenheitstage}
defaultValue={werte.bekleidungspauschale} />
/> </Feld>
</Feld> </Feldgruppe>
<Feld <Feld id="bemerkung" beschriftung="Bemerkung" fehler={zustand.felder?.bemerkung}>
id="abwesenheitstage" <Textarea name="bemerkung" defaultValue={werte.bemerkung} rows={2} />
beschriftung="Abwesenheitstage" </Feld>
fehler={zustand.felder?.abwesenheitstage} </CardContent>
erklaerung="Krankenhaus, Reha, Urlaub. Die ersten drei Tage sind voll zu zahlen; ab dem vierten mindert sich das Entgelt nach dem Rahmenvertrag — die Anwendung schätzt das nur, maßgeblich ist die Heimrechnung." </Card>
>
<Input <Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" />
name="abwesenheitstage"
type="number" <Absenden>Monat speichern</Absenden>
min={0} </form>
max={31} )
defaultValue={werte.abwesenheitstage} }
/>
</Feld>
</Feldgruppe>
<Feld id="bemerkung" beschriftung="Bemerkung" fehler={zustand.felder?.bemerkung}>
<Textarea name="bemerkung" defaultValue={werte.bemerkung} rows={2} />
</Feld>
</CardContent>
</Card>
<Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" />
<Absenden>Monat speichern</Absenden>
</form>
)
}
-61
View File
@@ -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
-67
View File
@@ -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')}`
}
+117 -56
View File
@@ -1,8 +1,13 @@
import {
LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE,
LEISTUNGSZUSCHLAG_STAFFEL,
LEISTUNGSZUSCHLAG_STAND,
} from '@/lib/pflege/konstanten'
import { anteilCent, stufeFuerMonate, verweildauerMonate } from '@/lib/pflege/leistungszuschlag'
import { import {
ABWESENHEIT_KARENZTAGE, ABWESENHEIT_KARENZTAGE,
ABWESENHEIT_MINDERUNG_PROZENT, ABWESENHEIT_MINDERUNG_PROZENT,
ABWESENHEIT_PLATZFREIHALTUNG_TAGE, ABWESENHEIT_PLATZFREIHALTUNG_TAGE,
LEISTUNGSZUSCHLAG_STAFFEL,
PFLEGEKASSE_RECHTSGRUNDLAGE, PFLEGEKASSE_RECHTSGRUNDLAGE,
PFLEGEKASSE_VOLLSTATIONAER_CENT, PFLEGEKASSE_VOLLSTATIONAER_CENT,
type Pflegegrad, type Pflegegrad,
@@ -12,78 +17,106 @@ import {
* Heimkosten-Gegenüberstellung: Was kostet der Monat, wer zahlt was, und * Heimkosten-Gegenüberstellung: Was kostet der Monat, wer zahlt was, und
* was bleibt offen? * 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 * Eine Heimrechnung rechnet in dieser Reihenfolge:
* 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.
* *
* Dazu kommen Zusatzleistungen (Friseur, besondere Komfortleistungen), die * Pflegesatz (pflegebedingter Aufwand, brutto)
* der Träger in aller Regel nicht übernimmt. * − 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. * 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 // 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.
/** export { LEISTUNGSZUSCHLAG_STAFFEL, LEISTUNGSZUSCHLAG_STAND, LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE }
* Prozentsatz des Leistungszuschlags nach der Dauer der export { verweildauerMonate }
* Leistungsinanspruchnahme.
* /** Prozentsatz nach vollendeten Monaten in der Einrichtung. */
* 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 function leistungszuschlagProzent(vollendeteMonate: number): number { export function leistungszuschlagProzent(vollendeteMonate: number): number {
const monate = Math.max(0, Math.floor(vollendeteMonate)) return stufeFuerMonate(Math.max(0, Math.floor(vollendeteMonate))).prozent
let prozent = LEISTUNGSZUSCHLAG_STAFFEL[0]!.prozent
for (const stufe of LEISTUNGSZUSCHLAG_STAFFEL) {
if (monate >= stufe.abVollendetenMonaten) prozent = stufe.prozent
}
return prozent
} }
/** Beschriftung der Stufe, für die Anzeige neben der Zahl. */ /** Beschriftung der Stufe, für die Anzeige neben der Zahl. */
export function leistungszuschlagStufe(vollendeteMonate: number): string { export function leistungszuschlagStufe(vollendeteMonate: number): string {
const prozent = leistungszuschlagProzent(vollendeteMonate) return stufeFuerMonate(Math.max(0, Math.floor(vollendeteMonate))).bezeichnung
return LEISTUNGSZUSCHLAG_STAFFEL.find((s) => s.prozent === prozent)?.beschriftung ?? ''
} }
/** /**
* Der Zuschlag in Cent - immer nur auf den pflegebedingten Eigenanteil. * Der Zuschlag in Cent — immer nur auf den pflegebedingten Eigenanteil,
* * nie auf Unterkunft, Investitionskosten oder Ausbildungsumlage.
* 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.
*/ */
export function leistungszuschlagCent( export function leistungszuschlagCent(
pflegeEigenanteilCent: bigint, pflegeEigenanteilCent: bigint,
vollendeteMonate: number, vollendeteMonate: number,
): bigint { ): bigint {
const prozent = BigInt(leistungszuschlagProzent(vollendeteMonate)) return anteilCent(pflegeEigenanteilCent, 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)
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -129,7 +162,7 @@ export type Abwesenheitsschaetzung = {
* *
* Gerundet wird abwärts (BigInt-Division schneidet ab). Die geschätzte * Gerundet wird abwärts (BigInt-Division schneidet ab). Die geschätzte
* Minderung fällt damit im Zweifel einen Cent zu klein und die * 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: { export function abwesenheitSchaetzen(eingabe: {
abwesenheitstage: number abwesenheitstage: number
@@ -204,6 +237,7 @@ export type HeimkostenEingabe = {
export type HeimkostenErgebnis = { export type HeimkostenErgebnis = {
jahr: number jahr: number
monat: number monat: number
lesart: Kostenlesart
heimkostenCent: bigint heimkostenCent: bigint
/** Heimkosten plus Barbetrag und Bekleidungspauschale. */ /** Heimkosten plus Barbetrag und Bekleidungspauschale. */
@@ -211,9 +245,16 @@ export type HeimkostenErgebnis = {
deckungCent: bigint deckungCent: bigint
verbleibendCent: 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 * Positiv = ungedeckt, negativ = Überdeckung. Für die Anzeige stehen
* daneben deckungsluecke Cent und ueberdeckungCent, jeweils >= 0. * daneben deckungslueckeCent und ueberdeckungCent, jeweils >= 0.
*/ */
saldoCent: bigint saldoCent: bigint
deckungslueckeCent: bigint deckungslueckeCent: bigint
@@ -232,11 +273,16 @@ export type HeimkostenErgebnis = {
* *
* Bedarfsbegriff: Zum Bedarf zählen nicht nur die Heimkosten, sondern auch * Bedarfsbegriff: Zum Bedarf zählen nicht nur die Heimkosten, sondern auch
* der Barbetrag nach § 27b Abs. 2 SGB XII und die Bekleidungspauschale. * 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 * Rechnung herauslässt, hält die Deckung für besser, als sie ist, und
* plündert am Monatsende das Taschengeld. * 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 = const heimkostenCent =
eingabe.pflegeEigenanteilCent + eingabe.pflegeEigenanteilCent +
eingabe.unterkunftVerpflegungCent + eingabe.unterkunftVerpflegungCent +
@@ -247,8 +293,12 @@ export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgeb
const verbleibendCent = eingabe.barbetragCent + eingabe.bekleidungspauschaleCent const verbleibendCent = eingabe.barbetragCent + eingabe.bekleidungspauschaleCent
const gesamtbedarfCent = heimkostenCent + verbleibendCent const gesamtbedarfCent = heimkostenCent + verbleibendCent
const nettoLesart = lesart === 'EIGENANTEIL_NETTO'
const pflegekasseAlsDeckungCent = nettoLesart ? 0n : eingabe.pflegekasseCent
const pflegekasseNachrichtlichCent = nettoLesart ? eingabe.pflegekasseCent : 0n
const deckungCent = const deckungCent =
eingabe.pflegekasseCent + pflegekasseAlsDeckungCent +
eingabe.leistungszuschlagCent + eingabe.leistungszuschlagCent +
eingabe.renteCent + eingabe.renteCent +
eingabe.sonstigeEinkuenfteCent + eingabe.sonstigeEinkuenfteCent +
@@ -267,6 +317,15 @@ export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgeb
const nachAbwesenheit = saldoCent - abwesenheit.geschaetzteMinderungCent const nachAbwesenheit = saldoCent - abwesenheit.geschaetzteMinderungCent
const hinweise: string[] = [] 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) { if (eingabe.zusatzleistungenCent > 0n) {
hinweise.push( hinweise.push(
'Zusatzleistungen (Friseur, Komfortleistungen) übernimmt der Träger in aller Regel ' + 'Zusatzleistungen (Friseur, Komfortleistungen) übernimmt der Träger in aller Regel ' +
@@ -297,10 +356,12 @@ export function heimkostenAuswerten(eingabe: HeimkostenEingabe): HeimkostenErgeb
return { return {
jahr: eingabe.jahr, jahr: eingabe.jahr,
monat: eingabe.monat, monat: eingabe.monat,
lesart,
heimkostenCent, heimkostenCent,
gesamtbedarfCent, gesamtbedarfCent,
deckungCent, deckungCent,
verbleibendCent, verbleibendCent,
pflegekasseNachrichtlichCent,
saldoCent, saldoCent,
deckungslueckeCent: saldoCent > 0n ? saldoCent : 0n, deckungslueckeCent: saldoCent > 0n ? saldoCent : 0n,
ueberdeckungCent: saldoCent < 0n ? -saldoCent : 0n, ueberdeckungCent: saldoCent < 0n ? -saldoCent : 0n,
@@ -317,7 +378,7 @@ export type Jahresverlauf = {
summeBedarfCent: bigint summeBedarfCent: bigint
summeDeckungCent: bigint summeDeckungCent: bigint
summeDeckungslueckeCent: 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 groessteLuecke: HeimkostenErgebnis | null
} }
+237 -256
View File
@@ -1,256 +1,237 @@
/** /**
* Beträge, Staffeln und Grenzwerte des Sozialhilfeverfahrens. * Beträge, Staffeln und Grenzwerte des Sozialhilfeverfahrens.
* *
* ACHTUNG - EINZIGE STELLE. * ACHTUNG - EINZIGE STELLE.
* Alles, was sich jährlich (oder mit dem nächsten Gesetzespaket) ändert, * 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 * steht hier und nirgendwo sonst. Jede Konstante trägt ihr Stand-Datum und
* die Fundstelle. Wer einen Wert im Fachcode findet, hat einen Fehler * die Fundstelle. Wer einen Wert im Fachcode findet, hat einen Fehler
* gefunden. * gefunden.
* *
* Und der wichtigste Satz vorweg: Diese Zahlen sind Arbeitsgrundlage für * Und der wichtigste Satz vorweg: Diese Zahlen sind Arbeitsgrundlage für
* die eigene Vorbereitung, nicht die Rechtslage. Maßgeblich ist immer der * die eigene Vorbereitung, nicht die Rechtslage. Maßgeblich ist immer der
* Bescheid des Trägers. Vor jedem Antrag und zu jedem Jahreswechsel * Bescheid des Trägers. Vor jedem Antrag und zu jedem Jahreswechsel
* gehören die Werte hier nachgeprüft. * gehören die Werte hier nachgeprüft.
*/ */
/** Wann wurden die Werte in dieser Datei zuletzt geprüft? */ /** Wann wurden die Werte in dieser Datei zuletzt geprüft? */
export const KONSTANTEN_STAND = '2025-01-01' export const KONSTANTEN_STAND = '2025-01-01'
/** /**
* Nächster Termin, zu dem die Werte hier durchzusehen sind. Die Oberfläche * Nächster Termin, zu dem die Werte hier durchzusehen sind. Die Oberfläche
* blendet ab diesem Datum einen Prüfhinweis ein. * blendet ab diesem Datum einen Prüfhinweis ein.
*/ */
export const KONSTANTEN_NAECHSTE_PRUEFUNG = '2027-01-01' export const KONSTANTEN_NAECHSTE_PRUEFUNG = '2027-01-01'
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Regelbedarf - Bezugsgröße für Barbetrag und Zuzahlungsbefreiung // Regelbedarf - Bezugsgröße für Barbetrag und Zuzahlungsbefreiung
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/** /**
* Regelbedarfsstufe 1 nach der Regelbedarfsstufen-Fortschreibungsverordnung. * Regelbedarfsstufe 1 nach der Regelbedarfsstufen-Fortschreibungsverordnung.
* Stand 01.01.2025: 563,00 EUR monatlich (unverändert gegenüber 2024, die * Stand 01.01.2025: 563,00 EUR monatlich (unverändert gegenüber 2024, die
* Fortschreibung ergab rechnerisch keine Erhöhung). * Fortschreibung ergab rechnerisch keine Erhöhung).
* *
* Aus diesem Wert leiten sich unten der Barbetrag und die Belastungsgrenze * Aus diesem Wert leiten sich unten der Barbetrag und die Belastungsgrenze
* der Zuzahlungsbefreiung ab - deshalb steht er hier oben und wird nicht * der Zuzahlungsbefreiung ab - deshalb steht er hier oben und wird nicht
* doppelt gepflegt. * doppelt gepflegt.
*/ */
export const REGELBEDARFSSTUFE_1_CENT = 56_300n export const REGELBEDARFSSTUFE_1_CENT = 56_300n
/** /**
* Barbetrag zur persönlichen Verfügung ("Taschengeld") für Menschen in * Barbetrag zur persönlichen Verfügung ("Taschengeld") für Menschen in
* Einrichtungen, § 27b Abs. 2 SGB XII: mindestens 27 % der * Einrichtungen, § 27b Abs. 2 SGB XII: mindestens 27 % der
* Regelbedarfsstufe 1. * Regelbedarfsstufe 1.
* *
* 27 % von 563,00 EUR = 152,01 EUR monatlich (Stand 01.01.2025). * 27 % von 563,00 EUR = 152,01 EUR monatlich (Stand 01.01.2025).
* *
* "Mindestens": Der Träger darf mehr festsetzen, und einzelne Bezirke tun * "Mindestens": Der Träger darf mehr festsetzen, und einzelne Bezirke tun
* das auch. Steht im Bescheid ein höherer Betrag, gilt der Bescheid. * das auch. Steht im Bescheid ein höherer Betrag, gilt der Bescheid.
*/ */
export const BARBETRAG_ANTEIL_PROZENT = 27 export const BARBETRAG_ANTEIL_PROZENT = 27
export const BARBETRAG_CENT = (REGELBEDARFSSTUFE_1_CENT * BigInt(BARBETRAG_ANTEIL_PROZENT)) / 100n export const BARBETRAG_CENT = (REGELBEDARFSSTUFE_1_CENT * BigInt(BARBETRAG_ANTEIL_PROZENT)) / 100n
/** /**
* Bekleidungspauschale. * Bekleidungspauschale.
* *
* Bewusst ohne Zahl. Es gibt keinen bundeseinheitlichen Betrag - die * Bewusst ohne Zahl. Es gibt keinen bundeseinheitlichen Betrag - die
* Bekleidungsbeihilfe nach § 27b Abs. 1 SGB XII setzt der jeweilige Träger * Bekleidungsbeihilfe nach § 27b Abs. 1 SGB XII setzt der jeweilige Träger
* fest, der Bezirk Oberfranken zahlt sie üblicherweise als Pauschale je * fest, der Bezirk Oberfranken zahlt sie üblicherweise als Pauschale je
* Quartal oder Jahr. Der Betrag ist dem Bewilligungsbescheid zu entnehmen * Quartal oder Jahr. Der Betrag ist dem Bewilligungsbescheid zu entnehmen
* und im Heimkostenmonat einzutragen. Eine erfundene Zahl wäre hier * und im Heimkostenmonat einzutragen. Eine erfundene Zahl wäre hier
* schädlicher als gar keine. * schädlicher als gar keine.
*/ */
export const BEKLEIDUNGSPAUSCHALE_CENT: bigint | null = null export const BEKLEIDUNGSPAUSCHALE_CENT: bigint | null = null
export const BEKLEIDUNGSPAUSCHALE_HINWEIS = export const BEKLEIDUNGSPAUSCHALE_HINWEIS =
'Die Höhe der Bekleidungspauschale legt der Träger fest (§ 27b Abs. 1 SGB XII). ' + '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.' 'Übernimm den Betrag aus dem Bewilligungsbescheid, er ist regional unterschiedlich.'
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Leistungen der Pflegekasse, § 43 SGB XI // Leistungen der Pflegekasse, § 43 SGB XI
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export type Pflegegrad = 1 | 2 | 3 | 4 | 5 export type Pflegegrad = 1 | 2 | 3 | 4 | 5
/** /**
* Monatliche Pauschalen der Pflegekasse bei vollstationärer Pflege, * Monatliche Pauschalen der Pflegekasse bei vollstationärer Pflege,
* § 43 Abs. 2 SGB XI. Stand 01.01.2025 (Erhöhung um 4,5 % durch das * § 43 Abs. 2 SGB XI. Stand 01.01.2025 (Erhöhung um 4,5 % durch das
* Pflegeunterstützungs- und -entlastungsgesetz). * Pflegeunterstützungs- und -entlastungsgesetz).
* *
* Pflegegrad 1 ist der Sonderfall: § 43 Abs. 2 SGB XI greift dort nicht. * Pflegegrad 1 ist der Sonderfall: § 43 Abs. 2 SGB XI greift dort nicht.
* Wer mit Pflegegrad 1 vollstationär gepflegt wird, erhält nach * Wer mit Pflegegrad 1 vollstationär gepflegt wird, erhält nach
* § 43 Abs. 3 SGB XI einen Zuschuss von 125 EUR monatlich - deshalb steht * § 43 Abs. 3 SGB XI einen Zuschuss von 125 EUR monatlich - deshalb steht
* er hier mit ebendiesem Betrag, aber unter eigener Rechtsgrundlage. * er hier mit ebendiesem Betrag, aber unter eigener Rechtsgrundlage.
* *
* Die nächste turnusmäßige Anpassung nach § 30 SGB XI ist gesetzlich für * Die nächste turnusmäßige Anpassung nach § 30 SGB XI ist gesetzlich für
* 2028 vorgesehen. Trotzdem: zum Jahreswechsel nachsehen. * 2028 vorgesehen. Trotzdem: zum Jahreswechsel nachsehen.
*/ */
export const PFLEGEKASSE_VOLLSTATIONAER_CENT: Record<Pflegegrad, bigint> = { export const PFLEGEKASSE_VOLLSTATIONAER_CENT: Record<Pflegegrad, bigint> = {
1: 12_500n, // § 43 Abs. 3 SGB XI - Zuschuss, keine Pauschale 1: 12_500n, // § 43 Abs. 3 SGB XI - Zuschuss, keine Pauschale
2: 80_500n, 2: 80_500n,
3: 131_900n, 3: 131_900n,
4: 185_500n, 4: 185_500n,
5: 209_600n, 5: 209_600n,
} }
export const PFLEGEKASSE_RECHTSGRUNDLAGE: Record<Pflegegrad, string> = { export const PFLEGEKASSE_RECHTSGRUNDLAGE: Record<Pflegegrad, string> = {
1: '§ 43 Abs. 3 SGB XI', 1: '§ 43 Abs. 3 SGB XI',
2: '§ 43 Abs. 2 SGB XI', 2: '§ 43 Abs. 2 SGB XI',
3: '§ 43 Abs. 2 SGB XI', 3: '§ 43 Abs. 2 SGB XI',
4: '§ 43 Abs. 2 SGB XI', 4: '§ 43 Abs. 2 SGB XI',
5: '§ 43 Abs. 2 SGB XI', 5: '§ 43 Abs. 2 SGB XI',
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Leistungszuschlag, § 43c SGB XI // Leistungszuschlag, § 43c SGB XI
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
//
/** // STEHT NICHT HIER. Die Staffel (15 / 30 / 50 / 75 Prozent nach
* Der Leistungszuschlag mindert den *pflegebedingten* Eigenanteil // Verweildauer, Stand 01.01.2024) gehoert dem Pflegemodul und liegt in
* (einrichtungseinheitlicher Eigenanteil, EEE) - nicht Unterkunft und // src/lib/pflege/konstanten.ts. Dieses Modul benutzt sie ueber
* Verpflegung, nicht die Investitionskosten und nicht die // src/lib/sozialhilfe/heimkosten.ts, kopiert sie aber nicht - zwei Kopien
* Ausbildungsumlage. Wer ihn auf die ganze Heimrechnung anwendet, rechnet // derselben Prozentsaetze werden bei der naechsten Gesetzesaenderung
* die Deckungslücke zu klein. // zuverlaessig nur an einer Stelle nachgezogen.
* //
* Die Staffel richtet sich nach der Dauer der Leistungsinanspruchnahme in // Merksatz zur Anwendung: Der Zuschlag mindert ausschliesslich den
* vollstationärer Pflege. Stand 01.01.2024 (angehoben durch das PUEG, // pflegebedingten Eigenanteil (EEE) - nicht Unterkunft und Verpflegung,
* vorher 5/25/45/70 %). // nicht die Investitionskosten, nicht die Ausbildungsumlage. Wer ihn auf
* // die ganze Heimrechnung anwendet, rechnet die Deckungsluecke zu klein.
* 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 %. // Abwesenheit (Krankenhaus, Reha, Urlaub)
*/ // -----------------------------------------------------------------------------
export type Leistungszuschlagstufe = {
/** Ab wie vielen vollendeten Monaten im Heim gilt diese Stufe? */ /**
abVollendetenMonaten: number * Abwesenheitstage mindern die Heimrechnung nicht ab dem ersten Tag.
prozent: number *
beschriftung: string * 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
export const LEISTUNGSZUSCHLAG_STAFFEL: readonly Leistungszuschlagstufe[] = [ * üblicherweise um 25 % von Pflegesatz sowie Unterkunft und Verpflegung.
{ abVollendetenMonaten: 0, prozent: 15, beschriftung: 'bis 12 Monate' }, * Die Investitionskosten laufen weiter, der Platz wird ja freigehalten
{ abVollendetenMonaten: 12, prozent: 30, beschriftung: 'mehr als 12 Monate' }, * (§ 87a Abs. 1 SGB XI, Platzfreihaltung bis 42 Tage im Kalenderjahr).
{ abVollendetenMonaten: 24, prozent: 50, beschriftung: 'mehr als 24 Monate' }, *
{ abVollendetenMonaten: 36, prozent: 75, beschriftung: 'mehr als 36 Monate' }, * Weil die Prozentsätze im Rahmenvertrag und im Heimvertrag stehen und
] as const * nicht im Gesetz, rechnet die Anwendung damit nur eine *Schätzung*. Was
* gilt, steht in der Heimrechnung.
export const LEISTUNGSZUSCHLAG_STAND = '2024-01-01' */
export const ABWESENHEIT_KARENZTAGE = 3
// ----------------------------------------------------------------------------- export const ABWESENHEIT_MINDERUNG_PROZENT = 25
// Abwesenheit (Krankenhaus, Reha, Urlaub) export const ABWESENHEIT_PLATZFREIHALTUNG_TAGE = 42
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/** // Vermögen, § 90 SGB XII
* 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 * Vermögensfreibetrag ("kleinere Barbeträge"), § 90 Abs. 2 Nr. 9 SGB XII
* zusammenhängenden Tag wird das Entgelt gemindert - in Bayern * i.V.m. § 1 Abs. 1 Nr. 1 lit. b der Verordnung zur Durchführung des
* üblicherweise um 25 % von Pflegesatz sowie Unterkunft und Verpflegung. * § 90 Abs. 2 Nr. 9 SGB XII: 10.000 EUR.
* Die Investitionskosten laufen weiter, der Platz wird ja freigehalten *
* (§ 87a Abs. 1 SGB XI, Platzfreihaltung bis 42 Tage im Kalenderjahr). * Stand 01.01.2023 (bis dahin 5.000 EUR). Für jede Person, die von der
* * leistungsberechtigten Person überwiegend unterhalten wird, kommen
* Weil die Prozentsätze im Rahmenvertrag und im Heimvertrag stehen und * weitere 500 EUR hinzu (§ 1 Abs. 2 der Verordnung).
* nicht im Gesetz, rechnet die Anwendung damit nur eine *Schätzung*. Was */
* gilt, steht in der Heimrechnung. export const VERMOEGENSFREIBETRAG_CENT = 1_000_000n
*/ export const VERMOEGENSFREIBETRAG_JE_UNTERHALTENE_PERSON_CENT = 50_000n
export const ABWESENHEIT_KARENZTAGE = 3 export const VERMOEGENSFREIBETRAG_STAND = '2023-01-01'
export const ABWESENHEIT_MINDERUNG_PROZENT = 25
export const ABWESENHEIT_PLATZFREIHALTUNG_TAGE = 42 /**
* Rückforderung einer Schenkung wegen Verarmung, § 528 BGB.
// ----------------------------------------------------------------------------- * Der Anspruch ist ausgeschlossen, wenn zum Zeitpunkt des Eintritts der
// Vermögen, § 90 SGB XII * 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.
/** */
* Vermögensfreibetrag ("kleinere Barbeträge"), § 90 Abs. 2 Nr. 9 SGB XII export const SCHENKUNG_RUECKFORDERUNG_JAHRE = 10
* 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. // -----------------------------------------------------------------------------
* // Rechtsbehelf
* 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). /** Regelfall: ein Monat ab Bekanntgabe, § 84 Abs. 1 Satz 1 SGG. */
*/ export const WIDERSPRUCHSFRIST_MONATE_REGEL = 1
export const VERMOEGENSFREIBETRAG_CENT = 1_000_000n
export const VERMOEGENSFREIBETRAG_JE_UNTERHALTENE_PERSON_CENT = 50_000n /**
export const VERMOEGENSFREIBETRAG_STAND = '2023-01-01' * 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.
* Rückforderung einer Schenkung wegen Verarmung, § 528 BGB. */
* Der Anspruch ist ausgeschlossen, wenn zum Zeitpunkt des Eintritts der export const WIDERSPRUCHSFRIST_MONATE_OHNE_BELEHRUNG = 12
* 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. * Zugangsvermutung bei Übermittlung durch die Post im Inland:
*/ * bekanntgegeben am dritten Tag nach der Aufgabe zur Post
export const SCHENKUNG_RUECKFORDERUNG_JAHRE = 10 * (§ 37 Abs. 2 Satz 1 SGB X).
*/
// ----------------------------------------------------------------------------- export const ZUGANGSVERMUTUNG_TAGE = 3
// Rechtsbehelf
// ----------------------------------------------------------------------------- /** Vorwarnstufen für die Frist "Widerspruch möglich bis" (Tage vorher). */
export const WIDERSPRUCH_VORWARNUNG_TAGE = [21, 14, 7, 3, 1]
/** Regelfall: ein Monat ab Bekanntgabe, § 84 Abs. 1 Satz 1 SGG. */
export const WIDERSPRUCHSFRIST_MONATE_REGEL = 1 /** Vorwarnstufen für eine vom Träger gesetzte Nachforderungsfrist. */
export const NACHFORDERUNG_VORWARNUNG_TAGE = [14, 7, 3, 1]
/**
* Fehlt die Rechtsbehelfsbelehrung oder ist sie unrichtig erteilt, beträgt // -----------------------------------------------------------------------------
* die Frist ein Jahr seit Bekanntgabe (§ 66 Abs. 2 Satz 1 SGG). // Weitere Leistungen
* Das wird ausdrücklich ausgewählt und nie automatisch angenommen. // -----------------------------------------------------------------------------
*/
export const WIDERSPRUCHSFRIST_MONATE_OHNE_BELEHRUNG = 12 /**
* Bayerisches Landespflegegeld: 1.000 EUR im Jahr, ab Pflegegrad 2,
/** * Antrag bis 31.12. des jeweiligen Jahres (Bayerisches
* Zugangsvermutung bei Übermittlung durch die Post im Inland: * Landespflegegeldgesetz - BayLPflGG).
* bekanntgegeben am dritten Tag nach der Aufgabe zur Post *
* (§ 37 Abs. 2 Satz 1 SGB X). * Der Betrag steht im Haushalt des Freistaats und wurde politisch schon
*/ * mehrfach in Frage gestellt. Vor der Antragstellung die aktuelle Höhe
export const ZUGANGSVERMUTUNG_TAGE = 3 * beim Landesamt für Pflege prüfen.
*/
/** Vorwarnstufen für die Frist "Widerspruch möglich bis" (Tage vorher). */ export const LANDESPFLEGEGELD_CENT = 100_000n
export const WIDERSPRUCH_VORWARNUNG_TAGE = [21, 14, 7, 3, 1] export const LANDESPFLEGEGELD_AB_PFLEGEGRAD = 2
export const LANDESPFLEGEGELD_ANTRAGSSCHLUSS = '31.12.'
/** Vorwarnstufen für eine vom Träger gesetzte Nachforderungsfrist. */
export const NACHFORDERUNG_VORWARNUNG_TAGE = [14, 7, 3, 1] /**
* Zuzahlungsbefreiung, § 62 SGB V: Die Belastungsgrenze liegt bei 2 % der
// ----------------------------------------------------------------------------- * jährlichen Bruttoeinnahmen zum Lebensunterhalt, bei schwerwiegend
// Weitere Leistungen * 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
* Bayerisches Landespflegegeld: 1.000 EUR im Jahr, ab Pflegegrad 2, * anzusetzen (§ 62 Abs. 2 Satz 5 und 6 SGB V) - nicht die tatsächliche
* Antrag bis 31.12. des jeweiligen Jahres (Bayerisches * Rente. Das macht die Befreiung meist schon im Januar erreichbar.
* Landespflegegeldgesetz - BayLPflGG). *
* * 2 % von 12 x 563,00 EUR = 135,12 EUR; 1 % = 67,56 EUR (Stand 2025).
* Der Betrag steht im Haushalt des Freistaats und wurde politisch schon */
* mehrfach in Frage gestellt. Vor der Antragstellung die aktuelle Höhe export const ZUZAHLUNG_BELASTUNGSGRENZE_PROZENT = 2
* beim Landesamt für Pflege prüfen. export const ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_PROZENT = 1
*/ export const ZUZAHLUNG_BELASTUNGSGRENZE_CENT =
export const LANDESPFLEGEGELD_CENT = 100_000n (REGELBEDARFSSTUFE_1_CENT * 12n * BigInt(ZUZAHLUNG_BELASTUNGSGRENZE_PROZENT)) / 100n
export const LANDESPFLEGEGELD_AB_PFLEGEGRAD = 2 export const ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_CENT =
export const LANDESPFLEGEGELD_ANTRAGSSCHLUSS = '31.12.' (REGELBEDARFSSTUFE_1_CENT * 12n * BigInt(ZUZAHLUNG_BELASTUNGSGRENZE_CHRONIKER_PROZENT)) / 100n
/** /**
* Zuzahlungsbefreiung, § 62 SGB V: Die Belastungsgrenze liegt bei 2 % der * Rundfunkbeitrag je Wohnung, § 8 Rundfunkfinanzierungsstaatsvertrag:
* jährlichen Bruttoeinnahmen zum Lebensunterhalt, bei schwerwiegend * 18,36 EUR monatlich, unverändert seit 01.08.2021. Die Ermäßigung auf ein
* chronisch Kranken bei 1 %. * Drittel (§ 4 Abs. 2 RBStV, Merkzeichen RF) beträgt 6,12 EUR.
* */
* Für Menschen, die Leistungen nach dem SGB XII in einer Einrichtung export const RUNDFUNKBEITRAG_CENT = 1_836n
* erhalten, ist als Bruttoeinnahme der Regelbedarf der Regelbedarfsstufe 1 export const RUNDFUNKBEITRAG_ERMAESSIGT_CENT = 612n
* anzusetzen (§ 62 Abs. 2 Satz 5 und 6 SGB V) - nicht die tatsächliche export const RUNDFUNKBEITRAG_STAND = '2021-08-01'
* 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
View File
@@ -1,437 +1,437 @@
import { z } from 'zod' import { z } from 'zod'
import { betragZuCent } from '@/lib/sozialhilfe/betrag' import { betragParsenWennMoeglich } from '@/lib/finanzen/betrag'
/** /**
* Eingabeprüfung für das Sozialhilfemodul. * Eingabeprüfung für das Sozialhilfemodul.
* *
* Geprüft wird immer serverseitig. Die Meldungen sagen, was zu tun ist - * 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 * nicht, welche Regel verletzt wurde. Wer am Handy im Heim steht und eine
* Nachforderung erfasst, hat keine Lust auf "invalid input". * Nachforderung erfasst, hat keine Lust auf "invalid input".
*/ */
const optionalerText = (max = 200) => const optionalerText = (max = 200) =>
z z
.string() .string()
.trim() .trim()
.max(max, `Bitte höchstens ${max} Zeichen.`) .max(max, `Bitte höchstens ${max} Zeichen.`)
.transform((wert) => (wert === '' ? null : wert)) .transform((wert) => (wert === '' ? null : wert))
.nullable() .nullable()
.default(null) .default(null)
const optionalesDatum = z const optionalesDatum = z
.string() .string()
.trim() .trim()
.transform((wert) => (wert === '' ? null : wert)) .transform((wert) => (wert === '' ? null : wert))
.nullable() .nullable()
.default(null) .default(null)
.refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), { .refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), {
message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.', message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.',
}) })
.transform((wert) => (wert === null ? null : new Date(`${wert}T00:00:00.000Z`))) .transform((wert) => (wert === null ? null : new Date(`${wert}T00:00:00.000Z`)))
const pflichtDatum = z const pflichtDatum = z
.string() .string()
.trim() .trim()
.min(1, 'Bitte ein Datum angeben.') .min(1, 'Bitte ein Datum angeben.')
.refine((wert) => /^\d{4}-\d{2}-\d{2}$/.test(wert), { .refine((wert) => /^\d{4}-\d{2}-\d{2}$/.test(wert), {
message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.', message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.',
}) })
.transform((wert) => new Date(`${wert}T00:00:00.000Z`)) .transform((wert) => new Date(`${wert}T00:00:00.000Z`))
/** /**
* Geldbetrag aus einem Textfeld. Leer wird zu null, nicht zu 0 - "keine * Geldbetrag aus einem Textfeld. Leer wird zu null, nicht zu 0 - "keine
* Angabe" und "null Euro" sind fachlich zweierlei. * Angabe" und "null Euro" sind fachlich zweierlei.
*/ */
const optionalerBetrag = z const optionalerBetrag = z
.string() .string()
.trim() .trim()
.default('') .default('')
.transform((wert, ctx) => { .transform((wert, ctx) => {
if (wert === '') return null if (wert === '') return null
const cent = betragZuCent(wert) const cent = betragParsenWennMoeglich(wert)
if (cent === null) { if (cent === null) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Bitte einen Betrag wie 1.234,56 eingeben.', message: 'Bitte einen Betrag wie 1.234,56 eingeben.',
}) })
return z.NEVER return z.NEVER
} }
return cent return cent
}) })
/** Betrag, der in einer Gegenüberstellung stehen muss - leer heißt 0. */ /** Betrag, der in einer Gegenüberstellung stehen muss - leer heißt 0. */
const betragOderNull = z const betragOderNull = z
.string() .string()
.trim() .trim()
.default('') .default('')
.transform((wert, ctx) => { .transform((wert, ctx) => {
if (wert === '') return 0n if (wert === '') return 0n
const cent = betragZuCent(wert) const cent = betragParsenWennMoeglich(wert)
if (cent === null) { if (cent === null) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Bitte einen Betrag wie 1.234,56 eingeben.', message: 'Bitte einen Betrag wie 1.234,56 eingeben.',
}) })
return z.NEVER return z.NEVER
} }
if (cent < 0n) { if (cent < 0n) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'In dieser Aufstellung stehen nur positive Beträge.', message: 'In dieser Aufstellung stehen nur positive Beträge.',
}) })
return z.NEVER return z.NEVER
} }
return cent return cent
}) })
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Antragsakte // Antragsakte
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const AntragSchema = z export const AntragSchema = z
.object({ .object({
traeger: z traeger: z
.string() .string()
.trim() .trim()
.min(2, 'Bitte den Träger angeben, z.B. „Bezirk Oberfranken“.') .min(2, 'Bitte den Träger angeben, z.B. „Bezirk Oberfranken“.')
.max(150), .max(150),
leistungsart: z leistungsart: z
.string() .string()
.trim() .trim()
.min(2, 'Bitte die Leistungsart angeben.') .min(2, 'Bitte die Leistungsart angeben.')
.max(200), .max(200),
aktenzeichen: optionalerText(80), aktenzeichen: optionalerText(80),
sachbearbeiter: optionalerText(150), sachbearbeiter: optionalerText(150),
telefon: optionalerText(50), telefon: optionalerText(50),
email: z email: z
.string() .string()
.trim() .trim()
.max(200) .max(200)
.transform((wert) => (wert === '' ? null : wert)) .transform((wert) => (wert === '' ? null : wert))
.nullable() .nullable()
.default(null) .default(null)
.refine((wert) => wert === null || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(wert), { .refine((wert) => wert === null || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(wert), {
message: 'Bitte eine gültige E-Mail-Adresse angeben oder das Feld leer lassen.', message: 'Bitte eine gültige E-Mail-Adresse angeben oder das Feld leer lassen.',
}), }),
antragAm: optionalesDatum, antragAm: optionalesDatum,
eingangBeimTraegerAm: optionalesDatum, eingangBeimTraegerAm: optionalesDatum,
bewilligtVon: optionalesDatum, bewilligtVon: optionalesDatum,
bewilligtBis: optionalesDatum, bewilligtBis: optionalesDatum,
notizen: optionalerText(4000), notizen: optionalerText(4000),
}) })
.refine( .refine(
(daten) => (daten) =>
!daten.bewilligtVon || !daten.bewilligtBis || daten.bewilligtVon <= daten.bewilligtBis, !daten.bewilligtVon || !daten.bewilligtBis || daten.bewilligtVon <= daten.bewilligtBis,
{ {
message: 'Das Ende des Bewilligungszeitraums liegt vor dem Beginn.', message: 'Das Ende des Bewilligungszeitraums liegt vor dem Beginn.',
path: ['bewilligtBis'], path: ['bewilligtBis'],
}, },
) )
.refine( .refine(
(daten) => (daten) =>
!daten.antragAm || !daten.antragAm ||
!daten.eingangBeimTraegerAm || !daten.eingangBeimTraegerAm ||
daten.antragAm <= daten.eingangBeimTraegerAm, daten.antragAm <= daten.eingangBeimTraegerAm,
{ {
message: 'Der Eingang beim Träger kann nicht vor dem Antragsdatum liegen.', message: 'Der Eingang beim Träger kann nicht vor dem Antragsdatum liegen.',
path: ['eingangBeimTraegerAm'], path: ['eingangBeimTraegerAm'],
}, },
) )
export type AntragEingabe = z.infer<typeof AntragSchema> export type AntragEingabe = z.infer<typeof AntragSchema>
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Nachforderung // Nachforderung
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const NACHFORDERUNG_STATUS_WERTE = [ export const NACHFORDERUNG_STATUS_WERTE = [
'OFFEN', 'OFFEN',
'IN_BESCHAFFUNG', 'IN_BESCHAFFUNG',
'UEBERSANDT', 'UEBERSANDT',
'ANERKANNT', 'ANERKANNT',
'NICHT_VORHANDEN', 'NICHT_VORHANDEN',
] as const ] as const
export const NachforderungSchema = z export const NachforderungSchema = z
.object({ .object({
bezeichnung: z bezeichnung: z
.string() .string()
.trim() .trim()
.min(2, 'Bitte benennen, welche Unterlage der Träger will.') .min(2, 'Bitte benennen, welche Unterlage der Träger will.')
.max(200), .max(200),
erlaeuterung: optionalerText(2000), erlaeuterung: optionalerText(2000),
status: z.enum(NACHFORDERUNG_STATUS_WERTE).default('OFFEN'), status: z.enum(NACHFORDERUNG_STATUS_WERTE).default('OFFEN'),
angefordertAm: optionalesDatum, angefordertAm: optionalesDatum,
fristBis: optionalesDatum, fristBis: optionalesDatum,
uebersandtAm: optionalesDatum, uebersandtAm: optionalesDatum,
begruendungNichtVorhanden: optionalerText(2000), begruendungNichtVorhanden: optionalerText(2000),
reihenfolge: z.coerce.number().int().min(0).max(10_000).default(0), reihenfolge: z.coerce.number().int().min(0).max(10_000).default(0),
dokumentId: optionalerText(40), dokumentId: optionalerText(40),
/** Soll aus der Frist des Trägers eine Frist im Fristenmodul werden? */ /** Soll aus der Frist des Trägers eine Frist im Fristenmodul werden? */
fristAnlegen: z.coerce.boolean().default(false), fristAnlegen: z.coerce.boolean().default(false),
}) })
.refine( .refine(
(daten) => (daten) =>
daten.status !== 'NICHT_VORHANDEN' || daten.status !== 'NICHT_VORHANDEN' ||
(daten.begruendungNichtVorhanden !== null && (daten.begruendungNichtVorhanden !== null &&
daten.begruendungNichtVorhanden.trim().length >= 5), daten.begruendungNichtVorhanden.trim().length >= 5),
{ {
message: message:
'Bitte begründen, warum es die Unterlage nicht gibt. Die Begründung geht mit ins ' + '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.', 'Unterlagenpaket — ohne sie sieht der Träger nur eine Lücke.',
path: ['begruendungNichtVorhanden'], path: ['begruendungNichtVorhanden'],
}, },
) )
.refine((daten) => daten.status !== 'UEBERSANDT' || daten.uebersandtAm !== null, { .refine((daten) => daten.status !== 'UEBERSANDT' || daten.uebersandtAm !== null, {
message: message:
'Bitte das Übersendungsdatum angeben — es ist der Nachweis der Mitwirkung, wenn der ' + 'Bitte das Übersendungsdatum angeben — es ist der Nachweis der Mitwirkung, wenn der ' +
'Träger später von fehlenden Unterlagen ausgeht.', 'Träger später von fehlenden Unterlagen ausgeht.',
path: ['uebersandtAm'], path: ['uebersandtAm'],
}) })
.refine((daten) => !daten.fristAnlegen || daten.fristBis !== null, { .refine((daten) => !daten.fristAnlegen || daten.fristBis !== null, {
message: 'Für eine Frist im Fristenmodul brauche ich ein Fristdatum.', message: 'Für eine Frist im Fristenmodul brauche ich ein Fristdatum.',
path: ['fristBis'], path: ['fristBis'],
}) })
export type NachforderungEingabe = z.infer<typeof NachforderungSchema> export type NachforderungEingabe = z.infer<typeof NachforderungSchema>
export const NachforderungStatusSchema = z export const NachforderungStatusSchema = z
.object({ .object({
nachforderungId: z.string().trim().min(1), nachforderungId: z.string().trim().min(1),
status: z.enum(NACHFORDERUNG_STATUS_WERTE), status: z.enum(NACHFORDERUNG_STATUS_WERTE),
begruendungNichtVorhanden: optionalerText(2000), begruendungNichtVorhanden: optionalerText(2000),
uebersandtAm: optionalesDatum, uebersandtAm: optionalesDatum,
}) })
.refine( .refine(
(daten) => (daten) =>
daten.status !== 'NICHT_VORHANDEN' || daten.status !== 'NICHT_VORHANDEN' ||
(daten.begruendungNichtVorhanden !== null && (daten.begruendungNichtVorhanden !== null &&
daten.begruendungNichtVorhanden.trim().length >= 5), daten.begruendungNichtVorhanden.trim().length >= 5),
{ {
message: 'Bitte begründen, warum es die Unterlage nicht gibt.', message: 'Bitte begründen, warum es die Unterlage nicht gibt.',
path: ['begruendungNichtVorhanden'], path: ['begruendungNichtVorhanden'],
}, },
) )
export const ChecklisteSchema = z.object({ export const ChecklisteSchema = z.object({
antragId: z.string().trim().min(1), antragId: z.string().trim().min(1),
schluessel: z.array(z.string().trim().min(1)).min(1, 'Bitte mindestens eine Position auswählen.'), schluessel: z.array(z.string().trim().min(1)).min(1, 'Bitte mindestens eine Position auswählen.'),
angefordertAm: optionalesDatum, angefordertAm: optionalesDatum,
fristBis: optionalesDatum, fristBis: optionalesDatum,
}) })
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Bescheid // Bescheid
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const BESCHEID_ART_WERTE = [ export const BESCHEID_ART_WERTE = [
'BEWILLIGUNG', 'BEWILLIGUNG',
'ABLEHNUNG', 'ABLEHNUNG',
'AENDERUNG', 'AENDERUNG',
'AUFHEBUNG', 'AUFHEBUNG',
'ERSTATTUNG', 'ERSTATTUNG',
'VORSCHUSS', 'VORSCHUSS',
'WIDERSPRUCHSBESCHEID', 'WIDERSPRUCHSBESCHEID',
] as const ] as const
export const PRUEFSTATUS_WERTE = [ export const PRUEFSTATUS_WERTE = [
'UNGEPRUEFT', 'UNGEPRUEFT',
'GEPRUEFT_OK', 'GEPRUEFT_OK',
'WIDERSPRUCH_EINGELEGT', 'WIDERSPRUCH_EINGELEGT',
'WIDERSPRUCH_ERLEDIGT', 'WIDERSPRUCH_ERLEDIGT',
'FRIST_VERSTRICHEN', 'FRIST_VERSTRICHEN',
] as const ] as const
export const BescheidSchema = z export const BescheidSchema = z
.object({ .object({
art: z.enum(BESCHEID_ART_WERTE), art: z.enum(BESCHEID_ART_WERTE),
bescheidVom: pflichtDatum, bescheidVom: pflichtDatum,
zugegangenAm: optionalesDatum, zugegangenAm: optionalesDatum,
zeitraumVon: optionalesDatum, zeitraumVon: optionalesDatum,
zeitraumBis: optionalesDatum, zeitraumBis: optionalesDatum,
betrag: optionalerBetrag, betrag: optionalerBetrag,
/** /**
* 1 = Regelfall (§ 84 SGG), 12 = fehlende oder fehlerhafte * 1 = Regelfall (§ 84 SGG), 12 = fehlende oder fehlerhafte
* Rechtsbehelfsbelehrung (§ 66 SGG). Wird ausgewählt, nie geraten. * Rechtsbehelfsbelehrung (§ 66 SGG). Wird ausgewählt, nie geraten.
*/ */
rechtsbehelfsfristMonate: z.coerce rechtsbehelfsfristMonate: z.coerce
.number() .number()
.int() .int()
.refine((wert) => wert === 1 || wert === 12, { .refine((wert) => wert === 1 || wert === 12, {
message: 'Es kommen nur ein Monat (Regelfall) oder ein Jahr (fehlende Belehrung) in Betracht.', message: 'Es kommen nur ein Monat (Regelfall) oder ein Jahr (fehlende Belehrung) in Betracht.',
}) })
.default(1), .default(1),
widerspruchEingelegtAm: optionalesDatum, widerspruchEingelegtAm: optionalesDatum,
notizen: optionalerText(4000), notizen: optionalerText(4000),
dokumentId: optionalerText(40), dokumentId: optionalerText(40),
/** Soll die Frist "Widerspruch möglich bis" angelegt werden? */ /** Soll die Frist "Widerspruch möglich bis" angelegt werden? */
fristAnlegen: z.coerce.boolean().default(false), fristAnlegen: z.coerce.boolean().default(false),
}) })
.refine( .refine(
(daten) => !daten.zugegangenAm || daten.zugegangenAm >= daten.bescheidVom, (daten) => !daten.zugegangenAm || daten.zugegangenAm >= daten.bescheidVom,
{ {
message: message:
'Der Bescheid kann nicht vor seinem Ausstellungsdatum zugegangen sein. Prüfe den ' + 'Der Bescheid kann nicht vor seinem Ausstellungsdatum zugegangen sein. Prüfe den ' +
'Poststempel.', 'Poststempel.',
path: ['zugegangenAm'], path: ['zugegangenAm'],
}, },
) )
.refine( .refine(
(daten) => !daten.zeitraumVon || !daten.zeitraumBis || daten.zeitraumVon <= daten.zeitraumBis, (daten) => !daten.zeitraumVon || !daten.zeitraumBis || daten.zeitraumVon <= daten.zeitraumBis,
{ message: 'Das Ende des Zeitraums liegt vor dem Beginn.', path: ['zeitraumBis'] }, { message: 'Das Ende des Zeitraums liegt vor dem Beginn.', path: ['zeitraumBis'] },
) )
.refine((daten) => !daten.fristAnlegen || daten.zugegangenAm !== null, { .refine((daten) => !daten.fristAnlegen || daten.zugegangenAm !== null, {
message: message:
'Ohne Zugangsdatum lässt sich die Widerspruchsfrist nicht berechnen — und damit auch ' + 'Ohne Zugangsdatum lässt sich die Widerspruchsfrist nicht berechnen — und damit auch ' +
'keine Frist anlegen.', 'keine Frist anlegen.',
path: ['zugegangenAm'], path: ['zugegangenAm'],
}) })
export type BescheidEingabe = z.infer<typeof BescheidSchema> export type BescheidEingabe = z.infer<typeof BescheidSchema>
export const BescheidStatusSchema = z.object({ export const BescheidStatusSchema = z.object({
bescheidId: z.string().trim().min(1), bescheidId: z.string().trim().min(1),
pruefStatus: z.enum(PRUEFSTATUS_WERTE), pruefStatus: z.enum(PRUEFSTATUS_WERTE),
widerspruchEingelegtAm: optionalesDatum, widerspruchEingelegtAm: optionalesDatum,
}) })
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Vermögensbewertung // Vermögensbewertung
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const BEWERTUNG_WERTE = ['UNGEPRUEFT', 'VERWERTBAR', 'GESCHUETZT', 'STRITTIG'] as const export const BEWERTUNG_WERTE = ['UNGEPRUEFT', 'VERWERTBAR', 'GESCHUETZT', 'STRITTIG'] as const
export const BewertungSchema = z export const BewertungSchema = z
.object({ .object({
herkunft: z.enum(['VERMOEGEN', 'VERTRAG']), herkunft: z.enum(['VERMOEGEN', 'VERTRAG']),
id: z.string().trim().min(1), id: z.string().trim().min(1),
bewertung: z.enum(BEWERTUNG_WERTE), bewertung: z.enum(BEWERTUNG_WERTE),
begruendung: optionalerText(6000), begruendung: optionalerText(6000),
}) })
.refine( .refine(
(daten) => (daten) =>
daten.bewertung === 'UNGEPRUEFT' || daten.bewertung === 'UNGEPRUEFT' ||
(daten.begruendung !== null && daten.begruendung.trim().length >= 10), (daten.begruendung !== null && daten.begruendung.trim().length >= 10),
{ {
message: message:
'Eine Bewertung ohne Begründung hilft im Widerspruchsverfahren nicht weiter. ' + 'Eine Bewertung ohne Begründung hilft im Widerspruchsverfahren nicht weiter. ' +
'Übernimm einen der Textbausteine und passe ihn an.', 'Übernimm einen der Textbausteine und passe ihn an.',
path: ['begruendung'], path: ['begruendung'],
}, },
) )
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Heimkosten // Heimkosten
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const HeimkostenSchema = z.object({ export const HeimkostenSchema = z.object({
jahr: z.coerce jahr: z.coerce
.number() .number()
.int() .int()
.min(2000, 'Das Jahr liegt außerhalb des sinnvollen Bereichs.') .min(2000, 'Das Jahr liegt außerhalb des sinnvollen Bereichs.')
.max(2100, '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), monat: z.coerce.number().int().min(1, 'Bitte einen Monat wählen.').max(12),
pflegeEigenanteil: betragOderNull, pflegeEigenanteil: betragOderNull,
unterkunftVerpflegung: betragOderNull, unterkunftVerpflegung: betragOderNull,
investitionskosten: betragOderNull, investitionskosten: betragOderNull,
ausbildungsumlage: betragOderNull, ausbildungsumlage: betragOderNull,
zusatzleistungen: betragOderNull, zusatzleistungen: betragOderNull,
pflegekasse: betragOderNull, pflegekasse: betragOderNull,
leistungszuschlagProzent: z.coerce leistungszuschlagProzent: z.coerce
.number() .number()
.int() .int()
.min(0, 'Der Zuschlag liegt zwischen 0 und 100 Prozent.') .min(0, 'Der Zuschlag liegt zwischen 0 und 100 Prozent.')
.max(100, 'Der Zuschlag liegt zwischen 0 und 100 Prozent.') .max(100, 'Der Zuschlag liegt zwischen 0 und 100 Prozent.')
.default(0), .default(0),
leistungszuschlag: betragOderNull, leistungszuschlag: betragOderNull,
rente: betragOderNull, rente: betragOderNull,
sonstigeEinkuenfte: betragOderNull, sonstigeEinkuenfte: betragOderNull,
sozialhilfe: betragOderNull, sozialhilfe: betragOderNull,
eigenmittel: betragOderNull, eigenmittel: betragOderNull,
barbetrag: betragOderNull, barbetrag: betragOderNull,
bekleidungspauschale: betragOderNull, bekleidungspauschale: betragOderNull,
abwesenheitstage: z.coerce abwesenheitstage: z.coerce
.number() .number()
.int() .int()
.min(0, 'Abwesenheitstage können nicht negativ sein.') .min(0, 'Abwesenheitstage können nicht negativ sein.')
.max(31, 'Ein Monat hat höchstens 31 Tage.') .max(31, 'Ein Monat hat höchstens 31 Tage.')
.default(0), .default(0),
bemerkung: optionalerText(2000), bemerkung: optionalerText(2000),
}) })
export type HeimkostenEingabeFormular = z.infer<typeof HeimkostenSchema> export type HeimkostenEingabeFormular = z.infer<typeof HeimkostenSchema>
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Weitere Leistungen // Weitere Leistungen
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const LEISTUNG_STATUS_WERTE = [ export const LEISTUNG_STATUS_WERTE = [
'NICHT_GEPRUEFT', 'NICHT_GEPRUEFT',
'PRUEFUNG_LAEUFT', 'PRUEFUNG_LAEUFT',
'BEANTRAGT', 'BEANTRAGT',
'BEWILLIGT', 'BEWILLIGT',
'ABGELEHNT', 'ABGELEHNT',
'LAEUFT', 'LAEUFT',
'NICHT_ZUTREFFEND', 'NICHT_ZUTREFFEND',
] as const ] as const
export const LeistungSchema = z export const LeistungSchema = z
.object({ .object({
leistungId: optionalerText(40), leistungId: optionalerText(40),
katalogSchluessel: optionalerText(80), katalogSchluessel: optionalerText(80),
bezeichnung: z.string().trim().min(2, 'Bitte die Leistung benennen.').max(200), bezeichnung: z.string().trim().min(2, 'Bitte die Leistung benennen.').max(200),
traeger: optionalerText(200), traeger: optionalerText(200),
rechtsgrundlage: optionalerText(200), rechtsgrundlage: optionalerText(200),
status: z.enum(LEISTUNG_STATUS_WERTE).default('NICHT_GEPRUEFT'), status: z.enum(LEISTUNG_STATUS_WERTE).default('NICHT_GEPRUEFT'),
beantragtAm: optionalesDatum, beantragtAm: optionalesDatum,
bewilligtAm: optionalesDatum, bewilligtAm: optionalesDatum,
gueltigBis: optionalesDatum, gueltigBis: optionalesDatum,
betrag: optionalerBetrag, betrag: optionalerBetrag,
jaehrlichNeuBeantragen: z.coerce.boolean().default(false), jaehrlichNeuBeantragen: z.coerce.boolean().default(false),
notizen: optionalerText(2000), notizen: optionalerText(2000),
}) })
.refine((daten) => !daten.bewilligtAm || !daten.beantragtAm || daten.bewilligtAm >= daten.beantragtAm, { .refine((daten) => !daten.bewilligtAm || !daten.beantragtAm || daten.bewilligtAm >= daten.beantragtAm, {
message: 'Die Bewilligung kann nicht vor dem Antrag liegen.', message: 'Die Bewilligung kann nicht vor dem Antrag liegen.',
path: ['bewilligtAm'], path: ['bewilligtAm'],
}) })
export const KatalogUebernehmenSchema = z.object({ export const KatalogUebernehmenSchema = z.object({
schluessel: z.array(z.string().trim().min(1)).min(1, 'Bitte mindestens eine Leistung auswählen.'), schluessel: z.array(z.string().trim().min(1)).min(1, 'Bitte mindestens eine Leistung auswählen.'),
}) })
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Formularhilfen // Formularhilfen
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/** Kontrollkästchen liefern "on" oder gar nichts. */ /** Kontrollkästchen liefern "on" oder gar nichts. */
export function kaestchen(formular: FormData, name: string): boolean { export function kaestchen(formular: FormData, name: string): boolean {
const wert = formular.get(name) const wert = formular.get(name)
return wert === 'on' || wert === 'true' || wert === '1' return wert === 'on' || wert === 'true' || wert === '1'
} }
/** Liest ein FormData flach in ein Objekt - ohne Mehrfachwerte. */ /** Liest ein FormData flach in ein Objekt - ohne Mehrfachwerte. */
export function formularLesen(formular: FormData): Record<string, unknown> { export function formularLesen(formular: FormData): Record<string, unknown> {
const roh: Record<string, unknown> = {} const roh: Record<string, unknown> = {}
for (const [name, wert] of formular.entries()) { for (const [name, wert] of formular.entries()) {
if (typeof wert === 'string') roh[name] = wert if (typeof wert === 'string') roh[name] = wert
} }
return roh return roh
} }
/** /**
* Zod-Feldfehler in die flache Form bringen, die die Formulare erwarten. * Zod-Feldfehler in die flache Form bringen, die die Formulare erwarten.
*/ */
export function feldfehler(fehler: z.ZodError): Record<string, string> { export function feldfehler(fehler: z.ZodError): Record<string, string> {
const flach = fehler.flatten().fieldErrors const flach = fehler.flatten().fieldErrors
const ergebnis: Record<string, string> = {} const ergebnis: Record<string, string> = {}
for (const [feld, meldungen] of Object.entries(flach)) { for (const [feld, meldungen] of Object.entries(flach)) {
const erste = meldungen?.[0] const erste = meldungen?.[0]
if (erste) ergebnis[feld] = erste if (erste) ergebnis[feld] = erste
} }
return ergebnis return ergebnis
} }
+637 -638
View File
File diff suppressed because it is too large Load Diff