From b69a12da8ff0e3e16703210cc9c791591d781ead Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 22 Aug 2026 06:22:24 +0200 Subject: [PATCH] feat: Immobilie, Versicherungen und Heim mit COPD-Verlauf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/(app)/immobilie/[id]/page.tsx | 2 +- src/app/(app)/pflege/heimvertrag/page.tsx | 87 +++ src/app/(app)/pflege/page.tsx | 252 ++++++++ src/app/(app)/pflege/verlauf/neu/page.tsx | 54 ++ src/app/(app)/pflege/verlauf/page.tsx | 107 ++++ src/app/(app)/pflege/wille/page.tsx | 175 +++++ .../(app)/vertraege/[id]/bearbeiten/page.tsx | 77 +++ .../(app)/vertraege/[id]/kuendigen/page.tsx | 96 +++ src/app/(app)/vertraege/[id]/page.tsx | 266 ++++++++ src/app/(app)/vertraege/neu/page.tsx | 31 + src/app/(app)/vertraege/page.tsx | 235 +++++++ src/components/pflege/abwesenheit-karte.tsx | 83 +++ src/components/pflege/ereignis-formular.tsx | 275 ++++++++ .../pflege/heimvertrag-formular.tsx | 196 ++++++ src/components/pflege/impfstatus.tsx | 84 +++ .../pflege/leistungszuschlag-karte.tsx | 178 ++++++ src/components/pflege/pflegegrad-karte.tsx | 200 ++++++ src/components/pflege/verlauf-auswertung.tsx | 90 +++ src/components/pflege/verlauf-zeitstrahl.tsx | 167 +++++ src/components/pflege/wille-formular.tsx | 70 ++ .../vertraege/kuendigungs-assistent.tsx | 273 ++++++++ .../vertraege/kuendigungs-uebersicht.tsx | 67 ++ src/components/vertraege/rueckkaufswert.tsx | 243 +++++++ src/components/vertraege/vertrag-formular.tsx | 485 +++++++------- src/lib/validierung/vertraege.ts | 343 +++++----- src/lib/vertraege/briefkopf.ts | 52 ++ src/lib/vertraege/kuendigung.ts | 434 +++++++------ tests/vertraege.test.ts | 604 +++++++++--------- 28 files changed, 4309 insertions(+), 917 deletions(-) create mode 100644 src/app/(app)/pflege/heimvertrag/page.tsx create mode 100644 src/app/(app)/pflege/page.tsx create mode 100644 src/app/(app)/pflege/verlauf/neu/page.tsx create mode 100644 src/app/(app)/pflege/verlauf/page.tsx create mode 100644 src/app/(app)/pflege/wille/page.tsx create mode 100644 src/app/(app)/vertraege/[id]/bearbeiten/page.tsx create mode 100644 src/app/(app)/vertraege/[id]/kuendigen/page.tsx create mode 100644 src/app/(app)/vertraege/[id]/page.tsx create mode 100644 src/app/(app)/vertraege/neu/page.tsx create mode 100644 src/app/(app)/vertraege/page.tsx create mode 100644 src/components/pflege/abwesenheit-karte.tsx create mode 100644 src/components/pflege/ereignis-formular.tsx create mode 100644 src/components/pflege/heimvertrag-formular.tsx create mode 100644 src/components/pflege/impfstatus.tsx create mode 100644 src/components/pflege/leistungszuschlag-karte.tsx create mode 100644 src/components/pflege/pflegegrad-karte.tsx create mode 100644 src/components/pflege/verlauf-auswertung.tsx create mode 100644 src/components/pflege/verlauf-zeitstrahl.tsx create mode 100644 src/components/pflege/wille-formular.tsx create mode 100644 src/components/vertraege/kuendigungs-assistent.tsx create mode 100644 src/components/vertraege/kuendigungs-uebersicht.tsx create mode 100644 src/components/vertraege/rueckkaufswert.tsx create mode 100644 src/lib/vertraege/briefkopf.ts diff --git a/src/app/(app)/immobilie/[id]/page.tsx b/src/app/(app)/immobilie/[id]/page.tsx index b974776..749ea6e 100644 --- a/src/app/(app)/immobilie/[id]/page.tsx +++ b/src/app/(app)/immobilie/[id]/page.tsx @@ -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} /> + + + + + +

