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}
|
immobilieId={id}
|
||||||
darfSchreiben={schreiben}
|
darfSchreiben={schreiben}
|
||||||
status={immobilie.statusSozialhilfe}
|
status={immobilie.statusSozialhilfe}
|
||||||
begruendung={offen.statusBegruendung ?? null}
|
begruendung={offen.statusBegruendung}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Kostenkonto
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { BedDouble } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import { Leerzustand } from '@/components/seiten-kopf'
|
||||||
|
import { ABWESENHEIT_HINWEIS } from '@/lib/pflege/konstanten'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abwesenheitstage je Jahr - Erfassung laeuft ueber den
|
||||||
|
* Gesundheitsverlauf, hier steht nur die Summe.
|
||||||
|
*/
|
||||||
|
export function AbwesenheitKarte({
|
||||||
|
jahre,
|
||||||
|
gesamt,
|
||||||
|
}: {
|
||||||
|
jahre: { jahr: number; tage: number; ereignisse: number }[]
|
||||||
|
gesamt: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BedDouble className="size-5" aria-hidden="true" />
|
||||||
|
Abwesenheitstage
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Tage außerhalb der Einrichtung, vor allem im Krankenhaus. Erfasst werden sie im
|
||||||
|
Gesundheitsverlauf; hier stehen die Jahressummen.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{jahre.length === 0 ? (
|
||||||
|
<Leerzustand
|
||||||
|
titel="Noch keine Abwesenheitstage erfasst"
|
||||||
|
beschreibung="Jeder Krankenhausaufenthalt mit Aufnahme- und Entlassdatum zählt hier automatisch mit."
|
||||||
|
icon={BedDouble}
|
||||||
|
aktion={
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link href="/pflege/verlauf/neu?art=KRANKENHAUS">
|
||||||
|
Krankenhausaufenthalt erfassen
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Jahr</TableHead>
|
||||||
|
<TableHead className="text-right">Ereignisse</TableHead>
|
||||||
|
<TableHead className="text-right">Abwesenheitstage</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{jahre.map((jahr) => (
|
||||||
|
<TableRow key={jahr.jahr}>
|
||||||
|
<TableCell className="font-medium">{jahr.jahr}</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">{jahr.ereignisse}</TableCell>
|
||||||
|
<TableCell className="text-right font-medium tabular-nums">
|
||||||
|
{jahr.tage}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
<TableRow className="border-t-2">
|
||||||
|
<TableCell className="font-semibold">insgesamt</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell className="text-right font-semibold tabular-nums">{gesamt}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="rounded-md border bg-muted/40 p-3 text-sm text-muted-foreground">
|
||||||
|
{ABWESENHEIT_HINWEIS}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState, useState } from 'react'
|
||||||
|
import type { GesundheitsArt } from '@prisma/client'
|
||||||
|
import { Lock } from 'lucide-react'
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Select } from '@/components/ui/select'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Feld, Feldgruppe } from '@/components/feld'
|
||||||
|
import { Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
|
||||||
|
import { detailfelderFuer } from '@/lib/pflege/ereignis-details'
|
||||||
|
import {
|
||||||
|
EXAZERBATION_AUSLOESER,
|
||||||
|
GESUNDHEITS_ART_BESCHRIFTUNG,
|
||||||
|
SCHNELLERFASSUNG_REIHENFOLGE,
|
||||||
|
} from '@/lib/pflege/konstanten'
|
||||||
|
import { gesundheitsereignisSpeichern } from '@/server/actions/pflege'
|
||||||
|
import type { Formularzustand } from '@/lib/validierung/substanz'
|
||||||
|
|
||||||
|
export type EreignisWerte = {
|
||||||
|
id?: string
|
||||||
|
datum: string
|
||||||
|
bisDatum: string
|
||||||
|
art: GesundheitsArt
|
||||||
|
titel: string
|
||||||
|
behandler: string
|
||||||
|
einrichtung: string
|
||||||
|
abwesenheitstage: string
|
||||||
|
beschreibung: string
|
||||||
|
details: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vorschlag für den Titel, damit die Schnellerfassung wirklich schnell ist. */
|
||||||
|
const TITELVORSCHLAG: Partial<Record<GesundheitsArt, string>> = {
|
||||||
|
EXAZERBATION: 'Exazerbation',
|
||||||
|
KRANKENHAUS: 'Krankenhausaufenthalt',
|
||||||
|
SAUERSTOFFVERSORGUNG: 'Sauerstoffversorgung angepasst',
|
||||||
|
MEDIKATIONSAENDERUNG: 'Medikation geändert',
|
||||||
|
INHALATION: 'Inhalation angepasst',
|
||||||
|
STURZ: 'Sturz',
|
||||||
|
HILFSMITTEL: 'Hilfsmittel',
|
||||||
|
IMPFUNG: 'Grippeimpfung',
|
||||||
|
ARZTBESUCH: 'Arztbesuch',
|
||||||
|
PFLEGEBEGUTACHTUNG: 'Pflegebegutachtung',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EreignisFormular({
|
||||||
|
betreuteId,
|
||||||
|
werte,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
werte: EreignisWerte
|
||||||
|
}) {
|
||||||
|
const [zustand, aktion] = useActionState<Formularzustand, FormData>(
|
||||||
|
gesundheitsereignisSpeichern,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
const [art, setArt] = useState<GesundheitsArt>(werte.art)
|
||||||
|
const [titelBeruehrt, setTitelBeruehrt] = useState(Boolean(werte.titel))
|
||||||
|
|
||||||
|
const felder = detailfelderFuer(art)
|
||||||
|
const zeitraum = art === 'KRANKENHAUS' || art === 'REHA'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={aktion} className="space-y-8" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
{werte.id && <input type="hidden" name="ereignisId" value={werte.id} />}
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={zustand} />
|
||||||
|
|
||||||
|
<Alert variant="hinweis">
|
||||||
|
<Lock aria-hidden="true" />
|
||||||
|
<AlertDescription>
|
||||||
|
Gesundheitsdaten sind besondere Kategorien nach Art. 9 DSGVO. Die Beschreibung und
|
||||||
|
alle Zusatzangaben werden verschlüsselt gespeichert; im Protokoll steht später nur
|
||||||
|
der Titel, nie der Inhalt. Schreib den Titel deshalb so, dass er ohne Diagnose
|
||||||
|
auskommt.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Feldgruppe titel="Ereignis" beschreibung="Was ist passiert und wann.">
|
||||||
|
<Feld id="art" beschriftung="Art" fehler={zustand.felder?.art} pflicht>
|
||||||
|
<Select
|
||||||
|
name="art"
|
||||||
|
value={art}
|
||||||
|
onChange={(e) => setArt(e.target.value as GesundheitsArt)}
|
||||||
|
>
|
||||||
|
{SCHNELLERFASSUNG_REIHENFOLGE.map((a) => (
|
||||||
|
<option key={a} value={a}>
|
||||||
|
{GESUNDHEITS_ART_BESCHRIFTUNG[a]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="titel"
|
||||||
|
beschriftung="Titel"
|
||||||
|
erklaerung="Kurz und ohne Diagnose — er steht unverschlüsselt in der Liste."
|
||||||
|
fehler={zustand.felder?.titel}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="titel"
|
||||||
|
required
|
||||||
|
maxLength={200}
|
||||||
|
key={titelBeruehrt ? 'fest' : art}
|
||||||
|
defaultValue={titelBeruehrt ? werte.titel : (TITELVORSCHLAG[art] ?? werte.titel)}
|
||||||
|
onChange={() => setTitelBeruehrt(true)}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="datum"
|
||||||
|
beschriftung={zeitraum ? 'Aufnahme am' : 'Datum'}
|
||||||
|
fehler={zustand.felder?.datum}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input name="datum" type="date" defaultValue={werte.datum} required />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="bisDatum"
|
||||||
|
beschriftung={zeitraum ? 'Entlassung am' : 'Ende (falls Zeitraum)'}
|
||||||
|
erklaerung={
|
||||||
|
zeitraum
|
||||||
|
? 'Aus Aufnahme und Entlassung werden die Abwesenheitstage gerechnet — der Entlassungstag zählt nicht mit.'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fehler={zustand.felder?.bisDatum}
|
||||||
|
>
|
||||||
|
<Input name="bisDatum" type="date" defaultValue={werte.bisDatum} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="behandler"
|
||||||
|
beschriftung="Behandler"
|
||||||
|
erklaerung="Arzt, Pflegedienstleitung, Gutachter."
|
||||||
|
fehler={zustand.felder?.behandler}
|
||||||
|
>
|
||||||
|
<Input name="behandler" defaultValue={werte.behandler} maxLength={150} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="einrichtung"
|
||||||
|
beschriftung="Einrichtung"
|
||||||
|
fehler={zustand.felder?.einrichtung}
|
||||||
|
>
|
||||||
|
<Input name="einrichtung" defaultValue={werte.einrichtung} maxLength={150} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="abwesenheitstage"
|
||||||
|
beschriftung="Abwesenheitstage"
|
||||||
|
erklaerung="Nur ausfüllen, wenn du die Tage abweichend zählen willst — sonst werden sie aus dem Zeitraum gerechnet. Was gilt, steht im Heimvertrag."
|
||||||
|
fehler={zustand.felder?.abwesenheitstage}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="abwesenheitstage"
|
||||||
|
inputMode="numeric"
|
||||||
|
defaultValue={werte.abwesenheitstage}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
</Feldgruppe>
|
||||||
|
|
||||||
|
{felder.length > 0 && (
|
||||||
|
<Feldgruppe
|
||||||
|
titel="Zusatzangaben"
|
||||||
|
beschreibung="Die Angaben, die bei dieser Art von Ereignis später zählen. Sie werden verschlüsselt in die Beschreibung geschrieben."
|
||||||
|
>
|
||||||
|
{felder.map((feld) => {
|
||||||
|
const id = `detail_${feld.name}`
|
||||||
|
const wert = werte.details[feld.name] ?? ''
|
||||||
|
|
||||||
|
if (feld.typ === 'jaNein') {
|
||||||
|
return (
|
||||||
|
<div key={feld.name} className="sm:col-span-2">
|
||||||
|
<label className="flex items-start gap-2.5">
|
||||||
|
<Checkbox name={id} defaultChecked={wert === 'ja'} className="mt-0.5" />
|
||||||
|
<span className="text-sm">
|
||||||
|
<span className="font-medium">{feld.beschriftung}</span>
|
||||||
|
{feld.erklaerung && (
|
||||||
|
<span className="block text-muted-foreground">{feld.erklaerung}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feld.typ === 'auswahl') {
|
||||||
|
return (
|
||||||
|
<Feld
|
||||||
|
key={feld.name}
|
||||||
|
id={id}
|
||||||
|
beschriftung={feld.beschriftung}
|
||||||
|
erklaerung={feld.erklaerung}
|
||||||
|
pflicht={feld.pflicht}
|
||||||
|
>
|
||||||
|
<Select name={id} defaultValue={wert}>
|
||||||
|
<option value="">— bitte wählen —</option>
|
||||||
|
{(feld.optionen ?? []).map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Feld>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feld.typ === 'mehrzeilig') {
|
||||||
|
return (
|
||||||
|
<Feld
|
||||||
|
key={feld.name}
|
||||||
|
id={id}
|
||||||
|
beschriftung={feld.beschriftung}
|
||||||
|
erklaerung={feld.erklaerung}
|
||||||
|
className="sm:col-span-2"
|
||||||
|
>
|
||||||
|
<Textarea name={id} defaultValue={wert} rows={2} maxLength={1000} />
|
||||||
|
</Feld>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Feld
|
||||||
|
key={feld.name}
|
||||||
|
id={id}
|
||||||
|
beschriftung={
|
||||||
|
feld.einheit ? `${feld.beschriftung} (${feld.einheit})` : feld.beschriftung
|
||||||
|
}
|
||||||
|
erklaerung={feld.erklaerung}
|
||||||
|
pflicht={feld.pflicht}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name={id}
|
||||||
|
defaultValue={wert}
|
||||||
|
inputMode={feld.typ === 'zahl' ? 'decimal' : 'text'}
|
||||||
|
maxLength={200}
|
||||||
|
list={
|
||||||
|
feld.name === 'ausloeser' && art === 'EXAZERBATION'
|
||||||
|
? 'ausloeser-vorschlaege'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{art === 'EXAZERBATION' && (
|
||||||
|
<datalist id="ausloeser-vorschlaege">
|
||||||
|
{EXAZERBATION_AUSLOESER.map((a) => (
|
||||||
|
<option key={a} value={a} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
)}
|
||||||
|
</Feldgruppe>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="beschreibung"
|
||||||
|
beschriftung="Weitere Beschreibung"
|
||||||
|
erklaerung="Wird verschlüsselt gespeichert. Hier gehört hinein, was du später im Jahresbericht oder bei einer Begutachtung brauchst."
|
||||||
|
fehler={zustand.felder?.beschreibung}
|
||||||
|
>
|
||||||
|
<Textarea name="beschreibung" defaultValue={werte.beschreibung} rows={5} maxLength={4000} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Absendeknopf>{werte.id ? 'Änderungen speichern' : 'Eintrag anlegen'}</Absendeknopf>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Feld, Feldgruppe } from '@/components/feld'
|
||||||
|
import { Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
|
||||||
|
import { heimvertragSpeichern } from '@/server/actions/pflege'
|
||||||
|
import type { Formularzustand } from '@/lib/validierung/substanz'
|
||||||
|
|
||||||
|
export type HeimvertragWerte = {
|
||||||
|
einrichtung: string
|
||||||
|
traeger: string
|
||||||
|
strasse: string
|
||||||
|
plz: string
|
||||||
|
ort: string
|
||||||
|
vertragAm: string
|
||||||
|
einzugAm: string
|
||||||
|
auszugAm: string
|
||||||
|
kuendigungsfristTage: string
|
||||||
|
pflegeEigenanteil: string
|
||||||
|
unterkunftVerpflegung: string
|
||||||
|
investitionskosten: string
|
||||||
|
ausbildungsumlage: string
|
||||||
|
notizen: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LEERER_HEIMVERTRAG: HeimvertragWerte = {
|
||||||
|
einrichtung: '',
|
||||||
|
traeger: '',
|
||||||
|
strasse: '',
|
||||||
|
plz: '',
|
||||||
|
ort: '',
|
||||||
|
vertragAm: '',
|
||||||
|
einzugAm: '',
|
||||||
|
auszugAm: '',
|
||||||
|
kuendigungsfristTage: '14',
|
||||||
|
pflegeEigenanteil: '',
|
||||||
|
unterkunftVerpflegung: '',
|
||||||
|
investitionskosten: '',
|
||||||
|
ausbildungsumlage: '',
|
||||||
|
notizen: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeimvertragFormular({
|
||||||
|
betreuteId,
|
||||||
|
werte,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
werte: HeimvertragWerte
|
||||||
|
}) {
|
||||||
|
const [zustand, aktion] = useActionState<Formularzustand, FormData>(heimvertragSpeichern, {})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={aktion} className="space-y-8" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={zustand} />
|
||||||
|
|
||||||
|
<Feldgruppe titel="Einrichtung" beschreibung="Wer betreibt das Heim und wo liegt es.">
|
||||||
|
<Feld
|
||||||
|
id="einrichtung"
|
||||||
|
beschriftung="Einrichtung"
|
||||||
|
fehler={zustand.felder?.einrichtung}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input name="einrichtung" defaultValue={werte.einrichtung} required maxLength={150} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="traeger"
|
||||||
|
beschriftung="Träger"
|
||||||
|
erklaerung="Wer betreibt das Heim? Bei Streit über die Rechnung ist der Träger der Vertragspartner, nicht die Heimleitung."
|
||||||
|
fehler={zustand.felder?.traeger}
|
||||||
|
>
|
||||||
|
<Input name="traeger" defaultValue={werte.traeger} maxLength={150} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld id="strasse" beschriftung="Straße und Hausnummer" fehler={zustand.felder?.strasse}>
|
||||||
|
<Input name="strasse" defaultValue={werte.strasse} maxLength={150} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[7rem_1fr] gap-3">
|
||||||
|
<Feld id="plz" beschriftung="PLZ" fehler={zustand.felder?.plz}>
|
||||||
|
<Input name="plz" defaultValue={werte.plz} inputMode="numeric" maxLength={10} />
|
||||||
|
</Feld>
|
||||||
|
<Feld id="ort" beschriftung="Ort" fehler={zustand.felder?.ort}>
|
||||||
|
<Input name="ort" defaultValue={werte.ort} maxLength={100} />
|
||||||
|
</Feld>
|
||||||
|
</div>
|
||||||
|
</Feldgruppe>
|
||||||
|
|
||||||
|
<Feldgruppe
|
||||||
|
titel="Vertrag"
|
||||||
|
beschreibung="Das Einzugsdatum steuert den Leistungszuschlag nach § 43c SGB XI — trag es unbedingt ein."
|
||||||
|
>
|
||||||
|
<Feld id="vertragAm" beschriftung="Vertragsdatum" fehler={zustand.felder?.vertragAm}>
|
||||||
|
<Input name="vertragAm" type="date" defaultValue={werte.vertragAm} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="einzugAm"
|
||||||
|
beschriftung="Einzug am"
|
||||||
|
erklaerung="Ab diesem Tag zählt die Verweildauer für den Leistungszuschlag."
|
||||||
|
fehler={zustand.felder?.einzugAm}
|
||||||
|
>
|
||||||
|
<Input name="einzugAm" type="date" defaultValue={werte.einzugAm} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld id="auszugAm" beschriftung="Auszug am" fehler={zustand.felder?.auszugAm}>
|
||||||
|
<Input name="auszugAm" type="date" defaultValue={werte.auszugAm} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="kuendigungsfristTage"
|
||||||
|
beschriftung="Kündigungsfrist in Tagen"
|
||||||
|
erklaerung="Nach dem WBVG kann die Bewohnerseite spätestens am dritten Werktag eines Monats zum Monatsende kündigen. Trag ein, was im Vertrag steht."
|
||||||
|
fehler={zustand.felder?.kuendigungsfristTage}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="kuendigungsfristTage"
|
||||||
|
inputMode="numeric"
|
||||||
|
defaultValue={werte.kuendigungsfristTage}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
</Feldgruppe>
|
||||||
|
|
||||||
|
<Feldgruppe
|
||||||
|
titel="Aktuelle Sätze im Monat"
|
||||||
|
beschreibung="Die vier Bestandteile der Heimrechnung. Historische Werte bleiben in der monatlichen Gegenüberstellung erhalten."
|
||||||
|
>
|
||||||
|
<Feld
|
||||||
|
id="pflegeEigenanteil"
|
||||||
|
beschriftung="Pflegebedingter Eigenanteil (EEE)"
|
||||||
|
erklaerung="Der einrichtungseinheitliche Eigenanteil — für alle Pflegegrade ab 2 gleich hoch. Nur auf ihn wirkt der Leistungszuschlag."
|
||||||
|
fehler={zustand.felder?.pflegeEigenanteil}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="pflegeEigenanteil"
|
||||||
|
inputMode="decimal"
|
||||||
|
defaultValue={werte.pflegeEigenanteil}
|
||||||
|
placeholder="1.450,00"
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="unterkunftVerpflegung"
|
||||||
|
beschriftung="Unterkunft und Verpflegung"
|
||||||
|
fehler={zustand.felder?.unterkunftVerpflegung}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="unterkunftVerpflegung"
|
||||||
|
inputMode="decimal"
|
||||||
|
defaultValue={werte.unterkunftVerpflegung}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="investitionskosten"
|
||||||
|
beschriftung="Investitionskosten"
|
||||||
|
erklaerung="Anteil an Gebäude und Ausstattung. Wird vom Leistungszuschlag nicht erfasst."
|
||||||
|
fehler={zustand.felder?.investitionskosten}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="investitionskosten"
|
||||||
|
inputMode="decimal"
|
||||||
|
defaultValue={werte.investitionskosten}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="ausbildungsumlage"
|
||||||
|
beschriftung="Ausbildungsumlage"
|
||||||
|
fehler={zustand.felder?.ausbildungsumlage}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="ausbildungsumlage"
|
||||||
|
inputMode="decimal"
|
||||||
|
defaultValue={werte.ausbildungsumlage}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
</Feldgruppe>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="notizen"
|
||||||
|
beschriftung="Notizen"
|
||||||
|
erklaerung="Wird verschlüsselt gespeichert. Hier gehört hinein, was der Heimvertrag zu Abwesenheitstagen regelt."
|
||||||
|
fehler={zustand.felder?.notizen}
|
||||||
|
>
|
||||||
|
<Textarea name="notizen" defaultValue={werte.notizen} rows={4} maxLength={4000} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Absendeknopf>Heimvertrag speichern</Absendeknopf>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { Syringe } from 'lucide-react'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { IMPFPLAN_STAND } from '@/lib/pflege/impfungen'
|
||||||
|
import { formatDatum } from '@/lib/utils'
|
||||||
|
|
||||||
|
export type ImpfZeile = {
|
||||||
|
schluessel: string
|
||||||
|
bezeichnung: string
|
||||||
|
begruendung: string
|
||||||
|
letzteImpfung: string | null
|
||||||
|
faelligAm: string | null
|
||||||
|
lage: 'unbekannt' | 'faellig' | 'bald' | 'erledigt'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const VARIANTE = {
|
||||||
|
unbekannt: 'gelb',
|
||||||
|
faellig: 'rot',
|
||||||
|
bald: 'orange',
|
||||||
|
erledigt: 'gruen',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const LAGE_TEXT = {
|
||||||
|
unbekannt: 'nicht dokumentiert',
|
||||||
|
faellig: 'fällig',
|
||||||
|
bald: 'bald fällig',
|
||||||
|
erledigt: 'aktuell',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function Impfstatus({ zeilen }: { zeilen: ImpfZeile[] }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Syringe className="size-5" aria-hidden="true" />
|
||||||
|
Impfungen
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Bei COPD ausdrücklich empfohlen: Jede Atemwegsinfektion kann eine Exazerbation
|
||||||
|
auslösen, und jede Exazerbation kostet Lungenfunktion, die nicht zurückkommt. Stand
|
||||||
|
der zugrunde liegenden STIKO-Empfehlungen: {IMPFPLAN_STAND}.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{zeilen.map((zeile) => (
|
||||||
|
<li key={zeile.schluessel} className="rounded-md border p-3">
|
||||||
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||||
|
<span className="font-medium">{zeile.bezeichnung}</span>
|
||||||
|
<Badge variant={VARIANTE[zeile.lage]}>{LAGE_TEXT[zeile.lage]}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{zeile.begruendung}</p>
|
||||||
|
<p className="mt-1 text-sm">
|
||||||
|
{zeile.letzteImpfung
|
||||||
|
? `Zuletzt am ${formatDatum(zeile.letzteImpfung)}. ${zeile.text}`
|
||||||
|
: zeile.text}
|
||||||
|
{zeile.faelligAm && ` (${formatDatum(zeile.faelligAm)})`}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Button asChild variant="outline" size="sm">
|
||||||
|
<Link href="/pflege/verlauf/neu?art=IMPFUNG">Impfung eintragen</Link>
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Erkannt wird die Impfung am Titel — schreib „Grippeimpfung“, „Pneumokokken“,
|
||||||
|
„COVID-19“ oder „RSV“ hinein.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Ob geimpft wird, entscheidet die betreute Person. Kann sie es nicht mehr, entscheidest
|
||||||
|
du nach ihrem Willen — nach ärztlicher Aufklärung und dokumentiert.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { ArrowUpRight, TrendingUp } from 'lucide-react'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||||
|
import {
|
||||||
|
LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE,
|
||||||
|
LEISTUNGSZUSCHLAG_STAFFEL,
|
||||||
|
LEISTUNGSZUSCHLAG_STAND,
|
||||||
|
} from '@/lib/pflege/konstanten'
|
||||||
|
import { formatCent, formatDatum } from '@/lib/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anzeige des Leistungszuschlags - reine Darstellung.
|
||||||
|
*
|
||||||
|
* Die monatliche Gegenueberstellung von Heimrechnung und Deckungsquellen
|
||||||
|
* baut das Sozialhilfemodul. Hier wird nur gezeigt, in welcher Stufe die
|
||||||
|
* betreute Person gerade ist und wann die naechste greift, und dann auf
|
||||||
|
* /sozialhilfe/heimkosten verlinkt.
|
||||||
|
*/
|
||||||
|
export type ZuschlagAnzeige = {
|
||||||
|
berechenbar: boolean
|
||||||
|
verweildauerMonate: number
|
||||||
|
prozent: number
|
||||||
|
stufeBezeichnung: string | null
|
||||||
|
zuschlagCent: number | null
|
||||||
|
verbleibtCent: number | null
|
||||||
|
eigenanteilCent: number | null
|
||||||
|
naechsteStufe: {
|
||||||
|
prozent: number
|
||||||
|
bezeichnung: string
|
||||||
|
abDatum: string
|
||||||
|
tageBis: number
|
||||||
|
mehrCent: number | null
|
||||||
|
} | null
|
||||||
|
hinweise: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LeistungszuschlagKarte({ zuschlag }: { zuschlag: ZuschlagAnzeige }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="size-5" aria-hidden="true" />
|
||||||
|
Leistungszuschlag
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Die Pflegekasse übernimmt einen wachsenden Anteil des pflegebedingten Eigenanteils,
|
||||||
|
je länger jemand in der Einrichtung lebt. Beantragen muss man ihn nicht — die
|
||||||
|
Einrichtung rechnet ihn ab. Genau deshalb fällt es niemandem auf, wenn eine Stufe
|
||||||
|
nicht umgestellt wurde.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-5">
|
||||||
|
{zuschlag.berechenbar ? (
|
||||||
|
<>
|
||||||
|
<dl className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<dt className="text-sm text-muted-foreground">Verweildauer</dt>
|
||||||
|
<dd className="text-lg font-semibold">
|
||||||
|
{zuschlag.verweildauerMonate} Monate
|
||||||
|
</dd>
|
||||||
|
{zuschlag.stufeBezeichnung && (
|
||||||
|
<dd className="text-sm text-muted-foreground">{zuschlag.stufeBezeichnung}</dd>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<dt className="text-sm text-muted-foreground">Aktuelle Stufe</dt>
|
||||||
|
<dd className="text-lg font-semibold">{zuschlag.prozent} %</dd>
|
||||||
|
<dd className="text-sm text-muted-foreground">
|
||||||
|
{zuschlag.zuschlagCent !== null
|
||||||
|
? `${formatCent(zuschlag.zuschlagCent)} im Monat`
|
||||||
|
: 'Betrag unbekannt'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<dt className="text-sm text-muted-foreground">Verbleibender Pflegeanteil</dt>
|
||||||
|
<dd className="text-lg font-semibold">
|
||||||
|
{formatCent(zuschlag.verbleibtCent)}
|
||||||
|
</dd>
|
||||||
|
<dd className="text-sm text-muted-foreground">
|
||||||
|
von {formatCent(zuschlag.eigenanteilCent)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{zuschlag.naechsteStufe && (
|
||||||
|
<div className="rounded-md border border-ampel-gruen/40 bg-ampel-gruen/5 p-4">
|
||||||
|
<p className="flex flex-wrap items-center gap-2 font-medium">
|
||||||
|
<ArrowUpRight className="size-4 text-ampel-gruen" aria-hidden="true" />
|
||||||
|
Nächste Stufe: {zuschlag.naechsteStufe.prozent} % ab dem{' '}
|
||||||
|
{formatDatum(zuschlag.naechsteStufe.abDatum)}
|
||||||
|
<Badge variant="gruen">
|
||||||
|
{zuschlag.naechsteStufe.tageBis > 0
|
||||||
|
? `in ${zuschlag.naechsteStufe.tageBis} Tagen`
|
||||||
|
: 'greift bereits'}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
|
{zuschlag.naechsteStufe.mehrCent !== null && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Das sind{' '}
|
||||||
|
<strong className="text-foreground">
|
||||||
|
{formatCent(zuschlag.naechsteStufe.mehrCent)}
|
||||||
|
</strong>{' '}
|
||||||
|
mehr im Monat, die die Pflegekasse übernimmt. Prüf die erste Heimrechnung
|
||||||
|
nach diesem Datum daraufhin — und wenn die Stufe fehlt, reklamier sie beim
|
||||||
|
Träger.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{zuschlag.hinweise[0] ?? 'Ohne Einzugsdatum lässt sich nichts berechnen.'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">
|
||||||
|
Staffel nach Verweildauer (Stand {LEISTUNGSZUSCHLAG_STAND})
|
||||||
|
</h3>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Zeitraum</TableHead>
|
||||||
|
<TableHead>Monate</TableHead>
|
||||||
|
<TableHead className="text-right">Zuschlag</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{LEISTUNGSZUSCHLAG_STAFFEL.map((stufe) => (
|
||||||
|
<TableRow
|
||||||
|
key={stufe.abMonat}
|
||||||
|
className={stufe.prozent === zuschlag.prozent && zuschlag.berechenbar ? 'bg-muted/60' : undefined}
|
||||||
|
>
|
||||||
|
<TableCell>{stufe.bezeichnung}</TableCell>
|
||||||
|
<TableCell className="tabular-nums">
|
||||||
|
{stufe.bisMonat === null
|
||||||
|
? `ab ${stufe.abMonat}`
|
||||||
|
: `${stufe.abMonat}–${stufe.bisMonat}`}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium tabular-nums">
|
||||||
|
{stufe.prozent} %
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Der Gesetzgeber ändert diese Staffel: Zum 01.01.2024 wurde sie von 5/25/45/70 auf
|
||||||
|
15/30/50/75 Prozent angehoben. Prüf den Stand, bevor du mit den Zahlen argumentierst.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-1.5 text-sm text-muted-foreground">
|
||||||
|
{zuschlag.hinweise.map((hinweis) => (
|
||||||
|
<li key={hinweis}>{hinweis}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link href="/sozialhilfe/heimkosten">Monatliche Gegenüberstellung ansehen</Link>
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Heimrechnung gegen Deckungsquellen — die führt das Sozialhilfemodul.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Rechtshinweis rechtsgrundlage={LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState, useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Activity, CalendarClock, Pencil, Stethoscope } from 'lucide-react'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Feld } from '@/components/feld'
|
||||||
|
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||||
|
import { Datenzeile, Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
|
||||||
|
import {
|
||||||
|
PFLEGEGRAD_BESCHRIFTUNG,
|
||||||
|
PFLEGEGRAD_RECHTSGRUNDLAGE_BEGUTACHTUNG,
|
||||||
|
} from '@/lib/pflege/konstanten'
|
||||||
|
import { hoeherstufungsterminSetzen } from '@/server/actions/pflege'
|
||||||
|
import { formatDatum } from '@/lib/utils'
|
||||||
|
import type { Formularzustand } from '@/lib/validierung/substanz'
|
||||||
|
|
||||||
|
export type BegutachtungZeile = {
|
||||||
|
id: string
|
||||||
|
datum: string
|
||||||
|
titel: string
|
||||||
|
einrichtung: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PflegegradKarte({
|
||||||
|
betreuteId,
|
||||||
|
pflegegrad,
|
||||||
|
pflegegradSeit,
|
||||||
|
pflegegradBescheidVom,
|
||||||
|
pruefungAm,
|
||||||
|
vorschlagPruefungAm,
|
||||||
|
begutachtungen,
|
||||||
|
verschlechterung,
|
||||||
|
darfSchreiben,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
pflegegrad: number | null
|
||||||
|
pflegegradSeit: string | null
|
||||||
|
pflegegradBescheidVom: string | null
|
||||||
|
pruefungAm: string | null
|
||||||
|
vorschlagPruefungAm: string
|
||||||
|
begutachtungen: BegutachtungZeile[]
|
||||||
|
verschlechterung: string | null
|
||||||
|
darfSchreiben: boolean
|
||||||
|
}) {
|
||||||
|
const [zustand, aktion] = useActionState<Formularzustand, FormData>(
|
||||||
|
hoeherstufungsterminSetzen,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
const [offen, setOffen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Activity className="size-5" aria-hidden="true" />
|
||||||
|
Pflegegrad
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Der aktuelle Grad, seine Historie und der Termin, an dem geprüft wird, ob eine
|
||||||
|
Höherstufung sinnvoll ist.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-5">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
{pflegegrad ? (
|
||||||
|
<Badge variant="default" className="text-sm">
|
||||||
|
{PFLEGEGRAD_BESCHRIFTUNG[pflegegrad] ?? `Pflegegrad ${pflegegrad}`}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="gelb">kein Pflegegrad erfasst</Badge>
|
||||||
|
)}
|
||||||
|
<Button asChild variant="ghost" size="sm">
|
||||||
|
<Link href="/stammdaten/bearbeiten">
|
||||||
|
<Pencil aria-hidden="true" />
|
||||||
|
In den Stammdaten ändern
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="divide-y">
|
||||||
|
<Datenzeile beschriftung="Pflegegrad seit">{formatDatum(pflegegradSeit)}</Datenzeile>
|
||||||
|
<Datenzeile beschriftung="Bescheid vom">
|
||||||
|
{formatDatum(pflegegradBescheidVom)}
|
||||||
|
</Datenzeile>
|
||||||
|
<Datenzeile beschriftung="Nächste Überprüfung">
|
||||||
|
{pruefungAm ? formatDatum(pruefungAm) : 'kein Termin gesetzt'}
|
||||||
|
</Datenzeile>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<Alert variant="hinweis">
|
||||||
|
<Stethoscope aria-hidden="true" />
|
||||||
|
<AlertTitle>COPD verschlechtert sich in Schüben</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
Eine chronisch obstruktive Lungenerkrankung schreitet fort. Nimmt der Hilfebedarf
|
||||||
|
zu — mehr Unterstützung beim Waschen und Ankleiden, Atemnot schon bei kleinen
|
||||||
|
Wegen, häufigere Schübe, Sauerstoff über mehr Stunden am Tag —, rechtfertigt das
|
||||||
|
einen Antrag auf Neubegutachtung. Ein solcher Antrag ist jederzeit möglich und
|
||||||
|
kostet nichts; die Pflegekasse muss ihn bearbeiten.
|
||||||
|
</p>
|
||||||
|
{verschlechterung && (
|
||||||
|
<p className="mt-2 font-medium text-foreground">{verschlechterung}</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-2">
|
||||||
|
Die Zahlen für die Begründung stehen im{' '}
|
||||||
|
<Link href="/pflege/verlauf" className="underline">
|
||||||
|
Gesundheitsverlauf
|
||||||
|
</Link>
|
||||||
|
. Nimm ein Pflegetagebuch über zwei Wochen dazu — das überzeugt Gutachter mehr
|
||||||
|
als jede Schilderung im Gespräch.
|
||||||
|
</p>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{begutachtungen.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Bisherige Begutachtungen</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{begutachtungen.map((b) => (
|
||||||
|
<li
|
||||||
|
key={b.id}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span className="font-medium">{b.titel}</span>
|
||||||
|
{b.einrichtung && (
|
||||||
|
<span className="block text-sm text-muted-foreground">{b.einrichtung}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{formatDatum(b.datum)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{darfSchreiben && (
|
||||||
|
<div className="rounded-md border p-4">
|
||||||
|
{!offen ? (
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOffen(true)}>
|
||||||
|
<CalendarClock aria-hidden="true" />
|
||||||
|
{pruefungAm ? 'Prüftermin verschieben' : 'Prüftermin setzen'}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<form action={aktion} className="space-y-4" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={zustand} />
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="pruefungAm"
|
||||||
|
beschriftung="Pflegegrad überprüfen am"
|
||||||
|
erklaerung="Wird als Frist angelegt und erscheint im Fristenmodul. Ein halbes Jahr Abstand ist ein guter Erfahrungswert."
|
||||||
|
fehler={zustand.felder?.pruefungAm}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="pruefungAm"
|
||||||
|
type="date"
|
||||||
|
defaultValue={pruefungAm ?? vorschlagPruefungAm}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="hoeherstufung-begruendung"
|
||||||
|
beschriftung="Worauf soll ich dann achten?"
|
||||||
|
fehler={zustand.felder?.begruendung}
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
name="begruendung"
|
||||||
|
rows={3}
|
||||||
|
maxLength={2000}
|
||||||
|
placeholder="z. B. Sauerstoffbedarf seit Februar auf 16 Stunden gestiegen, zweite Exazerbation im Jahr"
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Absendeknopf>Termin setzen</Absendeknopf>
|
||||||
|
<Button type="button" variant="ghost" onClick={() => setOffen(false)}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Rechtshinweis rechtsgrundlage={PFLEGEGRAD_RECHTSGRUNDLAGE_BEGUTACHTUNG} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { BarChart3 } from 'lucide-react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import { COPD_HINWEIS } from '@/lib/pflege/konstanten'
|
||||||
|
|
||||||
|
export type AuswertungsZeile = {
|
||||||
|
jahr: number
|
||||||
|
exazerbationen: number
|
||||||
|
krankenhausaufenthalte: number
|
||||||
|
krankenhaustage: number
|
||||||
|
abwesenheitstage: number
|
||||||
|
stuerze: number
|
||||||
|
satz: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die zwei Zahlen, die in den Jahresbericht und in jeden
|
||||||
|
* Hoeherstufungsantrag wandern.
|
||||||
|
*/
|
||||||
|
export function VerlaufAuswertung({
|
||||||
|
zeilen,
|
||||||
|
verschlechterung,
|
||||||
|
}: {
|
||||||
|
zeilen: AuswertungsZeile[]
|
||||||
|
verschlechterung: string | null
|
||||||
|
}) {
|
||||||
|
if (zeilen.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BarChart3 className="size-5" aria-hidden="true" />
|
||||||
|
Auswertung je Jahr
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{COPD_HINWEIS}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Jahr</TableHead>
|
||||||
|
<TableHead className="text-right">Exazerbationen</TableHead>
|
||||||
|
<TableHead className="text-right">Krankenhaus</TableHead>
|
||||||
|
<TableHead className="text-right">Krankenhaustage</TableHead>
|
||||||
|
<TableHead className="text-right">Abwesenheitstage</TableHead>
|
||||||
|
<TableHead className="text-right">Stürze</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{zeilen.map((zeile) => (
|
||||||
|
<TableRow key={zeile.jahr}>
|
||||||
|
<TableCell className="font-medium">{zeile.jahr}</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">{zeile.exazerbationen}</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{zeile.krankenhausaufenthalte}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">{zeile.krankenhaustage}</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">{zeile.abwesenheitstage}</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">{zeile.stuerze}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{verschlechterung && (
|
||||||
|
<p className="rounded-md border border-ampel-orange/40 bg-ampel-orange/10 p-3 text-sm">
|
||||||
|
{verschlechterung}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold">Für den Jahresbericht</h3>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{zeilen.map((zeile) => (
|
||||||
|
<li key={zeile.jahr} className="rounded-md border bg-muted/40 p-3 text-sm">
|
||||||
|
{zeile.satz}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Bewusst ohne Diagnosen formuliert: Der Jahresbericht geht ans Betreuungsgericht,
|
||||||
|
und dort gehört nur hinein, was für die Betreuung erheblich ist.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { GesundheitsArt } from '@prisma/client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
BedDouble,
|
||||||
|
HeartPulse,
|
||||||
|
PersonStanding,
|
||||||
|
Pill,
|
||||||
|
Plus,
|
||||||
|
Stethoscope,
|
||||||
|
Syringe,
|
||||||
|
Trash2,
|
||||||
|
Wind,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Leerzustand } from '@/components/seiten-kopf'
|
||||||
|
import { GESUNDHEITS_ART_BESCHRIFTUNG } from '@/lib/pflege/konstanten'
|
||||||
|
import { gesundheitsereignisLoeschen } from '@/server/actions/pflege'
|
||||||
|
import { formatDatum } from '@/lib/utils'
|
||||||
|
|
||||||
|
export type VerlaufZeile = {
|
||||||
|
id: string
|
||||||
|
datum: string
|
||||||
|
bisDatum: string | null
|
||||||
|
art: GesundheitsArt
|
||||||
|
titel: string
|
||||||
|
behandler: string | null
|
||||||
|
einrichtung: string | null
|
||||||
|
abwesenheitstage: number | null
|
||||||
|
beschreibung: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYMBOL: Partial<Record<GesundheitsArt, typeof Activity>> = {
|
||||||
|
EXAZERBATION: Wind,
|
||||||
|
KRANKENHAUS: BedDouble,
|
||||||
|
SAUERSTOFFVERSORGUNG: Wind,
|
||||||
|
MEDIKATIONSAENDERUNG: Pill,
|
||||||
|
INHALATION: Wind,
|
||||||
|
STURZ: PersonStanding,
|
||||||
|
IMPFUNG: Syringe,
|
||||||
|
ARZTBESUCH: Stethoscope,
|
||||||
|
PFLEGEBEGUTACHTUNG: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
const HERVORGEHOBEN: GesundheitsArt[] = ['EXAZERBATION', 'KRANKENHAUS', 'STURZ']
|
||||||
|
|
||||||
|
export function VerlaufZeitstrahl({
|
||||||
|
betreuteId,
|
||||||
|
ereignisse,
|
||||||
|
darfSchreiben,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
ereignisse: VerlaufZeile[]
|
||||||
|
darfSchreiben: boolean
|
||||||
|
}) {
|
||||||
|
if (ereignisse.length === 0) {
|
||||||
|
return (
|
||||||
|
<Leerzustand
|
||||||
|
titel="Noch nichts erfasst"
|
||||||
|
beschreibung="Fang mit dem an, was zuletzt war: der letzte Krankenhausaufenthalt, die letzte Exazerbation, die aktuelle Sauerstoffversorgung. Rückwirkend erfassen ist mühsam, aber die Zahlen brauchst du für den Jahresbericht und für jede Höherstufung."
|
||||||
|
icon={HeartPulse}
|
||||||
|
aktion={
|
||||||
|
darfSchreiben && (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/pflege/verlauf/neu">
|
||||||
|
<Plus aria-hidden="true" />
|
||||||
|
Ereignis erfassen
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach Jahr gruppieren - so liest sich der Zeitstrahl wie eine Akte.
|
||||||
|
const jahre = new Map<number, VerlaufZeile[]>()
|
||||||
|
for (const ereignis of ereignisse) {
|
||||||
|
const jahr = new Date(ereignis.datum).getUTCFullYear()
|
||||||
|
const liste = jahre.get(jahr)
|
||||||
|
if (liste) liste.push(ereignis)
|
||||||
|
else jahre.set(jahr, [ereignis])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{[...jahre.entries()].map(([jahr, liste]) => (
|
||||||
|
<section key={jahr} className="space-y-3">
|
||||||
|
<h2 className="text-lg font-semibold">{jahr}</h2>
|
||||||
|
|
||||||
|
<ol className="space-y-3 border-l-2 border-border pl-5">
|
||||||
|
{liste.map((ereignis) => {
|
||||||
|
const Icon = SYMBOL[ereignis.art] ?? HeartPulse
|
||||||
|
const wichtig = HERVORGEHOBEN.includes(ereignis.art)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={ereignis.id} className="relative">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
wichtig
|
||||||
|
? 'absolute -left-[1.6rem] top-4 size-3 rounded-full bg-ampel-orange ring-4 ring-background'
|
||||||
|
: 'absolute -left-[1.6rem] top-4 size-3 rounded-full bg-muted-foreground/40 ring-4 ring-background'
|
||||||
|
}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Icon className="mt-0.5 size-5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||||
|
<div className="min-w-0 flex-1 space-y-1.5">
|
||||||
|
<CardTitle className="text-base">{ereignis.titel}</CardTitle>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Badge variant={wichtig ? 'orange' : 'outline'}>
|
||||||
|
{GESUNDHEITS_ART_BESCHRIFTUNG[ereignis.art]}
|
||||||
|
</Badge>
|
||||||
|
<span>
|
||||||
|
{formatDatum(ereignis.datum)}
|
||||||
|
{ereignis.bisDatum && ` bis ${formatDatum(ereignis.bisDatum)}`}
|
||||||
|
</span>
|
||||||
|
{ereignis.abwesenheitstage !== null && (
|
||||||
|
<span>{ereignis.abwesenheitstage} Abwesenheitstage</span>
|
||||||
|
)}
|
||||||
|
{ereignis.behandler && <span>{ereignis.behandler}</span>}
|
||||||
|
{ereignis.einrichtung && <span>{ereignis.einrichtung}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{darfSchreiben && (
|
||||||
|
<form
|
||||||
|
action={async () => {
|
||||||
|
await gesundheitsereignisLoeschen(betreuteId, ereignis.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label={`„${ereignis.titel}“ in den Papierkorb legen`}
|
||||||
|
>
|
||||||
|
<Trash2 aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{ereignis.beschreibung && (
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<p className="whitespace-pre-line text-sm text-muted-foreground">
|
||||||
|
{ereignis.beschreibung}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Feld } from '@/components/feld'
|
||||||
|
import { Absendeknopf, Zustandsmeldung } from '@/components/substanz-formular'
|
||||||
|
import { ermitteltenWillenSpeichern } from '@/server/actions/pflege'
|
||||||
|
import type { Formularzustand } from '@/lib/validierung/substanz'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der nach § 1821 BGB zu ermittelnde Wille.
|
||||||
|
*
|
||||||
|
* Der Erklaerungstext ist absichtlich lang: Diese Vorschrift hat die
|
||||||
|
* Betreuung 2023 vom Kopf auf die Fuesse gestellt, und wer sie nicht
|
||||||
|
* kennt, betreut immer noch nach dem alten "objektiven Wohl".
|
||||||
|
*/
|
||||||
|
export function WilleFormular({
|
||||||
|
betreuteId,
|
||||||
|
ermittelterWille,
|
||||||
|
darfSchreiben,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
ermittelterWille: string | null
|
||||||
|
darfSchreiben: boolean
|
||||||
|
}) {
|
||||||
|
const [zustand, aktion] = useActionState<Formularzustand, FormData>(
|
||||||
|
ermitteltenWillenSpeichern,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!darfSchreiben) {
|
||||||
|
return ermittelterWille ? (
|
||||||
|
<p className="whitespace-pre-line rounded-md border bg-muted/40 p-3 text-sm">
|
||||||
|
{ermittelterWille}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">Noch nichts hinterlegt.</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={aktion} className="space-y-4" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={zustand} />
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="ermittelterWille"
|
||||||
|
beschriftung="Ermittelter Wille"
|
||||||
|
erklaerung="Wird verschlüsselt gespeichert. Halte fest, woher du es weißt — aus einem Gespräch, aus früheren Äußerungen, von Angehörigen — und wann. Ein Wille ohne Herkunft ist im Zweifel deine eigene Meinung."
|
||||||
|
fehler={zustand.felder?.ermittelterWille}
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
name="ermittelterWille"
|
||||||
|
defaultValue={ermittelterWille ?? ''}
|
||||||
|
rows={12}
|
||||||
|
maxLength={8000}
|
||||||
|
placeholder={
|
||||||
|
'Wohnen: Möchte nicht ins Krankenhaus, wenn es sich vermeiden lässt (Gespräch am 14.03.2026).\n' +
|
||||||
|
'Haus: Hängt am Elternhaus, will es nicht verkauft sehen — hat das mehrfach gesagt, zuletzt im Januar.\n' +
|
||||||
|
'Behandlung: Keine künstliche Beatmung (Patientenverfügung von 2019, im Gespräch bestätigt).\n' +
|
||||||
|
'Alltag: Besuch am Sonntagnachmittag, kein Fernsehen im Zimmer, eigene Bettwäsche.'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Absendeknopf>Speichern</Absendeknopf>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { CheckCircle2, FileWarning, Gavel, Scale, TriangleAlert } from 'lucide-react'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Select } from '@/components/ui/select'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Feld } from '@/components/feld'
|
||||||
|
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||||
|
import { Absendeknopf, Textbaustein, Zustandsmeldung } from '@/components/substanz-formular'
|
||||||
|
import { kuendigungVermerken } from '@/server/actions/vertraege'
|
||||||
|
import { formatDatum } from '@/lib/utils'
|
||||||
|
import type { Formularzustand } from '@/lib/validierung/substanz'
|
||||||
|
|
||||||
|
export type AssistentDaten = {
|
||||||
|
schreiben: string
|
||||||
|
warnungen: string[]
|
||||||
|
genehmigung: {
|
||||||
|
stellen: boolean
|
||||||
|
frage: string
|
||||||
|
rechtsgrundlage: string
|
||||||
|
hinweis: string
|
||||||
|
}
|
||||||
|
versandnachweis: {
|
||||||
|
moeglichkeiten: string[]
|
||||||
|
hinweis: string
|
||||||
|
}
|
||||||
|
plan: {
|
||||||
|
berechenbar: boolean
|
||||||
|
naechsterTermin: string | null
|
||||||
|
zugangSpaetestensAm: string | null
|
||||||
|
spaetesterAbsendetag: string | null
|
||||||
|
tageBisAbsendetag: number | null
|
||||||
|
verpasst: boolean
|
||||||
|
hinweise: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KuendigungsAssistent({
|
||||||
|
betreuteId,
|
||||||
|
vertragId,
|
||||||
|
daten,
|
||||||
|
heuteIso,
|
||||||
|
darfSchreiben,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
vertragId: string
|
||||||
|
daten: AssistentDaten
|
||||||
|
heuteIso: string
|
||||||
|
darfSchreiben: boolean
|
||||||
|
}) {
|
||||||
|
const [zustand, aktion] = useActionState<Formularzustand, FormData>(kuendigungVermerken, {})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{daten.warnungen.length > 0 && (
|
||||||
|
<Alert variant="warnung">
|
||||||
|
<TriangleAlert aria-hidden="true" />
|
||||||
|
<AlertTitle>Vor dem Absenden klären</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<ul className="ml-4 list-disc space-y-1.5">
|
||||||
|
{daten.warnungen.map((warnung) => (
|
||||||
|
<li key={warnung}>{warnung}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Scale className="size-5" aria-hidden="true" />
|
||||||
|
Frist geprüft
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Zu diesem Termin kann als Nächstes gekündigt werden — und bis wann der Brief dafür
|
||||||
|
raus muss.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{daten.plan.berechenbar ? (
|
||||||
|
<dl className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<dt className="text-sm text-muted-foreground">Kündigung zum</dt>
|
||||||
|
<dd className="text-lg font-semibold">
|
||||||
|
{formatDatum(daten.plan.naechsterTermin)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<dt className="text-sm text-muted-foreground">Zugang spätestens</dt>
|
||||||
|
<dd className="text-lg font-semibold">
|
||||||
|
{formatDatum(daten.plan.zugangSpaetestensAm)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
daten.plan.verpasst || (daten.plan.tageBisAbsendetag ?? 99) <= 30
|
||||||
|
? 'rounded-md border border-ampel-rot/40 bg-ampel-rot/5 p-3'
|
||||||
|
: 'rounded-md border p-3'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<dt className="text-sm text-muted-foreground">Brief spätestens absenden</dt>
|
||||||
|
<dd className="text-lg font-semibold">
|
||||||
|
{formatDatum(daten.plan.spaetesterAbsendetag)}
|
||||||
|
</dd>
|
||||||
|
{daten.plan.tageBisAbsendetag !== null && (
|
||||||
|
<dd className="text-sm text-muted-foreground">
|
||||||
|
{daten.plan.tageBisAbsendetag < 0
|
||||||
|
? `vor ${Math.abs(daten.plan.tageBisAbsendetag)} Tagen`
|
||||||
|
: `in ${daten.plan.tageBisAbsendetag} Tagen`}
|
||||||
|
</dd>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Ohne Laufzeitbeginn und Kündigungsfrist lässt sich der Termin nicht berechnen.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul className="space-y-1.5 text-sm text-muted-foreground">
|
||||||
|
{daten.plan.hinweise.map((hinweis) => (
|
||||||
|
<li key={hinweis}>{hinweis}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={
|
||||||
|
daten.genehmigung.stellen ? 'border-ampel-orange/50 bg-ampel-orange/5' : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Gavel className="size-5" aria-hidden="true" />
|
||||||
|
Genehmigungsfrage
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{daten.genehmigung.frage}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-sm">{daten.genehmigung.hinweis}</p>
|
||||||
|
<Rechtshinweis rechtsgrundlage={daten.genehmigung.rechtsgrundlage} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Kündigungsschreiben</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Entwurf zum Kopieren und Bearbeiten. Die App verschickt nichts.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Textbaustein
|
||||||
|
text={daten.schreiben}
|
||||||
|
beschriftung="Kündigungsschreiben"
|
||||||
|
hoehe="min-h-[36rem]"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FileWarning className="size-5" aria-hidden="true" />
|
||||||
|
Versandnachweis
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{daten.versandnachweis.hinweis}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<ul className="ml-4 list-disc space-y-1 text-sm">
|
||||||
|
{daten.versandnachweis.moeglichkeiten.map((moeglichkeit) => (
|
||||||
|
<li key={moeglichkeit}>{moeglichkeit}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{darfSchreiben && (
|
||||||
|
<form action={aktion} className="space-y-4 rounded-md border p-4" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
<input type="hidden" name="vertragId" value={vertragId} />
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={zustand} />
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<Feld
|
||||||
|
id="k-gekuendigtAm"
|
||||||
|
beschriftung="Versandt am"
|
||||||
|
fehler={zustand.felder?.gekuendigtAm}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input name="gekuendigtAm" type="date" defaultValue={heuteIso} required />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="k-kuendigungZum"
|
||||||
|
beschriftung="Gekündigt zum"
|
||||||
|
erklaerung="Der Termin aus der Berechnung oben."
|
||||||
|
fehler={zustand.felder?.kuendigungZum}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="kuendigungZum"
|
||||||
|
type="date"
|
||||||
|
defaultValue={daten.plan.naechsterTermin ?? ''}
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="k-versandart"
|
||||||
|
beschriftung="Versandart"
|
||||||
|
fehler={zustand.felder?.versandart}
|
||||||
|
pflicht
|
||||||
|
className="sm:col-span-2"
|
||||||
|
>
|
||||||
|
<Select name="versandart" defaultValue={daten.versandnachweis.moeglichkeiten[0]}>
|
||||||
|
{daten.versandnachweis.moeglichkeiten.map((m) => (
|
||||||
|
<option key={m} value={m}>
|
||||||
|
{m}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Feld>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-start gap-2.5">
|
||||||
|
<Checkbox name="nachweisVorhanden" className="mt-0.5" />
|
||||||
|
<span className="text-sm">
|
||||||
|
Der Versandnachweis liegt vor und ist als Dokument hochgeladen.
|
||||||
|
{zustand.felder?.nachweisVorhanden && (
|
||||||
|
<span className="mt-1 block font-medium text-destructive">
|
||||||
|
{zustand.felder.nachweisVorhanden}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-start gap-2.5">
|
||||||
|
<Checkbox name="genehmigungGeklaert" className="mt-0.5" />
|
||||||
|
<span className="text-sm">
|
||||||
|
Die Genehmigungsfrage ist mit dem Rechtspfleger geklärt (§ 1850 BGB).
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Feld id="k-bemerkung" beschriftung="Bemerkung" fehler={zustand.felder?.bemerkung}>
|
||||||
|
<Textarea
|
||||||
|
name="bemerkung"
|
||||||
|
rows={2}
|
||||||
|
maxLength={2000}
|
||||||
|
placeholder="z. B. Einlieferungsnummer des Einschreibens"
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Absendeknopf>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="size-4" aria-hidden="true" />
|
||||||
|
Kündigung als versandt vermerken
|
||||||
|
</span>
|
||||||
|
</Absendeknopf>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Setzt den Status auf „gekündigt“ und legt eine Wiedervorlage an, damit die
|
||||||
|
Bestätigung der Gesellschaft nicht vergessen wird.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { CalendarClock, TriangleAlert } from 'lucide-react'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { formatDatum } from '@/lib/utils'
|
||||||
|
|
||||||
|
export type Kuendigungswarnung = {
|
||||||
|
vertragId: string
|
||||||
|
bezeichnung: string
|
||||||
|
absendetag: string | null
|
||||||
|
tageBisAbsendetag: number | null
|
||||||
|
verpasst: boolean
|
||||||
|
variante: 'rot' | 'orange' | 'gelb' | 'gruen'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Warnkachel oben auf der Vertragsuebersicht.
|
||||||
|
*
|
||||||
|
* Sie zeigt nur, was in den naechsten 30 Tagen raus muss. Eine Liste, die
|
||||||
|
* alles zeigt, wird nicht gelesen - und dann geht genau die eine Frist
|
||||||
|
* unter, auf die es ankam.
|
||||||
|
*/
|
||||||
|
export function KuendigungsUebersicht({ warnungen }: { warnungen: Kuendigungswarnung[] }) {
|
||||||
|
if (warnungen.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border-ampel-orange/40 bg-ampel-orange/5">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<TriangleAlert className="size-5 text-ampel-orange" aria-hidden="true" />
|
||||||
|
Kündigungsfrist rückt näher
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Eine Kündigung wird erst mit dem Zugang wirksam. Der Absendetag ist deshalb früher
|
||||||
|
als der Fristablauf — hier ist der Postlauf schon abgezogen.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{warnungen.map((warnung) => (
|
||||||
|
<li
|
||||||
|
key={warnung.vertragId}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-background p-3"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/vertraege/${warnung.vertragId}`}
|
||||||
|
className="font-medium hover:underline"
|
||||||
|
>
|
||||||
|
{warnung.bezeichnung}
|
||||||
|
</Link>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{warnung.absendetag && (
|
||||||
|
<span className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<CalendarClock className="size-3.5" aria-hidden="true" />
|
||||||
|
Brief bis {formatDatum(warnung.absendetag)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Badge variant={warnung.variante}>{warnung.text}</Badge>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState, useState } from 'react'
|
||||||
|
import { Coins, Mail, Plus } from 'lucide-react'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Select } from '@/components/ui/select'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Feld } from '@/components/feld'
|
||||||
|
import { Leerzustand } from '@/components/seiten-kopf'
|
||||||
|
import { Absendeknopf, Textbaustein, Zustandsmeldung } from '@/components/substanz-formular'
|
||||||
|
import { WERTQUELLEN_VERTRAG } from '@/lib/vertraege/konstanten'
|
||||||
|
import { rueckkaufswertAnfordern, rueckkaufswertErfassen } from '@/server/actions/vertraege'
|
||||||
|
import { formatCent, formatDatum } from '@/lib/utils'
|
||||||
|
import type { Formularzustand } from '@/lib/validierung/substanz'
|
||||||
|
|
||||||
|
export type RueckkaufswertZeile = {
|
||||||
|
id: string
|
||||||
|
stichtag: string
|
||||||
|
wertCent: number
|
||||||
|
wertquelle: string
|
||||||
|
bemerkung: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Rueckkaufswert({
|
||||||
|
betreuteId,
|
||||||
|
vertragId,
|
||||||
|
werte,
|
||||||
|
angefragtAm,
|
||||||
|
erinnerungFaellig,
|
||||||
|
tageSeitAnfrage,
|
||||||
|
anschreiben,
|
||||||
|
stichtagVorschlag,
|
||||||
|
darfSchreiben,
|
||||||
|
}: {
|
||||||
|
betreuteId: string
|
||||||
|
vertragId: string
|
||||||
|
werte: RueckkaufswertZeile[]
|
||||||
|
angefragtAm: string | null
|
||||||
|
erinnerungFaellig: boolean
|
||||||
|
tageSeitAnfrage: number | null
|
||||||
|
anschreiben: string
|
||||||
|
stichtagVorschlag: string
|
||||||
|
darfSchreiben: boolean
|
||||||
|
}) {
|
||||||
|
const [erfassen, erfassenAktion] = useActionState<Formularzustand, FormData>(
|
||||||
|
rueckkaufswertErfassen,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
const [anfrage, anfrageAktion] = useActionState<Formularzustand, FormData>(
|
||||||
|
rueckkaufswertAnfordern,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
const [briefOffen, setBriefOffen] = useState(false)
|
||||||
|
const [erfassenOffen, setErfassenOffen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Coins className="size-5" aria-hidden="true" />
|
||||||
|
Rückkaufswert
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Der Bezirk verlangt zu jedem Versicherungsvertrag den Rückkaufswert zu einem
|
||||||
|
Stichtag — schriftlich von der Gesellschaft, nicht geschätzt. Die Antwort dauert
|
||||||
|
erfahrungsgemäß drei bis sechs Wochen.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-5">
|
||||||
|
{angefragtAm && (
|
||||||
|
<Alert variant={erinnerungFaellig ? 'warnung' : 'hinweis'}>
|
||||||
|
<Mail aria-hidden="true" />
|
||||||
|
<AlertTitle>
|
||||||
|
Angefragt am {formatDatum(angefragtAm)}
|
||||||
|
{tageSeitAnfrage !== null && ` — vor ${tageSeitAnfrage} Tagen`}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{erinnerungFaellig
|
||||||
|
? 'Die vier Wochen sind um. Ruf an und halte das Gespräch im Kontaktprotokoll fest — schriftlich nachfassen dauert wieder vier Wochen.'
|
||||||
|
: 'Die Wiedervorlage steht in den Fristen. Kommt die Antwort früher, hak die Frist dort ab.'}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{werte.length === 0 ? (
|
||||||
|
<Leerzustand
|
||||||
|
titel="Noch kein Rückkaufswert erfasst"
|
||||||
|
beschreibung="Fordere ihn bei der Gesellschaft an. Frag im selben Schreiben nach der beitragsfreien Versicherungssumme — sonst musst du für den Vergleich ein zweites Mal schreiben."
|
||||||
|
icon={Coins}
|
||||||
|
aktion={
|
||||||
|
darfSchreiben && (
|
||||||
|
<Button type="button" onClick={() => setBriefOffen(true)}>
|
||||||
|
<Mail aria-hidden="true" />
|
||||||
|
Anschreiben erzeugen
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Stichtag</TableHead>
|
||||||
|
<TableHead className="text-right">Rückkaufswert</TableHead>
|
||||||
|
<TableHead>Quelle</TableHead>
|
||||||
|
<TableHead>Bemerkung</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{werte.map((zeile) => (
|
||||||
|
<TableRow key={zeile.id}>
|
||||||
|
<TableCell className="whitespace-nowrap">{formatDatum(zeile.stichtag)}</TableCell>
|
||||||
|
<TableCell className="text-right font-medium tabular-nums">
|
||||||
|
{formatCent(zeile.wertCent)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{zeile.wertquelle}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{zeile.bemerkung ?? '–'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{darfSchreiben && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setBriefOffen((o) => !o)}>
|
||||||
|
<Mail aria-hidden="true" />
|
||||||
|
{briefOffen ? 'Anschreiben schließen' : 'Rückkaufswert anfordern'}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setErfassenOffen((o) => !o)}>
|
||||||
|
<Plus aria-hidden="true" />
|
||||||
|
{erfassenOffen ? 'Eingabe schließen' : 'Mitgeteilten Wert eintragen'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{darfSchreiben && briefOffen && (
|
||||||
|
<div className="space-y-4 rounded-md border p-4">
|
||||||
|
<Textbaustein
|
||||||
|
text={anschreiben}
|
||||||
|
beschriftung="Anschreiben an die Gesellschaft"
|
||||||
|
hoehe="min-h-[32rem]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form action={anfrageAktion} className="space-y-4" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
<input type="hidden" name="vertragId" value={vertragId} />
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={anfrage} />
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<Feld
|
||||||
|
id="anfrage-stichtag"
|
||||||
|
beschriftung="Stichtag der Anfrage"
|
||||||
|
erklaerung="Der Tag, zu dem der Wert mitgeteilt werden soll. Für die Sozialhilfe meist der Tag der Antragstellung."
|
||||||
|
fehler={anfrage.felder?.stichtag}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="stichtag"
|
||||||
|
type="date"
|
||||||
|
defaultValue={stichtagVorschlag}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Absendeknopf>Anfrage vermerken und Wiedervorlage anlegen</Absendeknopf>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Setzt das Datum der Anfrage am Vertrag und legt eine Wiedervorlage in vier
|
||||||
|
Wochen an. Den Brief selbst schickst du von Hand ab — die App verschickt nichts.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{darfSchreiben && erfassenOffen && (
|
||||||
|
<form action={erfassenAktion} className="space-y-4 rounded-md border p-4" noValidate>
|
||||||
|
<input type="hidden" name="betreuteId" value={betreuteId} />
|
||||||
|
<input type="hidden" name="vertragId" value={vertragId} />
|
||||||
|
|
||||||
|
<Zustandsmeldung zustand={erfassen} />
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<Feld
|
||||||
|
id="rkw-stichtag"
|
||||||
|
beschriftung="Stichtag"
|
||||||
|
fehler={erfassen.felder?.stichtag}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input name="stichtag" type="date" required />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="rkw-wert"
|
||||||
|
beschriftung="Rückkaufswert"
|
||||||
|
fehler={erfassen.felder?.wert}
|
||||||
|
pflicht
|
||||||
|
>
|
||||||
|
<Input name="wert" inputMode="decimal" placeholder="4.812,55" required />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="rkw-quelle"
|
||||||
|
beschriftung="Quelle"
|
||||||
|
erklaerung="Woher stammt die Zahl? Eine telefonische Auskunft ist besser als nichts, aber kein Nachweis."
|
||||||
|
fehler={erfassen.felder?.wertquelle}
|
||||||
|
pflicht
|
||||||
|
className="sm:col-span-2"
|
||||||
|
>
|
||||||
|
<Select name="wertquelle" defaultValue={WERTQUELLEN_VERTRAG[0]}>
|
||||||
|
{WERTQUELLEN_VERTRAG.map((q) => (
|
||||||
|
<option key={q} value={q}>
|
||||||
|
{q}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Feld>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="rkw-bemerkung"
|
||||||
|
beschriftung="Bemerkung"
|
||||||
|
erklaerung="z. B. die beitragsfreie Versicherungssumme, wenn sie mitgeteilt wurde."
|
||||||
|
fehler={erfassen.felder?.bemerkung}
|
||||||
|
>
|
||||||
|
<Textarea name="bemerkung" rows={2} maxLength={1000} />
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Absendeknopf>Wert aufnehmen</Absendeknopf>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ export type VertragWerte = {
|
|||||||
vertragsnummer: string
|
vertragsnummer: string
|
||||||
beginnAm: string
|
beginnAm: string
|
||||||
endeAm: string
|
endeAm: string
|
||||||
laufzeitJahre: string
|
|
||||||
kuendigungsfristMonate: string
|
kuendigungsfristMonate: string
|
||||||
kuendigungZum: string
|
kuendigungZum: string
|
||||||
gekuendigtAm: string
|
gekuendigtAm: string
|
||||||
@@ -47,7 +46,6 @@ export const LEERER_VERTRAG: VertragWerte = {
|
|||||||
vertragsnummer: '',
|
vertragsnummer: '',
|
||||||
beginnAm: '',
|
beginnAm: '',
|
||||||
endeAm: '',
|
endeAm: '',
|
||||||
laufzeitJahre: '',
|
|
||||||
kuendigungsfristMonate: '3',
|
kuendigungsfristMonate: '3',
|
||||||
kuendigungZum: '',
|
kuendigungZum: '',
|
||||||
gekuendigtAm: '',
|
gekuendigtAm: '',
|
||||||
@@ -133,21 +131,12 @@ export function VertragFormular({
|
|||||||
<Feld
|
<Feld
|
||||||
id="endeAm"
|
id="endeAm"
|
||||||
beschriftung="Vereinbartes Ende"
|
beschriftung="Vereinbartes Ende"
|
||||||
erklaerung="Nur bei fester Ablaufzeit, z. B. einer Kapitallebensversicherung."
|
erklaerung="Nur bei fester Ablaufzeit, z. B. einer Kapitallebensversicherung. Steht hier ein Datum, ergibt sich daraus zugleich die Laufzeit, um die sich der Vertrag verlängert — sonst wird mit einem Jahr gerechnet."
|
||||||
fehler={zustand.felder?.endeAm}
|
fehler={zustand.felder?.endeAm}
|
||||||
>
|
>
|
||||||
<Input name="endeAm" type="date" defaultValue={werte.endeAm} />
|
<Input name="endeAm" type="date" defaultValue={werte.endeAm} />
|
||||||
</Feld>
|
</Feld>
|
||||||
|
|
||||||
<Feld
|
|
||||||
id="laufzeitJahre"
|
|
||||||
beschriftung="Laufzeit in Jahren"
|
|
||||||
erklaerung="Um wie viel verlängert sich der Vertrag? Leer lassen für den Regelfall von einem Jahr."
|
|
||||||
fehler={zustand.felder?.laufzeitJahre}
|
|
||||||
>
|
|
||||||
<Input name="laufzeitJahre" inputMode="numeric" defaultValue={werte.laufzeitJahre} />
|
|
||||||
</Feld>
|
|
||||||
|
|
||||||
<Feld
|
<Feld
|
||||||
id="kuendigungsfristMonate"
|
id="kuendigungsfristMonate"
|
||||||
beschriftung="Kündigungsfrist in Monaten"
|
beschriftung="Kündigungsfrist in Monaten"
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export const VertragSchema = z
|
|||||||
120,
|
120,
|
||||||
'Bitte die Kündigungsfrist in ganzen Monaten angeben.',
|
'Bitte die Kündigungsfrist in ganzen Monaten angeben.',
|
||||||
),
|
),
|
||||||
laufzeitJahre: optionaleZahl(1, 99, 'Bitte die Laufzeit in ganzen Jahren angeben.'),
|
|
||||||
kuendigungZum: optionalesDatum,
|
kuendigungZum: optionalesDatum,
|
||||||
gekuendigtAm: optionalesDatum,
|
gekuendigtAm: optionalesDatum,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Briefkopf } from '@/lib/vertraege/anschreiben'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut den Briefkopf aus Stammdaten und angemeldetem Benutzer.
|
||||||
|
*
|
||||||
|
* Die Anschrift des Betreuers steht im Datenmodell nicht - der Benutzer
|
||||||
|
* hat nur Name und E-Mail. Sie bleibt deshalb leer; im erzeugten Text
|
||||||
|
* fehlen dann schlicht die beiden Zeilen, und wer den Entwurf kopiert,
|
||||||
|
* ergaenzt sie im Briefkopf seines Textverarbeitungsprogramms ohnehin.
|
||||||
|
*/
|
||||||
|
export type BriefkopfQuelle = {
|
||||||
|
betreute: {
|
||||||
|
vorname: string
|
||||||
|
nachname: string
|
||||||
|
anrede: string | null
|
||||||
|
geburtsdatum: Date
|
||||||
|
heimName: string | null
|
||||||
|
heimStrasse: string | null
|
||||||
|
heimPlz: string | null
|
||||||
|
heimOrt: string | null
|
||||||
|
betreuungsgericht: string | null
|
||||||
|
aktenzeichenGericht: string | null
|
||||||
|
bestellungAm: Date | null
|
||||||
|
}
|
||||||
|
benutzer: { name: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function briefkopfBauen(quelle: BriefkopfQuelle): Briefkopf {
|
||||||
|
const b = quelle.betreute
|
||||||
|
const anschrift = [
|
||||||
|
b.heimName,
|
||||||
|
b.heimStrasse,
|
||||||
|
[b.heimPlz, b.heimOrt].filter(Boolean).join(' ') || null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
return {
|
||||||
|
betreuerName: quelle.benutzer.name,
|
||||||
|
betreuerStrasse: null,
|
||||||
|
betreuerPlz: null,
|
||||||
|
// Der Ort in der Datumszeile ist der Ort des Betreuers, nicht der des
|
||||||
|
// Heims - er steht nirgends im Datenmodell und bleibt deshalb leer.
|
||||||
|
betreuerOrt: null,
|
||||||
|
betreuteName: `${b.anrede ? `${b.anrede} ` : ''}${b.vorname} ${b.nachname}`.trim(),
|
||||||
|
betreuteGeburtsdatum: b.geburtsdatum,
|
||||||
|
betreuteAnschrift: anschrift || null,
|
||||||
|
betreuungsgericht: b.betreuungsgericht,
|
||||||
|
aktenzeichenGericht: b.aktenzeichenGericht,
|
||||||
|
bestellungAm: b.bestellungAm,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -205,3 +205,23 @@ export function kuendigungsampel(plan: Kuendigungsplan): Kuendigungsampel | null
|
|||||||
if (tage <= 60) return { variante: 'gelb', text: `Absendetag in ${tage} Tagen` }
|
if (tage <= 60) return { variante: 'gelb', text: `Absendetag in ${tage} Tagen` }
|
||||||
return { variante: 'gruen', text: `Absendetag in ${tage} Tagen` }
|
return { variante: 'gruen', text: `Absendetag in ${tage} Tagen` }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Vertragsdauer aus Beginn und vereinbartem Ende.
|
||||||
|
*
|
||||||
|
* Das Datenmodell kennt kein Feld "Laufzeit" (siehe
|
||||||
|
* docs/schema-wuensche/substanz.md, Punkt 8). Stehen aber beide Daten im
|
||||||
|
* Vertrag, ergibt sich die Dauer daraus: Ein Vertrag vom 01.01.2018 bis
|
||||||
|
* 01.01.2021 hat eine dreijaehrige Laufzeit und verlaengert sich um
|
||||||
|
* dieselbe Spanne. Gerundet wird auf volle Jahre - ein Jahr sind 365 Tage,
|
||||||
|
* ein Schaltjahr 366, und beides soll dieselbe Laufzeit ergeben. Liegt die
|
||||||
|
* Spanne unter einem halben Jahr, bleibt es beim Standard.
|
||||||
|
*/
|
||||||
|
export function laufzeitAusVertrag(
|
||||||
|
beginnAm: Date | null,
|
||||||
|
endeAm: Date | null,
|
||||||
|
): number | null {
|
||||||
|
if (!beginnAm || !endeAm) return null
|
||||||
|
const jahre = Math.round(tageZwischen(beginnAm, endeAm) / 365.25)
|
||||||
|
return jahre >= 1 ? jahre : null
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
kuendigungsampel,
|
kuendigungsampel,
|
||||||
kuendigungsplan,
|
kuendigungsplan,
|
||||||
KUENDIGUNG_VORWARNUNG_TAGE,
|
KUENDIGUNG_VORWARNUNG_TAGE,
|
||||||
|
laufzeitAusVertrag,
|
||||||
POSTLAUF_PUFFER_TAGE,
|
POSTLAUF_PUFFER_TAGE,
|
||||||
} from '@/lib/vertraege/kuendigung'
|
} from '@/lib/vertraege/kuendigung'
|
||||||
import { kuendigungsassistent, genehmigungsfrage } from '@/lib/vertraege/kuendigungsassistent'
|
import { kuendigungsassistent, genehmigungsfrage } from '@/lib/vertraege/kuendigungsassistent'
|
||||||
@@ -287,3 +288,28 @@ describe('Beiträge und Vertragsarten', () => {
|
|||||||
expect(kannRueckkaufswertHaben('STROM')).toBe(false)
|
expect(kannRueckkaufswertHaben('STROM')).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Vertragsdauer aus Beginn und Ende', () => {
|
||||||
|
it('leitet die Laufzeit aus einem festen Vertragsende ab', () => {
|
||||||
|
expect(laufzeitAusVertrag(tag(2018, 1, 1), tag(2021, 1, 1))).toBe(3)
|
||||||
|
expect(laufzeitAusVertrag(tag(2020, 3, 15), tag(2021, 3, 15))).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('gibt null zurück, wenn ein Datum fehlt oder die Spanne unter einem Jahr liegt', () => {
|
||||||
|
expect(laufzeitAusVertrag(tag(2020, 1, 1), null)).toBeNull()
|
||||||
|
expect(laufzeitAusVertrag(null, tag(2021, 1, 1))).toBeNull()
|
||||||
|
expect(laufzeitAusVertrag(tag(2020, 1, 1), tag(2020, 7, 1))).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lässt einen dreijährigen Vertrag um drei Jahre weiterspringen', () => {
|
||||||
|
const plan = kuendigungsplan({
|
||||||
|
beginnAm: tag(2018, 1, 1),
|
||||||
|
endeAm: tag(2021, 1, 1),
|
||||||
|
laufzeitJahre: laufzeitAusVertrag(tag(2018, 1, 1), tag(2021, 1, 1)),
|
||||||
|
kuendigungsfristMonate: 3,
|
||||||
|
heute: tag(2026, 2, 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(plan.naechsterTermin).toEqual(tag(2027, 1, 1))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user