import React, { useMemo, useState, useEffect } from 'react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell, LineChart, Line, AreaChart, Area, RadialBarChart, RadialBar } from 'recharts'; import { BarChart3, PieChart as PieChartIcon, LineChart as LineChartIcon, TrendingUp, Users, ShieldCheck, Zap, AlertCircle, Clock, Activity, Layers, Inbox, User, Loader2, Bot, Star, Target } from 'lucide-react'; import { supabase } from "../../lib/supabaseClient"; import StatCard from '../components/StatCard'; import { Card, CardContent } from "../../components/ui/card"; import useAuthStore from "../../store/authStore"; import { formatTimelineDate } from "../../utils/dateUtils"; const COLORS = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#a855f7', '#ec4899']; const AdminAnalytics = () => { const { profile } = useAuthStore(); const [tickets, setTickets] = useState([]); const [loading, setLoading] = useState(true); const fetchAnalytics = async () => { setLoading(true); try { let query = supabase .from('tickets') .select(` *, creator:profiles!tickets_user_id_fkey(full_name, email, profile_picture) `); if (profile?.role === 'admin' && profile?.company) { query = query.eq('company', profile.company); } const { data, error: sbError } = await query.order('created_at', { ascending: false }); if (sbError) { console.warn("Analytics relationship join failed, retrying simple select...", sbError); const { data: basicData, error: basicError } = await supabase .from('tickets') .select('*') .eq('company', profile?.company) .order('created_at', { ascending: false }); if (basicError) throw basicError; setTickets(basicData || []); } else { setTickets(data || []); } } catch (err) { console.error("Analytics fetch error:", err); } finally { setLoading(false); } }; useEffect(() => { if (profile) { fetchAnalytics(); } }, [profile]); const stats = useMemo(() => { if (!tickets.length) return { total: 0, open: 0, resolved: 0, highPriority: 0, volumeTimeline: [], categoryData: [], teamData: [], resolutionData: [], liveFeed: [] }; const total = tickets.length; const resolved = tickets.filter(t => t.status?.toLowerCase().includes('resolv')).length; const open = tickets.filter(t => !t.status?.toLowerCase().includes('resolv')).length; const highPriority = tickets.filter(t => t.priority?.toLowerCase() === 'high').length; // 1. Tickets Per Day (Volume Timeline) const timeMap = {}; tickets.forEach(t => { if (t.created_at) { const date = formatTimelineDate(t.created_at).split(',')[0]; // Extract just the date part for the axis timeMap[date] = (timeMap[date] || 0) + 1; } }); const volumeTimeline = Object.keys(timeMap).map(key => ({ date: key, count: timeMap[key] })) .sort((a, b) => new Date(a.date) - new Date(b.date)); // 2. Tickets Per Category const catMap = {}; tickets.forEach(t => { const cat = t.category || 'Uncategorized'; catMap[cat] = (catMap[cat] || 0) + 1; }); const categoryData = Object.keys(catMap).map(key => ({ name: key, count: catMap[key] })) .sort((a, b) => b.count - a.count); // 3. Tickets Per Team const teamMap = {}; tickets.forEach(t => { const team = t.assigned_team || 'Unassigned'; teamMap[team] = (teamMap[team] || 0) + 1; }); const teamData = Object.keys(teamMap).map(key => ({ name: key, value: teamMap[key] })) .sort((a, b) => b.value - a.value); // 4. Resolution Distribution (Open vs Resolved vs In Progress) const statusMap = {}; tickets.forEach(t => { const s = t.status?.charAt(0).toUpperCase() + t.status?.slice(1) || 'Unknown'; statusMap[s] = (statusMap[s] || 0) + 1; }); const resolutionData = Object.keys(statusMap).map(key => ({ name: key, value: statusMap[key] })); // 5. Live Activity Feed (Latest 10) const liveFeed = tickets.slice(0, 10).map(t => ({ ticket_id: t.id, user: t.creator?.full_name || t.profiles?.full_name || (t.user_id ? `User ${t.user_id.slice(0, 5)}` : 'Anonymous'), action: `Ticket ${t.status || 'Updated'}`, type: t.status === 'open' ? 'create' : t.status === 'resolved' ? 'resolve' : 'assign', timeFormatted: formatTimelineDate(t.created_at), time: new Date(t.created_at).getTime() })); return { total, open, resolved, highPriority, volumeTimeline, categoryData, teamData, resolutionData, liveFeed }; }, [tickets]); // AI-specific metrics const aiStats = useMemo(() => { if (!tickets.length) return { accuracyRate: 0, resolutionSplit: [], misclassifiedCategories: [], avgCsatScore: null, avgResponseTime: 0, }; // AI Accuracy: tickets that were NOT manually corrected by admin const corrected = tickets.filter(t => t.metadata?.corrected_at).length; const accuracyRate = tickets.length ? (((tickets.length - corrected) / tickets.length) * 100).toFixed(1) : 0; // AI vs Manual resolution split const aiResolved = tickets.filter(t => t.status?.toLowerCase()?.includes('auto')).length; const humanResolved = tickets.filter(t => ['resolved', 'closed'].includes(t.status?.toLowerCase()) && !t.status?.toLowerCase()?.includes('auto')).length; const resolutionSplit = [ { name: 'AI Auto-Resolved', value: aiResolved, fill: '#10b981' }, { name: 'Human Resolved', value: humanResolved, fill: '#6366f1' }, { name: 'Open/Pending', value: tickets.length - aiResolved - humanResolved, fill: '#f59e0b' }, ].filter(d => d.value > 0); // Top categories that were corrected by admins const correctedTickets = tickets.filter(t => t.metadata?.corrected_at); const misclassMap = {}; correctedTickets.forEach(t => { const cat = t.category || 'Unknown'; misclassMap[cat] = (misclassMap[cat] || 0) + 1; }); const misclassifiedCategories = Object.keys(misclassMap) .map(key => ({ name: key, corrections: misclassMap[key] })) .sort((a, b) => b.corrections - a.corrections) .slice(0, 6); // Average CSAT const ratedTickets = tickets.filter(t => t.csat_rating); const avgCsatScore = ratedTickets.length ? (ratedTickets.reduce((sum, t) => sum + t.csat_rating, 0) / ratedTickets.length).toFixed(1) : null; return { accuracyRate, resolutionSplit, misclassifiedCategories, avgCsatScore }; }, [tickets]); // Removed tab state - moving to single dashboard layout if (loading) return (

Analyzing ticket data...

); return (
{/* Header section */}

Analytics

AI & System Insights

{/* Combined KPI Grid */}
{aiStats.accuracyRate}%} subtitle="Correct auto-classifications" icon={Target} color="emerald" /> No data} subtitle="Customer satisfaction score" icon={Star} color="indigo" /> {stats.highPriority}} subtitle="Tickets needing attention" icon={AlertCircle} color="red" />
{/* Main Content (8 cols) */}
{/* Volume Timeline Chart */}

