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