Files
betreuungs-cockpit/src/components/vertraege/kuendigungs-assistent.tsx
T
Florian Wilde c21d03abf6 Schreiben erzeugen, ausgeben und ablegen
Die Anschreiben gab es bisher nur als Text zum Kopieren. Danach war
Handarbeit: in Word nachbauen, ausdrucken, abschicken - und noch einmal
von Hand als Dokument in die Akte legen. Der letzte Schritt wurde am
haeufigsten vergessen, und dann fehlt der Nachweis im Bericht.

Neu unter /schreiben/neu: bearbeiten, als Word-Datei herunterladen, in
die Akte legen, Wiedervorlage setzen - in einem Zug. Eine Seite fuer alle
Arten und alle drei Empfaengerwege, statt die Bearbeitung dreimal an
Vertrag, Vorgang und Freitext nachzubauen.

Der dritte Weg ist der eigentliche Zugewinn: Schreiben an eine Stelle,
zu der es noch keinen Datensatz gibt. Am Anfang einer Betreuung ist das
der Normalfall - man fragt ja an, um zu erfahren, ob dort ueberhaupt
etwas besteht. Aus der freien Anschrift laesst sich mit einem Klick ein
Kontakt anlegen.

Zwei Entscheidungen, die erklaert gehoeren:

Die Akte bekommt ein PDF, heruntergeladen wird .docx. Der
Objektspeicher nimmt nur PDF und Bilder an, geprueft an den Magic Bytes.
Eine .docx ist ein ZIP-Archiv; sie durchzulassen hiesse jedes ZIP
durchzulassen, und diese Pruefung ist eine bewusste Zusage der
Anwendung. Sachlich ist PDF fuer den Nachweis ohnehin richtiger: Es
aendert sich nicht mehr. Die Oberflaeche sagt dazu, dass eine spaeter in
Word geaenderte Fassung von der Akte abweicht.

Der Berichtssatz in berichte/docx.ts bleibt unangetastet. Er setzt einen
Bericht mit Kopfzeilen und Unterschriftenblock; ein Brief braucht
Absender, Anschriftenfeld, Datumszeile und Grussformel. Beides in eine
Funktion zu zwingen haette beide Ausgaben schlechter gemacht. Der
Umsetzungshinweis im Issue ("muss nur angebunden werden") war insoweit
falsch - von mir selbst geschrieben.

Die vorhandenen Erzeuger (Rueckkaufswert, Kuendigung, Sachstandsanfrage)
bleiben unveraendert; ihr Wortlaut ist getestet und erprobt.

Geprueft: zehn Einheitstests, darunter der Gleichschritt der
Wiedervorlagefristen ueber zwei Module hinweg. Fuenf End-to-End-Tests
auf beiden Geraeteprofilen: Entwurf, Vertragsbezug, Word-Download,
Ablage samt Wiedervorlage, Kontakt aus freier Anschrift.

Refs #4
2026-08-23 10:14:59 +02:00

285 lines
10 KiB
TypeScript

'use client'
import Link from 'next/link'
import { useActionState } from 'react'
import { CheckCircle2, FileDown, 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 { Button } from '@/components/ui/button'
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]"
/>
<div className="mt-4">
<Button asChild variant="outline">
<Link href={`/schreiben/neu?art=KUENDIGUNG&vertragId=${vertragId}`}>
<FileDown aria-hidden="true" />
Bearbeiten, als Word-Datei und in die Akte
</Link>
</Button>
</div>
</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>
)
}