import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { issuesApi } from "../api/issues"; import { useApi } from "../hooks/useApi"; import { useAgents } from "../hooks/useAgents"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; 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 [tab, setTab] = useState("all"); const { data: agents } = useAgents(selectedCompanyId); useEffect(() => { setBreadcrumbs([{ label: "Issues" }]); }, [setBreadcrumbs]); const fetcher = useCallback(() => { if (!selectedCompanyId) return Promise.resolve([]); return issuesApi.list(selectedCompanyId); }, [selectedCompanyId]); const { data: issues, loading, error, reload } = useApi(fetcher); const agentName = (id: string | null) => { if (!id || !agents) return null; return agents.find((a) => a.id === id)?.name ?? null; }; async function handleStatusChange(issue: Issue, status: string) { await issuesApi.update(issue.id, { status }); reload(); } 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 {loading &&

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={ <> handleStatusChange(issue, s)} /> } trailing={
{issue.assigneeAgentId && ( {agentName(issue.assigneeAgentId) ?? issue.assigneeAgentId.slice(0, 8)} )} {formatDate(issue.createdAt)}
} /> ))}
))}
); }