Statuswechsel als Karte statt als wandernde Auswahlliste
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

Das Auswahlfeld enthielt nur die gerade erreichbaren Zustaende. Damit sah
man weder, wo die Akte steht, noch welche Zustaende es ueberhaupt gibt - und
nach jedem Wechsel standen andere Eintraege darin. Eine Auswahl, die ihre
Eintraege tauscht, ist keine Orientierung.

Jetzt stehen immer alle sieben da, in der Reihenfolge des Lebenszyklus:
Entwurf, Offen, In Pruefung, Abgeschlossen, Archiviert als eine Linie,
darunter Ruhend und Versiegelt als Nebenwege. Der jetzige Zustand ist
markiert, erreichbare sind Knoepfe, gesperrte bleiben blass stehen statt zu
verschwinden - mit dem Grund als Titel daran.

Der Grund ist dabei nicht dasselbe wie die Sperre: "von hier fuehrt kein
Weg dorthin" laesst sich durch kein Recht beheben, "dir fehlt case.close"
schon. Wer das verwechselt, sucht an der falschen Stelle.

Was ein Wechsel verlangt, steht jetzt vorher da statt als Absage danach:
die Pflichtbegruendung beim Wiedereroeffnen und beim Versiegeln, und dass
vor dem Abschluss die noetigen Abschnitte gefuellt sein muessen. Bei
Pflichtbegruendung bleibt der Knopf gesperrt, bis sie dasteht.

Die Rechtepruefung rechnet der Server, mit derselben Funktion, die der
Dienst beim Wechsel anwendet - vorher zeigte die Liste auch Ziele, die
danach abgelehnt wurden. Zwei Pruefungen halten das fest, und zwar in beide
Richtungen: nichts angeboten, was scheitern wuerde, und nichts verschwiegen,
was ginge. Die zweite ist die wichtigere, weil ihr Fehlen nicht auffaellt.
This commit is contained in:
Harlekin
2026-09-04 16:26:29 +02:00
parent 42dbf0f6b1
commit bd750f26d0
6 changed files with 496 additions and 57 deletions
@@ -1,9 +1,11 @@
'use client'
import { useState, useTransition } from 'react'
import { caseText, entryText } from '@/lib/i18n/cases'
import { caseText } from '@/lib/i18n/cases'
import { actionErrorsText, commonText } from '@/lib/i18n/common'
import { changeStatusAction, reclassifyAction, setDueDateAction } from '../actions'
import { reclassifyAction, setDueDateAction } from '../actions'
import { StatusMap } from '@/components/cases/StatusMap'
import type { StatusOption } from '@/lib/cases/lifecycle'
/**
* Statuswechsel und Umklassifizierung.
@@ -15,21 +17,22 @@ import { changeStatusAction, reclassifyAction, setDueDateAction } from '../actio
export function CaseActions({
caseNumber,
classification,
statusTargets,
status,
statusOptions,
levels,
maxLevel,
dueAt,
}: {
caseNumber: string
classification: number
statusTargets: string[]
status: string
/** Zu jedem Zustand des Lebenszyklus, ob er von hier aus geht. */
statusOptions: StatusOption[]
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)
@@ -49,54 +52,7 @@ export function CaseActions({
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}
<StatusMap caseNumber={caseNumber} current={status} options={statusOptions} />
<div>
<p className="font-condensed text-text-dim">{caseText.dueAt}</p>