import React, { useState } from 'react'; import { motion } from 'framer-motion'; import { Bot, Zap, Brain, Activity, ChevronRight, Info, ChevronDown, ChevronUp, TrendingUp, BarChart2, Target, Repeat, AlertTriangle, Users, Sparkles, XCircle, AlertOctagon, ShieldAlert } from 'lucide-react'; import type { AnalysisData, HeatmapDataPoint, SubFactor, DrilldownDataPoint, OriginalQueueMetrics, AgenticTier, AgenticScoreBreakdown } from '../../types'; import { Card, Badge, TierBadge, SectionHeader, DistributionBar, Collapsible, } from '../ui'; import { cn, COLORS, STATUS_CLASSES, TIER_CLASSES, getStatusFromScore, formatCurrency, formatNumber, formatPercent, } from '../../config/designSystem'; import { useTranslation } from 'react-i18next'; // ============================================ // RED FLAGS CONFIGURATION AND DETECTION // ============================================ // v3.5: Red Flags Configuration interface RedFlagConfig { id: string; label: string; shortLabel: string; threshold: number; operator: '>' | '<'; getValue: (queue: OriginalQueueMetrics) => number; format: (value: number) => string; color: string; description: string; } // Note: These configs will be translated in the component using t() function const getRedFlagConfigs = (t: any): RedFlagConfig[] => [ { id: 'cv_high', label: t('agenticReadiness.redFlags.cvCritical'), shortLabel: t('agenticReadiness.redFlags.cvCriticalShort'), threshold: 120, operator: '>', getValue: (q) => q.cv_aht, format: (v) => `${v.toFixed(0)}%`, color: 'red', description: t('agenticReadiness.redFlags.cvCriticalDesc') }, { id: 'transfer_high', label: t('agenticReadiness.redFlags.transferExcessive'), shortLabel: t('agenticReadiness.redFlags.transferExcessiveShort'), threshold: 50, operator: '>', getValue: (q) => q.transfer_rate, format: (v) => `${v.toFixed(0)}%`, color: 'orange', description: t('agenticReadiness.redFlags.transferExcessiveDesc') }, { id: 'volume_low', label: t('agenticReadiness.redFlags.volumeInsufficient'), shortLabel: t('agenticReadiness.redFlags.volumeInsufficientShort'), threshold: 50, operator: '<', getValue: (q) => q.volume, format: (v) => v.toLocaleString(), color: 'slate', description: t('agenticReadiness.redFlags.volumeInsufficientDesc') }, { id: 'valid_low', label: t('agenticReadiness.redFlags.dataQualityLow'), shortLabel: t('agenticReadiness.redFlags.dataQualityLowShort'), threshold: 30, operator: '<', getValue: (q) => q.volume > 0 ? (q.volumeValid / q.volume) * 100 : 0, format: (v) => `${v.toFixed(0)}%`, color: 'amber', description: t('agenticReadiness.redFlags.dataQualityLowDesc') } ]; // v3.5: Detect red flags for a queue interface DetectedRedFlag { config: RedFlagConfig; value: number; } function detectRedFlags(queue: OriginalQueueMetrics, configs: RedFlagConfig[]): DetectedRedFlag[] { const flags: DetectedRedFlag[] = []; for (const config of configs) { const value = config.getValue(queue); const hasFlag = config.operator === '>' ? value > config.threshold : value < config.threshold; if (hasFlag) { flags.push({ config, value }); } } return flags; } // v3.5: Individual Red Flag badge component function RedFlagBadge({ flag, size = 'sm', t }: { flag: DetectedRedFlag; size?: 'sm' | 'md'; t: any }) { const sizeClasses = size === 'md' ? 'px-2 py-1 text-xs' : 'px-1.5 py-0.5 text-[10px]'; return ( {flag.config.shortLabel}: {flag.config.format(flag.value)} ); } // v3.5: Componente de lista de Red Flags de una cola function RedFlagsList({ queue, compact = false, configs, t }: { queue: OriginalQueueMetrics; compact?: boolean; configs: RedFlagConfig[]; t: any }) { const flags = detectRedFlags(queue, configs); if (flags.length === 0) return null; if (compact) { return (
{flags.map(flag => ( ))}
); } return (
{flags.map(flag => (
{flag.config.label}: {flag.config.format(flag.value)} ({t('agenticReadiness.redFlags.threshold', { operator: flag.config.operator, value: flag.config.threshold })})
))}
); } interface AgenticReadinessTabProps { data: AnalysisData; onTabChange?: (tab: string) => void; } // ============================================ // METHODOLOGY INTRODUCTION SECTION // ============================================ interface TierExplanation { tier: AgenticTier; label: string; emoji: string; color: string; bgColor: string; description: string; criteria: string; recommendation: string; } const getTierExplanations = (t: any): TierExplanation[] => [ { tier: 'AUTOMATE', label: t('agenticReadiness.automatable'), emoji: '🤖', color: '#10b981', bgColor: '#d1fae5', description: t('agenticReadiness.automatableDesc'), criteria: t('agenticReadiness.automatableCriteria'), recommendation: t('agenticReadiness.automatableAction') }, { tier: 'ASSIST', label: t('agenticReadiness.assistable'), emoji: '🤝', color: '#3b82f6', bgColor: '#dbeafe', description: t('agenticReadiness.assistableDesc'), criteria: t('agenticReadiness.assistableCriteria'), recommendation: t('agenticReadiness.assistableAction') }, { tier: 'AUGMENT', label: t('agenticReadiness.optimizable'), emoji: '📚', color: '#f59e0b', bgColor: '#fef3c7', description: t('agenticReadiness.optimizableDesc'), criteria: t('agenticReadiness.optimizableCriteria'), recommendation: t('agenticReadiness.optimizableAction') }, { tier: 'HUMAN-ONLY', label: t('agenticReadiness.humanOnly'), emoji: '👤', color: '#6b7280', bgColor: '#f3f4f6', description: t('agenticReadiness.humanOnlyDesc'), criteria: t('agenticReadiness.humanOnlyCriteria'), recommendation: t('agenticReadiness.humanOnlyAction') } ]; function AgenticMethodologyIntro({ tierData, totalVolume, totalQueues }: { tierData: TierDataType; totalVolume: number; totalQueues: number; }) { const [isExpanded, setIsExpanded] = useState(false); // Calcular estadísticas para el roadmap const automatizableQueues = tierData.AUTOMATE.count + tierData.ASSIST.count; const optimizableQueues = tierData.AUGMENT.count; const humanOnlyQueues = tierData['HUMAN-ONLY'].count; const automatizablePct = totalVolume > 0 ? Math.round((tierData.AUTOMATE.volume + tierData.ASSIST.volume) / totalVolume * 100) : 0; return ( {/* Header con toggle */}
setIsExpanded(!isExpanded)} >

¿Qué es el Índice de Agentic Readiness?

Metodología de evaluación y guía de navegación de este análisis

