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
285 lines
10 KiB
TypeScript
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>
|
|
)
|
|
}
|