+ 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. +

+ + ) +} diff --git a/src/app/(app)/pflege/page.tsx b/src/app/(app)/pflege/page.tsx new file mode 100644 index 0000000..32a62fc --- /dev/null +++ b/src/app/(app)/pflege/page.tsx @@ -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 ( +
+ + + +
+ } + /> + + + + + + + Einrichtung, Träger und die aktuellen Sätze. Historische Werte stehen in der + monatlichen Gegenüberstellung des Sozialhilfemoduls. + + + + {!heimvertrag ? ( + + Heimvertrag erfassen + + ) + } + /> + ) : ( +
+
+ {heimvertrag.einrichtung} + {heimvertrag.traeger ?? '–'} + + {[heimvertrag.strasse, [heimvertrag.plz, heimvertrag.ort].filter(Boolean).join(' ')] + .filter(Boolean) + .join(', ') || '–'} + + + {formatDatum(heimvertrag.vertragAm)} + + {formatDatum(heimvertrag.einzugAm)} + + {heimvertrag.kuendigungsfristTage !== null + ? `${heimvertrag.kuendigungsfristTage} Tage` + : '–'} + +
+
+ + {formatCent(heimvertrag.pflegeEigenanteilCent)} + + + {formatCent(heimvertrag.unterkunftVerpflegungCent)} + + + {formatCent(heimvertrag.investitionskostenCent)} + + + {formatCent(heimvertrag.ausbildungsumlageCent)} + + + {formatCent(gesamtCent)} + +
+ + {offenHeim?.notizen && ( +
+

{offenHeim.notizen}

+
+ )} + + {schreiben && ( +
+ +
+ )} +
+ )} +
+
+ + + + e.art === 'PFLEGEBEGUTACHTUNG') + .map((e) => ({ + id: e.id, + datum: e.datum.toISOString(), + titel: e.titel, + einrichtung: e.einrichtung, + }))} + /> + + + + ({ + 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, + }))} + /> + + ) +} diff --git a/src/app/(app)/pflege/verlauf/neu/page.tsx b/src/app/(app)/pflege/verlauf/neu/page.tsx new file mode 100644 index 0000000..0a382b9 --- /dev/null +++ b/src/app/(app)/pflege/verlauf/neu/page.tsx @@ -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 ( +
+ + + +
+ ) +} diff --git a/src/app/(app)/pflege/verlauf/page.tsx b/src/app/(app)/pflege/verlauf/page.tsx new file mode 100644 index 0000000..bdd8655 --- /dev/null +++ b/src/app/(app)/pflege/verlauf/page.tsx @@ -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 ( +
+ + +
+ ) +} diff --git a/src/app/(app)/pflege/wille/page.tsx b/src/app/(app)/pflege/wille/page.tsx new file mode 100644 index 0000000..c9d3142 --- /dev/null +++ b/src/app/(app)/pflege/wille/page.tsx @@ -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 ( +
+ + + + + + + + Seit der Betreuungsrechtsreform 2023 ist der Wille der betreuten Person die + Leitschnur des Betreuerhandelns — nicht mehr das, was objektiv vernünftig + erscheint. + + + + +

+ 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. +

+ + +
+
+ + + + + + + Die beiden Häkchen liegen an den Stammdaten. Ändern lassen sie sich dort — hier + stehen sie nur zur Übersicht. + + + +
+
+

Patientenverfügung

+ + {aktive.patientenverfuegungVorhanden ? 'vorhanden' : 'nicht vorhanden'} + +

+ 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. +

+
+
+

Vorsorgevollmacht

+ + {aktive.vorsorgevollmachtVorhanden ? 'vorhanden' : 'nicht vorhanden'} + +

+ 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. +

+
+
+ + +
+
+ + + + Abgelegte Unterlagen + + Vollmachten, Urkunden und Atteste aus der Dokumentenablage. Im Notfall zählt, dass + das Papier greifbar ist — notier deshalb immer auch, wo das Original liegt. + + + + {unterlagen.length === 0 ? ( +

+ Noch nichts abgelegt. Scanne Patientenverfügung und Vorsorgevollmacht ein und + hinterlege beim Dokument, wo das Original liegt. +

+ ) : ( +
    + {unterlagen.map((dokument) => ( +
  • + + {dokument.titel} + + + {dokument.art} + {dokument.schreibenVom && {formatDatum(dokument.schreibenVom)}} + {dokument.ablageortPapier && Original: {dokument.ablageortPapier}} + +
  • + ))} +
+ )} + + +
+
+
+ ) +} diff --git a/src/app/(app)/vertraege/[id]/bearbeiten/page.tsx b/src/app/(app)/vertraege/[id]/bearbeiten/page.tsx new file mode 100644 index 0000000..3a32a10 --- /dev/null +++ b/src/app/(app)/vertraege/[id]/bearbeiten/page.tsx @@ -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 ( +
+ + + +
+ ) +} diff --git a/src/app/(app)/vertraege/[id]/kuendigen/page.tsx b/src/app/(app)/vertraege/[id]/kuendigen/page.tsx new file mode 100644 index 0000000..e3278d1 --- /dev/null +++ b/src/app/(app)/vertraege/[id]/kuendigen/page.tsx @@ -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 ( +
+ + + +
+ ) +} diff --git a/src/app/(app)/vertraege/[id]/page.tsx b/src/app/(app)/vertraege/[id]/page.tsx new file mode 100644 index 0000000..db0f353 --- /dev/null +++ b/src/app/(app)/vertraege/[id]/page.tsx @@ -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 ( +
+ + + +
+ ) + } + /> + + + + + + + Die Vertragsnummer wird verschlüsselt gespeichert und hier nur zum Lesen angezeigt. + + + +
+ + {offen.vertragsnummer ?? '–'} + + + {VERTRAG_STATUS_BESCHRIFTUNG[vertrag.status]} + + + {formatDatum(vertrag.beginnAm)} + + + {formatDatum(vertrag.endeAm)} + + + {vertrag.kuendigungsfristMonate !== null + ? `${vertrag.kuendigungsfristMonate} Monate` + : 'nicht erfasst'} + +
+
+ + {formatCent(vertrag.beitragCent)} + {vertrag.rhythmus ? ` (${RHYTHMUS_BESCHRIFTUNG[vertrag.rhythmus]})` : ''} + + {formatCent(jahresbeitrag)} + + {formatDatum(vertrag.gekuendigtAm)} + + + {formatDatum(vertrag.kuendigungZum)} + + + {formatDatum(vertrag.rueckkaufswertAngefragtAm)} + +
+ + {offen.notizen && ( +
+

{offen.notizen}

+
+ )} +
+
+ + + + Kündigungsfrist + + Eine Kündigung wird mit dem Zugang wirksam, nicht mit dem Absenden. Deshalb ist der + Absendetag früher als der Fristablauf. + + + + {plan.berechenbar ? ( + <> +
+
+
Nächster Kündigungstermin
+
{formatDatum(plan.naechsterTermin)}
+
+
+
Zugang spätestens
+
+ {formatDatum(plan.zugangSpaetestensAm)} +
+
+
+
Brief spätestens absenden
+
+ {formatDatum(plan.spaetesterAbsendetag)} +
+ {ampel && ( +
+ {ampel.text} +
+ )} +
+
+
    + {plan.hinweise.map((hinweis) => ( +
  • {hinweis}
  • + ))} +
+ + ) : ( +

+ {plan.hinweise[0] ?? + 'Ohne Laufzeitbeginn lässt sich kein Kündigungstermin berechnen.'} +

+ )} +
+
+ + {kannRueckkaufswertHaben(vertrag.art) && ( + ({ + id: w.id, + stichtag: w.stichtag.toISOString(), + wertCent: centZuZahl(w.wertCent) ?? 0, + wertquelle: w.wertquelle, + bemerkung: w.bemerkung, + }))} + /> + )} + + + + Bewertung fürs Sozialamt + + Meine Einordnung und meine Begründung — keine Rechtsauskunft. + + + + {BEWERTUNG_BESCHRIFTUNG[vertrag.bewertungSozialhilfe]} +