{/* Contenido expandible */} {isExpanded && (
{/* Sección 1: Definición del índice */}

Definición del Índice

El Índice de Agentic Readiness evalúa qué porcentaje del volumen de interacciones está preparado para ser gestionado por agentes virtuales o asistido por IA. Se calcula analizando cada cola individualmente según 5 factores clave:

Predictibilidad
30% peso
CV AHT <75%
Resolutividad
25% peso
FCR alto, Transfer bajo
Volumen
25% peso
ROI positivo >500/mes
Calidad Datos
10% peso
% registros válidos
Simplicidad
10% peso
AHT bajo, proceso simple
{/* Sección 2: Los 4 Tiers explicados */}

Las 4 Categorías de Clasificación

Cada cola se clasifica en uno de los siguientes tiers según su score compuesto:

{TIER_EXPLANATIONS.map(tier => (
{tier.emoji} {tier.label} {tier.tier}

{tier.description}

Criterios: {tier.criteria}
Acción: {tier.recommendation}
))}
{/* Sección 3: Roadmap de navegación */}

Contenido de este Análisis

Este tab presenta el análisis de automatización en el siguiente orden:

{/* Paso 1 */}
1
Visión Global de Distribución

Porcentaje de volumen en cada categoría ({automatizablePct}% automatizable). Las 4 cajas muestran cómo se distribuyen las {totalVolume.toLocaleString()} interacciones.

{/* Paso 2 */}
2
Candidatos Prioritarios {automatizableQueues} colas

Colas AUTOMATE y ASSIST ordenadas por potencial de ahorro. Quick wins con mayor ROI para priorizar en el roadmap.

{/* Paso 3 */}
3
Colas a Optimizar {optimizableQueues} colas

Tier AUGMENT: requieren estandarización previa (reducir variabilidad, mejorar FCR, documentar procesos) antes de automatizar.

{/* Paso 4 */}
4
No Automatizables {humanOnlyQueues} colas

Tier HUMAN-ONLY: volumen insuficiente (ROI negativo), calidad de datos baja, variabilidad extrema, o complejidad que requiere juicio humano.

{/* Nota metodológica */}
Nota metodológica: El índice se calcula por cola individual, no como promedio global. Esto permite identificar oportunidades específicas incluso cuando la media operativa sea baja. Los umbrales están calibrados según benchmarks de industria (COPC, Gartner).
)} {/* Mini resumen cuando está colapsado */} {!isExpanded && (
5 factores ponderados 4 categorías de clasificación {totalQueues} colas analizadas Click para expandir metodología
)}
); } // Factor configuration with weights (must sum to 1.0) interface FactorConfig { id: string; title: string; weight: number; icon: React.ElementType; color: string; description: string; methodology: string; benchmark: string; implications: { high: string; low: string }; } const FACTOR_CONFIGS: FactorConfig[] = [ { id: 'predictibilidad', title: 'Predictibilidad', weight: 0.30, icon: Brain, color: '#6D84E3', description: 'Consistencia en tiempos de gestión', methodology: 'Score = 10 - (CV_AHT / 10). CV AHT < 30% → Score > 7', benchmark: 'CV AHT óptimo < 25%', implications: { high: 'Tiempos consistentes, ideal para IA', low: 'Requiere estandarización' } }, { id: 'complejidad_inversa', title: 'Simplicidad', weight: 0.20, icon: Zap, color: '#10B981', description: 'Bajo nivel de juicio humano requerido', methodology: 'Score = 10 - (Tasa_Transfer × 0.4). Transfer <10% → Score > 6', benchmark: 'Transferencias óptimas <10%', implications: { high: 'Procesos simples, automatizables', low: 'Alta complejidad, requiere copilot' } }, { id: 'repetitividad', title: 'Volumen', weight: 0.25, icon: Repeat, color: '#F59E0B', description: 'Escala para justificar inversión', methodology: 'Score = log10(Volumen) normalizado. >5000 → 10, <100 → 2', benchmark: 'ROI positivo requiere >500/mes', implications: { high: 'Alto volumen justifica inversión', low: 'Considerar soluciones compartidas' } }, { id: 'roi_potencial', title: 'ROI Potencial', weight: 0.25, icon: TrendingUp, color: '#8B5CF6', description: 'Retorno económico esperado', methodology: 'Score basado en coste anual total. >€500K → 10', benchmark: 'ROI >150% a 12 meses', implications: { high: 'Caso de negocio sólido', low: 'ROI marginal, evaluar otros beneficios' } } ]; // v3.4: Helper para obtener estilo de Tier function getTierStyle(tier: AgenticTier): { bg: string; text: string; icon: React.ReactNode; label: string } { switch (tier) { case 'AUTOMATE': return { bg: 'bg-emerald-100', text: 'text-emerald-700', icon: , label: 'Automatizar' }; case 'ASSIST': return { bg: 'bg-blue-100', text: 'text-blue-700', icon: , label: 'Asistir' }; case 'AUGMENT': return { bg: 'bg-amber-100', text: 'text-amber-700', icon: , label: 'Optimizar' }; case 'HUMAN-ONLY': return { bg: 'bg-gray-100', text: 'text-gray-600', icon: , label: 'Humano' }; default: return { bg: 'bg-gray-100', text: 'text-gray-600', icon: null, label: tier }; } } // v3.4: Componente de desglose de score function ScoreBreakdownTooltip({ breakdown }: { breakdown: AgenticScoreBreakdown }) { return (
Predictibilidad (30%) {breakdown.predictibilidad.toFixed(1)}
Resolutividad (25%) {breakdown.resolutividad.toFixed(1)}
Volumen (25%) {breakdown.volumen.toFixed(1)}
Calidad Datos (10%) {breakdown.calidadDatos.toFixed(1)}
Simplicidad (10%) {breakdown.simplicidad.toFixed(1)}
); } // Tooltip component for methodology function InfoTooltip({ content, children }: { content: React.ReactNode; children: React.ReactNode }) { const [isVisible, setIsVisible] = useState(false); return (
setIsVisible(true)} onMouseLeave={() => setIsVisible(false)} className="cursor-help" > {children}
{isVisible && (
{content}
)}
); } // Calcular factores desde datos reales function calculateFactorsFromData(heatmapData: HeatmapDataPoint[]): { id: string; score: number; detail: string }[] { if (heatmapData.length === 0) return []; const totalVolume = heatmapData.reduce((sum, h) => sum + h.volume, 0) || 1; // Predictibilidad: basada en CV AHT promedio ponderado const avgCvAht = heatmapData.reduce((sum, h) => sum + (h.variability.cv_aht * h.volume), 0) / totalVolume; const predictScore = Math.max(0, Math.min(10, 10 - (avgCvAht / 10))); // Simplicidad: basada en tasa de transferencias promedio ponderada const avgTransfer = heatmapData.reduce((sum, h) => sum + (h.variability.transfer_rate * h.volume), 0) / totalVolume; const simplicityScore = Math.max(0, Math.min(10, 10 - (avgTransfer * 0.4))); // Volumen: basado en volumen total (escala logarítmica) const volScore = totalVolume > 50000 ? 10 : totalVolume > 20000 ? 9 : totalVolume > 10000 ? 8 : totalVolume > 5000 ? 7 : totalVolume > 2000 ? 6 : totalVolume > 1000 ? 5 : totalVolume > 500 ? 4 : totalVolume > 100 ? 3 : 2; // ROI potencial: basado en coste anual total const totalCost = heatmapData.reduce((sum, h) => sum + (h.annual_cost || h.volume * h.aht_seconds * 0.005), 0); const roiScore = totalCost > 1000000 ? 10 : totalCost > 500000 ? 9 : totalCost > 300000 ? 8 : totalCost > 200000 ? 7 : totalCost > 100000 ? 6 : totalCost > 50000 ? 5 : 4; return [ { id: 'predictibilidad', score: predictScore, detail: `CV AHT: ${avgCvAht.toFixed(0)}%` }, { id: 'complejidad_inversa', score: simplicityScore, detail: `Transfer: ${avgTransfer.toFixed(0)}%` }, { id: 'repetitividad', score: volScore, detail: `${totalVolume.toLocaleString()} int.` }, { id: 'roi_potencial', score: roiScore, detail: `€${(totalCost/1000).toFixed(0)}K` } ]; } // Calculate weighted global score from factors function calculateWeightedScore(factors: { id: string; score: number }[]): number { if (factors.length === 0) return 5; let weightedSum = 0; let totalWeight = 0; for (const factor of factors) { const config = FACTOR_CONFIGS.find(c => c.id === factor.id); if (config) { weightedSum += factor.score * config.weight; totalWeight += config.weight; } } return totalWeight > 0 ? weightedSum / totalWeight * 10 : 5; // Normalize to ensure weights sum correctly } // v3.4: Tipo para datos de Tier interface TierDataType { AUTOMATE: { count: number; volume: number }; ASSIST: { count: number; volume: number }; AUGMENT: { count: number; volume: number }; 'HUMAN-ONLY': { count: number; volume: number }; } // ============================================ // v3.10: OPPORTUNITY BUBBLE CHART // ============================================ // Colores por tier para el bubble chart const TIER_BUBBLE_COLORS: Record = { 'AUTOMATE': { fill: '#10b981', stroke: '#059669' }, // Emerald 'ASSIST': { fill: '#6d84e3', stroke: '#4f63b8' }, // Primary blue 'AUGMENT': { fill: '#f59e0b', stroke: '#d97706' }, // Amber 'HUMAN-ONLY': { fill: '#94a3b8', stroke: '#64748b' } // Slate }; // Calcular radio con escala logarítmica function calcularRadioBurbuja(volumen: number, maxVolumen: number): number { const minRadio = 6; const maxRadio = 35; if (volumen <= 0 || maxVolumen <= 0) return minRadio; const escala = Math.log10(volumen + 1) / Math.log10(maxVolumen + 1); return minRadio + (maxRadio - minRadio) * escala; } // Período de datos: el volumen corresponde a 11 meses, no es mensual const DATA_PERIOD_MONTHS = 11; // Calcular ahorro TCO por cola // v4.2: Corregido para convertir volumen de 11 meses a anual function calcularAhorroTCO(queue: OriginalQueueMetrics): number { // CPI Config similar a RoadmapTab const CPI_HUMANO = 2.33; const CPI_BOT = 0.15; const CPI_ASSIST = 1.50; const CPI_AUGMENT = 2.00; const ratesByTier: Record = { 'AUTOMATE': { rate: 0.70, cpi: CPI_BOT }, 'ASSIST': { rate: 0.30, cpi: CPI_ASSIST }, 'AUGMENT': { rate: 0.15, cpi: CPI_AUGMENT }, 'HUMAN-ONLY': { rate: 0, cpi: CPI_HUMANO } }; const config = ratesByTier[queue.tier]; // Ahorro anual = (volumen/11) × 12 × rate × (CPI_humano - CPI_target) const annualVolume = (queue.volume / DATA_PERIOD_MONTHS) * 12; const ahorroAnual = annualVolume * config.rate * (CPI_HUMANO - config.cpi); return Math.round(ahorroAnual); } // Interfaz para datos de burbuja interface BubbleData { id: string; name: string; skillName: string; score: number; tier: AgenticTier; volume: number; ahorro: number; cv: number; fcr: number; transfer: number; x: number; // Posición X (score) y: number; // Posición Y (ahorro) radius: number; } // Componente del Bubble Chart de Oportunidades function OpportunityBubbleChart({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { // Estados para filtros const [tierFilter, setTierFilter] = useState<'Todos' | AgenticTier>('Todos'); const [minAhorro, setMinAhorro] = useState(0); const [minVolumen, setMinVolumen] = useState(0); const [hoveredBubble, setHoveredBubble] = useState(null); const [selectedBubble, setSelectedBubble] = useState(null); // Responsive chart dimensions const containerRef = React.useRef(null); const [containerWidth, setContainerWidth] = React.useState(700); React.useEffect(() => { const updateWidth = () => { if (containerRef.current) { const width = containerRef.current.offsetWidth; setContainerWidth(Math.max(320, width - 32)); // min 320px, account for padding } }; updateWidth(); window.addEventListener('resize', updateWidth); return () => window.removeEventListener('resize', updateWidth); }, []); // Dimensiones del chart - responsive const chartWidth = containerWidth; const chartHeight = Math.min(400, containerWidth * 0.6); // aspect ratio ~1.67:1 const margin = { top: 30, right: containerWidth < 500 ? 15 : 30, bottom: 50, left: containerWidth < 500 ? 45 : 70 }; const innerWidth = chartWidth - margin.left - margin.right; const innerHeight = chartHeight - margin.top - margin.bottom; // Extraer todas las colas y calcular ahorro const allQueues = React.useMemo(() => { return drilldownData.flatMap(skill => skill.originalQueues.map(q => ({ ...q, skillName: skill.skill, ahorro: calcularAhorroTCO(q) })) ); }, [drilldownData]); // Filtrar colas según criterios const filteredQueues = React.useMemo(() => { return allQueues .filter(q => q.tier !== 'HUMAN-ONLY') // Excluir HUMAN-ONLY (no tienen ahorro) .filter(q => q.ahorro > minAhorro) .filter(q => q.volume >= minVolumen) .filter(q => tierFilter === 'Todos' || q.tier === tierFilter) .sort((a, b) => b.ahorro - a.ahorro) // Ordenar por ahorro descendente .slice(0, 20); // Mostrar hasta 20 burbujas }, [allQueues, tierFilter, minAhorro, minVolumen]); // Calcular escalas const maxVolumen = Math.max(...allQueues.map(q => q.volume), 1); const maxAhorro = Math.max(...filteredQueues.map(q => q.ahorro), 1); // Crear datos de burbujas con posiciones const bubbleData: BubbleData[] = React.useMemo(() => { return filteredQueues.map(q => ({ id: q.original_queue_id, name: q.original_queue_id, skillName: q.skillName, score: q.agenticScore, tier: q.tier, volume: q.volume, ahorro: q.ahorro, cv: q.cv_aht, // FCR Técnico para consistencia con Executive Summary (fallback: 100 - transfer_rate) fcr: q.fcr_tecnico ?? (100 - q.transfer_rate), transfer: q.transfer_rate, // Escala X: score 0-10 -> 0-innerWidth x: (q.agenticScore / 10) * innerWidth, // Escala Y: ahorro 0-max -> innerHeight-0 (invertido para que arriba sea más) y: innerHeight - (q.ahorro / maxAhorro) * innerHeight, radius: calcularRadioBurbuja(q.volume, maxVolumen) })); }, [filteredQueues, maxVolumen, maxAhorro, innerWidth, innerHeight]); // v3.12: Contadores por cuadrante sincronizados con filtros // Umbrales fijos para score, umbral relativo para ahorro (30% del max visible) const SCORE_AUTOMATE = 7.5; const SCORE_ASSIST = 5.5; const AHORRO_THRESHOLD_PCT = 0.3; const quadrantStats = React.useMemo(() => { const ahorroThreshold = maxAhorro * AHORRO_THRESHOLD_PCT; // Cuadrantes basados en posición visual const quickWins = bubbleData.filter(b => b.score >= SCORE_AUTOMATE && b.ahorro >= ahorroThreshold ); const highPotential = bubbleData.filter(b => b.score >= SCORE_ASSIST && b.score < SCORE_AUTOMATE && b.ahorro >= ahorroThreshold ); const lowHanging = bubbleData.filter(b => b.score >= SCORE_AUTOMATE && b.ahorro < ahorroThreshold ); const nurture = bubbleData.filter(b => b.score < SCORE_ASSIST ); const backlog = bubbleData.filter(b => b.score >= SCORE_ASSIST && b.score < SCORE_AUTOMATE && b.ahorro < ahorroThreshold ); const sumAhorro = (items: BubbleData[]) => items.reduce((sum, b) => sum + b.ahorro, 0); return { quickWins: { items: quickWins, count: quickWins.length, ahorro: sumAhorro(quickWins) }, highPotential: { items: highPotential, count: highPotential.length, ahorro: sumAhorro(highPotential) }, lowHanging: { items: lowHanging, count: lowHanging.length, ahorro: sumAhorro(lowHanging) }, nurture: { items: nurture, count: nurture.length, ahorro: sumAhorro(nurture) }, backlog: { items: backlog, count: backlog.length, ahorro: sumAhorro(backlog) }, total: bubbleData.length, totalAhorro: sumAhorro(bubbleData), ahorroThreshold }; }, [bubbleData, maxAhorro]); const sumAhorro = (items: BubbleData[]) => items.reduce((sum, b) => sum + b.ahorro, 0); // Indicador de filtros activos const hasActiveFilters = minAhorro > 0 || minVolumen > 0 || tierFilter !== 'Todos'; const formatCurrency = (val: number) => { if (val >= 1000000) return `€${(val / 1000000).toFixed(1)}M`; if (val >= 1000) return `€${Math.round(val / 1000)}K`; return `€${val}`; }; const formatVolume = (v: number) => { if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`; if (v >= 1000) return `${Math.round(v / 1000)}K`; return v.toString(); }; // Umbral de score para línea vertical AUTOMATE const automateThresholdX = (7.5 / 10) * innerWidth; const assistThresholdX = (5.5 / 10) * innerWidth; return (
{/* Header */}

