"use client"; import { useState } from "react"; import Link from "next/link"; import type { Route } from "next"; import { trpc } from "~/lib/trpc/client.js"; // ─── Anomaly type badge colors ─────────────────────────────────────────────── const SEVERITY_STYLES = { critical: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", warning: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300", } as const; const TYPE_LABELS: Record = { budget: "Budget", staffing: "Staffing", utilization: "Utilization", timeline: "Timeline", }; const TYPE_ICONS: Record = { budget: "\u20AC", // Euro sign staffing: "\u2642", // Person sign utilization: "\u2B24", // Circle timeline: "\u23F0", // Clock }; // ─── Shimmer skeleton ──────────────────────────────────────────────────────── function Shimmer({ className = "" }: { className?: string }) { return (
); } function AnomalyListSkeleton() { return (
{Array.from({ length: 4 }).map((_, i) => (
))}
); } function NarrativeSkeleton() { return (
); } // ─── Entity link helper ────────────────────────────────────────────────────── function entityLink(type: string, entityId: string): string { if (type === "utilization") return `/resources/${entityId}`; return `/projects/${entityId}`; } // ─── Main component ────────────────────────────────────────────────────────── export function InsightsPanel() { const [selectedProjectId, setSelectedProjectId] = useState(""); const [narrativeFilter, setNarrativeFilter] = useState(null); // Fetch anomalies const anomaliesQuery = trpc.insights.detectAnomalies.useQuery(undefined, { staleTime: 60_000, refetchOnWindowFocus: false, }); // Fetch AI configuration status const aiConfigQuery = trpc.settings.getAiConfigured.useQuery(undefined, { staleTime: 300_000, }); // Fetch project list for dropdown const projectsQuery = trpc.project.list.useQuery( { page: 1, limit: 200 }, { staleTime: 60_000, refetchOnWindowFocus: false }, ); // Fetch cached narrative for selected project const cachedNarrativeQuery = trpc.insights.getCachedNarrative.useQuery( { projectId: selectedProjectId }, { enabled: !!selectedProjectId, staleTime: 30_000, }, ); // Generate narrative mutation const generateMutation = trpc.insights.generateProjectNarrative.useMutation({ onSuccess: () => { // Refetch the cached narrative void cachedNarrativeQuery.refetch(); }, }); const anomalies = anomaliesQuery.data ?? []; const projects = projectsQuery.data?.projects ?? []; // Filter anomalies const filteredAnomalies = narrativeFilter ? anomalies.filter((a) => a.type === narrativeFilter) : anomalies; const summaryCountsByType = anomalies.reduce( (acc, a) => { acc[a.type] = (acc[a.type] ?? 0) + 1; return acc; }, {} as Record, ); const criticalCount = anomalies.filter((a) => a.severity === "critical").length; return (
{/* ── Summary cards ─────────────────────────────────────────────── */}
{(["budget", "staffing", "utilization", "timeline"] as const).map((type) => { const count = summaryCountsByType[type] ?? 0; const isActive = narrativeFilter === type; return ( ); })}
{/* ── Anomaly feed ──────────────────────────────────────────────── */}

Anomaly Feed {criticalCount > 0 && ( {criticalCount} critical )}

{narrativeFilter && ( )}
{anomaliesQuery.isLoading ? ( ) : anomaliesQuery.error ? (
Failed to load anomalies: {anomaliesQuery.error.message}
) : filteredAnomalies.length === 0 ? (
All clear

No anomalies detected across active projects.

) : (
{filteredAnomalies.map((anomaly, idx) => (
{/* Severity badge */} {anomaly.severity === "critical" ? "Critical" : "Warning"} {/* Content */}
{TYPE_ICONS[anomaly.type]} {TYPE_LABELS[anomaly.type]}

{anomaly.message}

{anomaly.entityName} →
))}
)}
{/* ── Project narrative ─────────────────────────────────────────── */}

Project Narrative

{!aiConfigQuery.data?.configured ? (

AI is not configured.{" "} Configure AI credentials in Admin Settings {" "} to enable project narratives.

) : (
{/* Project selector */}
{/* Narrative display */}
{generateMutation.isPending ? ( ) : generateMutation.error ? (
{generateMutation.error.message}
) : generateMutation.data ? (

{generateMutation.data.narrative}

Generated {new Date(generateMutation.data.generatedAt).toLocaleString()}

) : cachedNarrativeQuery.data?.narrative ? (

{cachedNarrativeQuery.data.narrative}

Previously generated{" "} {cachedNarrativeQuery.data.generatedAt ? new Date(cachedNarrativeQuery.data.generatedAt).toLocaleString() : ""}

) : selectedProjectId ? (

Click "Generate Summary" to create an AI-powered executive narrative for this project.

) : (

Select a project above to generate or view its executive summary.

)}
)}
); }