Daily Ticket Volume

{stats.volumeTimeline.length > 0 ? ( ) : (
No activity data
)}
{/* Resolution Split Chart */}

Resolution Status

{aiStats.resolutionSplit.length > 0 ? ( {aiStats.resolutionSplit.map((entry, index) => ( ))} {value}} /> ) : (
No resolution data
)}
{/* Category Breakdown */}

Tickets by Category

{stats.categoryData.length > 0 ? ( ) : (
No category data
)}
{/* AI Correction Log */}

AI Correction Log

Categories with manual corrections

{aiStats.misclassifiedCategories.length > 0 ? ( ) : (

AI classifications are performing optimally.

)}
{/* Live Activity (4 cols) */}

Recent Ticket Actions

{stats.liveFeed.length > 0 ? ( stats.liveFeed.map((event, idx) => { let badgeStyle = { background: '#fef3c7', color: '#d97706' }; // Default amber let badgeText = 'Escalated'; if (event.action.toLowerCase().includes('resolve')) { if (event.action.toLowerCase().includes('auto')) { badgeStyle = { background: '#dbeafe', color: '#2563eb' }; // blue pill for auto badgeText = 'AI Resolved'; } else { badgeStyle = { background: '#dcfce7', color: '#15803d' }; // green pill for human resolved badgeText = 'Resolved'; } } else if (event.type === 'create') { badgeStyle = { background: '#fef9c3', color: '#ca8a04' }; // yellow pill for created/open badgeText = 'Open'; } return (
{event.type === 'create' ? : event.type === 'resolve' ? : }
{idx !== stats.liveFeed.length - 1 &&
}
#{event.ticket_id.slice(0, 8)} {event.timeFormatted.split(',')[1]}
{badgeText}

{event.action}

{event.user}

); }) ) : (

Waiting for signal...

)}
); }; export default AdminAnalytics;