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
248 lines
8.3 KiB
TypeScript
248 lines
8.3 KiB
TypeScript
'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>
|
||
)
|
||
}
|