// Incident status workflow, shared so the web app can show the same options and reasons the API enforces. import type { ActionStatus } from './api-types.ts' import { hasRole, type IncidentStatus, type Role, type Severity } from './enums.ts' export interface Transition { from: IncidentStatus to: IncidentStatus minRole: Role label: string } export const transitions: Transition[] = [ { from: 'reported', to: 'triage', minRole: 'supervisor', label: 'Start triage' }, { from: 'triage', to: 'investigation', minRole: 'supervisor', label: 'Start investigation' }, { from: 'triage', to: 'actions', minRole: 'supervisor', label: 'Skip to corrective actions' }, { from: 'triage', to: 'closed', minRole: 'supervisor', label: 'Close: no further action' }, { from: 'investigation', to: 'actions', minRole: 'supervisor', label: 'Move to corrective actions' }, { from: 'actions', to: 'investigation', minRole: 'supervisor', label: 'Back to investigation' }, { from: 'actions', to: 'verification', minRole: 'supervisor', label: 'Send for verification' }, { from: 'verification', to: 'actions', minRole: 'safety_manager', label: 'Return to actions' }, { from: 'verification', to: 'closed', minRole: 'safety_manager', label: 'Verify and close' }, { from: 'closed', to: 'investigation', minRole: 'safety_manager', label: 'Reopen' }, ] export interface WorkflowFacts { severity: Severity rootCauseCount: number actionStatuses: ActionStatus[] } /** Why a transition isn't possible yet, or undefined if it is. */ export function transitionBlocker(transition: Transition, facts: WorkflowFacts): string | undefined { const live = facts.actionStatuses.filter((status) => status !== 'cancelled') if (transition.from === 'triage' && transition.to === 'closed' && (facts.severity === 'high' || facts.severity === 'critical')) { return 'High and critical incidents must be investigated before closing' } if (transition.to === 'actions' && transition.from === 'investigation' && facts.rootCauseCount === 0) { return 'Record at least one root cause first' } if (transition.to === 'verification') { if (live.length === 0) return 'Add at least one corrective action first' if (live.some((status) => status === 'open' || status === 'in_progress')) return 'All corrective actions must be done first' } if (transition.from === 'verification' && transition.to === 'closed' && live.some((status) => status !== 'verified')) { return 'Verify every corrective action first' } return undefined } /** Transitions from `status` that `role` may perform, with blockers explained. */ export function availableTransitions(status: IncidentStatus, role: Role, facts: WorkflowFacts) { return transitions .filter((t) => t.from === status && hasRole(role, t.minRole)) .map((t) => ({ to: t.to, label: t.label, blockedReason: transitionBlocker(t, facts) })) }