import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { FlaskConical, Loader2, Plus } from "lucide-react"; import { statusCardsApi } from "@/api/statusCards"; import { describeError } from "@/api/errors"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { useNavigate, useParams } from "@/lib/router"; import { queryKeys } from "@/lib/queryKeys"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/EmptyState"; import { InlineBanner } from "@/components/InlineBanner"; import { formatCents, formatTokens } from "./format"; import { StatusCardTile } from "./StatusCardTile"; import { ArchivedStatusCardRow } from "./ArchivedStatusCardRow"; import { CreateStatusCardDialog } from "./CreateStatusCardDialog"; import { StatusCardDetailDrawer } from "./StatusCardDetailDrawer"; import type { StatusCardView } from "./types"; export function StatusCards() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { cardId } = useParams<{ cardId?: string }>(); const [showArchived, setShowArchived] = useState(false); const [createOpen, setCreateOpen] = useState(false); // Which tab the detail drawer opens to (the tile's "Query debug"/"Edit" // actions deep-link into Settings). const [detailTab, setDetailTab] = useState("summary"); const [actionError, setActionError] = useState(null); useEffect(() => { setBreadcrumbs([{ label: "Status" }]); }, [setBreadcrumbs]); const activeQuery = useQuery({ queryKey: selectedCompanyId ? queryKeys.statusCards.list(selectedCompanyId, false) : ["status-cards", "none", "active"], queryFn: () => statusCardsApi.list(selectedCompanyId!, false), enabled: Boolean(selectedCompanyId), }); const activeView = useQueryView(activeQuery); const archivedQuery = useQuery({ queryKey: selectedCompanyId ? queryKeys.statusCards.list(selectedCompanyId, true) : ["status-cards", "none", "archived"], queryFn: () => statusCardsApi.list(selectedCompanyId!, true), enabled: Boolean(selectedCompanyId), }); const activeCards = (activeQuery.data ?? []) as StatusCardView[]; const archivedCards = (archivedQuery.data ?? []) as StatusCardView[]; // Deep-linked detail: prefer a card already in a loaded list, fall back to a // by-id fetch for direct navigation. const cardInLists = useMemo( () => [...activeCards, ...archivedCards].find((card) => card.id === cardId) ?? null, [activeCards, archivedCards, cardId], ); const detailFallbackQuery = useQuery({ queryKey: cardId ? queryKeys.statusCards.detail(cardId) : ["status-cards", "detail", "none"], queryFn: () => statusCardsApi.get(cardId!), enabled: Boolean(cardId) && !cardInLists, }); const detailCard = (cardInLists ?? detailFallbackQuery.data ?? null) as StatusCardView | null; const invalidateLists = () => selectedCompanyId ? Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.statusCards.list(selectedCompanyId, false) }), queryClient.invalidateQueries({ queryKey: queryKeys.statusCards.list(selectedCompanyId, true) }), ]) : Promise.resolve(); const refreshMutation = useMutation({ mutationFn: (id: string) => statusCardsApi.refresh(id), onMutate: () => setActionError(null), onSuccess: () => invalidateLists(), onError: (err) => setActionError(describeError(err, { action: "refresh the card" }).body), }); const recompileMutation = useMutation({ mutationFn: (id: string) => statusCardsApi.recompile(id), onMutate: () => setActionError(null), onSuccess: () => invalidateLists(), onError: (err) => setActionError(describeError(err, { action: "run the card" }).body), }); const archiveMutation = useMutation({ mutationFn: (id: string) => statusCardsApi.patch(id, { archived: true }), onMutate: () => setActionError(null), onSuccess: () => invalidateLists(), onError: (err) => setActionError(describeError(err, { action: "archive the card" }).body), }); const restoreMutation = useMutation({ mutationFn: (id: string) => statusCardsApi.patch(id, { archived: false }), onMutate: () => setActionError(null), onSuccess: () => invalidateLists(), onError: (err) => setActionError(describeError(err, { action: "restore the card" }).body), }); const openDetail = (id: string, tab: string = "summary") => { setDetailTab(tab); navigate(`/status/${id}`); }; const closeDetail = () => navigate("/status"); const todayTotals = activeCards.reduce( (acc, card) => ({ tokens: acc.tokens + (card.todayTokens ?? 0), cents: acc.cents + (card.todayCostCents ?? 0), }), { tokens: 0, cents: 0 }, ); const showCostMeter = todayTotals.tokens > 0 || todayTotals.cents > 0; return (
{/* Header */}

Status

Experimental
{showCostMeter ? ( Today: {formatTokens(todayTotals.tokens)} · ~{formatCents(todayTotals.cents)} ) : null}
{actionError ? {actionError} : null} {activeView.kind === "loading" || activeView.kind === "reconnecting" ? (
Loading cards…
) : activeView.kind === "error" ? ( ) : activeCards.length === 0 ? ( setCreateOpen(true)} /> ) : (
{activeCards.map((card) => ( openDetail(card.id)} onRefresh={() => refreshMutation.mutate(card.id)} onRecompile={() => recompileMutation.mutate(card.id)} onEditInterest={() => openDetail(card.id, "settings")} onOpenDebug={() => openDetail(card.id, "settings")} onArchive={() => archiveMutation.mutate(card.id)} refreshPending={refreshMutation.isPending && refreshMutation.variables === card.id} recompilePending={recompileMutation.isPending && recompileMutation.variables === card.id} /> ))}
)} {archivedCards.length > 0 ? (
{showArchived ? archivedCards.map((card) => ( openDetail(card.id)} onRestore={() => restoreMutation.mutate(card.id)} restorePending={restoreMutation.isPending && restoreMutation.variables === card.id} /> )) : null}
) : null} {selectedCompanyId ? ( ) : null} (open ? undefined : closeDetail())} initialTab={detailTab} />
); } export default StatusCards;