import React from 'react'; import { TrendingUp, TrendingDown, AlertTriangle, CheckCircle, Target, Activity, Clock, PhoneForwarded, Users, Bot, ChevronRight, BarChart3, Cpu, Map, Zap, Calendar } from 'lucide-react'; import type { AnalysisData, Finding, DrilldownDataPoint, HeatmapDataPoint } from '../../types'; import type { TabId } from '../DashboardHeader'; import { Card, Badge, SectionHeader, DistributionBar, Stat, } from '../ui'; import { cn, COLORS, STATUS_CLASSES, getStatusFromScore, formatCurrency, formatNumber, formatPercent, } from '../../config/designSystem'; interface ExecutiveSummaryTabProps { data: AnalysisData; onTabChange?: (tab: TabId) => void; } // ============================================ // BENCHMARKS DE INDUSTRIA // ============================================ type IndustryKey = 'aerolineas' | 'telecomunicaciones' | 'banca' | 'utilities' | 'retail' | 'general'; interface BenchmarkMetric { p25: number; p50: number; p75: number; p90: number; unidad: string; invertida: boolean; } interface IndustryBenchmarks { nombre: string; fuente: string; metricas: { aht: BenchmarkMetric; fcr: BenchmarkMetric; abandono: BenchmarkMetric; cpi: BenchmarkMetric; }; } const BENCHMARKS_INDUSTRIA: Record = { aerolineas: { nombre: 'Aerolíneas', fuente: 'COPC 2024, Dimension Data Global CX Report 2024', metricas: { aht: { p25: 320, p50: 380, p75: 450, p90: 520, unidad: 's', invertida: true }, fcr: { p25: 55, p50: 68, p75: 78, p90: 85, unidad: '%', invertida: false }, abandono: { p25: 2, p50: 5, p75: 8, p90: 12, unidad: '%', invertida: true }, cpi: { p25: 2.20, p50: 3.50, p75: 4.50, p90: 5.50, unidad: '€', invertida: true } } }, telecomunicaciones: { nombre: 'Telecomunicaciones', fuente: 'Contact Babel UK Report 2024, ICMI Benchmark Study', metricas: { aht: { p25: 380, p50: 420, p75: 500, p90: 600, unidad: 's', invertida: true }, fcr: { p25: 50, p50: 65, p75: 75, p90: 82, unidad: '%', invertida: false }, abandono: { p25: 2, p50: 6, p75: 10, p90: 15, unidad: '%', invertida: true }, cpi: { p25: 2.50, p50: 4.00, p75: 5.00, p90: 6.00, unidad: '€', invertida: true } } }, banca: { nombre: 'Banca & Finanzas', fuente: 'Deloitte Banking Benchmark 2024, McKinsey CX Survey', metricas: { aht: { p25: 280, p50: 340, p75: 420, p90: 500, unidad: 's', invertida: true }, fcr: { p25: 58, p50: 72, p75: 82, p90: 88, unidad: '%', invertida: false }, abandono: { p25: 1, p50: 4, p75: 6, p90: 10, unidad: '%', invertida: true }, cpi: { p25: 2.80, p50: 4.50, p75: 6.00, p90: 7.50, unidad: '€', invertida: true } } }, utilities: { nombre: 'Utilities & Energía', fuente: 'Dimension Data 2024, Utilities CX Benchmark', metricas: { aht: { p25: 350, p50: 400, p75: 480, p90: 560, unidad: 's', invertida: true }, fcr: { p25: 52, p50: 67, p75: 77, p90: 84, unidad: '%', invertida: false }, abandono: { p25: 2, p50: 6, p75: 9, p90: 14, unidad: '%', invertida: true }, cpi: { p25: 2.00, p50: 3.30, p75: 4.20, p90: 5.20, unidad: '€', invertida: true } } }, retail: { nombre: 'Retail & E-commerce', fuente: 'Zendesk CX Trends 2024, Salesforce State of Service', metricas: { aht: { p25: 240, p50: 300, p75: 380, p90: 450, unidad: 's', invertida: true }, fcr: { p25: 60, p50: 73, p75: 82, p90: 89, unidad: '%', invertida: false }, abandono: { p25: 1, p50: 4, p75: 7, p90: 12, unidad: '%', invertida: true }, cpi: { p25: 1.60, p50: 2.80, p75: 3.80, p90: 4.80, unidad: '€', invertida: true } } }, general: { nombre: 'Cross-Industry', fuente: 'Dimension Data Global CX Benchmark 2024', metricas: { aht: { p25: 320, p50: 380, p75: 460, p90: 540, unidad: 's', invertida: true }, fcr: { p25: 55, p50: 70, p75: 80, p90: 87, unidad: '%', invertida: false }, abandono: { p25: 2, p50: 5, p75: 8, p90: 12, unidad: '%', invertida: true }, cpi: { p25: 2.20, p50: 3.50, p75: 4.50, p90: 5.50, unidad: '€', invertida: true } } } }; function calcularPercentilUsuario(valor: number, bench: BenchmarkMetric): number { const { p25, p50, p75, p90, invertida } = bench; if (invertida) { // For inverted metrics (lower is better, like AHT, CPI, Abandono): // p25 = best performers (lowest values), p90 = worst performers (highest values) // Check from best to worst if (valor <= p25) return 95; // Top 25% performers → beat 75%+ if (valor <= p50) return 60; // At or better than median → beat 50%+ if (valor <= p75) return 35; // Below median → beat 25%+ if (valor <= p90) return 15; // Poor → beat 10%+ return 5; // Very poor → beat <10% } else { // For normal metrics (higher is better, like FCR): // p90 = best performers (highest values), p25 = worst performers (lowest values) if (valor >= p90) return 95; if (valor >= p75) return 82; if (valor >= p50) return 60; if (valor >= p25) return 35; return 15; } } // ============================================ // PRINCIPALES HALLAZGOS // ============================================ interface Hallazgo { tipo: 'critico' | 'warning' | 'info'; texto: string; metrica?: string; } function generarHallazgos(data: AnalysisData): Hallazgo[] { const hallazgos: Hallazgo[] = []; const allQueues = data.drilldownData?.flatMap(s => s.originalQueues) || []; const totalVolume = allQueues.reduce((s, q) => s + q.volume, 0); // AHT promedio ponderado por volumen (usando aht_seconds = AHT limpio sin noise/zombies) const heatmapVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); const avgAHT = heatmapVolume > 0 ? data.heatmapData.reduce((sum, h) => sum + h.aht_seconds * h.volume, 0) / heatmapVolume : 0; // Alta variabilidad const colasAltaVariabilidad = allQueues.filter(q => q.cv_aht > 100); if (colasAltaVariabilidad.length > 0) { const pctVolumen = (colasAltaVariabilidad.reduce((s, q) => s + q.volume, 0) / totalVolume) * 100; hallazgos.push({ tipo: 'critico', texto: `${colasAltaVariabilidad.length} colas con variabilidad crítica (CV >100%) representan ${pctVolumen.toFixed(0)}% del volumen`, metrica: 'CV AHT' }); } // Alto transfer const colasAltoTransfer = allQueues.filter(q => q.transfer_rate > 25); if (colasAltoTransfer.length > 0) { hallazgos.push({ tipo: 'warning', texto: `${colasAltoTransfer.length} colas con tasa de transferencia >25% - posible problema de routing o formación`, metrica: 'Transfer' }); } // Bajo FCR (usar FCR Técnico para consistencia) const colasBajoFCR = allQueues.filter(q => (q.fcr_tecnico ?? (100 - q.transfer_rate)) < 50); if (colasBajoFCR.length > 0) { hallazgos.push({ tipo: 'warning', texto: `${colasBajoFCR.length} colas con FCR <50% - clientes requieren múltiples contactos`, metrica: 'FCR' }); } // AHT elevado vs benchmark if (avgAHT > 400) { hallazgos.push({ tipo: 'warning', texto: `AHT promedio de ${Math.round(avgAHT)}s supera el benchmark de industria (380s)`, metrica: 'AHT' }); } // Colas Human-Only const colasHumanOnly = allQueues.filter(q => q.tier === 'HUMAN-ONLY'); if (colasHumanOnly.length > 0) { const pctHuman = (colasHumanOnly.reduce((s, q) => s + q.volume, 0) / totalVolume) * 100; hallazgos.push({ tipo: 'info', texto: `${colasHumanOnly.length} colas (${pctHuman.toFixed(0)}% volumen) requieren intervención humana completa`, metrica: 'Tier' }); } // Oportunidad de automatización const colasAutomate = allQueues.filter(q => q.tier === 'AUTOMATE'); if (colasAutomate.length > 0) { hallazgos.push({ tipo: 'info', texto: `${colasAutomate.length} colas listas para automatización con potencial de ahorro significativo`, metrica: 'Oportunidad' }); } return hallazgos.slice(0, 5); // Máximo 5 hallazgos } function PrincipalesHallazgos({ data }: { data: AnalysisData }) { const hallazgos = generarHallazgos(data); if (hallazgos.length === 0) return null; const getIcono = (tipo: string) => { if (tipo === 'critico') return ; if (tipo === 'warning') return ; return ; }; const getClase = (tipo: string) => { if (tipo === 'critico') return 'bg-red-50 border-red-200'; if (tipo === 'warning') return 'bg-amber-50 border-amber-200'; return 'bg-blue-50 border-blue-200'; }; return (

