feat: Immobilie, Versicherungen und Heim mit COPD-Verlauf

Etappe 6. Das leerstehende Haus, die Vertraege und der
Gesundheitsverlauf - die drei Bereiche, in denen fuer diesen Fall das
meiste Geld und das meiste Risiko liegt.

Leerstands-Checkliste mit zehn Punkten, jeder mit Begruendung. Der
wichtigste: den Gebaeudeversicherer ueber den Leerstand informieren -
sonst droht Leistungsfreiheit, und zwar genau dann, wenn es brennt oder
eine Leitung platzt. Dazu Verkehrssicherungspflicht, Frostschutz zum
Oktober, Kontrollgaenge, Zaehlerstaende, Schluesselverzeichnis.

Der Kuendigungsrechner nennt den spaetesten ABSENDETAG, nicht den
Fristablauf: eine Kuendigung wirkt mit dem Zugang, nicht mit dem
Abschicken. Drei Tage Postlaufpuffer sind einkalkuliert.

Der Kuendigungs-Assistent beantwortet die Genehmigungsfrage nach
§ 1850 BGB nicht - er stellt sie und verlangt den Gang zum
Rechtspfleger. Der Versandnachweis ist Pflichtfeld: ohne ihn kommt die
Kuendigung nicht durch die Pruefung.

Leistungszuschlag nach § 43c SGB XI mit Stand 01.01.2024
(15/30/50/75 Prozent nach Verweildauer, erst ab Pflegegrad 2). Die
Oberflaeche zeigt, ab wann die naechste Stufe greift - das ist bares
Geld und wird regelmaessig uebersehen.

COPD-Schnellerfassung fuer Exazerbation, Sauerstoff, Sturz,
Krankenhaus, Hilfsmittel und Impfungen, mit STIKO-Plan fuer COPD.
Beschreibungen verschluesselt. Die Jahresauswertung ueber Exazerbationen
und Krankenhaustage wandert in den Jahresbericht und begruendet eine
Hoeherstufung.

74 Tests.

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