Mapa de Oportunidades

{bubbleData.length} colas

Tamaño = Volumen · Color = Tier · Posición = Score vs Ahorro TCO

{/* Filtros */}
Tier:
Ahorro mín:
Volumen mín:
{/* v3.12: Indicador de filtros activos con resumen de cuadrantes */} {hasActiveFilters && (
Filtros activos: {minAhorro > 0 && Ahorro ≥€{minAhorro >= 1000 ? `${minAhorro/1000}K` : minAhorro}} {minVolumen > 0 && Vol ≥{minVolumen >= 1000 ? `${minVolumen/1000}K` : minVolumen}} {tierFilter !== 'Todos' && Tier: {tierFilter}} | {quadrantStats.total} de {allQueues.filter(q => q.tier !== 'HUMAN-ONLY').length} colas
)}
{/* SVG Chart */}
{/* Definiciones para gradientes y filtros */} {/* Fondo de cuadrantes */} {/* Quick Wins (top-right) */} {/* High Potential (top-center) */} {/* Nurture (left) */} {/* Líneas de umbral verticales */} {/* v3.12: Etiquetas de cuadrante sincronizadas con filtros */} {/* Quick Wins (top-right) */} 🎯 QUICK WINS {quadrantStats.quickWins.count} colas · {formatCurrency(quadrantStats.quickWins.ahorro)} {/* Alto Potencial (top-center) */} ⚡ ALTO POTENCIAL {quadrantStats.highPotential.count} colas · {formatCurrency(quadrantStats.highPotential.ahorro)} {/* Desarrollar / Nurture (left column) */} 📈 DESARROLLAR {quadrantStats.nurture.count} colas · {formatCurrency(quadrantStats.nurture.ahorro)} {/* Low Hanging Fruit (bottom-right) - Fácil pero bajo ahorro */} {quadrantStats.lowHanging.count > 0 && ( <> ✅ FÁCIL IMPL. {quadrantStats.lowHanging.count} · {formatCurrency(quadrantStats.lowHanging.ahorro)} )} {/* Backlog (bottom-center) */} {quadrantStats.backlog.count > 0 && ( <> 📋 BACKLOG {quadrantStats.backlog.count} · {formatCurrency(quadrantStats.backlog.ahorro)} )} {/* Ejes */} {/* Eje X */} {/* Ticks X */} {[0, 2, 4, 5.5, 6, 7.5, 8, 10].map(score => { const x = (score / 10) * innerWidth; return ( {score} ); })} Agentic Score {/* Eje Y */} {/* Ticks Y */} {[0, 0.25, 0.5, 0.75, 1].map(pct => { const y = innerHeight - pct * innerHeight; const value = pct * maxAhorro; return ( {formatCurrency(value)} ); })} Ahorro TCO Anual {/* Burbujas */} {bubbleData.map((bubble, idx) => ( setHoveredBubble(bubble)} onMouseLeave={() => setHoveredBubble(null)} onClick={() => setSelectedBubble(bubble)} style={{ cursor: 'pointer' }} > {/* Etiqueta si burbuja es grande */} {bubble.radius > 18 && ( {bubble.name.length > 8 ? bubble.name.substring(0, 6) + '…' : bubble.name} )} ))} {/* Mensaje si no hay datos */} {bubbleData.length === 0 && ( No hay colas que cumplan los filtros seleccionados )} {/* Tooltip flotante */} {hoveredBubble && (
{hoveredBubble.name} {hoveredBubble.tier}
Score: {hoveredBubble.score.toFixed(1)}
Volumen: {formatVolume(hoveredBubble.volume)}/mes
Ahorro: {formatCurrency(hoveredBubble.ahorro)}/año
CV AHT: 120 ? 'text-red-500' : hoveredBubble.cv > 75 ? 'text-amber-500' : 'text-emerald-500'}`}> {hoveredBubble.cv.toFixed(0)}%
FCR: {hoveredBubble.fcr.toFixed(0)}%

Click para ver detalle

)}
{/* Leyenda */}
{/* Leyenda de colores */}

COLOR = TIER

{(['AUTOMATE', 'ASSIST', 'AUGMENT'] as AgenticTier[]).map(tier => (
{tier === 'AUTOMATE' ? '≥7.5' : tier === 'ASSIST' ? '≥5.5' : '≥3.5'}
))}
{/* Leyenda de tamaños */}

TAMAÑO = VOLUMEN

<1K
1K-10K
>10K
{/* v3.12: Resumen con breakdown de cuadrantes */}
{/* Breakdown de cuadrantes */}
🎯 {quadrantStats.quickWins.count} ⚡ {quadrantStats.highPotential.count} 📈 {quadrantStats.nurture.count} {quadrantStats.lowHanging.count > 0 && ( ✅ {quadrantStats.lowHanging.count} )} {quadrantStats.backlog.count > 0 && ( 📋 {quadrantStats.backlog.count} )} = {quadrantStats.total} total
{/* Ahorro total */}

AHORRO VISIBLE

{formatCurrency(quadrantStats.totalAhorro)}

{/* Modal de detalle */} {selectedBubble && (
setSelectedBubble(null)}>
e.stopPropagation()}>

{selectedBubble.name}

{selectedBubble.tier} Skill: {selectedBubble.skillName}

Agentic Score

{selectedBubble.score.toFixed(1)}

Ahorro Anual

{formatCurrency(selectedBubble.ahorro)}

Volumen/mes

{formatVolume(selectedBubble.volume)}

CV AHT

120 ? 'text-red-500' : selectedBubble.cv > 75 ? 'text-amber-500' : 'text-emerald-500'}`}> {selectedBubble.cv.toFixed(0)}%

FCR

{selectedBubble.fcr.toFixed(0)}%

Transfer Rate

50 ? 'text-red-500' : selectedBubble.transfer > 30 ? 'text-amber-500' : 'text-gray-700'}`}> {selectedBubble.transfer.toFixed(0)}%

{selectedBubble.tier === 'AUTOMATE' ? '🎯 Candidato a Quick Win' : selectedBubble.tier === 'ASSIST' ? '⚡ Alto Potencial con Copilot' : '📈 Requiere estandarización previa'}

{selectedBubble.tier === 'AUTOMATE' ? 'Score ≥7.5 indica procesos maduros listos para automatización completa.' : selectedBubble.tier === 'ASSIST' ? 'Score 5.5-7.5 se beneficia de asistencia IA (Copilot) para elevar a Tier 1.' : 'Score <5.5 requiere trabajo previo de estandarización antes de automatizar.'}

)}
); } // ========== Cabecera Agentic Readiness Score con colores corporativos ========== function AgenticReadinessHeader({ tierData, totalVolume, totalQueues }: { tierData: TierDataType; totalVolume: number; totalQueues: number; }) { // Calcular volumen automatizable (AUTOMATE + ASSIST) const automatizableVolume = tierData.AUTOMATE.volume + tierData.ASSIST.volume; const automatizablePct = totalVolume > 0 ? (automatizableVolume / totalVolume) * 100 : 0; // Porcentajes por tier const tierPcts = { AUTOMATE: totalVolume > 0 ? (tierData.AUTOMATE.volume / totalVolume) * 100 : 0, ASSIST: totalVolume > 0 ? (tierData.ASSIST.volume / totalVolume) * 100 : 0, AUGMENT: totalVolume > 0 ? (tierData.AUGMENT.volume / totalVolume) * 100 : 0, 'HUMAN-ONLY': totalVolume > 0 ? (tierData['HUMAN-ONLY'].volume / totalVolume) * 100 : 0 }; // Formatear volumen const formatVolume = (v: number) => { if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`; if (v >= 1000) return `${Math.round(v / 1000)}K`; return v.toLocaleString(); }; // Tier card config con colores consistentes con la sección introductoria const tierConfigs = [ { key: 'AUTOMATE', label: 'AUTOMATE', emoji: '🤖', sublabel: 'Full IA', color: '#10b981', bgColor: '#d1fae5' }, { key: 'ASSIST', label: 'ASSIST', emoji: '🤝', sublabel: 'Copilot', color: '#3b82f6', bgColor: '#dbeafe' }, { key: 'AUGMENT', label: 'AUGMENT', emoji: '📚', sublabel: 'Tools', color: '#f59e0b', bgColor: '#fef3c7' }, { key: 'HUMAN-ONLY', label: 'HUMAN', emoji: '👤', sublabel: 'Manual', color: '#6b7280', bgColor: '#f3f4f6' } ]; // Calcular porcentaje de colas AUTOMATE const pctColasAutomate = totalQueues > 0 ? (tierData.AUTOMATE.count / totalQueues) * 100 : 0; // Generar interpretación que explica la diferencia volumen vs colas const getInterpretation = () => { // El score principal (88%) se basa en VOLUMEN de interacciones // El % de colas AUTOMATE (26%) es diferente porque hay pocas colas de alto volumen return `El ${Math.round(automatizablePct)}% representa el volumen de interacciones automatizables (AUTOMATE + ASSIST). ` + `Solo el ${Math.round(pctColasAutomate)}% de las colas (${tierData.AUTOMATE.count} de ${totalQueues}) son AUTOMATE, ` + `pero concentran ${Math.round(tierPcts.AUTOMATE)}% del volumen total. ` + `Esto indica pocas colas de alto volumen automatizables - oportunidad concentrada en Quick Wins de alto impacto.`; }; return ( {/* Header */}

