feat: Immobilie, Versicherungen und Heim mit COPD-Verlauf

Etappe 6. Das leerstehende Haus, die Vertraege und der
Gesundheitsverlauf - die drei Bereiche, in denen fuer diesen Fall das
meiste Geld und das meiste Risiko liegt.

Leerstands-Checkliste mit zehn Punkten, jeder mit Begruendung. Der
wichtigste: den Gebaeudeversicherer ueber den Leerstand informieren -
sonst droht Leistungsfreiheit, und zwar genau dann, wenn es brennt oder
eine Leitung platzt. Dazu Verkehrssicherungspflicht, Frostschutz zum
Oktober, Kontrollgaenge, Zaehlerstaende, Schluesselverzeichnis.

Der Kuendigungsrechner nennt den spaetesten ABSENDETAG, nicht den
Fristablauf: eine Kuendigung wirkt mit dem Zugang, nicht mit dem
Abschicken. Drei Tage Postlaufpuffer sind einkalkuliert.

Der Kuendigungs-Assistent beantwortet die Genehmigungsfrage nach
§ 1850 BGB nicht - er stellt sie und verlangt den Gang zum
Rechtspfleger. Der Versandnachweis ist Pflichtfeld: ohne ihn kommt die
Kuendigung nicht durch die Pruefung.

Leistungszuschlag nach § 43c SGB XI mit Stand 01.01.2024
(15/30/50/75 Prozent nach Verweildauer, erst ab Pflegegrad 2). Die
Oberflaeche zeigt, ab wann die naechste Stufe greift - das ist bares
Geld und wird regelmaessig uebersehen.

COPD-Schnellerfassung fuer Exazerbation, Sauerstoff, Sturz,
Krankenhaus, Hilfsmittel und Impfungen, mit STIKO-Plan fuer COPD.
Beschreibungen verschluesselt. Die Jahresauswertung ueber Exazerbationen
und Krankenhaustage wandert in den Jahresbericht und begruendet eine
Hoeherstufung.

74 Tests.

