"use client"; import { CheckCircle, Clock, Euro, Globe, LogOut, MessageCircle, MessageSquare, MoreVertical, RefreshCw, TrendingUp, Users, Zap, } from "lucide-react"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; import { signOut, useSession } from "next-auth/react"; import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Skeleton } from "@/components/ui/skeleton"; import { formatEnumValue } from "@/lib/format-enums"; import ModernBarChart from "../../../components/charts/bar-chart"; import ModernDonutChart from "../../../components/charts/donut-chart"; import ModernLineChart from "../../../components/charts/line-chart"; import GeographicMap from "../../../components/GeographicMap"; import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution"; import TopQuestionsChart from "../../../components/TopQuestionsChart"; import MetricCard from "../../../components/ui/metric-card"; import type { Company, MetricsResult } from "../../../lib/types"; // Dynamic import for heavy D3-based WordCloud component (~50KB) const WordCloud = dynamic(() => import("../../../components/WordCloud"), { ssr: false, loading: () => (
), }); // Safely wrapped component with useSession function DashboardContent() { const { data: session, status } = useSession(); const router = useRouter(); const [metrics, setMetrics] = useState(null); const [company, setCompany] = useState(null); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [isInitialLoad, setIsInitialLoad] = useState(true); const refreshStatusId = useId(); const isAuditor = session?.user?.role === "AUDITOR"; // Function to fetch metrics with optional date range const fetchMetrics = useCallback( async (startDate?: string, endDate?: string, isInitial = false) => { setLoading(true); try { let url = "/api/dashboard/metrics"; if (startDate && endDate) { url += `?startDate=${startDate}&endDate=${endDate}`; } const res = await fetch(url); const data = await res.json(); setMetrics(data.metrics); setCompany(data.company); // Set initial load flag if (isInitial) { setIsInitialLoad(false); } } catch (error) { console.error("Error fetching metrics:", error); } finally { setLoading(false); } }, [] ); useEffect(() => { // Redirect if not authenticated if (status === "unauthenticated") { router.push("/login"); return; } // Fetch metrics and company on mount if authenticated if (status === "authenticated" && isInitialLoad) { fetchMetrics(undefined, undefined, true); } }, [status, router, isInitialLoad, fetchMetrics]); const handleRefresh = useCallback(async () => { if (isAuditor) return; if (!company?.id) { alert("Cannot refresh: Company ID is missing"); return; } setRefreshing(true); try { // Start both requests in parallel - metrics will be ready when refresh completes const [refreshRes, metricsRes] = await Promise.all([ fetch("/api/admin/refresh-sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ companyId: company.id }), }), fetch("/api/dashboard/metrics"), ]); if (refreshRes.ok) { const data = await metricsRes.json(); setMetrics(data.metrics); } else { const errorData = await refreshRes.json(); alert(`Failed to refresh sessions: ${errorData.error}`); } } finally { setRefreshing(false); } }, [isAuditor, company?.id]); // Memoized data preparation - must be before any early returns (Rules of Hooks) const sentimentData = useMemo(() => { if (!metrics) return []; return [ { name: "Positive", value: metrics.sentimentPositiveCount ?? 0, color: "hsl(var(--chart-1))", }, { name: "Neutral", value: metrics.sentimentNeutralCount ?? 0, color: "hsl(var(--chart-2))", }, { name: "Negative", value: metrics.sentimentNegativeCount ?? 0, color: "hsl(var(--chart-3))", }, ]; }, [metrics]); const sessionsOverTimeData = useMemo(() => { if (!metrics?.days) return []; return Object.entries(metrics.days).map(([date, value]) => ({ date: new Date(date).toLocaleDateString("en-US", { month: "short", day: "numeric", }), value: value as number, })); }, [metrics?.days]); const categoriesData = useMemo(() => { if (!metrics?.categories) return []; return Object.entries(metrics.categories).map(([name, value]) => { const formattedName = formatEnumValue(name) || name; return { name: formattedName.length > 15 ? `${formattedName.substring(0, 15)}...` : formattedName, value: value as number, }; }); }, [metrics?.categories]); const languagesData = useMemo(() => { if (!metrics?.languages) return []; return Object.entries(metrics.languages).map(([name, value]) => ({ name, value: value as number, })); }, [metrics?.languages]); const wordCloudData = useMemo( () => metrics?.wordCloudData ?? [], [metrics?.wordCloudData] ); const countryData = useMemo(() => { if (!metrics?.countries) return {}; return Object.entries(metrics.countries).reduce( (acc, [code, count]) => { if (code && count) { acc[code] = count; } return acc; }, {} as Record ); }, [metrics?.countries]); // Use seeded random for stable response time distribution const responseTimeData = useMemo(() => { const avgTime = metrics?.avgResponseTime || 1.5; const seed = avgTime * 1000; const seededRandom = (i: number) => { const x = Math.sin(seed + i) * 10000; return x - Math.floor(x); }; return Array.from( { length: 50 }, (_, i) => avgTime * (0.5 + seededRandom(i)) ); }, [metrics?.avgResponseTime]); // Show loading state while session status is being determined if (status === "loading") { return (

Loading session...

); } if (status === "unauthenticated") { return (

Redirecting to login...

); } if (loading || !metrics || !company) { return (
{/* Header Skeleton */}
{/* Metrics Grid Skeleton */}
{Array.from({ length: 8 }, (_, i) => { const metricTypes = [ "sessions", "users", "time", "response", "costs", "peak", "resolution", "languages", ]; return ( ); })}
{/* Charts Skeleton */}
); } return (
{/* Modern Header */}

{company.name}

Analytics Dashboard

Last updated{" "} {new Date(metrics.lastUpdated || Date.now()).toLocaleString()}

{refreshing && (
Dashboard data is being refreshed
)} signOut({ callbackUrl: "/login" })} >
{/* Date Range Picker - Temporarily disabled to debug infinite loop */} {/* {dateRange && ( )} */} {/* Modern Metrics Grid */}
} trend={{ value: metrics.sessionTrend ?? 0, isPositive: (metrics.sessionTrend ?? 0) >= 0, }} variant="primary" /> } trend={{ value: metrics.usersTrend ?? 0, isPositive: (metrics.usersTrend ?? 0) >= 0, }} variant="success" /> } trend={{ value: metrics.avgSessionTimeTrend ?? 0, isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0, }} /> } trend={{ value: metrics.avgResponseTimeTrend ?? 0, isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, }} variant="warning" /> } description="Average per day" /> } description="Busiest hour" /> } trend={{ value: metrics.resolvedChatsPercentage ?? 0, isPositive: (metrics.resolvedChatsPercentage ?? 0) >= 80, }} variant={ metrics.resolvedChatsPercentage && metrics.resolvedChatsPercentage >= 80 ? "success" : "warning" } /> } description="Languages detected" />
{/* Charts Section */}
{/* Geographic and Topics Section */}
Geographic Distribution Common Topics
{/* Top Questions Chart */} {/* Response Time Distribution */} Response Time Distribution {/* Token Usage Summary */}
AI Usage & Costs
Total Tokens: {metrics.totalTokens?.toLocaleString() || 0} Total Cost:€ {metrics.totalTokensEur?.toFixed(4) || 0}

Token usage chart will be implemented with historical data

); } export default function DashboardPage() { return ; }