import React, { useState, useEffect, useMemo } from 'react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, PieChart, Pie, Legend } from 'recharts'; import { Users, Clock, Phone, TrendingUp, Filter } from 'lucide-react'; import { getAttendances, getUsers, getTeams, getUserById, getFunnels, getOrigins } from '../services/dataService'; import { COLORS } from '../constants'; import { Attendance, DashboardFilter, FunnelStage, User, FunnelStageDef, OriginItemDef } from '../types'; import { KPICard } from '../components/KPICard'; import { DateRangePicker } from '../components/DateRangePicker'; import { SellersTable } from '../components/SellersTable'; import { ProductLists } from '../components/ProductLists'; // Interface for seller statistics accumulator interface SellerStats { total: number; converted: number; scoreSum: number; count: number; timeSum: number; } export const Dashboard: React.FC = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState([]); const [prevData, setPrevData] = useState([]); const [users, setUsers] = useState([]); const [teams, setTeams] = useState([]); const [funnelDefs, setFunnelDefs] = useState([]); const [originDefs, setOriginDefs] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [filters, setFilters] = useState({ dateRange: { start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Last 30 days end: new Date(), }, userId: 'all', teamId: 'all', funnelStage: 'all', origin: 'all', }); useEffect(() => { const fetchData = async () => { setLoading(true); try { const tenantId = localStorage.getItem('ctms_tenant_id'); const storedUserId = localStorage.getItem('ctms_user_id'); if (!tenantId) return; // Calculate previous date range for accurate period-over-period trend const duration = filters.dateRange.end.getTime() - filters.dateRange.start.getTime(); const prevStart = new Date(filters.dateRange.start.getTime() - duration); const prevEnd = new Date(filters.dateRange.start.getTime()); const prevFilters = { ...filters, dateRange: { start: prevStart, end: prevEnd } }; // Fetch users, attendances, teams, funnels and current user in parallel const [fetchedUsers, fetchedData, prevFetchedData, fetchedTeams, fetchedFunnels, fetchedOrigins, me] = await Promise.all([ getUsers(tenantId), getAttendances(tenantId, filters), getAttendances(tenantId, prevFilters), getTeams(tenantId), getFunnels(tenantId), getOrigins(tenantId), storedUserId ? getUserById(storedUserId) : null ]); setUsers(fetchedUsers); setData(fetchedData); setPrevData(prevFetchedData); setTeams(fetchedTeams); setOriginDefs(fetchedOrigins); if (me) setCurrentUser(me); // Determine which funnel to display const targetTeamId = filters.teamId !== 'all' ? filters.teamId : (me?.team_id || null); let activeFunnel = fetchedFunnels[0]; // fallback to first/default if (targetTeamId) { const matchedFunnel = fetchedFunnels.find(f => f.teamIds?.includes(targetTeamId)); if (matchedFunnel) activeFunnel = matchedFunnel; } setFunnelDefs(activeFunnel && activeFunnel.stages ? activeFunnel.stages.sort((a: any, b: any) => a.order_index - b.order_index) : []); // Determine which origins to display let activeOriginGroup = fetchedOrigins[0]; if (targetTeamId) { const matchedOrigin = fetchedOrigins.find(o => o.teamIds?.includes(targetTeamId)); if (matchedOrigin) activeOriginGroup = matchedOrigin; } setOriginDefs(activeOriginGroup && activeOriginGroup.items ? activeOriginGroup.items : []); } catch (error) { console.error("Error loading dashboard data:", error); } finally { setLoading(false); } }; fetchData(); }, [filters]); // --- Metrics Calculations --- const totalLeads = data.length; const avgScore = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.score, 0) / data.length).toFixed(1) : "0"; const avgResponseTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.first_response_time_min, 0) / data.length).toFixed(0) : "0"; const avgHandleTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.handling_time_min, 0) / data.length).toFixed(0) : "0"; // --- Dynamic Trends Calculation --- const trends = useMemo(() => { if (data.length === 0 && prevData.length === 0) { return { leads: null, score: null, resp: null }; } const calcAvg = (arr: Attendance[], key: keyof Attendance) => arr.length ? arr.reduce((acc, curr) => acc + (curr[key] as number), 0) / arr.length : 0; // Leads Trend (%) const recentLeads = data.length; const prevLeads = prevData.length; let leadsTrend = 0; if (prevLeads > 0) { leadsTrend = ((recentLeads - prevLeads) / prevLeads) * 100; } else if (recentLeads > 0) { leadsTrend = 100; } // Score Trend (Absolute point difference) const recentScore = calcAvg(data, 'score'); const prevScore = calcAvg(prevData, 'score'); let scoreTrend = 0; if (prevData.length > 0) { scoreTrend = recentScore - prevScore; } else { scoreTrend = recentScore; } // Response Time Trend (%) const recentResp = calcAvg(data, 'first_response_time_min'); const prevResp = calcAvg(prevData, 'first_response_time_min'); let respTrend = 0; if (prevResp > 0) { respTrend = ((recentResp - prevResp) / prevResp) * 100; } else if (recentResp > 0) { respTrend = 100; // Time increased from 0, which is worse } return { leads: leadsTrend, score: scoreTrend, resp: respTrend }; }, [data, prevData]); // --- Chart Data: Funnel --- const funnelData = useMemo(() => { const counts = data.reduce((acc, curr) => { acc[curr.funnel_stage] = (acc[curr.funnel_stage] || 0) + 1; return acc; }, {} as Record); if (funnelDefs.length > 0) { return funnelDefs.map(stage => ({ name: stage.name, value: counts[stage.name] || 0, color: stage.color_class.split(' ')[0].replace('bg-', '') // Extract base color name for Recharts })); } // Fallback if funnels aren't loaded yet const stagesOrder = [ FunnelStage.NO_CONTACT, FunnelStage.IDENTIFICATION, FunnelStage.NEGOTIATION, FunnelStage.WON, FunnelStage.LOST ]; return stagesOrder.map(stage => ({ name: stage, value: counts[stage] || 0 })); }, [data, funnelDefs]); const tailwindToHex: Record = { 'zinc': '#71717a', 'blue': '#3b82f6', 'purple': '#a855f7', 'green': '#22c55e', 'red': '#ef4444', 'pink': '#ec4899', 'orange': '#f97316', 'yellow': '#eab308' }; // --- Chart Data: Origin --- const originData = useMemo(() => { const counts = data.reduce((acc, curr) => { acc[curr.origin] = (acc[curr.origin] || 0) + 1; return acc; }, {} as Record); if (originDefs.length > 0) { const activeOrigins = originDefs.map(def => { let hexColor = '#71717a'; // Default zinc if (def.color_class) { const match = def.color_class.match(/bg-([a-z]+)-\d+/); if (match && tailwindToHex[match[1]]) { hexColor = tailwindToHex[match[1]]; } } return { name: def.name, value: counts[def.name] || 0, hexColor }; }); // Calculate "Outros" for data that doesn't match current active origins const activeNames = new Set(originDefs.map(d => d.name)); const othersValue = (Object.entries(counts) as [string, number][]) .filter(([name]) => !activeNames.has(name)) .reduce((sum, [_, val]) => sum + val, 0); if (othersValue > 0) { activeOrigins.push({ name: 'Outros', value: othersValue, hexColor: '#94a3b8' // Gray-400 }); } return activeOrigins.sort((a, b) => b.value - a.value); } return []; // No definitions = No chart (matches funnel behavior) }, [data, originDefs]); // --- Table Data: Sellers Ranking --- const sellersRanking = useMemo(() => { const stats = data.reduce>((acc, curr) => { if (!acc[curr.user_id]) { acc[curr.user_id] = { total: 0, converted: 0, scoreSum: 0, count: 0, timeSum: 0 }; } acc[curr.user_id].total += 1; if (curr.converted) acc[curr.user_id].converted += 1; acc[curr.user_id].scoreSum += curr.score; acc[curr.user_id].timeSum += curr.first_response_time_min; acc[curr.user_id].count += 1; return acc; }, {}); return Object.entries(stats) .map(([userId, s]) => { const stat = s as SellerStats; const user = users.find(u => u.id === userId); if (!user) return null; return { user, total: stat.total, avgScore: (stat.scoreSum / stat.count).toFixed(1), conversionRate: ((stat.converted / stat.total) * 100).toFixed(1), responseTime: (stat.timeSum / stat.count).toFixed(0) }; }) .filter((item): item is NonNullable => item !== null); }, [data, users]); // --- Lists Data: Products --- const productStats = useMemo(() => { const requested: Record = {}; const sold: Record = {}; data.forEach(d => { if (d.product_requested) requested[d.product_requested] = (requested[d.product_requested] || 0) + 1; if (d.product_sold) sold[d.product_sold] = (sold[d.product_sold] || 0) + 1; }); const formatList = (record: Record, total: number) => Object.entries(record) .map(([name, count]) => ({ name, count, percentage: Math.round((count / (total || 1)) * 100) })) .sort((a, b) => b.count - a.count) .slice(0, 5); const totalReq = Object.values(requested).reduce((a, b) => a + b, 0); const totalSold = Object.values(sold).reduce((a, b) => a + b, 0); return { requested: formatList(requested, totalReq), sold: formatList(sold, totalSold) }; }, [data]); const handleFilterChange = (key: keyof DashboardFilter, value: any) => { setFilters(prev => ({ ...prev, [key]: value })); }; if (loading && data.length === 0) { return
Carregando Dashboard...
; } const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'super_admin' || currentUser?.role === 'manager'; return (
{/* Filters Bar */}
Filtros:
handleFilterChange('dateRange', range)} /> {isAdmin && ( <> {currentUser?.role !== 'manager' && ( )} )}
{/* KPI Cards */}
0 ? 'up' : trends.leads < 0 ? 'down' : 'neutral'} trendValue={`${Math.abs(trends.leads).toFixed(1)}%`} icon={Users} colorClass="text-brand-yellow" /> 0 ? 'up' : trends.score < 0 ? 'down' : 'neutral'} trendValue={Math.abs(trends.score).toFixed(1)} icon={TrendingUp} colorClass="text-zinc-500" /> 0 ? 'down' : 'neutral'} // Faster response is better (up) trendValue={`${Math.abs(trends.resp).toFixed(1)}%`} icon={Clock} colorClass="text-orange-500" />
{/* Charts Section */}
{/* Funnel Chart */}

Funil de Vendas

[value, 'Quantidade']} contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)', backgroundColor: '#1a1a1a', color: '#ededed' }} itemStyle={{ color: '#ededed' }} /> {funnelData.map((entry, index) => ( ))}
{/* Origin Pie Chart */}

Origem dos Leads

{originData.map((entry, index) => ( ))} [value, 'Leads']} contentStyle={{ borderRadius: '12px', backgroundColor: '#1a1a1a', border: 'none', color: '#ededed' }} itemStyle={{ color: '#ededed' }} />
{/* Ranking Table */} {/* Product Lists */}
); };