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
+1 -1
View File
@@ -195,7 +195,7 @@ export default async function ImmobilieDetail({ params }: { params: Promise<{ id
immobilieId={id}
darfSchreiben={schreiben}
status={immobilie.statusSozialhilfe}
begruendung={offen.statusBegruendung ?? null}
begruendung={offen.statusBegruendung}
/>
<Kostenkonto
+87
View File
@@ -0,0 +1,87 @@
import { redirect } from 'next/navigation'
import { SeitenKopf } from '@/components/seiten-kopf'
import { Rechtshinweis } from '@/components/rechtshinweis'
import {
HeimvertragFormular,
LEERER_HEIMVERTRAG,
} from '@/components/pflege/heimvertrag-formular'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { db } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { alsIsoTag } from '@/lib/fristen/datum'
export const dynamic = 'force-dynamic'
/** Cent als Eingabetext: 145000 wird zu "1450,00". */
function centAlsText(cent: bigint | null): string {
if (cent === null) return ''
const negativ = cent < 0n
const betrag = negativ ? -cent : cent
return `${negativ ? '-' : ''}${betrag / 100n},${(betrag % 100n).toString().padStart(2, '0')}`
}
export default async function HeimvertragSeite() {
const benutzer = await erfordereBenutzer('/pflege/heimvertrag')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'GESUNDHEIT', 'lesen')
if (!darfSchreiben('GESUNDHEIT')) redirect('/pflege')
const heimvertrag = await db.heimvertrag.findUnique({ where: { betreuteId: aktive.id } })
const offen = heimvertrag ? oeffnen('Heimvertrag', heimvertrag) : null
return (
<div className="space-y-6">
<SeitenKopf
titel={heimvertrag ? 'Heimvertrag bearbeiten' : 'Heimvertrag erfassen'}
beschreibung="Die Sätze stehen im Vertrag und auf jeder Heimrechnung. Ändern sie sich, trag sie hier nach — die alten Werte bleiben in der monatlichen Gegenüberstellung erhalten."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Heim & Pflege', href: '/pflege' },
{ titel: 'Heimvertrag' },
]}
/>
<HeimvertragFormular
betreuteId={aktive.id}
werte={
heimvertrag
? {
einrichtung: heimvertrag.einrichtung,
traeger: heimvertrag.traeger ?? '',
strasse: heimvertrag.strasse ?? '',
plz: heimvertrag.plz ?? '',
ort: heimvertrag.ort ?? '',
vertragAm: heimvertrag.vertragAm ? alsIsoTag(heimvertrag.vertragAm) : '',
einzugAm: heimvertrag.einzugAm ? alsIsoTag(heimvertrag.einzugAm) : '',
auszugAm: heimvertrag.auszugAm ? alsIsoTag(heimvertrag.auszugAm) : '',
kuendigungsfristTage: heimvertrag.kuendigungsfristTage?.toString() ?? '',
pflegeEigenanteil: centAlsText(heimvertrag.pflegeEigenanteilCent),
unterkunftVerpflegung: centAlsText(heimvertrag.unterkunftVerpflegungCent),
investitionskosten: centAlsText(heimvertrag.investitionskostenCent),
ausbildungsumlage: centAlsText(heimvertrag.ausbildungsumlageCent),
notizen: offen?.notizen ?? '',
}
: {
...LEERER_HEIMVERTRAG,
einrichtung: aktive.heimName ?? '',
strasse: aktive.heimStrasse ?? '',
plz: aktive.heimPlz ?? '',
ort: aktive.heimOrt ?? '',
einzugAm: aktive.heimEinzugAm ? alsIsoTag(aktive.heimEinzugAm) : '',
}
}
/>
<Rechtshinweis rechtsgrundlage="§ 43c SGB XI, WBVG" />
<p className="text-sm text-muted-foreground">
Kurz übersetzt: Der pflegebedingte Eigenanteil ist der einzige Posten, auf den der
Leistungszuschlag nach § 43c SGB XI wirkt. Das Wohn- und Betreuungsvertragsgesetz
(WBVG) regelt, wie und wann gekündigt werden kann und wann die Einrichtung Entgelte
erhöhen darf — eine Erhöhung muss schriftlich angekündigt und begründet werden. Prüf
jede Ankündigung, statt sie stillschweigend hinzunehmen.
</p>
</div>
)
}
+252
View File
@@ -0,0 +1,252 @@
import Link from 'next/link'
import { Building, HeartPulse, Pencil, ScrollText } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Leerzustand, SeitenKopf } from '@/components/seiten-kopf'
import { Datenzeile } from '@/components/substanz-formular'
import { AbwesenheitKarte } from '@/components/pflege/abwesenheit-karte'
import { Impfstatus } from '@/components/pflege/impfstatus'
import { LeistungszuschlagKarte } from '@/components/pflege/leistungszuschlag-karte'
import { PflegegradKarte } from '@/components/pflege/pflegegrad-karte'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { alsIsoTag, heuteAlsTag, plusMonate } from '@/lib/fristen/datum'
import { abwesenheitstageGesamt, abwesenheitstageJeJahr } from '@/lib/pflege/abwesenheit'
import { impfstatusBerechnen } from '@/lib/pflege/impfungen'
import { leistungszuschlagBerechnen } from '@/lib/pflege/leistungszuschlag'
import { HOEHERSTUFUNG_ABSTAND_MONATE } from '@/lib/pflege/konstanten'
import { verlaufAuswerten, verschlechterungErkannt } from '@/lib/pflege/verlauf'
import { formatCent, formatDatum } from '@/lib/utils'
export const dynamic = 'force-dynamic'
export default async function PflegeUebersicht() {
const benutzer = await erfordereBenutzer('/pflege')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'GESUNDHEIT', 'lesen')
const schreiben = darfSchreiben('GESUNDHEIT')
const [heimvertrag, ereignisse, pruefFrist] = await Promise.all([
db.heimvertrag.findUnique({ where: { betreuteId: aktive.id } }),
db.gesundheitsereignis.findMany({
where: { betreuteId: aktive.id, ...nichtGeloescht },
orderBy: { datum: 'desc' },
}),
db.frist.findUnique({
where: {
betreuteId_systemSchluessel: {
betreuteId: aktive.id,
systemSchluessel: 'pflegegrad-pruefung',
},
},
}),
])
const heute = heuteAlsTag()
const zuschlag = leistungszuschlagBerechnen({
einzugAm: heimvertrag?.einzugAm ?? aktive.heimEinzugAm,
eigenanteilCent: heimvertrag?.pflegeEigenanteilCent ?? null,
pflegegrad: aktive.pflegegrad,
stichtag: heute,
})
const abwesend = abwesenheitstageJeJahr(
ereignisse.map((e) => ({
datum: e.datum,
bisDatum: e.bisDatum,
abwesenheitstage: e.abwesenheitstage,
art: e.art,
})),
)
const jahre = verlaufAuswerten(
ereignisse.map((e) => ({
datum: e.datum,
bisDatum: e.bisDatum,
art: e.art,
abwesenheitstage: e.abwesenheitstage,
})),
)
const befund = verschlechterungErkannt(jahre, heute.getUTCFullYear())
const impfungen = impfstatusBerechnen(
ereignisse.filter((e) => e.art === 'IMPFUNG').map((e) => ({ datum: e.datum, titel: e.titel })),
heute,
)
const offenHeim = heimvertrag ? oeffnen('Heimvertrag', heimvertrag) : null
const gesamtCent =
(heimvertrag?.pflegeEigenanteilCent ?? 0n) +
(heimvertrag?.unterkunftVerpflegungCent ?? 0n) +
(heimvertrag?.investitionskostenCent ?? 0n) +
(heimvertrag?.ausbildungsumlageCent ?? 0n)
return (
<div className="space-y-6">
<SeitenKopf
titel="Heim & Pflege"
beschreibung="Heimvertrag, Leistungszuschlag, Pflegegrad und der Gesundheitsverlauf — die Grundlage für Jahresbericht und Höherstufung."
pfad={[{ titel: 'Übersicht', href: '/heute' }, { titel: 'Heim & Pflege' }]}
aktionen={
<div className="flex flex-wrap gap-2">
<Button asChild variant="outline">
<Link href="/pflege/wille">
<ScrollText aria-hidden="true" />
Wille & Vorsorge
</Link>
</Button>
<Button asChild>
<Link href="/pflege/verlauf">
<HeartPulse aria-hidden="true" />
Gesundheitsverlauf
</Link>
</Button>
</div>
}
/>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building className="size-5" aria-hidden="true" />
Heimvertrag
</CardTitle>
<CardDescription>
Einrichtung, Träger und die aktuellen Sätze. Historische Werte stehen in der
monatlichen Gegenüberstellung des Sozialhilfemoduls.
</CardDescription>
</CardHeader>
<CardContent>
{!heimvertrag ? (
<Leerzustand
titel="Noch kein Heimvertrag erfasst"
beschreibung="Ohne Einzugsdatum lässt sich der Leistungszuschlag nach § 43c SGB XI nicht berechnen — und der ist bares Geld."
icon={Building}
aktion={
schreiben && (
<Button asChild>
<Link href="/pflege/heimvertrag">Heimvertrag erfassen</Link>
</Button>
)
}
/>
) : (
<div className="grid gap-x-8 sm:grid-cols-2">
<dl className="divide-y">
<Datenzeile beschriftung="Einrichtung">{heimvertrag.einrichtung}</Datenzeile>
<Datenzeile beschriftung="Träger">{heimvertrag.traeger ?? '–'}</Datenzeile>
<Datenzeile beschriftung="Anschrift">
{[heimvertrag.strasse, [heimvertrag.plz, heimvertrag.ort].filter(Boolean).join(' ')]
.filter(Boolean)
.join(', ') || '–'}
</Datenzeile>
<Datenzeile beschriftung="Vertragsdatum">
{formatDatum(heimvertrag.vertragAm)}
</Datenzeile>
<Datenzeile beschriftung="Einzug">{formatDatum(heimvertrag.einzugAm)}</Datenzeile>
<Datenzeile beschriftung="Kündigungsfrist">
{heimvertrag.kuendigungsfristTage !== null
? `${heimvertrag.kuendigungsfristTage} Tage`
: '–'}
</Datenzeile>
</dl>
<dl className="divide-y">
<Datenzeile beschriftung="Pflegebedingter Eigenanteil">
{formatCent(heimvertrag.pflegeEigenanteilCent)}
</Datenzeile>
<Datenzeile beschriftung="Unterkunft und Verpflegung">
{formatCent(heimvertrag.unterkunftVerpflegungCent)}
</Datenzeile>
<Datenzeile beschriftung="Investitionskosten">
{formatCent(heimvertrag.investitionskostenCent)}
</Datenzeile>
<Datenzeile beschriftung="Ausbildungsumlage">
{formatCent(heimvertrag.ausbildungsumlageCent)}
</Datenzeile>
<Datenzeile beschriftung="Heimkosten im Monat">
<strong>{formatCent(gesamtCent)}</strong>
</Datenzeile>
</dl>
{offenHeim?.notizen && (
<div className="mt-4 rounded-md border bg-muted/40 p-3 sm:col-span-2">
<p className="whitespace-pre-line text-sm">{offenHeim.notizen}</p>
</div>
)}
{schreiben && (
<div className="mt-4 sm:col-span-2">
<Button asChild variant="outline">
<Link href="/pflege/heimvertrag">
<Pencil aria-hidden="true" />
Heimvertrag bearbeiten
</Link>
</Button>
</div>
)}
</div>
)}
</CardContent>
</Card>
<LeistungszuschlagKarte
zuschlag={{
berechenbar: zuschlag.berechenbar,
verweildauerMonate: zuschlag.verweildauerMonate,
prozent: zuschlag.prozent,
stufeBezeichnung: zuschlag.stufe?.bezeichnung ?? null,
zuschlagCent: centZuZahl(zuschlag.zuschlagCent),
verbleibtCent: centZuZahl(zuschlag.verbleibtCent),
eigenanteilCent: centZuZahl(heimvertrag?.pflegeEigenanteilCent ?? null),
naechsteStufe: zuschlag.naechsteStufe
? {
prozent: zuschlag.naechsteStufe.stufe.prozent,
bezeichnung: zuschlag.naechsteStufe.stufe.bezeichnung,
abDatum: zuschlag.naechsteStufe.abDatum.toISOString(),
tageBis: zuschlag.naechsteStufe.tageBis,
mehrCent: centZuZahl(zuschlag.naechsteStufe.mehrCent),
}
: null,
hinweise: zuschlag.hinweise,
}}
/>
<PflegegradKarte
betreuteId={aktive.id}
darfSchreiben={schreiben}
pflegegrad={aktive.pflegegrad}
pflegegradSeit={aktive.pflegegradSeit?.toISOString() ?? null}
pflegegradBescheidVom={aktive.pflegegradBescheidVom?.toISOString() ?? null}
pruefungAm={pruefFrist?.datum.toISOString() ?? null}
vorschlagPruefungAm={alsIsoTag(plusMonate(heute, HOEHERSTUFUNG_ABSTAND_MONATE))}
verschlechterung={befund.begruendung}
begutachtungen={ereignisse
.filter((e) => e.art === 'PFLEGEBEGUTACHTUNG')
.map((e) => ({
id: e.id,
datum: e.datum.toISOString(),
titel: e.titel,
einrichtung: e.einrichtung,
}))}
/>
<AbwesenheitKarte jahre={abwesend} gesamt={abwesenheitstageGesamt(abwesend)} />
<Impfstatus
zeilen={impfungen.map((i) => ({
schluessel: i.empfehlung.schluessel,
bezeichnung: i.empfehlung.bezeichnung,
begruendung: i.empfehlung.begruendung,
letzteImpfung: i.letzteImpfung?.toISOString() ?? null,
faelligAm: i.faelligAm?.toISOString() ?? null,
lage: i.lage,
text: i.text,
}))}
/>
</div>
)
}
+54
View File
@@ -0,0 +1,54 @@
import { redirect } from 'next/navigation'
import { SeitenKopf } from '@/components/seiten-kopf'
import { EreignisFormular } from '@/components/pflege/ereignis-formular'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
import { GESUNDHEITS_ARTEN } from '@/lib/validierung/pflege'
export const dynamic = 'force-dynamic'
export default async function EreignisAnlegen({
searchParams,
}: {
searchParams: Promise<{ art?: string }>
}) {
const benutzer = await erfordereBenutzer('/pflege/verlauf/neu')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'GESUNDHEIT', 'lesen')
if (!darfSchreiben('GESUNDHEIT')) redirect('/pflege/verlauf')
const { art } = await searchParams
const vorgabe = GESUNDHEITS_ARTEN.find((a) => a === art) ?? 'EXAZERBATION'
return (
<div className="space-y-6">
<SeitenKopf
titel="Ereignis erfassen"
beschreibung="Schnellerfassung für die Fälle, die bei einer COPD immer wiederkehren. Die Zusatzfelder wechseln mit der Art."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Heim & Pflege', href: '/pflege' },
{ titel: 'Verlauf', href: '/pflege/verlauf' },
{ titel: 'Erfassen' },
]}
/>
<EreignisFormular
betreuteId={aktive.id}
werte={{
datum: alsIsoTag(heuteAlsTag()),
bisDatum: '',
art: vorgabe,
titel: '',
behandler: '',
einrichtung: aktive.heimName ?? '',
abwesenheitstage: '',
beschreibung: '',
details: {},
}}
/>
</div>
)
}
+107
View File
@@ -0,0 +1,107 @@
import Link from 'next/link'
import { Plus } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { SeitenKopf } from '@/components/seiten-kopf'
import { Rechtshinweis } from '@/components/rechtshinweis'
import { VerlaufAuswertung } from '@/components/pflege/verlauf-auswertung'
import { VerlaufZeitstrahl } from '@/components/pflege/verlauf-zeitstrahl'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { db, nichtGeloescht } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { heuteAlsTag } from '@/lib/fristen/datum'
import {
verlaufAuswerten,
verlaufZusammenfassung,
verschlechterungErkannt,
} from '@/lib/pflege/verlauf'
export const dynamic = 'force-dynamic'
export default async function GesundheitsVerlauf() {
const benutzer = await erfordereBenutzer('/pflege/verlauf')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'GESUNDHEIT', 'lesen')
const schreiben = darfSchreiben('GESUNDHEIT')
const ereignisse = await db.gesundheitsereignis.findMany({
where: { betreuteId: aktive.id, ...nichtGeloescht },
orderBy: { datum: 'desc' },
})
const jahre = verlaufAuswerten(
ereignisse.map((e) => ({
datum: e.datum,
bisDatum: e.bisDatum,
art: e.art,
abwesenheitstage: e.abwesenheitstage,
})),
)
const befund = verschlechterungErkannt(jahre, heuteAlsTag().getUTCFullYear())
return (
<div className="space-y-6">
<SeitenKopf
titel="Gesundheitsverlauf"
beschreibung="Zeitstrahl aller erfassten Ereignisse. Beschreibungen und Zusatzangaben liegen verschlüsselt — Gesundheitsdaten sind besondere Kategorien nach Art. 9 DSGVO."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Heim & Pflege', href: '/pflege' },
{ titel: 'Verlauf' },
]}
aktionen={
schreiben && (
<Button asChild>
<Link href="/pflege/verlauf/neu">
<Plus aria-hidden="true" />
Ereignis erfassen
</Link>
</Button>
)
}
/>
<VerlaufAuswertung
verschlechterung={befund.begruendung}
zeilen={jahre.map((jahr) => ({
jahr: jahr.jahr,
exazerbationen: jahr.exazerbationen,
krankenhausaufenthalte: jahr.krankenhausaufenthalte,
krankenhaustage: jahr.krankenhaustage,
abwesenheitstage: jahr.abwesenheitstage,
stuerze: jahr.stuerze,
satz: verlaufZusammenfassung(jahr),
}))}
/>
<VerlaufZeitstrahl
betreuteId={aktive.id}
darfSchreiben={schreiben}
ereignisse={ereignisse.map((e) => {
const offen = oeffnen('Gesundheitsereignis', e)
return {
id: e.id,
datum: e.datum.toISOString(),
bisDatum: e.bisDatum?.toISOString() ?? null,
art: e.art,
titel: e.titel,
behandler: e.behandler,
einrichtung: e.einrichtung,
abwesenheitstage: e.abwesenheitstage,
beschreibung: offen.beschreibung,
}
})}
/>
<Rechtshinweis rechtsgrundlage="§ 18 SGB XI, § 1863 BGB" />
<p className="text-sm text-muted-foreground">
Kurz übersetzt: § 1863 BGB verlangt einmal im Jahr einen Bericht ans Betreuungsgericht,
und dazu gehört die persönliche Situation der betreuten Person. § 18 SGB XI regelt die
Begutachtung durch den Medizinischen Dienst — ein Antrag auf Neubegutachtung ist
jederzeit möglich, wenn sich der Hilfebedarf ändert. Beide Male ist dieser Verlauf die
Grundlage: Er macht aus „es geht ihr schlechter“ eine belegbare Zahl.
</p>
</div>
)
}
+175
View File
@@ -0,0 +1,175 @@
import Link from 'next/link'
import { FileText, Pencil, ScrollText, Upload } 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 { Rechtshinweis } from '@/components/rechtshinweis'
import { WilleFormular } from '@/components/pflege/wille-formular'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { db, nichtGeloescht } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { formatDatum } from '@/lib/utils'
export const dynamic = 'force-dynamic'
export default async function WilleUndVorsorge() {
const benutzer = await erfordereBenutzer('/pflege/wille')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'GESUNDHEIT', 'lesen')
const schreiben = darfSchreiben('GESUNDHEIT')
const unterlagen = await db.dokument.findMany({
where: {
betreuteId: aktive.id,
art: { in: ['VOLLMACHT', 'URKUNDE', 'ATTEST'] },
...nichtGeloescht,
},
select: { id: true, titel: true, art: true, schreibenVom: true, ablageortPapier: true },
orderBy: { schreibenVom: 'desc' },
take: 20,
})
const offen = oeffnen('Betreute', aktive)
return (
<div className="space-y-6">
<SeitenKopf
titel="Wille und Vorsorge"
beschreibung="Patientenverfügung, Vorsorgevollmacht, Behandlungswünsche — und der ermittelte Wille der betreuten Person."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Heim & Pflege', href: '/pflege' },
{ titel: 'Wille & Vorsorge' },
]}
/>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ScrollText className="size-5" aria-hidden="true" />
Der ermittelte Wille
</CardTitle>
<CardDescription>
Seit der Betreuungsrechtsreform 2023 ist der Wille der betreuten Person die
Leitschnur des Betreuerhandelns — nicht mehr das, was objektiv vernünftig
erscheint.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Rechtshinweis rechtsgrundlage="§ 1821 BGB" />
<p className="text-sm text-muted-foreground">
Kurz übersetzt: Du hast den Wünschen der betreuten Person zu entsprechen, auch wenn
du sie für unklug hältst. Erst wenn ein Wunsch ihre Person oder ihr Vermögen
erheblich gefährdet oder dir nicht zuzumuten ist, darfst du davon abweichen — und
das musst du begründen können. Kann sie ihren Willen nicht mehr äußern, gilt der
früher geäußerte oder der mutmaßliche Wille. Deshalb steht hier nicht deine
Einschätzung, sondern was sie gesagt hat, wann sie es gesagt hat und woher du es
weißt.
</p>
<WilleFormular
betreuteId={aktive.id}
darfSchreiben={schreiben}
ermittelterWille={offen.ermittelterWille}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="size-5" aria-hidden="true" />
Vorsorgeunterlagen
</CardTitle>
<CardDescription>
Die beiden Häkchen liegen an den Stammdaten. Ändern lassen sie sich dort — hier
stehen sie nur zur Übersicht.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-md border p-3">
<p className="text-sm text-muted-foreground">Patientenverfügung</p>
<Badge variant={aktive.patientenverfuegungVorhanden ? 'gruen' : 'gelb'}>
{aktive.patientenverfuegungVorhanden ? 'vorhanden' : 'nicht vorhanden'}
</Badge>
<p className="mt-2 text-sm text-muted-foreground">
Liegt eine wirksame Patientenverfügung vor und passt sie auf die aktuelle
Behandlungssituation, gilt sie unmittelbar — dann hast du ihr Ausdruck und
Geltung zu verschaffen (§ 1827 BGB). Eine Genehmigung des Gerichts braucht es
nur, wenn Uneinigkeit mit dem Arzt besteht.
</p>
</div>
<div className="rounded-md border p-3">
<p className="text-sm text-muted-foreground">Vorsorgevollmacht</p>
<Badge variant={aktive.vorsorgevollmachtVorhanden ? 'gruen' : 'gelb'}>
{aktive.vorsorgevollmachtVorhanden ? 'vorhanden' : 'nicht vorhanden'}
</Badge>
<p className="mt-2 text-sm text-muted-foreground">
Eine wirksame Vorsorgevollmacht geht der Betreuung vor. Gibt es sie für Bereiche,
die dein Aufgabenkreis abdeckt, gehört das dem Gericht mitgeteilt — dann ist die
Betreuung insoweit nicht erforderlich.
</p>
</div>
</div>
<Button asChild variant="outline">
<Link href="/stammdaten/bearbeiten">
<Pencil aria-hidden="true" />
In den Stammdaten ändern
</Link>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Abgelegte Unterlagen</CardTitle>
<CardDescription>
Vollmachten, Urkunden und Atteste aus der Dokumentenablage. Im Notfall zählt, dass
das Papier greifbar ist — notier deshalb immer auch, wo das Original liegt.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{unterlagen.length === 0 ? (
<p className="text-sm text-muted-foreground">
Noch nichts abgelegt. Scanne Patientenverfügung und Vorsorgevollmacht ein und
hinterlege beim Dokument, wo das Original liegt.
</p>
) : (
<ul className="space-y-2">
{unterlagen.map((dokument) => (
<li
key={dokument.id}
className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3"
>
<Link
href={`/dokumente/${dokument.id}`}
className="font-medium hover:underline"
>
{dokument.titel}
</Link>
<span className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Badge variant="outline">{dokument.art}</Badge>
{dokument.schreibenVom && <span>{formatDatum(dokument.schreibenVom)}</span>}
{dokument.ablageortPapier && <span>Original: {dokument.ablageortPapier}</span>}
</span>
</li>
))}
</ul>
)}
<Button asChild variant="outline">
<Link href="/dokumente/neu?art=VOLLMACHT">
<Upload aria-hidden="true" />
Unterlage hochladen
</Link>
</Button>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,77 @@
import { notFound, redirect } from 'next/navigation'
import { SeitenKopf } from '@/components/seiten-kopf'
import { VertragFormular } from '@/components/vertraege/vertrag-formular'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { db, nichtGeloescht } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { alsIsoTag } from '@/lib/fristen/datum'
export const dynamic = 'force-dynamic'
/** Cent als Eingabetext: 481255 wird zu "4812,55". */
function centAlsText(cent: bigint | null): string {
if (cent === null) 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')}`
}
export default async function VertragBearbeiten({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const benutzer = await erfordereBenutzer(`/vertraege/${id}/bearbeiten`)
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
if (!darfSchreiben('VERTRAEGE')) redirect(`/vertraege/${id}`)
const vertrag = await db.vertrag.findFirst({
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
})
if (!vertrag) notFound()
const offen = oeffnen('Vertrag', vertrag)
return (
<div className="space-y-6">
<SeitenKopf
titel={`${vertrag.bezeichnung} bearbeiten`}
beschreibung="Änderungen werden protokolliert. Die Vertragsnummer wird verschlüsselt gespeichert."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Verträge', href: '/vertraege' },
{ titel: vertrag.bezeichnung, href: `/vertraege/${id}` },
{ titel: 'Bearbeiten' },
]}
/>
<VertragFormular
betreuteId={aktive.id}
werte={{
id: vertrag.id,
bezeichnung: vertrag.bezeichnung,
gesellschaft: vertrag.gesellschaft ?? '',
art: vertrag.art,
status: vertrag.status,
vertragsnummer: offen.vertragsnummer ?? '',
beginnAm: vertrag.beginnAm ? alsIsoTag(vertrag.beginnAm) : '',
endeAm: vertrag.endeAm ? alsIsoTag(vertrag.endeAm) : '',
kuendigungsfristMonate: vertrag.kuendigungsfristMonate?.toString() ?? '',
kuendigungZum: vertrag.kuendigungZum ? alsIsoTag(vertrag.kuendigungZum) : '',
gekuendigtAm: vertrag.gekuendigtAm ? alsIsoTag(vertrag.gekuendigtAm) : '',
beitrag: centAlsText(vertrag.beitragCent),
rhythmus: vertrag.rhythmus ?? '',
bewertungSozialhilfe: vertrag.bewertungSozialhilfe,
bewertungBegruendung: offen.bewertungBegruendung ?? '',
notizen: offen.notizen ?? '',
}}
/>
</div>
)
}
@@ -0,0 +1,96 @@
import { notFound } from 'next/navigation'
import { SeitenKopf } from '@/components/seiten-kopf'
import { KuendigungsAssistent } from '@/components/vertraege/kuendigungs-assistent'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { 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 { kuendigungsassistent } from '@/lib/vertraege/kuendigungsassistent'
import { laufzeitAusVertrag } from '@/lib/vertraege/kuendigung'
import { VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
export const dynamic = 'force-dynamic'
export default async function VertragKuendigen({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const benutzer = await erfordereBenutzer(`/vertraege/${id}/kuendigen`)
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 },
})
if (!vertrag) notFound()
const offen = oeffnen('Vertrag', vertrag)
const heute = heuteAlsTag()
const paket = kuendigungsassistent({
kopf: briefkopfBauen({ betreute: aktive, benutzer }),
empfaenger: { gesellschaft: vertrag.gesellschaft, anschrift: null },
vertragsbezeichnung: `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}`,
vertragsnummer: offen.vertragsnummer,
art: vertrag.art,
laufzeit: {
beginnAm: vertrag.beginnAm,
endeAm: vertrag.endeAm,
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
},
heute,
})
return (
<div className="space-y-6">
<SeitenKopf
titel="Kündigungs-Assistent"
beschreibung={`${vertrag.bezeichnung} — Frist prüfen, Genehmigungsfrage stellen, Schreiben erzeugen, Versandnachweis sichern.`}
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Verträge', href: '/vertraege' },
{ titel: vertrag.bezeichnung, href: `/vertraege/${id}` },
{ titel: 'Kündigen' },
]}
/>
<KuendigungsAssistent
betreuteId={aktive.id}
vertragId={id}
darfSchreiben={schreiben}
heuteIso={alsIsoTag(heute)}
daten={{
schreiben: paket.schreiben,
warnungen: paket.warnungen,
genehmigung: paket.genehmigung,
versandnachweis: {
moeglichkeiten: [...paket.versandnachweis.moeglichkeiten],
hinweis: paket.versandnachweis.hinweis,
},
plan: {
berechenbar: paket.plan.berechenbar,
naechsterTermin: paket.plan.naechsterTermin
? alsIsoTag(paket.plan.naechsterTermin)
: null,
zugangSpaetestensAm: paket.plan.zugangSpaetestensAm
? alsIsoTag(paket.plan.zugangSpaetestensAm)
: null,
spaetesterAbsendetag: paket.plan.spaetesterAbsendetag
? alsIsoTag(paket.plan.spaetesterAbsendetag)
: null,
tageBisAbsendetag: paket.plan.tageBisAbsendetag,
verpasst: paket.plan.verpasst,
hinweise: paket.plan.hinweise,
},
}}
/>
</div>
)
}
+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>
)
}
+31
View File
@@ -0,0 +1,31 @@
import { redirect } from 'next/navigation'
import { SeitenKopf } from '@/components/seiten-kopf'
import { LEERER_VERTRAG, VertragFormular } from '@/components/vertraege/vertrag-formular'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
export const dynamic = 'force-dynamic'
export default async function VertragAnlegen() {
const benutzer = await erfordereBenutzer('/vertraege/neu')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
if (!darfSchreiben('VERTRAEGE')) redirect('/vertraege')
return (
<div className="space-y-6">
<SeitenKopf
titel="Vertrag anlegen"
beschreibung="Beginn und Kündigungsfrist sind die beiden Angaben, aus denen alles Weitere folgt. Stehen sie nicht in der Police, frag bei der Gesellschaft nach."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Verträge', href: '/vertraege' },
{ titel: 'Anlegen' },
]}
/>
<VertragFormular betreuteId={aktive.id} werte={LEERER_VERTRAG} />
</div>
)
}
+235
View File
@@ -0,0 +1,235 @@
import Link from 'next/link'
import { Plus, 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Leerzustand, SeitenKopf } from '@/components/seiten-kopf'
import { Rechtshinweis } from '@/components/rechtshinweis'
import {
KuendigungsUebersicht,
type Kuendigungswarnung,
} from '@/components/vertraege/kuendigungs-uebersicht'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
import { oeffnen } from '@/lib/crypto/feld'
import { heuteAlsTag } from '@/lib/fristen/datum'
import {
kuendigungsampel,
kuendigungsplan,
laufzeitAusVertrag,
} from '@/lib/vertraege/kuendigung'
import {
BEWERTUNG_BESCHRIFTUNG,
jahresbeitragCent,
RHYTHMUS_BESCHRIFTUNG,
VERTRAG_ART_BESCHRIFTUNG,
VERTRAG_STATUS_BESCHRIFTUNG,
} from '@/lib/vertraege/konstanten'
import { formatCent, formatDatum } from '@/lib/utils'
export const dynamic = 'force-dynamic'
const BEWERTUNG_VARIANTE = {
UNGEPRUEFT: 'outline',
VERWERTBAR: 'orange',
GESCHUETZT: 'gruen',
STRITTIG: 'gelb',
} as const
export default async function VertraegeUebersicht() {
const benutzer = await erfordereBenutzer('/vertraege')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
const schreiben = darfSchreiben('VERTRAEGE')
const vertraege = await db.vertrag.findMany({
where: { betreuteId: aktive.id, ...nichtGeloescht },
include: {
vermoegensposten: {
where: { art: 'VERSICHERUNG', ...nichtGeloescht },
include: { wertstaende: { orderBy: { stichtag: 'desc' }, take: 1 } },
},
},
orderBy: [{ status: 'asc' }, { bezeichnung: 'asc' }],
})
const heute = heuteAlsTag()
const warnungen: Kuendigungswarnung[] = []
let jahresbeitragSumme = 0n
const zeilen = vertraege.map((vertrag) => {
const offen = oeffnen('Vertrag', vertrag)
const plan = kuendigungsplan({
beginnAm: vertrag.beginnAm,
endeAm: vertrag.endeAm,
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
heute,
})
const ampel = kuendigungsampel(plan)
const jahr = jahresbeitragCent(vertrag.beitragCent, vertrag.rhythmus)
if (jahr && vertrag.status === 'AKTIV') jahresbeitragSumme += jahr
const laufend = vertrag.status === 'AKTIV' || vertrag.status === 'BEITRAGSFREI'
if (laufend && ampel && (ampel.variante === 'rot' || ampel.variante === 'orange')) {
warnungen.push({
vertragId: vertrag.id,
bezeichnung: vertrag.bezeichnung,
absendetag: plan.spaetesterAbsendetag?.toISOString() ?? null,
tageBisAbsendetag: plan.tageBisAbsendetag,
verpasst: plan.verpasst,
variante: ampel.variante,
text: ampel.text,
})
}
return {
id: vertrag.id,
bezeichnung: vertrag.bezeichnung,
gesellschaft: vertrag.gesellschaft,
art: vertrag.art,
status: vertrag.status,
vertragsnummer: offen.vertragsnummer,
beitragCent: centZuZahl(vertrag.beitragCent),
rhythmus: vertrag.rhythmus,
bewertung: vertrag.bewertungSozialhilfe,
rueckkaufswertCent: centZuZahl(
vertrag.vermoegensposten[0]?.wertstaende[0]?.wertCent ?? null,
),
rueckkaufswertStichtag:
vertrag.vermoegensposten[0]?.wertstaende[0]?.stichtag.toISOString() ?? null,
ampel,
}
})
return (
<div className="space-y-6">
<SeitenKopf
titel="Versicherungen & Verträge"
beschreibung="Alle laufenden Verträge mit Beitrag, Kündigungsfrist, Rückkaufswert und der Einordnung fürs Sozialamt."
pfad={[{ titel: 'Übersicht', href: '/heute' }, { titel: 'Verträge' }]}
aktionen={
schreiben && (
<Button asChild>
<Link href="/vertraege/neu">
<Plus aria-hidden="true" />
Vertrag anlegen
</Link>
</Button>
)
}
/>
<KuendigungsUebersicht warnungen={warnungen} />
{vertraege.length === 0 ? (
<Leerzustand
titel="Noch kein Vertrag erfasst"
beschreibung="Nimm die Kontoauszüge des letzten Jahres zur Hand: Jede wiederkehrende Abbuchung ist ein Vertrag. Das ist der zuverlässigste Weg, nichts zu übersehen — Policen liegen selten vollständig im Ordner."
icon={Shield}
aktion={
schreiben && (
<Button asChild>
<Link href="/vertraege/neu">
<Plus aria-hidden="true" />
Vertrag anlegen
</Link>
</Button>
)
}
/>
) : (
<Card>
<CardHeader>
<CardTitle>Alle Verträge</CardTitle>
<CardDescription>
Jahresbeitrag der aktiven Verträge zusammen:{' '}
<strong className="text-foreground">
{formatCent(centZuZahl(jahresbeitragSumme))}
</strong>
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Vertrag</TableHead>
<TableHead>Art</TableHead>
<TableHead className="text-right">Beitrag</TableHead>
<TableHead className="text-right">Rückkaufswert</TableHead>
<TableHead>Kündigung</TableHead>
<TableHead>Sozialamt</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{zeilen.map((zeile) => (
<TableRow key={zeile.id}>
<TableCell>
<Link
href={`/vertraege/${zeile.id}`}
className="font-medium hover:underline"
>
{zeile.bezeichnung}
</Link>
<span className="block text-sm text-muted-foreground">
{[zeile.gesellschaft, zeile.vertragsnummer].filter(Boolean).join(' · ') ||
'Gesellschaft nicht erfasst'}
</span>
</TableCell>
<TableCell className="text-sm">
{VERTRAG_ART_BESCHRIFTUNG[zeile.art]}
</TableCell>
<TableCell className="text-right tabular-nums">
{formatCent(zeile.beitragCent)}
{zeile.rhythmus && (
<span className="block text-xs text-muted-foreground">
{RHYTHMUS_BESCHRIFTUNG[zeile.rhythmus]}
</span>
)}
</TableCell>
<TableCell className="text-right tabular-nums">
{formatCent(zeile.rueckkaufswertCent)}
{zeile.rueckkaufswertStichtag && (
<span className="block text-xs text-muted-foreground">
{formatDatum(zeile.rueckkaufswertStichtag)}
</span>
)}
</TableCell>
<TableCell>
{zeile.ampel ? (
<Badge variant={zeile.ampel.variante}>{zeile.ampel.text}</Badge>
) : (
<span className="text-sm text-muted-foreground">nicht berechenbar</span>
)}
</TableCell>
<TableCell>
<Badge variant={BEWERTUNG_VARIANTE[zeile.bewertung]}>
{BEWERTUNG_BESCHRIFTUNG[zeile.bewertung]}
</Badge>
</TableCell>
<TableCell className="text-sm">
{VERTRAG_STATUS_BESCHRIFTUNG[zeile.status]}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII, § 1850 BGB" />
<p className="text-sm text-muted-foreground">
Kurz übersetzt: Ob ein Vertrag vor der Sozialhilfe einzusetzen ist, richtet sich nach
§ 90 SGB XII — eine angemessene Bestattungsvorsorge bleibt regelmäßig geschützt. Und
bevor eine Lebens- oder Rentenversicherung verwertet wird, gehört die Frage nach der
Genehmigung des Betreuungsgerichts (§ 1850 BGB) an den Rechtspfleger, nicht an diese App.
</p>
</div>
)
}
@@ -0,0 +1,83 @@
import Link from 'next/link'
import { BedDouble } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Leerzustand } from '@/components/seiten-kopf'
import { ABWESENHEIT_HINWEIS } from '@/lib/pflege/konstanten'
/**
* Abwesenheitstage je Jahr - Erfassung laeuft ueber den
* Gesundheitsverlauf, hier steht nur die Summe.
*/
export function AbwesenheitKarte({
jahre,
gesamt,
}: {
jahre: { jahr: number; tage: number; ereignisse: number }[]
gesamt: number
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BedDouble className="size-5" aria-hidden="true" />
Abwesenheitstage
</CardTitle>
<CardDescription>
Tage außerhalb der Einrichtung, vor allem im Krankenhaus. Erfasst werden sie im
Gesundheitsverlauf; hier stehen die Jahressummen.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{jahre.length === 0 ? (
<Leerzustand
titel="Noch keine Abwesenheitstage erfasst"
beschreibung="Jeder Krankenhausaufenthalt mit Aufnahme- und Entlassdatum zählt hier automatisch mit."
icon={BedDouble}
aktion={
<Button asChild variant="outline">
<Link href="/pflege/verlauf/neu?art=KRANKENHAUS">
Krankenhausaufenthalt erfassen
</Link>
</Button>
}
/>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Jahr</TableHead>
<TableHead className="text-right">Ereignisse</TableHead>
<TableHead className="text-right">Abwesenheitstage</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{jahre.map((jahr) => (
<TableRow key={jahr.jahr}>
<TableCell className="font-medium">{jahr.jahr}</TableCell>
<TableCell className="text-right tabular-nums">{jahr.ereignisse}</TableCell>
<TableCell className="text-right font-medium tabular-nums">
{jahr.tage}
</TableCell>
</TableRow>
))}
<TableRow className="border-t-2">
<TableCell className="font-semibold">insgesamt</TableCell>
<TableCell />
<TableCell className="text-right font-semibold tabular-nums">{gesamt}</TableCell>
</TableRow>
</TableBody>
</Table>
</>
)}
<p className="rounded-md border bg-muted/40 p-3 text-sm text-muted-foreground">
{ABWESENHEIT_HINWEIS}
</p>
</CardContent>
</Card>
)
}
+275
View File
@@ -0,0 +1,275 @@
'use client'
import { useActionState, useState } from 'react'
import type { GesundheitsArt } from '@prisma/client'
import { Lock } from 'lucide-react'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Select } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { Feld, Feldgruppe } from '@/components/feld'
import { Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
import { detailfelderFuer } from '@/lib/pflege/ereignis-details'
import {
EXAZERBATION_AUSLOESER,
GESUNDHEITS_ART_BESCHRIFTUNG,
SCHNELLERFASSUNG_REIHENFOLGE,
} from '@/lib/pflege/konstanten'
import { gesundheitsereignisSpeichern } from '@/server/actions/pflege'
import type { Formularzustand } from '@/lib/validierung/substanz'
export type EreignisWerte = {
id?: string
datum: string
bisDatum: string
art: GesundheitsArt
titel: string
behandler: string
einrichtung: string
abwesenheitstage: string
beschreibung: string
details: Record<string, string>
}
/** Vorschlag für den Titel, damit die Schnellerfassung wirklich schnell ist. */
const TITELVORSCHLAG: Partial<Record<GesundheitsArt, string>> = {
EXAZERBATION: 'Exazerbation',
KRANKENHAUS: 'Krankenhausaufenthalt',
SAUERSTOFFVERSORGUNG: 'Sauerstoffversorgung angepasst',
MEDIKATIONSAENDERUNG: 'Medikation geändert',
INHALATION: 'Inhalation angepasst',
STURZ: 'Sturz',
HILFSMITTEL: 'Hilfsmittel',
IMPFUNG: 'Grippeimpfung',
ARZTBESUCH: 'Arztbesuch',
PFLEGEBEGUTACHTUNG: 'Pflegebegutachtung',
}
export function EreignisFormular({
betreuteId,
werte,
}: {
betreuteId: string
werte: EreignisWerte
}) {
const [zustand, aktion] = useActionState<Formularzustand, FormData>(
gesundheitsereignisSpeichern,
{},
)
const [art, setArt] = useState<GesundheitsArt>(werte.art)
const [titelBeruehrt, setTitelBeruehrt] = useState(Boolean(werte.titel))
const felder = detailfelderFuer(art)
const zeitraum = art === 'KRANKENHAUS' || art === 'REHA'
return (
<form action={aktion} className="space-y-8" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
{werte.id && <input type="hidden" name="ereignisId" value={werte.id} />}
<Zustandsmeldung zustand={zustand} />
<Alert variant="hinweis">
<Lock aria-hidden="true" />
<AlertDescription>
Gesundheitsdaten sind besondere Kategorien nach Art. 9 DSGVO. Die Beschreibung und
alle Zusatzangaben werden verschlüsselt gespeichert; im Protokoll steht später nur
der Titel, nie der Inhalt. Schreib den Titel deshalb so, dass er ohne Diagnose
auskommt.
</AlertDescription>
</Alert>
<Feldgruppe titel="Ereignis" beschreibung="Was ist passiert und wann.">
<Feld id="art" beschriftung="Art" fehler={zustand.felder?.art} pflicht>
<Select
name="art"
value={art}
onChange={(e) => setArt(e.target.value as GesundheitsArt)}
>
{SCHNELLERFASSUNG_REIHENFOLGE.map((a) => (
<option key={a} value={a}>
{GESUNDHEITS_ART_BESCHRIFTUNG[a]}
</option>
))}
</Select>
</Feld>
<Feld
id="titel"
beschriftung="Titel"
erklaerung="Kurz und ohne Diagnose — er steht unverschlüsselt in der Liste."
fehler={zustand.felder?.titel}
pflicht
>
<Input
name="titel"
required
maxLength={200}
key={titelBeruehrt ? 'fest' : art}
defaultValue={titelBeruehrt ? werte.titel : (TITELVORSCHLAG[art] ?? werte.titel)}
onChange={() => setTitelBeruehrt(true)}
/>
</Feld>
<Feld
id="datum"
beschriftung={zeitraum ? 'Aufnahme am' : 'Datum'}
fehler={zustand.felder?.datum}
pflicht
>
<Input name="datum" type="date" defaultValue={werte.datum} required />
</Feld>
<Feld
id="bisDatum"
beschriftung={zeitraum ? 'Entlassung am' : 'Ende (falls Zeitraum)'}
erklaerung={
zeitraum
? 'Aus Aufnahme und Entlassung werden die Abwesenheitstage gerechnet — der Entlassungstag zählt nicht mit.'
: undefined
}
fehler={zustand.felder?.bisDatum}
>
<Input name="bisDatum" type="date" defaultValue={werte.bisDatum} />
</Feld>
<Feld
id="behandler"
beschriftung="Behandler"
erklaerung="Arzt, Pflegedienstleitung, Gutachter."
fehler={zustand.felder?.behandler}
>
<Input name="behandler" defaultValue={werte.behandler} maxLength={150} />
</Feld>
<Feld
id="einrichtung"
beschriftung="Einrichtung"
fehler={zustand.felder?.einrichtung}
>
<Input name="einrichtung" defaultValue={werte.einrichtung} maxLength={150} />
</Feld>
<Feld
id="abwesenheitstage"
beschriftung="Abwesenheitstage"
erklaerung="Nur ausfüllen, wenn du die Tage abweichend zählen willst — sonst werden sie aus dem Zeitraum gerechnet. Was gilt, steht im Heimvertrag."
fehler={zustand.felder?.abwesenheitstage}
>
<Input
name="abwesenheitstage"
inputMode="numeric"
defaultValue={werte.abwesenheitstage}
/>
</Feld>
</Feldgruppe>
{felder.length > 0 && (
<Feldgruppe
titel="Zusatzangaben"
beschreibung="Die Angaben, die bei dieser Art von Ereignis später zählen. Sie werden verschlüsselt in die Beschreibung geschrieben."
>
{felder.map((feld) => {
const id = `detail_${feld.name}`
const wert = werte.details[feld.name] ?? ''
if (feld.typ === 'jaNein') {
return (
<div key={feld.name} className="sm:col-span-2">
<label className="flex items-start gap-2.5">
<Checkbox name={id} defaultChecked={wert === 'ja'} className="mt-0.5" />
<span className="text-sm">
<span className="font-medium">{feld.beschriftung}</span>
{feld.erklaerung && (
<span className="block text-muted-foreground">{feld.erklaerung}</span>
)}
</span>
</label>
</div>
)
}
if (feld.typ === 'auswahl') {
return (
<Feld
key={feld.name}
id={id}
beschriftung={feld.beschriftung}
erklaerung={feld.erklaerung}
pflicht={feld.pflicht}
>
<Select name={id} defaultValue={wert}>
<option value="">— bitte wählen —</option>
{(feld.optionen ?? []).map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Select>
</Feld>
)
}
if (feld.typ === 'mehrzeilig') {
return (
<Feld
key={feld.name}
id={id}
beschriftung={feld.beschriftung}
erklaerung={feld.erklaerung}
className="sm:col-span-2"
>
<Textarea name={id} defaultValue={wert} rows={2} maxLength={1000} />
</Feld>
)
}
return (
<Feld
key={feld.name}
id={id}
beschriftung={
feld.einheit ? `${feld.beschriftung} (${feld.einheit})` : feld.beschriftung
}
erklaerung={feld.erklaerung}
pflicht={feld.pflicht}
>
<Input
name={id}
defaultValue={wert}
inputMode={feld.typ === 'zahl' ? 'decimal' : 'text'}
maxLength={200}
list={
feld.name === 'ausloeser' && art === 'EXAZERBATION'
? 'ausloeser-vorschlaege'
: undefined
}
/>
</Feld>
)
})}
{art === 'EXAZERBATION' && (
<datalist id="ausloeser-vorschlaege">
{EXAZERBATION_AUSLOESER.map((a) => (
<option key={a} value={a} />
))}
</datalist>
)}
</Feldgruppe>
)}
<Feld
id="beschreibung"
beschriftung="Weitere Beschreibung"
erklaerung="Wird verschlüsselt gespeichert. Hier gehört hinein, was du später im Jahresbericht oder bei einer Begutachtung brauchst."
fehler={zustand.felder?.beschreibung}
>
<Textarea name="beschreibung" defaultValue={werte.beschreibung} rows={5} maxLength={4000} />
</Feld>
<Absendeknopf>{werte.id ? 'Änderungen speichern' : 'Eintrag anlegen'}</Absendeknopf>
</form>
)
}
@@ -0,0 +1,196 @@
'use client'
import { useActionState } from 'react'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Feld, Feldgruppe } from '@/components/feld'
import { Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
import { heimvertragSpeichern } from '@/server/actions/pflege'
import type { Formularzustand } from '@/lib/validierung/substanz'
export type HeimvertragWerte = {
einrichtung: string
traeger: string
strasse: string
plz: string
ort: string
vertragAm: string
einzugAm: string
auszugAm: string
kuendigungsfristTage: string
pflegeEigenanteil: string
unterkunftVerpflegung: string
investitionskosten: string
ausbildungsumlage: string
notizen: string
}
export const LEERER_HEIMVERTRAG: HeimvertragWerte = {
einrichtung: '',
traeger: '',
strasse: '',
plz: '',
ort: '',
vertragAm: '',
einzugAm: '',
auszugAm: '',
kuendigungsfristTage: '14',
pflegeEigenanteil: '',
unterkunftVerpflegung: '',
investitionskosten: '',
ausbildungsumlage: '',
notizen: '',
}
export function HeimvertragFormular({
betreuteId,
werte,
}: {
betreuteId: string
werte: HeimvertragWerte
}) {
const [zustand, aktion] = useActionState<Formularzustand, FormData>(heimvertragSpeichern, {})
return (
<form action={aktion} className="space-y-8" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
<Zustandsmeldung zustand={zustand} />
<Feldgruppe titel="Einrichtung" beschreibung="Wer betreibt das Heim und wo liegt es.">
<Feld
id="einrichtung"
beschriftung="Einrichtung"
fehler={zustand.felder?.einrichtung}
pflicht
>
<Input name="einrichtung" defaultValue={werte.einrichtung} required maxLength={150} />
</Feld>
<Feld
id="traeger"
beschriftung="Träger"
erklaerung="Wer betreibt das Heim? Bei Streit über die Rechnung ist der Träger der Vertragspartner, nicht die Heimleitung."
fehler={zustand.felder?.traeger}
>
<Input name="traeger" defaultValue={werte.traeger} maxLength={150} />
</Feld>
<Feld id="strasse" beschriftung="Straße und Hausnummer" fehler={zustand.felder?.strasse}>
<Input name="strasse" defaultValue={werte.strasse} maxLength={150} />
</Feld>
<div className="grid grid-cols-[7rem_1fr] gap-3">
<Feld id="plz" beschriftung="PLZ" fehler={zustand.felder?.plz}>
<Input name="plz" defaultValue={werte.plz} inputMode="numeric" maxLength={10} />
</Feld>
<Feld id="ort" beschriftung="Ort" fehler={zustand.felder?.ort}>
<Input name="ort" defaultValue={werte.ort} maxLength={100} />
</Feld>
</div>
</Feldgruppe>
<Feldgruppe
titel="Vertrag"
beschreibung="Das Einzugsdatum steuert den Leistungszuschlag nach § 43c SGB XI — trag es unbedingt ein."
>
<Feld id="vertragAm" beschriftung="Vertragsdatum" fehler={zustand.felder?.vertragAm}>
<Input name="vertragAm" type="date" defaultValue={werte.vertragAm} />
</Feld>
<Feld
id="einzugAm"
beschriftung="Einzug am"
erklaerung="Ab diesem Tag zählt die Verweildauer für den Leistungszuschlag."
fehler={zustand.felder?.einzugAm}
>
<Input name="einzugAm" type="date" defaultValue={werte.einzugAm} />
</Feld>
<Feld id="auszugAm" beschriftung="Auszug am" fehler={zustand.felder?.auszugAm}>
<Input name="auszugAm" type="date" defaultValue={werte.auszugAm} />
</Feld>
<Feld
id="kuendigungsfristTage"
beschriftung="Kündigungsfrist in Tagen"
erklaerung="Nach dem WBVG kann die Bewohnerseite spätestens am dritten Werktag eines Monats zum Monatsende kündigen. Trag ein, was im Vertrag steht."
fehler={zustand.felder?.kuendigungsfristTage}
>
<Input
name="kuendigungsfristTage"
inputMode="numeric"
defaultValue={werte.kuendigungsfristTage}
/>
</Feld>
</Feldgruppe>
<Feldgruppe
titel="Aktuelle Sätze im Monat"
beschreibung="Die vier Bestandteile der Heimrechnung. Historische Werte bleiben in der monatlichen Gegenüberstellung erhalten."
>
<Feld
id="pflegeEigenanteil"
beschriftung="Pflegebedingter Eigenanteil (EEE)"
erklaerung="Der einrichtungseinheitliche Eigenanteil — für alle Pflegegrade ab 2 gleich hoch. Nur auf ihn wirkt der Leistungszuschlag."
fehler={zustand.felder?.pflegeEigenanteil}
>
<Input
name="pflegeEigenanteil"
inputMode="decimal"
defaultValue={werte.pflegeEigenanteil}
placeholder="1.450,00"
/>
</Feld>
<Feld
id="unterkunftVerpflegung"
beschriftung="Unterkunft und Verpflegung"
fehler={zustand.felder?.unterkunftVerpflegung}
>
<Input
name="unterkunftVerpflegung"
inputMode="decimal"
defaultValue={werte.unterkunftVerpflegung}
/>
</Feld>
<Feld
id="investitionskosten"
beschriftung="Investitionskosten"
erklaerung="Anteil an Gebäude und Ausstattung. Wird vom Leistungszuschlag nicht erfasst."
fehler={zustand.felder?.investitionskosten}
>
<Input
name="investitionskosten"
inputMode="decimal"
defaultValue={werte.investitionskosten}
/>
</Feld>
<Feld
id="ausbildungsumlage"
beschriftung="Ausbildungsumlage"
fehler={zustand.felder?.ausbildungsumlage}
>
<Input
name="ausbildungsumlage"
inputMode="decimal"
defaultValue={werte.ausbildungsumlage}
/>
</Feld>
</Feldgruppe>
<Feld
id="notizen"
beschriftung="Notizen"
erklaerung="Wird verschlüsselt gespeichert. Hier gehört hinein, was der Heimvertrag zu Abwesenheitstagen regelt."
fehler={zustand.felder?.notizen}
>
<Textarea name="notizen" defaultValue={werte.notizen} rows={4} maxLength={4000} />
</Feld>
<Absendeknopf>Heimvertrag speichern</Absendeknopf>
</form>
)
}
+84
View File
@@ -0,0 +1,84 @@
import Link from 'next/link'
import { Syringe } 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 { IMPFPLAN_STAND } from '@/lib/pflege/impfungen'
import { formatDatum } from '@/lib/utils'
export type ImpfZeile = {
schluessel: string
bezeichnung: string
begruendung: string
letzteImpfung: string | null
faelligAm: string | null
lage: 'unbekannt' | 'faellig' | 'bald' | 'erledigt'
text: string
}
const VARIANTE = {
unbekannt: 'gelb',
faellig: 'rot',
bald: 'orange',
erledigt: 'gruen',
} as const
const LAGE_TEXT = {
unbekannt: 'nicht dokumentiert',
faellig: 'fällig',
bald: 'bald fällig',
erledigt: 'aktuell',
} as const
export function Impfstatus({ zeilen }: { zeilen: ImpfZeile[] }) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Syringe className="size-5" aria-hidden="true" />
Impfungen
</CardTitle>
<CardDescription>
Bei COPD ausdrücklich empfohlen: Jede Atemwegsinfektion kann eine Exazerbation
auslösen, und jede Exazerbation kostet Lungenfunktion, die nicht zurückkommt. Stand
der zugrunde liegenden STIKO-Empfehlungen: {IMPFPLAN_STAND}.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<ul className="space-y-2">
{zeilen.map((zeile) => (
<li key={zeile.schluessel} className="rounded-md border p-3">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<span className="font-medium">{zeile.bezeichnung}</span>
<Badge variant={VARIANTE[zeile.lage]}>{LAGE_TEXT[zeile.lage]}</Badge>
</div>
<p className="mt-1 text-sm text-muted-foreground">{zeile.begruendung}</p>
<p className="mt-1 text-sm">
{zeile.letzteImpfung
? `Zuletzt am ${formatDatum(zeile.letzteImpfung)}. ${zeile.text}`
: zeile.text}
{zeile.faelligAm && ` (${formatDatum(zeile.faelligAm)})`}
</p>
</li>
))}
</ul>
<div className="flex flex-wrap items-center gap-3">
<Button asChild variant="outline" size="sm">
<Link href="/pflege/verlauf/neu?art=IMPFUNG">Impfung eintragen</Link>
</Button>
<span className="text-sm text-muted-foreground">
Erkannt wird die Impfung am Titel — schreib „Grippeimpfung“, „Pneumokokken“,
„COVID-19“ oder „RSV“ hinein.
</span>
</div>
<p className="text-sm text-muted-foreground">
Ob geimpft wird, entscheidet die betreute Person. Kann sie es nicht mehr, entscheidest
du nach ihrem Willen — nach ärztlicher Aufklärung und dokumentiert.
</p>
</CardContent>
</Card>
)
}
@@ -0,0 +1,178 @@
import Link from 'next/link'
import { ArrowUpRight, TrendingUp } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Rechtshinweis } from '@/components/rechtshinweis'
import {
LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE,
LEISTUNGSZUSCHLAG_STAFFEL,
LEISTUNGSZUSCHLAG_STAND,
} from '@/lib/pflege/konstanten'
import { formatCent, formatDatum } from '@/lib/utils'
/**
* Anzeige des Leistungszuschlags - reine Darstellung.
*
* Die monatliche Gegenueberstellung von Heimrechnung und Deckungsquellen
* baut das Sozialhilfemodul. Hier wird nur gezeigt, in welcher Stufe die
* betreute Person gerade ist und wann die naechste greift, und dann auf
* /sozialhilfe/heimkosten verlinkt.
*/
export type ZuschlagAnzeige = {
berechenbar: boolean
verweildauerMonate: number
prozent: number
stufeBezeichnung: string | null
zuschlagCent: number | null
verbleibtCent: number | null
eigenanteilCent: number | null
naechsteStufe: {
prozent: number
bezeichnung: string
abDatum: string
tageBis: number
mehrCent: number | null
} | null
hinweise: string[]
}
export function LeistungszuschlagKarte({ zuschlag }: { zuschlag: ZuschlagAnzeige }) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="size-5" aria-hidden="true" />
Leistungszuschlag
</CardTitle>
<CardDescription>
Die Pflegekasse übernimmt einen wachsenden Anteil des pflegebedingten Eigenanteils,
je länger jemand in der Einrichtung lebt. Beantragen muss man ihn nicht — die
Einrichtung rechnet ihn ab. Genau deshalb fällt es niemandem auf, wenn eine Stufe
nicht umgestellt wurde.
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
{zuschlag.berechenbar ? (
<>
<dl className="grid gap-3 sm:grid-cols-3">
<div className="rounded-md border p-3">
<dt className="text-sm text-muted-foreground">Verweildauer</dt>
<dd className="text-lg font-semibold">
{zuschlag.verweildauerMonate} Monate
</dd>
{zuschlag.stufeBezeichnung && (
<dd className="text-sm text-muted-foreground">{zuschlag.stufeBezeichnung}</dd>
)}
</div>
<div className="rounded-md border p-3">
<dt className="text-sm text-muted-foreground">Aktuelle Stufe</dt>
<dd className="text-lg font-semibold">{zuschlag.prozent} %</dd>
<dd className="text-sm text-muted-foreground">
{zuschlag.zuschlagCent !== null
? `${formatCent(zuschlag.zuschlagCent)} im Monat`
: 'Betrag unbekannt'}
</dd>
</div>
<div className="rounded-md border p-3">
<dt className="text-sm text-muted-foreground">Verbleibender Pflegeanteil</dt>
<dd className="text-lg font-semibold">
{formatCent(zuschlag.verbleibtCent)}
</dd>
<dd className="text-sm text-muted-foreground">
von {formatCent(zuschlag.eigenanteilCent)}
</dd>
</div>
</dl>
{zuschlag.naechsteStufe && (
<div className="rounded-md border border-ampel-gruen/40 bg-ampel-gruen/5 p-4">
<p className="flex flex-wrap items-center gap-2 font-medium">
<ArrowUpRight className="size-4 text-ampel-gruen" aria-hidden="true" />
Nächste Stufe: {zuschlag.naechsteStufe.prozent} % ab dem{' '}
{formatDatum(zuschlag.naechsteStufe.abDatum)}
<Badge variant="gruen">
{zuschlag.naechsteStufe.tageBis > 0
? `in ${zuschlag.naechsteStufe.tageBis} Tagen`
: 'greift bereits'}
</Badge>
</p>
{zuschlag.naechsteStufe.mehrCent !== null && (
<p className="mt-1 text-sm text-muted-foreground">
Das sind{' '}
<strong className="text-foreground">
{formatCent(zuschlag.naechsteStufe.mehrCent)}
</strong>{' '}
mehr im Monat, die die Pflegekasse übernimmt. Prüf die erste Heimrechnung
nach diesem Datum daraufhin — und wenn die Stufe fehlt, reklamier sie beim
Träger.
</p>
)}
</div>
)}
</>
) : (
<p className="text-sm text-muted-foreground">
{zuschlag.hinweise[0] ?? 'Ohne Einzugsdatum lässt sich nichts berechnen.'}
</p>
)}
<div>
<h3 className="mb-2 text-sm font-semibold">
Staffel nach Verweildauer (Stand {LEISTUNGSZUSCHLAG_STAND})
</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Zeitraum</TableHead>
<TableHead>Monate</TableHead>
<TableHead className="text-right">Zuschlag</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{LEISTUNGSZUSCHLAG_STAFFEL.map((stufe) => (
<TableRow
key={stufe.abMonat}
className={stufe.prozent === zuschlag.prozent && zuschlag.berechenbar ? 'bg-muted/60' : undefined}
>
<TableCell>{stufe.bezeichnung}</TableCell>
<TableCell className="tabular-nums">
{stufe.bisMonat === null
? `ab ${stufe.abMonat}`
: `${stufe.abMonat}–${stufe.bisMonat}`}
</TableCell>
<TableCell className="text-right font-medium tabular-nums">
{stufe.prozent} %
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<p className="mt-2 text-sm text-muted-foreground">
Der Gesetzgeber ändert diese Staffel: Zum 01.01.2024 wurde sie von 5/25/45/70 auf
15/30/50/75 Prozent angehoben. Prüf den Stand, bevor du mit den Zahlen argumentierst.
</p>
</div>
<ul className="space-y-1.5 text-sm text-muted-foreground">
{zuschlag.hinweise.map((hinweis) => (
<li key={hinweis}>{hinweis}</li>
))}
</ul>
<div className="flex flex-wrap items-center gap-3">
<Button asChild variant="outline">
<Link href="/sozialhilfe/heimkosten">Monatliche Gegenüberstellung ansehen</Link>
</Button>
<span className="text-sm text-muted-foreground">
Heimrechnung gegen Deckungsquellen — die führt das Sozialhilfemodul.
</span>
</div>
<Rechtshinweis rechtsgrundlage={LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE} />
</CardContent>
</Card>
)
}
+200
View File
@@ -0,0 +1,200 @@
'use client'
import { useActionState, useState } from 'react'
import Link from 'next/link'
import { Activity, CalendarClock, Pencil, Stethoscope } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Feld } from '@/components/feld'
import { Rechtshinweis } from '@/components/rechtshinweis'
import { Datenzeile, Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
import {
PFLEGEGRAD_BESCHRIFTUNG,
PFLEGEGRAD_RECHTSGRUNDLAGE_BEGUTACHTUNG,
} from '@/lib/pflege/konstanten'
import { hoeherstufungsterminSetzen } from '@/server/actions/pflege'
import { formatDatum } from '@/lib/utils'
import type { Formularzustand } from '@/lib/validierung/substanz'
export type BegutachtungZeile = {
id: string
datum: string
titel: string
einrichtung: string | null
}
export function PflegegradKarte({
betreuteId,
pflegegrad,
pflegegradSeit,
pflegegradBescheidVom,
pruefungAm,
vorschlagPruefungAm,
begutachtungen,
verschlechterung,
darfSchreiben,
}: {
betreuteId: string
pflegegrad: number | null
pflegegradSeit: string | null
pflegegradBescheidVom: string | null
pruefungAm: string | null
vorschlagPruefungAm: string
begutachtungen: BegutachtungZeile[]
verschlechterung: string | null
darfSchreiben: boolean
}) {
const [zustand, aktion] = useActionState<Formularzustand, FormData>(
hoeherstufungsterminSetzen,
{},
)
const [offen, setOffen] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="size-5" aria-hidden="true" />
Pflegegrad
</CardTitle>
<CardDescription>
Der aktuelle Grad, seine Historie und der Termin, an dem geprüft wird, ob eine
Höherstufung sinnvoll ist.
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
<div className="flex flex-wrap items-center gap-3">
{pflegegrad ? (
<Badge variant="default" className="text-sm">
{PFLEGEGRAD_BESCHRIFTUNG[pflegegrad] ?? `Pflegegrad ${pflegegrad}`}
</Badge>
) : (
<Badge variant="gelb">kein Pflegegrad erfasst</Badge>
)}
<Button asChild variant="ghost" size="sm">
<Link href="/stammdaten/bearbeiten">
<Pencil aria-hidden="true" />
In den Stammdaten ändern
</Link>
</Button>
</div>
<dl className="divide-y">
<Datenzeile beschriftung="Pflegegrad seit">{formatDatum(pflegegradSeit)}</Datenzeile>
<Datenzeile beschriftung="Bescheid vom">
{formatDatum(pflegegradBescheidVom)}
</Datenzeile>
<Datenzeile beschriftung="Nächste Überprüfung">
{pruefungAm ? formatDatum(pruefungAm) : 'kein Termin gesetzt'}
</Datenzeile>
</dl>
<Alert variant="hinweis">
<Stethoscope aria-hidden="true" />
<AlertTitle>COPD verschlechtert sich in Schüben</AlertTitle>
<AlertDescription>
<p>
Eine chronisch obstruktive Lungenerkrankung schreitet fort. Nimmt der Hilfebedarf
zu — mehr Unterstützung beim Waschen und Ankleiden, Atemnot schon bei kleinen
Wegen, häufigere Schübe, Sauerstoff über mehr Stunden am Tag —, rechtfertigt das
einen Antrag auf Neubegutachtung. Ein solcher Antrag ist jederzeit möglich und
kostet nichts; die Pflegekasse muss ihn bearbeiten.
</p>
{verschlechterung && (
<p className="mt-2 font-medium text-foreground">{verschlechterung}</p>
)}
<p className="mt-2">
Die Zahlen für die Begründung stehen im{' '}
<Link href="/pflege/verlauf" className="underline">
Gesundheitsverlauf
</Link>
. Nimm ein Pflegetagebuch über zwei Wochen dazu — das überzeugt Gutachter mehr
als jede Schilderung im Gespräch.
</p>
</AlertDescription>
</Alert>
{begutachtungen.length > 0 && (
<div>
<h3 className="mb-2 text-sm font-semibold">Bisherige Begutachtungen</h3>
<ul className="space-y-2">
{begutachtungen.map((b) => (
<li
key={b.id}
className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3"
>
<span>
<span className="font-medium">{b.titel}</span>
{b.einrichtung && (
<span className="block text-sm text-muted-foreground">{b.einrichtung}</span>
)}
</span>
<span className="text-sm text-muted-foreground">{formatDatum(b.datum)}</span>
</li>
))}
</ul>
</div>
)}
{darfSchreiben && (
<div className="rounded-md border p-4">
{!offen ? (
<Button type="button" variant="outline" onClick={() => setOffen(true)}>
<CalendarClock aria-hidden="true" />
{pruefungAm ? 'Prüftermin verschieben' : 'Prüftermin setzen'}
</Button>
) : (
<form action={aktion} className="space-y-4" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
<Zustandsmeldung zustand={zustand} />
<Feld
id="pruefungAm"
beschriftung="Pflegegrad überprüfen am"
erklaerung="Wird als Frist angelegt und erscheint im Fristenmodul. Ein halbes Jahr Abstand ist ein guter Erfahrungswert."
fehler={zustand.felder?.pruefungAm}
pflicht
>
<Input
name="pruefungAm"
type="date"
defaultValue={pruefungAm ?? vorschlagPruefungAm}
required
/>
</Feld>
<Feld
id="hoeherstufung-begruendung"
beschriftung="Worauf soll ich dann achten?"
fehler={zustand.felder?.begruendung}
>
<Textarea
name="begruendung"
rows={3}
maxLength={2000}
placeholder="z. B. Sauerstoffbedarf seit Februar auf 16 Stunden gestiegen, zweite Exazerbation im Jahr"
/>
</Feld>
<div className="flex flex-wrap gap-2">
<Absendeknopf>Termin setzen</Absendeknopf>
<Button type="button" variant="ghost" onClick={() => setOffen(false)}>
Abbrechen
</Button>
</div>
</form>
)}
</div>
)}
<Rechtshinweis rechtsgrundlage={PFLEGEGRAD_RECHTSGRUNDLAGE_BEGUTACHTUNG} />
</CardContent>
</Card>
)
}
@@ -0,0 +1,90 @@
import { BarChart3 } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { COPD_HINWEIS } from '@/lib/pflege/konstanten'
export type AuswertungsZeile = {
jahr: number
exazerbationen: number
krankenhausaufenthalte: number
krankenhaustage: number
abwesenheitstage: number
stuerze: number
satz: string
}
/**
* Die zwei Zahlen, die in den Jahresbericht und in jeden
* Hoeherstufungsantrag wandern.
*/
export function VerlaufAuswertung({
zeilen,
verschlechterung,
}: {
zeilen: AuswertungsZeile[]
verschlechterung: string | null
}) {
if (zeilen.length === 0) return null
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="size-5" aria-hidden="true" />
Auswertung je Jahr
</CardTitle>
<CardDescription>{COPD_HINWEIS}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Jahr</TableHead>
<TableHead className="text-right">Exazerbationen</TableHead>
<TableHead className="text-right">Krankenhaus</TableHead>
<TableHead className="text-right">Krankenhaustage</TableHead>
<TableHead className="text-right">Abwesenheitstage</TableHead>
<TableHead className="text-right">Stürze</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{zeilen.map((zeile) => (
<TableRow key={zeile.jahr}>
<TableCell className="font-medium">{zeile.jahr}</TableCell>
<TableCell className="text-right tabular-nums">{zeile.exazerbationen}</TableCell>
<TableCell className="text-right tabular-nums">
{zeile.krankenhausaufenthalte}
</TableCell>
<TableCell className="text-right tabular-nums">{zeile.krankenhaustage}</TableCell>
<TableCell className="text-right tabular-nums">{zeile.abwesenheitstage}</TableCell>
<TableCell className="text-right tabular-nums">{zeile.stuerze}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{verschlechterung && (
<p className="rounded-md border border-ampel-orange/40 bg-ampel-orange/10 p-3 text-sm">
{verschlechterung}
</p>
)}
<div>
<h3 className="mb-2 text-sm font-semibold">Für den Jahresbericht</h3>
<ul className="space-y-1.5">
{zeilen.map((zeile) => (
<li key={zeile.jahr} className="rounded-md border bg-muted/40 p-3 text-sm">
{zeile.satz}
</li>
))}
</ul>
<p className="mt-2 text-sm text-muted-foreground">
Bewusst ohne Diagnosen formuliert: Der Jahresbericht geht ans Betreuungsgericht,
und dort gehört nur hinein, was für die Betreuung erheblich ist.
</p>
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,167 @@
'use client'
import type { GesundheitsArt } from '@prisma/client'
import Link from 'next/link'
import {
Activity,
BedDouble,
HeartPulse,
PersonStanding,
Pill,
Plus,
Stethoscope,
Syringe,
Trash2,
Wind,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Leerzustand } from '@/components/seiten-kopf'
import { GESUNDHEITS_ART_BESCHRIFTUNG } from '@/lib/pflege/konstanten'
import { gesundheitsereignisLoeschen } from '@/server/actions/pflege'
import { formatDatum } from '@/lib/utils'
export type VerlaufZeile = {
id: string
datum: string
bisDatum: string | null
art: GesundheitsArt
titel: string
behandler: string | null
einrichtung: string | null
abwesenheitstage: number | null
beschreibung: string | null
}
const SYMBOL: Partial<Record<GesundheitsArt, typeof Activity>> = {
EXAZERBATION: Wind,
KRANKENHAUS: BedDouble,
SAUERSTOFFVERSORGUNG: Wind,
MEDIKATIONSAENDERUNG: Pill,
INHALATION: Wind,
STURZ: PersonStanding,
IMPFUNG: Syringe,
ARZTBESUCH: Stethoscope,
PFLEGEBEGUTACHTUNG: Activity,
}
const HERVORGEHOBEN: GesundheitsArt[] = ['EXAZERBATION', 'KRANKENHAUS', 'STURZ']
export function VerlaufZeitstrahl({
betreuteId,
ereignisse,
darfSchreiben,
}: {
betreuteId: string
ereignisse: VerlaufZeile[]
darfSchreiben: boolean
}) {
if (ereignisse.length === 0) {
return (
<Leerzustand
titel="Noch nichts erfasst"
beschreibung="Fang mit dem an, was zuletzt war: der letzte Krankenhausaufenthalt, die letzte Exazerbation, die aktuelle Sauerstoffversorgung. Rückwirkend erfassen ist mühsam, aber die Zahlen brauchst du für den Jahresbericht und für jede Höherstufung."
icon={HeartPulse}
aktion={
darfSchreiben && (
<Button asChild>
<Link href="/pflege/verlauf/neu">
<Plus aria-hidden="true" />
Ereignis erfassen
</Link>
</Button>
)
}
/>
)
}
// Nach Jahr gruppieren - so liest sich der Zeitstrahl wie eine Akte.
const jahre = new Map<number, VerlaufZeile[]>()
for (const ereignis of ereignisse) {
const jahr = new Date(ereignis.datum).getUTCFullYear()
const liste = jahre.get(jahr)
if (liste) liste.push(ereignis)
else jahre.set(jahr, [ereignis])
}
return (
<div className="space-y-8">
{[...jahre.entries()].map(([jahr, liste]) => (
<section key={jahr} className="space-y-3">
<h2 className="text-lg font-semibold">{jahr}</h2>
<ol className="space-y-3 border-l-2 border-border pl-5">
{liste.map((ereignis) => {
const Icon = SYMBOL[ereignis.art] ?? HeartPulse
const wichtig = HERVORGEHOBEN.includes(ereignis.art)
return (
<li key={ereignis.id} className="relative">
<span
className={
wichtig
? 'absolute -left-[1.6rem] top-4 size-3 rounded-full bg-ampel-orange ring-4 ring-background'
: 'absolute -left-[1.6rem] top-4 size-3 rounded-full bg-muted-foreground/40 ring-4 ring-background'
}
aria-hidden="true"
/>
<Card>
<CardHeader className="pb-3">
<div className="flex items-start gap-3">
<Icon className="mt-0.5 size-5 shrink-0 text-muted-foreground" aria-hidden="true" />
<div className="min-w-0 flex-1 space-y-1.5">
<CardTitle className="text-base">{ereignis.titel}</CardTitle>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Badge variant={wichtig ? 'orange' : 'outline'}>
{GESUNDHEITS_ART_BESCHRIFTUNG[ereignis.art]}
</Badge>
<span>
{formatDatum(ereignis.datum)}
{ereignis.bisDatum && ` bis ${formatDatum(ereignis.bisDatum)}`}
</span>
{ereignis.abwesenheitstage !== null && (
<span>{ereignis.abwesenheitstage} Abwesenheitstage</span>
)}
{ereignis.behandler && <span>{ereignis.behandler}</span>}
{ereignis.einrichtung && <span>{ereignis.einrichtung}</span>}
</div>
</div>
{darfSchreiben && (
<form
action={async () => {
await gesundheitsereignisLoeschen(betreuteId, ereignis.id)
}}
>
<Button
type="submit"
variant="ghost"
size="icon"
aria-label={`„${ereignis.titel}“ in den Papierkorb legen`}
>
<Trash2 aria-hidden="true" />
</Button>
</form>
)}
</div>
</CardHeader>
{ereignis.beschreibung && (
<CardContent className="pt-0">
<p className="whitespace-pre-line text-sm text-muted-foreground">
{ereignis.beschreibung}
</p>
</CardContent>
)}
</Card>
</li>
)
})}
</ol>
</section>
))}
</div>
)
}
+70
View File
@@ -0,0 +1,70 @@
'use client'
import { useActionState } from 'react'
import { Textarea } from '@/components/ui/textarea'
import { Feld } from '@/components/feld'
import { Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
import { ermitteltenWillenSpeichern } from '@/server/actions/pflege'
import type { Formularzustand } from '@/lib/validierung/substanz'
/**
* Der nach § 1821 BGB zu ermittelnde Wille.
*
* Der Erklaerungstext ist absichtlich lang: Diese Vorschrift hat die
* Betreuung 2023 vom Kopf auf die Fuesse gestellt, und wer sie nicht
* kennt, betreut immer noch nach dem alten "objektiven Wohl".
*/
export function WilleFormular({
betreuteId,
ermittelterWille,
darfSchreiben,
}: {
betreuteId: string
ermittelterWille: string | null
darfSchreiben: boolean
}) {
const [zustand, aktion] = useActionState<Formularzustand, FormData>(
ermitteltenWillenSpeichern,
{},
)
if (!darfSchreiben) {
return ermittelterWille ? (
<p className="whitespace-pre-line rounded-md border bg-muted/40 p-3 text-sm">
{ermittelterWille}
</p>
) : (
<p className="text-sm text-muted-foreground">Noch nichts hinterlegt.</p>
)
}
return (
<form action={aktion} className="space-y-4" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
<Zustandsmeldung zustand={zustand} />
<Feld
id="ermittelterWille"
beschriftung="Ermittelter Wille"
erklaerung="Wird verschlüsselt gespeichert. Halte fest, woher du es weißt — aus einem Gespräch, aus früheren Äußerungen, von Angehörigen — und wann. Ein Wille ohne Herkunft ist im Zweifel deine eigene Meinung."
fehler={zustand.felder?.ermittelterWille}
>
<Textarea
name="ermittelterWille"
defaultValue={ermittelterWille ?? ''}
rows={12}
maxLength={8000}
placeholder={
'Wohnen: Möchte nicht ins Krankenhaus, wenn es sich vermeiden lässt (Gespräch am 14.03.2026).\n' +
'Haus: Hängt am Elternhaus, will es nicht verkauft sehen — hat das mehrfach gesagt, zuletzt im Januar.\n' +
'Behandlung: Keine künstliche Beatmung (Patientenverfügung von 2019, im Gespräch bestätigt).\n' +
'Alltag: Besuch am Sonntagnachmittag, kein Fernsehen im Zimmer, eigene Bettwäsche.'
}
/>
</Feld>
<Absendeknopf>Speichern</Absendeknopf>
</form>
)
}
@@ -0,0 +1,273 @@
'use client'
import { useActionState } from 'react'
import { CheckCircle2, FileWarning, Gavel, Scale, TriangleAlert } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Select } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { Feld } from '@/components/feld'
import { Rechtshinweis } from '@/components/rechtshinweis'
import { Absendeknopf, Textbaustein, Zustandsmeldung } from '@/components/substanz-formular'
import { kuendigungVermerken } from '@/server/actions/vertraege'
import { formatDatum } from '@/lib/utils'
import type { Formularzustand } from '@/lib/validierung/substanz'
export type AssistentDaten = {
schreiben: string
warnungen: string[]
genehmigung: {
stellen: boolean
frage: string
rechtsgrundlage: string
hinweis: string
}
versandnachweis: {
moeglichkeiten: string[]
hinweis: string
}
plan: {
berechenbar: boolean
naechsterTermin: string | null
zugangSpaetestensAm: string | null
spaetesterAbsendetag: string | null
tageBisAbsendetag: number | null
verpasst: boolean
hinweise: string[]
}
}
export function KuendigungsAssistent({
betreuteId,
vertragId,
daten,
heuteIso,
darfSchreiben,
}: {
betreuteId: string
vertragId: string
daten: AssistentDaten
heuteIso: string
darfSchreiben: boolean
}) {
const [zustand, aktion] = useActionState<Formularzustand, FormData>(kuendigungVermerken, {})
return (
<div className="space-y-6">
{daten.warnungen.length > 0 && (
<Alert variant="warnung">
<TriangleAlert aria-hidden="true" />
<AlertTitle>Vor dem Absenden klären</AlertTitle>
<AlertDescription>
<ul className="ml-4 list-disc space-y-1.5">
{daten.warnungen.map((warnung) => (
<li key={warnung}>{warnung}</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Scale className="size-5" aria-hidden="true" />
Frist geprüft
</CardTitle>
<CardDescription>
Zu diesem Termin kann als Nächstes gekündigt werden — und bis wann der Brief dafür
raus muss.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{daten.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">Kündigung zum</dt>
<dd className="text-lg font-semibold">
{formatDatum(daten.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(daten.plan.zugangSpaetestensAm)}
</dd>
</div>
<div
className={
daten.plan.verpasst || (daten.plan.tageBisAbsendetag ?? 99) <= 30
? 'rounded-md border border-ampel-rot/40 bg-ampel-rot/5 p-3'
: 'rounded-md border p-3'
}
>
<dt className="text-sm text-muted-foreground">Brief spätestens absenden</dt>
<dd className="text-lg font-semibold">
{formatDatum(daten.plan.spaetesterAbsendetag)}
</dd>
{daten.plan.tageBisAbsendetag !== null && (
<dd className="text-sm text-muted-foreground">
{daten.plan.tageBisAbsendetag < 0
? `vor ${Math.abs(daten.plan.tageBisAbsendetag)} Tagen`
: `in ${daten.plan.tageBisAbsendetag} Tagen`}
</dd>
)}
</div>
</dl>
) : (
<p className="text-sm text-muted-foreground">
Ohne Laufzeitbeginn und Kündigungsfrist lässt sich der Termin nicht berechnen.
</p>
)}
<ul className="space-y-1.5 text-sm text-muted-foreground">
{daten.plan.hinweise.map((hinweis) => (
<li key={hinweis}>{hinweis}</li>
))}
</ul>
</CardContent>
</Card>
<Card
className={
daten.genehmigung.stellen ? 'border-ampel-orange/50 bg-ampel-orange/5' : undefined
}
>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Gavel className="size-5" aria-hidden="true" />
Genehmigungsfrage
</CardTitle>
<CardDescription>{daten.genehmigung.frage}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm">{daten.genehmigung.hinweis}</p>
<Rechtshinweis rechtsgrundlage={daten.genehmigung.rechtsgrundlage} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Kündigungsschreiben</CardTitle>
<CardDescription>
Entwurf zum Kopieren und Bearbeiten. Die App verschickt nichts.
</CardDescription>
</CardHeader>
<CardContent>
<Textbaustein
text={daten.schreiben}
beschriftung="Kündigungsschreiben"
hoehe="min-h-[36rem]"
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileWarning className="size-5" aria-hidden="true" />
Versandnachweis
</CardTitle>
<CardDescription>{daten.versandnachweis.hinweis}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ul className="ml-4 list-disc space-y-1 text-sm">
{daten.versandnachweis.moeglichkeiten.map((moeglichkeit) => (
<li key={moeglichkeit}>{moeglichkeit}</li>
))}
</ul>
{darfSchreiben && (
<form action={aktion} className="space-y-4 rounded-md border p-4" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
<input type="hidden" name="vertragId" value={vertragId} />
<Zustandsmeldung zustand={zustand} />
<div className="grid gap-4 sm:grid-cols-2">
<Feld
id="k-gekuendigtAm"
beschriftung="Versandt am"
fehler={zustand.felder?.gekuendigtAm}
pflicht
>
<Input name="gekuendigtAm" type="date" defaultValue={heuteIso} required />
</Feld>
<Feld
id="k-kuendigungZum"
beschriftung="Gekündigt zum"
erklaerung="Der Termin aus der Berechnung oben."
fehler={zustand.felder?.kuendigungZum}
>
<Input
name="kuendigungZum"
type="date"
defaultValue={daten.plan.naechsterTermin ?? ''}
/>
</Feld>
<Feld
id="k-versandart"
beschriftung="Versandart"
fehler={zustand.felder?.versandart}
pflicht
className="sm:col-span-2"
>
<Select name="versandart" defaultValue={daten.versandnachweis.moeglichkeiten[0]}>
{daten.versandnachweis.moeglichkeiten.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</Select>
</Feld>
</div>
<label className="flex items-start gap-2.5">
<Checkbox name="nachweisVorhanden" className="mt-0.5" />
<span className="text-sm">
Der Versandnachweis liegt vor und ist als Dokument hochgeladen.
{zustand.felder?.nachweisVorhanden && (
<span className="mt-1 block font-medium text-destructive">
{zustand.felder.nachweisVorhanden}
</span>
)}
</span>
</label>
<label className="flex items-start gap-2.5">
<Checkbox name="genehmigungGeklaert" className="mt-0.5" />
<span className="text-sm">
Die Genehmigungsfrage ist mit dem Rechtspfleger geklärt (§ 1850 BGB).
</span>
</label>
<Feld id="k-bemerkung" beschriftung="Bemerkung" fehler={zustand.felder?.bemerkung}>
<Textarea
name="bemerkung"
rows={2}
maxLength={2000}
placeholder="z. B. Einlieferungsnummer des Einschreibens"
/>
</Feld>
<Absendeknopf>
<span className="flex items-center gap-2">
<CheckCircle2 className="size-4" aria-hidden="true" />
Kündigung als versandt vermerken
</span>
</Absendeknopf>
<p className="text-sm text-muted-foreground">
Setzt den Status auf „gekündigt“ und legt eine Wiedervorlage an, damit die
Bestätigung der Gesellschaft nicht vergessen wird.
</p>
</form>
)}
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,67 @@
import Link from 'next/link'
import { CalendarClock, TriangleAlert } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { formatDatum } from '@/lib/utils'
export type Kuendigungswarnung = {
vertragId: string
bezeichnung: string
absendetag: string | null
tageBisAbsendetag: number | null
verpasst: boolean
variante: 'rot' | 'orange' | 'gelb' | 'gruen'
text: string
}
/**
* Die Warnkachel oben auf der Vertragsuebersicht.
*
* Sie zeigt nur, was in den naechsten 30 Tagen raus muss. Eine Liste, die
* alles zeigt, wird nicht gelesen - und dann geht genau die eine Frist
* unter, auf die es ankam.
*/
export function KuendigungsUebersicht({ warnungen }: { warnungen: Kuendigungswarnung[] }) {
if (warnungen.length === 0) return null
return (
<Card className="border-ampel-orange/40 bg-ampel-orange/5">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<TriangleAlert className="size-5 text-ampel-orange" aria-hidden="true" />
Kündigungsfrist rückt näher
</CardTitle>
<CardDescription>
Eine Kündigung wird erst mit dem Zugang wirksam. Der Absendetag ist deshalb früher
als der Fristablauf — hier ist der Postlauf schon abgezogen.
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{warnungen.map((warnung) => (
<li
key={warnung.vertragId}
className="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-background p-3"
>
<Link
href={`/vertraege/${warnung.vertragId}`}
className="font-medium hover:underline"
>
{warnung.bezeichnung}
</Link>
<span className="flex items-center gap-2">
{warnung.absendetag && (
<span className="flex items-center gap-1 text-sm text-muted-foreground">
<CalendarClock className="size-3.5" aria-hidden="true" />
Brief bis {formatDatum(warnung.absendetag)}
</span>
)}
<Badge variant={warnung.variante}>{warnung.text}</Badge>
</span>
</li>
))}
</ul>
</CardContent>
</Card>
)
}
+243
View File
@@ -0,0 +1,243 @@
'use client'
import { useActionState, useState } from 'react'
import { Coins, Mail, Plus } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select } from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea'
import { Feld } from '@/components/feld'
import { Leerzustand } from '@/components/seiten-kopf'
import { Absendeknopf, Textbaustein, Zustandsmeldung } from '@/components/substanz-formular'
import { WERTQUELLEN_VERTRAG } from '@/lib/vertraege/konstanten'
import { rueckkaufswertAnfordern, rueckkaufswertErfassen } from '@/server/actions/vertraege'
import { formatCent, formatDatum } from '@/lib/utils'
import type { Formularzustand } from '@/lib/validierung/substanz'
export type RueckkaufswertZeile = {
id: string
stichtag: string
wertCent: number
wertquelle: string
bemerkung: string | null
}
export function Rueckkaufswert({
betreuteId,
vertragId,
werte,
angefragtAm,
erinnerungFaellig,
tageSeitAnfrage,
anschreiben,
stichtagVorschlag,
darfSchreiben,
}: {
betreuteId: string
vertragId: string
werte: RueckkaufswertZeile[]
angefragtAm: string | null
erinnerungFaellig: boolean
tageSeitAnfrage: number | null
anschreiben: string
stichtagVorschlag: string
darfSchreiben: boolean
}) {
const [erfassen, erfassenAktion] = useActionState<Formularzustand, FormData>(
rueckkaufswertErfassen,
{},
)
const [anfrage, anfrageAktion] = useActionState<Formularzustand, FormData>(
rueckkaufswertAnfordern,
{},
)
const [briefOffen, setBriefOffen] = useState(false)
const [erfassenOffen, setErfassenOffen] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Coins className="size-5" aria-hidden="true" />
Rückkaufswert
</CardTitle>
<CardDescription>
Der Bezirk verlangt zu jedem Versicherungsvertrag den Rückkaufswert zu einem
Stichtag — schriftlich von der Gesellschaft, nicht geschätzt. Die Antwort dauert
erfahrungsgemäß drei bis sechs Wochen.
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
{angefragtAm && (
<Alert variant={erinnerungFaellig ? 'warnung' : 'hinweis'}>
<Mail aria-hidden="true" />
<AlertTitle>
Angefragt am {formatDatum(angefragtAm)}
{tageSeitAnfrage !== null && ` — vor ${tageSeitAnfrage} Tagen`}
</AlertTitle>
<AlertDescription>
{erinnerungFaellig
? 'Die vier Wochen sind um. Ruf an und halte das Gespräch im Kontaktprotokoll fest — schriftlich nachfassen dauert wieder vier Wochen.'
: 'Die Wiedervorlage steht in den Fristen. Kommt die Antwort früher, hak die Frist dort ab.'}
</AlertDescription>
</Alert>
)}
{werte.length === 0 ? (
<Leerzustand
titel="Noch kein Rückkaufswert erfasst"
beschreibung="Fordere ihn bei der Gesellschaft an. Frag im selben Schreiben nach der beitragsfreien Versicherungssumme — sonst musst du für den Vergleich ein zweites Mal schreiben."
icon={Coins}
aktion={
darfSchreiben && (
<Button type="button" onClick={() => setBriefOffen(true)}>
<Mail aria-hidden="true" />
Anschreiben erzeugen
</Button>
)
}
/>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Stichtag</TableHead>
<TableHead className="text-right">Rückkaufswert</TableHead>
<TableHead>Quelle</TableHead>
<TableHead>Bemerkung</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{werte.map((zeile) => (
<TableRow key={zeile.id}>
<TableCell className="whitespace-nowrap">{formatDatum(zeile.stichtag)}</TableCell>
<TableCell className="text-right font-medium tabular-nums">
{formatCent(zeile.wertCent)}
</TableCell>
<TableCell>{zeile.wertquelle}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{zeile.bemerkung ?? '–'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{darfSchreiben && (
<div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" onClick={() => setBriefOffen((o) => !o)}>
<Mail aria-hidden="true" />
{briefOffen ? 'Anschreiben schließen' : 'Rückkaufswert anfordern'}
</Button>
<Button type="button" variant="outline" onClick={() => setErfassenOffen((o) => !o)}>
<Plus aria-hidden="true" />
{erfassenOffen ? 'Eingabe schließen' : 'Mitgeteilten Wert eintragen'}
</Button>
</div>
)}
{darfSchreiben && briefOffen && (
<div className="space-y-4 rounded-md border p-4">
<Textbaustein
text={anschreiben}
beschriftung="Anschreiben an die Gesellschaft"
hoehe="min-h-[32rem]"
/>
<form action={anfrageAktion} className="space-y-4" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
<input type="hidden" name="vertragId" value={vertragId} />
<Zustandsmeldung zustand={anfrage} />
<div className="grid gap-4 sm:grid-cols-2">
<Feld
id="anfrage-stichtag"
beschriftung="Stichtag der Anfrage"
erklaerung="Der Tag, zu dem der Wert mitgeteilt werden soll. Für die Sozialhilfe meist der Tag der Antragstellung."
fehler={anfrage.felder?.stichtag}
pflicht
>
<Input
name="stichtag"
type="date"
defaultValue={stichtagVorschlag}
required
/>
</Feld>
</div>
<Absendeknopf>Anfrage vermerken und Wiedervorlage anlegen</Absendeknopf>
<p className="text-sm text-muted-foreground">
Setzt das Datum der Anfrage am Vertrag und legt eine Wiedervorlage in vier
Wochen an. Den Brief selbst schickst du von Hand ab — die App verschickt nichts.
</p>
</form>
</div>
)}
{darfSchreiben && erfassenOffen && (
<form action={erfassenAktion} className="space-y-4 rounded-md border p-4" noValidate>
<input type="hidden" name="betreuteId" value={betreuteId} />
<input type="hidden" name="vertragId" value={vertragId} />
<Zustandsmeldung zustand={erfassen} />
<div className="grid gap-4 sm:grid-cols-2">
<Feld
id="rkw-stichtag"
beschriftung="Stichtag"
fehler={erfassen.felder?.stichtag}
pflicht
>
<Input name="stichtag" type="date" required />
</Feld>
<Feld
id="rkw-wert"
beschriftung="Rückkaufswert"
fehler={erfassen.felder?.wert}
pflicht
>
<Input name="wert" inputMode="decimal" placeholder="4.812,55" required />
</Feld>
<Feld
id="rkw-quelle"
beschriftung="Quelle"
erklaerung="Woher stammt die Zahl? Eine telefonische Auskunft ist besser als nichts, aber kein Nachweis."
fehler={erfassen.felder?.wertquelle}
pflicht
className="sm:col-span-2"
>
<Select name="wertquelle" defaultValue={WERTQUELLEN_VERTRAG[0]}>
{WERTQUELLEN_VERTRAG.map((q) => (
<option key={q} value={q}>
{q}
</option>
))}
</Select>
</Feld>
</div>
<Feld
id="rkw-bemerkung"
beschriftung="Bemerkung"
erklaerung="z. B. die beitragsfreie Versicherungssumme, wenn sie mitgeteilt wurde."
fehler={erfassen.felder?.bemerkung}
>
<Textarea name="bemerkung" rows={2} maxLength={1000} />
</Feld>
<Absendeknopf>Wert aufnehmen</Absendeknopf>
</form>
)}
</CardContent>
</Card>
)
}
+1 -12
View File
@@ -28,7 +28,6 @@ export type VertragWerte = {
vertragsnummer: string
beginnAm: string
endeAm: string
laufzeitJahre: string
kuendigungsfristMonate: string
kuendigungZum: string
gekuendigtAm: string
@@ -47,7 +46,6 @@ export const LEERER_VERTRAG: VertragWerte = {
vertragsnummer: '',
beginnAm: '',
endeAm: '',
laufzeitJahre: '',
kuendigungsfristMonate: '3',
kuendigungZum: '',
gekuendigtAm: '',
@@ -133,21 +131,12 @@ export function VertragFormular({
<Feld
id="endeAm"
beschriftung="Vereinbartes Ende"
erklaerung="Nur bei fester Ablaufzeit, z. B. einer Kapitallebensversicherung."
erklaerung="Nur bei fester Ablaufzeit, z. B. einer Kapitallebensversicherung. Steht hier ein Datum, ergibt sich daraus zugleich die Laufzeit, um die sich der Vertrag verlängert — sonst wird mit einem Jahr gerechnet."
fehler={zustand.felder?.endeAm}
>
<Input name="endeAm" type="date" defaultValue={werte.endeAm} />
</Feld>
<Feld
id="laufzeitJahre"
beschriftung="Laufzeit in Jahren"
erklaerung="Um wie viel verlängert sich der Vertrag? Leer lassen für den Regelfall von einem Jahr."
fehler={zustand.felder?.laufzeitJahre}
>
<Input name="laufzeitJahre" inputMode="numeric" defaultValue={werte.laufzeitJahre} />
</Feld>
<Feld
id="kuendigungsfristMonate"
beschriftung="Kündigungsfrist in Monaten"
-1
View File
@@ -77,7 +77,6 @@ export const VertragSchema = z
120,
'Bitte die Kündigungsfrist in ganzen Monaten angeben.',
),
laufzeitJahre: optionaleZahl(1, 99, 'Bitte die Laufzeit in ganzen Jahren angeben.'),
kuendigungZum: optionalesDatum,
gekuendigtAm: optionalesDatum,
+52
View File
@@ -0,0 +1,52 @@
import type { Briefkopf } from '@/lib/vertraege/anschreiben'
/**
* Baut den Briefkopf aus Stammdaten und angemeldetem Benutzer.
*
* Die Anschrift des Betreuers steht im Datenmodell nicht - der Benutzer
* hat nur Name und E-Mail. Sie bleibt deshalb leer; im erzeugten Text
* fehlen dann schlicht die beiden Zeilen, und wer den Entwurf kopiert,
* ergaenzt sie im Briefkopf seines Textverarbeitungsprogramms ohnehin.
*/
export type BriefkopfQuelle = {
betreute: {
vorname: string
nachname: string
anrede: string | null
geburtsdatum: Date
heimName: string | null
heimStrasse: string | null
heimPlz: string | null
heimOrt: string | null
betreuungsgericht: string | null
aktenzeichenGericht: string | null
bestellungAm: Date | null
}
benutzer: { name: string }
}
export function briefkopfBauen(quelle: BriefkopfQuelle): Briefkopf {
const b = quelle.betreute
const anschrift = [
b.heimName,
b.heimStrasse,
[b.heimPlz, b.heimOrt].filter(Boolean).join(' ') || null,
]
.filter(Boolean)
.join('\n')
return {
betreuerName: quelle.benutzer.name,
betreuerStrasse: null,
betreuerPlz: null,
// Der Ort in der Datumszeile ist der Ort des Betreuers, nicht der des
// Heims - er steht nirgends im Datenmodell und bleibt deshalb leer.
betreuerOrt: null,
betreuteName: `${b.anrede ? `${b.anrede} ` : ''}${b.vorname} ${b.nachname}`.trim(),
betreuteGeburtsdatum: b.geburtsdatum,
betreuteAnschrift: anschrift || null,
betreuungsgericht: b.betreuungsgericht,
aktenzeichenGericht: b.aktenzeichenGericht,
bestellungAm: b.bestellungAm,
}
}
+20
View File
@@ -205,3 +205,23 @@ export function kuendigungsampel(plan: Kuendigungsplan): Kuendigungsampel | null
if (tage <= 60) return { variante: 'gelb', text: `Absendetag in ${tage} Tagen` }
return { variante: 'gruen', text: `Absendetag in ${tage} Tagen` }
}
/**
* Die Vertragsdauer aus Beginn und vereinbartem Ende.
*
* Das Datenmodell kennt kein Feld "Laufzeit" (siehe
* docs/schema-wuensche/substanz.md, Punkt 8). Stehen aber beide Daten im
* Vertrag, ergibt sich die Dauer daraus: Ein Vertrag vom 01.01.2018 bis
* 01.01.2021 hat eine dreijaehrige Laufzeit und verlaengert sich um
* dieselbe Spanne. Gerundet wird auf volle Jahre - ein Jahr sind 365 Tage,
* ein Schaltjahr 366, und beides soll dieselbe Laufzeit ergeben. Liegt die
* Spanne unter einem halben Jahr, bleibt es beim Standard.
*/
export function laufzeitAusVertrag(
beginnAm: Date | null,
endeAm: Date | null,
): number | null {
if (!beginnAm || !endeAm) return null
const jahre = Math.round(tageZwischen(beginnAm, endeAm) / 365.25)
return jahre >= 1 ? jahre : null
}
+26
View File
@@ -5,6 +5,7 @@ import {
kuendigungsampel,
kuendigungsplan,
KUENDIGUNG_VORWARNUNG_TAGE,
laufzeitAusVertrag,
POSTLAUF_PUFFER_TAGE,
} from '@/lib/vertraege/kuendigung'
import { kuendigungsassistent, genehmigungsfrage } from '@/lib/vertraege/kuendigungsassistent'
@@ -287,3 +288,28 @@ describe('Beiträge und Vertragsarten', () => {
expect(kannRueckkaufswertHaben('STROM')).toBe(false)
})
})
describe('Vertragsdauer aus Beginn und Ende', () => {
it('leitet die Laufzeit aus einem festen Vertragsende ab', () => {
expect(laufzeitAusVertrag(tag(2018, 1, 1), tag(2021, 1, 1))).toBe(3)
expect(laufzeitAusVertrag(tag(2020, 3, 15), tag(2021, 3, 15))).toBe(1)
})
it('gibt null zurück, wenn ein Datum fehlt oder die Spanne unter einem Jahr liegt', () => {
expect(laufzeitAusVertrag(tag(2020, 1, 1), null)).toBeNull()
expect(laufzeitAusVertrag(null, tag(2021, 1, 1))).toBeNull()
expect(laufzeitAusVertrag(tag(2020, 1, 1), tag(2020, 7, 1))).toBeNull()
})
it('lässt einen dreijährigen Vertrag um drei Jahre weiterspringen', () => {
const plan = kuendigungsplan({
beginnAm: tag(2018, 1, 1),
endeAm: tag(2021, 1, 1),
laufzeitJahre: laufzeitAusVertrag(tag(2018, 1, 1), tag(2021, 1, 1)),
kuendigungsfristMonate: 3,
heute: tag(2026, 2, 1),
})
expect(plan.naechsterTermin).toEqual(tag(2027, 1, 1))
})
})