Agentic Readiness Score

{/* Score Principal - Centrado */}
{Math.round(automatizablePct)}%
Volumen Automatizable
(Tier AUTOMATE + ASSIST)
{/* 4 Tier Cards - colores consistentes con sección introductoria */}
{tierConfigs.map(config => { const tierKey = config.key as keyof TierDataType; const data = tierData[tierKey]; const pct = tierPcts[tierKey]; return (
{config.label}
{Math.round(pct)}%
{formatVolume(data.volume)} int
{config.emoji} {config.sublabel}
{data.count} colas
); })}
{/* Barra de distribución visual - colores consistentes */}
{tierPcts.AUTOMATE > 0 && (
)} {tierPcts.ASSIST > 0 && (
)} {tierPcts.AUGMENT > 0 && (
)} {tierPcts['HUMAN-ONLY'] > 0 && (
)}
0% 50% 100%
{/* Interpretación condensada en una línea */}

📊 Interpretación: {getInterpretation()}

{/* Footer con totales */}
Total: {formatVolume(totalVolume)} interacciones {totalQueues} colas analizadas
); } // ========== Sección de Factores del Score Global ========== function GlobalFactorsSection({ drilldownData, tierData, totalVolume }: { drilldownData: DrilldownDataPoint[]; tierData: TierDataType; totalVolume: number; }) { const allQueues = drilldownData.flatMap(skill => skill.originalQueues); // Calcular métricas globales ponderadas por volumen const totalQueueVolume = allQueues.reduce((sum, q) => sum + q.volume, 0); // CV AHT promedio ponderado const avgCV = totalQueueVolume > 0 ? allQueues.reduce((sum, q) => sum + q.cv_aht * q.volume, 0) / totalQueueVolume : 0; // FCR Técnico promedio ponderado (consistente con Executive Summary) const avgFCR = totalQueueVolume > 0 ? allQueues.reduce((sum, q) => sum + (q.fcr_tecnico ?? (100 - q.transfer_rate)) * q.volume, 0) / totalQueueVolume : 0; // Transfer rate promedio ponderado const avgTransfer = totalQueueVolume > 0 ? allQueues.reduce((sum, q) => sum + q.transfer_rate * q.volume, 0) / totalQueueVolume : 0; // AHT promedio ponderado const avgAHT = totalQueueVolume > 0 ? allQueues.reduce((sum, q) => sum + q.aht_mean * q.volume, 0) / totalQueueVolume : 0; // Calidad de datos: % registros válidos (aproximación) const validRecordsRatio = allQueues.length > 0 ? allQueues.reduce((sum, q) => sum + (q.volumeValid / Math.max(1, q.volume)) * q.volume, 0) / totalQueueVolume : 0; const dataQualityPct = Math.round(validRecordsRatio * 100); // Calcular scores de cada factor (0-10) // Predictibilidad: basado en CV AHT (CV < 75% = bueno) const predictabilityScore = Math.max(0, Math.min(10, 10 - (avgCV / 20))); // Resolutividad: FCR (60%) + Transfer inverso (40%) const fcrComponent = (avgFCR / 100) * 10 * 0.6; const transferComponent = Math.max(0, (1 - avgTransfer / 50)) * 10 * 0.4; const resolutionScore = Math.min(10, fcrComponent + transferComponent); // Volumen: logarítmico basado en volumen del periodo const volumeScore = Math.min(10, Math.log10(totalQueueVolume + 1) * 2.5); // Calidad datos: % válidos const dataQualityScore = dataQualityPct / 10; // Simplicidad: basado en AHT (< 180s = 10, > 600s = 0) const simplicityScore = Math.max(0, Math.min(10, 10 - ((avgAHT - 180) / 60))); // Score global ponderado const weights = { predictability: 0.30, resolution: 0.25, volume: 0.25, dataQuality: 0.10, simplicity: 0.10 }; const globalScore = ( predictabilityScore * weights.predictability + resolutionScore * weights.resolution + volumeScore * weights.volume + dataQualityScore * weights.dataQuality + simplicityScore * weights.simplicity ); // Automatizable % const automatizableVolume = tierData.AUTOMATE.volume + tierData.ASSIST.volume; const automatizablePct = totalVolume > 0 ? Math.round((automatizableVolume / totalVolume) * 100) : 0; const getStatus = (score: number): { emoji: string; label: string; color: string } => { if (score >= 7) return { emoji: '🟢', label: 'Alto', color: COLORS.primary }; if (score >= 5) return { emoji: '🟡', label: 'Medio', color: COLORS.dark }; if (score >= 3) return { emoji: '🟠', label: 'Bajo', color: COLORS.medium }; return { emoji: '🔴', label: 'Crítico', color: COLORS.medium }; }; const getGlobalLabel = (score: number): string => { if (score >= 7) return 'Listo para automatización'; if (score >= 5) return 'Potencial moderado'; if (score >= 3) return 'Requiere optimización'; return 'No preparado'; }; const formatVolume = (v: number) => { if (v >= 1000000) return `${(v / 1000000).toFixed(2)}M`; if (v >= 1000) return `${(v / 1000).toFixed(0)}K`; return v.toLocaleString(); }; const factors = [ { name: 'Predictibilidad', score: predictabilityScore, weight: '30%', metric: `CV ${avgCV.toFixed(0)}%`, status: getStatus(predictabilityScore) }, { name: 'Resolutividad', score: resolutionScore, weight: '25%', metric: `FCR ${avgFCR.toFixed(0)}%/Tr ${avgTransfer.toFixed(0)}%`, status: getStatus(resolutionScore) }, { name: 'Volumen', score: volumeScore, weight: '25%', metric: `${formatVolume(totalQueueVolume)} int`, status: getStatus(volumeScore) }, { name: 'Calidad Datos', score: dataQualityScore, weight: '10%', metric: `${dataQualityPct}% VALID`, status: getStatus(dataQualityScore) }, { name: 'Simplicidad', score: simplicityScore, weight: '10%', metric: `AHT ${Math.round(avgAHT)}s`, status: getStatus(simplicityScore) } ]; return (
{/* Header */}

Factores del Score (Nivel Operación Global)

{/* Nota explicativa */}

⚠️ NOTA: Estos factores son promedios globales. El scoring por cola usa estos mismos factores calculados individualmente para cada cola.

{/* Tabla de factores */}
{factors.map((factor, idx) => ( ))}
Factor Score Peso Métrica Real Status
{factor.name} {factor.score.toFixed(1)} {factor.weight} {factor.metric} {factor.status.emoji} {factor.status.label}
SCORE GLOBAL {globalScore.toFixed(1)} {getGlobalLabel(globalScore)}
{/* Insight explicativo */}

💡 El score global ({globalScore.toFixed(1)}) refleja la operación completa. Sin embargo, {automatizablePct}% del volumen está en colas individuales que SÍ cumplen criterios de automatización.

); } // ========== Clasificación por Skill con distribución por Tier ========== function SkillClassificationSection({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { // Calcular métricas por skill const skillData = drilldownData.map(skill => { const queues = skill.originalQueues; const totalVolume = queues.reduce((sum, q) => sum + q.volume, 0); // Contar colas y volumen por tier const tierStats = { AUTOMATE: { count: queues.filter(q => q.tier === 'AUTOMATE').length, volume: queues.filter(q => q.tier === 'AUTOMATE').reduce((s, q) => s + q.volume, 0) }, ASSIST: { count: queues.filter(q => q.tier === 'ASSIST').length, volume: queues.filter(q => q.tier === 'ASSIST').reduce((s, q) => s + q.volume, 0) }, AUGMENT: { count: queues.filter(q => q.tier === 'AUGMENT').length, volume: queues.filter(q => q.tier === 'AUGMENT').reduce((s, q) => s + q.volume, 0) }, 'HUMAN-ONLY': { count: queues.filter(q => q.tier === 'HUMAN-ONLY').length, volume: queues.filter(q => q.tier === 'HUMAN-ONLY').reduce((s, q) => s + q.volume, 0) } }; // Porcentajes por volumen const tierPcts = { AUTOMATE: totalVolume > 0 ? (tierStats.AUTOMATE.volume / totalVolume) * 100 : 0, ASSIST: totalVolume > 0 ? (tierStats.ASSIST.volume / totalVolume) * 100 : 0, AUGMENT: totalVolume > 0 ? (tierStats.AUGMENT.volume / totalVolume) * 100 : 0, 'HUMAN-ONLY': totalVolume > 0 ? (tierStats['HUMAN-ONLY'].volume / totalVolume) * 100 : 0 }; // Tier dominante por volumen const dominantTier = Object.entries(tierPcts).reduce((max, [tier, pct]) => pct > max.pct ? { tier, pct } : max , { tier: 'HUMAN-ONLY', pct: 0 }); // Volumen en T1+T2 const t1t2Pct = tierPcts.AUTOMATE + tierPcts.ASSIST; // Determinar acción recomendada let action = ''; let isWarning = false; if (tierPcts.AUTOMATE >= 50) { action = '→ Wave 4: Bot Full'; } else if (t1t2Pct >= 60) { action = '→ Wave 3: Copilot'; } else if (tierPcts.AUGMENT >= 30) { action = '→ Wave 2: Tools'; } else if (tierPcts['HUMAN-ONLY'] >= 50) { action = '→ Wave 1: Foundation'; isWarning = true; } else { action = '→ Wave 2: Copilot'; } return { skill: skill.skill, volume: totalVolume, tierStats, tierPcts, dominantTier, t1t2Pct, action, isWarning }; }).sort((a, b) => b.volume - a.volume); // Identificar quick wins y alertas const quickWins = skillData.filter(s => s.tierPcts.AUTOMATE >= 40 || s.t1t2Pct >= 70); const alerts = skillData.filter(s => s.tierPcts['HUMAN-ONLY'] >= 50); const formatVolume = (v: number) => { if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`; if (v >= 1000) return `${Math.round(v / 1000)}K`; return v.toLocaleString(); }; return (
{/* Header */}

