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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user