import { useEffect, useMemo, useState } from 'react'; import ReactECharts from 'echarts-for-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUsers, faPhoneVolume, faCalendarCheck, faPhoneMissed, faPercent, faTriangleExclamation, } from '@fortawesome/pro-duotone-svg-icons'; import { TopBar } from '@/components/layout/top-bar'; import { Badge } from '@/components/base/badges/badges'; import { Table } from '@/components/application/table/table'; import { apiClient } from '@/lib/api-client'; import { cx } from '@/utils/cx'; type DateRange = 'today' | 'week' | 'month' | 'year'; const getDateRange = (range: DateRange): { gte: string; lte: string } => { const now = new Date(); const lte = now.toISOString(); const start = new Date(now); if (range === 'today') start.setHours(0, 0, 0, 0); else if (range === 'week') { start.setDate(start.getDate() - start.getDay() + 1); start.setHours(0, 0, 0, 0); } else if (range === 'month') { start.setDate(1); start.setHours(0, 0, 0, 0); } else if (range === 'year') { start.setMonth(0, 1); start.setHours(0, 0, 0, 0); } return { gte: start.toISOString(), lte }; }; const parseTime = (timeStr: string): number => { if (!timeStr) return 0; const parts = timeStr.split(':').map(Number); if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; return 0; }; type AgentPerf = { name: string; ozonetelagentid: string; npsscore: number | null; maxidleminutes: number | null; minnpsthreshold: number | null; minconversionpercent: number | null; calls: number; inbound: number; missed: number; followUps: number; leads: number; appointments: number; convPercent: number; idleMinutes: number; activeMinutes: number; wrapMinutes: number; breakMinutes: number; timeBreakdown: any; }; const DateFilter = ({ value, onChange }: { value: DateRange; onChange: (v: DateRange) => void }) => (
{(['today', 'week', 'month', 'year'] as DateRange[]).map(r => ( ))}
); const KpiCard = ({ icon, value, label, color }: { icon: any; value: string | number; label: string; color?: string }) => (

{value}

{label}

); export const TeamPerformancePage = () => { const [range, setRange] = useState('week'); const [agents, setAgents] = useState([]); const [allCalls, setAllCalls] = useState([]); const [allAppointments, setAllAppointments] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const load = async () => { setLoading(true); const { gte, lte } = getDateRange(range); const dateStr = new Date().toISOString().split('T')[0]; try { const [callsData, apptsData, leadsData, followUpsData, teamData] = await Promise.all([ apiClient.graphql(`{ calls(first: 500, filter: { startedAt: { gte: "${gte}", lte: "${lte}" } }) { edges { node { id direction callStatus agentName startedAt } } } }`, undefined, { silent: true }), apiClient.graphql(`{ appointments(first: 200, filter: { scheduledAt: { gte: "${gte}", lte: "${lte}" } }) { edges { node { id status } } } }`, undefined, { silent: true }), apiClient.graphql(`{ leads(first: 200) { edges { node { id assignedAgent status } } } }`, undefined, { silent: true }), apiClient.graphql(`{ followUps(first: 200) { edges { node { id assignedAgent } } } }`, undefined, { silent: true }), apiClient.get(`/api/supervisor/team-performance?date=${dateStr}`, { silent: true }).catch(() => ({ agents: [] })), ]); const calls = callsData?.calls?.edges?.map((e: any) => e.node) ?? []; const appts = apptsData?.appointments?.edges?.map((e: any) => e.node) ?? []; const leads = leadsData?.leads?.edges?.map((e: any) => e.node) ?? []; const followUps = followUpsData?.followUps?.edges?.map((e: any) => e.node) ?? []; const teamAgents = teamData?.agents ?? []; setAllCalls(calls); setAllAppointments(appts); // Build per-agent metrics let agentPerfs: AgentPerf[]; if (teamAgents.length > 0) { // Real Ozonetel data available agentPerfs = teamAgents.map((agent: any) => { const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelagentid); const agentLeads = leads.filter((l: any) => l.assignedAgent === agent.name); const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === agent.name); const agentAppts = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length; const totalCalls = agentCalls.length; const inbound = agentCalls.filter((c: any) => c.direction === 'INBOUND').length; const missed = agentCalls.filter((c: any) => c.callStatus === 'MISSED').length; const tb = agent.timeBreakdown; const idleSec = tb ? parseTime(tb.totalIdleTime ?? '0:0:0') : 0; const activeSec = tb ? parseTime(tb.totalBusyTime ?? '0:0:0') : 0; const wrapSec = tb ? parseTime(tb.totalWrapupTime ?? '0:0:0') : 0; const breakSec = tb ? parseTime(tb.totalPauseTime ?? '0:0:0') : 0; return { name: agent.name ?? agent.ozonetelagentid, ozonetelagentid: agent.ozonetelagentid, npsscore: agent.npsscore, maxidleminutes: agent.maxidleminutes, minnpsthreshold: agent.minnpsthreshold, minconversionpercent: agent.minconversionpercent, calls: totalCalls, inbound, missed, followUps: agentFollowUps.length, leads: agentLeads.length, appointments: agentAppts, convPercent: totalCalls > 0 ? Math.round((agentAppts / totalCalls) * 100) : 0, idleMinutes: Math.round(idleSec / 60), activeMinutes: Math.round(activeSec / 60), wrapMinutes: Math.round(wrapSec / 60), breakMinutes: Math.round(breakSec / 60), timeBreakdown: tb, }; }); } else { // Fallback: build agent list from call records const agentNames = [...new Set(calls.map((c: any) => c.agentName).filter(Boolean))] as string[]; agentPerfs = agentNames.map((name) => { const agentCalls = calls.filter((c: any) => c.agentName === name); const agentLeads = leads.filter((l: any) => l.assignedAgent === name); const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === name); const completed = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length; const totalCalls = agentCalls.length; return { name, ozonetelagentid: name, npsscore: null, maxidleminutes: null, minnpsthreshold: null, minconversionpercent: null, calls: totalCalls, inbound: agentCalls.filter((c: any) => c.direction === 'INBOUND').length, missed: agentCalls.filter((c: any) => c.callStatus === 'MISSED').length, followUps: agentFollowUps.length, leads: agentLeads.length, appointments: completed, convPercent: totalCalls > 0 ? Math.round((completed / totalCalls) * 100) : 0, idleMinutes: 0, activeMinutes: 0, wrapMinutes: 0, breakMinutes: 0, timeBreakdown: null, }; }); } setAgents(agentPerfs); } catch (err) { console.error('Failed to load team performance:', err); } finally { setLoading(false); } }; load(); }, [range]); // Aggregate KPIs const totalCalls = allCalls.length; const totalMissed = allCalls.filter(c => c.callStatus === 'MISSED').length; const totalAppts = allAppointments.length; const convRate = totalCalls > 0 ? Math.round((totalAppts / totalCalls) * 100) : 0; const activeAgents = agents.length; // Call trend by day const callTrendOption = useMemo(() => { const dayMap: Record = {}; for (const c of allCalls) { if (!c.startedAt) continue; const day = new Date(c.startedAt).toLocaleDateString('en-IN', { weekday: 'short' }); if (!dayMap[day]) dayMap[day] = { inbound: 0, outbound: 0 }; if (c.direction === 'INBOUND') dayMap[day].inbound++; else dayMap[day].outbound++; } const days = Object.keys(dayMap); return { tooltip: { trigger: 'axis' }, legend: { data: ['Inbound', 'Outbound'], bottom: 0, textStyle: { fontSize: 11 } }, grid: { top: 10, right: 10, bottom: 50, left: 40 }, xAxis: { type: 'category', data: days }, yAxis: { type: 'value' }, series: [ { name: 'Inbound', type: 'line', data: days.map(d => dayMap[d].inbound), smooth: true, color: '#2060A0' }, { name: 'Outbound', type: 'line', data: days.map(d => dayMap[d].outbound), smooth: true, color: '#E88C30' }, ], }; }, [allCalls]); // NPS const avgNps = useMemo(() => { const withNps = agents.filter(a => a.npsscore != null); if (withNps.length === 0) return 0; return Math.round(withNps.reduce((sum, a) => sum + (a.npsscore ?? 0), 0) / withNps.length); }, [agents]); const npsOption = useMemo(() => ({ tooltip: { trigger: 'item' }, series: [{ type: 'gauge', startAngle: 180, endAngle: 0, min: 0, max: 100, pointer: { show: false }, progress: { show: true, width: 18, roundCap: true, itemStyle: { color: avgNps >= 70 ? '#22C55E' : avgNps >= 50 ? '#F59E0B' : '#EF4444' } }, axisLine: { lineStyle: { width: 18, color: [[1, '#E5E7EB']] } }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false }, detail: { valueAnimation: true, fontSize: 28, fontWeight: 'bold', offsetCenter: [0, '-10%'], formatter: '{value}' }, data: [{ value: avgNps }], }], }), [avgNps]); // Performance alerts const alerts = useMemo(() => { const list: { agent: string; type: string; value: string; severity: 'error' | 'warning' }[] = []; for (const a of agents) { if (a.maxidleminutes && a.idleMinutes > a.maxidleminutes) { list.push({ agent: a.name, type: 'Excessive Idle Time', value: `${a.idleMinutes}m`, severity: 'error' }); } if (a.minnpsthreshold && (a.npsscore ?? 100) < a.minnpsthreshold) { list.push({ agent: a.name, type: 'Low NPS', value: String(a.npsscore ?? 0), severity: 'warning' }); } if (a.minconversionpercent && a.convPercent < a.minconversionpercent) { list.push({ agent: a.name, type: 'Low Conversion', value: `${a.convPercent}%`, severity: 'warning' }); } } return list; }, [agents]); // Team time averages const teamAvg = useMemo(() => { if (agents.length === 0) return { active: 0, wrap: 0, idle: 0, break_: 0 }; return { active: Math.round(agents.reduce((s, a) => s + a.activeMinutes, 0) / agents.length), wrap: Math.round(agents.reduce((s, a) => s + a.wrapMinutes, 0) / agents.length), idle: Math.round(agents.reduce((s, a) => s + a.idleMinutes, 0) / agents.length), break_: Math.round(agents.reduce((s, a) => s + a.breakMinutes, 0) / agents.length), }; }, [agents]); if (loading) { return ( <>

Loading team performance...

); } return ( <>
{/* Section 1: Key Metrics */}

Key Metrics

{/* Section 2: Call Breakdown Trends */}

Call Breakdown Trends

Inbound vs Outbound

{/* Section 3: Agent Performance Table */}

Agent Performance

{(agent) => ( {agent.name} {agent.calls} {agent.inbound} {agent.missed} {agent.followUps} {agent.leads} = 25 ? 'text-success-primary' : 'text-error-primary')}> {agent.convPercent}% = 70 ? 'text-success-primary' : (agent.npsscore ?? 0) >= 50 ? 'text-warning-primary' : 'text-error-primary')}> {agent.npsscore ?? '—'} agent.maxidleminutes ? 'text-error-primary font-bold' : 'text-primary')}> {agent.idleMinutes}m )}
{/* Section 4: Time Breakdown */}

