Files
betreuungs-cockpit/src/app/(app)/sozialhilfe/vermoegen/page.tsx
T
Florian Wilde 1ed19eb76e Zeilenenden der 24 verbliebenen Dateien normalisieren
Ausschliesslich Zeilenenden. Nachgewiesen mit
"git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich
kein Zeichen Inhalt geaendert.

Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit
echten Aenderungen zu verstecken, macht die Historie an dieser Stelle
unlesbar. So laesst er sich beim Durchsehen ueberspringen.

Refs #26
2026-08-23 23:37:02 +02:00

233 lines
8.5 KiB
TypeScript

import { SeitenKopf } from '@/components/seiten-kopf'
import { Rechtshinweis } from '@/components/rechtshinweis'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Card, CardContent } from '@/components/ui/card'
import { Vermoegensliste, type ListenPosten } from '@/components/sozialhilfe/vermoegensliste'
import { oeffnen } from '@/lib/crypto/feld'
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
import { alsIsoTag } from '@/lib/fristen/datum'
import { VERMOEGENS_ART_BESCHRIFTUNG } from '@/lib/finanzen/kategorien'
import { MIT_RUECKKAUFSWERT, VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
import { BEWERTUNG_BESCHRIFTUNG } from '@/lib/sozialhilfe/schonvermoegen'
import { freibetragVergleichen, uebersichtBilden } from '@/lib/sozialhilfe/vermoegensuebersicht'
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
import { formatCent } from '@/lib/utils'
import { AlertCircle } from 'lucide-react'
export const dynamic = 'force-dynamic'
/**
* 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.
* Deshalb heißt die Spalte "Einschätzung" und nicht "Bewertung".
*/
export default async function VermoegensbewertungSeite() {
const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
const [posten, vertraege] = await Promise.all([
db.vermoegensposten.findMany({
where: { betreuteId, ...nichtGeloescht },
include: { wertstaende: { orderBy: { stichtag: 'desc' }, take: 1 } },
orderBy: { bezeichnung: 'asc' },
}),
db.vertrag.findMany({
where: {
betreuteId,
// Nur Vertraege, die ueberhaupt einen Wert darstellen koennen. Ein
// Stromvertrag hat keinen Rueckkaufswert und wuerde die Liste zumuellen.
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),
)
const zeilen: ListenPosten[] = [
...posten.map((p) => {
const geoeffnet = oeffnen('Vermoegensposten', p)
const stand = p.wertstaende[0]
return {
id: p.id,
bezeichnung: p.bezeichnung,
herkunft: 'VERMOEGEN' as const,
art: p.art,
artBeschriftung: VERMOEGENS_ART_BESCHRIFTUNG[p.art],
bewertung: p.bewertungSozialhilfe,
wertCent: stand ? centZuZahl(stand.wertCent) : null,
wertStichtag: stand ? alsIsoTag(stand.stichtag) : null,
begruendung: geoeffnet.bewertungBegruendung ?? '',
zusatz: stand ? stand.wertquelle : null,
}
}),
...vertraege
.filter((v) => !bereitsAlsPosten.has(v.id))
.map((v) => {
const geoeffnet = oeffnen('Vertrag', v)
return {
id: v.id,
bezeichnung: v.bezeichnung,
herkunft: 'VERTRAG' as const,
art: v.art,
artBeschriftung: VERTRAG_ART_BESCHRIFTUNG[v.art],
bewertung: v.bewertungSozialhilfe,
// Der Rückkaufswert steht am Vertrag nicht als Betrag; er wird
// als Vermögensposten mit Wertstand geführt. Bis dahin: offen.
wertCent: null,
wertStichtag: null,
begruendung: geoeffnet.bewertungBegruendung ?? '',
zusatz: v.gesellschaft,
}
}),
]
const uebersicht = uebersichtBilden(
zeilen.map((z) => ({
id: z.id,
bezeichnung: z.bezeichnung,
herkunft: z.herkunft,
artBeschriftung: z.artBeschriftung,
bewertung: z.bewertung,
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">
<SeitenKopf
titel="Vermögen bewerten"
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."
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}
anzahl={uebersicht.jeBewertung.VERWERTBAR.anzahl}
ohneWert={uebersicht.jeBewertung.VERWERTBAR.ohneWert}
farbe="text-ampel-rot"
/>
<Summenkachel
titel={BEWERTUNG_BESCHRIFTUNG.GESCHUETZT}
betrag={uebersicht.jeBewertung.GESCHUETZT.summeCent}
anzahl={uebersicht.jeBewertung.GESCHUETZT.anzahl}
ohneWert={uebersicht.jeBewertung.GESCHUETZT.ohneWert}
farbe="text-ampel-gruen"
/>
<Summenkachel
titel={BEWERTUNG_BESCHRIFTUNG.STRITTIG}
betrag={uebersicht.jeBewertung.STRITTIG.summeCent}
anzahl={uebersicht.jeBewertung.STRITTIG.anzahl}
ohneWert={uebersicht.jeBewertung.STRITTIG.ohneWert}
farbe="text-ampel-orange"
/>
<Summenkachel
titel={BEWERTUNG_BESCHRIFTUNG.UNGEPRUEFT}
betrag={uebersicht.jeBewertung.UNGEPRUEFT.summeCent}
anzahl={uebersicht.jeBewertung.UNGEPRUEFT.anzahl}
ohneWert={uebersicht.jeBewertung.UNGEPRUEFT.ohneWert}
farbe="text-muted-foreground"
/>
</div>
<Card>
<CardContent className="space-y-2 pt-5">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<span className="text-sm text-muted-foreground">
Vermögensfreibetrag (§ 90 Abs. 2 Nr. 9 SGB XII)
</span>
<span className="text-lg font-semibold tabular-nums">
{formatCent(vergleich.freibetragCent)}
</span>
</div>
<div className="flex flex-wrap items-baseline justify-between gap-3">
<span className="text-sm text-muted-foreground">Über dem Freibetrag</span>
<span
className={`text-lg font-semibold tabular-nums ${
vergleich.ausgeschoepft ? 'text-ampel-rot' : 'text-ampel-gruen'
}`}
>
{formatCent(vergleich.ueberFreibetragCent)}
</span>
</div>
<p className="text-sm text-muted-foreground">{vergleich.hinweis}</p>
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII" kurz />
</CardContent>
</Card>
{uebersicht.ungeprueft.length > 0 && (
<Alert variant="warnung">
<AlertCircle aria-hidden="true" />
<AlertTitle>
{uebersicht.ungeprueft.length}{' '}
{uebersicht.ungeprueft.length === 1 ? 'Posten ist' : 'Posten sind'} noch ungeprüft
</AlertTitle>
<AlertDescription>
Vor dem Antrag sollte hier nichts mehr stehen — der Träger fragt sonst nach, und das
kostet Wochen.
</AlertDescription>
</Alert>
)}
{uebersicht.ohneBegruendung.length > 0 && (
<Alert variant="warnung">
<AlertCircle aria-hidden="true" />
<AlertTitle>Bewertung ohne Begründung</AlertTitle>
<AlertDescription>
{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>
</Alert>
)}
<Vermoegensliste posten={zeilen} darfSchreiben={darfSchreiben} />
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII, § 91 SGB XII" />
</div>
)
}
function Summenkachel({
titel,
betrag,
anzahl,
ohneWert,
farbe,
}: {
titel: string
betrag: bigint
anzahl: number
ohneWert: number
farbe: string
}) {
return (
<Card>
<CardContent className="pt-5">
<p className="text-sm text-muted-foreground">{titel}</p>
<p className={`text-xl font-semibold tabular-nums ${farbe}`}>{formatCent(betrag)}</p>
<p className="mt-1 text-xs text-muted-foreground">
{anzahl} {anzahl === 1 ? 'Posten' : 'Posten'}
{ohneWert > 0 && `, davon ${ohneWert} ohne Wert`}
</p>
</CardContent>
</Card>
)
}