Files
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +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>
)
}