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'; // ============================================ // RED FLAGS CONFIGURATION AND DETECTION // ============================================ // v3.5: Configuración de Red Flags interface RedFlagConfig { id: string; label: string; shortLabel: string; threshold: number; operator: '>' | '<'; getValue: (queue: OriginalQueueMetrics) => number; format: (value: number) => string; color: string; description: string; } const RED_FLAG_CONFIGS: RedFlagConfig[] = [ { id: 'cv_high', label: 'CV AHT Crítico', shortLabel: 'CV', threshold: 120, operator: '>', getValue: (q) => q.cv_aht, format: (v) => `${v.toFixed(0)}%`, color: 'red', description: 'Variabilidad extrema - procesos impredecibles' }, { id: 'transfer_high', label: 'Transfer Excesivo', shortLabel: 'Transfer', threshold: 50, operator: '>', getValue: (q) => q.transfer_rate, format: (v) => `${v.toFixed(0)}%`, color: 'orange', description: 'Alta complejidad - requiere escalado frecuente' }, { id: 'volume_low', label: 'Volumen Insuficiente', shortLabel: 'Vol', threshold: 50, operator: '<', getValue: (q) => q.volume, format: (v) => v.toLocaleString(), color: 'slate', description: 'ROI negativo - volumen no justifica inversión' }, { id: 'valid_low', label: 'Calidad Datos Baja', shortLabel: 'Valid', threshold: 30, operator: '<', getValue: (q) => q.volume > 0 ? (q.volumeValid / q.volume) * 100 : 0, format: (v) => `${v.toFixed(0)}%`, color: 'amber', description: 'Datos poco fiables - métricas distorsionadas' } ]; // v3.5: Detectar red flags de una cola interface DetectedRedFlag { config: RedFlagConfig; value: number; } function detectRedFlags(queue: OriginalQueueMetrics): DetectedRedFlag[] { const flags: DetectedRedFlag[] = []; for (const config of RED_FLAG_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: Componente de badge de Red Flag individual function RedFlagBadge({ flag, size = 'sm' }: { flag: DetectedRedFlag; size?: 'sm' | 'md' }) { 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 }: { queue: OriginalQueueMetrics; compact?: boolean }) { const flags = detectRedFlags(queue); if (flags.length === 0) return null; if (compact) { return (
{flags.map(flag => ( ))}
); } return (
{flags.map(flag => (
{flag.config.label}: {flag.config.format(flag.value)} (umbral: {flag.config.operator}{flag.config.threshold})
))}
); } interface AgenticReadinessTabProps { data: AnalysisData; onTabChange?: (tab: string) => void; } // 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 badge de Tier function TierBadge({ tier, size = 'sm' }: { tier: AgenticTier; size?: 'sm' | 'md' }) { const style = getTierStyle(tier); const sizeClasses = size === 'md' ? 'px-2.5 py-1 text-xs' : 'px-2 py-0.5 text-xs'; return ( {style.icon} {style.label} ); } // 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 }; } // Colores corporativos const COLORS = { primary: '#6d84e3', dark: '#3f3f3f', medium: '#b1b1b0', light: '#e4e3e3', white: '#ffffff' }; // ============================================ // 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; } // Calcular ahorro TCO por cola 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 × 12 × rate × (CPI_humano - CPI_target) const ahorroAnual = queue.volume * 12 * 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: q.fcr_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 corporativos const tierConfigs = [ { key: 'AUTOMATE', label: 'AUTOMATE', emoji: '🤖', sublabel: 'Full IA', color: COLORS.primary }, { key: 'ASSIST', label: 'ASSIST', emoji: '🤝', sublabel: 'Copilot', color: COLORS.dark }, { key: 'AUGMENT', label: 'AUGMENT', emoji: '📚', sublabel: 'Tools', color: COLORS.medium }, { key: 'HUMAN-ONLY', label: 'HUMAN', emoji: '👤', sublabel: 'Manual', color: COLORS.medium } ]; // 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 */}
{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 */}
{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 promedio ponderado const avgFCR = totalQueueVolume > 0 ? allQueues.reduce((sum, q) => sum + q.fcr_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')}`; } // Formatear moneda function formatCurrency(value: number): string { if (value >= 1000000) return `€${(value / 1000000).toFixed(1)}M`; if (value >= 1000) return `€${Math.round(value / 1000)}K`; return `€${value.toLocaleString()}`; } // 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_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); 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_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_rate.toFixed(0)}% {dataPoint.agenticScore.toFixed(1)} {formatCurrency(potentialSavings)}
)} ); } // 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) })).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; // v3.11: Coste usando modelo CPI (consistente con Roadmap y Executive Summary) const CPI_HUMANO_RF = 2.33; // €/interacción (coste unitario humano) const costeAnualRedFlags = Math.round(totalVolumeRedFlags * 12 * CPI_HUMANO_RF); const costeAnualTotal = Math.round(totalVolumeAllQueues * 12 * CPI_HUMANO_RF); const pctCosteRedFlags = costeAnualTotal > 0 ? (costeAnualRedFlags / costeAnualTotal) * 100 : 0; // Estadísticas detalladas por tipo de red flag const flagStats = RED_FLAG_CONFIGS.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 ); // v3.10: Cálculo de ahorros alineado con modelo TCO del Roadmap // Fórmula: Vol × 12 × Rate × (CPI_humano - CPI_target) 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 * 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 * 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) { // 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 (
{/* Cabecera Agentic Readiness Score - Rediseñada */} {/* Factores del Score Global */} {data.drilldownData && data.drilldownData.length > 0 && ( )} {/* v3.10: Mapa de Oportunidades de Automatización (Bubble Chart) */} {data.drilldownData && data.drilldownData.length > 0 && ( )} {/* v3.1: Primero lo positivo - Candidatos Prioritarios (panel principal expandible) */} {data.drilldownData && data.drilldownData.length > 0 ? ( <> {/* v3.5: Red Flags para colas HUMAN-ONLY */} ) : ( /* Fallback a tabla por Línea de Negocio si no hay drilldown data */ )} {/* Link al Roadmap */} {onTabChange && (
)}
); } export default AgenticReadinessTab;