import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { dashboardApi } from "../api/dashboard"; import { activityApi } from "../api/activity"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { MetricCard } from "../components/MetricCard"; import { EmptyState } from "../components/EmptyState"; import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { timeAgo } from "../lib/timeAgo"; import { formatCents } from "../lib/utils"; import { Bot, CircleDot, DollarSign, ShieldCheck, LayoutDashboard, Clock } from "lucide-react"; import type { Issue } from "@paperclip/shared"; const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; function formatAction(action: string): string { const actionMap: Record = { "company.created": "Company created", "agent.created": "Agent created", "agent.updated": "Agent updated", "agent.key_created": "API key created", "issue.created": "Issue created", "issue.updated": "Issue updated", "issue.checked_out": "Issue checked out", "issue.released": "Issue released", "issue.commented": "Comment added", "heartbeat.invoked": "Heartbeat invoked", "heartbeat.completed": "Heartbeat completed", "approval.created": "Approval requested", "approval.approved": "Approval granted", "approval.rejected": "Approval rejected", "project.created": "Project created", "goal.created": "Goal created", "cost.recorded": "Cost recorded", }; return actionMap[action] ?? action.replace(/[._]/g, " "); } function getStaleIssues(issues: Issue[]): Issue[] { const now = Date.now(); return issues .filter( (i) => ["in_progress", "todo"].includes(i.status) && now - new Date(i.updatedAt).getTime() > STALE_THRESHOLD_MS ) .sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()); } export function Dashboard() { const { selectedCompanyId, selectedCompany, companies } = useCompany(); const { openOnboarding } = useDialog(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); useEffect(() => { setBreadcrumbs([{ label: "Dashboard" }]); }, [setBreadcrumbs]); const { data, isLoading, error } = useQuery({ queryKey: queryKeys.dashboard(selectedCompanyId!), queryFn: () => dashboardApi.summary(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: activity } = useQuery({ queryKey: queryKeys.activity(selectedCompanyId!), queryFn: () => activityApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: issues } = useQuery({ queryKey: queryKeys.issues.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const staleIssues = issues ? getStaleIssues(issues) : []; const agentName = (id: string | null) => { if (!id || !agents) return null; return agents.find((a) => a.id === id)?.name ?? null; }; if (!selectedCompanyId) { if (companies.length === 0) { return ( ); } return ( ); } return (
{selectedCompany && (

{selectedCompany.name}

)} {isLoading &&

Loading...

} {error &&

{error.message}

} {data && ( <>
{/* Recent Activity */} {activity && activity.length > 0 && (

Recent Activity

{activity.slice(0, 10).map((event) => (
{formatAction(event.action)} {event.entityId.slice(0, 8)}
{timeAgo(event.createdAt)}
))}
)} {/* Stale Tasks */}

Stale Tasks

{staleIssues.length === 0 ? (

No stale tasks. All work is up to date.

) : (
{staleIssues.slice(0, 10).map((issue) => (
navigate(`/issues/${issue.id}`)} > {issue.title} {issue.assigneeAgentId && ( {agentName(issue.assigneeAgentId) ?? issue.assigneeAgentId.slice(0, 8)} )} {timeAgo(issue.updatedAt)}
))}
)}
)}
); }