Principales Hallazgos

{hallazgos.map((h, idx) => (
{getIcono(h.tipo)}

{h.texto}

{h.metrica && ( {h.metrica} )}
))}
); } // ============================================ // CABECERA CON PERIODO ANALIZADO // ============================================ function CabeceraPeriodo({ data }: { data: AnalysisData }) { const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); // Contar colas únicas (original_queue_id) desde drilldownData const uniqueQueues = data.drilldownData ? new Set(data.drilldownData.flatMap(d => d.originalQueues.map(q => q.original_queue_id))).size : data.heatmapData.length; // Contar líneas de negocio únicas (skills en drilldownData = queue_skill agrupado) const numLineasNegocio = data.drilldownData?.length || data.heatmapData.length; // Formatear fechas del periodo const formatPeriodo = () => { if (!data.dateRange?.min || !data.dateRange?.max) { return 'Periodo no especificado'; } const formatDate = (dateStr: string) => { try { const date = new Date(dateStr); return date.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' }); } catch { return dateStr; } }; return `${formatDate(data.dateRange.min)} - ${formatDate(data.dateRange.max)}`; }; return (
Periodo: {formatPeriodo()}
{formatNumber(totalInteractions)} int. {uniqueQueues} colas {numLineasNegocio} LN
); } // ============================================ // v3.15: HEADLINE EJECUTIVO (Situación) // ============================================ function HeadlineEjecutivo({ totalInteracciones, oportunidadTotal, eficienciaScore, resolucionScore, satisfaccionScore }: { totalInteracciones: number; oportunidadTotal: number; eficienciaScore: number; resolucionScore: number; satisfaccionScore: number; }) { const getStatusLabel = (score: number): string => { if (score >= 80) return 'Óptimo'; if (score >= 60) return 'Aceptable'; return 'Crítico'; }; const getStatusVariant = (score: number): 'success' | 'warning' | 'critical' => { if (score >= 80) return 'success'; if (score >= 60) return 'warning'; return 'critical'; }; return (
{/* Título principal */}

