fix: Doppelte Wurzelroute entfernt, Produktionsbuild geprueft

Zwei Dateien loesten denselben Pfad "/" auf: src/app/page.tsx und
src/app/(app)/page.tsx. Next.js uebersetzt das noch, erzeugt beim
Standalone-Build aber ein unvollstaendiges Client-Manifest - und genau
dieses Verzeichnis kopiert das Docker-Abbild. Im Behaelter waere die
Anwendung damit erst beim Start umgefallen. Die Weiterleitung liegt
jetzt nur noch ausserhalb der Routengruppe.

Dazu vier ungenutzte Importe und Konstanten, die den Build blockierten,
und eine ESLint-Regel, die mit Unterstrich benannte Parameter durchgehen
laesst - _gemeldeterMimeTyp in der Dateiablage wird absichtlich nicht
benutzt, weil der vom Browser gemeldete MIME-Typ nichts beweist. Der
Name sagt das; ihn zu entfernen wuerde die Schnittstelle verschlechtern.

Geprueft am fertigen Standalone-Build, also genau so, wie der Behaelter
startet:
- strenge CSP greift (nonce plus strict-dynamic, kein unsafe-inline fuer
  Skripte) und blockiert dabei nichts Eigenes
- Anmeldung, alle Fachseiten, Benutzermenue und Schnellerfassung
  funktionieren
- kein Konsolenfehler, keine blockierte Anfrage
- keine einzige Anfrage an einen fremden Host

