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.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { useActionState } from 'react'
|
|
import { clearanceText, commonText } from '@/lib/i18n/common'
|
|
import { personnelText } from '@/lib/i18n/people'
|
|
import { createPersonnelAction, type ActionResult } from '../actions'
|
|
|
|
/**
|
|
* Neue Personalakte.
|
|
*
|
|
* Das Discord-Konto ist **optional**. Personen ohne Konto - Zeugen, ehemalige
|
|
* Mitarbeiter, Externe - brauchen genauso eine Akte, und ihr Name ist dann das
|
|
* einzige, was sie identifiziert.
|
|
*/
|
|
export function NewPersonnelForm({
|
|
members,
|
|
levels,
|
|
statusTags,
|
|
departments,
|
|
defaultLevel,
|
|
}: {
|
|
members: { id: string; displayName: string }[]
|
|
levels: { level: number; name: string }[]
|
|
statusTags: { key: string; name: string }[]
|
|
departments: { id: string; key: string; name: string }[]
|
|
defaultLevel: number
|
|
}) {
|
|
const [state, action, pending] = useActionState<ActionResult | null, FormData>(
|
|
createPersonnelAction,
|
|
null,
|
|
)
|
|
|
|
return (
|
|
<form action={action} className="mt-6 space-y-5">
|
|
<Feld id="displayName" label={personnelText.name} required minLength={2} />
|
|
|
|
<div>
|
|
<label htmlFor="userId" className="field-label">
|
|
{personnelText.linkedAccount}
|
|
</label>
|
|
<select
|
|
id="userId"
|
|
name="userId"
|
|
defaultValue=""
|
|
className="mt-1.5 block w-full field"
|
|
>
|
|
<option value="">{personnelText.noAccount}</option>
|
|
{members.map((member) => (
|
|
<option key={member.id} value={member.id}>
|
|
{member.displayName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="grid gap-5 sm:grid-cols-2">
|
|
<Feld id="rank" label={personnelText.rank} />
|
|
<label className="block">
|
|
<span className="field-label">
|
|
{personnelText.department}
|
|
</span>
|
|
<select name="departmentId" defaultValue="" className="field w-full">
|
|
<option value="">{personnelText.noDepartment}</option>
|
|
{departments.map((abteilung) => (
|
|
<option key={abteilung.id} value={abteilung.id}>
|
|
{abteilung.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<fieldset>
|
|
<legend className="font-condensed text-xs text-text-dim">{personnelText.status}</legend>
|
|
{/* Kästchen statt Auswahlliste: mehrere Etiketten sind der Normalfall,
|
|
und eine Mehrfachauswahl mit Strg-Klick findet niemand. */}
|
|
<div className="mt-2 flex flex-wrap gap-x-6 gap-y-2">
|
|
{statusTags.map((tag) => (
|
|
<label key={tag.key} className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" name="statusTags" value={tag.key} />
|
|
{tag.name}
|
|
</label>
|
|
))}
|
|
</div>
|
|
<p className="field-hint">{personnelText.statusHint}</p>
|
|
</fieldset>
|
|
|
|
<div>
|
|
<label htmlFor="note" className="field-label">
|
|
{personnelText.note}
|
|
</label>
|
|
<textarea
|
|
id="note"
|
|
name="note"
|
|
rows={3}
|
|
className="mt-1.5 block w-full field"
|
|
/>
|
|
<p className="field-hint">{personnelText.noteHelp}</p>
|
|
</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>
|
|
|
|
{state && !state.ok ? (
|
|
<p className="font-condensed text-2xs text-stamp-4">{state.error}</p>
|
|
) : null}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={pending}
|
|
className="btn btn-primary"
|
|
>
|
|
{pending ? commonText.loading : personnelText.create}
|
|
</button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
function Feld({
|
|
id,
|
|
label,
|
|
required,
|
|
minLength,
|
|
}: {
|
|
id: string
|
|
label: string
|
|
required?: boolean
|
|
minLength?: number
|
|
}) {
|
|
return (
|
|
<div>
|
|
<label htmlFor={id} className="field-label">
|
|
{label}
|
|
</label>
|
|
<input
|
|
id={id}
|
|
name={id}
|
|
required={required}
|
|
minLength={minLength}
|
|
maxLength={120}
|
|
className="mt-1.5 block w-full field"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|