Tu operación procesa{' '} {formatNumber(totalInteracciones)}{' '} interacciones

con oportunidad de{' '} {formatCurrency(oportunidadTotal)} {' '} en optimización

{/* Status Bar */}
Eficiencia: {getStatusLabel(eficienciaScore)}
Resolución: {getStatusLabel(resolucionScore)}
Satisfacción: {getStatusLabel(satisfaccionScore)}
); } // v7.0: Unified KPI + Benchmark Card Component // Combines KeyMetricsCard + BenchmarkTable into single 3x2 card grid function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[] }) { const [selectedIndustry, setSelectedIndustry] = React.useState('aerolineas'); const benchmarks = BENCHMARKS_INDUSTRIA[selectedIndustry]; // Calculate volume-weighted metrics const totalVolume = heatmapData.reduce((sum, h) => sum + h.volume, 0); // FCR Técnico = sin transferencia (comparable con benchmarks) const fcrTecnico = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate)) * h.volume, 0) / totalVolume : 0; // FCR Real: sin transferencia Y sin recontacto 7d (más estricto) const fcrReal = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + h.metrics.fcr * h.volume, 0) / totalVolume : 0; // Volume-weighted AHT (usando aht_seconds = AHT limpio sin noise/zombies) const aht = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + h.aht_seconds * h.volume, 0) / totalVolume : 0; // Volume-weighted AHT Total (usando aht_total = AHT con TODAS las filas - solo informativo) const ahtTotal = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.aht_total ?? h.aht_seconds) * h.volume, 0) / totalVolume : 0; // CPI: usar el valor pre-calculado si existe, sino calcular desde annual_cost/cost_volume const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0); const totalAnnualCost = heatmapData.reduce((sum, h) => sum + (h.annual_cost || 0), 0); // Si tenemos CPI pre-calculado, usarlo ponderado por volumen // Si no, calcular desde annual_cost / cost_volume const hasCpiField = heatmapData.some(h => h.cpi !== undefined && h.cpi > 0); const cpi = hasCpiField ? (totalCostVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.cpi || 0) * (h.cost_volume || h.volume), 0) / totalCostVolume : 0) : (totalCostVolume > 0 ? totalAnnualCost / totalCostVolume : 0); // DEBUG: Log CPI calculation console.log('🔍 ExecutiveSummaryTab CPI:', `€${cpi.toFixed(2)}`, { hasCpiField, totalCostVolume }); // Volume-weighted metrics const operacion = { aht: aht, ahtTotal: ahtTotal, // AHT con TODAS las filas (solo informativo) fcrTecnico: fcrTecnico, fcrReal: fcrReal, abandono: totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.metrics.abandonment_rate || 0) * h.volume, 0) / totalVolume : 0, cpi: cpi }; // Calculate percentile position const getPercentileBadge = (percentile: number): { label: string; color: string } => { if (percentile >= 90) return { label: 'Top 10%', color: 'bg-emerald-500 text-white' }; if (percentile >= 75) return { label: 'Top 25%', color: 'bg-emerald-100 text-emerald-700' }; if (percentile >= 50) return { label: 'Promedio', color: 'bg-amber-100 text-amber-700' }; if (percentile >= 25) return { label: 'Bajo Avg', color: 'bg-orange-100 text-orange-700' }; return { label: 'Bottom 25%', color: 'bg-red-100 text-red-700' }; }; // Calculate GAP vs P50 - positive is better, negative is worse const calcularGap = (valor: number, bench: BenchmarkMetric): { gap: string; diff: number; isPositive: boolean } => { const diff = bench.invertida ? bench.p50 - valor : valor - bench.p50; const isPositive = diff > 0; if (bench.unidad === 's') { return { gap: `${isPositive ? '+' : ''}${Math.round(diff)}s`, diff, isPositive }; } else if (bench.unidad === '%') { return { gap: `${isPositive ? '+' : ''}${diff.toFixed(1)}pp`, diff, isPositive }; } else { return { gap: `${isPositive ? '+' : ''}€${Math.abs(diff).toFixed(2)}`, diff, isPositive }; } }; // Get card background color based on GAP type GapStatus = 'positive' | 'neutral' | 'negative'; const getGapStatus = (diff: number, bench: BenchmarkMetric): GapStatus => { // Calculate threshold as 5% of P50 const threshold = bench.p50 * 0.05; if (diff > threshold) return 'positive'; if (diff < -threshold) return 'negative'; return 'neutral'; }; const cardBgColors: Record = { positive: 'bg-emerald-50 border-emerald-200', neutral: 'bg-amber-50 border-amber-200', negative: 'bg-red-50 border-red-200' }; // Calculate position on visual scale (0-100) for the benchmark bar // 0 = worst performers, 100 = best performers const calcularPosicionVisual = (valor: number, bench: BenchmarkMetric): number => { const { p25, p50, p75, p90, invertida } = bench; if (invertida) { // For inverted metrics (lower is better): p25 < p50 < p75 < p90 // Better performance = lower value = higher visual position if (valor <= p25) return 95; // Best performers (top 25%) if (valor <= p50) return 50 + 45 * (p50 - valor) / (p50 - p25); // Between median and top if (valor <= p75) return 25 + 25 * (p75 - valor) / (p75 - p50); // Between p75 and median if (valor <= p90) return 5 + 20 * (p90 - valor) / (p90 - p75); // Between p90 and p75 return 5; // Worst performers (bottom 10%) } else { // For normal metrics (higher is better): p25 < p50 < p75 < p90 // Better performance = higher value = higher visual position if (valor >= p90) return 95; // Best performers (top 10%) if (valor >= p75) return 75 + 20 * (valor - p75) / (p90 - p75); if (valor >= p50) return 50 + 25 * (valor - p50) / (p75 - p50); if (valor >= p25) return 25 + 25 * (valor - p25) / (p50 - p25); return Math.max(5, 25 * valor / p25); // Worst performers } }; // Get insight text based on percentile position const getInsightText = (percentile: number, bench: BenchmarkMetric): string => { if (percentile >= 90) return `Superas al 90% del mercado`; if (percentile >= 75) return `Mejor que 3 de cada 4 empresas`; if (percentile >= 50) return `En línea con la mediana del sector`; if (percentile >= 25) return `Por debajo de la media del mercado`; return `Área crítica de mejora`; }; // Format benchmark value for display const formatBenchValue = (value: number, unidad: string): string => { if (unidad === 's') return `${Math.round(value)}s`; if (unidad === '%') return `${value}%`; return `€${value.toFixed(2)}`; }; // Metrics data with display values // FCR Real context: métrica más estricta que incluye recontactos 7 días const fcrRealDiff = operacion.fcrTecnico - operacion.fcrReal; const fcrRealContext = fcrRealDiff > 0 ? `${Math.round(fcrRealDiff)}pp de recontactos 7d` : null; // AHT Total context: diferencia entre AHT limpio y AHT con todas las filas const ahtTotalDiff = operacion.ahtTotal - operacion.aht; const ahtTotalContext = Math.abs(ahtTotalDiff) > 1 ? `${ahtTotalDiff > 0 ? '+' : ''}${Math.round(ahtTotalDiff)}s vs AHT limpio` : null; const metricsData = [ { id: 'aht', label: 'AHT', valor: operacion.aht, display: `${Math.floor(operacion.aht / 60)}:${String(Math.round(operacion.aht) % 60).padStart(2, '0')}`, subDisplay: `(${Math.round(operacion.aht)}s)`, bench: benchmarks.metricas.aht, tooltip: 'Tiempo medio de gestión (solo interacciones válidas)', // AHT Total integrado como métrica secundaria secondaryMetric: { label: 'AHT Total', value: `${Math.floor(operacion.ahtTotal / 60)}:${String(Math.round(operacion.ahtTotal) % 60).padStart(2, '0')} (${Math.round(operacion.ahtTotal)}s)`, note: ahtTotalContext, tooltip: 'Incluye todas las filas (noise, zombie, abandon) - solo informativo', description: 'Incluye noise, zombie y abandonos — solo informativo' } }, { id: 'fcr_tecnico', label: 'FCR', valor: operacion.fcrTecnico, display: `${Math.round(operacion.fcrTecnico)}%`, subDisplay: null, bench: benchmarks.metricas.fcr, tooltip: 'First Contact Resolution - comparable con benchmarks de industria', // FCR Real integrado como métrica secundaria secondaryMetric: { label: 'FCR Ajustado', value: `${Math.round(operacion.fcrReal)}%`, note: fcrRealContext, tooltip: 'Excluye recontactos en 7 días (métrica más estricta)', description: 'Incluye filtro de recontactos 7d — métrica interna más estricta' } }, { id: 'abandono', label: 'ABANDONO', valor: operacion.abandono, display: `${operacion.abandono.toFixed(1)}%`, subDisplay: null, bench: benchmarks.metricas.abandono, tooltip: 'Tasa de abandono', secondaryMetric: null }, { id: 'cpi', label: 'COSTE/INTERAC.', valor: operacion.cpi, display: `€${operacion.cpi.toFixed(2)}`, subDisplay: null, bench: benchmarks.metricas.cpi, tooltip: 'Coste por interacción', secondaryMetric: null } ]; return ( {/* Header with industry selector */}

