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 },
});
}
}