Files
betreuungs-cockpit/src/components/vorgaenge/vorgangs-formular.tsx
T
Florian Wilde 9e68d10787 Ansprechpartner am Vorgang hinterlegen
Ein namentlich adressiertes Schreiben landet bei der Person, die den
Vorgang kennt, statt in der Poststelle. Am Vorgang laesst sich jetzt ein
Kontakt aus dem Verzeichnis als Ansprechpartner benennen; die
Sachstandsanfrage redet ihn an, und sein Name steht ueber der Behoerde.

Die Sachstandsanfrage konnte das schon - sie bekam den Namen nur nie
uebergeben. entwurf.ts lud einen der verknuepften Kontakte und verwarf
ihn wieder. Wer angeredet wird, ist jetzt eine Entscheidung und kein
Zufall.

Aufbau:
- Migration: ansprechpartnerId am Vorgang (SetNull - ein Vorgang darf
  nicht daran haengen, dass es den Kontakt noch gibt) und anrede am
  Kontakt.
- Die Anrede als reine Funktion in kontakte/anrede.ts. Geraten wird nie:
  ohne hinterlegte Anrede wird mit vollem Namen angeredet. Ein leerer
  Name mit gesetzter Anrede faellt auf die allgemeine Anrede zurueck -
  "Sehr geehrte Frau ," darf nie herausgehen.
- Der Wechsel des Ansprechpartners steht mit NAMEN im Protokoll. Zwei
  cuids nebeneinander waeren protokolliert, aber nicht nachvollziehbar,
  und nachvollziehbar war die Anforderung.
- Der Ansprechpartner wird gegen die Akte geprueft, bevor er gespeichert
  wird - sein Name steht anschliessend im Briefkopf.

Nebenbei behoben: Der Schreiben-Editor verwarf die empfaengerAnschrift
aus dem Entwurf, sein State startete immer leer. Jetzt kommt die
Anschrift des Ansprechpartners im Formular an.

Geprueft: neun Einheitstests fuer die Anrede, fuenf End-to-End-Tests von
der Auswahl bis zum Protokolleintrag.

Refs #9
2026-08-23 16:31:10 +02:00

