diff --git a/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx b/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx index 612c21c..bf4896a 100644 --- a/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx +++ b/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx @@ -439,45 +439,76 @@ export default function DeliverableDetailPage() { /> - {available.length > 0 && ( -
- {available.map((nextStatus) => { - const isSkip = nextStatus === "SKIPPED"; - const isReopen = - (nextStatus === "IN_PROGRESS" && - (stage.status === "APPROVED" || - stage.status === "DELIVERED")) || - (nextStatus === "NOT_STARTED" && - stage.status === "SKIPPED"); - const label = isReopen - ? "Reopen" - : (TRANSITION_LABELS[nextStatus] ?? nextStatus); + {(() => { + // Visible action set rules (Phase 5b — declutter): + // - BLOCKED rows: no buttons (path forward is the + // upstream prereq; we don't want producers to click + // "Skip" by accident). The lock icon below still + // explains why nothing is actionable. + // - Stages with approvalType !== NONE: the + // StageReviewPanel owns the Approve / Request changes + // / Send to client / New revision affordances. No + // duplicates in this row. + // - All other transitions are visible — but + // destructive / sideways moves (Skip, Reopen) stay + // muted outline so the forward path is the obvious + // primary action. + const stageApprovalType = + stage.stageDefinition?.approvalType ?? + stage.template?.approvalType ?? + "NONE"; + const hasReviewPanel = stageApprovalType !== "NONE"; + const isBlocked = stage.status === "BLOCKED"; - return ( - - ); - })} -
- )} + if (isBlocked || hasReviewPanel || available.length === 0) { + return null; + } + + return ( +
+ {available.map((nextStatus) => { + const isSkip = nextStatus === "SKIPPED"; + const isReopen = + (nextStatus === "IN_PROGRESS" && + (stage.status === "APPROVED" || + stage.status === "DELIVERED")) || + (nextStatus === "NOT_STARTED" && + stage.status === "SKIPPED"); + const label = isReopen + ? "Reopen" + : (TRANSITION_LABELS[nextStatus] ?? nextStatus); + + return ( + + ); + })} +
+ ); + })()} {/* Sub-status + blocked hint on their own third line only when present */} diff --git a/src/components/stages/stage-review-panel.tsx b/src/components/stages/stage-review-panel.tsx index 7e4fd52..d36ae82 100644 --- a/src/components/stages/stage-review-panel.tsx +++ b/src/components/stages/stage-review-panel.tsx @@ -193,9 +193,10 @@ export function StageReviewPanel({ className="h-6 px-2 text-[12px]" disabled={updateStageStatus.isPending} onClick={handleApprove} + title="Approve this stage and auto-open the next stage(s)" > - Approve + Approve & advance )} @@ -206,6 +207,7 @@ export function StageReviewPanel({ className="h-6 px-2 text-[12px]" disabled={updateStageStatus.isPending} onClick={handleRequestChanges} + title="Mark this stage as needing changes; producer will iterate" > Request changes diff --git a/src/lib/services/stage-service.ts b/src/lib/services/stage-service.ts index 9c83af1..9bedc84 100644 --- a/src/lib/services/stage-service.ts +++ b/src/lib/services/stage-service.ts @@ -403,11 +403,12 @@ export async function updateStageStatus( downstreamStageNames: [] as string[], }; - // Check if this will unblock downstream stages - if ( - (newStatus === "APPROVED" || newStatus === "DELIVERED") && - stage.template.isCriticalGate - ) { + // Check if this will auto-advance downstream stages. Phase 5b: ALL + // terminal transitions (APPROVED / DELIVERED / SKIPPED) auto-open + // every stage whose prerequisites are now satisfied — not just + // critical gates. Producers stop hitting "Start Work" on every row. + const TERMINAL: StageStatus[] = ["APPROVED", "DELIVERED", "SKIPPED"]; + if (TERMINAL.includes(newStatus)) { const allStages = stage.deliverable.stages.map((s) => ({ id: s.id, status: s.id === stageId ? newStatus : s.status, @@ -462,8 +463,11 @@ export async function updateStageStatus( include: { template: { include: { dependsOn: true } } }, }); - // If this stage was just APPROVED/DELIVERED and is a critical gate, unblock downstream - if ((newStatus === "APPROVED" || newStatus === "DELIVERED") && stage.template.isCriticalGate) { + // Phase 5b: any terminal transition auto-advances every downstream + // stage whose prerequisites are now satisfied. Set them IN_PROGRESS + // directly (not NOT_STARTED) — producers shouldn't have to click + // "Start Work" on every unblocked row. + if (TERMINAL.includes(newStatus)) { const allStages = stage.deliverable.stages.map((s) => ({ id: s.id, status: s.id === stageId ? newStatus : s.status, @@ -482,7 +486,7 @@ export async function updateStageStatus( if (idsToUnblock.length > 0) { await tx.deliverableStage.updateMany({ where: { id: { in: idsToUnblock } }, - data: { status: "NOT_STARTED" }, + data: { status: "IN_PROGRESS", startDate: now }, }); } }