+ {BEWERTUNG_ERKLAERUNG[vertrag.bewertungSozialhilfe]} +

+ {offen.bewertungBegruendung ? ( +

+ {offen.bewertungBegruendung} +

+ ) : ( +

+ Noch keine Begründung hinterlegt. Sie gehört dazu, bevor der Vertrag in einer + Aufstellung an den Bezirk auftaucht. +

+ )} + +
+
+ + ) +} diff --git a/src/app/(app)/vertraege/neu/page.tsx b/src/app/(app)/vertraege/neu/page.tsx new file mode 100644 index 0000000..0a43f0e --- /dev/null +++ b/src/app/(app)/vertraege/neu/page.tsx @@ -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 ( +
+ + + +
+ ) +} diff --git a/src/app/(app)/vertraege/page.tsx b/src/app/(app)/vertraege/page.tsx new file mode 100644 index 0000000..eae6751 --- /dev/null +++ b/src/app/(app)/vertraege/page.tsx @@ -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 ( +
+ + +
+ ) +} diff --git a/src/components/pflege/abwesenheit-karte.tsx b/src/components/pflege/abwesenheit-karte.tsx new file mode 100644 index 0000000..3bfefb8 --- /dev/null +++ b/src/components/pflege/abwesenheit-karte.tsx @@ -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 ( + + + + + + Tage außerhalb der Einrichtung, vor allem im Krankenhaus. Erfasst werden sie im + Gesundheitsverlauf; hier stehen die Jahressummen. + + + + + {jahre.length === 0 ? ( + + + Krankenhausaufenthalt erfassen + + + } + /> + ) : ( + <> + + + + Jahr + Ereignisse + Abwesenheitstage + + + + {jahre.map((jahr) => ( + + {jahr.jahr} + {jahr.ereignisse} + + {jahr.tage} + + + ))} + + insgesamt + + {gesamt} + + +
+ + )} + +

