"use client"; import { useCallback, useMemo, useState } from "react"; import { useAuth } from "@/auth/clerk"; import { useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, Clock } from "lucide-react"; import { Cell, Pie, PieChart } from "recharts"; import { ApiError } from "@/api/mutator"; import { type listApprovalsApiV1BoardsBoardIdApprovalsGetResponse, getListApprovalsApiV1BoardsBoardIdApprovalsGetQueryKey, useListApprovalsApiV1BoardsBoardIdApprovalsGet, useUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch, } from "@/api/generated/approvals/approvals"; import type { ApprovalRead } from "@/api/generated/model"; import { StatusDot } from "@/components/atoms/StatusDot"; import { ChartContainer, ChartTooltip, ChartTooltipCard, type ChartConfig, } from "@/components/charts/chart"; import { Button } from "@/components/ui/button"; import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime"; import { cn } from "@/lib/utils"; type Approval = ApprovalRead & { status: string }; const normalizeApproval = (approval: ApprovalRead): Approval => ({ ...approval, status: approval.status ?? "pending", }); type BoardApprovalsPanelProps = { boardId: string; approvals?: ApprovalRead[]; isLoading?: boolean; error?: string | null; onDecision?: (approvalId: string, status: "approved" | "rejected") => void; scrollable?: boolean; boardLabelById?: Record; }; const formatTimestamp = (value?: string | null) => { if (!value) return "—"; const date = parseApiDatetime(value); if (!date) return value; return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; const statusBadgeClass = (status: string) => { if (status === "approved") { return "bg-emerald-50 text-emerald-700"; } if (status === "rejected") { return "bg-rose-50 text-rose-700"; } return "bg-amber-100 text-amber-700"; }; const confidenceBadgeClass = (confidence: number) => { if (confidence >= 90) { return "bg-emerald-50 text-emerald-700"; } if (confidence >= 80) { return "bg-amber-100 text-amber-700"; } return "bg-orange-100 text-orange-700"; }; const humanizeAction = (value: string) => value .split(".") .map((part) => part.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()), ) .join(" · "); const formatStatusLabel = (status: string) => status.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()); const rubricColors = [ "#0f172a", "#1d4ed8", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", ]; type TooltipValue = number | string | Array; const formatRubricTooltipValue = ( value?: TooltipValue, name?: TooltipValue, item?: { color?: string | null; payload?: { name?: string; fill?: string; percent?: number; percentLabel?: string; }; } | null, ) => { const payload = item?.payload; const label = payload?.name ?? (typeof name === "string" || typeof name === "number" ? String(name) : ""); const percentLabel = payload?.percentLabel ?? (typeof payload?.percent === "number" && Number.isFinite(payload.percent) ? `${payload.percent.toFixed(1)}%` : null); const fallback = value === null || value === undefined ? "" : String(value ?? ""); const displayValue = percentLabel ?? fallback; const indicatorColor = payload?.fill ?? item?.color ?? "#94a3b8"; return (
{label} {displayValue}
); }; const payloadValue = (payload: Approval["payload"], key: string) => { if (!payload) return null; const value = payload[key as keyof typeof payload]; if (typeof value === "string" || typeof value === "number") { return String(value); } return null; }; const approvalSummary = (approval: Approval, boardLabel?: string | null) => { const payload = approval.payload ?? {}; const taskId = approval.task_id ?? payloadValue(payload, "task_id") ?? payloadValue(payload, "taskId") ?? payloadValue(payload, "taskID"); const assignedAgentId = payloadValue(payload, "assigned_agent_id") ?? payloadValue(payload, "assignedAgentId"); const reason = payloadValue(payload, "reason"); const title = payloadValue(payload, "title"); const description = payloadValue(payload, "description"); const role = payloadValue(payload, "role"); const isAssign = approval.action_type.includes("assign"); const rows: Array<{ label: string; value: string }> = []; if (boardLabel) rows.push({ label: "Board", value: boardLabel }); if (taskId) rows.push({ label: "Task", value: taskId }); if (isAssign) { rows.push({ label: "Assignee", value: assignedAgentId ?? "Unassigned", }); } if (title) rows.push({ label: "Title", value: title }); if (role) rows.push({ label: "Role", value: role }); return { taskId, reason, rows, description }; }; export function BoardApprovalsPanel({ boardId, approvals: externalApprovals, isLoading: externalLoading, error: externalError, onDecision, scrollable = false, boardLabelById, }: BoardApprovalsPanelProps) { const { isSignedIn } = useAuth(); const queryClient = useQueryClient(); const [error, setError] = useState(null); const [updatingId, setUpdatingId] = useState(null); const [selectedId, setSelectedId] = useState(null); const usingExternal = Array.isArray(externalApprovals); const approvalsKey = useMemo( () => getListApprovalsApiV1BoardsBoardIdApprovalsGetQueryKey(boardId), [boardId], ); const approvalsQuery = useListApprovalsApiV1BoardsBoardIdApprovalsGet< listApprovalsApiV1BoardsBoardIdApprovalsGetResponse, ApiError >(boardId, undefined, { query: { enabled: Boolean(!usingExternal && isSignedIn && boardId), refetchInterval: 15_000, refetchOnMount: "always", retry: false, }, }); const updateApprovalMutation = useUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch(); const approvals = useMemo(() => { const raw = usingExternal ? (externalApprovals ?? []) : approvalsQuery.data?.status === 200 ? (approvalsQuery.data.data.items ?? []) : []; return raw.map(normalizeApproval); }, [approvalsQuery.data, externalApprovals, usingExternal]); const loadingState = usingExternal ? (externalLoading ?? false) : approvalsQuery.isLoading; const errorState = usingExternal ? (externalError ?? null) : (error ?? approvalsQuery.error?.message ?? null); const handleDecision = useCallback( (approvalId: string, status: "approved" | "rejected") => { const pendingNext = [...approvals] .filter((item) => item.id !== approvalId) .filter((item) => item.status === "pending") .sort( (a, b) => (apiDatetimeToMs(b.created_at) ?? 0) - (apiDatetimeToMs(a.created_at) ?? 0), )[0]?.id; if (pendingNext) { setSelectedId(pendingNext); } if (onDecision) { onDecision(approvalId, status); return; } if (usingExternal) return; if (!isSignedIn || !boardId) return; setUpdatingId(approvalId); setError(null); updateApprovalMutation.mutate( { boardId, approvalId, data: { status } }, { onSuccess: (result) => { if (result.status !== 200) return; queryClient.setQueryData( approvalsKey, (previous) => { if (!previous || previous.status !== 200) return previous; return { ...previous, data: { ...previous.data, items: previous.data.items.map((item) => item.id === approvalId ? result.data : item, ), }, }; }, ); }, onError: (err) => { setError(err.message || "Unable to update approval."); }, onSettled: () => { setUpdatingId(null); queryClient.invalidateQueries({ queryKey: approvalsKey }); }, }, ); }, [ approvals, approvalsKey, boardId, isSignedIn, onDecision, queryClient, updateApprovalMutation, usingExternal, ], ); const sortedApprovals = useMemo(() => { const sortByTime = (items: Approval[]) => [...items].sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return bTime - aTime; }); const pending = sortByTime( approvals.filter((item) => item.status === "pending"), ); const resolved = sortByTime( approvals.filter((item) => item.status !== "pending"), ); return { pending, resolved }; }, [approvals]); const orderedApprovals = useMemo( () => [...sortedApprovals.pending, ...sortedApprovals.resolved], [sortedApprovals.pending, sortedApprovals.resolved], ); const effectiveSelectedId = useMemo(() => { if (orderedApprovals.length === 0) return null; if (selectedId && orderedApprovals.some((item) => item.id === selectedId)) { return selectedId; } return orderedApprovals[0].id; }, [orderedApprovals, selectedId]); const selectedApproval = useMemo(() => { if (!effectiveSelectedId) return null; return ( orderedApprovals.find((item) => item.id === effectiveSelectedId) ?? null ); }, [effectiveSelectedId, orderedApprovals]); const pendingCount = sortedApprovals.pending.length; const resolvedCount = sortedApprovals.resolved.length; return (
{errorState ? (
{errorState}
) : null} {loadingState ? (

Loading approvals…

) : pendingCount === 0 && resolvedCount === 0 ? (

All clear

No approvals to review right now. New approvals will show up here as soon as they arrive.

) : (

Unapproved tasks

{pendingCount} pending · {resolvedCount} resolved

{orderedApprovals.map((approval) => { const summary = approvalSummary( approval, boardLabelById?.[approval.board_id] ?? null, ); const isSelected = effectiveSelectedId === approval.id; const isPending = approval.status === "pending"; const titleRow = summary.rows.find( (row) => row.label.toLowerCase() === "title", ); const fallbackRow = summary.rows.find( (row) => row.label.toLowerCase() !== "title" && row.label.toLowerCase() !== "board", ); const primaryLabel = titleRow?.value ?? fallbackRow?.value ?? "Untitled"; const boardRow = summary.rows.find( (row) => row.label.toLowerCase() === "board", ); const boardText = boardRow && boardRow.value !== primaryLabel ? boardRow.value : null; return ( ); })}

{selectedApproval?.status === "pending" ? "Latest unapproved task" : "Approval detail"}

{!selectedApproval ? (
Select an approval to review details.
) : ( (() => { const summary = approvalSummary( selectedApproval, boardLabelById?.[selectedApproval.board_id] ?? null, ); const titleRow = summary.rows.find( (row) => row.label.toLowerCase() === "title", ); const titleText = titleRow?.value?.trim() ?? ""; const descriptionText = summary.description?.trim() ?? ""; const reasoningText = summary.reason?.trim() ?? ""; const extraRows = summary.rows.filter((row) => { const normalized = row.label.toLowerCase(); if (normalized === "title") return false; if (normalized === "task") return false; if (normalized === "assignee") return false; return true; }); const rubricEntries = Object.entries( selectedApproval.rubric_scores ?? {}, ).map(([key, value]) => ({ label: key .replace(/_/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()), value, })); const rubricTotal = rubricEntries.reduce( (total, entry) => total + entry.value, 0, ); const hasRubric = rubricEntries.length > 0 && rubricTotal > 0; const rubricChartData = rubricEntries.map((entry, index) => { const percent = rubricTotal > 0 ? (entry.value / rubricTotal) * 100 : 0; return { key: entry.label.toLowerCase().replace(/[^a-z0-9]+/g, "_"), name: entry.label, value: entry.value, percent, percentLabel: `${percent.toFixed(1)}%`, fill: rubricColors[index % rubricColors.length], }; }); const rubricChartConfig = rubricChartData.reduce( (accumulator, entry) => { accumulator[entry.key] = { label: entry.name, color: entry.fill, }; return accumulator; }, {}, ); return (

{humanizeAction(selectedApproval.action_type)}

Requested{" "} {formatTimestamp(selectedApproval.created_at)}

{selectedApproval.confidence}% confidence {selectedApproval.status === "pending" ? (
) : null}

Status

{formatStatusLabel(selectedApproval.status)} ·{" "} {selectedApproval.status === "pending" ? "Awaiting your decision" : "Decision complete"}

{titleText ? (

Title

{titleText}
) : null} {descriptionText ? (

Description

{descriptionText}
) : null} {reasoningText ? (

Lead reasoning

{reasoningText}

) : null} {extraRows.length > 0 ? (

Details

{extraRows.map((row) => (

{row.label}

{row.value}

))}
) : null} {hasRubric ? (

Rubric scores

{rubricChartData.map((entry) => (
{entry.name}
{entry.percentLabel}
))}
{rubricTotal > 0 ? ( } /> ) : null} {rubricChartData.map((entry) => ( ))}
) : null}
); })() )}
)}
); } export default BoardApprovalsPanel;