fix(ceo-approve): require substantive notes; panel collects them

ceo-approve was bound to QANotes (notes required), so the panel's
one-click approve (posts {}) 422'd. The wrong fix is to waive notes —
that empties the audit record for a production merge. Instead require
substantive notes (>=20 chars, mirroring pass-qa) and make the panel
COLLECT them: a new CeoApproveDialog (mirrors the reject dialog) gates
the 'Approve & Merge' action, and the dashboard approval-queue enforces
the same before POSTing. The CEO sign-off note is now always captured.
This commit is contained in:
Renn F
2026-05-24 06:59:37 +02:00
parent d49d1cdb37
commit c093996efc
5 changed files with 133 additions and 8 deletions
@@ -7,6 +7,7 @@ import { useCreateBranch, useCreatePR } from "@/hooks/use-git";
import { TaskHeader, TaskMetadata, TaskTabs } from "@/components/tasks/task-detail";
import {
EscalateToCeoDialog,
CeoApproveDialog,
CeoRejectDialog,
CreateBranchDialog,
CreatePRDialog,
@@ -32,6 +33,7 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
// Dialog states
const [escalateDialogOpen, setEscalateDialogOpen] = useState(false);
const [approveDialogOpen, setApproveDialogOpen] = useState(false);
const [rejectDialogOpen, setRejectDialogOpen] = useState(false);
const [branchDialogOpen, setBranchDialogOpen] = useState(false);
const [prDialogOpen, setPrDialogOpen] = useState(false);
@@ -103,9 +105,8 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
toast.success("Submitted for PM review");
break;
case "ceo-approve":
await lifecycle.ceoApprove.mutateAsync({ taskId: task.id });
toast.success("Task approved and completed");
break;
setApproveDialogOpen(true);
return; // Don't refetch yet — dialog collects the required note
case "ceo-reject":
setRejectDialogOpen(true);
return; // Don't refetch yet, dialog will handle it
@@ -167,6 +168,19 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
}
};
const handleCeoApprove = async (notes: string) => {
if (!task) return;
try {
await lifecycle.ceoApprove.mutateAsync({ taskId: task.id, notes });
toast.success("Task approved and completed");
setApproveDialogOpen(false);
refetch();
} catch (err) {
toast.error("Failed to approve task");
console.error(err);
}
};
const handleCreateBranch = async (branchType: string) => {
if (!task || !project) return;
try {
@@ -280,6 +294,13 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
isPending={lifecycle.escalateToCeo.isPending}
/>
<CeoApproveDialog
open={approveDialogOpen}
onOpenChange={setApproveDialogOpen}
onConfirm={handleCeoApprove}
isPending={lifecycle.ceoApprove.isPending}
/>
<CeoRejectDialog
open={rejectDialogOpen}
onOpenChange={setRejectDialogOpen}