import React from 'react'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { ChevronRight, TrendingUp, TrendingDown, Minus, AlertTriangle, Lightbulb, DollarSign, Clock } from 'lucide-react'; import type { AnalysisData, DimensionAnalysis, Finding, Recommendation, HeatmapDataPoint } from '../../types'; import { Card, Badge, } from '../ui'; import { cn, COLORS, STATUS_CLASSES, getStatusFromScore, formatCurrency, formatNumber, formatPercent, } from '../../config/designSystem'; interface DimensionAnalysisTabProps { data: AnalysisData; } // ========== HALLAZGO CLAVE CON IMPACTO ECONÓMICO ========== interface CausalAnalysis { finding: string; probableCause: string; economicImpact: number; recommendation: string; severity: 'critical' | 'warning' | 'info'; } // v3.11: Interfaz extendida para incluir fórmula de cálculo interface CausalAnalysisExtended extends CausalAnalysis { impactFormula?: string; // Explicación de cómo se calculó el impacto hasRealData: boolean; // True si hay datos reales para calcular timeSavings?: string; // Ahorro de tiempo para dar credibilidad al impacto económico } // Genera hallazgo clave basado en dimensión y datos function generateCausalAnalysis( dimension: DimensionAnalysis, heatmapData: HeatmapDataPoint[], economicModel: { currentAnnualCost: number }, t: (key: string, options?: any) => string, staticConfig?: { cost_per_hour: number }, dateRange?: { min: string; max: string } ): CausalAnalysisExtended[] { const analyses: CausalAnalysisExtended[] = []; const totalVolume = heatmapData.reduce((sum, h) => sum + h.volume, 0); // Coste horario del agente desde config (default €20 si no está definido) const HOURLY_COST = staticConfig?.cost_per_hour ?? 20; // Calcular factor de anualización basado en el período de datos // Si tenemos dateRange, calculamos cuántos días cubre y extrapolamos a año let annualizationFactor = 1; // Por defecto, asumimos que los datos ya son anuales if (dateRange?.min && dateRange?.max) { const startDate = new Date(dateRange.min); const endDate = new Date(dateRange.max); const daysCovered = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1); annualizationFactor = 365 / daysCovered; } // v3.11: CPI consistente con Executive Summary - benchmark aerolíneas p50 const CPI_TCO = 3.50; // Benchmark aerolíneas (p50) para cálculos de impacto // Usar CPI pre-calculado de heatmapData si existe, sino calcular desde annual_cost/cost_volume // IMPORTANTE: Mismo cálculo que ExecutiveSummaryTab para consistencia const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0); const totalAnnualCost = heatmapData.reduce((sum, h) => sum + (h.annual_cost || 0), 0); const hasCpiField = heatmapData.some(h => h.cpi !== undefined && h.cpi > 0); const CPI = hasCpiField ? (totalCostVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.cpi || 0) * (h.cost_volume || h.volume), 0) / totalCostVolume : 0) : (totalCostVolume > 0 ? totalAnnualCost / totalCostVolume : 0); // Calcular métricas agregadas const avgCVAHT = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.variability?.cv_aht || 0) * h.volume, 0) / totalVolume : 0; const avgTransferRate = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + h.metrics.transfer_rate * h.volume, 0) / totalVolume : 0; // Usar FCR Técnico (100 - transfer_rate) en lugar de FCR Real (con filtro recontacto 7d) // FCR Técnico es más comparable con benchmarks de industria const avgFCR = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate)) * h.volume, 0) / totalVolume : 0; const avgAHT = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + h.aht_seconds * h.volume, 0) / totalVolume : 0; const avgCSAT = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.metrics?.csat || 0) * h.volume, 0) / totalVolume : 0; const avgHoldTime = totalVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.metrics?.hold_time || 0) * h.volume, 0) / totalVolume : 0; // Skills con problemas específicos const skillsHighCV = heatmapData.filter(h => (h.variability?.cv_aht || 0) > 100); // Usar FCR Técnico para identificar skills con bajo FCR const skillsLowFCR = heatmapData.filter(h => (h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate)) < 50); const skillsHighTransfer = heatmapData.filter(h => h.metrics.transfer_rate > 20); // Parsear P50 AHT del KPI del header para consistencia visual // El KPI puede ser "345s (P50)" o similar const parseKpiAhtSeconds = (kpiValue: string): number | null => { const match = kpiValue.match(/(\d+)s/); return match ? parseInt(match[1], 10) : null; }; switch (dimension.name) { case 'operational_efficiency': // Obtener P50 AHT del header para mostrar valor consistente const p50Aht = parseKpiAhtSeconds(dimension.kpi.value) ?? avgAHT; // Eficiencia Operativa: enfocada en AHT (valor absoluto) // CV AHT se analiza en Complejidad & Predictibilidad (best practice) const hasHighAHT = p50Aht > 300; // 5:00 benchmark const ahtBenchmark = 300; // 5:00 objetivo if (hasHighAHT) { // Calcular impacto económico por AHT excesivo const excessSeconds = p50Aht - ahtBenchmark; const annualVolume = Math.round(totalVolume * annualizationFactor); const excessHours = Math.round((excessSeconds / 3600) * annualVolume); const ahtExcessCost = Math.round(excessHours * HOURLY_COST); // Estimar ahorro con solución Copilot (25-30% reducción AHT) const copilotSavings = Math.round(ahtExcessCost * 0.28); const ahtFormatted = `${Math.floor(p50Aht / 60)}:${String(Math.round(p50Aht) % 60).padStart(2, '0')}`; analyses.push({ finding: t('dimensionAnalysis.operationalEfficiency.highAHTFinding', { aht: ahtFormatted }), probableCause: t('dimensionAnalysis.operationalEfficiency.highAHTCause'), economicImpact: ahtExcessCost, impactFormula: `${excessHours.toLocaleString()}h × €${HOURLY_COST}/h`, timeSavings: `${excessHours.toLocaleString()} horas/año en exceso de AHT`, recommendation: t('dimensionAnalysis.operationalEfficiency.highAHTRecommendation', { savings: formatCurrency(copilotSavings) }), severity: p50Aht > 420 ? 'critical' : 'warning', hasRealData: true }); } else { // AHT dentro de benchmark - mostrar estado positivo const ahtFormatted = `${Math.floor(p50Aht / 60)}:${String(Math.round(p50Aht) % 60).padStart(2, '0')}`; analyses.push({ finding: t('dimensionAnalysis.operationalEfficiency.goodAHTFinding', { aht: ahtFormatted }), probableCause: t('dimensionAnalysis.operationalEfficiency.goodAHTCause'), economicImpact: 0, impactFormula: t('dimensionAnalysis.operationalEfficiency.goodAHTImpact'), timeSavings: t('dimensionAnalysis.operationalEfficiency.goodAHTTimeSavings'), recommendation: t('dimensionAnalysis.operationalEfficiency.goodAHTRecommendation'), severity: 'info', hasRealData: true }); } break; case 'effectiveness_resolution': // Análisis principal: FCR Técnico y tasa de transferencias const annualVolumeEff = Math.round(totalVolume * annualizationFactor); const transferCount = Math.round(annualVolumeEff * (avgTransferRate / 100)); // Calcular impacto económico de transferencias const transferCostTotal = Math.round(transferCount * CPI_TCO * 0.5); // Potencial de mejora con IA const improvementPotential = avgFCR < 90 ? Math.round((90 - avgFCR) / 100 * annualVolumeEff) : 0; const potentialSavingsEff = Math.round(improvementPotential * CPI_TCO * 0.3); // Determinar severidad basada en FCR const effSeverity = avgFCR < 70 ? 'critical' : avgFCR < 85 ? 'warning' : 'info'; // Construir causa basada en datos let effCause = ''; if (avgFCR < 70) { effCause = skillsLowFCR.length > 0 ? t('dimensionAnalysis.effectiveness.criticalCause', { transfer: avgTransferRate.toFixed(0), skills: skillsLowFCR.slice(0, 2).map(s => s.skill).join(', ') }) : t('dimensionAnalysis.effectiveness.criticalCauseGeneric', { transfer: avgTransferRate.toFixed(0) }); } else if (avgFCR < 85) { effCause = t('dimensionAnalysis.effectiveness.warningCause', { transfer: avgTransferRate.toFixed(0) }); } else { effCause = t('dimensionAnalysis.effectiveness.goodCause', { transfer: avgTransferRate.toFixed(0) }); } // Construir recomendación let effRecommendation = ''; if (avgFCR < 70) { effRecommendation = t('dimensionAnalysis.effectiveness.criticalRecommendation', { savings: formatCurrency(potentialSavingsEff) }); } else if (avgFCR < 85) { effRecommendation = t('dimensionAnalysis.effectiveness.warningRecommendation'); } else { effRecommendation = t('dimensionAnalysis.effectiveness.goodRecommendation'); } analyses.push({ finding: t('dimensionAnalysis.effectiveness.finding', { fcr: avgFCR.toFixed(0), transfer: avgTransferRate.toFixed(0) }), probableCause: effCause, economicImpact: transferCostTotal, impactFormula: t('dimensionAnalysis.effectiveness.impactFormula', { count: transferCount.toLocaleString(), cpi: CPI_TCO }), timeSavings: t('dimensionAnalysis.effectiveness.timeSavings', { count: transferCount.toLocaleString(), pct: avgTransferRate.toFixed(0) }), recommendation: effRecommendation, severity: effSeverity, hasRealData: true }); break; case 'volumetry_distribution': // Análisis de concentración de volumen const topSkill = [...heatmapData].sort((a, b) => b.volume - a.volume)[0]; const topSkillPct = topSkill ? (topSkill.volume / totalVolume) * 100 : 0; if (topSkillPct > 40 && topSkill) { const annualTopSkillVolume = Math.round(topSkill.volume * annualizationFactor); const deflectionPotential = Math.round(annualTopSkillVolume * CPI_TCO * 0.20); const interactionsDeflectable = Math.round(annualTopSkillVolume * 0.20); analyses.push({ finding: t('dimensionAnalysis.volumetry.concentrationFinding', { skill: topSkill.skill, pct: topSkillPct.toFixed(0) }), probableCause: t('dimensionAnalysis.volumetry.concentrationCause'), economicImpact: deflectionPotential, impactFormula: t('dimensionAnalysis.volumetry.impactFormula', { volume: topSkill.volume.toLocaleString(), cpi: CPI_TCO }), timeSavings: t('dimensionAnalysis.volumetry.timeSavings', { volume: annualTopSkillVolume.toLocaleString(), skill: topSkill.skill, deflectable: interactionsDeflectable.toLocaleString() }), recommendation: t('dimensionAnalysis.volumetry.concentrationRecommendation', { skill: topSkill.skill, savings: formatCurrency(deflectionPotential) }), severity: 'info', hasRealData: true }); } break; case 'complexity_predictability': // KPI principal: CV AHT (predictability metric per industry standards) // Siempre mostrar análisis de CV AHT ya que es el KPI de esta dimensión const cvBenchmark = 75; // Best practice: CV AHT < 75% if (avgCVAHT > cvBenchmark) { const staffingCost = Math.round(economicModel.currentAnnualCost * 0.03); const staffingHours = Math.round(staffingCost / HOURLY_COST); const standardizationSavings = Math.round(staffingCost * 0.50); // Determinar severidad basada en CV AHT const cvSeverity = avgCVAHT > 125 ? 'critical' : avgCVAHT > 100 ? 'warning' : 'warning'; // Causa dinámica basada en nivel de variabilidad const cvCause = avgCVAHT > 125 ? t('dimensionAnalysis.complexity.highCVCauseCritical') : t('dimensionAnalysis.complexity.highCVCauseWarning'); analyses.push({ finding: t('dimensionAnalysis.complexity.highCVFinding', { cv: avgCVAHT.toFixed(0), benchmark: cvBenchmark }), probableCause: cvCause, economicImpact: staffingCost, impactFormula: t('dimensionAnalysis.complexity.highCVImpactFormula'), timeSavings: t('dimensionAnalysis.complexity.highCVTimeSavings', { hours: staffingHours.toLocaleString() }), recommendation: t('dimensionAnalysis.complexity.highCVRecommendation', { savings: formatCurrency(standardizationSavings) }), severity: cvSeverity, hasRealData: true }); } else { // CV AHT dentro de benchmark - mostrar estado positivo analyses.push({ finding: t('dimensionAnalysis.complexity.goodCVFinding', { cv: avgCVAHT.toFixed(0), benchmark: cvBenchmark }), probableCause: t('dimensionAnalysis.complexity.goodCVCause'), economicImpact: 0, impactFormula: t('dimensionAnalysis.complexity.goodCVImpactFormula'), timeSavings: t('dimensionAnalysis.complexity.goodCVTimeSavings'), recommendation: t('dimensionAnalysis.complexity.goodCVRecommendation'), severity: 'info', hasRealData: true }); } // Análisis secundario: Hold Time (proxy de complejidad) if (avgHoldTime > 45) { const excessHold = avgHoldTime - 30; const annualVolumeHold = Math.round(totalVolume * annualizationFactor); const excessHoldHours = Math.round((excessHold / 3600) * annualVolumeHold); const holdCost = Math.round(excessHoldHours * HOURLY_COST); const searchCopilotSavings = Math.round(holdCost * 0.60); analyses.push({ finding: t('dimensionAnalysis.complexity.holdTimeFinding', { holdTime: avgHoldTime.toFixed(0) }), probableCause: t('dimensionAnalysis.complexity.holdTimeCause'), economicImpact: holdCost, impactFormula: t('dimensionAnalysis.complexity.holdTimeImpactFormula', { excess: Math.round(excessHold), volume: totalVolume.toLocaleString(), cost: HOURLY_COST }), timeSavings: t('dimensionAnalysis.complexity.holdTimeTimeSavings', { hours: excessHoldHours.toLocaleString() }), recommendation: t('dimensionAnalysis.complexity.holdTimeRecommendation', { savings: formatCurrency(searchCopilotSavings) }), severity: avgHoldTime > 60 ? 'critical' : 'warning', hasRealData: true }); } break; case 'customer_satisfaction': // Solo generar análisis si hay datos de CSAT reales if (avgCSAT > 0) { if (avgCSAT < 70) { const annualVolumeCsat = Math.round(totalVolume * annualizationFactor); const customersAtRisk = Math.round(annualVolumeCsat * 0.02); const churnRisk = Math.round(customersAtRisk * 50); analyses.push({ finding: t('dimensionAnalysis.satisfaction.lowCSATFinding', { csat: avgCSAT.toFixed(0) }), probableCause: t('dimensionAnalysis.satisfaction.lowCSATCause'), economicImpact: churnRisk, impactFormula: t('dimensionAnalysis.satisfaction.lowCSATImpactFormula', { volume: totalVolume.toLocaleString() }), timeSavings: t('dimensionAnalysis.satisfaction.lowCSATTimeSavings', { customers: customersAtRisk.toLocaleString() }), recommendation: t('dimensionAnalysis.satisfaction.lowCSATRecommendation'), severity: avgCSAT < 50 ? 'critical' : 'warning', hasRealData: true }); } } break; case 'economy_cpi': case 'economy_costs': // También manejar el ID del backend // Análisis de CPI if (CPI > 3.5) { const excessCPI = CPI - CPI_TCO; const annualVolumeCpi = Math.round(totalVolume * annualizationFactor); const potentialSavings = Math.round(annualVolumeCpi * excessCPI); const excessHours = Math.round(potentialSavings / HOURLY_COST); analyses.push({ finding: t('dimensionAnalysis.economy.highCPIFinding', { cpi: CPI.toFixed(2), target: CPI_TCO }), probableCause: t('dimensionAnalysis.economy.highCPICause'), economicImpact: potentialSavings, impactFormula: t('dimensionAnalysis.economy.highCPIImpactFormula', { volume: totalVolume.toLocaleString(), excess: excessCPI.toFixed(2) }), timeSavings: t('dimensionAnalysis.economy.highCPITimeSavings', { excess: excessCPI.toFixed(2), volume: annualVolumeCpi.toLocaleString(), hours: excessHours.toLocaleString() }), recommendation: t('dimensionAnalysis.economy.highCPIRecommendation', { target: CPI_TCO }), severity: CPI > 5 ? 'critical' : 'warning', hasRealData: true }); } break; } // v3.11: NO generar fallback con impacto económico falso // Si no hay análisis específico, simplemente retornar array vacío // La UI mostrará "Sin hallazgos críticos" en lugar de un impacto inventado return analyses; } // Formateador de moneda (usa la función importada de designSystem) // v3.15: Dimension Card Component - con diseño McKinsey function DimensionCard({ dimension, findings, recommendations, causalAnalyses, delay = 0, t }: { dimension: DimensionAnalysis; findings: Finding[]; recommendations: Recommendation[]; causalAnalyses: CausalAnalysisExtended[]; delay?: number; t: (key: string, options?: any) => string; }) { const Icon = dimension.icon; const getScoreVariant = (score: number): 'success' | 'warning' | 'critical' | 'default' => { if (score < 0) return 'default'; // N/A if (score >= 70) return 'success'; if (score >= 40) return 'warning'; return 'critical'; }; const getScoreLabel = (score: number): string => { if (score < 0) return t('common.na'); if (score >= 80) return t('common.optimal'); if (score >= 60) return t('common.acceptable'); if (score >= 40) return t('common.improvable'); return t('common.critical'); }; const getSeverityConfig = (severity: string) => { if (severity === 'critical') return STATUS_CLASSES.critical; if (severity === 'warning') return STATUS_CLASSES.warning; return STATUS_CLASSES.info; }; // Get KPI trend icon const TrendIcon = dimension.kpi.changeType === 'positive' ? TrendingUp : dimension.kpi.changeType === 'negative' ? TrendingDown : Minus; const trendColor = dimension.kpi.changeType === 'positive' ? 'text-emerald-600' : dimension.kpi.changeType === 'negative' ? 'text-red-600' : 'text-gray-500'; // Calcular impacto total de esta dimensión const totalImpact = causalAnalyses.reduce((sum, a) => sum + a.economicImpact, 0); const scoreVariant = getScoreVariant(dimension.score); return ( {/* Header */}

