Files
AfiS-Aktensystem/web/app/(app)/faelle/neu/NewCaseForm.tsx
T
Harlekin 738330cee5 Vollständiger Statusfilter, Beteiligte beim Anlegen, Knöpfe in den Listen
Vier Sachen aus der Durchsicht.

**Der Statusfilter zeigte fünf von sieben Zuständen.** DRAFT und SEALED
fehlten - ausgerechnet die beiden, die in der Liste am häufigsten standen; man
konnte nach dem, was man sah, gerade nicht filtern. Das erklärt auch die Frage,
was „In Prüfung" und „Ruhend" bedeuten: die Zustände standen nirgends in der
Oberfläche. Jetzt alle sieben in der Reihenfolge des Lebenszyklus, mit einer
Erklärung als title, und Versiegelt nur für den, der Versiegeltes lesen darf.
Ein Test verlangt zu jedem Zustand einen Namen und eine Erklärung.

**Beliebig viele Beteiligte beim Anlegen.** Das Formular kannte genau eine
„betroffene Person", und die Rolle war auf SUBJECT festgelegt. Jetzt beliebig
viele Zeilen mit je einer Rolle, Konten und Personalakten in einer Auswahl, wie
auf der Aktenseite. Die Zerlegung steht als parseParticipantDrafts in
lib/cases/terms.ts und nicht in der Server Action: seit die Rolle über den
Zugriff entscheidet, darf eine erfundene Rolle nicht durchrutschen.

**Die Filterleisten sind Knöpfe** wie überall sonst, mit gefülltem aktivem
Filter.

**Keine leere Kennungsspalte mehr.** In „Zuletzt geöffnet" stand bei einer
Person nichts, wo sonst das Aktenzeichen steht - jetzt steht dort
„Personalakte". Nebenbei zwei Stellen zurückgenommen, an denen die
Spaltenklasse im Seitenkopf gelandet war statt in einer Zeile.
2026-09-03 23:38:56 +02:00

188 lines
6.2 KiB
TypeScript

'use client'
import { useActionState, useState } from 'react'
import { caseText, participantText } from '@/lib/i18n/cases'
import { clearanceText } from '@/lib/i18n/common'
import { PARTICIPANT_ROLES } from '@/lib/cases/terms'
import { createCaseAction, type CreateState } from '../actions'
/** Ein Beteiligter im Formular, bevor die Akte existiert. */
type Entwurf = { schluessel: number; ziel: string; rolle: string }
export function NewCaseForm({
templates,
levels,
people,
members,
defaultLevel,
}: {
templates: { key: string; name: string }[]
levels: { level: number; name: string }[]
people: { id: string; displayName: string }[]
members: { id: string; displayName: string }[]
defaultLevel: number
}) {
const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {})
// Beliebig viele Beteiligte, jeder mit seiner Rolle. Vorher gab es genau
// eine „betroffene Person" — wer einen Zeugen und einen Täter eintragen
// wollte, musste die Akte erst anlegen und dann noch einmal hinein.
const [beteiligte, setBeteiligte] = useState<Entwurf[]>([])
const [naechster, setNaechster] = useState(1)
function hinzufuegen() {
setBeteiligte((bisher) => [...bisher, { schluessel: naechster, ziel: '', rolle: 'SUBJECT' }])
setNaechster((n) => n + 1)
}
function aendern(schluessel: number, teil: Partial<Entwurf>) {
setBeteiligte((bisher) =>
bisher.map((eintrag) =>
eintrag.schluessel === schluessel ? { ...eintrag, ...teil } : eintrag,
),
)
}
return (
<form action={action} className="mt-6 space-y-5">
<div>
<label htmlFor="title" className="field-label">
{caseText.title}
</label>
<input
id="title"
name="title"
required
minLength={3}
maxLength={200}
className="mt-1.5 block w-full field"
/>
</div>
<div>
<label htmlFor="templateKey" className="field-label">
{caseText.type}
</label>
<select
id="templateKey"
name="templateKey"
required
className="mt-1.5 block field"
>
{templates.map((template) => (
<option key={template.key} value={template.key}>
{template.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="classification" className="field-label">
{clearanceText.classification}
</label>
{/* Die Auswahl zeigt nur, was erlaubt ist. Geprüft wird trotzdem serverseitig. */}
<select
id="classification"
name="classification"
defaultValue={defaultLevel}
className="mt-1.5 block field"
>
{levels.map((level) => (
<option key={level.level} value={level.level}>
{level.level} · {level.name}
</option>
))}
</select>
</div>
<fieldset>
<legend className="field-label">{participantText.heading}</legend>
<p className="field-hint">{participantText.hint}</p>
{beteiligte.length === 0 ? (
<p className="mt-3 text-2xs text-text-dim">{participantText.empty}</p>
) : (
<ul className="mt-3 space-y-2">
{beteiligte.map((eintrag) => (
<li key={eintrag.schluessel} className="flex flex-wrap items-center gap-3">
{/* Ein verstecktes Feld je Zeile: die Server Action liest sie
mit getAll und braucht keine Zählung im Namen. */}
<input
type="hidden"
name="participants"
value={`${eintrag.ziel}|${eintrag.rolle}`}
/>
<select
aria-label={participantText.role}
value={eintrag.rolle}
onChange={(event) =>
aendern(eintrag.schluessel, { rolle: event.target.value })
}
className="field w-48"
>
{PARTICIPANT_ROLES.map((value) => (
<option key={value} value={value}>
{participantText.roles[value] ?? value}
</option>
))}
</select>
<select
aria-label={participantText.person}
value={eintrag.ziel}
onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })}
className="field grow"
>
<option value="">—</option>
<optgroup label={participantText.pickUser}>
{members.map((member) => (
<option key={member.id} value={`user:${member.id}`}>
{member.displayName}
</option>
))}
</optgroup>
<optgroup label={participantText.pickPersonnel}>
{people.map((person) => (
<option key={person.id} value={`file:${person.id}`}>
{person.displayName}
</option>
))}
</optgroup>
</select>
<button
type="button"
onClick={() =>
setBeteiligte((bisher) =>
bisher.filter((e) => e.schluessel !== eintrag.schluessel),
)
}
className="btn btn-sm"
>
{participantText.remove}
</button>
</li>
))}
</ul>
)}
<button type="button" onClick={hinzufuegen} className="mt-3 btn btn-sm">
{participantText.add}
</button>
</fieldset>
<div className="flex items-center gap-3">
<button
type="submit"
disabled={pending}
className="btn btn-primary"
>
{pending ? caseText.saving : caseText.create}
</button>
{state.error ? (
<span className="font-condensed text-2xs text-stamp-4">{state.error}</span>
) : null}
</div>
</form>
)
}