import type { EffectivePermissions, Permission } from '@/lib/auth/permissions' import { canEditCase } from '@/lib/access/caseWrite' /** * Lebenszyklus einer Akte, siehe docs/05-akten-und-vorlagen.md. * * DRAFT ──► OPEN ──► REVIEW ──► CLOSED ──► ARCHIVED * │ ▲ │ * ▼ │ └──► OPEN (Rückgabe zur Nachbesserung) * DORMANT * * SEALED ist aus jedem Zustand erreichbar und nur mit Recht 'admin' oder * Stufe 5 lesbar. */ export type CaseStatusName = | 'DRAFT' | 'OPEN' | 'DORMANT' | 'REVIEW' | 'CLOSED' | 'ARCHIVED' | 'SEALED' export type Transition = { from: CaseStatusName to: CaseStatusName permission: Permission /** Verlangt eine Begründung, die als Vermerk im Verlauf landet. */ requiresReason?: boolean /** Verlangt, dass alle `requiredToClose`-Felder gefüllt sind. */ requiresCompleteSections?: boolean } export const TRANSITIONS: readonly Transition[] = [ { from: 'DRAFT', to: 'OPEN', permission: 'case.edit' }, { from: 'OPEN', to: 'DORMANT', permission: 'case.edit' }, { from: 'DORMANT', to: 'OPEN', permission: 'case.edit' }, { from: 'OPEN', to: 'REVIEW', permission: 'case.edit' }, { from: 'REVIEW', to: 'OPEN', permission: 'case.edit' }, { from: 'REVIEW', to: 'CLOSED', permission: 'case.close', requiresCompleteSections: true }, { from: 'CLOSED', to: 'OPEN', permission: 'case.reopen', requiresReason: true }, { from: 'CLOSED', to: 'ARCHIVED', permission: 'case.close' }, ] /** SEALED erreicht man aus jedem Zustand, aber nur mit dem Recht 'admin'. */ export const SEAL_PERMISSION: Permission = 'admin' export function findTransition( from: CaseStatusName, to: CaseStatusName, ): Transition | undefined { if (to === 'SEALED' && from !== 'SEALED') { return { from, to, permission: SEAL_PERMISSION, requiresReason: true } } return TRANSITIONS.find((t) => t.from === from && t.to === to) } export function allowedTargets(from: CaseStatusName): CaseStatusName[] { const targets = TRANSITIONS.filter((t) => t.from === from).map((t) => t.to) if (from !== 'SEALED') targets.push('SEALED') return targets } /** * Der Hauptweg einer Akte, in der Reihenfolge, in der sie ihn geht. * * Steht hier und nicht in der Oberflaeche, damit die Darstellung dieselbe * Ordnung benutzt wie der Lebenszyklus - und nicht irgendeine. */ export const LIFECYCLE_MAIN: readonly CaseStatusName[] = [ 'DRAFT', 'OPEN', 'REVIEW', 'CLOSED', 'ARCHIVED', ] /** Was neben dem Hauptweg liegt: eine Pause und die Versiegelung. */ export const LIFECYCLE_SIDE: readonly CaseStatusName[] = ['DORMANT', 'SEALED'] /** Warum ein Zustand gerade nicht angesteuert werden kann. */ export type StatusBlock = | { kind: 'current' } /** Vom jetzigen Zustand fuehrt kein Weg dorthin. */ | { kind: 'unreachable' } /** Der Weg gaebe es, aber das Recht fehlt. */ | { kind: 'no-permission'; permission: Permission } /** Die Akte ist schreibgeschuetzt oder abgeschlossen. */ | { kind: 'locked'; reason: string } export type StatusOption = { status: CaseStatusName /** Anklickbar? Nur dann fuehrt der Server den Wechsel auch aus. */ available: boolean block: StatusBlock | null requiresReason: boolean requiresCompleteSections: boolean } /** * Zu **jedem** Zustand die Auskunft, ob er von hier aus erreichbar ist. * * Bewusst ueber alle sieben und nicht nur ueber die erreichbaren: eine Auswahl, * die je nach Zustand andere Eintraege hat, laesst sich nicht lesen - man sieht * nie, wo man steht und was es sonst noch gibt. Die Oberflaeche zeigt deshalb * immer dieselbe Karte und schaltet nur, was gerade geht. * * Die Rechtepruefung steht hier und nicht im Browser. Sie ist dieselbe, die der * Dienst beim Wechsel anwendet - ein Knopf, der zu einer Absage fuehrt, ist * schlimmer als keiner. */ export function statusOptions( p: EffectivePermissions, target: { status: string; classification: number }, ): StatusOption[] { const von = target.status as CaseStatusName const alle = [...LIFECYCLE_MAIN, ...LIFECYCLE_SIDE] return alle.map((status) => { if (status === von) { return { status, available: false, block: { kind: 'current' } as StatusBlock, requiresReason: false, requiresCompleteSections: false, } } const uebergang = findTransition(von, status) if (!uebergang) { return { status, available: false, block: { kind: 'unreachable' } as StatusBlock, requiresReason: false, requiresCompleteSections: false, } } const erlaubt = canEditCase(p, target, uebergang.permission) const block: StatusBlock | null = erlaubt.ok ? null : erlaubt.reason === 'no-permission' ? { kind: 'no-permission', permission: erlaubt.permission } : { kind: 'locked', reason: erlaubt.reason } return { status, available: erlaubt.ok, block, requiresReason: uebergang.requiresReason ?? false, requiresCompleteSections: uebergang.requiresCompleteSections ?? false, } }) }