CLASIFICACIÓN POR SKILL

{/* Tabla */}
{skillData.map((skill, idx) => ( 0 ? `1px solid ${COLORS.light}` : undefined }}> {/* Skill name */} {/* Volume */} {/* Tier counts */} {/* Action */} ))}
Skill Volumen Distribución Colas por Tier Acción
AUTO ASIST AUGM HUMAN
{skill.skill} {formatVolume(skill.volume)}
= 30 ? COLORS.primary : COLORS.medium }}> {skill.tierStats.AUTOMATE.count}
({Math.round(skill.tierPcts.AUTOMATE)}%)
= 30 ? COLORS.dark : COLORS.medium }}> {skill.tierStats.ASSIST.count}
({Math.round(skill.tierPcts.ASSIST)}%)
{skill.tierStats.AUGMENT.count}
({Math.round(skill.tierPcts.AUGMENT)}%)
= 50 ? COLORS.dark : COLORS.medium }}> {skill.tierStats['HUMAN-ONLY'].count}
({Math.round(skill.tierPcts['HUMAN-ONLY'])}%)
{skill.action}
{skill.tierPcts['HUMAN-ONLY'] >= 50 ? ( Vol en T4: {Math.round(skill.tierPcts['HUMAN-ONLY'])}% ⚠️ ) : ( Vol en T1+T2: {Math.round(skill.t1t2Pct)}% )}
{/* Insights */}
{quickWins.length > 0 && (

🎯 Quick Wins:{' '} {quickWins.map(s => s.skill).join(' + ')} tienen >60% volumen en T1+T2

)} {alerts.length > 0 && (

⚠️ Atención:{' '} {alerts.map(s => `${s.skill} tiene ${Math.round(s.tierPcts['HUMAN-ONLY'])}% en HUMAN`).join('; ')} → priorizar en Wave 1

)} {quickWins.length === 0 && alerts.length === 0 && (

Distribución equilibrada entre tiers. Revisar colas individuales para priorización.

)}
); } // Skills Heatmap/Table (fallback cuando no hay drilldownData) function SkillsReadinessTable({ heatmapData }: { heatmapData: HeatmapDataPoint[] }) { const sortedData = [...heatmapData].sort((a, b) => b.automation_readiness - a.automation_readiness); const formatVolume = (v: number) => v >= 1000 ? `${Math.round(v / 1000)}K` : v.toString(); return (

Análisis por Skill

