"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 (
);
}
if (status === "unauthenticated") {
return (
);
}
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" })}
>
Sign out
{/* 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 ;
}