docs: README, Datenschutztexte, Dockerfile und Sicherungsskripte
Damit ist Etappe 1 abgeschlossen und betriebsfertig dokumentiert. README mit beiden Betriebswegen. Docker Compose auf eigenem Server ist der Hauptweg; Vercel und Neon sind vollstaendig beschrieben, aber mit einer klaren Begruendung, warum ich davon abrate: beide Anbieter sind US-Unternehmen und unterliegen auch bei EU-Region potenziell dem CLOUD-Act. Bei Gesundheitsdaten einer nicht entscheidungsfaehigen Person ist das eine Abwaegung, die man bewusst treffen muss. Reverse-Proxy-Beispiele fuer Caddy, nginx und Traefik - der Proxy selbst ist nicht Teil des Compose-Setups, weil er auf dem Zielserver schon steht. Die Datenbank hat keine Portfreigabe. Sicherung: pg_dump schreibt direkt in die Verschluesselung, der Klartext landet nie auf der Platte. Danach eine Selbstpruefung - laesst sich die Datei nicht wieder entschluesseln, endet der Lauf mit Fehler statt mit einem stillen Scheinerfolg. Der jeweils erste Stand eines Monats bleibt dauerhaft stehen. Die README sagt ausdruecklich, was das Backup NICHT enthaelt: den Schluessel ENC_KEY_V1 und die Dateien im Objektspeicher. Dazu eine Anleitung, die Wiederherstellung einmal im Jahr zu ueben - mit dem letzten Schritt, in der Anwendung nachzusehen, ob die Diagnosen im Klartext erscheinen. Erst dann stimmt nicht nur die Datenbank, sondern auch der Schluessel. Verarbeitungsverzeichnis und Datenschutzhinweise als ausfuellbare Textbausteine. Die Hinweise sind in einfacher Sprache geschrieben - sie sollen der betreuten Person vorgelesen werden koennen. Die Seite /datenschutz liest denselben Text aus docs/, damit es nicht zwei Fassungen gibt, die auseinanderlaufen. Dockerfile in drei Stufen mit Tesseract, deutschen Sprachdaten und poppler-utils. Zeitzone Europe/Berlin ist gesetzt - ohne sie rechnet der Server in UTC und verschiebt Fristen um einen Tag. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import Link from 'next/link'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { HeimkostenFormular } from '@/components/sozialhilfe/heimkosten-formular'
|
||||
import { HeimkostenTabelle, type MonatsZeile } from '@/components/sozialhilfe/heimkosten-tabelle'
|
||||
import { centZuEingabe } from '@/lib/sozialhilfe/betrag'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import {
|
||||
BARBETRAG_CENT,
|
||||
BEKLEIDUNGSPAUSCHALE_HINWEIS,
|
||||
KONSTANTEN_NAECHSTE_PRUEFUNG,
|
||||
KONSTANTEN_STAND,
|
||||
} from '@/lib/sozialhilfe/konstanten'
|
||||
import {
|
||||
heimkostenAuswerten,
|
||||
jahresverlaufBilden,
|
||||
leistungszuschlagProzent,
|
||||
leistungszuschlagStufe,
|
||||
monatsname,
|
||||
pflegekassePauschaleCent,
|
||||
pflegekasseRechtsgrundlage,
|
||||
verweildauerMonate,
|
||||
} from '@/lib/sozialhilfe/heimkosten'
|
||||
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
|
||||
import { formatCent } from '@/lib/utils'
|
||||
import { Info, TrendingDown } from 'lucide-react'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Heimkosten-Gegenüberstellung.
|
||||
*
|
||||
* Die eine Zahl, um die es geht, ist die Deckungslücke: Was bleibt übrig,
|
||||
* nachdem Pflegekasse, Rente und Sozialhilfe gezahlt haben? Sie steht je
|
||||
* Monat und als Jahressumme da - denn eine Lücke von 180 Euro im Monat
|
||||
* sind über ein Jahr mehr als zweitausend.
|
||||
*/
|
||||
export default async function HeimkostenSeite({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ jahr?: string }>
|
||||
}) {
|
||||
const { betreuteId, betreute } = await sozialhilfeKontext()
|
||||
const parameter = await searchParams
|
||||
|
||||
const heute = heuteAlsTag()
|
||||
const gewaehltesJahr = Number.parseInt(parameter.jahr ?? '', 10)
|
||||
const jahr = Number.isFinite(gewaehltesJahr) ? gewaehltesJahr : heute.getUTCFullYear()
|
||||
|
||||
const [monate, heimvertrag] = await Promise.all([
|
||||
db.heimkostenmonat.findMany({
|
||||
where: { betreuteId, jahr },
|
||||
orderBy: { monat: 'asc' },
|
||||
}),
|
||||
db.heimvertrag.findUnique({ where: { betreuteId } }),
|
||||
])
|
||||
|
||||
const ergebnisse = monate.map((m) =>
|
||||
heimkostenAuswerten({
|
||||
jahr: m.jahr,
|
||||
monat: m.monat,
|
||||
pflegeEigenanteilCent: m.pflegeEigenanteilCent,
|
||||
unterkunftVerpflegungCent: m.unterkunftVerpflegungCent,
|
||||
investitionskostenCent: m.investitionskostenCent,
|
||||
ausbildungsumlageCent: m.ausbildungsumlageCent,
|
||||
zusatzleistungenCent: m.zusatzleistungenCent,
|
||||
pflegekasseCent: m.pflegekasseCent,
|
||||
leistungszuschlagCent: m.leistungszuschlagCent,
|
||||
renteCent: m.renteCent,
|
||||
sonstigeEinkuenfteCent: m.sonstigeEinkuenfteCent,
|
||||
sozialhilfeCent: m.sozialhilfeCent,
|
||||
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 ergebnis = ergebnisse.find((e) => e.monat === monat)
|
||||
const roh = monate.find((m) => m.monat === monat)
|
||||
return {
|
||||
jahr,
|
||||
monat,
|
||||
heimkostenCent: ergebnis?.heimkostenCent ?? 0n,
|
||||
gesamtbedarfCent: ergebnis?.gesamtbedarfCent ?? 0n,
|
||||
deckungCent: ergebnis?.deckungCent ?? 0n,
|
||||
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
|
||||
? {
|
||||
betrag: centZuEingabe(pauschale),
|
||||
text: `Pflegegrad ${betreute.pflegegrad}: ${formatCent(pauschale)} monatlich (${pflegekasseRechtsgrundlage(betreute.pflegegrad)}, Stand ${KONSTANTEN_STAND}).`,
|
||||
}
|
||||
: null
|
||||
|
||||
const einzug = heimvertrag?.einzugAm ?? betreute.heimEinzugAm ?? null
|
||||
const verweildauer = einzug ? verweildauerMonate(einzug, heute) : null
|
||||
|
||||
const naechsterOffenerMonat =
|
||||
zeilen.find((z) => !z.erfasst && z.monat <= (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 12))
|
||||
?.monat ?? (jahr === heute.getUTCFullYear() ? heute.getUTCMonth() + 1 : 1)
|
||||
|
||||
const jahre = Array.from({ length: 6 }, (_, i) => heute.getUTCFullYear() - 3 + i)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SeitenKopf
|
||||
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."
|
||||
pfad={[
|
||||
{ titel: 'Start', href: '/heute' },
|
||||
{ titel: 'Sozialhilfe', href: '/sozialhilfe' },
|
||||
{ titel: 'Heimkosten' },
|
||||
]}
|
||||
aktionen={
|
||||
<nav aria-label="Jahr wählen" className="flex flex-wrap gap-1">
|
||||
{jahre.map((j) => (
|
||||
<Link
|
||||
key={j}
|
||||
href={`/sozialhilfe/heimkosten?jahr=${j}`}
|
||||
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 ${
|
||||
j === jahr
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'border border-input hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{j}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Card>
|
||||
<CardContent className="pt-5">
|
||||
<p className="text-sm text-muted-foreground">Bedarf {jahr}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">
|
||||
{formatCent(verlauf.summeBedarfCent)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-5">
|
||||
<p className="text-sm text-muted-foreground">Deckung {jahr}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">
|
||||
{formatCent(verlauf.summeDeckungCent)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className={verlauf.summeDeckungslueckeCent > 0n ? 'border-ampel-rot/40' : undefined}>
|
||||
<CardContent className="pt-5">
|
||||
<p className="text-sm text-muted-foreground">Deckungslücke {jahr}</p>
|
||||
<p
|
||||
className={`text-xl font-semibold tabular-nums ${
|
||||
verlauf.summeDeckungslueckeCent > 0n ? 'text-ampel-rot' : 'text-ampel-gruen'
|
||||
}`}
|
||||
>
|
||||
{formatCent(verlauf.summeDeckungslueckeCent)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{verlauf.groessteLuecke && (
|
||||
<Alert variant="warnung">
|
||||
<TrendingDown aria-hidden="true" />
|
||||
<AlertTitle>
|
||||
Größte Lücke im {monatsname(verlauf.groessteLuecke.monat)}:{' '}
|
||||
{formatCent(verlauf.groessteLuecke.deckungslueckeCent)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
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
|
||||
für einen Widerspruch.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Jahresverlauf</CardTitle>
|
||||
<CardDescription>
|
||||
Gesamtbedarf ist die Heimrechnung plus Barbetrag und Bekleidungspauschale.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<HeimkostenTabelle zeilen={zeilen} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{ergebnisse.some((e) => e.hinweise.length > 0) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Was auffällt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-3">
|
||||
{ergebnisse.flatMap((e) =>
|
||||
e.hinweise.map((hinweis, i) => (
|
||||
<li key={`${e.monat}-${i}`} className="text-sm">
|
||||
<span className="font-medium">{monatsname(e.monat)}: </span>
|
||||
{hinweis}
|
||||
</li>
|
||||
)),
|
||||
)}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{ergebnisse.some((e) => e.abwesenheit.abwesenheitstage > 0) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Abwesenheitstage</CardTitle>
|
||||
<CardDescription>
|
||||
Was die Abwesenheit voraussichtlich bringt — eine Schätzung nach dem Rahmenvertrag,
|
||||
nicht die Abrechnung.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-3">
|
||||
{ergebnisse
|
||||
.filter((e) => e.abwesenheit.abwesenheitstage > 0)
|
||||
.map((e) => (
|
||||
<li key={e.monat} className="text-sm">
|
||||
<span className="font-medium">{monatsname(e.monat)}: </span>
|
||||
{e.abwesenheit.erklaerung} Geschätzte Minderung{' '}
|
||||
<span className="font-medium tabular-nums">
|
||||
{formatCent(e.abwesenheit.geschaetzteMinderungCent)}
|
||||
</span>
|
||||
, die Lücke sänke damit auf{' '}
|
||||
<span className="font-medium tabular-nums">
|
||||
{formatCent(e.deckungslueckeNachAbwesenheitCent)}
|
||||
</span>
|
||||
.
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Rechtshinweis className="mt-4" rechtsgrundlage="§ 87a Abs. 1 SGB XI, § 75 SGB XI" kurz />
|
||||
</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}
|
||||
vorschlagPflegekasse={vorschlagPflegekasse}
|
||||
vorschlagBarbetrag={{
|
||||
betrag: centZuEingabe(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: centZuEingabe(heimvertrag?.pflegeEigenanteilCent ?? null),
|
||||
unterkunftVerpflegung: centZuEingabe(heimvertrag?.unterkunftVerpflegungCent ?? null),
|
||||
investitionskosten: centZuEingabe(heimvertrag?.investitionskostenCent ?? null),
|
||||
ausbildungsumlage: centZuEingabe(heimvertrag?.ausbildungsumlageCent ?? null),
|
||||
zusatzleistungen: '',
|
||||
pflegekasse: pauschale ? centZuEingabe(pauschale) : '',
|
||||
leistungszuschlagProzent:
|
||||
verweildauer !== null ? leistungszuschlagProzent(verweildauer) : 0,
|
||||
leistungszuschlag: '',
|
||||
rente: '',
|
||||
sonstigeEinkuenfte: '',
|
||||
sozialhilfe: '',
|
||||
eigenmittel: '',
|
||||
barbetrag: centZuEingabe(BARBETRAG_CENT),
|
||||
bekleidungspauschale: '',
|
||||
abwesenheitstage: 0,
|
||||
bemerkung: '',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§ 43 SGB XI, § 43c SGB XI, § 27b SGB XII" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user