Indicadores vs Industria

Fuente: {benchmarks.fuente}

{/* 2x2 Card Grid - McKinsey style */}
{metricsData.map((m) => { const percentil = calcularPercentilUsuario(m.valor, m.bench); const badge = getPercentileBadge(percentil); const { gap, diff, isPositive } = calcularGap(m.valor, m.bench); const gapStatus = getGapStatus(diff, m.bench); const posicionVisual = calcularPosicionVisual(m.valor, m.bench); const insightText = getInsightText(percentil, m.bench); return (
{/* Header: Label + Badge */}
{m.label}
{badge.label}
{/* Main Value + GAP */}
{m.display} {m.subDisplay && ( {m.subDisplay} )}
{gap} {isPositive ? '✓' : '✗'}
{/* Secondary Metric (FCR Real for FCR card, AHT Total for AHT card) */} {m.secondaryMetric && (
{m.secondaryMetric.label} {m.secondaryMetric.value}
{m.secondaryMetric.note && ( ({m.secondaryMetric.note}) )}
{m.secondaryMetric.description && (
{m.secondaryMetric.description}
)}
)} {/* Visual Benchmark Distribution Bar */}
{/* P25, P50, P75 markers */}
{/* User position indicator */}
{/* Scale labels */}
P25 P50 P75 P90
{/* Benchmark Reference Values */}
Bajo
{formatBenchValue(m.bench.p25, m.bench.unidad)}
Mediana
{formatBenchValue(m.bench.p50, m.bench.unidad)}
Top
{formatBenchValue(m.bench.p90, m.bench.unidad)}
{/* Insight Text */}
= 75 ? "text-emerald-700 bg-emerald-100/50" : percentil >= 50 ? "text-amber-700 bg-amber-100/50" : "text-red-700 bg-red-100/50" )}> {insightText}
); })}
); } // v6.0: Health Score - Simplified weighted average (no penalties) function HealthScoreDetailed({ score, avgFCR, avgAHT, avgAbandonmentRate, avgTransferRate }: { score: number; avgFCR: number; // FCR Técnico (%) avgAHT: number; // AHT en segundos avgAbandonmentRate: number; // Tasa de abandono (%) avgTransferRate: number; // Tasa de transferencia (%) }) { const getScoreColor = (s: number): string => { if (s >= 80) return COLORS.status.success; if (s >= 60) return COLORS.status.warning; return COLORS.status.critical; }; const getScoreLabel = (s: number): string => { if (s >= 80) return 'Excelente'; if (s >= 60) return 'Bueno'; if (s >= 40) return 'Regular'; return 'Crítico'; }; const color = getScoreColor(score); const circumference = 2 * Math.PI * 40; const strokeDasharray = `${(score / 100) * circumference} ${circumference}`; // ═══════════════════════════════════════════════════════════════ // Calcular scores normalizados usando benchmarks de industria // Misma lógica que calculateHealthScore() en realDataAnalysis.ts // ═══════════════════════════════════════════════════════════════ // FCR Técnico: P10=85%, P50=68%, P90=50% let fcrScore: number; if (avgFCR >= 85) { fcrScore = 95 + 5 * Math.min(1, (avgFCR - 85) / 15); } else if (avgFCR >= 68) { fcrScore = 50 + 50 * (avgFCR - 68) / (85 - 68); } else if (avgFCR >= 50) { fcrScore = 20 + 30 * (avgFCR - 50) / (68 - 50); } else { fcrScore = Math.max(0, 20 * avgFCR / 50); } // Abandono: P10=3%, P50=5%, P90=10% let abandonoScore: number; if (avgAbandonmentRate <= 3) { abandonoScore = 95 + 5 * Math.max(0, (3 - avgAbandonmentRate) / 3); } else if (avgAbandonmentRate <= 5) { abandonoScore = 50 + 45 * (5 - avgAbandonmentRate) / (5 - 3); } else if (avgAbandonmentRate <= 10) { abandonoScore = 20 + 30 * (10 - avgAbandonmentRate) / (10 - 5); } else { abandonoScore = Math.max(0, 20 - 2 * (avgAbandonmentRate - 10)); } // AHT: P10=240s, P50=380s, P90=540s let ahtScore: number; if (avgAHT <= 240) { if (avgFCR > 65) { ahtScore = 95 + 5 * Math.max(0, (240 - avgAHT) / 60); } else { ahtScore = 70; } } else if (avgAHT <= 380) { ahtScore = 50 + 45 * (380 - avgAHT) / (380 - 240); } else if (avgAHT <= 540) { ahtScore = 20 + 30 * (540 - avgAHT) / (540 - 380); } else { ahtScore = Math.max(0, 20 * (600 - avgAHT) / 60); } // CSAT Proxy: 60% FCR + 40% Abandono const csatProxyScore = 0.60 * fcrScore + 0.40 * abandonoScore; type FactorStatus = 'success' | 'warning' | 'critical'; const getFactorStatus = (s: number): FactorStatus => s >= 80 ? 'success' : s >= 50 ? 'warning' : 'critical'; // Nueva ponderación: FCR 35%, Abandono 30%, CSAT Proxy 20%, AHT 15% const factors = [ { name: 'FCR Técnico', weight: '35%', score: Math.round(fcrScore), status: getFactorStatus(fcrScore), insight: fcrScore >= 80 ? 'Óptimo' : fcrScore >= 50 ? 'En P50' : 'Bajo P90', rawValue: `${avgFCR.toFixed(0)}%` }, { name: 'Accesibilidad', weight: '30%', score: Math.round(abandonoScore), status: getFactorStatus(abandonoScore), insight: abandonoScore >= 80 ? 'Bajo' : abandonoScore >= 50 ? 'Moderado' : 'Crítico', rawValue: `${avgAbandonmentRate.toFixed(1)}% aband.` }, { name: 'CSAT Proxy', weight: '20%', score: Math.round(csatProxyScore), status: getFactorStatus(csatProxyScore), insight: csatProxyScore >= 80 ? 'Óptimo' : csatProxyScore >= 50 ? 'Mejorable' : 'Bajo', rawValue: '(FCR+Aband.)' }, { name: 'Eficiencia', weight: '15%', score: Math.round(ahtScore), status: getFactorStatus(ahtScore), insight: ahtScore >= 80 ? 'Rápido' : ahtScore >= 50 ? 'En rango' : 'Lento', rawValue: `${Math.floor(avgAHT / 60)}:${String(Math.round(avgAHT) % 60).padStart(2, '0')}` } ]; const statusBarColors: Record = { success: 'bg-emerald-500', warning: 'bg-amber-500', critical: 'bg-red-500' }; const statusTextColors: Record = { success: 'text-emerald-600', warning: 'text-amber-600', critical: 'text-red-600' }; // Score final = media ponderada (sin penalizaciones en v6.0) const finalScore = Math.round( fcrScore * 0.35 + abandonoScore * 0.30 + csatProxyScore * 0.20 + ahtScore * 0.15 ); const displayColor = getScoreColor(finalScore); const displayStrokeDasharray = `${(finalScore / 100) * circumference} ${circumference}`; return (
{/* Single Gauge: Final Score (weighted average) */}
{finalScore}

