import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; 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 { groupBy } from "../lib/groupBy"; import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { EntityRow } from "../components/EntityRow"; import { EmptyState } from "../components/EmptyState"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { CircleDot, Plus } from "lucide-react"; import { formatDate } from "../lib/utils"; import type { Issue } from "@paperclip/shared"; const statusOrder = ["in_progress", "todo", "backlog", "in_review", "blocked", "done", "cancelled"]; function statusLabel(status: string): string { return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } type TabFilter = "all" | "active" | "backlog" | "done"; function filterIssues(issues: Issue[], tab: TabFilter): Issue[] { switch (tab) { case "active": return issues.filter((i) => ["todo", "in_progress", "in_review", "blocked"].includes(i.status)); case "backlog": return issues.filter((i) => i.status === "backlog"); case "done": return issues.filter((i) => ["done", "cancelled"].includes(i.status)); default: return issues; } } export function Issues() { const { selectedCompanyId } = useCompany(); const { openNewIssue } = useDialog(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [tab, setTab] = useState("all"); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); useEffect(() => { setBreadcrumbs([{ label: "Issues" }]); }, [setBreadcrumbs]); const { data: issues, isLoading, error } = useQuery({ queryKey: queryKeys.issues.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const updateStatus = useMutation({ mutationFn: ({ id, status }: { id: string; status: string }) => issuesApi.update(id, { status }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId!) }); }, }); const agentName = (id: string | null) => { if (!id || !agents) return null; return agents.find((a) => a.id === id)?.name ?? null; }; if (!selectedCompanyId) { return ; } const filtered = filterIssues(issues ?? [], tab); const grouped = groupBy(filtered, (i) => i.status); const orderedGroups = statusOrder .filter((s) => grouped[s]?.length) .map((s) => ({ status: s, items: grouped[s]! })); return (

Issues

setTab(v as TabFilter)}> All Issues Active Backlog Done {isLoading &&

Loading...

} {error &&

{error.message}

} {issues && filtered.length === 0 && ( openNewIssue()} /> )} {orderedGroups.map(({ status, items }) => (
{statusLabel(status)} {items.length}
{items.map((issue) => ( navigate(`/issues/${issue.id}`)} leading={ <> updateStatus.mutate({ id: issue.id, status: s })} /> } trailing={
{issue.assigneeAgentId && ( {agentName(issue.assigneeAgentId) ?? issue.assigneeAgentId.slice(0, 8)} )} {formatDate(issue.createdAt)}
} /> ))}
))}
); }