"use client"; import { useState, useMemo, useEffect, useCallback } from "react"; import { trpc } from "~/lib/trpc/client.js"; import { RESOURCE_VIEW_DOMAINS, PROJECT_VIEW_DOMAINS, type Domain, type GraphNode, } from "./domain-colors"; import { buildForceGraphData, getConnectedNodeIds, type PositionedNode, type ForceGraphData } from "./graph-data"; export type ViewMode = "resource" | "project"; export interface ComputationGraphState { viewMode: ViewMode; setViewMode: (m: ViewMode) => void; resourceId: string; setResourceId: (id: string) => void; month: string; setMonth: (m: string) => void; projectId: string; setProjectId: (id: string) => void; resources: Array<{ id: string; displayName: string; eid: string }>; projects: Array<{ id: string; name: string; shortCode?: string | null }>; isLoading: boolean; activeDomains: Domain[]; graphData: ForceGraphData; highlightedNodes: Set | null; setHighlightedNodes: (s: Set | null) => void; hoveredNode: PositionedNode | null; setHoveredNode: (n: PositionedNode | null) => void; domainFilter: Set; toggleDomain: (domain: Domain) => void; handleNodeClick: (nodeId: string) => void; } export function useComputationGraphData(): ComputationGraphState { const [viewMode, setViewMode] = useState("resource"); const [resourceId, setResourceId] = useState(""); const [month, setMonth] = useState(() => { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; }); const [projectId, setProjectId] = useState(""); const [highlightedNodes, setHighlightedNodes] = useState | null>(null); const [hoveredNode, setHoveredNode] = useState(null); const [domainFilter, setDomainFilter] = useState>(new Set()); // Load selectors const { data: resourceData } = trpc.resource.list.useQuery( { isActive: true, limit: 500 }, { staleTime: 60_000 }, ); const resources = resourceData?.resources ?? []; const { data: projectData } = trpc.project.list.useQuery( {}, { staleTime: 60_000 }, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const projects: Array<{ id: string; name: string; shortCode?: string | null }> = (projectData as any)?.projects ?? (projectData as any) ?? []; // Auto-select first resource/project useEffect(() => { if (!resourceId && resources.length > 0) { setResourceId(resources[0]!.id); } }, [resources, resourceId]); useEffect(() => { if (!projectId && Array.isArray(projects) && projects.length > 0) { setProjectId(projects[0]!.id); } }, [projects, projectId]); // Fetch graph data const { data: resourceGraphData, isLoading: resourceLoading } = trpc.computationGraph.getResourceData.useQuery( { resourceId, month }, { enabled: viewMode === "resource" && !!resourceId, staleTime: 30_000 }, ); const { data: projectGraphData, isLoading: projectLoading } = trpc.computationGraph.getProjectData.useQuery( { projectId }, { enabled: viewMode === "project" && !!projectId, staleTime: 30_000 }, ); const rawData = viewMode === "resource" ? resourceGraphData : projectGraphData; const isLoading = viewMode === "resource" ? resourceLoading : projectLoading; const activeDomains = viewMode === "resource" ? RESOURCE_VIEW_DOMAINS : PROJECT_VIEW_DOMAINS; // Build graph data with positions const graphData = useMemo(() => { if (!rawData) return { nodes: [], links: [] }; let filteredNodes = rawData.nodes as GraphNode[]; if (domainFilter.size > 0) { filteredNodes = filteredNodes.filter((nd: GraphNode) => !domainFilter.has(nd.domain as Domain)); } const filteredNodeIds = new Set(filteredNodes.map((nd: GraphNode) => nd.id)); const filteredLinks = rawData.links.filter( (lk: { source: string; target: string }) => filteredNodeIds.has(lk.source) && filteredNodeIds.has(lk.target), ); return buildForceGraphData(filteredNodes, filteredLinks); }, [rawData, domainFilter]); // Domain filter toggle const toggleDomain = useCallback((domain: Domain) => { setDomainFilter((prev) => { const next = new Set(prev); if (next.has(domain)) { next.delete(domain); } else { next.add(domain); } return next; }); }, []); // Node click → highlight path const handleNodeClick = useCallback( (nodeId: string) => { if (highlightedNodes?.has(nodeId)) { setHighlightedNodes(null); } else { const connected = getConnectedNodeIds(nodeId, graphData.links); setHighlightedNodes(connected); } }, [graphData.links, highlightedNodes], ); return { viewMode, setViewMode, resourceId, setResourceId, month, setMonth, projectId, setProjectId, resources, projects, isLoading, activeDomains, graphData, highlightedNodes, setHighlightedNodes, hoveredNode, setHoveredNode, domainFilter, toggleDomain, handleNodeClick, }; }