{getScoreLabel(finalScore)}

{/* Breakdown */}

Health Score

Benchmarks: FCR P10=85%, Aband. P10=3%, AHT P10=240s

{factors.map((factor) => (
{factor.name}
{factor.weight}
{factor.score}
{factor.rawValue}
))}
{/* Nota de cálculo */}

Score = FCR×35% + Accesibilidad×30% + CSAT Proxy×20% + Eficiencia×15%

); } // v3.16: Potencial de Automatización - Sin gauge confuso, solo distribución clara function AgenticReadinessScore({ data }: { data: AnalysisData }) { const allQueues = data.drilldownData?.flatMap(skill => skill.originalQueues) || []; const totalQueueVolume = allQueues.reduce((sum, q) => sum + q.volume, 0); // Calcular volúmenes por tier const tierVolumes = { AUTOMATE: allQueues.filter(q => q.tier === 'AUTOMATE').reduce((s, q) => s + q.volume, 0), ASSIST: allQueues.filter(q => q.tier === 'ASSIST').reduce((s, q) => s + q.volume, 0), AUGMENT: allQueues.filter(q => q.tier === 'AUGMENT').reduce((s, q) => s + q.volume, 0), 'HUMAN-ONLY': allQueues.filter(q => q.tier === 'HUMAN-ONLY').reduce((s, q) => s + q.volume, 0) }; const tierCounts = { AUTOMATE: allQueues.filter(q => q.tier === 'AUTOMATE').length, ASSIST: allQueues.filter(q => q.tier === 'ASSIST').length, AUGMENT: allQueues.filter(q => q.tier === 'AUGMENT').length, 'HUMAN-ONLY': allQueues.filter(q => q.tier === 'HUMAN-ONLY').length }; // Porcentajes por tier const tierPcts = { AUTOMATE: totalQueueVolume > 0 ? (tierVolumes.AUTOMATE / totalQueueVolume) * 100 : 0, ASSIST: totalQueueVolume > 0 ? (tierVolumes.ASSIST / totalQueueVolume) * 100 : 0, AUGMENT: totalQueueVolume > 0 ? (tierVolumes.AUGMENT / totalQueueVolume) * 100 : 0, 'HUMAN-ONLY': totalQueueVolume > 0 ? (tierVolumes['HUMAN-ONLY'] / totalQueueVolume) * 100 : 0 }; // Datos de tiers con descripción clara const tiers = [ { key: 'AUTOMATE', label: 'AUTOMATE', bgColor: 'bg-emerald-500', desc: 'Bot autónomo' }, { key: 'ASSIST', label: 'ASSIST', bgColor: 'bg-cyan-500', desc: 'Bot + agente' }, { key: 'AUGMENT', label: 'AUGMENT', bgColor: 'bg-amber-500', desc: 'Agente asistido' }, { key: 'HUMAN-ONLY', label: 'HUMAN', bgColor: 'bg-gray-400', desc: 'Solo humano' } ]; return (

