"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { tasksApi } from "@/lib/api"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { CheckCircle2, XCircle, Clock, FileText, ExternalLink, Rocket, } from "lucide-react"; import Link from "next/link"; import { TaskStatus, Team, type Task } from "@/types"; import { toast } from "sonner"; interface CeoApprovalQueueProps { className?: string; } export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) { const queryClient = useQueryClient(); const [selectedTask, setSelectedTask] = useState(null); const [actionType, setActionType] = useState< "approve" | "reject" | "start" | null >(null); const [notes, setNotes] = useState(""); // Fetch tasks awaiting CEO approval (the end-of-work, pre-merge gate) const { data: tasks, isLoading } = useQuery({ queryKey: ["tasks", "awaiting-ceo-approval"], queryFn: () => tasksApi.getAwaitingCeoApproval(), refetchInterval: 30000, // Refresh every 30 seconds }); // Fetch tasks waiting on the CEO's Approve & Start (board review done, still // PENDING). The orchestrator sets board_review_complete + notifies the CEO // but leaves the task pending, so it never appears in the awaiting-ceo list. // Surface it here, or the CEO has no idea a task is waiting on them. // // approve_and_start does NOT change status — it re-targets the task to the // Main PM (team → main_pm). So exclude team === MAIN_PM, otherwise an // already-approved task stays in this list forever. const { data: startTasks } = useQuery({ queryKey: ["tasks", "awaiting-approve-start"], queryFn: async () => { // Full-fat fetch: this card renders quick_context, which the trimmed // summary list deliberately omits. The PENDING set is small. const pending = await tasksApi.listFull({ status: TaskStatus.PENDING }); return pending.filter( (t) => t.board_review_complete === true && t.team !== Team.MAIN_PM, ); }, refetchInterval: 30000, }); // Approve mutation const approveMutation = useMutation({ mutationFn: ({ taskId, notes }: { taskId: string; notes?: string }) => tasksApi.ceoApprove(taskId, notes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tasks"] }); toast.success("Task approved and completed"); closeDialog(); }, onError: (error) => { toast.error( `Failed to approve: ${error instanceof Error ? error.message : "Unknown error"}`, ); }, }); // Reject mutation const rejectMutation = useMutation({ mutationFn: ({ taskId, notes }: { taskId: string; notes: string }) => tasksApi.ceoReject(taskId, notes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tasks"] }); toast.success("Task rejected and sent back for revision"); closeDialog(); }, onError: (error) => { toast.error( `Failed to reject: ${error instanceof Error ? error.message : "Unknown error"}`, ); }, }); // Approve & Start mutation — hands a board-reviewed task to the Main PM. const approveStartMutation = useMutation({ mutationFn: ({ taskId, notes }: { taskId: string; notes: string }) => tasksApi.approveAndStart(taskId, notes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tasks"] }); toast.success("Task approved and handed to Main PM"); closeDialog(); }, onError: (error) => { toast.error( `Failed to approve & start: ${error instanceof Error ? error.message : "Unknown error"}`, ); }, }); const openDialog = (task: Task, action: "approve" | "reject" | "start") => { setSelectedTask(task); setActionType(action); setNotes(""); }; const closeDialog = () => { setSelectedTask(null); setActionType(null); setNotes(""); }; const handleConfirm = () => { if (!selectedTask) return; if (actionType === "approve") { // The approval note is the audit record for merging to production — // required and substantive (>= 20 chars), matching the server gate. if (notes.trim().length < 20) { toast.error("Approval notes are required (>= 20 characters)"); return; } approveMutation.mutate({ taskId: selectedTask.id, notes: notes.trim() }); } else if (actionType === "start") { // Server requires substantive approval notes (>= 20 chars). if (notes.trim().length < 20) { toast.error("Approval notes are required (>= 20 characters)"); return; } approveStartMutation.mutate({ taskId: selectedTask.id, notes: notes.trim(), }); } else if (actionType === "reject") { if (!notes.trim()) { toast.error("Rejection reason is required"); return; } rejectMutation.mutate({ taskId: selectedTask.id, notes }); } }; const getPriorityBadge = (priority: number) => { const variants: Record< number, { label: string; variant: "default" | "secondary" | "destructive" | "outline"; } > = { 0: { label: "P0", variant: "destructive" }, 1: { label: "P1", variant: "destructive" }, 2: { label: "P2", variant: "secondary" }, 3: { label: "P3", variant: "outline" }, }; const { label, variant } = variants[priority] || { label: `P${priority}`, variant: "outline" as const, }; return {label}; }; if (isLoading) { return ( CEO Approval Queue Tasks awaiting your approval
{[1, 2, 3].map((i) => ( ))}
); } const pendingTasks = tasks || []; const readyToStart = startTasks || []; const totalCount = pendingTasks.length + readyToStart.length; const renderRow = (task: Task, kind: "start" | "approve") => (
{getPriorityBadge(task.priority)} {task.team}
{task.title} {task.quick_context && (

{task.quick_context}

)}
{/* Action cluster — stacks below the task text on narrow instead of clipping against it (mirrors the dialog footer's flex-col sm:flex-row). */}
{kind === "start" ? ( ) : ( )}
); return ( <> CEO Approval Queue {totalCount > 0 && ( {totalCount} )} Tasks waiting on your decision {totalCount === 0 ? (

No tasks awaiting approval

) : (
{readyToStart.length > 0 && (

Ready to start · board reviewed

{readyToStart.map((task) => renderRow(task, "start"))}
)} {pendingTasks.length > 0 && (

Final approval · work complete

{pendingTasks.map((task) => renderRow(task, "approve"))}
)}
)}
{/* Confirmation Dialog */} closeDialog()} > {actionType === "approve" ? "Approve Task" : actionType === "start" ? "Approve & Start Task" : "Reject Task"} {actionType === "approve" ? "This will complete the task and notify the team." : actionType === "start" ? "This hands the task to the Main PM to delegate to the cells and begin work." : "This will send the task back for revision."} {selectedTask && (
{getPriorityBadge(selectedTask.priority)} {selectedTask.team}

{selectedTask.title}

{selectedTask.description && (

{selectedTask.description}

)} View full details
)}