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.
162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import { useActionState, useState } from 'react'
|
|
import { departmentAdminText } from '@/lib/i18n/departments'
|
|
import {
|
|
createDepartmentAction,
|
|
updateDepartmentAction,
|
|
type ActionResult,
|
|
} from './actions'
|
|
|
|
type Existing = {
|
|
id: string
|
|
key: string
|
|
name: string
|
|
description: string | null
|
|
minClearance: number
|
|
isArchived: boolean
|
|
memberCount: number | null
|
|
}
|
|
|
|
/**
|
|
* Anlegen und Ändern in einem Formular.
|
|
*
|
|
* Das Kürzel steht nach dem Anlegen fest: es steckt in der Adresse jeder
|
|
* Abteilungsseite, und ein wandernder Bezeichner macht geteilte Links wertlos.
|
|
* Wer sich vertan hat, legt neu an und archiviert die alte - archivieren
|
|
* statt löschen, weil an einer Abteilung Vermerke und Zuordnungen hängen.
|
|
*/
|
|
export function DepartmentForm({
|
|
levels,
|
|
existing,
|
|
}: {
|
|
levels: { level: number; name: string }[]
|
|
existing?: Existing
|
|
}) {
|
|
const [state, action, pending] = useActionState<ActionResult | null, FormData>(
|
|
existing ? updateDepartmentAction : createDepartmentAction,
|
|
null,
|
|
)
|
|
const [offen, setOffen] = useState(!existing)
|
|
|
|
if (existing && !offen) {
|
|
return (
|
|
<div className="record-row">
|
|
<span className="record-key">{existing.key}</span>
|
|
<span className="record-title">{existing.name}</span>
|
|
{existing.memberCount === null ? null : (
|
|
<span className="font-condensed text-2xs text-text-dim">
|
|
{departmentAdminText.members} {existing.memberCount}
|
|
</span>
|
|
)}
|
|
{existing.isArchived ? (
|
|
<span className="font-condensed text-2xs text-text-dim">
|
|
{departmentAdminText.archived}
|
|
</span>
|
|
) : null}
|
|
<button type="button" onClick={() => setOffen(true)} className="btn btn-sm">
|
|
{departmentAdminText.edit}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form action={action} className="max-w-(--container-measure) space-y-4">
|
|
{existing ? <input type="hidden" name="id" value={existing.id} /> : null}
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<label className="block">
|
|
<span className="field-label">
|
|
{departmentAdminText.key}
|
|
</span>
|
|
{existing ? (
|
|
<p className="mt-1.5 font-mono text-sm">{existing.key}</p>
|
|
) : (
|
|
<>
|
|
<input name="key" required maxLength={21} className="field w-full font-mono" />
|
|
<span className="field-hint">
|
|
{departmentAdminText.keyHint}
|
|
</span>
|
|
</>
|
|
)}
|
|
</label>
|
|
|
|
<label className="block">
|
|
<span className="field-label">
|
|
{departmentAdminText.name}
|
|
</span>
|
|
<input
|
|
name="name"
|
|
required
|
|
maxLength={120}
|
|
defaultValue={existing?.name}
|
|
className="field w-full"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<label className="block">
|
|
<span className="field-label">
|
|
{departmentAdminText.description}
|
|
</span>
|
|
<textarea
|
|
name="description"
|
|
rows={2}
|
|
maxLength={500}
|
|
defaultValue={existing?.description ?? ''}
|
|
className="field w-full"
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex flex-wrap items-end gap-6">
|
|
<label className="block">
|
|
<span className="field-label">
|
|
{departmentAdminText.minClearance}
|
|
</span>
|
|
<select
|
|
name="minClearance"
|
|
defaultValue={existing?.minClearance ?? 0}
|
|
className="field"
|
|
>
|
|
{levels.map((level) => (
|
|
<option key={level.level} value={level.level}>
|
|
{level.level} · {level.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="field-hint">
|
|
{departmentAdminText.minClearanceHint}
|
|
</span>
|
|
</label>
|
|
|
|
{existing ? (
|
|
<label className="flex items-center gap-2 pb-2 text-sm">
|
|
<input type="checkbox" name="isArchived" defaultChecked={existing.isArchived} />
|
|
{departmentAdminText.archived}
|
|
</label>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-4">
|
|
<button type="submit" disabled={pending} aria-busy={pending} className="btn btn-primary">
|
|
{existing ? departmentAdminText.save : departmentAdminText.create}
|
|
</button>
|
|
{existing ? (
|
|
<button type="button" onClick={() => setOffen(false)} className="btn btn-sm">
|
|
{departmentAdminText.cancel}
|
|
</button>
|
|
) : null}
|
|
{state?.ok ? (
|
|
<span className="font-condensed text-2xs text-text-dim">
|
|
{existing ? departmentAdminText.saved : departmentAdminText.created}
|
|
</span>
|
|
) : null}
|
|
{state && !state.ok ? (
|
|
<span className="font-condensed text-2xs text-stamp-4">{state.error}</span>
|
|
) : null}
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|