Time Breakdown

{teamAvg.active === 0 && teamAvg.idle === 0 && teamAvg.wrap === 0 && teamAvg.break_ === 0 && (

Time utilisation data unavailable — requires Ozonetel agent session data.

)}
{teamAvg.active}m Active
{teamAvg.wrap}m Wrap
{teamAvg.idle}m Idle
{teamAvg.break_}m Break
{agents.map(agent => { const total = agent.activeMinutes + agent.wrapMinutes + agent.idleMinutes + agent.breakMinutes || 1; const isHighIdle = agent.maxidleminutes && agent.idleMinutes > agent.maxidleminutes; return (

{agent.name}

Active {agent.activeMinutes}m Wrap {agent.wrapMinutes}m Idle {agent.idleMinutes}m Break {agent.breakMinutes}m
); })}
{/* Section 5: NPS + Conversion */}

Overall NPS

{agents.every(a => a.npsscore == null) ? (

NPS data unavailable — configure NPS scores on agent profiles.

) : ( <>
{agents.filter(a => a.npsscore != null).map(a => (
{a.name}
= 70 ? 'bg-success-solid' : (a.npsscore ?? 0) >= 50 ? 'bg-warning-solid' : 'bg-error-solid')} style={{ width: `${a.npsscore ?? 0}%` }} />
{a.npsscore}
))}
)}

Conversion Metrics

{convRate}%

Call → Appointment

{agents.length > 0 ? Math.round(agents.reduce((s, a) => s + (a.leads > 0 ? 1 : 0), 0) / agents.length * 100) : 0}%

Lead → Contact

{agents.map(a => (
{a.name} = 25 ? 'success' : 'error'}>{a.convPercent}%
))}
{/* Section 6: Performance Alerts */} {alerts.length > 0 && (

Performance Alerts ({alerts.length})

{alerts.map((alert, i) => (
{alert.agent} — {alert.type}
{alert.value}
))}
)}
); };