paperclip/ui/src/pages/Approvals.tsx
Forgotten ea60e4800f UI: task sessions in agent detail, ApprovalCard extraction, and company settings page
Show task sessions list in AgentDetail with per-session reset. Extract
ApprovalCard into standalone component from Approvals and Inbox pages,
reducing duplication. Add CompanySettings page with issuePrefix configuration.
Fix Sidebar active state for settings route. Display sessionDisplayId
in agent properties. Various cleanups to Approvals and Inbox pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:02:29 -06:00

128 lines
4.7 KiB
TypeScript

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery, useMutation, 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 { cn } from "../lib/utils";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ShieldCheck } from "lucide-react";
import { ApprovalCard } from "../components/ApprovalCard";
type StatusFilter = "pending" | "all";
export function Approvals() {
const { selectedCompanyId } = useCompany();
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [statusFilter, setStatusFilter] = useState<StatusFilter>("pending");
const [actionError, setActionError] = useState<string | null>(null);
useEffect(() => {
setBreadcrumbs([{ label: "Approvals" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
queryKey: queryKeys.approvals.list(selectedCompanyId!),
queryFn: () => approvalsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const { data: agents } = useQuery({
queryKey: queryKeys.agents.list(selectedCompanyId!),
queryFn: () => agentsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const approveMutation = useMutation({
mutationFn: (id: string) => approvalsApi.approve(id),
onSuccess: (_approval, id) => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
navigate(`/approvals/${id}?resolved=approved`);
},
onError: (err) => {
setActionError(err instanceof Error ? err.message : "Failed to approve");
},
});
const rejectMutation = useMutation({
mutationFn: (id: string) => approvalsApi.reject(id),
onSuccess: () => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
},
onError: (err) => {
setActionError(err instanceof Error ? err.message : "Failed to reject");
},
});
const filtered = (data ?? [])
.filter(
(a) => statusFilter === "all" || a.status === "pending" || a.status === "revision_requested",
)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const pendingCount = (data ?? []).filter(
(a) => a.status === "pending" || a.status === "revision_requested",
).length;
if (!selectedCompanyId) {
return <p className="text-sm text-muted-foreground">Select a company first.</p>;
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Tabs value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
<TabsList variant="line">
<TabsTrigger value="pending">
Pending
{pendingCount > 0 && (
<span className={cn(
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
"bg-yellow-500/20 text-yellow-500"
)}>
{pendingCount}
</span>
)}
</TabsTrigger>
<TabsTrigger value="all">All</TabsTrigger>
</TabsList>
</Tabs>
</div>
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
{error && <p className="text-sm text-destructive">{error.message}</p>}
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
{!isLoading && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-center">
<ShieldCheck className="h-8 w-8 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground">
{statusFilter === "pending" ? "No pending approvals." : "No approvals yet."}
</p>
</div>
)}
{filtered.length > 0 && (
<div className="grid gap-3">
{filtered.map((approval) => (
<ApprovalCard
key={approval.id}
approval={approval}
requesterAgent={approval.requestedByAgentId ? (agents ?? []).find((a) => a.id === approval.requestedByAgentId) ?? null : null}
onApprove={() => approveMutation.mutate(approval.id)}
onReject={() => rejectMutation.mutate(approval.id)}
onOpen={() => navigate(`/approvals/${approval.id}`)}
isPending={approveMutation.isPending || rejectMutation.isPending}
/>
))}
</div>
)}
</div>
);
}