"use client"; import { useState } from "react"; import Link from "next/link"; import { trpc } from "~/lib/trpc/client.js"; import type { WidgetProps } from "~/components/dashboard/widget-registry.js"; import { formatCents, formatMoney } from "~/lib/format.js"; import { ProjectStatus } from "@capakraken/shared/types"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { PROJECT_STATUS_BADGE as STATUS_COLORS } from "~/lib/status-styles.js"; import { AnimatedNumber } from "~/components/ui/AnimatedNumber.js"; export function ProjectTableWidget({ config, onConfigChange }: WidgetProps) { const status = (config.status as ProjectStatus) || undefined; const search = (config.search as string) || ""; const { data: projects, isLoading } = trpc.project.listWithCosts.useQuery( { status, search: search || undefined }, { staleTime: 60_000 }, ); type SortKey = "code" | "name" | "status" | "cost" | "personDays"; const [sortKey, setSortKey] = useState("name"); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); function toggleSort(key: SortKey) { if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc")); else { setSortKey(key); setSortDir("asc"); } } if (isLoading) { return (
{/* header row */}
{[40, 120, 80, 60, 60].map((w, i) => (
))}
{/* data rows */} {[...Array(6)].map((_, i) => (
))}
); } interface ProjectRow { id: string; shortCode: string; name: string; status: string; budgetCents: number; totalCostCents: number; totalPersonDays: number; utilizationPercent: number; } const list = ((projects as unknown as { projects: ProjectRow[] } | undefined)?.projects ?? []) as ProjectRow[]; const sorted = [...list].sort((a, b) => { const mult = sortDir === "asc" ? 1 : -1; switch (sortKey) { case "code": return mult * a.shortCode.localeCompare(b.shortCode); case "name": return mult * a.name.localeCompare(b.name); case "status": return mult * a.status.localeCompare(b.status); case "cost": return mult * (a.totalCostCents - b.totalCostCents); case "personDays": return mult * (a.totalPersonDays - b.totalPersonDays); default: return 0; } }); return (
{/* Filters */}
onConfigChange?.({ search: e.target.value })} className="app-input min-w-0 flex-1 text-xs" />
{/* Table */}
{sorted.map((p) => { const overBudget = p.budgetCents > 0 && p.totalCostCents > p.budgetCents; const util = p.utilizationPercent; return ( ); })}
Budget
{p.shortCode} {p.name} {p.status} {formatCents(p.totalCostCents)} € {p.budgetCents > 0 ? (
{formatMoney(p.budgetCents)} = 80 ? "bg-amber-500" : "bg-green-500"}`} title={`${util}% utilized${overBudget ? " — over budget!" : ""}`} />
) : ( )}
{list.length === 0 && (
No projects found.
)}
); }