+ {ABWESENHEIT_HINWEIS} +

+
+
+ ) +} diff --git a/src/components/pflege/ereignis-formular.tsx b/src/components/pflege/ereignis-formular.tsx new file mode 100644 index 0000000..9279efd --- /dev/null +++ b/src/components/pflege/ereignis-formular.tsx @@ -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 +} + +/** Vorschlag für den Titel, damit die Schnellerfassung wirklich schnell ist. */ +const TITELVORSCHLAG: Partial> = { + 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( + gesundheitsereignisSpeichern, + {}, + ) + const [art, setArt] = useState(werte.art) + const [titelBeruehrt, setTitelBeruehrt] = useState(Boolean(werte.titel)) + + const felder = detailfelderFuer(art) + const zeitraum = art === 'KRANKENHAUS' || art === 'REHA' + + return ( +
+ + {werte.id && } + + + + + + + + + + + + + setTitelBeruehrt(true)} + /> + + + + + + + + + + + + + + + + + + + + + + + + {felder.length > 0 && ( + + {felder.map((feld) => { + const id = `detail_${feld.name}` + const wert = werte.details[feld.name] ?? '' + + if (feld.typ === 'jaNein') { + return ( +
+ +
+ ) + } + + if (feld.typ === 'auswahl') { + return ( + + + + ) + } + + if (feld.typ === 'mehrzeilig') { + return ( + +