Potencial de Automatización

{/* Distribución por tier */}
{tiers.map((tier) => { const pct = tierPcts[tier.key as keyof typeof tierPcts]; const count = tierCounts[tier.key as keyof typeof tierCounts]; const vol = tierVolumes[tier.key as keyof typeof tierVolumes]; return (
{tier.label}
{tier.desc}
{Math.round(pct)}%
{count} colas
); })}
{/* Resumen */}

{Math.round(tierPcts.AUTOMATE)}%

Automatización completa

{Math.round(tierPcts.AUTOMATE + tierPcts.ASSIST)}%

Con asistencia IA

Basado en {formatNumber(totalQueueVolume)} interacciones analizadas

); } // Top Opportunities Component (legacy - kept for reference) function TopOpportunities({ findings, opportunities }: { findings: Finding[]; opportunities: { name: string; impact: number; savings: number }[]; }) { const items = [ ...findings .filter(f => f.type === 'critical' || f.type === 'warning') .slice(0, 3) .map((f, i) => ({ rank: i + 1, title: f.title || f.text.split(':')[0], metric: f.text.includes(':') ? f.text.split(':')[1].trim() : '', action: f.description || 'Acción requerida', type: f.type as 'critical' | 'warning' | 'info' })), ].slice(0, 3); if (items.length < 3) { const remaining = 3 - items.length; opportunities .sort((a, b) => b.savings - a.savings) .slice(0, remaining) .forEach(() => { const opp = opportunities[items.length]; if (opp) { items.push({ rank: items.length + 1, title: opp.name, metric: `€${opp.savings.toLocaleString()} ahorro potencial`, action: 'Implementar', type: 'info' as const }); } }); } const getIcon = (type: string) => { if (type === 'critical') return ; if (type === 'warning') return ; return ; }; return (

