refactor: implement i18n in ExecutiveSummary and DimensionAnalysis tabs (phase 2)

Successfully refactored two major tab components to use react-i18next:
- ExecutiveSummaryTab: All metrics, benchmarks, findings, tooltips, industry names
- DimensionAnalysisTab: All dimension analyses, findings, causes, recommendations

Added 140+ comprehensive translation keys to es.json and en.json:
- executiveSummary section: metrics, benchmarks, tooltips, percentiles
- dimensionAnalysis section: findings, causes, recommendations for all 6 dimensions
- industries section: all industry names
- agenticReadiness section: extensive keys for future use (400+ keys)

Note: AgenticReadinessTab refactoring deferred due to file complexity (3721 lines).
Translation keys prepared for future implementation.

Build verified successfully.

https://claude.ai/code/session_4f888c33-8937-4db8-8a9d-ddc9ac51a725
This commit is contained in:
Claude
2026-02-06 18:55:47 +00:00
parent 9bc1a1c0d3
commit 94247ceb9a
4 changed files with 794 additions and 176 deletions

View File

@@ -1,4 +1,5 @@
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';
@@ -42,6 +43,7 @@ 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[] {
@@ -129,28 +131,29 @@ function generateCausalAnalysis(
// Estimar ahorro con solución Copilot (25-30% reducción AHT)
const copilotSavings = Math.round(ahtExcessCost * 0.28);
// Causa basada en AHT elevado
const cause = 'Agentes dedican tiempo excesivo a búsqueda manual de información, navegación entre sistemas y tareas repetitivas.';
const ahtFormatted = `${Math.floor(p50Aht / 60)}:${String(Math.round(p50Aht) % 60).padStart(2, '0')}`;
analyses.push({
finding: `AHT elevado: P50 ${Math.floor(p50Aht / 60)}:${String(Math.round(p50Aht) % 60).padStart(2, '0')} (benchmark: 5:00)`,
probableCause: cause,
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: `Desplegar Copilot IA para agentes: (1) Auto-búsqueda en KB; (2) Sugerencias contextuales en tiempo real; (3) Scripts guiados para casos frecuentes. Reducción esperada: 20-30% AHT. Ahorro: ${formatCurrency(copilotSavings)}/año.`,
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: `AHT dentro de benchmark: P50 ${Math.floor(p50Aht / 60)}:${String(Math.round(p50Aht) % 60).padStart(2, '0')} (benchmark: 5:00)`,
probableCause: 'Tiempos de gestión eficientes. Procesos operativos optimizados.',
finding: t('dimensionAnalysis.operationalEfficiency.goodAHTFinding', { aht: ahtFormatted }),
probableCause: t('dimensionAnalysis.operationalEfficiency.goodAHTCause'),
economicImpact: 0,
impactFormula: 'Sin exceso de coste por AHT',
timeSavings: 'Operación eficiente',
recommendation: 'Mantener nivel actual. Considerar Copilot para mejora continua y reducción adicional de tiempos en casos complejos.',
impactFormula: t('dimensionAnalysis.operationalEfficiency.goodAHTImpact'),
timeSavings: t('dimensionAnalysis.operationalEfficiency.goodAHTTimeSavings'),
recommendation: t('dimensionAnalysis.operationalEfficiency.goodAHTRecommendation'),
severity: 'info',
hasRealData: true
});
@@ -176,30 +179,42 @@ function generateCausalAnalysis(
let effCause = '';
if (avgFCR < 70) {
effCause = skillsLowFCR.length > 0
? `Alta tasa de transferencias (${avgTransferRate.toFixed(0)}%) indica falta de herramientas o autoridad. Crítico en ${skillsLowFCR.slice(0, 2).map(s => s.skill).join(', ')}.`
: `Transferencias elevadas (${avgTransferRate.toFixed(0)}%): agentes sin información contextual o sin autoridad para resolver.`;
? 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 = `Transferencias del ${avgTransferRate.toFixed(0)}% indican oportunidad de mejora con asistencia IA para casos complejos.`;
effCause = t('dimensionAnalysis.effectiveness.warningCause', { transfer: avgTransferRate.toFixed(0) });
} else {
effCause = `FCR Técnico en nivel óptimo. Transferencias del ${avgTransferRate.toFixed(0)}% principalmente en casos que requieren escalación legítima.`;
effCause = t('dimensionAnalysis.effectiveness.goodCause', { transfer: avgTransferRate.toFixed(0) });
}
// Construir recomendación
let effRecommendation = '';
if (avgFCR < 70) {
effRecommendation = `Desplegar Knowledge Copilot con búsqueda inteligente en KB + Guided Resolution Copilot para casos complejos. Objetivo: FCR >85%. Potencial ahorro: ${formatCurrency(potentialSavingsEff)}/año.`;
effRecommendation = t('dimensionAnalysis.effectiveness.criticalRecommendation', { savings: formatCurrency(potentialSavingsEff) });
} else if (avgFCR < 85) {
effRecommendation = `Implementar Copilot de asistencia en tiempo real: sugerencias contextuales + conexión con expertos virtuales para reducir transferencias. Objetivo: FCR >90%.`;
effRecommendation = t('dimensionAnalysis.effectiveness.warningRecommendation');
} else {
effRecommendation = `Mantener nivel actual. Considerar IA para análisis de transferencias legítimas y optimización de enrutamiento predictivo.`;
effRecommendation = t('dimensionAnalysis.effectiveness.goodRecommendation');
}
analyses.push({
finding: `FCR Técnico: ${avgFCR.toFixed(0)}% | Transferencias: ${avgTransferRate.toFixed(0)}% (benchmark: FCR >85%, Transfer <10%)`,
finding: t('dimensionAnalysis.effectiveness.finding', {
fcr: avgFCR.toFixed(0),
transfer: avgTransferRate.toFixed(0)
}),
probableCause: effCause,
economicImpact: transferCostTotal,
impactFormula: `${transferCount.toLocaleString()} transferencias/año ×${CPI_TCO}/int × 50% coste adicional`,
timeSavings: `${transferCount.toLocaleString()} transferencias/año (${avgTransferRate.toFixed(0)}% del volumen)`,
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
@@ -215,12 +230,25 @@ function generateCausalAnalysis(
const deflectionPotential = Math.round(annualTopSkillVolume * CPI_TCO * 0.20);
const interactionsDeflectable = Math.round(annualTopSkillVolume * 0.20);
analyses.push({
finding: `Concentración de volumen: ${topSkill.skill} representa ${topSkillPct.toFixed(0)}% del total`,
probableCause: `Alta concentración en un skill indica consultas repetitivas con potencial de automatización.`,
finding: t('dimensionAnalysis.volumetry.concentrationFinding', {
skill: topSkill.skill,
pct: topSkillPct.toFixed(0)
}),
probableCause: t('dimensionAnalysis.volumetry.concentrationCause'),
economicImpact: deflectionPotential,
impactFormula: `${topSkill.volume.toLocaleString()} int × anualización ×${CPI_TCO} × 20% deflexión potencial`,
timeSavings: `${annualTopSkillVolume.toLocaleString()} interacciones/año en ${topSkill.skill} (${interactionsDeflectable.toLocaleString()} automatizables)`,
recommendation: `Analizar tipologías de ${topSkill.skill} para deflexión a autoservicio o agente virtual. Potencial: ${formatCurrency(deflectionPotential)}/año.`,
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
});
@@ -242,28 +270,34 @@ function generateCausalAnalysis(
// Causa dinámica basada en nivel de variabilidad
const cvCause = avgCVAHT > 125
? 'Dispersión extrema en tiempos de atención impide planificación efectiva de recursos. Probable falta de scripts o procesos estandarizados.'
: 'Variabilidad moderada en tiempos indica oportunidad de estandarización para mejorar planificación WFM.';
? t('dimensionAnalysis.complexity.highCVCauseCritical')
: t('dimensionAnalysis.complexity.highCVCauseWarning');
analyses.push({
finding: `CV AHT elevado: ${avgCVAHT.toFixed(0)}% (benchmark: <${cvBenchmark}%)`,
finding: t('dimensionAnalysis.complexity.highCVFinding', {
cv: avgCVAHT.toFixed(0),
benchmark: cvBenchmark
}),
probableCause: cvCause,
economicImpact: staffingCost,
impactFormula: `~3% del coste operativo por ineficiencia de staffing`,
timeSavings: `~${staffingHours.toLocaleString()} horas/año en sobre/subdimensionamiento`,
recommendation: `Implementar scripts guiados por IA que estandaricen la atención. Reducción esperada: -50% variabilidad. Ahorro: ${formatCurrency(standardizationSavings)}/año.`,
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: `CV AHT dentro de benchmark: ${avgCVAHT.toFixed(0)}% (benchmark: <${cvBenchmark}%)`,
probableCause: 'Tiempos de atención consistentes. Buena estandarización de procesos.',
finding: t('dimensionAnalysis.complexity.goodCVFinding', {
cv: avgCVAHT.toFixed(0),
benchmark: cvBenchmark
}),
probableCause: t('dimensionAnalysis.complexity.goodCVCause'),
economicImpact: 0,
impactFormula: 'Sin impacto por variabilidad',
timeSavings: 'Planificación WFM eficiente',
recommendation: 'Mantener nivel actual. Analizar casos atípicos para identificar oportunidades de mejora continua.',
impactFormula: t('dimensionAnalysis.complexity.goodCVImpactFormula'),
timeSavings: t('dimensionAnalysis.complexity.goodCVTimeSavings'),
recommendation: t('dimensionAnalysis.complexity.goodCVRecommendation'),
severity: 'info',
hasRealData: true
});
@@ -277,12 +311,16 @@ function generateCausalAnalysis(
const holdCost = Math.round(excessHoldHours * HOURLY_COST);
const searchCopilotSavings = Math.round(holdCost * 0.60);
analyses.push({
finding: `Hold time elevado: ${avgHoldTime.toFixed(0)}s promedio (benchmark: <30s)`,
probableCause: 'Agentes ponen cliente en espera para buscar información. Sistemas no presentan datos de forma contextual.',
finding: t('dimensionAnalysis.complexity.holdTimeFinding', { holdTime: avgHoldTime.toFixed(0) }),
probableCause: t('dimensionAnalysis.complexity.holdTimeCause'),
economicImpact: holdCost,
impactFormula: `Exceso ${Math.round(excessHold)}s × ${totalVolume.toLocaleString()} int × anualización ×${HOURLY_COST}/h`,
timeSavings: `${excessHoldHours.toLocaleString()} horas/año de cliente en espera`,
recommendation: `Desplegar vista 360° con contexto automático: historial, productos y acciones sugeridas visibles al contestar. Reducción esperada: -60% hold time. Ahorro: ${formatCurrency(searchCopilotSavings)}/año.`,
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
});
@@ -297,12 +335,12 @@ function generateCausalAnalysis(
const customersAtRisk = Math.round(annualVolumeCsat * 0.02);
const churnRisk = Math.round(customersAtRisk * 50);
analyses.push({
finding: `CSAT por debajo del objetivo: ${avgCSAT.toFixed(0)}% (benchmark: >80%)`,
probableCause: 'Clientes insatisfechos por esperas, falta de resolución o experiencia de atención deficiente.',
finding: t('dimensionAnalysis.satisfaction.lowCSATFinding', { csat: avgCSAT.toFixed(0) }),
probableCause: t('dimensionAnalysis.satisfaction.lowCSATCause'),
economicImpact: churnRisk,
impactFormula: `${totalVolume.toLocaleString()} clientes × anualización × 2% riesgo churn × €50 valor`,
timeSavings: `${customersAtRisk.toLocaleString()} clientes/año en riesgo de fuga`,
recommendation: `Implementar programa VoC: encuestas post-contacto + análisis de causas raíz + acción correctiva en 48h. Objetivo: CSAT >80%.`,
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
});
@@ -319,12 +357,22 @@ function generateCausalAnalysis(
const potentialSavings = Math.round(annualVolumeCpi * excessCPI);
const excessHours = Math.round(potentialSavings / HOURLY_COST);
analyses.push({
finding: `CPI por encima del benchmark: €${CPI.toFixed(2)} (objetivo: €${CPI_TCO})`,
probableCause: 'Coste por interacción elevado por AHT alto, baja ocupación o estructura de costes ineficiente.',
finding: t('dimensionAnalysis.economy.highCPIFinding', {
cpi: CPI.toFixed(2),
target: CPI_TCO
}),
probableCause: t('dimensionAnalysis.economy.highCPICause'),
economicImpact: potentialSavings,
impactFormula: `${totalVolume.toLocaleString()} int × anualización ×${excessCPI.toFixed(2)} exceso CPI`,
timeSavings: `${excessCPI.toFixed(2)} exceso/int × ${annualVolumeCpi.toLocaleString()} int = ${excessHours.toLocaleString()}h equivalentes`,
recommendation: `Optimizar mix de canales + reducir AHT con automatización + revisar modelo de staffing. Objetivo: CPI <€${CPI_TCO}.`,
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
});
@@ -347,13 +395,15 @@ function DimensionCard({
findings,
recommendations,
causalAnalyses,
delay = 0
delay = 0,
t
}: {
dimension: DimensionAnalysis;
findings: Finding[];
recommendations: Recommendation[];
causalAnalyses: CausalAnalysisExtended[];
delay?: number;
t: (key: string, options?: any) => string;
}) {
const Icon = dimension.icon;
@@ -365,11 +415,11 @@ function DimensionCard({
};
const getScoreLabel = (score: number): string => {
if (score < 0) return 'N/A';
if (score >= 80) return 'Óptimo';
if (score >= 60) return 'Aceptable';
if (score >= 40) return 'Mejorable';
return 'Crítico';
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) => {
@@ -410,13 +460,13 @@ function DimensionCard({
</div>
<div className="text-right">
<Badge
label={dimension.score >= 0 ? `${dimension.score} ${getScoreLabel(dimension.score)}` : '— N/A'}
label={dimension.score >= 0 ? `${dimension.score} ${getScoreLabel(dimension.score)}` : `${t('common.na')}`}
variant={scoreVariant}
size="md"
/>
{totalImpact > 0 && (
<p className="text-xs text-red-600 font-medium mt-1">
Impacto: {formatCurrency(totalImpact)}
{t('dimensionAnalysis.impact')} {formatCurrency(totalImpact)}
</p>
)}
</div>
@@ -459,7 +509,7 @@ function DimensionCard({
<div className="p-3 bg-gray-50 rounded-lg border border-gray-200">
<p className="text-sm text-gray-500 italic flex items-center gap-2">
<Minus className="w-4 h-4" />
Sin datos disponibles para esta dimensión.
{t('dimensionAnalysis.noDataAvailable')}
</p>
</div>
</div>
@@ -469,7 +519,7 @@ function DimensionCard({
{dimension.score >= 0 && causalAnalyses.length > 0 && (
<div className="p-4 space-y-3">
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
Hallazgo Clave
{t('dimensionAnalysis.keyFinding')}
</h4>
{causalAnalyses.map((analysis, idx) => {
const config = getSeverityConfig(analysis.severity);
@@ -485,7 +535,7 @@ function DimensionCard({
{/* Causa probable */}
<div className="ml-6 mb-2">
<p className="text-xs text-gray-500 font-medium mb-0.5">Causa probable:</p>
<p className="text-xs text-gray-500 font-medium mb-0.5">{t('dimensionAnalysis.probableCause')}</p>
<p className="text-xs text-gray-700">{analysis.probableCause}</p>
</div>
@@ -498,7 +548,7 @@ function DimensionCard({
<span className="text-xs font-bold text-red-600">
{formatCurrency(analysis.economicImpact)}
</span>
<span className="text-xs text-gray-500">impacto anual (coste del problema)</span>
<span className="text-xs text-gray-500">{t('dimensionAnalysis.annualImpact')}</span>
<span className="text-xs text-gray-400">i</span>
</div>
@@ -527,7 +577,7 @@ function DimensionCard({
{dimension.score >= 0 && causalAnalyses.length === 0 && findings.length > 0 && (
<div className="p-4">
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
Hallazgos Clave
{t('dimensionAnalysis.keyFindings')}
</h4>
<ul className="space-y-2">
{findings.slice(0, 3).map((finding, idx) => (
@@ -550,7 +600,7 @@ function DimensionCard({
<div className={cn('p-3 rounded-lg border', STATUS_CLASSES.success.bg, STATUS_CLASSES.success.border)}>
<p className={cn('text-sm flex items-center gap-2', STATUS_CLASSES.success.text)}>
<ChevronRight className="w-4 h-4" />
Métricas dentro de rangos aceptables. Sin hallazgos críticos.
{t('dimensionAnalysis.withinAcceptable')}
</p>
</div>
</div>
@@ -561,7 +611,7 @@ function DimensionCard({
<div className="px-4 pb-4">
<div className="p-3 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-start gap-2">
<span className="text-xs font-semibold text-blue-600">Recomendación:</span>
<span className="text-xs font-semibold text-blue-600">{t('dimensionAnalysis.recommendation')}</span>
<span className="text-xs text-gray-600">{recommendations[0].text}</span>
</div>
</div>
@@ -574,6 +624,8 @@ function DimensionCard({
// ========== 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' ||
@@ -609,7 +661,7 @@ export function DimensionAnalysisTab({ data }: DimensionAnalysisTabProps) {
// Generar hallazgo clave para cada dimensión
const getCausalAnalysisForDimension = (dimension: DimensionAnalysis) =>
generateCausalAnalysis(dimension, data.heatmapData, data.economicModel, data.staticConfig, data.dateRange);
generateCausalAnalysis(dimension, data.heatmapData, data.economicModel, t, data.staticConfig, data.dateRange);
// Calcular impacto total de todas las dimensiones con datos
const impactoTotal = coreDimensions
@@ -627,10 +679,10 @@ export function DimensionAnalysisTab({ data }: DimensionAnalysisTabProps) {
<div className="space-y-6">
{/* v3.16: Header simplificado - solo título y subtítulo */}
<div className="mb-2">
<h2 className="text-lg font-bold text-gray-900">Diagnóstico por Dimensión</h2>
<h2 className="text-lg font-bold text-gray-900">{t('dimensionAnalysis.title')}</h2>
<p className="text-sm text-gray-500">
{coreDimensions.length} dimensiones analizadas
{sinDatos.length > 0 && ` (${sinDatos.length} sin datos)`}
{t('dimensionAnalysis.dimensionsAnalyzed', { count: coreDimensions.length })}
{sinDatos.length > 0 && ` ${t('dimensionAnalysis.noData', { count: sinDatos.length })}`}
</p>
</div>
@@ -644,6 +696,7 @@ export function DimensionAnalysisTab({ data }: DimensionAnalysisTabProps) {
recommendations={getRecommendationsForDimension(dimension.id)}
causalAnalyses={getCausalAnalysisForDimension(dimension)}
delay={idx * 0.05}
t={t}
/>
))}
</div>