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 (
{dimension.summary}
{t('dimensionAnalysis.impact')} {formatCurrency(totalImpact)}
{analysis.finding} {t('dimensionAnalysis.probableCause')} {analysis.probableCause} {analysis.recommendation}
{dimension.title}
{t('dimensionAnalysis.keyFinding')}
{causalAnalyses.map((analysis, idx) => {
const config = getSeverityConfig(analysis.severity);
return (
{t('dimensionAnalysis.keyFindings')}
{findings.slice(0, 3).map((finding, idx) => (
{t('dimensionAnalysis.dimensionsAnalyzed', { count: coreDimensions.length })} {sinDatos.length > 0 && ` ${t('dimensionAnalysis.noData', { count: sinDatos.length })}`}