stage-machine: allow IN_PROGRESS → APPROVED shortcut for non-review stages

NONE-approval stages bypass the review surface by design — the UI shows
"Mark Complete" which sets APPROVED directly. The state machine still
enforced the legacy "go through IN_REVIEW first" rule, so the click
returned `Cannot transition from IN_PROGRESS to APPROVED`.

Expands the allowed transitions to cover the producer-friendly
shortcuts they'd otherwise need to bounce through:

  IN_PROGRESS         → +APPROVED, +CHANGES_REQUESTED, +SKIPPED
  CHANGES_REQUESTED   → +APPROVED
  BLOCKED             → +IN_PROGRESS
  IN_REVIEW           → +IN_PROGRESS (cancel review path)
  DELIVERED           → +APPROVED (downgrade if mistakenly delivered)
  SKIPPED             → +IN_PROGRESS (unskip directly without NOT_STARTED)

FORMAL/SIMPLE stages still flow naturally through IN_REVIEW because
the StageReviewPanel buttons set those explicitly — the state-machine
loosening doesn't change their UX, just stops blocking the NONE path.

Auto-advance (819288d) already treats APPROVED / DELIVERED / SKIPPED
as terminal so this Mark-Complete click on Inputfile correctly
cascades and unblocks Internal Approval downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DJP 2026-05-15 20:26:25 -04:00
parent 5f409c6c46
commit 46cd8f8401

View file

@ -2,16 +2,26 @@ import type { StageStatus } from "@/generated/prisma/client";
/**
* Valid stage status transitions.
*
* NONE-approval stages take the IN_PROGRESS APPROVED shortcut ("Mark
* Complete"). FORMAL/SIMPLE-approval stages route through IN_REVIEW
* first (the StageReviewPanel's Submit-for-Review Approve flow).
* Both paths are allowed at the state-machine level; UI gating decides
* which one is exposed.
*
* CHANGES_REQUESTED APPROVED and IN_PROGRESS SKIPPED are quality-
* of-life shortcuts producers asked for so they don't have to bounce
* through an extra status to close out simple work.
*/
const TRANSITIONS: Record<StageStatus, StageStatus[]> = {
BLOCKED: ["NOT_STARTED", "SKIPPED"],
BLOCKED: ["NOT_STARTED", "IN_PROGRESS", "SKIPPED"],
NOT_STARTED: ["IN_PROGRESS", "SKIPPED"],
IN_PROGRESS: ["IN_REVIEW"],
IN_REVIEW: ["APPROVED", "CHANGES_REQUESTED"],
CHANGES_REQUESTED: ["IN_PROGRESS"],
IN_PROGRESS: ["IN_REVIEW", "APPROVED", "CHANGES_REQUESTED", "SKIPPED"],
IN_REVIEW: ["APPROVED", "CHANGES_REQUESTED", "IN_PROGRESS"],
CHANGES_REQUESTED: ["IN_PROGRESS", "APPROVED"],
APPROVED: ["DELIVERED", "IN_PROGRESS"], // reopen if client sends updated files
DELIVERED: ["IN_PROGRESS"], // reopen for rework
SKIPPED: ["NOT_STARTED"], // unskip if stage becomes needed
DELIVERED: ["IN_PROGRESS", "APPROVED"], // reopen for rework
SKIPPED: ["NOT_STARTED", "IN_PROGRESS"], // unskip if stage becomes needed
};
export function canTransition(