Etappe: 6
Phase: Substanz
This commit is contained in:
Florian Wilde
2026-08-22 06:22:24 +02:00
parent d35e24f830
commit b69a12da8f
28 changed files with 4309 additions and 917 deletions
+266
View File
@@ -0,0 +1,266 @@
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { FileX2, Pencil, Shield } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { SeitenKopf } from '@/components/seiten-kopf'
import { Datenzeile } from '@/components/substanz-formular'
import { Rechtshinweis } from '@/components/rechtshinweis'
import { Rueckkaufswert } from '@/components/vertraege/rueckkaufswert'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
import {
kuendigungsampel,
kuendigungsplan,
laufzeitAusVertrag,
} from '@/lib/vertraege/kuendigung'
import { anfrageStatus, rueckkaufswertAnschreiben } from '@/lib/vertraege/rueckkaufswert'
import {
BEWERTUNG_BESCHRIFTUNG,
BEWERTUNG_ERKLAERUNG,
jahresbeitragCent,
kannRueckkaufswertHaben,
RHYTHMUS_BESCHRIFTUNG,
VERTRAG_ART_BESCHRIFTUNG,
VERTRAG_STATUS_BESCHRIFTUNG,
} from '@/lib/vertraege/konstanten'
import { formatCent, formatDatum } from '@/lib/utils'
export const dynamic = 'force-dynamic'
export default async function VertragDetail({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const benutzer = await erfordereBenutzer(`/vertraege/${id}`)
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
const schreiben = darfSchreiben('VERTRAEGE')
const vertrag = await db.vertrag.findFirst({
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
include: {
vermoegensposten: {
where: { art: 'VERSICHERUNG', ...nichtGeloescht },
include: { wertstaende: { orderBy: { stichtag: 'desc' } } },
},
},
})
if (!vertrag) notFound()
const offen = oeffnen('Vertrag', vertrag)
const heute = heuteAlsTag()
const plan = kuendigungsplan({
beginnAm: vertrag.beginnAm,
endeAm: vertrag.endeAm,
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
heute,
})
const ampel = kuendigungsampel(plan)
const anfrage = anfrageStatus(vertrag.rueckkaufswertAngefragtAm, heute)
const jahresbeitrag = jahresbeitragCent(vertrag.beitragCent, vertrag.rhythmus)
const kopf = briefkopfBauen({ betreute: aktive, benutzer })
const anschreiben = rueckkaufswertAnschreiben({
kopf,
empfaenger: { gesellschaft: vertrag.gesellschaft, anschrift: null },
vertragsbezeichnung: `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}`,
vertragsnummer: offen.vertragsnummer,
stichtag: heute,
heute,
})
const wertstaende = vertrag.vermoegensposten[0]?.wertstaende ?? []
return (
<div className="space-y-6">
<SeitenKopf
titel={vertrag.bezeichnung}
beschreibung={
[vertrag.gesellschaft, VERTRAG_ART_BESCHRIFTUNG[vertrag.art]]
.filter(Boolean)
.join(' · ') || VERTRAG_ART_BESCHRIFTUNG[vertrag.art]
}
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Verträge', href: '/vertraege' },
{ titel: vertrag.bezeichnung },
]}
aktionen={
schreiben && (
<div className="flex flex-wrap gap-2">
<Button asChild variant="outline">
<Link href={`/vertraege/${id}/kuendigen`}>
<FileX2 aria-hidden="true" />
Kündigen
</Link>
</Button>
<Button asChild>
<Link href={`/vertraege/${id}/bearbeiten`}>
<Pencil aria-hidden="true" />
Bearbeiten
</Link>
</Button>
</div>
)
}
/>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="size-5" aria-hidden="true" />
Vertragsdaten
</CardTitle>
<CardDescription>
Die Vertragsnummer wird verschlüsselt gespeichert und hier nur zum Lesen angezeigt.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-x-8 sm:grid-cols-2">
<dl className="divide-y">
<Datenzeile beschriftung="Vertragsnummer">
{offen.vertragsnummer ?? '–'}
</Datenzeile>
<Datenzeile beschriftung="Status">
{VERTRAG_STATUS_BESCHRIFTUNG[vertrag.status]}
</Datenzeile>
<Datenzeile beschriftung="Laufzeitbeginn">
{formatDatum(vertrag.beginnAm)}
</Datenzeile>
<Datenzeile beschriftung="Vereinbartes Ende">
{formatDatum(vertrag.endeAm)}
</Datenzeile>
<Datenzeile beschriftung="Kündigungsfrist">
{vertrag.kuendigungsfristMonate !== null
? `${vertrag.kuendigungsfristMonate} Monate`
: 'nicht erfasst'}
</Datenzeile>
</dl>
<dl className="divide-y">
<Datenzeile beschriftung="Beitrag">
{formatCent(vertrag.beitragCent)}
{vertrag.rhythmus ? ` (${RHYTHMUS_BESCHRIFTUNG[vertrag.rhythmus]})` : ''}
</Datenzeile>
<Datenzeile beschriftung="Jahresbeitrag">{formatCent(jahresbeitrag)}</Datenzeile>
<Datenzeile beschriftung="Gekündigt am">
{formatDatum(vertrag.gekuendigtAm)}
</Datenzeile>
<Datenzeile beschriftung="Gekündigt zum">
{formatDatum(vertrag.kuendigungZum)}
</Datenzeile>
<Datenzeile beschriftung="Rückkaufswert angefragt">
{formatDatum(vertrag.rueckkaufswertAngefragtAm)}
</Datenzeile>
</dl>
{offen.notizen && (
<div className="mt-4 rounded-md border bg-muted/40 p-3 sm:col-span-2">
<p className="whitespace-pre-line text-sm">{offen.notizen}</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Kündigungsfrist</CardTitle>
<CardDescription>
Eine Kündigung wird mit dem Zugang wirksam, nicht mit dem Absenden. Deshalb ist der
Absendetag früher als der Fristablauf.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{plan.berechenbar ? (
<>
<dl className="grid gap-3 sm:grid-cols-3">
<div className="rounded-md border p-3">
<dt className="text-sm text-muted-foreground">Nächster Kündigungstermin</dt>
<dd className="text-lg font-semibold">{formatDatum(plan.naechsterTermin)}</dd>
</div>
<div className="rounded-md border p-3">
<dt className="text-sm text-muted-foreground">Zugang spätestens</dt>
<dd className="text-lg font-semibold">
{formatDatum(plan.zugangSpaetestensAm)}
</dd>
</div>
<div className="rounded-md border p-3">
<dt className="text-sm text-muted-foreground">Brief spätestens absenden</dt>
<dd className="text-lg font-semibold">
{formatDatum(plan.spaetesterAbsendetag)}
</dd>
{ampel && (
<dd className="mt-1">
<Badge variant={ampel.variante}>{ampel.text}</Badge>
</dd>
)}
</div>
</dl>
<ul className="space-y-1.5 text-sm text-muted-foreground">
{plan.hinweise.map((hinweis) => (
<li key={hinweis}>{hinweis}</li>
))}
</ul>
</>
) : (
<p className="text-sm text-muted-foreground">
{plan.hinweise[0] ??
'Ohne Laufzeitbeginn lässt sich kein Kündigungstermin berechnen.'}
</p>
)}
</CardContent>
</Card>
{kannRueckkaufswertHaben(vertrag.art) && (
<Rueckkaufswert
betreuteId={aktive.id}
vertragId={id}
darfSchreiben={schreiben}
angefragtAm={vertrag.rueckkaufswertAngefragtAm?.toISOString() ?? null}
erinnerungFaellig={anfrage.erinnerungFaellig}
tageSeitAnfrage={anfrage.tageSeitAnfrage}
anschreiben={anschreiben}
stichtagVorschlag={alsIsoTag(heute)}
werte={wertstaende.map((w) => ({
id: w.id,
stichtag: w.stichtag.toISOString(),
wertCent: centZuZahl(w.wertCent) ?? 0,
wertquelle: w.wertquelle,
bemerkung: w.bemerkung,
}))}
/>
)}
<Card>
<CardHeader>
<CardTitle>Bewertung fürs Sozialamt</CardTitle>
<CardDescription>
Meine Einordnung und meine Begründung — keine Rechtsauskunft.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Badge variant="outline">{BEWERTUNG_BESCHRIFTUNG[vertrag.bewertungSozialhilfe]}</Badge>
<p className="text-sm text-muted-foreground">
{BEWERTUNG_ERKLAERUNG[vertrag.bewertungSozialhilfe]}
</p>
{offen.bewertungBegruendung ? (
<p className="whitespace-pre-line rounded-md border bg-muted/40 p-3 text-sm">
{offen.bewertungBegruendung}
</p>
) : (
<p className="text-sm text-muted-foreground">
Noch keine Begründung hinterlegt. Sie gehört dazu, bevor der Vertrag in einer
Aufstellung an den Bezirk auftaucht.
</p>
)}
<Rechtshinweis rechtsgrundlage="§ 90 Abs. 3 SGB XII" kurz />
</CardContent>
</Card>
</div>
)
}