{sortedData.map((item, idx) => ( 0 ? `1px solid ${COLORS.light}` : undefined }}> ))}
Skill Volumen AHT CV AHT Score
{item.skill} {formatVolume(item.volume)} {item.aht_seconds}s 75 ? COLORS.dark : COLORS.medium }}> {item.variability.cv_aht.toFixed(0)}% {(item.automation_readiness / 10).toFixed(1)}
); } // Formatear AHT en formato mm:ss function formatAHT(seconds: number): string { const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } // v3.4: Fila expandible por queue_skill (muestra original_queue_id al expandir con Tiers) function ExpandableSkillRow({ dataPoint, idx, isExpanded, onToggle }: { dataPoint: DrilldownDataPoint; idx: number; isExpanded: boolean; onToggle: () => void; }) { // v3.4: Contar colas por Tier const tierCounts = { AUTOMATE: dataPoint.originalQueues.filter(q => q.tier === 'AUTOMATE').length, ASSIST: dataPoint.originalQueues.filter(q => q.tier === 'ASSIST').length, AUGMENT: dataPoint.originalQueues.filter(q => q.tier === 'AUGMENT').length, 'HUMAN-ONLY': dataPoint.originalQueues.filter(q => q.tier === 'HUMAN-ONLY').length }; // Tier dominante del skill (por volumen) const tierVolumes = { AUTOMATE: dataPoint.originalQueues.filter(q => q.tier === 'AUTOMATE').reduce((s, q) => s + q.volume, 0), ASSIST: dataPoint.originalQueues.filter(q => q.tier === 'ASSIST').reduce((s, q) => s + q.volume, 0), AUGMENT: dataPoint.originalQueues.filter(q => q.tier === 'AUGMENT').reduce((s, q) => s + q.volume, 0), 'HUMAN-ONLY': dataPoint.originalQueues.filter(q => q.tier === 'HUMAN-ONLY').reduce((s, q) => s + q.volume, 0) }; const dominantTier = (Object.keys(tierVolumes) as AgenticTier[]).reduce((a, b) => tierVolumes[a] > tierVolumes[b] ? a : b ); const potentialSavings = dataPoint.annualCost ? Math.round(dataPoint.annualCost * 0.35 / 12) : 0; const automateQueues = tierCounts.AUTOMATE; return ( <>
{dataPoint.skill} {dataPoint.originalQueues.length} colas {/* v3.4: Mostrar tiers disponibles */} {automateQueues > 0 && ( {automateQueues} AUTOMATE )} {tierCounts.ASSIST > 0 && ( {tierCounts.ASSIST} ASSIST )}
{dataPoint.volume.toLocaleString()} {formatAHT(dataPoint.aht_mean)} {dataPoint.cv_aht.toFixed(0)}% {formatCurrency(potentialSavings)}/mes
{/* Fila expandida con tabla de original_queue_id */} {isExpanded && (
{/* Header de resumen con Tiers */}
{dataPoint.originalQueues.length} colas | {/* v3.4: Mostrar distribución por Tier */} {tierCounts.AUTOMATE > 0 && ( {tierCounts.AUTOMATE} AUTOMATE )} {tierCounts.ASSIST > 0 && ( {tierCounts.ASSIST} ASSIST )} {tierCounts.AUGMENT > 0 && ( {tierCounts.AUGMENT} AUGMENT )} {tierCounts['HUMAN-ONLY'] > 0 && ( {tierCounts['HUMAN-ONLY']} HUMAN )} | Coste: {formatCurrency(dataPoint.annualCost || 0)}/año | Ahorro: {formatCurrency(potentialSavings * 12)}/año
FCR: {(dataPoint.fcr_tecnico ?? (100 - dataPoint.transfer_rate)).toFixed(0)}% | Transfer: {dataPoint.transfer_rate.toFixed(0)}%
{/* Tabla de colas (original_queue_id) con Tiers */}
{dataPoint.originalQueues.map((queue, queueIdx) => { const queueMonthlySavings = queue.annualCost ? Math.round(queue.annualCost * 0.35 / 12) : 0; const tierStyle = getTierStyle(queue.tier); const redFlags = detectRedFlags(queue, redFlagConfigs); return ( 0 ? 'bg-red-50/20' : ''}`} > ); })} {/* Fila de totales */}
Cola (original_queue_id) Volumen AHT CV Transfer FCR Score Tier Red Flags Ahorro/mes
{queue.original_queue_id} {/* Mostrar motivo del tier en tooltip */} {queue.tierMotivo && ( {queue.tierMotivo}
}> )}
{queue.volume.toLocaleString()} {formatAHT(queue.aht_mean)} 120 ? 'text-red-600' : 'text-amber-600'}`}> {queue.cv_aht.toFixed(0)}% 50 ? 'text-red-600' : queue.transfer_rate > 30 ? 'text-amber-600' : 'text-gray-600'}`}> {queue.transfer_rate.toFixed(0)}% {(queue.fcr_tecnico ?? (100 - queue.transfer_rate)).toFixed(0)}% {queue.scoreBreakdown ? ( }> {queue.agenticScore.toFixed(1)} ) : ( {queue.agenticScore.toFixed(1)} )} {redFlags.length > 0 ? (
{redFlags.map(flag => ( ))}
) : ( )}
{formatCurrency(queueMonthlySavings)}
TOTAL ({dataPoint.originalQueues.length} colas) {dataPoint.volume.toLocaleString()} {formatAHT(dataPoint.aht_mean)} {dataPoint.cv_aht.toFixed(0)}% {dataPoint.transfer_rate.toFixed(0)}% {(dataPoint.fcr_tecnico ?? (100 - dataPoint.transfer_rate)).toFixed(0)}% {dataPoint.agenticScore.toFixed(1)} {formatCurrency(potentialSavings)}
)} ); } // ============================================ // v4.0: NUEVAS SECCIONES POR TIER // ============================================ // Configuración de colores y estilos por tier const TIER_SECTION_CONFIG: Record = { 'AUTOMATE': { color: '#10b981', bgColor: '#d1fae5', borderColor: '#10b98140', gradientFrom: 'from-emerald-50', gradientTo: 'to-emerald-100/50', icon: Sparkles, title: 'Colas AUTOMATE', subtitle: 'Listas para automatización completa con agente virtual (Score ≥7.5)', emptyMessage: 'No hay colas clasificadas como AUTOMATE' }, 'ASSIST': { color: '#3b82f6', bgColor: '#dbeafe', borderColor: '#3b82f640', gradientFrom: 'from-blue-50', gradientTo: 'to-blue-100/50', icon: Bot, title: 'Colas ASSIST', subtitle: 'Candidatas a Copilot - IA asiste al agente humano (Score 5.5-7.5)', emptyMessage: 'No hay colas clasificadas como ASSIST' }, 'AUGMENT': { color: '#f59e0b', bgColor: '#fef3c7', borderColor: '#f59e0b40', gradientFrom: 'from-amber-50', gradientTo: 'to-amber-100/50', icon: TrendingUp, title: 'Colas AUGMENT', subtitle: 'Requieren optimización previa: estandarizar procesos, reducir variabilidad (Score 3.5-5.5)', emptyMessage: 'No hay colas clasificadas como AUGMENT' }, 'HUMAN-ONLY': { color: '#6b7280', bgColor: '#f3f4f6', borderColor: '#6b728040', gradientFrom: 'from-gray-50', gradientTo: 'to-gray-100/50', icon: Users, title: 'Colas HUMAN-ONLY', subtitle: 'No aptas para automatización: volumen insuficiente, datos de baja calidad o complejidad extrema', emptyMessage: 'No hay colas clasificadas como HUMAN-ONLY' } }; // Componente de tabla de colas por Tier (AUTOMATE, ASSIST, AUGMENT) function TierQueueSection({ drilldownData, tier }: { drilldownData: DrilldownDataPoint[]; tier: 'AUTOMATE' | 'ASSIST' | 'AUGMENT'; }) { const [expandedSkills, setExpandedSkills] = useState>(new Set()); const config = TIER_SECTION_CONFIG[tier]; const IconComponent = config.icon; // Extraer todas las colas del tier específico, agrupadas por skill const skillsWithTierQueues = drilldownData .map(skill => ({ skill: skill.skill, queues: skill.originalQueues.filter(q => q.tier === tier), totalVolume: skill.originalQueues.filter(q => q.tier === tier).reduce((s, q) => s + q.volume, 0), totalAnnualCost: skill.originalQueues.filter(q => q.tier === tier).reduce((s, q) => s + (q.annualCost || 0), 0) })) .filter(s => s.queues.length > 0) .sort((a, b) => b.totalVolume - a.totalVolume); const totalQueues = skillsWithTierQueues.reduce((sum, s) => sum + s.queues.length, 0); const totalVolume = skillsWithTierQueues.reduce((sum, s) => sum + s.totalVolume, 0); const totalCost = skillsWithTierQueues.reduce((sum, s) => sum + s.totalAnnualCost, 0); // Calcular ahorro potencial según tier const savingsRate = tier === 'AUTOMATE' ? 0.70 : tier === 'ASSIST' ? 0.30 : 0.15; const potentialSavings = Math.round(totalCost * savingsRate); const toggleSkill = (skill: string) => { const newExpanded = new Set(expandedSkills); if (newExpanded.has(skill)) { newExpanded.delete(skill); } else { newExpanded.add(skill); } setExpandedSkills(newExpanded); }; if (totalQueues === 0) { return null; } return (
{/* Header */}

{config.title}

{config.subtitle}

{totalQueues}

colas en {skillsWithTierQueues.length} skills

{/* Resumen */}
Volumen: {totalVolume.toLocaleString()} int/mes Coste: {formatCurrency(totalCost)}/año
Ahorro potencial: {formatCurrency(potentialSavings)}/año
{/* Tabla por Business Unit (skill) */}
{skillsWithTierQueues.map((skillData, idx) => { const isExpanded = expandedSkills.has(skillData.skill); const avgAHT = skillData.queues.reduce((s, q) => s + q.aht_mean * q.volume, 0) / skillData.totalVolume; const avgCV = skillData.queues.reduce((s, q) => s + q.cv_aht * q.volume, 0) / skillData.totalVolume; const avgFCR = skillData.queues.reduce((s, q) => s + (q.fcr_tecnico ?? (100 - q.transfer_rate)) * q.volume, 0) / skillData.totalVolume; const skillSavings = Math.round(skillData.totalAnnualCost * savingsRate); return ( {/* Fila del Skill */} toggleSkill(skillData.skill)} > {/* Detalle expandible: colas individuales */} {isExpanded && ( )} ); })}
Business Unit (Skill) Colas Volumen AHT Prom. CV Prom. FCR Ahorro Potencial
{isExpanded ? ( ) : ( )} {skillData.skill} {skillData.queues.length} {skillData.totalVolume.toLocaleString()} {formatAHT(avgAHT)} {avgCV.toFixed(0)}% {avgFCR.toFixed(0)}% {formatCurrency(skillSavings)}
{skillData.queues.map((queue, qIdx) => { const queueSavings = Math.round((queue.annualCost || 0) * savingsRate); return ( ); })}
Cola (ID) Volumen AHT CV Transfer FCR Score Ahorro
{queue.original_queue_id} {queue.volume.toLocaleString()} {formatAHT(queue.aht_mean)} {queue.cv_aht.toFixed(0)}% {queue.transfer_rate.toFixed(0)}% {(queue.fcr_tecnico ?? (100 - queue.transfer_rate)).toFixed(0)}% {queue.agenticScore.toFixed(1)} {formatCurrency(queueSavings)}
{/* Footer */}
Click en un skill para ver el detalle de colas individuales
); } // Componente para colas HUMAN-ONLY agrupadas por razón/red flag function HumanOnlyByReasonSection({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { const [expandedReasons, setExpandedReasons] = useState>(new Set()); const config = TIER_SECTION_CONFIG['HUMAN-ONLY']; // Extraer todas las colas HUMAN-ONLY const allHumanOnlyQueues = drilldownData.flatMap(skill => skill.originalQueues .filter(q => q.tier === 'HUMAN-ONLY') .map(q => ({ ...q, skillName: skill.skill })) ); if (allHumanOnlyQueues.length === 0) { return null; } // Agrupar por razón principal (red flag dominante o "Sin red flags") const queuesByReason: Record = {}; allHumanOnlyQueues.forEach(queue => { const flags = detectRedFlags(queue, redFlagConfigs); // Determinar razón principal (prioridad: cv_high > transfer_high > volume_low > valid_low) let reason = 'Sin Red Flags específicos'; let reasonId = 'no_flags'; if (flags.length > 0) { // Ordenar por severidad implícita const priorityOrder = ['cv_high', 'transfer_high', 'volume_low', 'valid_low']; const sortedFlags = [...flags].sort((a, b) => priorityOrder.indexOf(a.config.id) - priorityOrder.indexOf(b.config.id) ); reasonId = sortedFlags[0].config.id; reason = sortedFlags[0].config.label; } if (!queuesByReason[reasonId]) { queuesByReason[reasonId] = []; } queuesByReason[reasonId].push(queue); }); // Convertir a array y ordenar por volumen const reasonGroups = Object.entries(queuesByReason) .map(([reasonId, queues]) => { const flagConfig = redFlagConfigs.find(c => c.id === reasonId); return { reasonId, reason: flagConfig?.label || 'Sin Red Flags específicos', description: flagConfig?.description || 'Colas que no cumplen criterios de automatización', action: flagConfig ? getActionForFlag(flagConfig.id) : 'Revisar manualmente', queues, totalVolume: queues.reduce((s, q) => s + q.volume, 0), queueCount: queues.length }; }) .sort((a, b) => b.totalVolume - a.totalVolume); const totalQueues = allHumanOnlyQueues.length; const totalVolume = allHumanOnlyQueues.reduce((s, q) => s + q.volume, 0); const toggleReason = (reasonId: string) => { const newExpanded = new Set(expandedReasons); if (newExpanded.has(reasonId)) { newExpanded.delete(reasonId); } else { newExpanded.add(reasonId); } setExpandedReasons(newExpanded); }; function getActionForFlag(flagId: string): string { switch (flagId) { case 'cv_high': return 'Estandarizar procesos y scripts'; case 'transfer_high': return 'Simplificar flujo, capacitar agentes'; case 'volume_low': return 'Consolidar con colas similares'; case 'valid_low': return 'Mejorar captura de datos'; default: return 'Revisar manualmente'; } } return (
{/* Header */}

{config.title}

{config.subtitle}

{totalQueues}

colas agrupadas por {reasonGroups.length} razones

{/* Resumen */}
Volumen total: {totalVolume.toLocaleString()} int/mes Estas colas requieren intervención antes de considerar automatización
{/* Tabla agrupada por razón */}
{reasonGroups.map((group) => { const isExpanded = expandedReasons.has(group.reasonId); return ( {/* Fila de la razón */} toggleReason(group.reasonId)} > {/* Detalle expandible: colas de esta razón */} {isExpanded && ( )} ); })}
Razón / Red Flag Colas Volumen Acción Recomendada
{isExpanded ? ( ) : ( )}
{group.reason}

{group.description}

{group.queueCount} {group.totalVolume.toLocaleString()} {group.action}
{group.queues.slice(0, 20).map((queue) => { const flags = detectRedFlags(queue, redFlagConfigs); return ( ); })}
Cola (ID) Skill Volumen CV AHT Transfer Score Red Flags
{queue.original_queue_id} {queue.skillName} {queue.volume.toLocaleString()} 120 ? 'text-red-600 font-medium' : 'text-gray-600'}> {queue.cv_aht.toFixed(0)}% 50 ? 'text-red-600 font-medium' : 'text-gray-600'}> {queue.transfer_rate.toFixed(0)}% {queue.agenticScore.toFixed(1)}
{flags.map(flag => ( ))}
{group.queues.length > 20 && (
Mostrando 20 de {group.queues.length} colas
)}
{/* Footer */}
Click en una razón para ver las colas afectadas. Priorizar acciones según volumen impactado.
); } // v3.4: Sección de Candidatos Prioritarios - Por queue_skill con drill-down a original_queue_id function PriorityCandidatesSection({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { const [expandedRows, setExpandedRows] = useState>(new Set()); // Filtrar skills que tienen al menos una cola AUTOMATE const candidateSkills = drilldownData.filter(d => d.isPriorityCandidate); // Toggle expansión de fila const toggleRow = (skill: string) => { const newExpanded = new Set(expandedRows); if (newExpanded.has(skill)) { newExpanded.delete(skill); } else { newExpanded.add(skill); } setExpandedRows(newExpanded); }; // Calcular totales const totalVolume = candidateSkills.reduce((sum, c) => sum + c.volume, 0); const totalCost = candidateSkills.reduce((sum, c) => sum + (c.annualCost || 0), 0); const potentialMonthlySavings = Math.round(totalCost * 0.35 / 12); // v3.4: Contar colas por Tier en todos los skills con candidatos const allQueuesInCandidates = candidateSkills.flatMap(s => s.originalQueues); const tierCounts = { AUTOMATE: allQueuesInCandidates.filter(q => q.tier === 'AUTOMATE').length, ASSIST: allQueuesInCandidates.filter(q => q.tier === 'ASSIST').length, AUGMENT: allQueuesInCandidates.filter(q => q.tier === 'AUGMENT').length, 'HUMAN-ONLY': allQueuesInCandidates.filter(q => q.tier === 'HUMAN-ONLY').length }; // Volumen por Tier const tierVolumes = { AUTOMATE: allQueuesInCandidates.filter(q => q.tier === 'AUTOMATE').reduce((s, q) => s + q.volume, 0), ASSIST: allQueuesInCandidates.filter(q => q.tier === 'ASSIST').reduce((s, q) => s + q.volume, 0), AUGMENT: allQueuesInCandidates.filter(q => q.tier === 'AUGMENT').reduce((s, q) => s + q.volume, 0), 'HUMAN-ONLY': allQueuesInCandidates.filter(q => q.tier === 'HUMAN-ONLY').reduce((s, q) => s + q.volume, 0) }; if (drilldownData.length === 0) { return null; } return (
{/* Header */}

CLASIFICACIÓN POR TIER DE AUTOMATIZACIÓN

Skills con colas clasificadas como AUTOMATE (score ≥ 7.5, CV ≤ 75%, transfer ≤ 20%)

{tierCounts.AUTOMATE}

colas AUTOMATE en {candidateSkills.length} skills

{/* v3.4: Resumen por Tier */}
{tierCounts.AUTOMATE} AUTOMATE ({tierVolumes.AUTOMATE.toLocaleString()} int)
{tierCounts.ASSIST} ASSIST ({tierVolumes.ASSIST.toLocaleString()} int)
{tierCounts.AUGMENT} AUGMENT ({tierVolumes.AUGMENT.toLocaleString()} int)
{tierCounts['HUMAN-ONLY'] > 0 && (
{tierCounts['HUMAN-ONLY']} HUMAN ({tierVolumes['HUMAN-ONLY'].toLocaleString()} int)
)}
{formatCurrency(potentialMonthlySavings)} ahorro/mes potencial
{/* Tabla por queue_skill */} {candidateSkills.length > 0 ? (
{candidateSkills.map((dataPoint, idx) => ( toggleRow(dataPoint.skill)} /> ))}
Queue Skill (Estratégico) Volumen AHT Prom. CV Prom. Ahorro Potencial Tier Dom.
) : (

No se encontraron colas clasificadas como AUTOMATE

Todas las colas requieren optimización antes de automatizar

)} {/* Footer */}

{candidateSkills.length} de {drilldownData.length} skills tienen al menos una cola tier AUTOMATE

Haz clic en un skill para ver las colas individuales con desglose de score

); } // v3.6: Sección de Colas HUMAN-ONLY con Red Flags - Contextualizada function HumanOnlyRedFlagsSection({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { const [showTable, setShowTable] = useState(false); // Extraer todas las colas const allQueues = drilldownData.flatMap(skill => skill.originalQueues.map(q => ({ ...q, skillName: skill.skill })) ); // Extraer todas las colas HUMAN-ONLY const humanOnlyQueues = allQueues.filter(q => q.tier === 'HUMAN-ONLY'); // Colas con red flags (la mayoría de HUMAN-ONLY tendrán red flags por definición) const queuesWithFlags = humanOnlyQueues.map(q => ({ queue: q, flags: detectRedFlags(q, redFlagConfigs) })).filter(qf => qf.flags.length > 0); // Ordenar por volumen (mayor primero para priorizar) queuesWithFlags.sort((a, b) => b.queue.volume - a.queue.volume); if (queuesWithFlags.length === 0) { return null; } // Calcular totales const totalVolumeAllQueues = allQueues.reduce((sum, q) => sum + q.volume, 0); const totalVolumeRedFlags = queuesWithFlags.reduce((sum, qf) => sum + qf.queue.volume, 0); const pctVolumeRedFlags = totalVolumeAllQueues > 0 ? (totalVolumeRedFlags / totalVolumeAllQueues) * 100 : 0; // v4.2: Coste usando modelo CPI (consistente con Roadmap y Executive Summary) // IMPORTANTE: El volumen es de 11 meses, se convierte a anual: (Vol/11) × 12 const CPI_HUMANO_RF = 2.33; // €/interacción (coste unitario humano) const costeAnualRedFlags = Math.round((totalVolumeRedFlags / DATA_PERIOD_MONTHS) * 12 * CPI_HUMANO_RF); const costeAnualTotal = Math.round((totalVolumeAllQueues / DATA_PERIOD_MONTHS) * 12 * CPI_HUMANO_RF); const pctCosteRedFlags = costeAnualTotal > 0 ? (costeAnualRedFlags / costeAnualTotal) * 100 : 0; // Estadísticas detalladas por tipo de red flag const flagStats = redFlagConfigs.map(config => { const matchingQueues = queuesWithFlags.filter(qf => qf.flags.some(f => f.config.id === config.id) ); const queueCount = matchingQueues.length; const volumeAffected = matchingQueues.reduce((sum, qf) => sum + qf.queue.volume, 0); const pctTotal = totalVolumeAllQueues > 0 ? (volumeAffected / totalVolumeAllQueues) * 100 : 0; // Acción recomendada por tipo let action = ''; switch (config.id) { case 'cv_high': action = 'Estandarizar procesos'; break; case 'transfer_high': action = 'Simplificar flujo / capacitar'; break; case 'volume_low': action = 'Consolidar con similar'; break; case 'valid_low': action = 'Mejorar captura datos'; break; } return { config, queueCount, volumeAffected, pctTotal, action }; }).filter(s => s.queueCount > 0); // Insight contextual const isHighCountLowVolume = queuesWithFlags.length > 20 && pctVolumeRedFlags < 15; const isLowCountHighVolume = queuesWithFlags.length < 10 && pctVolumeRedFlags > 20; const dominantFlag = flagStats.reduce((a, b) => a.volumeAffected > b.volumeAffected ? a : b, flagStats[0]); // Mostrar top 15 en tabla const displayQueues = queuesWithFlags.slice(0, 15); return ( {/* Header */}

Skills con Red Flags

Colas que requieren intervención antes de automatizar

{/* RESUMEN DE IMPACTO */}
{queuesWithFlags.length}
Colas Afectadas
{Math.round((queuesWithFlags.length / allQueues.length) * 100)}% del total
{totalVolumeRedFlags >= 1000 ? `${(totalVolumeRedFlags/1000).toFixed(0)}K` : totalVolumeRedFlags}
Volumen Afectado
{pctVolumeRedFlags.toFixed(1)}% del total
{costeAnualRedFlags >= 1000000 ? `€${(costeAnualRedFlags / 1000000).toFixed(1)}M` : `€${(costeAnualRedFlags / 1000).toFixed(0)}K`}
Coste Bloqueado/año
{/* INSIGHT */}
Insight:{' '} {isHighCountLowVolume && ( <> Muchas colas ({queuesWithFlags.length}) pero bajo volumen ({pctVolumeRedFlags.toFixed(1)}%). Prioridad: Consolidar colas similares para ganar escala. )} {isLowCountHighVolume && ( <> Pocas colas ({queuesWithFlags.length}) concentran alto volumen ({pctVolumeRedFlags.toFixed(1)}%). Prioridad: Atacar estas colas primero para máximo impacto. )} {!isHighCountLowVolume && !isLowCountHighVolume && dominantFlag && ( <> Red flag dominante: {dominantFlag.config.label} ({dominantFlag.queueCount} colas). Acción: {dominantFlag.action}. )}
{/* DISTRIBUCIÓN DE RED FLAGS */}

DISTRIBUCIÓN DE RED FLAGS

{flagStats.map(stat => ( ))}
Red Flag Colas Vol. Afectado % Total Acción Recomendada
{stat.config.label}
{stat.config.description}
{stat.queueCount} {stat.volumeAffected.toLocaleString()} 10 ? 'text-red-600' : ''}`} style={{ color: stat.pctTotal <= 10 ? COLORS.medium : undefined }} > {stat.pctTotal.toFixed(1)}% {stat.action}
{/* PRIORIDAD */}
⚠️
PRIORIDAD:{' '} {flagStats.find(s => s.config.id === 'cv_high')?.queueCount && flagStats.find(s => s.config.id === 'cv_high')!.queueCount > 0 ? ( <> Resolver primero colas con CV >120% — son las más impredecibles y bloquean cualquier automatización efectiva. ) : flagStats.find(s => s.config.id === 'transfer_high')?.queueCount && flagStats.find(s => s.config.id === 'transfer_high')!.queueCount > 0 ? ( <> Priorizar colas con Transfer >50% — alta dependencia de escalado indica complejidad que debe simplificarse. ) : flagStats.find(s => s.config.id === 'volume_low')?.queueCount && flagStats.find(s => s.config.id === 'volume_low')!.queueCount > 0 ? ( <> Consolidar colas con Vol <50 — el bajo volumen no justifica inversión individual. ) : ( <> Mejorar calidad de datos antes de cualquier iniciativa de automatización. )}
{/* Botón para ver detalle de colas */}
{/* Tabla de colas con Red Flags (colapsable) */} {showTable && (
{displayQueues.map((qf, idx) => ( ))}
Cola Skill Volumen CV AHT Transfer Red Flags
{qf.queue.original_queue_id} {(qf.queue as any).skillName} {qf.queue.volume.toLocaleString()} 120 ? 'text-red-600' : ''}`} style={{ color: qf.queue.cv_aht <= 120 ? COLORS.dark : undefined }}> {qf.queue.cv_aht.toFixed(0)}% 50 ? 'text-red-600' : ''}`} style={{ color: qf.queue.transfer_rate <= 50 ? COLORS.dark : undefined }}> {qf.queue.transfer_rate.toFixed(0)}%
{qf.flags.map(flag => ( ))}
{queuesWithFlags.length > 15 && (
Mostrando top 15 de {queuesWithFlags.length} colas (ordenadas por volumen)
)}
)}
); } // v3.11: Umbrales para highlighting de métricas const METRIC_THRESHOLDS = { fcr: { critical: 50, warning: 60, good: 70, benchmark: 68 }, cv_aht: { critical: 100, warning: 75, good: 60 }, transfer: { critical: 25, warning: 15, good: 10, benchmark: 12 } }; // v3.11: Evaluar métrica y devolver estilo + mensaje function getMetricStatus(value: number, metric: 'fcr' | 'cv_aht' | 'transfer'): { className: string; isCritical: boolean; message: string; } { const thresholds = METRIC_THRESHOLDS[metric]; if (metric === 'fcr') { // Mayor es mejor if (value < thresholds.critical) { return { className: 'text-red-600 font-bold', isCritical: true, message: `FCR ${value.toFixed(0)}% muy por debajo del benchmark (${thresholds.benchmark}%)` }; } if (value < thresholds.warning) { return { className: 'text-amber-600 font-medium', isCritical: false, message: '' }; } return { className: 'text-emerald-600', isCritical: false, message: '' }; } // Para CV y Transfer, menor es mejor if (value > thresholds.critical) { return { className: 'text-red-600 font-bold', isCritical: true, message: metric === 'cv_aht' ? `CV ${value.toFixed(0)}% indica proceso muy inestable/impredecible` : `Transfer ${value.toFixed(0)}% muy alto — revisar routing y capacitación` }; } if (value > thresholds.warning) { return { className: 'text-amber-600 font-medium', isCritical: false, message: '' }; } return { className: 'text-gray-600', isCritical: false, message: '' }; } // v3.11: Componente para métricas con highlighting condicional function MetricCell({ value, metric, suffix = '%' }: { value: number; metric: 'fcr' | 'cv_aht' | 'transfer'; suffix?: string; }) { const status = getMetricStatus(value, metric); return ( {value.toFixed(0)}{suffix} {status.isCritical && ( )} ); } // v3.4: Sección secundaria de Skills sin colas AUTOMATE (requieren optimización) function SkillsToOptimizeSection({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { const [showAll, setShowAll] = useState(false); // Filtrar skills sin colas AUTOMATE const skillsToOptimize = drilldownData.filter(d => !d.isPriorityCandidate); if (skillsToOptimize.length === 0) { return null; } // Mostrar top 20 o todos const displaySkills = showAll ? skillsToOptimize : skillsToOptimize.slice(0, 20); // Calcular totales const totalVolume = skillsToOptimize.reduce((sum, s) => sum + s.volume, 0); const totalCost = skillsToOptimize.reduce((sum, s) => sum + (s.annualCost || 0), 0); // v3.4: Contar colas por Tier en skills a optimizar const allQueuesInOptimize = skillsToOptimize.flatMap(s => s.originalQueues); const tierCounts = { ASSIST: allQueuesInOptimize.filter(q => q.tier === 'ASSIST').length, AUGMENT: allQueuesInOptimize.filter(q => q.tier === 'AUGMENT').length, 'HUMAN-ONLY': allQueuesInOptimize.filter(q => q.tier === 'HUMAN-ONLY').length }; return (
{/* Header */}

