import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { approvalsApi } from "../api/approvals"; import { agentsApi } from "../api/agents"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { StatusBadge } from "../components/StatusBadge"; import { Identity } from "../components/Identity"; import { typeLabel, typeIcon, defaultTypeIcon, ApprovalPayloadRenderer } from "../components/ApprovalPayload"; import { PageSkeleton } from "../components/PageSkeleton"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { CheckCircle2, ChevronRight, Sparkles } from "lucide-react"; import type { ApprovalComment } from "@paperclipai/shared"; import { MarkdownBody } from "../components/MarkdownBody"; export function ApprovalDetail() { const { approvalId } = useParams<{ approvalId: string }>(); const { selectedCompanyId, setSelectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const [commentBody, setCommentBody] = useState(""); const [error, setError] = useState(null); const [showRawPayload, setShowRawPayload] = useState(false); const { data: approval, isLoading } = useQuery({ queryKey: queryKeys.approvals.detail(approvalId!), queryFn: () => approvalsApi.get(approvalId!), enabled: !!approvalId, }); const resolvedCompanyId = approval?.companyId ?? selectedCompanyId; const { data: comments } = useQuery({ queryKey: queryKeys.approvals.comments(approvalId!), queryFn: () => approvalsApi.listComments(approvalId!), enabled: !!approvalId, }); const { data: linkedIssues } = useQuery({ queryKey: queryKeys.approvals.issues(approvalId!), queryFn: () => approvalsApi.listIssues(approvalId!), enabled: !!approvalId, }); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(resolvedCompanyId ?? ""), queryFn: () => agentsApi.list(resolvedCompanyId ?? ""), enabled: !!resolvedCompanyId, }); useEffect(() => { if (!approval?.companyId || approval.companyId === selectedCompanyId) return; setSelectedCompanyId(approval.companyId, { source: "route_sync" }); }, [approval?.companyId, selectedCompanyId, setSelectedCompanyId]); const agentNameById = useMemo(() => { const map = new Map(); for (const agent of agents ?? []) map.set(agent.id, agent.name); return map; }, [agents]); useEffect(() => { setBreadcrumbs([ { label: "Approvals", href: "/approvals" }, { label: approval?.id?.slice(0, 8) ?? approvalId ?? "Approval" }, ]); }, [setBreadcrumbs, approval, approvalId]); const refresh = () => { if (!approvalId) return; queryClient.invalidateQueries({ queryKey: queryKeys.approvals.detail(approvalId) }); queryClient.invalidateQueries({ queryKey: queryKeys.approvals.comments(approvalId) }); queryClient.invalidateQueries({ queryKey: queryKeys.approvals.issues(approvalId) }); if (approval?.companyId) { queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(approval.companyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(approval.companyId, "pending"), }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(approval.companyId) }); } }; const approveMutation = useMutation({ mutationFn: () => approvalsApi.approve(approvalId!), onSuccess: () => { setError(null); refresh(); navigate(`/approvals/${approvalId}?resolved=approved`, { replace: true }); }, onError: (err) => setError(err instanceof Error ? err.message : "Approve failed"), }); const rejectMutation = useMutation({ mutationFn: () => approvalsApi.reject(approvalId!), onSuccess: () => { setError(null); refresh(); }, onError: (err) => setError(err instanceof Error ? err.message : "Reject failed"), }); const revisionMutation = useMutation({ mutationFn: () => approvalsApi.requestRevision(approvalId!), onSuccess: () => { setError(null); refresh(); }, onError: (err) => setError(err instanceof Error ? err.message : "Revision request failed"), }); const resubmitMutation = useMutation({ mutationFn: () => approvalsApi.resubmit(approvalId!), onSuccess: () => { setError(null); refresh(); }, onError: (err) => setError(err instanceof Error ? err.message : "Resubmit failed"), }); const addCommentMutation = useMutation({ mutationFn: () => approvalsApi.addComment(approvalId!, commentBody.trim()), onSuccess: () => { setCommentBody(""); setError(null); refresh(); }, onError: (err) => setError(err instanceof Error ? err.message : "Comment failed"), }); const deleteAgentMutation = useMutation({ mutationFn: (agentId: string) => agentsApi.remove(agentId), onSuccess: () => { setError(null); refresh(); navigate("/approvals"); }, onError: (err) => setError(err instanceof Error ? err.message : "Delete failed"), }); if (isLoading) return ; if (!approval) return

Approval not found.

; const payload = approval.payload as Record; const linkedAgentId = typeof payload.agentId === "string" ? payload.agentId : null; const isActionable = approval.status === "pending" || approval.status === "revision_requested"; const isBudgetApproval = approval.type === "budget_override_required"; const TypeIcon = typeIcon[approval.type] ?? defaultTypeIcon; const showApprovedBanner = searchParams.get("resolved") === "approved" && approval.status === "approved"; const primaryLinkedIssue = linkedIssues?.[0] ?? null; const resolvedCta = primaryLinkedIssue ? { label: (linkedIssues?.length ?? 0) > 1 ? "Review linked issues" : "Review linked issue", to: `/issues/${primaryLinkedIssue.identifier ?? primaryLinkedIssue.id}`, } : linkedAgentId ? { label: "Open hired agent", to: `/agents/${linkedAgentId}`, } : { label: "Back to approvals", to: "/approvals", }; return (
{showApprovedBanner && (

Approval confirmed

Requesting agent was notified to review this approval and linked issues.

)}

{typeLabel[approval.type] ?? approval.type.replace(/_/g, " ")}

{approval.id}

{approval.requestedByAgentId && (
Requested by
)} {showRawPayload && (
              {JSON.stringify(payload, null, 2)}
            
)} {approval.decisionNote && (

Decision note: {approval.decisionNote}

)}
{error &&

{error}

} {linkedIssues && linkedIssues.length > 0 && (

Linked Issues

{linkedIssues.map((issue) => ( {issue.identifier ?? issue.id.slice(0, 8)} {issue.title} ))}

Linked issues remain open until the requesting agent follows up and closes them.

)}
{isActionable && !isBudgetApproval && ( <> )} {isBudgetApproval && approval.status === "pending" && (

Resolve this budget stop from the budget controls on /costs.

)} {approval.status === "pending" && ( )} {approval.status === "revision_requested" && ( )} {approval.status === "rejected" && approval.type === "hire_agent" && linkedAgentId && ( )}

Comments ({comments?.length ?? 0})

{(comments ?? []).map((comment: ApprovalComment) => (
{comment.authorAgentId ? ( ) : ( )} {new Date(comment.createdAt).toLocaleString()}
{comment.body}
))}