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