import { useEffect } from "react"; import { useNavigate, useSearchParams } 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 { PageTabBar } from "../components/PageTabBar"; import { Button } from "@/components/ui/button"; import { Tabs } from "@/components/ui/tabs"; import { CircleDot, Plus } from "lucide-react"; import { formatDate } from "../lib/utils"; import { Identity } from "../components/Identity"; 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"; const issueTabItems = [ { value: "all", label: "All Issues" }, { value: "active", label: "Active" }, { value: "backlog", label: "Backlog" }, { value: "done", label: "Done" }, ] as const; function parseIssueTab(value: string | null): TabFilter { if (value === "active" || value === "backlog" || value === "done") return value; return "all"; } 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 [searchParams, setSearchParams] = useSearchParams(); const tab = parseIssueTab(searchParams.get("tab")); 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 updateIssue = useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => issuesApi.update(id, data), 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]! })); const setTab = (nextTab: TabFilter) => { const next = new URLSearchParams(searchParams); if (nextTab === "all") next.delete("tab"); else next.set("tab", nextTab); setSearchParams(next); }; return (
setTab(v as TabFilter)}>
{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={ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
e.stopPropagation()}> updateIssue.mutate({ id: issue.id, data: { priority: p } })} /> updateIssue.mutate({ id: issue.id, data: { status: s } })} />
} trailing={
{issue.assigneeAgentId && (() => { const name = agentName(issue.assigneeAgentId); return name ? : {issue.assigneeAgentId.slice(0, 8)}; })()} {formatDate(issue.createdAt)}
} /> ))}
))}
); }