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:
@@ -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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TrendingUp, TrendingDown, AlertTriangle, CheckCircle, Target, Activity, Clock, PhoneForwarded, Users, Bot, ChevronRight, BarChart3, Cpu, Map, Zap, Calendar } from 'lucide-react';
|
||||
import type { AnalysisData, Finding, DrilldownDataPoint, HeatmapDataPoint } from '../../types';
|
||||
import type { TabId } from '../DashboardHeader';
|
||||
@@ -146,7 +147,7 @@ interface Hallazgo {
|
||||
metrica?: string;
|
||||
}
|
||||
|
||||
function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
function generarHallazgos(data: AnalysisData, t: any): Hallazgo[] {
|
||||
const hallazgos: Hallazgo[] = [];
|
||||
const allQueues = data.drilldownData?.flatMap(s => s.originalQueues) || [];
|
||||
const totalVolume = allQueues.reduce((s, q) => s + q.volume, 0);
|
||||
@@ -163,7 +164,7 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
const pctVolumen = (colasAltaVariabilidad.reduce((s, q) => s + q.volume, 0) / totalVolume) * 100;
|
||||
hallazgos.push({
|
||||
tipo: 'critico',
|
||||
texto: `${colasAltaVariabilidad.length} colas con variabilidad crítica (CV >100%) representan ${pctVolumen.toFixed(0)}% del volumen`,
|
||||
texto: t('executiveSummary.highVariabilityQueues', { count: colasAltaVariabilidad.length, pct: pctVolumen.toFixed(0) }),
|
||||
metrica: 'CV AHT'
|
||||
});
|
||||
}
|
||||
@@ -173,7 +174,7 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
if (colasAltoTransfer.length > 0) {
|
||||
hallazgos.push({
|
||||
tipo: 'warning',
|
||||
texto: `${colasAltoTransfer.length} colas con tasa de transferencia >25% - posible problema de routing o formación`,
|
||||
texto: t('executiveSummary.highTransferQueues', { count: colasAltoTransfer.length }),
|
||||
metrica: 'Transfer'
|
||||
});
|
||||
}
|
||||
@@ -183,7 +184,7 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
if (colasBajoFCR.length > 0) {
|
||||
hallazgos.push({
|
||||
tipo: 'warning',
|
||||
texto: `${colasBajoFCR.length} colas con FCR <50% - clientes requieren múltiples contactos`,
|
||||
texto: t('executiveSummary.lowFCRQueues', { count: colasBajoFCR.length }),
|
||||
metrica: 'FCR'
|
||||
});
|
||||
}
|
||||
@@ -192,7 +193,7 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
if (avgAHT > 400) {
|
||||
hallazgos.push({
|
||||
tipo: 'warning',
|
||||
texto: `AHT promedio de ${Math.round(avgAHT)}s supera el benchmark de industria (380s)`,
|
||||
texto: t('executiveSummary.ahtAboveBenchmark', { aht: Math.round(avgAHT) }),
|
||||
metrica: 'AHT'
|
||||
});
|
||||
}
|
||||
@@ -203,7 +204,7 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
const pctHuman = (colasHumanOnly.reduce((s, q) => s + q.volume, 0) / totalVolume) * 100;
|
||||
hallazgos.push({
|
||||
tipo: 'info',
|
||||
texto: `${colasHumanOnly.length} colas (${pctHuman.toFixed(0)}% volumen) requieren intervención humana completa`,
|
||||
texto: t('executiveSummary.humanOnlyQueues', { count: colasHumanOnly.length, pct: pctHuman.toFixed(0) }),
|
||||
metrica: 'Tier'
|
||||
});
|
||||
}
|
||||
@@ -213,8 +214,8 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
if (colasAutomate.length > 0) {
|
||||
hallazgos.push({
|
||||
tipo: 'info',
|
||||
texto: `${colasAutomate.length} colas listas para automatización con potencial de ahorro significativo`,
|
||||
metrica: 'Oportunidad'
|
||||
texto: t('executiveSummary.automateReadyQueues', { count: colasAutomate.length }),
|
||||
metrica: t('executiveSummary.opportunity')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,7 +223,8 @@ function generarHallazgos(data: AnalysisData): Hallazgo[] {
|
||||
}
|
||||
|
||||
function PrincipalesHallazgos({ data }: { data: AnalysisData }) {
|
||||
const hallazgos = generarHallazgos(data);
|
||||
const { t } = useTranslation();
|
||||
const hallazgos = generarHallazgos(data, t);
|
||||
|
||||
if (hallazgos.length === 0) return null;
|
||||
|
||||
@@ -240,7 +242,7 @@ function PrincipalesHallazgos({ data }: { data: AnalysisData }) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Principales Hallazgos</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-3">{t('executiveSummary.title')}</h3>
|
||||
<div className="space-y-2">
|
||||
{hallazgos.map((h, idx) => (
|
||||
<div key={idx} className={cn('flex items-start gap-2 p-2 rounded-lg border', getClase(h.tipo))}>
|
||||
@@ -265,6 +267,7 @@ function PrincipalesHallazgos({ data }: { data: AnalysisData }) {
|
||||
// ============================================
|
||||
|
||||
function CabeceraPeriodo({ data }: { data: AnalysisData }) {
|
||||
const { t } = useTranslation();
|
||||
const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
|
||||
|
||||
// Contar colas únicas (original_queue_id) desde drilldownData
|
||||
@@ -278,7 +281,7 @@ function CabeceraPeriodo({ data }: { data: AnalysisData }) {
|
||||
// Formatear fechas del periodo
|
||||
const formatPeriodo = () => {
|
||||
if (!data.dateRange?.min || !data.dateRange?.max) {
|
||||
return 'Periodo no especificado';
|
||||
return t('executiveSummary.periodNotSpecified');
|
||||
}
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
@@ -295,13 +298,13 @@ function CabeceraPeriodo({ data }: { data: AnalysisData }) {
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4 py-3 px-3 sm:px-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Calendar className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-xs sm:text-sm font-medium">Periodo:</span>
|
||||
<span className="text-xs sm:text-sm font-medium">{t('executiveSummary.period')}</span>
|
||||
<span className="text-xs sm:text-sm">{formatPeriodo()}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4 md:gap-6 text-xs sm:text-sm text-gray-500">
|
||||
<span><strong>{formatNumber(totalInteractions)}</strong> int.</span>
|
||||
<span><strong>{uniqueQueues}</strong> colas</span>
|
||||
<span><strong>{numLineasNegocio}</strong> LN</span>
|
||||
<span><strong>{formatNumber(totalInteractions)}</strong> {t('executiveSummary.interactions')}</span>
|
||||
<span><strong>{uniqueQueues}</strong> {t('executiveSummary.queues')}</span>
|
||||
<span><strong>{numLineasNegocio}</strong> {t('executiveSummary.businessLines')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -323,10 +326,12 @@ function HeadlineEjecutivo({
|
||||
resolucionScore: number;
|
||||
satisfaccionScore: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getStatusLabel = (score: number): string => {
|
||||
if (score >= 80) return 'Óptimo';
|
||||
if (score >= 60) return 'Aceptable';
|
||||
return 'Crítico';
|
||||
if (score >= 80) return t('common.optimal');
|
||||
if (score >= 60) return t('common.acceptable');
|
||||
return t('common.critical');
|
||||
};
|
||||
|
||||
const getStatusVariant = (score: number): 'success' | 'warning' | 'critical' => {
|
||||
@@ -340,16 +345,10 @@ function HeadlineEjecutivo({
|
||||
{/* Título principal */}
|
||||
<div className="mb-3 sm:mb-4">
|
||||
<h1 className="text-lg sm:text-xl md:text-2xl font-light mb-1">
|
||||
Tu operación procesa{' '}
|
||||
<span className="font-bold text-white">{formatNumber(totalInteracciones)}</span>{' '}
|
||||
interacciones
|
||||
{t('executiveSummary.yourOperation', { total: formatNumber(totalInteracciones) })}
|
||||
</h1>
|
||||
<p className="text-sm sm:text-lg text-gray-300">
|
||||
con oportunidad de{' '}
|
||||
<span className="font-bold text-emerald-400">
|
||||
{formatCurrency(oportunidadTotal)}
|
||||
</span>{' '}
|
||||
en optimización
|
||||
{t('executiveSummary.withOpportunity', { amount: formatCurrency(oportunidadTotal) })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -361,7 +360,7 @@ function HeadlineEjecutivo({
|
||||
)}>
|
||||
<Clock className={cn('w-4 h-4', STATUS_CLASSES[getStatusVariant(eficienciaScore)].text)} />
|
||||
<span className={cn('text-sm font-medium', STATUS_CLASSES[getStatusVariant(eficienciaScore)].text)}>
|
||||
Eficiencia: {getStatusLabel(eficienciaScore)}
|
||||
{t('executiveSummary.efficiency')} {getStatusLabel(eficienciaScore)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(
|
||||
@@ -370,7 +369,7 @@ function HeadlineEjecutivo({
|
||||
)}>
|
||||
<CheckCircle className={cn('w-4 h-4', STATUS_CLASSES[getStatusVariant(resolucionScore)].text)} />
|
||||
<span className={cn('text-sm font-medium', STATUS_CLASSES[getStatusVariant(resolucionScore)].text)}>
|
||||
Resolución: {getStatusLabel(resolucionScore)}
|
||||
{t('executiveSummary.resolution')} {getStatusLabel(resolucionScore)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(
|
||||
@@ -379,7 +378,7 @@ function HeadlineEjecutivo({
|
||||
)}>
|
||||
<Users className={cn('w-4 h-4', STATUS_CLASSES[getStatusVariant(satisfaccionScore)].text)} />
|
||||
<span className={cn('text-sm font-medium', STATUS_CLASSES[getStatusVariant(satisfaccionScore)].text)}>
|
||||
Satisfacción: {getStatusLabel(satisfaccionScore)}
|
||||
{t('executiveSummary.satisfaction')} {getStatusLabel(satisfaccionScore)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -390,6 +389,7 @@ function HeadlineEjecutivo({
|
||||
// v7.0: Unified KPI + Benchmark Card Component
|
||||
// Combines KeyMetricsCard + BenchmarkTable into single 3x2 card grid
|
||||
function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[] }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedIndustry, setSelectedIndustry] = React.useState<IndustryKey>('aerolineas');
|
||||
const benchmarks = BENCHMARKS_INDUSTRIA[selectedIndustry];
|
||||
|
||||
@@ -442,11 +442,11 @@ function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[]
|
||||
|
||||
// Calculate percentile position
|
||||
const getPercentileBadge = (percentile: number): { label: string; color: string } => {
|
||||
if (percentile >= 90) return { label: 'Top 10%', color: 'bg-emerald-500 text-white' };
|
||||
if (percentile >= 75) return { label: 'Top 25%', color: 'bg-emerald-100 text-emerald-700' };
|
||||
if (percentile >= 50) return { label: 'Promedio', color: 'bg-amber-100 text-amber-700' };
|
||||
if (percentile >= 25) return { label: 'Bajo Avg', color: 'bg-orange-100 text-orange-700' };
|
||||
return { label: 'Bottom 25%', color: 'bg-red-100 text-red-700' };
|
||||
if (percentile >= 90) return { label: t('executiveSummary.top10'), color: 'bg-emerald-500 text-white' };
|
||||
if (percentile >= 75) return { label: t('executiveSummary.top25'), color: 'bg-emerald-100 text-emerald-700' };
|
||||
if (percentile >= 50) return { label: t('executiveSummary.average'), color: 'bg-amber-100 text-amber-700' };
|
||||
if (percentile >= 25) return { label: t('executiveSummary.belowAvg'), color: 'bg-orange-100 text-orange-700' };
|
||||
return { label: t('executiveSummary.bottom25'), color: 'bg-red-100 text-red-700' };
|
||||
};
|
||||
|
||||
// Calculate GAP vs P50 - positive is better, negative is worse
|
||||
@@ -504,11 +504,11 @@ function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[]
|
||||
|
||||
// Get insight text based on percentile position
|
||||
const getInsightText = (percentile: number, bench: BenchmarkMetric): string => {
|
||||
if (percentile >= 90) return `Superas al 90% del mercado`;
|
||||
if (percentile >= 75) return `Mejor que 3 de cada 4 empresas`;
|
||||
if (percentile >= 50) return `En línea con la mediana del sector`;
|
||||
if (percentile >= 25) return `Por debajo de la media del mercado`;
|
||||
return `Área crítica de mejora`;
|
||||
if (percentile >= 90) return t('executiveSummary.surpasses90');
|
||||
if (percentile >= 75) return t('executiveSummary.betterThan75');
|
||||
if (percentile >= 50) return t('executiveSummary.alignedWithMedian');
|
||||
if (percentile >= 25) return t('executiveSummary.belowAverage');
|
||||
return t('executiveSummary.criticalArea');
|
||||
};
|
||||
|
||||
// Format benchmark value for display
|
||||
@@ -522,79 +522,89 @@ function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[]
|
||||
// FCR Real context: métrica más estricta que incluye recontactos 7 días
|
||||
const fcrRealDiff = operacion.fcrTecnico - operacion.fcrReal;
|
||||
const fcrRealContext = fcrRealDiff > 0
|
||||
? `${Math.round(fcrRealDiff)}pp de recontactos 7d`
|
||||
? `${Math.round(fcrRealDiff)}pp ${t('executiveSummary.recontacts7d')}`
|
||||
: null;
|
||||
|
||||
// AHT Total context: diferencia entre AHT limpio y AHT con todas las filas
|
||||
const ahtTotalDiff = operacion.ahtTotal - operacion.aht;
|
||||
const ahtTotalContext = Math.abs(ahtTotalDiff) > 1
|
||||
? `${ahtTotalDiff > 0 ? '+' : ''}${Math.round(ahtTotalDiff)}s vs AHT limpio`
|
||||
? `${ahtTotalDiff > 0 ? '+' : ''}${Math.round(ahtTotalDiff)}s ${t('executiveSummary.vsCleanAht')}`
|
||||
: null;
|
||||
|
||||
const metricsData = [
|
||||
{
|
||||
id: 'aht',
|
||||
label: 'AHT',
|
||||
label: t('executiveSummary.aht'),
|
||||
valor: operacion.aht,
|
||||
display: `${Math.floor(operacion.aht / 60)}:${String(Math.round(operacion.aht) % 60).padStart(2, '0')}`,
|
||||
subDisplay: `(${Math.round(operacion.aht)}s)`,
|
||||
bench: benchmarks.metricas.aht,
|
||||
tooltip: 'Tiempo medio de gestión (solo interacciones válidas)',
|
||||
tooltip: t('executiveSummary.ahtTooltip'),
|
||||
// AHT Total integrado como métrica secundaria
|
||||
secondaryMetric: {
|
||||
label: 'AHT Total',
|
||||
label: t('executiveSummary.ahtTotal'),
|
||||
value: `${Math.floor(operacion.ahtTotal / 60)}:${String(Math.round(operacion.ahtTotal) % 60).padStart(2, '0')} (${Math.round(operacion.ahtTotal)}s)`,
|
||||
note: ahtTotalContext,
|
||||
tooltip: 'Incluye todas las filas (noise, zombie, abandon) - solo informativo',
|
||||
description: 'Incluye noise, zombie y abandonos — solo informativo'
|
||||
tooltip: t('executiveSummary.ahtTotalTooltip'),
|
||||
description: t('executiveSummary.ahtTotalDesc')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'fcr_tecnico',
|
||||
label: 'FCR',
|
||||
label: t('executiveSummary.fcr'),
|
||||
valor: operacion.fcrTecnico,
|
||||
display: `${Math.round(operacion.fcrTecnico)}%`,
|
||||
subDisplay: null,
|
||||
bench: benchmarks.metricas.fcr,
|
||||
tooltip: 'First Contact Resolution - comparable con benchmarks de industria',
|
||||
tooltip: t('executiveSummary.fcrTooltip'),
|
||||
// FCR Real integrado como métrica secundaria
|
||||
secondaryMetric: {
|
||||
label: 'FCR Ajustado',
|
||||
label: t('executiveSummary.fcrAdjusted'),
|
||||
value: `${Math.round(operacion.fcrReal)}%`,
|
||||
note: fcrRealContext,
|
||||
tooltip: 'Excluye recontactos en 7 días (métrica más estricta)',
|
||||
description: 'Incluye filtro de recontactos 7d — métrica interna más estricta'
|
||||
tooltip: t('executiveSummary.fcrAdjustedTooltip'),
|
||||
description: t('executiveSummary.fcrAdjustedDesc')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'abandono',
|
||||
label: 'ABANDONO',
|
||||
label: t('executiveSummary.abandonment'),
|
||||
valor: operacion.abandono,
|
||||
display: `${operacion.abandono.toFixed(1)}%`,
|
||||
subDisplay: null,
|
||||
bench: benchmarks.metricas.abandono,
|
||||
tooltip: 'Tasa de abandono',
|
||||
tooltip: t('executiveSummary.abandonmentTooltip'),
|
||||
secondaryMetric: null
|
||||
},
|
||||
{
|
||||
id: 'cpi',
|
||||
label: 'COSTE/INTERAC.',
|
||||
label: t('executiveSummary.costPerInteraction'),
|
||||
valor: operacion.cpi,
|
||||
display: `€${operacion.cpi.toFixed(2)}`,
|
||||
subDisplay: null,
|
||||
bench: benchmarks.metricas.cpi,
|
||||
tooltip: 'Coste por interacción',
|
||||
tooltip: t('executiveSummary.cpiTooltip'),
|
||||
secondaryMetric: null
|
||||
}
|
||||
];
|
||||
|
||||
// Map industry keys to translation keys
|
||||
const industryNameMap: Record<IndustryKey, string> = {
|
||||
aerolineas: t('industries.airlines'),
|
||||
telecomunicaciones: t('industries.telco'),
|
||||
banca: t('industries.banking'),
|
||||
utilities: t('industries.utilities'),
|
||||
retail: t('industries.retail'),
|
||||
general: t('industries.crossIndustry')
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{/* Header with industry selector */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">Indicadores vs Industria</h3>
|
||||
<p className="text-xs text-gray-500">Fuente: {benchmarks.fuente}</p>
|
||||
<h3 className="font-semibold text-gray-900">{t('executiveSummary.indicators')}</h3>
|
||||
<p className="text-xs text-gray-500">{t('benchmark.source', { source: benchmarks.fuente })}</p>
|
||||
</div>
|
||||
<select
|
||||
value={selectedIndustry}
|
||||
@@ -602,7 +612,7 @@ function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[]
|
||||
className="text-sm border border-gray-300 rounded-md px-2 py-1 bg-white w-full sm:w-auto"
|
||||
>
|
||||
{Object.entries(BENCHMARKS_INDUSTRIA).map(([key, val]) => (
|
||||
<option key={key} value={key}>{val.nombre}</option>
|
||||
<option key={key} value={key}>{industryNameMap[key as IndustryKey]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -700,15 +710,15 @@ function UnifiedKPIBenchmark({ heatmapData }: { heatmapData: HeatmapDataPoint[]
|
||||
{/* Benchmark Reference Values */}
|
||||
<div className="grid grid-cols-3 gap-1 text-center mb-2 py-1.5 bg-white/50 rounded">
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-400">Bajo</div>
|
||||
<div className="text-[9px] text-gray-400">{t('executiveSummary.benchmarkLow')}</div>
|
||||
<div className="text-[10px] font-medium text-gray-600">{formatBenchValue(m.bench.p25, m.bench.unidad)}</div>
|
||||
</div>
|
||||
<div className="border-x border-gray-200">
|
||||
<div className="text-[9px] text-gray-400">Mediana</div>
|
||||
<div className="text-[9px] text-gray-400">{t('executiveSummary.benchmarkMedian')}</div>
|
||||
<div className="text-[10px] font-semibold text-gray-700">{formatBenchValue(m.bench.p50, m.bench.unidad)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-400">Top</div>
|
||||
<div className="text-[9px] text-gray-400">{t('executiveSummary.benchmarkTop')}</div>
|
||||
<div className="text-[10px] font-medium text-emerald-600">{formatBenchValue(m.bench.p90, m.bench.unidad)}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -744,6 +754,8 @@ function HealthScoreDetailed({
|
||||
avgAbandonmentRate: number; // Tasa de abandono (%)
|
||||
avgTransferRate: number; // Tasa de transferencia (%)
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getScoreColor = (s: number): string => {
|
||||
if (s >= 80) return COLORS.status.success;
|
||||
if (s >= 60) return COLORS.status.warning;
|
||||
@@ -751,10 +763,10 @@ function HealthScoreDetailed({
|
||||
};
|
||||
|
||||
const getScoreLabel = (s: number): string => {
|
||||
if (s >= 80) return 'Excelente';
|
||||
if (s >= 60) return 'Bueno';
|
||||
if (s >= 40) return 'Regular';
|
||||
return 'Crítico';
|
||||
if (s >= 80) return t('executiveSummary.excellent');
|
||||
if (s >= 60) return t('executiveSummary.good');
|
||||
if (s >= 40) return t('executiveSummary.regular');
|
||||
return t('common.critical');
|
||||
};
|
||||
|
||||
const color = getScoreColor(score);
|
||||
@@ -815,35 +827,35 @@ function HealthScoreDetailed({
|
||||
// Nueva ponderación: FCR 35%, Abandono 30%, CSAT Proxy 20%, AHT 15%
|
||||
const factors = [
|
||||
{
|
||||
name: 'FCR Técnico',
|
||||
name: t('executiveSummary.fcrTechnical'),
|
||||
weight: '35%',
|
||||
score: Math.round(fcrScore),
|
||||
status: getFactorStatus(fcrScore),
|
||||
insight: fcrScore >= 80 ? 'Óptimo' : fcrScore >= 50 ? 'En P50' : 'Bajo P90',
|
||||
insight: fcrScore >= 80 ? t('common.optimal') : fcrScore >= 50 ? t('executiveSummary.atP50') : t('executiveSummary.lowP90'),
|
||||
rawValue: `${avgFCR.toFixed(0)}%`
|
||||
},
|
||||
{
|
||||
name: 'Accesibilidad',
|
||||
name: t('executiveSummary.accessibility'),
|
||||
weight: '30%',
|
||||
score: Math.round(abandonoScore),
|
||||
status: getFactorStatus(abandonoScore),
|
||||
insight: abandonoScore >= 80 ? 'Bajo' : abandonoScore >= 50 ? 'Moderado' : 'Crítico',
|
||||
insight: abandonoScore >= 80 ? t('common.low') : abandonoScore >= 50 ? t('executiveSummary.moderate') : t('common.critical'),
|
||||
rawValue: `${avgAbandonmentRate.toFixed(1)}% aband.`
|
||||
},
|
||||
{
|
||||
name: 'CSAT Proxy',
|
||||
name: t('executiveSummary.csatProxy'),
|
||||
weight: '20%',
|
||||
score: Math.round(csatProxyScore),
|
||||
status: getFactorStatus(csatProxyScore),
|
||||
insight: csatProxyScore >= 80 ? 'Óptimo' : csatProxyScore >= 50 ? 'Mejorable' : 'Bajo',
|
||||
insight: csatProxyScore >= 80 ? t('common.optimal') : csatProxyScore >= 50 ? t('common.improvable') : t('common.low'),
|
||||
rawValue: '(FCR+Aband.)'
|
||||
},
|
||||
{
|
||||
name: 'Eficiencia',
|
||||
name: t('executiveSummary.efficiencyMetric'),
|
||||
weight: '15%',
|
||||
score: Math.round(ahtScore),
|
||||
status: getFactorStatus(ahtScore),
|
||||
insight: ahtScore >= 80 ? 'Rápido' : ahtScore >= 50 ? 'En rango' : 'Lento',
|
||||
insight: ahtScore >= 80 ? t('executiveSummary.fast') : ahtScore >= 50 ? t('executiveSummary.inRange') : t('executiveSummary.slow'),
|
||||
rawValue: `${Math.floor(avgAHT / 60)}:${String(Math.round(avgAHT) % 60).padStart(2, '0')}`
|
||||
}
|
||||
];
|
||||
@@ -896,9 +908,9 @@ function HealthScoreDetailed({
|
||||
|
||||
{/* Breakdown */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Health Score</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">{t('executiveSummary.healthScore')}</h3>
|
||||
<p className="text-[10px] text-gray-400 mb-2">
|
||||
Benchmarks: FCR P10=85%, Aband. P10=3%, AHT P10=240s
|
||||
{t('executiveSummary.healthScoreBenchmark')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -923,7 +935,7 @@ function HealthScoreDetailed({
|
||||
{/* Nota de cálculo */}
|
||||
<div className="mt-3 pt-2 border-t border-gray-100">
|
||||
<p className="text-[9px] text-gray-400 text-center">
|
||||
Score = FCR×35% + Accesibilidad×30% + CSAT Proxy×20% + Eficiencia×15%
|
||||
{t('executiveSummary.healthScoreFormula')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -934,6 +946,7 @@ function HealthScoreDetailed({
|
||||
|
||||
// v3.16: Potencial de Automatización - Sin gauge confuso, solo distribución clara
|
||||
function AgenticReadinessScore({ data }: { data: AnalysisData }) {
|
||||
const { t } = useTranslation();
|
||||
const allQueues = data.drilldownData?.flatMap(skill => skill.originalQueues) || [];
|
||||
const totalQueueVolume = allQueues.reduce((sum, q) => sum + q.volume, 0);
|
||||
|
||||
@@ -962,17 +975,17 @@ function AgenticReadinessScore({ data }: { data: AnalysisData }) {
|
||||
|
||||
// Datos de tiers con descripción clara
|
||||
const tiers = [
|
||||
{ key: 'AUTOMATE', label: 'AUTOMATE', bgColor: 'bg-emerald-500', desc: 'Bot autónomo' },
|
||||
{ key: 'ASSIST', label: 'ASSIST', bgColor: 'bg-cyan-500', desc: 'Bot + agente' },
|
||||
{ key: 'AUGMENT', label: 'AUGMENT', bgColor: 'bg-amber-500', desc: 'Agente asistido' },
|
||||
{ key: 'HUMAN-ONLY', label: 'HUMAN', bgColor: 'bg-gray-400', desc: 'Solo humano' }
|
||||
{ key: 'AUTOMATE', label: t('executiveSummary.automate'), bgColor: 'bg-emerald-500', desc: t('executiveSummary.autonomousBot') },
|
||||
{ key: 'ASSIST', label: t('executiveSummary.assist'), bgColor: 'bg-cyan-500', desc: t('executiveSummary.botPlusAgent') },
|
||||
{ key: 'AUGMENT', label: t('executiveSummary.augment'), bgColor: 'bg-amber-500', desc: t('executiveSummary.assistedAgent') },
|
||||
{ key: 'HUMAN-ONLY', label: t('executiveSummary.human'), bgColor: 'bg-gray-400', desc: t('executiveSummary.humanOnly') }
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Bot className="w-5 h-5 text-blue-600" />
|
||||
<h3 className="font-semibold text-gray-900">Potencial de Automatización</h3>
|
||||
<h3 className="font-semibold text-gray-900">{t('executiveSummary.automationPotential')}</h3>
|
||||
</div>
|
||||
|
||||
{/* Distribución por tier */}
|
||||
@@ -996,7 +1009,7 @@ function AgenticReadinessScore({ data }: { data: AnalysisData }) {
|
||||
<div className="w-14 text-right">
|
||||
<div className="text-sm font-semibold text-gray-700">{Math.round(pct)}%</div>
|
||||
</div>
|
||||
<div className="w-16 text-xs text-gray-400 text-right">{count} colas</div>
|
||||
<div className="w-16 text-xs text-gray-400 text-right">{count} {t('executiveSummary.queuesLabel')}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1007,15 +1020,15 @@ function AgenticReadinessScore({ data }: { data: AnalysisData }) {
|
||||
<div className="grid grid-cols-2 gap-3 text-center">
|
||||
<div className="p-2 bg-emerald-50 rounded-lg">
|
||||
<p className="text-lg font-bold text-emerald-700">{Math.round(tierPcts.AUTOMATE)}%</p>
|
||||
<p className="text-[10px] text-emerald-600">Automatización completa</p>
|
||||
<p className="text-[10px] text-emerald-600">{t('executiveSummary.fullAutomation')}</p>
|
||||
</div>
|
||||
<div className="p-2 bg-cyan-50 rounded-lg">
|
||||
<p className="text-lg font-bold text-cyan-700">{Math.round(tierPcts.AUTOMATE + tierPcts.ASSIST)}%</p>
|
||||
<p className="text-[10px] text-cyan-600">Con asistencia IA</p>
|
||||
<p className="text-[10px] text-cyan-600">{t('executiveSummary.withAIAssistance')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 text-center mt-2">
|
||||
Basado en {formatNumber(totalQueueVolume)} interacciones analizadas
|
||||
{t('executiveSummary.basedOnInteractions', { total: formatNumber(totalQueueVolume) })}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1094,29 +1107,31 @@ function TopOpportunities({ findings, opportunities }: {
|
||||
|
||||
// v3.15: Economic Summary Compact
|
||||
function EconomicSummary({ economicModel }: { economicModel: AnalysisData['economicModel'] }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card padding="md">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Impacto Económico</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-3">{t('executiveSummary.economicImpact')}</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||
<Stat
|
||||
value={formatCurrency(economicModel.currentAnnualCost)}
|
||||
label="Coste Anual"
|
||||
label={t('executiveSummary.annualCost')}
|
||||
/>
|
||||
<Stat
|
||||
value={formatCurrency(economicModel.annualSavings)}
|
||||
label="Ahorro Potencial"
|
||||
label={t('executiveSummary.potentialSavings')}
|
||||
status="success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-2.5 bg-blue-50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-blue-600">ROI 3 años</p>
|
||||
<p className="text-xs text-blue-600">{t('executiveSummary.roi3Years')}</p>
|
||||
<p className="text-lg font-bold text-blue-600">{economicModel.roi3yr}%</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500">Payback</p>
|
||||
<p className="text-xs text-gray-500">{t('executiveSummary.payback')}</p>
|
||||
<p className="text-lg font-bold text-gray-700">{economicModel.paybackMonths}m</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1125,6 +1140,8 @@ function EconomicSummary({ economicModel }: { economicModel: AnalysisData['econo
|
||||
}
|
||||
|
||||
export function ExecutiveSummaryTab({ data, onTabChange }: ExecutiveSummaryTabProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Métricas básicas - VOLUME-WEIGHTED para consistencia con calculateHealthScore()
|
||||
const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
|
||||
|
||||
@@ -1204,7 +1221,7 @@ export function ExecutiveSummaryTab({ data, onTabChange }: ExecutiveSummaryTabPr
|
||||
{onTabChange && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
|
||||
Explorar análisis detallado
|
||||
{t('executiveSummary.exploreDetailed')}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
@@ -1218,12 +1235,12 @@ export function ExecutiveSummaryTab({ data, onTabChange }: ExecutiveSummaryTabPr
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-700 text-sm">Dimensiones</span>
|
||||
<span className="font-medium text-gray-700 text-sm">{t('executiveSummary.dimensionsTab')}</span>
|
||||
{dimensionesConProblemas > 0 && (
|
||||
<Badge label={`${dimensionesConProblemas} críticas`} variant="warning" size="sm" />
|
||||
<Badge label={`${dimensionesConProblemas} ${t('executiveSummary.criticalQueues')}`} variant="warning" size="sm" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">Eficiencia, resolución, satisfacción</p>
|
||||
<p className="text-xs text-gray-400">{t('executiveSummary.dimensionsDesc')}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-gray-500 group-hover:translate-x-0.5 transition-all" />
|
||||
</button>
|
||||
@@ -1238,12 +1255,12 @@ export function ExecutiveSummaryTab({ data, onTabChange }: ExecutiveSummaryTabPr
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-700 text-sm">Agentic Readiness</span>
|
||||
<span className="font-medium text-gray-700 text-sm">{t('executiveSummary.agenticReadinessTab')}</span>
|
||||
{colasAutomate.length > 0 && (
|
||||
<Badge label={`${colasAutomate.length} listas`} variant="success" size="sm" />
|
||||
<Badge label={`${colasAutomate.length} ${t('executiveSummary.readyQueues')}`} variant="success" size="sm" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">Colas elegibles para automatización</p>
|
||||
<p className="text-xs text-gray-400">{t('executiveSummary.agenticReadinessDesc')}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-gray-500 group-hover:translate-x-0.5 transition-all" />
|
||||
</button>
|
||||
@@ -1258,11 +1275,11 @@ export function ExecutiveSummaryTab({ data, onTabChange }: ExecutiveSummaryTabPr
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-700 text-sm">Plan de Acción</span>
|
||||
<Badge label="Prioridad" variant="critical" size="sm" />
|
||||
<span className="font-medium text-gray-700 text-sm">{t('executiveSummary.actionPlan')}</span>
|
||||
<Badge label={t('executiveSummary.priority')} variant="critical" size="sm" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
{ahorroTotal > 0 ? `Potencial: ${formatCurrency(ahorroTotal)}/año` : 'Roadmap de implementación'}
|
||||
{ahorroTotal > 0 ? t('executiveSummary.potentialPerYear', { amount: formatCurrency(ahorroTotal) }) : t('executiveSummary.roadmapImplementation')}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-gray-500 group-hover:translate-x-0.5 transition-all" />
|
||||
|
||||
Reference in New Issue
Block a user