Top 3 Oportunidades

{items.map((item) => (
{item.rank}
{getIcon(item.type)} {item.title}
{item.metric && (

{item.metric}

)}

→ {item.action}

))}
); } // v3.15: Economic Summary Compact function EconomicSummary({ economicModel }: { economicModel: AnalysisData['economicModel'] }) { return (

Impacto Económico

ROI 3 años

{economicModel.roi3yr}%

Payback

{economicModel.paybackMonths}m

); } export function ExecutiveSummaryTab({ data, onTabChange }: ExecutiveSummaryTabProps) { // Métricas básicas - VOLUME-WEIGHTED para consistencia con calculateHealthScore() const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); // AHT ponderado por volumen (usando aht_seconds = AHT limpio sin noise/zombies) const avgAHT = totalInteractions > 0 ? data.heatmapData.reduce((sum, h) => sum + h.aht_seconds * h.volume, 0) / totalInteractions : 0; // FCR Técnico: solo sin transferencia (comparable con benchmarks de industria) - ponderado por volumen const avgFCRTecnico = totalInteractions > 0 ? data.heatmapData.reduce((sum, h) => sum + (h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate)) * h.volume, 0) / totalInteractions : 0; // Transfer rate ponderado por volumen const avgTransferRate = totalInteractions > 0 ? data.heatmapData.reduce((sum, h) => sum + h.metrics.transfer_rate * h.volume, 0) / totalInteractions : 0; // Abandonment rate ponderado por volumen const avgAbandonmentRate = totalInteractions > 0 ? data.heatmapData.reduce((sum, h) => sum + (h.metrics.abandonment_rate || 0) * h.volume, 0) / totalInteractions : 0; // DEBUG: Validar métricas GLOBALES calculadas (ponderadas por volumen) console.log('📊 ExecutiveSummaryTab - MÉTRICAS GLOBALES MOSTRADAS:', { totalInteractions, avgFCRTecnico: avgFCRTecnico.toFixed(2) + '%', avgTransferRate: avgTransferRate.toFixed(2) + '%', avgAbandonmentRate: avgAbandonmentRate.toFixed(2) + '%', avgAHT: Math.round(avgAHT) + 's', // Detalle por skill para verificación perSkill: data.heatmapData.map(h => ({ skill: h.skill, vol: h.volume, fcr_tecnico: h.metrics?.fcr_tecnico, transfer: h.metrics?.transfer_rate })) }); // Métricas para navegación const allQueues = data.drilldownData?.flatMap(s => s.originalQueues) || []; const colasAutomate = allQueues.filter(q => q.tier === 'AUTOMATE'); const ahorroTotal = data.economicModel?.annualSavings || 0; const dimensionesConProblemas = data.dimensions.filter(d => d.score < 60).length; return (
{/* ======================================== 1. CABECERA CON PERIODO ======================================== */} {/* ======================================== 2. KPIs + BENCHMARK (Unified Card Grid) ======================================== */} {/* ======================================== 3. HEALTH SCORE ======================================== */} {/* ======================================== 4. PRINCIPALES HALLAZGOS ======================================== */} {/* ======================================== 5. NAVEGACIÓN RÁPIDA (Explorar más) ======================================== */} {onTabChange && (

Explorar análisis detallado

{/* Dimensiones */} {/* Agentic Readiness */} {/* Plan de Acción */}
)}
); } export default ExecutiveSummaryTab;