"use client"; import { useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; import type { WidgetProps } from "~/components/dashboard/widget-registry.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; interface ResourceRow { id: string; eid: string; displayName: string; chapter: string | null; chargeabilityTarget: number; bookingCount: number; utilizationPercent: number; isOverbooked: boolean; } export function ResourceTableWidget({ config, onConfigChange }: WidgetProps) { const chapter = (config.chapter as string) || ""; const [includeProposed, setIncludeProposed] = useState(false); const now = new Date(); const startDate = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); const endDate = new Date(now.getFullYear(), now.getMonth() + 3, 0).toISOString(); const { data: resources, isLoading } = trpc.resource.listWithUtilization.useQuery( { chapter: chapter || undefined, includeProposed, startDate, endDate }, { staleTime: 60_000 }, ); const { data: chapterData } = trpc.resource.chapters.useQuery(undefined, { staleTime: 120_000 }); const chapters = chapterData ?? []; type SortKey = "eid" | "name" | "chapter" | "bookings" | "utilization" | "target"; 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) => (
))}
); } const list = (resources ?? []) as unknown as ResourceRow[]; const sorted = [...list].sort((a, b) => { const mult = sortDir === "asc" ? 1 : -1; switch (sortKey) { case "eid": return mult * a.eid.localeCompare(b.eid); case "name": return mult * a.displayName.localeCompare(b.displayName); case "chapter": return mult * (a.chapter ?? "").localeCompare(b.chapter ?? ""); case "bookings": return mult * (a.bookingCount - b.bookingCount); case "utilization": return mult * (a.utilizationPercent - b.utilizationPercent); case "target": return mult * (a.chargeabilityTarget - b.chargeabilityTarget); default: return 0; } }); return (
{/* Filter */}
{chapters.length > 0 && ( )}
{/* Table */}
{sorted.map((r) => ( ))}
Booked hours ÷ available hours × 100 for the period.
Available hours = working days × hours from personal schedule.
Proposed rows are only counted when the toggle is enabled.
Orange = >85% ·{" "} Red = >100%
} width="w-72" />
{r.eid} {r.displayName} {r.chapter ?? "—"} {r.bookingCount} 100 ? "text-red-600" : r.utilizationPercent > 85 ? "text-orange-600" : "text-green-700"}`} > {r.utilizationPercent}% {r.chargeabilityTarget}%
{list.length === 0 && (
No resources found.
)}
); }