Etappe: 8
Phase: Haertung
This commit is contained in:
Florian Wilde
2026-08-22 07:31:55 +02:00
parent 1dba4f3413
commit 468202249a
7 changed files with 400 additions and 396 deletions
+11 -1
View File
@@ -1,3 +1,13 @@
{ {
"extends": ["next/core-web-vitals", "next/typescript"] "extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
}
} }
-9
View File
@@ -1,9 +0,0 @@
import { redirect } from 'next/navigation'
/**
* Die Wurzel der Anwendung hat keinen eigenen Inhalt: Wer angemeldet ist,
* will wissen, was heute zu tun ist.
*/
export default function StartPage() {
redirect('/heute')
}
+383 -383
View File
@@ -1,383 +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 { betragFormatieren } from '@/lib/finanzen/betrag' import { betragFormatieren } from '@/lib/finanzen/betrag'
import { db, nichtGeloescht } from '@/lib/db' import { db } 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,
KOSTENLESART_BESCHRIFTUNG, KOSTENLESART_BESCHRIFTUNG,
KOSTENLESART_ERKLAERUNG, KOSTENLESART_ERKLAERUNG,
KOSTENLESART_STANDARD, KOSTENLESART_STANDARD,
leistungszuschlagProzent, leistungszuschlagProzent,
leistungszuschlagStufe, leistungszuschlagStufe,
monatsname, monatsname,
pflegekassePauschaleCent, pflegekassePauschaleCent,
pflegekasseRechtsgrundlage, pflegekasseRechtsgrundlage,
verweildauerMonate, verweildauerMonate,
} from '@/lib/sozialhilfe/heimkosten' } from '@/lib/sozialhilfe/heimkosten'
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang' import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
import { formatCent } from '@/lib/utils' import { formatCent } from '@/lib/utils'
import { Info, TrendingDown } from 'lucide-react' import { Info, TrendingDown } from 'lucide-react'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
/** /**
* Heimkosten-Gegenüberstellung. * Heimkosten-Gegenüberstellung.
* *
* Die eine Zahl, um die es geht, ist die Deckungslücke: Was bleibt übrig, * Die eine Zahl, um die es geht, ist die Deckungslücke: Was bleibt übrig,
* nachdem Pflegekasse, Rente und Sozialhilfe gezahlt haben? Sie steht je * nachdem Pflegekasse, Rente und Sozialhilfe gezahlt haben? Sie steht je
* Monat und als Jahressumme da - denn eine Lücke von 180 Euro im Monat * Monat und als Jahressumme da - denn eine Lücke von 180 Euro im Monat
* sind über ein Jahr mehr als zweitausend. * sind über ein Jahr mehr als zweitausend.
*/ */
export default async function HeimkostenSeite({ export default async function HeimkostenSeite({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ jahr?: string }> searchParams: Promise<{ jahr?: string }>
}) { }) {
const { betreuteId, betreute } = await sozialhilfeKontext() const { betreuteId, betreute } = await sozialhilfeKontext()
const parameter = await searchParams const parameter = await searchParams
const heute = heuteAlsTag() const heute = heuteAlsTag()
const gewaehltesJahr = Number.parseInt(parameter.jahr ?? '', 10) const gewaehltesJahr = Number.parseInt(parameter.jahr ?? '', 10)
const jahr = Number.isFinite(gewaehltesJahr) ? gewaehltesJahr : heute.getUTCFullYear() const jahr = Number.isFinite(gewaehltesJahr) ? gewaehltesJahr : heute.getUTCFullYear()
const [monate, heimvertrag] = await Promise.all([ const [monate, heimvertrag] = await Promise.all([
db.heimkostenmonat.findMany({ db.heimkostenmonat.findMany({
where: { betreuteId, jahr }, where: { betreuteId, jahr },
orderBy: { monat: 'asc' }, orderBy: { monat: 'asc' },
}), }),
db.heimvertrag.findUnique({ where: { betreuteId } }), db.heimvertrag.findUnique({ where: { betreuteId } }),
]) ])
const ergebnisse = monate.map((m) => const ergebnisse = monate.map((m) =>
heimkostenAuswerten({ heimkostenAuswerten({
jahr: m.jahr, jahr: m.jahr,
monat: m.monat, monat: m.monat,
pflegeEigenanteilCent: m.pflegeEigenanteilCent, pflegeEigenanteilCent: m.pflegeEigenanteilCent,
unterkunftVerpflegungCent: m.unterkunftVerpflegungCent, unterkunftVerpflegungCent: m.unterkunftVerpflegungCent,
investitionskostenCent: m.investitionskostenCent, investitionskostenCent: m.investitionskostenCent,
ausbildungsumlageCent: m.ausbildungsumlageCent, ausbildungsumlageCent: m.ausbildungsumlageCent,
zusatzleistungenCent: m.zusatzleistungenCent, zusatzleistungenCent: m.zusatzleistungenCent,
pflegekasseCent: m.pflegekasseCent, pflegekasseCent: m.pflegekasseCent,
leistungszuschlagCent: m.leistungszuschlagCent, leistungszuschlagCent: m.leistungszuschlagCent,
renteCent: m.renteCent, renteCent: m.renteCent,
sonstigeEinkuenfteCent: m.sonstigeEinkuenfteCent, sonstigeEinkuenfteCent: m.sonstigeEinkuenfteCent,
sozialhilfeCent: m.sozialhilfeCent, sozialhilfeCent: m.sozialhilfeCent,
eigenmittelCent: m.eigenmittelCent, eigenmittelCent: m.eigenmittelCent,
barbetragCent: m.barbetragCent, barbetragCent: m.barbetragCent,
bekleidungspauschaleCent: m.bekleidungspauschaleCent, bekleidungspauschaleCent: m.bekleidungspauschaleCent,
abwesenheitstage: m.abwesenheitstage, abwesenheitstage: m.abwesenheitstage,
}), }),
) )
const verlauf = jahresverlaufBilden(jahr, ergebnisse) const verlauf = jahresverlaufBilden(jahr, ergebnisse)
const zeilen: MonatsZeile[] = Array.from({ length: 12 }, (_, i) => { const zeilen: MonatsZeile[] = Array.from({ length: 12 }, (_, i) => {
const monat = i + 1 const monat = i + 1
const ergebnis = ergebnisse.find((e) => e.monat === monat) const ergebnis = ergebnisse.find((e) => e.monat === monat)
const roh = monate.find((m) => m.monat === monat) const roh = monate.find((m) => m.monat === monat)
return { return {
jahr, jahr,
monat, monat,
heimkostenCent: ergebnis?.heimkostenCent ?? 0n, heimkostenCent: ergebnis?.heimkostenCent ?? 0n,
gesamtbedarfCent: ergebnis?.gesamtbedarfCent ?? 0n, gesamtbedarfCent: ergebnis?.gesamtbedarfCent ?? 0n,
deckungCent: ergebnis?.deckungCent ?? 0n, deckungCent: ergebnis?.deckungCent ?? 0n,
deckungslueckeCent: ergebnis?.deckungslueckeCent ?? 0n, deckungslueckeCent: ergebnis?.deckungslueckeCent ?? 0n,
ueberdeckungCent: ergebnis?.ueberdeckungCent ?? 0n, ueberdeckungCent: ergebnis?.ueberdeckungCent ?? 0n,
abwesenheitstage: roh?.abwesenheitstage ?? 0, abwesenheitstage: roh?.abwesenheitstage ?? 0,
erfasst: ergebnis !== undefined, erfasst: ergebnis !== undefined,
} }
}) })
// Vorschläge fürs Formular: Pauschale der Pflegekasse zum Pflegegrad // Vorschläge fürs Formular: Pauschale der Pflegekasse zum Pflegegrad
// und der gesetzliche Mindest-Barbetrag. // und der gesetzliche Mindest-Barbetrag.
const pauschale = pflegekassePauschaleCent(betreute.pflegegrad) const pauschale = pflegekassePauschaleCent(betreute.pflegegrad)
// Bewusst nur ein Hinweis, kein Vorschlagswert zum Uebernehmen: Unter der // Bewusst nur ein Hinweis, kein Vorschlagswert zum Uebernehmen: Unter der
// Lesart EIGENANTEIL_NETTO steckt die Pauschale schon im Eigenanteil. // Lesart EIGENANTEIL_NETTO steckt die Pauschale schon im Eigenanteil.
// Wer sie hier zusaetzlich eintraegt, zaehlt dasselbe Geld zweimal. // Wer sie hier zusaetzlich eintraegt, zaehlt dasselbe Geld zweimal.
const hinweisPflegekasse = pauschale const hinweisPflegekasse = pauschale
? `Pflegegrad ${betreute.pflegegrad}: ${formatCent(pauschale)} monatlich ` + ? `Pflegegrad ${betreute.pflegegrad}: ${formatCent(pauschale)} monatlich ` +
`(${pflegekasseRechtsgrundlage(betreute.pflegegrad)}, Stand ${KONSTANTEN_STAND}). ` + `(${pflegekasseRechtsgrundlage(betreute.pflegegrad)}, Stand ${KONSTANTEN_STAND}). ` +
`Dieser Betrag ist im Eigenanteil oben bereits abgezogen — lass das Feld deshalb auf 0. ` + `Dieser Betrag ist im Eigenanteil oben bereits abgezogen — lass das Feld deshalb auf 0. ` +
`Es dient nur der Erinnerung, was die Pflegekasse direkt an die Einrichtung zahlt.` `Es dient nur der Erinnerung, was die Pflegekasse direkt an die Einrichtung zahlt.`
: 'Pauschale nach § 43 SGB XI. Sie ist im Eigenanteil bereits abgezogen — lass das Feld ' + : '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.' 'auf 0. Hinterlege den Pflegegrad in den Stammdaten, dann steht hier der Betrag.'
const einzug = heimvertrag?.einzugAm ?? betreute.heimEinzugAm ?? null const einzug = heimvertrag?.einzugAm ?? betreute.heimEinzugAm ?? null
const verweildauer = einzug ? verweildauerMonate(einzug, heute) : null const verweildauer = einzug ? verweildauerMonate(einzug, heute) : null
const naechsterOffenerMonat = const naechsterOffenerMonat =
zeilen.find((z) => !z.erfasst && z.monat <= (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 12)) zeilen.find((z) => !z.erfasst && z.monat <= (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 12))
?.monat ?? (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 1) ?.monat ?? (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 1)
const jahre = Array.from({ length: 6 }, (_, i) => heute.getUTCFullYear() - 3 + i) const jahre = Array.from({ length: 6 }, (_, i) => heute.getUTCFullYear() - 3 + i)
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<SeitenKopf <SeitenKopf
titel={`Heimkosten ${jahr}`} titel={`Heimkosten ${jahr}`}
beschreibung="Kosten gegen Deckung, Monat für Monat. Zum Bedarf zählen auch Barbetrag und Bekleidungspauschale — sie müssen der betreuten Person verbleiben." beschreibung="Kosten gegen Deckung, Monat für Monat. Zum Bedarf zählen auch Barbetrag und Bekleidungspauschale — sie müssen der betreuten Person verbleiben."
pfad={[ pfad={[
{ titel: 'Start', href: '/heute' }, { titel: 'Start', href: '/heute' },
{ titel: 'Sozialhilfe', href: '/sozialhilfe' }, { titel: 'Sozialhilfe', href: '/sozialhilfe' },
{ titel: 'Heimkosten' }, { titel: 'Heimkosten' },
]} ]}
aktionen={ aktionen={
<nav aria-label="Jahr wählen" className="flex flex-wrap gap-1"> <nav aria-label="Jahr wählen" className="flex flex-wrap gap-1">
{jahre.map((j) => ( {jahre.map((j) => (
<Link <Link
key={j} key={j}
href={`/sozialhilfe/heimkosten?jahr=${j}`} href={`/sozialhilfe/heimkosten?jahr=${j}`}
aria-current={j === jahr ? 'page' : undefined} 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 ${ className={`inline-flex h-11 min-w-11 items-center justify-center rounded-md px-3 text-sm font-medium ${
j === jahr j === jahr
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: 'border border-input hover:bg-accent' : 'border border-input hover:bg-accent'
}`} }`}
> >
{j} {j}
</Link> </Link>
))} ))}
</nav> </nav>
} }
/> />
<div className="grid gap-4 sm:grid-cols-3"> <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">Bedarf {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.summeBedarfCent)} {formatCent(verlauf.summeBedarfCent)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<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">Deckung {jahr}</p>
<p className="text-xl font-semibold tabular-nums"> <p className="text-xl font-semibold tabular-nums">
{formatCent(verlauf.summeDeckungCent)} {formatCent(verlauf.summeDeckungCent)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card className={verlauf.summeDeckungslueckeCent > 0n ? 'border-ampel-rot/40' : undefined}> <Card className={verlauf.summeDeckungslueckeCent > 0n ? 'border-ampel-rot/40' : undefined}>
<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">Deckungslücke {jahr}</p>
<p <p
className={`text-xl font-semibold tabular-nums ${ className={`text-xl font-semibold tabular-nums ${
verlauf.summeDeckungslueckeCent > 0n ? 'text-ampel-rot' : 'text-ampel-gruen' verlauf.summeDeckungslueckeCent > 0n ? 'text-ampel-rot' : 'text-ampel-gruen'
}`} }`}
> >
{formatCent(verlauf.summeDeckungslueckeCent)} {formatCent(verlauf.summeDeckungslueckeCent)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{verlauf.groessteLuecke && ( {verlauf.groessteLuecke && (
<Alert variant="warnung"> <Alert variant="warnung">
<TrendingDown aria-hidden="true" /> <TrendingDown aria-hidden="true" />
<AlertTitle> <AlertTitle>
Größte Lücke im {monatsname(verlauf.groessteLuecke.monat)}:{' '} Größte Lücke im {monatsname(verlauf.groessteLuecke.monat)}:{' '}
{formatCent(verlauf.groessteLuecke.deckungslueckeCent)} {formatCent(verlauf.groessteLuecke.deckungslueckeCent)}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
Prüfe, ob der Bescheid für diesen Monat den vollen Bedarf abdeckt. Weicht die Rechnung Prüfe, ob der Bescheid für diesen Monat den vollen Bedarf abdeckt. Weicht die Rechnung
vom Bescheid ab, ist das der Anlass für eine Rückfrage beim Träger — und gegebenenfalls vom Bescheid ab, ist das der Anlass für eine Rückfrage beim Träger — und gegebenenfalls
für einen Widerspruch. für einen Widerspruch.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Was hier gegen was gerechnet wird</CardTitle> <CardTitle>Was hier gegen was gerechnet wird</CardTitle>
<CardDescription> <CardDescription>
Lesart: {KOSTENLESART_BESCHRIFTUNG[KOSTENLESART_STANDARD]}. Ohne diese Festlegung Lesart: {KOSTENLESART_BESCHRIFTUNG[KOSTENLESART_STANDARD]}. Ohne diese Festlegung
lässt sich die Rechnung nicht mit der Heimrechnung vergleichen. lässt sich die Rechnung nicht mit der Heimrechnung vergleichen.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 text-sm"> <CardContent className="space-y-4 text-sm">
<p>{KOSTENLESART_ERKLAERUNG[KOSTENLESART_STANDARD]}</p> <p>{KOSTENLESART_ERKLAERUNG[KOSTENLESART_STANDARD]}</p>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-md border p-3"> <div className="rounded-md border p-3">
<p className="font-semibold">Bedarf</p> <p className="font-semibold">Bedarf</p>
<ul className="mt-1 list-inside list-disc text-muted-foreground"> <ul className="mt-1 list-inside list-disc text-muted-foreground">
<li>einrichtungseinheitlicher Eigenanteil (bereits netto)</li> <li>einrichtungseinheitlicher Eigenanteil (bereits netto)</li>
<li>Unterkunft und Verpflegung</li> <li>Unterkunft und Verpflegung</li>
<li>Investitionskosten</li> <li>Investitionskosten</li>
<li>Ausbildungsumlage</li> <li>Ausbildungsumlage</li>
<li>Zusatzleistungen</li> <li>Zusatzleistungen</li>
<li>Barbetrag (§ 27b Abs. 2 SGB XII)</li> <li>Barbetrag (§ 27b Abs. 2 SGB XII)</li>
<li>Bekleidungspauschale</li> <li>Bekleidungspauschale</li>
</ul> </ul>
</div> </div>
<div className="rounded-md border p-3"> <div className="rounded-md border p-3">
<p className="font-semibold">Deckung</p> <p className="font-semibold">Deckung</p>
<ul className="mt-1 list-inside list-disc text-muted-foreground"> <ul className="mt-1 list-inside list-disc text-muted-foreground">
<li>Leistungszuschlag (§ 43c SGB XI)</li> <li>Leistungszuschlag (§ 43c SGB XI)</li>
<li>Rente</li> <li>Rente</li>
<li>sonstige Einkünfte</li> <li>sonstige Einkünfte</li>
<li>Anteil der Sozialhilfe</li> <li>Anteil der Sozialhilfe</li>
<li>Eigenmittel</li> <li>Eigenmittel</li>
</ul> </ul>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
<span className="font-medium">Nicht dabei:</span> die Pauschale der Pflegekasse <span className="font-medium">Nicht dabei:</span> die Pauschale der Pflegekasse
nach § 43 Abs. 2 SGB XI. Sie ist im Eigenanteil schon abgezogen und würde hier ein nach § 43 Abs. 2 SGB XI. Sie ist im Eigenanteil schon abgezogen und würde hier ein
zweites Mal zählen. zweites Mal zählen.
</p> </p>
</div> </div>
</div> </div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Barbetrag und Bekleidungspauschale stehen bewusst auf der Bedarfsseite: Sie müssen der Barbetrag und Bekleidungspauschale stehen bewusst auf der Bedarfsseite: Sie müssen der
betreuten Person tatsächlich verbleiben. Wer sie herauslässt, hält die Deckung für betreuten Person tatsächlich verbleiben. Wer sie herauslässt, hält die Deckung für
besser, als sie ist. besser, als sie ist.
</p> </p>
<Rechtshinweis rechtsgrundlage="§ 43 Abs. 2 SGB XI, § 43c SGB XI, § 27b SGB XII" kurz /> <Rechtshinweis rechtsgrundlage="§ 43 Abs. 2 SGB XI, § 43c SGB XI, § 27b SGB XII" kurz />
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Jahresverlauf</CardTitle> <CardTitle>Jahresverlauf</CardTitle>
<CardDescription> <CardDescription>
Gesamtbedarf ist die Heimrechnung plus Barbetrag und Bekleidungspauschale. Gesamtbedarf ist die Heimrechnung plus Barbetrag und Bekleidungspauschale.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<HeimkostenTabelle zeilen={zeilen} /> <HeimkostenTabelle zeilen={zeilen} />
</CardContent> </CardContent>
</Card> </Card>
{ergebnisse.some((e) => e.hinweise.length > 0) && ( {ergebnisse.some((e) => e.hinweise.length > 0) && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Was auffällt</CardTitle> <CardTitle>Was auffällt</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="space-y-3"> <ul className="space-y-3">
{ergebnisse.flatMap((e) => {ergebnisse.flatMap((e) =>
e.hinweise.map((hinweis, i) => ( e.hinweise.map((hinweis, i) => (
<li key={`${e.monat}-${i}`} className="text-sm"> <li key={`${e.monat}-${i}`} className="text-sm">
<span className="font-medium">{monatsname(e.monat)}: </span> <span className="font-medium">{monatsname(e.monat)}: </span>
{hinweis} {hinweis}
</li> </li>
)), )),
)} )}
</ul> </ul>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{ergebnisse.some((e) => e.abwesenheit.abwesenheitstage > 0) && ( {ergebnisse.some((e) => e.abwesenheit.abwesenheitstage > 0) && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Abwesenheitstage</CardTitle> <CardTitle>Abwesenheitstage</CardTitle>
<CardDescription> <CardDescription>
Was die Abwesenheit voraussichtlich bringt — eine Schätzung nach dem Rahmenvertrag, Was die Abwesenheit voraussichtlich bringt — eine Schätzung nach dem Rahmenvertrag,
nicht die Abrechnung. nicht die Abrechnung.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="space-y-3"> <ul className="space-y-3">
{ergebnisse {ergebnisse
.filter((e) => e.abwesenheit.abwesenheitstage > 0) .filter((e) => e.abwesenheit.abwesenheitstage > 0)
.map((e) => ( .map((e) => (
<li key={e.monat} className="text-sm"> <li key={e.monat} className="text-sm">
<span className="font-medium">{monatsname(e.monat)}: </span> <span className="font-medium">{monatsname(e.monat)}: </span>
{e.abwesenheit.erklaerung} Geschätzte Minderung{' '} {e.abwesenheit.erklaerung} Geschätzte Minderung{' '}
<span className="font-medium tabular-nums"> <span className="font-medium tabular-nums">
{formatCent(e.abwesenheit.geschaetzteMinderungCent)} {formatCent(e.abwesenheit.geschaetzteMinderungCent)}
</span> </span>
, die Lücke sänke damit auf{' '} , die Lücke sänke damit auf{' '}
<span className="font-medium tabular-nums"> <span className="font-medium tabular-nums">
{formatCent(e.deckungslueckeNachAbwesenheitCent)} {formatCent(e.deckungslueckeNachAbwesenheitCent)}
</span> </span>
. .
</li> </li>
))} ))}
</ul> </ul>
<Rechtshinweis className="mt-4" rechtsgrundlage="§ 87a Abs. 1 SGB XI, § 75 SGB XI" kurz /> <Rechtshinweis className="mt-4" rechtsgrundlage="§ 87a Abs. 1 SGB XI, § 75 SGB XI" kurz />
</CardContent> </CardContent>
</Card> </Card>
)} )}
<Alert variant="hinweis"> <Alert variant="hinweis">
<Info aria-hidden="true" /> <Info aria-hidden="true" />
<AlertTitle>Woher die Vorschlagswerte kommen</AlertTitle> <AlertTitle>Woher die Vorschlagswerte kommen</AlertTitle>
<AlertDescription className="space-y-1"> <AlertDescription className="space-y-1">
<p> <p>
Die hinterlegten Beträge haben den Stand {KONSTANTEN_STAND} und sind bis spätestens{' '} 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 {KONSTANTEN_NAECHSTE_PRUEFUNG} nachzuprüfen. Maßgeblich ist immer die Heimrechnung und
der Bescheid des Trägers. der Bescheid des Trägers.
</p> </p>
{verweildauer !== null && ( {verweildauer !== null && (
<p> <p>
Verweildauer im Heim: {verweildauer}{' '} Verweildauer im Heim: {verweildauer}{' '}
{verweildauer === 1 ? 'vollendeter Monat' : 'vollendete Monate'} — Leistungszuschlag{' '} {verweildauer === 1 ? 'vollendeter Monat' : 'vollendete Monate'} — Leistungszuschlag{' '}
{leistungszuschlagProzent(verweildauer)} % ({leistungszuschlagStufe(verweildauer)}, {leistungszuschlagProzent(verweildauer)} % ({leistungszuschlagStufe(verweildauer)},
§ 43c SGB XI). § 43c SGB XI).
</p> </p>
)} )}
<p>{BEKLEIDUNGSPAUSCHALE_HINWEIS}</p> <p>{BEKLEIDUNGSPAUSCHALE_HINWEIS}</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<section className="space-y-4"> <section className="space-y-4">
<h2 className="text-lg font-semibold">Monat erfassen oder ändern</h2> <h2 className="text-lg font-semibold">Monat erfassen oder ändern</h2>
<HeimkostenFormular <HeimkostenFormular
jahre={jahre} jahre={jahre}
hinweisPflegekasse={hinweisPflegekasse} hinweisPflegekasse={hinweisPflegekasse}
vorschlagBarbetrag={{ vorschlagBarbetrag={{
betrag: betragFormatieren(BARBETRAG_CENT), 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.`, 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={{ werte={{
jahr, jahr,
monat: naechsterOffenerMonat, monat: naechsterOffenerMonat,
pflegeEigenanteil: eingabefeld(heimvertrag?.pflegeEigenanteilCent ?? null), pflegeEigenanteil: eingabefeld(heimvertrag?.pflegeEigenanteilCent ?? null),
unterkunftVerpflegung: eingabefeld(heimvertrag?.unterkunftVerpflegungCent ?? null), unterkunftVerpflegung: eingabefeld(heimvertrag?.unterkunftVerpflegungCent ?? null),
investitionskosten: eingabefeld(heimvertrag?.investitionskostenCent ?? null), investitionskosten: eingabefeld(heimvertrag?.investitionskostenCent ?? null),
ausbildungsumlage: eingabefeld(heimvertrag?.ausbildungsumlageCent ?? null), ausbildungsumlage: eingabefeld(heimvertrag?.ausbildungsumlageCent ?? null),
zusatzleistungen: '', zusatzleistungen: '',
// Absichtlich leer: siehe hinweisPflegekasse. // Absichtlich leer: siehe hinweisPflegekasse.
pflegekasse: '', pflegekasse: '',
leistungszuschlagProzent: leistungszuschlagProzent:
verweildauer !== null ? leistungszuschlagProzent(verweildauer) : 0, verweildauer !== null ? leistungszuschlagProzent(verweildauer) : 0,
leistungszuschlag: '', leistungszuschlag: '',
rente: '', rente: '',
sonstigeEinkuenfte: '', sonstigeEinkuenfte: '',
sozialhilfe: '', sozialhilfe: '',
eigenmittel: '', eigenmittel: '',
barbetrag: betragFormatieren(BARBETRAG_CENT), barbetrag: betragFormatieren(BARBETRAG_CENT),
bekleidungspauschale: '', bekleidungspauschale: '',
abwesenheitstage: 0, abwesenheitstage: 0,
bemerkung: '', bemerkung: '',
}} }}
/> />
</section> </section>
<Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" /> <Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" />
</div> </div>
) )
} }
/** Cent fuer ein Eingabefeld, null wird zu leer. */ /** Cent fuer ein Eingabefeld, null wird zu leer. */
function eingabefeld(cent: bigint | null): string { function eingabefeld(cent: bigint | null): string {
return cent === null ? '' : betragFormatieren(cent) return cent === null ? '' : betragFormatieren(cent)
} }
+6
View File
@@ -3,6 +3,12 @@ import { redirect } from 'next/navigation'
/** /**
* Einstiegspunkt. Wer angemeldet ist, landet auf dem Tagesdashboard; * Einstiegspunkt. Wer angemeldet ist, landet auf dem Tagesdashboard;
* alles Weitere entscheidet der Anwendungsrahmen unter (app). * alles Weitere entscheidet der Anwendungsrahmen unter (app).
*
* Diese Weiterleitung liegt bewusst AUSSERHALB der Gruppe (app). Eine
* page.tsx innerhalb einer Routengruppe loest denselben Pfad "/" auf wie
* diese hier - zwei Dateien fuer eine Adresse. Next.js uebersetzt das
* zwar noch, erzeugt beim Standalone-Build aber ein unvollstaendiges
* Manifest, und genau dieses Verzeichnis kopiert das Docker-Abbild.
*/ */
export default function Startseite() { export default function Startseite() {
redirect('/heute') redirect('/heute')
@@ -15,7 +15,6 @@ import { NachforderungAbzeichen } from '@/components/sozialhilfe/abzeichen'
import { import {
LEERE_NACHFORDERUNG, LEERE_NACHFORDERUNG,
NachforderungFormular, NachforderungFormular,
type NachforderungWerte,
} from '@/components/sozialhilfe/nachforderung-formular' } from '@/components/sozialhilfe/nachforderung-formular'
import { fristAmpel, trackerFortschritt } from '@/lib/sozialhilfe/nachforderungen' import { fristAmpel, trackerFortschritt } from '@/lib/sozialhilfe/nachforderungen'
import { formatDatum } from '@/lib/utils' import { formatDatum } from '@/lib/utils'
-1
View File
@@ -33,7 +33,6 @@ export type KontaktZustand = {
werte?: Record<string, string> werte?: Record<string, string>
} }
const LEER: KontaktZustand = {}
function felderAusZod(fehler: { fieldErrors: Record<string, string[] | undefined> }) { function felderAusZod(fehler: { fieldErrors: Record<string, string[] | undefined> }) {
const felder: Record<string, string> = {} const felder: Record<string, string> = {}
-1
View File
@@ -51,7 +51,6 @@ export type Formularzustand = {
erfolg?: string erfolg?: string
} }
const LEER: Formularzustand = {}
function pfadeErneuern(...pfade: string[]): void { function pfadeErneuern(...pfade: string[]): void {
for (const pfad of pfade) revalidatePath(pfad) for (const pfad of pfade) revalidatePath(pfad)