{dimension.title}

{dimension.summary}

= 0 ? `${dimension.score} ${getScoreLabel(dimension.score)}` : `— ${t('common.na')}`} variant={scoreVariant} size="md" /> {totalImpact > 0 && (

{t('dimensionAnalysis.impact')} {formatCurrency(totalImpact)}

)}
{/* KPI Highlight */}
{dimension.kpi.label}
{dimension.kpi.value} {dimension.kpi.change && (
{dimension.kpi.change}
)}
{dimension.percentile && (
Percentil P{dimension.percentile}
)}
{/* Si no hay datos para esta dimensión (score < 0 = N/A) */} {dimension.score < 0 && (

{t('dimensionAnalysis.noDataAvailable')}

)} {/* Hallazgo Clave - Solo si hay datos */} {dimension.score >= 0 && causalAnalyses.length > 0 && (

{t('dimensionAnalysis.keyFinding')}

{causalAnalyses.map((analysis, idx) => { const config = getSeverityConfig(analysis.severity); return (
{/* Hallazgo */}

{analysis.finding}

{/* Causa probable */}

{t('dimensionAnalysis.probableCause')}

{analysis.probableCause}

{/* Impacto económico */}
{formatCurrency(analysis.economicImpact)} {t('dimensionAnalysis.annualImpact')} i
{/* Ahorro de tiempo - da credibilidad al cálculo económico */} {analysis.timeSavings && (
{analysis.timeSavings}
)} {/* Recomendación inline */}

{analysis.recommendation}

); })}
)} {/* Fallback: Hallazgos originales si no hay hallazgo clave - Solo si hay datos */} {dimension.score >= 0 && causalAnalyses.length === 0 && findings.length > 0 && (

{t('dimensionAnalysis.keyFindings')}

    {findings.slice(0, 3).map((finding, idx) => (
  • {finding.text}
  • ))}
)} {/* Si no hay análisis ni hallazgos pero sí hay datos */} {dimension.score >= 0 && causalAnalyses.length === 0 && findings.length === 0 && (

{t('dimensionAnalysis.withinAcceptable')}

)} {/* Recommendations Preview - Solo si no hay hallazgo clave y hay datos */} {dimension.score >= 0 && causalAnalyses.length === 0 && recommendations.length > 0 && (
{t('dimensionAnalysis.recommendation')} {recommendations[0].text}
)} ); } // ========== v3.16: COMPONENTE PRINCIPAL ========== export function DimensionAnalysisTab({ data }: DimensionAnalysisTabProps) { const { t } = useTranslation(); // DEBUG: Verificar CPI en dimensión vs heatmapData const economyDim = data.dimensions.find(d => d.id === 'economy_costs' || d.name === 'economy_costs' || d.id === 'economy_cpi' || d.name === 'economy_cpi' ); const heatmapData = data.heatmapData; const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0); const hasCpiField = heatmapData.some(h => h.cpi !== undefined && h.cpi > 0); const calculatedCPI = hasCpiField ? (totalCostVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.cpi || 0) * (h.cost_volume || h.volume), 0) / totalCostVolume : 0) : (totalCostVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.annual_cost || 0), 0) / totalCostVolume : 0); console.log('🔍 DimensionAnalysisTab DEBUG:'); console.log(' - economyDim found:', !!economyDim, economyDim?.id || economyDim?.name); console.log(' - economyDim.kpi.value:', economyDim?.kpi?.value); console.log(' - calculatedCPI from heatmapData:', `€${calculatedCPI.toFixed(2)}`); console.log(' - hasCpiField:', hasCpiField); console.log(' - MATCH:', economyDim?.kpi?.value === `€${calculatedCPI.toFixed(2)}`); // Filter out agentic_readiness (has its own tab) const coreDimensions = data.dimensions.filter(d => d.name !== 'agentic_readiness'); // Group findings and recommendations by dimension const getFindingsForDimension = (dimensionId: string) => data.findings.filter(f => f.dimensionId === dimensionId); const getRecommendationsForDimension = (dimensionId: string) => data.recommendations.filter(r => r.dimensionId === dimensionId); // Generar hallazgo clave para cada dimensión const getCausalAnalysisForDimension = (dimension: DimensionAnalysis) => generateCausalAnalysis(dimension, data.heatmapData, data.economicModel, t, data.staticConfig, data.dateRange); // Calcular impacto total de todas las dimensiones con datos const impactoTotal = coreDimensions .filter(d => d.score !== null && d.score !== undefined) .reduce((total, dimension) => { const analyses = getCausalAnalysisForDimension(dimension); return total + analyses.reduce((sum, a) => sum + a.economicImpact, 0); }, 0); // v3.16: Contar dimensiones por estado para el header const conDatos = coreDimensions.filter(d => d.score !== null && d.score !== undefined && d.score >= 0); const sinDatos = coreDimensions.filter(d => d.score === null || d.score === undefined || d.score < 0); return (
{/* v3.16: Header simplificado - solo título y subtítulo */}

{t('dimensionAnalysis.title')}

{t('dimensionAnalysis.dimensionsAnalyzed', { count: coreDimensions.length })} {sinDatos.length > 0 && ` ${t('dimensionAnalysis.noData', { count: sinDatos.length })}`}

{/* v3.16: Grid simple con todas las dimensiones sin agrupación */}
{coreDimensions.map((dimension, idx) => ( ))}
); } export default DimensionAnalysisTab;