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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
188 lines
6.2 KiB
TypeScript
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>
|
|
)
|
|
}
|