Skills sin colas AUTOMATE

Procesos tier ASSIST/AUGMENT/HUMAN — requieren optimización antes de automatizar

{skillsToOptimize.length}

skills

{/* v3.4: Resumen por Tier */}
{tierCounts.ASSIST} ASSIST
{tierCounts.AUGMENT} AUGMENT
{tierCounts['HUMAN-ONLY'] > 0 && (
{tierCounts['HUMAN-ONLY']} HUMAN
)} | Volumen: {totalVolume.toLocaleString()} Coste: {formatCurrency(totalCost)}
{/* Tabla */}
{displaySkills.map((item, idx) => { // v3.4: Calcular tier dominante del skill const skillTierVolumes = { ASSIST: item.originalQueues.filter(q => q.tier === 'ASSIST').reduce((s, q) => s + q.volume, 0), AUGMENT: item.originalQueues.filter(q => q.tier === 'AUGMENT').reduce((s, q) => s + q.volume, 0), 'HUMAN-ONLY': item.originalQueues.filter(q => q.tier === 'HUMAN-ONLY').reduce((s, q) => s + q.volume, 0) }; const dominantTier = (Object.keys(skillTierVolumes) as ('ASSIST' | 'AUGMENT' | 'HUMAN-ONLY')[]) .reduce((a, b) => skillTierVolumes[a] > skillTierVolumes[b] ? a : b) as AgenticTier; return ( ); })}
Queue Skill Colas Volumen AHT CV AHT Transfer FCR Tier Dom.
{item.skill} {item.originalQueues.length} {item.volume.toLocaleString()} {formatAHT(item.aht_mean)}
{/* Footer */} {skillsToOptimize.length > 20 && (
)}
); } // v3.6: Sección de conexión con Roadmap function RoadmapConnectionSection({ drilldownData }: { drilldownData: DrilldownDataPoint[] }) { // Extraer todas las colas const allQueues = drilldownData.flatMap(skill => skill.originalQueues.map(q => ({ ...q, skillName: skill.skill })) ); const totalVolume = allQueues.reduce((sum, q) => sum + q.volume, 0); // AUTOMATE queues (Quick Wins) const automateQueues = allQueues.filter(q => q.tier === 'AUTOMATE'); const automateVolume = automateQueues.reduce((sum, q) => sum + q.volume, 0); // ASSIST queues (Wave 1 target) const assistQueues = allQueues.filter(q => q.tier === 'ASSIST'); const assistVolume = assistQueues.reduce((sum, q) => sum + q.volume, 0); const assistPct = totalVolume > 0 ? (assistVolume / totalVolume) * 100 : 0; // HUMAN-ONLY queues with high transfer (Wave 1 focus) const humanOnlyHighTransfer = allQueues.filter(q => q.tier === 'HUMAN-ONLY' && q.transfer_rate > 50 ); // v4.2: Cálculo de ahorros alineado con modelo TCO del Roadmap // Fórmula: (Vol/11) × 12 × Rate × (CPI_humano - CPI_target) // IMPORTANTE: El volumen es de 11 meses, se convierte a anual const CPI_HUMANO = 2.33; const CPI_BOT = 0.15; const CPI_ASSIST_TARGET = 1.50; const RATE_AUTOMATE = 0.70; // 70% contención const RATE_ASSIST = 0.30; // 30% deflection // Quick Wins (AUTOMATE): 70% de interacciones pueden ser atendidas por bot const annualSavingsAutomate = Math.round((automateVolume / DATA_PERIOD_MONTHS) * 12 * RATE_AUTOMATE * (CPI_HUMANO - CPI_BOT)); const monthlySavingsAutomate = Math.round(annualSavingsAutomate / 12); // Potential savings from ASSIST (si implementan Copilot): 30% deflection const potentialAnnualAssist = Math.round((assistVolume / DATA_PERIOD_MONTHS) * 12 * RATE_ASSIST * (CPI_HUMANO - CPI_ASSIST_TARGET)); // Get top skills with AUTOMATE queues const skillsWithAutomate = drilldownData .filter(skill => skill.originalQueues.some(q => q.tier === 'AUTOMATE')) .map(skill => skill.skill) .slice(0, 3); // Get top skills needing Wave 1 (high HUMAN-ONLY %) const skillsNeedingWave1 = drilldownData .map(skill => { const humanVolume = skill.originalQueues .filter(q => q.tier === 'HUMAN-ONLY') .reduce((s, q) => s + q.volume, 0); const skillVolume = skill.originalQueues.reduce((s, q) => s + q.volume, 0); const humanPct = skillVolume > 0 ? (humanVolume / skillVolume) * 100 : 0; return { skill: skill.skill, humanPct }; }) .filter(s => s.humanPct > 50) .sort((a, b) => b.humanPct - a.humanPct) .slice(0, 2); // Don't render if no data if (automateQueues.length === 0 && assistQueues.length === 0) { return null; } return (
{/* Header */}

PRÓXIMOS PASOS → ROADMAP

BASADO EN ESTE ANÁLISIS:

{/* Quick Wins */} {automateQueues.length > 0 && (
QUICK WINS INMEDIATOS (sin Wave 1)

{automateQueues.length} colas AUTOMATE con{' '} {(automateVolume / 1000).toFixed(0)}K interacciones/mes

Ahorro potencial: €{(annualSavingsAutomate / 1000000).toFixed(1)}M/año (70% contención × €2.18/int)

{skillsWithAutomate.length > 0 && (

Skills: {skillsWithAutomate.join(', ')}

)}

→ Alineado con Wave 4 del Roadmap. Pueden implementarse en paralelo a Wave 1.

)} {/* Wave 1: Foundation */} {assistQueues.length > 0 && (
🔧 WAVE 1-3: FOUNDATION → ASSIST ({assistQueues.length} colas)

{(assistVolume / 1000).toFixed(0)}K interacciones/mes en tier ASSIST

{skillsNeedingWave1.length > 0 && (

Foco Wave 1: Reducir transfer en{' '} {skillsNeedingWave1.map(s => s.skill).join(' & ')}{' '} ({Math.round(skillsNeedingWave1[0]?.humanPct || 0)}% HUMAN)

)}

Potencial con Copilot:{' '} €{potentialAnnualAssist >= 1000000 ? `${(potentialAnnualAssist / 1000000).toFixed(1)}M` : `${(potentialAnnualAssist / 1000).toFixed(0)}K` }/año (30% deflection × €0.83/int)

→ Requiere Wave 1 (Foundation) para habilitar Copilot en Wave 3

)} {/* Link to Roadmap */}
👉 Ver pestaña Roadmap para plan detallado
); } export function AgenticReadinessTab({ data, onTabChange }: AgenticReadinessTabProps) { const { t } = useTranslation(); const redFlagConfigs = getRedFlagConfigs(t); // Debug: Log drilldown data status console.log('🔍 AgenticReadinessTab - drilldownData:', { exists: !!data.drilldownData, length: data.drilldownData?.length || 0, sample: data.drilldownData?.slice(0, 2) }); // Calculate factors from real data (para mostrar detalle de dimensiones) const factors = calculateFactorsFromData(data.heatmapData); // v3.4: Extraer todas las colas (original_queue_id) de drilldownData const allQueues = data.drilldownData?.flatMap(skill => skill.originalQueues) || []; const totalQueues = allQueues.length; // v3.4: Calcular conteos y volúmenes por Tier const tierData = { AUTOMATE: { count: allQueues.filter(q => q.tier === 'AUTOMATE').length, volume: allQueues.filter(q => q.tier === 'AUTOMATE').reduce((s, q) => s + q.volume, 0) }, ASSIST: { count: allQueues.filter(q => q.tier === 'ASSIST').length, volume: allQueues.filter(q => q.tier === 'ASSIST').reduce((s, q) => s + q.volume, 0) }, AUGMENT: { count: allQueues.filter(q => q.tier === 'AUGMENT').length, volume: allQueues.filter(q => q.tier === 'AUGMENT').reduce((s, q) => s + q.volume, 0) }, 'HUMAN-ONLY': { count: allQueues.filter(q => q.tier === 'HUMAN-ONLY').length, volume: allQueues.filter(q => q.tier === 'HUMAN-ONLY').reduce((s, q) => s + q.volume, 0) } }; // v3.4: Agentic Readiness Score = Volumen en colas AUTOMATE / Volumen Total const totalVolume = allQueues.reduce((sum, q) => sum + q.volume, 0); const automatizableVolume = tierData.AUTOMATE.volume; const agenticReadinessPercent = totalVolume > 0 ? (automatizableVolume / totalVolume) * 100 : 0; // Count skills (queue_skill level) const totalSkills = data.drilldownData?.length || data.heatmapData.length; return (
{/* SECCIÓN 0: Introducción Metodológica (colapsable) */} {/* SECCIÓN 1: Cabecera Agentic Readiness Score - Visión Global */} {/* SECCIÓN 2-5: Desglose por Colas en 4 Tablas por Tier */} {data.drilldownData && data.drilldownData.length > 0 ? ( <> {/* TABLA 1: Colas AUTOMATE - Listas para automatización */} {/* TABLA 2: Colas ASSIST - Candidatas a Copilot */} {/* TABLA 3: Colas AUGMENT - Requieren optimización */} {/* TABLA 4: Colas HUMAN-ONLY - Agrupadas por razón/red flag */} ) : ( /* Fallback a tabla por Línea de Negocio si no hay drilldown data */ )} {/* Link al Roadmap */} {onTabChange && (
)}
); } export default AgenticReadinessTab;