Files
AfiS-Aktensystem/web/app/(app)/faelle/[caseNumber]/CaseActions.tsx
T
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

178 lines
6.2 KiB
TypeScript

'use client'
import { useState, useTransition } from 'react'
import { caseText, entryText } from '@/lib/i18n/cases'
import { actionErrorsText, commonText } from '@/lib/i18n/common'
import { changeStatusAction, reclassifyAction, setDueDateAction } from '../actions'
/**
* Statuswechsel und Umklassifizierung.
*
* Bewusst **nicht** optimistisch: beides ist folgenreich und teils nicht
* umkehrbar. Stattdessen ein sofortiger Wartezustand am Knopf und eine klare
* Rückmeldung. Siehe docs/09-performance-und-ux.md.
*/
export function CaseActions({
caseNumber,
classification,
statusTargets,
levels,
maxLevel,
dueAt,
}: {
caseNumber: string
classification: number
statusTargets: string[]
levels: { level: number; name: string }[]
maxLevel: number
/** Als `YYYY-MM-DD`, wie das Datumsfeld es erwartet. Leer heisst: keine. */
dueAt: string
}) {
const [nextStatus, setNextStatus] = useState(statusTargets[0] ?? '')
const [statusReason, setStatusReason] = useState('')
const [nextLevel, setNextLevel] = useState(classification)
const [levelReason, setLevelReason] = useState('')
const [frist, setFrist] = useState(dueAt)
const [error, setError] = useState<string | null>(null)
const [pending, startTransition] = useTransition()
const lowering = nextLevel < classification
function run(action: () => Promise<{ ok: boolean; error?: string }>, confirmText: string) {
if (!confirm(confirmText)) return
setError(null)
startTransition(async () => {
const result = await action()
if (!result.ok) setError(result.error ?? actionErrorsText.default!)
})
}
return (
<div className="mt-6 flex flex-wrap gap-x-10 gap-y-5 border-t border-line pt-5 text-xs">
{statusTargets.length > 0 ? (
<div>
<p className="font-condensed text-text-dim">{caseText.statusChange}</p>
<div className="mt-1.5 flex flex-wrap items-center gap-2">
<select
value={nextStatus}
onChange={(event) => setNextStatus(event.target.value)}
className="field"
>
{statusTargets.map((target) => (
<option key={target} value={target} title={caseText.statusHints[target]}>
{caseText.statusNames[target] ?? target}
</option>
))}
</select>
<input
value={statusReason}
onChange={(event) => setStatusReason(event.target.value)}
placeholder={entryText.changeReason}
className="w-56 field"
/>
<button
type="button"
disabled={pending || !nextStatus}
onClick={() =>
run(
() =>
changeStatusAction({
caseNumber,
to: nextStatus,
reason: statusReason || undefined,
}),
`${caseText.statusChange}: ${caseText.statusNames[nextStatus] ?? nextStatus}?`,
)
}
className="btn btn-primary"
>
{pending ? caseText.saving : commonText.save}
</button>
</div>
{/* Welche Zustände von hier aus erreichbar sind, ergibt sich aus dem
Lebenszyklus - aus einem Entwurf führt nur „Offen" heraus. Ohne
diesen Satz sieht die kurze Auswahl aus wie ein Fehler. */}
<p className="mt-1.5 max-w-sm text-2xs text-text-dim">
{caseText.statusHints[nextStatus] ?? caseText.statusChangeHint}
</p>
</div>
) : null}
<div>
<p className="font-condensed text-text-dim">{caseText.dueAt}</p>
<div className="mt-1.5 flex flex-wrap items-center gap-2">
<input
type="date"
value={frist}
onChange={(event) => setFrist(event.target.value)}
className="field font-mono"
/>
<button
type="button"
disabled={pending || frist === dueAt}
onClick={() =>
run(
() => setDueDateAction({ caseNumber, dueAt: frist || undefined }),
frist ? caseText.dueAtConfirm(frist) : caseText.dueAtClearConfirm,
)
}
className="btn btn-primary"
>
{pending ? caseText.saving : commonText.save}
</button>
</div>
<p className="mt-1.5 max-w-sm text-2xs text-text-dim">{caseText.dueAtHint}</p>
</div>
<div>
<p className="font-condensed text-text-dim">{caseText.reclassify}</p>
<div className="mt-1.5 flex flex-wrap items-center gap-2">
<select
value={nextLevel}
onChange={(event) => setNextLevel(Number(event.target.value))}
className="field"
>
{levels
.filter((level) => level.level <= maxLevel)
.map((level) => (
<option key={level.level} value={level.level}>
{level.level} · {level.name}
</option>
))}
</select>
{/* Eine Herabstufung macht Inhalte für Leute sichtbar, die sie vorher
nicht sehen durften. Deshalb ist die Begründung Pflicht. */}
{lowering ? (
<input
value={levelReason}
onChange={(event) => setLevelReason(event.target.value)}
placeholder={caseText.reclassifyReason}
className="w-64 field"
/>
) : null}
<button
type="button"
disabled={pending || nextLevel === classification}
onClick={() =>
run(
() =>
reclassifyAction({
caseNumber,
to: nextLevel,
reason: levelReason || undefined,
}),
`${caseText.reclassify}: ${nextLevel}?`,
)
}
className="btn btn-primary"
>
{pending ? caseText.saving : commonText.save}
</button>
</div>
</div>
{error ? <p className="w-full font-condensed text-2xs text-stamp-4">{error}</p> : null}
</div>
)
}