248 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useActionState } from 'react'
import Link from 'next/link'
import { AlertCircle } from 'lucide-react'
import type { VorgangKategorie, VorgangStatus } from '@prisma/client'
import { Button } from '@/components/ui/button'
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 { Rechtshinweis } from '@/components/rechtshinweis'
import { vorgangAendern, vorgangAnlegen } from '@/server/actions/vorgaenge'
import {
KATEGORIE_BESCHRIFTUNG,
PRIORITAET_BESCHRIFTUNG,
VORGANG_KATEGORIEN,
VORGANG_STATUS,
type Formularzustand,
} from '@/lib/validierung/vorgaenge'
import { GENEHMIGUNG_WARNUNG } from '@/lib/vorgaenge/genehmigungskette'
import { VORGANG_STATUS_BESCHRIFTUNG } from '@/lib/vorgaenge/zeitstrahl'
/** Date -> "2026-08-22" für <input type="date">. */
function alsFeldwert(datum: Date | null | undefined): string {
if (!datum) return ''
return datum.toISOString().slice(0, 10)
}
export type VorgangsFormularWerte = {
titel: string
kategorie: VorgangKategorie
status: VorgangStatus
prioritaet: number
beschreibung: string | null
gegenstelle: string | null
aktenzeichen: string | null
ansprechpartnerId: string | null
verantwortlich: string | null
naechsterSchritt: string | null
faelligAm: Date | null
letzterKontaktAm: Date | null
genehmigungspflichtig: boolean
}
export function VorgangsFormular({
modus,
betreuteId,
vorgangId,
vorlageSchluessel,
werte,
abbrechenHref,
kontakte,
}: {
modus: 'anlegen' | 'aendern'
betreuteId: string
vorgangId?: string
vorlageSchluessel?: string | null
werte: VorgangsFormularWerte
abbrechenHref: string
/** Kontakte dieser Akte, zur Wahl des Ansprechpartners. */
kontakte: { id: string; name: string; organisation: string | null }[]
}) {
const [zustand, aktion, laeuft] = useActionState<Formularzustand, FormData>(
modus === 'anlegen' ? vorgangAnlegen : vorgangAendern,
{},
)
return (
<form action={aktion} className="space-y-8">
<input type="hidden" name="betreuteId" value={betreuteId} />
{vorgangId && <input type="hidden" name="vorgangId" value={vorgangId} />}
{modus === 'anlegen' && (
<input type="hidden" name="vorlageSchluessel" value={vorlageSchluessel ?? ''} />
)}
<Feldgruppe titel="Worum geht es?">
<Feld
id="titel"
beschriftung="Titel"
erklaerung="So, dass du den Vorgang in einem Jahr wiedererkennst."
fehler={zustand.felder?.titel}
pflicht
className="sm:col-span-2"
>
<Input name="titel" defaultValue={werte.titel} required maxLength={200} />
</Feld>
<Feld id="kategorie" beschriftung="Kategorie" fehler={zustand.felder?.kategorie} pflicht>
<Select name="kategorie" defaultValue={werte.kategorie}>
{VORGANG_KATEGORIEN.map((wert) => (
<option key={wert} value={wert}>
{KATEGORIE_BESCHRIFTUNG[wert]}
</option>
))}
</Select>
</Feld>
<Feld id="status" beschriftung="Status" fehler={zustand.felder?.status}>
<Select name="status" defaultValue={werte.status}>
{VORGANG_STATUS.map((wert) => (
<option key={wert} value={wert}>
{VORGANG_STATUS_BESCHRIFTUNG[wert]}
</option>
))}
</Select>
</Feld>
<Feld
id="prioritaet"
beschriftung="Priorität"
erklaerung="Hoch heißt: das treibt gerade."
fehler={zustand.felder?.prioritaet}
>
<Select name="prioritaet" defaultValue={String(werte.prioritaet)}>
{[1, 2, 3].map((wert) => (
<option key={wert} value={wert}>
{PRIORITAET_BESCHRIFTUNG[wert]}
</option>
))}
</Select>
</Feld>
<Feld id="verantwortlich" beschriftung="Verantwortlich" fehler={zustand.felder?.verantwortlich}>
<Input name="verantwortlich" defaultValue={werte.verantwortlich ?? ''} maxLength={120} />
</Feld>
<Feld
id="beschreibung"
beschriftung="Beschreibung"
erklaerung="Wird verschlüsselt gespeichert."
fehler={zustand.felder?.beschreibung}
className="sm:col-span-2"
>
<Textarea name="beschreibung" defaultValue={werte.beschreibung ?? ''} rows={5} />
</Feld>
</Feldgruppe>
<Feldgruppe
titel="Wer ist am Zug?"
beschreibung="Behörde, Aktenzeichen und der letzte Kontakt - daraus entsteht später die Sachstandsanfrage."
>
<Feld id="gegenstelle" beschriftung="Gegenstelle" fehler={zustand.felder?.gegenstelle}>
<Input
name="gegenstelle"
defaultValue={werte.gegenstelle ?? ''}
maxLength={150}
placeholder="z. B. Bezirk Oberfranken"
/>
</Feld>
<Feld
id="ansprechpartnerId"
beschriftung="Ansprechpartner"
erklaerung="Wer dort die Sache bearbeitet. Schreiben zu diesem Vorgang reden ihn namentlich an — das bringt sie an der Poststelle vorbei."
fehler={zustand.felder?.ansprechpartnerId}
>
<Select name="ansprechpartnerId" defaultValue={werte.ansprechpartnerId ?? ''}>
<option value="">– keiner benannt –</option>
{kontakte.map((kontakt) => (
<option key={kontakt.id} value={kontakt.id}>
{[kontakt.name, kontakt.organisation].filter(Boolean).join(' · ')}
</option>
))}
</Select>
</Feld>
<Feld
id="aktenzeichen"
beschriftung="Aktenzeichen"
erklaerung="Das Zeichen, unter dem die Gegenstelle den Vorgang führt."
fehler={zustand.felder?.aktenzeichen}
>
<Input name="aktenzeichen" defaultValue={werte.aktenzeichen ?? ''} maxLength={80} />
</Feld>
<Feld
id="naechsterSchritt"
beschriftung="Nächster Schritt"
erklaerung="Der eine konkrete Handgriff, der als Nächstes ansteht."
fehler={zustand.felder?.naechsterSchritt}
className="sm:col-span-2"
>
<Input
name="naechsterSchritt"
defaultValue={werte.naechsterSchritt ?? ''}
maxLength={300}
/>
</Feld>
<Feld id="faelligAm" beschriftung="Fällig am" fehler={zustand.felder?.faelligAm}>
<Input name="faelligAm" type="date" defaultValue={alsFeldwert(werte.faelligAm)} />
</Feld>
<Feld
id="letzterKontaktAm"
beschriftung="Letzter Kontakt am"
fehler={zustand.felder?.letzterKontaktAm}
>
<Input
name="letzterKontaktAm"
type="date"
defaultValue={alsFeldwert(werte.letzterKontaktAm)}
/>
</Feld>
</Feldgruppe>
<div className="space-y-3">
<div className="flex items-start gap-3">
<Checkbox
id="genehmigungspflichtig"
name="genehmigungspflichtig"
defaultChecked={werte.genehmigungspflichtig}
className="mt-1"
/>
<label htmlFor="genehmigungspflichtig" className="text-base font-medium">
Dieser Vorgang braucht eine Genehmigung des Betreuungsgerichts
</label>
</div>
<p className="text-sm text-muted-foreground">{GENEHMIGUNG_WARNUNG}</p>
<Rechtshinweis rechtsgrundlage="§ 1858 BGB" kurz />
</div>
{zustand.fehler && (
<p className="flex items-start gap-1.5 text-sm font-medium text-destructive" role="alert">
<AlertCircle className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
{zustand.fehler}
</p>
)}
{zustand.erfolg && (
<p className="text-sm text-muted-foreground" role="status">
{zustand.erfolg}
</p>
)}
<div className="flex flex-wrap gap-3">
<Button type="submit" disabled={laeuft}>
{laeuft ? 'Speichert…' : modus === 'anlegen' ? 'Vorgang anlegen' : 'Speichern'}
</Button>
<Button type="button" variant="ghost" asChild>
<Link href={abbrechenHref}>Abbrechen</Link>
</Button>
</div>
</form>
)
}