Translate Phase 1 high-priority frontend utils (backendMapper, analysisGenerator, realDataAnalysis)

Phase 1 of Spanish-to-English translation for critical path files:
- backendMapper.ts: Translated ~50 occurrences (comments, labels, dimension titles)
- analysisGenerator.ts: Translated ~49 occurrences (findings, recommendations, dimension content)
- realDataAnalysis.ts: Translated ~92 occurrences (clasificarTier functions, inline comments)

All function names and API variable names preserved for compatibility.
Frontend compilation tested and verified successful.

Related to TRANSLATION_STATUS.md Phase 1 objectives.

https://claude.ai/code/session_01GNbnkFoESkRcnPr3bLCYDg
This commit is contained in:
Claude
2026-02-07 10:35:40 +00:00
parent f18bdea812
commit 94178eaaae
3 changed files with 870 additions and 870 deletions

View File

@@ -23,13 +23,13 @@ function safeNumber(value: any, fallback = 0): number {
function normalizeAhtMetric(ahtSeconds: number): number {
if (!Number.isFinite(ahtSeconds) || ahtSeconds <= 0) return 0;
// Ajusta estos números si ves que tus AHTs reales son muy distintos
const MIN_AHT = 300; // AHT muy bueno
const MAX_AHT = 1000; // AHT muy malo
// Adjust these numbers if your actual AHTs are very different
const MIN_AHT = 300; // Very good AHT
const MAX_AHT = 1000; // Very bad AHT
const clamped = Math.max(MIN_AHT, Math.min(MAX_AHT, ahtSeconds));
const ratio = (clamped - MIN_AHT) / (MAX_AHT - MIN_AHT); // 0 (mejor) -> 1 (peor)
const score = 100 - ratio * 100; // 100 (mejor) -> 0 (peor)
const ratio = (clamped - MIN_AHT) / (MAX_AHT - MIN_AHT); // 0 (better) -> 1 (worse)
const score = 100 - ratio * 100; // 100 (better) -> 0 (worse)
return Math.round(score);
}
@@ -74,7 +74,7 @@ function getTopLabel(
return String(labels[maxIdx]);
}
// ==== Helpers para distribución horaria (desde heatmap_24x7) ====
// ==== Helpers for hourly distribution (from heatmap_24x7) ====
function computeHourlyFromHeatmap(heatmap24x7: any): number[] {
if (!Array.isArray(heatmap24x7) || !heatmap24x7.length) {
@@ -146,7 +146,7 @@ function mapAgenticReadiness(
description:
value?.reason ||
value?.details?.description ||
'Sub-factor calculado a partir de KPIs agregados.',
'Sub-factor calculated from aggregated KPIs.',
details: value?.details || {},
};
}
@@ -156,7 +156,7 @@ function mapAgenticReadiness(
const interpretation =
classification?.description ||
`Puntuación de preparación agentic: ${score.toFixed(1)}/10`;
`Agentic readiness score: ${score.toFixed(1)}/10`;
const computedCount = Object.values(sub_scores).filter(
(s: any) => s?.computed
@@ -176,7 +176,7 @@ function mapAgenticReadiness(
};
}
// ==== Volumetría (dimensión + KPIs) ====
// ==== Volumetry (dimension + KPIs) ====
function buildVolumetryDimension(
raw: BackendRawResults
@@ -216,13 +216,13 @@ function buildVolumetryDimension(
const topChannel = getTopLabel(volumeByChannel?.labels, channelValues);
const topSkill = getTopLabel(skillLabels, skillValues);
// Heatmap 24x7 -> distribución horaria
// Heatmap 24x7 -> hourly distribution
const heatmap24x7 = volumetry?.heatmap_24x7;
const hourly = computeHourlyFromHeatmap(heatmap24x7);
const offHoursPct = hourly.length ? calcOffHoursPct(hourly) : 0;
const peakHours = hourly.length ? findPeakHours(hourly) : [];
console.log('📊 Volumetría backend (mapper):', {
console.log('📊 Backend volumetry (mapper):', {
volumetry,
volumeByChannel,
volumeBySkill,
@@ -240,21 +240,21 @@ function buildVolumetryDimension(
if (totalVolume > 0) {
extraKpis.push({
label: 'Volumen total (backend)',
label: 'Total volume (backend)',
value: totalVolume.toLocaleString('es-ES'),
});
}
if (numChannels > 0) {
extraKpis.push({
label: 'Canales analizados',
label: 'Channels analyzed',
value: String(numChannels),
});
}
if (numSkills > 0) {
extraKpis.push({
label: 'Skills analizadas',
label: 'Skills analyzed',
value: String(numSkills),
});
@@ -271,14 +271,14 @@ function buildVolumetryDimension(
if (topChannel) {
extraKpis.push({
label: 'Canal principal',
label: 'Main channel',
value: topChannel,
});
}
if (topSkill) {
extraKpis.push({
label: 'Skill principal',
label: 'Main skill',
value: topSkill,
});
}
@@ -287,28 +287,28 @@ function buildVolumetryDimension(
return { dimension: undefined, extraKpis };
}
// Calcular ratio pico/valle para evaluar concentración de demanda
// Calculate peak/valley ratio to evaluate demand concentration
const validHourly = hourly.filter(v => v > 0);
const maxHourly = validHourly.length > 0 ? Math.max(...validHourly) : 0;
const minHourly = validHourly.length > 0 ? Math.min(...validHourly) : 1;
const peakValleyRatio = minHourly > 0 ? maxHourly / minHourly : 1;
console.log(`⏰ Hourly distribution (backend path): total=${totalVolume}, peak=${maxHourly}, valley=${minHourly}, ratio=${peakValleyRatio.toFixed(2)}`);
// Score basado en:
// - % fuera de horario (>30% penaliza)
// - Ratio pico/valle (>3x penaliza)
// NO penalizar por tener volumen alto
// Score based on:
// - % off-hours (>30% penalty)
// - Peak/valley ratio (>3x penalty)
// DO NOT penalize for having high volume
let score = 100;
// Penalización por fuera de horario
// Penalty for off-hours
const offHoursPctValue = offHoursPct * 100;
if (offHoursPctValue > 30) {
score -= Math.min(40, (offHoursPctValue - 30) * 2); // -2 pts por cada % sobre 30%
score -= Math.min(40, (offHoursPctValue - 30) * 2); // -2 pts per % over30%
} else if (offHoursPctValue > 20) {
score -= (offHoursPctValue - 20); // -1 pt por cada % entre 20-30%
score -= (offHoursPctValue - 20); // -1 pt per % between 20-30%
}
// Penalización por ratio pico/valle alto
// Penalty for high peak/valley ratio
if (peakValleyRatio > 5) {
score -= 30;
} else if (peakValleyRatio > 3) {
@@ -321,32 +321,32 @@ function buildVolumetryDimension(
const summaryParts: string[] = [];
summaryParts.push(
`${totalVolume.toLocaleString('es-ES')} interacciones analizadas.`
`${totalVolume.toLocaleString('es-ES')} interactions analyzed.`
);
summaryParts.push(
`${(offHoursPct * 100).toFixed(0)}% fuera de horario laboral (8-19h).`
`${(offHoursPct * 100).toFixed(0)}% outside business hours (8-19h).`
);
if (peakValleyRatio > 2) {
summaryParts.push(
`Ratio pico/valle: ${peakValleyRatio.toFixed(1)}x - alta concentración de demanda.`
`Peak/valley ratio: ${peakValleyRatio.toFixed(1)}x - high demand concentration.`
);
}
if (topSkill) {
summaryParts.push(`Skill principal: ${topSkill}.`);
summaryParts.push(`Main skill: ${topSkill}.`);
}
// Métrica principal accionable: % fuera de horario
// Main actionable metric: % off-hours
const dimension: DimensionAnalysis = {
id: 'volumetry_distribution',
name: 'volumetry_distribution',
title: 'Volumetría y distribución de demanda',
title: 'Volumetry and demand distribution',
score,
percentile: undefined,
summary: summaryParts.join(' '),
kpi: {
label: 'Fuera de horario',
label: 'Off-hours',
value: `${(offHoursPct * 100).toFixed(0)}%`,
change: peakValleyRatio > 2 ? `Pico/valle: ${peakValleyRatio.toFixed(1)}x` : undefined,
change: peakValleyRatio > 2 ? `Peak/valley: ${peakValleyRatio.toFixed(1)}x` : undefined,
changeType: offHoursPct > 0.3 ? 'negative' : offHoursPct > 0.2 ? 'neutral' : 'positive'
},
icon: BarChartHorizontal,
@@ -362,7 +362,7 @@ function buildVolumetryDimension(
return { dimension, extraKpis };
}
// ==== Eficiencia Operativa (v3.2 - con segmentación horaria) ====
// ==== Operational Efficiency (v3.2 - with hourly segmentation) ====
function buildOperationalEfficiencyDimension(
raw: BackendRawResults,
@@ -371,25 +371,25 @@ function buildOperationalEfficiencyDimension(
const op = raw?.operational_performance;
if (!op) return undefined;
// AHT Global
// Global AHT
const ahtP50 = safeNumber(op.aht_distribution?.p50, 0);
const ahtP90 = safeNumber(op.aht_distribution?.p90, 0);
const ratioGlobal = ahtP90 > 0 && ahtP50 > 0 ? ahtP90 / ahtP50 : safeNumber(op.aht_distribution?.p90_p50_ratio, 1.5);
// AHT Horario Laboral (8-19h) - estimación basada en distribución
// Asumimos que el AHT en horario laboral es ligeramente menor (más eficiente)
const ahtBusinessHours = Math.round(ahtP50 * 0.92); // ~8% más eficiente en horario laboral
const ratioBusinessHours = ratioGlobal * 0.85; // Menor variabilidad en horario laboral
// Business Hours AHT (8-19h) - estimation based on distribution
// We assume that AHT during business hours is slightly lower (more efficient)
const ahtBusinessHours = Math.round(ahtP50 * 0.92); // ~8% more efficient during business hours
const ratioBusinessHours = ratioGlobal * 0.85; // Lower variability during business hours
// Determinar si la variabilidad se reduce fuera de horario
// Determine if variability reduces outside hours
const variabilityReduction = ratioGlobal - ratioBusinessHours;
const variabilityInsight = variabilityReduction > 0.3
? 'La variabilidad se reduce significativamente en horario laboral.'
? 'Variability significantly reduces during business hours.'
: variabilityReduction > 0.1
? 'La variabilidad se mantiene similar en ambos horarios.'
: 'La variabilidad es consistente independientemente del horario.';
? 'Variability remains similar in both schedules.'
: 'Variability is consistent regardless of schedule.';
// Score basado en escala definida:
// Score based on defined scale:
// <1.5 = 100pts, 1.5-2.0 = 70pts, 2.0-2.5 = 50pts, 2.5-3.0 = 30pts, >3.0 = 20pts
let score: number;
if (ratioGlobal < 1.5) {
@@ -404,9 +404,9 @@ function buildOperationalEfficiencyDimension(
score = 20;
}
// Summary con segmentación
let summary = `AHT Global: ${Math.round(ahtP50)}s (P50), ratio ${ratioGlobal.toFixed(2)}. `;
summary += `AHT Horario Laboral (8-19h): ${ahtBusinessHours}s (P50), ratio ${ratioBusinessHours.toFixed(2)}. `;
// Summary with segmentation
let summary = `Global AHT: ${Math.round(ahtP50)}s (P50), ratio ${ratioGlobal.toFixed(2)}. `;
summary += `Business Hours AHT (8-19h): ${ahtBusinessHours}s (P50), ratio ${ratioBusinessHours.toFixed(2)}. `;
summary += variabilityInsight;
// KPI principal: AHT P50 (industry standard for operational efficiency)
@@ -420,7 +420,7 @@ function buildOperationalEfficiencyDimension(
const dimension: DimensionAnalysis = {
id: 'operational_efficiency',
name: 'operational_efficiency',
title: 'Eficiencia Operativa',
title: 'Operational Efficiency',
score,
percentile: undefined,
summary,
@@ -431,7 +431,7 @@ function buildOperationalEfficiencyDimension(
return dimension;
}
// ==== Efectividad & Resolución (v3.2 - enfocada en FCR Técnico) ====
// ==== Effectiveness & Resolution (v3.2 - focused on Technical FCR) ====
function buildEffectivenessResolutionDimension(
raw: BackendRawResults
@@ -439,20 +439,20 @@ function buildEffectivenessResolutionDimension(
const op = raw?.operational_performance;
if (!op) return undefined;
// FCR Técnico = 100 - transfer_rate (comparable con benchmarks de industria)
// Usamos escalation_rate que es la tasa de transferencias
// Technical FCR = 100 - transfer_rate (comparable with industry benchmarks)
// We use escalation_rate which is the transfer rate
const escalationRate = safeNumber(op.escalation_rate, NaN);
const abandonmentRate = safeNumber(op.abandonment_rate, 0);
// FCR Técnico: 100 - tasa de transferencia
// Technical FCR: 100 - tasa de transferencia
const fcrRate = Number.isFinite(escalationRate) && escalationRate >= 0
? Math.max(0, Math.min(100, 100 - escalationRate))
: 70; // valor por defecto benchmark aéreo
: 70; // default airline benchmark value
// Tasa de transferencia (complemento del FCR Técnico)
// Transfer rate (complement of Technical FCR)
const transferRate = Number.isFinite(escalationRate) ? escalationRate : 100 - fcrRate;
// Score basado en FCR Técnico (benchmark sector aéreo: 85-90%)
// Score based on Technical FCR (benchmark airline sector: 85-90%)
// FCR >= 90% = 100pts, 85-90% = 80pts, 80-85% = 60pts, 75-80% = 40pts, <75% = 20pts
let score: number;
if (fcrRate >= 90) {
@@ -467,25 +467,25 @@ function buildEffectivenessResolutionDimension(
score = 20;
}
// Penalización adicional por abandono alto (>8%)
// Additional penalty for high abandonment (>8%)
if (abandonmentRate > 8) {
score = Math.max(0, score - Math.round((abandonmentRate - 8) * 2));
}
// Summary enfocado en FCR Técnico
let summary = `FCR Técnico: ${fcrRate.toFixed(1)}% (benchmark: 85-90%). `;
summary += `Tasa de transferencia: ${transferRate.toFixed(1)}%. `;
// Summary focused on Technical FCR
let summary = `Technical FCR: ${fcrRate.toFixed(1)}% (benchmark: 85-90%). `;
summary += `Transfer rate: ${transferRate.toFixed(1)}%. `;
if (fcrRate >= 90) {
summary += 'Excelente resolución en primer contacto.';
summary += 'Excellent first contact resolution.';
} else if (fcrRate >= 85) {
summary += 'Resolución dentro del benchmark del sector.';
summary += 'Resolution within sector benchmark.';
} else {
summary += 'Oportunidad de mejora reduciendo transferencias.';
summary += 'Opportunity to improve by reducing transfers.';
}
const kpi: Kpi = {
label: 'FCR Técnico',
label: 'Technical FCR',
value: `${fcrRate.toFixed(0)}%`,
change: `Transfer: ${transferRate.toFixed(0)}%`,
changeType: fcrRate >= 85 ? 'positive' : fcrRate >= 80 ? 'neutral' : 'negative'
@@ -494,7 +494,7 @@ function buildEffectivenessResolutionDimension(
const dimension: DimensionAnalysis = {
id: 'effectiveness_resolution',
name: 'effectiveness_resolution',
title: 'Efectividad & Resolución',
title: 'Effectiveness & Resolution',
score,
percentile: undefined,
summary,
@@ -505,7 +505,7 @@ function buildEffectivenessResolutionDimension(
return dimension;
}
// ==== Complejidad & Predictibilidad (v3.4 - basada en CV AHT per industry standards) ====
// ==== Complexity & Predictability (v3.4 - based on CV AHT per industry standards) ====
function buildComplexityPredictabilityDimension(
raw: BackendRawResults
@@ -535,9 +535,9 @@ function buildComplexityPredictabilityDimension(
}
}
// Score basado en CV AHT (benchmark: <75% = excelente, <100% = aceptable)
// Score based on CV AHT (benchmark: <75% = excellent, <100% = acceptable)
// CV <= 75% = 100pts (alta predictibilidad)
// CV 75-100% = 80pts (predictibilidad aceptable)
// CV 75-100% = 80pts (acceptable predictability)
// CV 100-125% = 60pts (variabilidad moderada)
// CV 125-150% = 40pts (alta variabilidad)
// CV > 150% = 20pts (muy alta variabilidad)
@@ -558,16 +558,16 @@ function buildComplexityPredictabilityDimension(
let summary = `CV AHT: ${cvAhtPercent}% (benchmark: <75%). `;
if (cvAhtPercent <= 75) {
summary += 'Alta predictibilidad: tiempos de atención consistentes. Excelente para planificación WFM.';
summary += 'High predictability: consistent handling times. Excellent for WFM planning.';
} else if (cvAhtPercent <= 100) {
summary += 'Predictibilidad aceptable: variabilidad moderada en tiempos de atención.';
summary += 'Acceptable predictability: moderate variability in handling times.';
} else if (cvAhtPercent <= 125) {
summary += 'Variabilidad notable: dificulta la planificación de recursos. Considerar estandarización.';
summary += 'Notable variability: complicates resource planning. Consider standardization.';
} else {
summary += 'Alta variabilidad: tiempos muy dispersos. Priorizar scripts guiados y estandarización.';
summary += 'High variability: very scattered times. Prioritize guided scripts and standardization.';
}
// Añadir info de Hold P50 promedio si está disponible (proxy de complejidad)
// Add Hold P50 average info if available (complexity proxy)
if (avgHoldP50 > 0) {
summary += ` Hold Time P50: ${Math.round(avgHoldP50)}s.`;
}
@@ -583,7 +583,7 @@ function buildComplexityPredictabilityDimension(
const dimension: DimensionAnalysis = {
id: 'complexity_predictability',
name: 'complexity_predictability',
title: 'Complejidad & Predictibilidad',
title: 'Complexity & Predictability',
score,
percentile: undefined,
summary,
@@ -594,7 +594,7 @@ function buildComplexityPredictabilityDimension(
return dimension;
}
// ==== Satisfacción del Cliente (v3.1) ====
// ==== Customer Satisfaction (v3.1) ====
function buildSatisfactionDimension(
raw: BackendRawResults
@@ -604,19 +604,19 @@ function buildSatisfactionDimension(
const hasCSATData = Number.isFinite(csatGlobalRaw) && csatGlobalRaw > 0;
// Si no hay CSAT, mostrar dimensión con "No disponible"
// Si no hay CSAT, mostrar dimensión con "Not available"
const dimension: DimensionAnalysis = {
id: 'customer_satisfaction',
name: 'customer_satisfaction',
title: 'Satisfacción del Cliente',
score: hasCSATData ? Math.round((csatGlobalRaw / 5) * 100) : -1, // -1 indica N/A
title: 'Customer Satisfaction',
score: hasCSATData ? Math.round((csatGlobalRaw / 5) * 100) : -1, // -1 indicates N/A
percentile: undefined,
summary: hasCSATData
? `CSAT global: ${csatGlobalRaw.toFixed(1)}/5. ${csatGlobalRaw >= 4.0 ? 'Nivel de satisfacción óptimo.' : csatGlobalRaw >= 3.5 ? 'Satisfacción aceptable, margen de mejora.' : 'Satisfacción baja, requiere atención urgente.'}`
: 'CSAT no disponible en el dataset. Para incluir esta dimensión, añadir datos de encuestas de satisfacción.',
? `Global CSAT: ${csatGlobalRaw.toFixed(1)}/5. ${csatGlobalRaw >= 4.0 ? 'Optimal satisfaction level.' : csatGlobalRaw >= 3.5 ? 'Acceptable satisfaction, room for improvement.' : 'Low satisfaction, requires urgent attention.'}`
: 'CSAT not available in dataset. To include this dimension, add satisfaction survey data.',
kpi: {
label: 'CSAT',
value: hasCSATData ? `${csatGlobalRaw.toFixed(1)}/5` : 'No disponible',
value: hasCSATData ? `${csatGlobalRaw.toFixed(1)}/5` : 'Not available',
changeType: hasCSATData
? (csatGlobalRaw >= 4.0 ? 'positive' : csatGlobalRaw >= 3.5 ? 'neutral' : 'negative')
: 'neutral'
@@ -627,7 +627,7 @@ function buildSatisfactionDimension(
return dimension;
}
// ==== Economía - Coste por Interacción (v3.1) ====
// ==== Economy - Cost per Interaction (v3.1) ====
function buildEconomyDimension(
raw: BackendRawResults,
@@ -637,9 +637,9 @@ function buildEconomyDimension(
const op = raw?.operational_performance;
const totalAnnual = safeNumber(econ?.cost_breakdown?.total_annual, 0);
// Benchmark CPI aerolíneas (consistente con ExecutiveSummaryTab)
// Airline CPI benchmark (consistent with ExecutiveSummaryTab)
// p25: 2.20, p50: 3.50, p75: 4.50, p90: 5.50
const CPI_BENCHMARK = 3.50; // p50 aerolíneas
const CPI_BENCHMARK = 3.50; // airline p50
if (totalAnnual <= 0 || totalInteractions <= 0) {
return undefined;
@@ -652,12 +652,12 @@ function buildEconomyDimension(
// Calcular CPI usando cost_volume (non-abandoned) como denominador
const cpi = costVolume > 0 ? totalAnnual / costVolume : totalAnnual / totalInteractions;
// Score basado en percentiles de aerolíneas (CPI invertido: menor = mejor)
// CPI <= 2.20 (p25) = 100pts (excelente, top 25%)
// Score based on airline percentiles (inverse CPI: lower = better)
// CPI <= 2.20 (p25) = 100pts (excellent, top 25%)
// CPI 2.20-3.50 (p25-p50) = 80pts (bueno, top 50%)
// CPI 3.50-4.50 (p50-p75) = 60pts (promedio)
// CPI 3.50-4.50 (p50-p75) = 60pts (average)
// CPI 4.50-5.50 (p75-p90) = 40pts (por debajo)
// CPI > 5.50 (>p90) = 20pts (crítico)
// CPI > 5.50 (>p90) = 20pts (critical)
let score: number;
if (cpi <= 2.20) {
score = 100;
@@ -674,24 +674,24 @@ function buildEconomyDimension(
const cpiDiff = cpi - CPI_BENCHMARK;
const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative';
let summary = `Coste por interacción: €${cpi.toFixed(2)} vs benchmark €${CPI_BENCHMARK.toFixed(2)}. `;
let summary = `Cost per interaction: €${cpi.toFixed(2)} vs benchmark €${CPI_BENCHMARK.toFixed(2)}. `;
if (cpi <= CPI_BENCHMARK) {
summary += 'Eficiencia de costes óptima, por debajo del benchmark del sector.';
summary += 'Optimal cost efficiency, below sector benchmark.';
} else if (cpi <= 4.50) {
summary += 'Coste ligeramente por encima del benchmark, oportunidad de optimización.';
summary += 'Cost slightly above benchmark, optimization opportunity.';
} else {
summary += 'Coste elevado respecto al sector. Priorizar iniciativas de eficiencia.';
summary += 'High cost relative to sector. Prioritize efficiency initiatives.';
}
const dimension: DimensionAnalysis = {
id: 'economy_costs',
name: 'economy_costs',
title: 'Economía & Costes',
title: 'Economy & Costs',
score,
percentile: undefined,
summary,
kpi: {
label: 'Coste por Interacción',
label: 'Cost per Interaction',
value: `${cpi.toFixed(2)}`,
change: `vs benchmark €${CPI_BENCHMARK.toFixed(2)}`,
changeType: cpiStatus as 'positive' | 'neutral' | 'negative'
@@ -779,7 +779,7 @@ function buildAgenticReadinessDimension(
}
// ==== Economía y costes (economy_costs) ====
// ==== Economy and costs (economy_costs) ====
function buildEconomicModel(raw: BackendRawResults): EconomicModelData {
const econ = raw?.economy_costs;
@@ -814,17 +814,17 @@ function buildEconomicModel(raw: BackendRawResults): EconomicModelData {
const savingsBreakdown = annualSavings
? [
{
category: 'Ineficiencias operativas (AHT, escalaciones)',
category: 'Operational inefficiencies (AHT, escalations)',
amount: Math.round(annualSavings * 0.5),
percentage: 50,
},
{
category: 'Automatización de volumen repetitivo',
category: 'Automation of repetitive volume',
amount: Math.round(annualSavings * 0.3),
percentage: 30,
},
{
category: 'Otros beneficios (calidad, CX)',
category: 'Other benefits (quality, CX)',
amount: Math.round(annualSavings * 0.2),
percentage: 20,
},
@@ -834,7 +834,7 @@ function buildEconomicModel(raw: BackendRawResults): EconomicModelData {
const costBreakdown = currentAnnualCost
? [
{
category: 'Coste laboral',
category: 'Labor cost',
amount: laborAnnual,
percentage: Math.round(
(laborAnnual / currentAnnualCost) * 100
@@ -848,7 +848,7 @@ function buildEconomicModel(raw: BackendRawResults): EconomicModelData {
),
},
{
category: 'Tecnología',
category: 'Technology',
amount: techAnnual,
percentage: Math.round(
(techAnnual / currentAnnualCost) * 100
@@ -914,7 +914,7 @@ export function mapBackendResultsToAnalysisData(
Math.min(100, Math.round(arScore * 10))
);
// v3.3: 7 dimensiones (Complejidad recuperada con métrica Hold Time >60s)
// v3.3: 7 dimensions (Complexity recovered with Hold Time metric >60s)
const { dimension: volumetryDimension, extraKpis } =
buildVolumetryDimension(raw);
const operationalEfficiencyDimension = buildOperationalEfficiencyDimension(raw);
@@ -946,7 +946,7 @@ export function mapBackendResultsToAnalysisData(
const csatAvg = computeCsatAverage(cs);
// CSAT global (opcional)
// Global CSAT (opcional)
const csatGlobalRaw = safeNumber(cs?.csat_global, NaN);
const csatGlobal =
Number.isFinite(csatGlobalRaw) && csatGlobalRaw > 0
@@ -954,7 +954,7 @@ export function mapBackendResultsToAnalysisData(
: undefined;
// KPIs de resumen (los 4 primeros son los que se ven en "Métricas de Contacto")
// Summary KPIs (the first 4 are shown in "Contact Metrics")
const summaryKpis: Kpi[] = [];
// 1) Interacciones Totales (volumen backend)
@@ -975,9 +975,9 @@ export function mapBackendResultsToAnalysisData(
: 'N/D',
});
// 3) Tasa FCR
// 3) FCR Rate
summaryKpis.push({
label: 'Tasa FCR',
label: 'FCR Rate',
value:
fcrPct !== undefined
? `${Math.round(fcrPct)}%`
@@ -993,18 +993,18 @@ export function mapBackendResultsToAnalysisData(
: 'N/D',
});
// --- KPIs adicionales, usados en otras secciones ---
// --- Additional KPIs, used in other sections ---
if (numChannels > 0) {
summaryKpis.push({
label: 'Canales analizados',
label: 'Channels analyzed',
value: String(numChannels),
});
}
if (numSkills > 0) {
summaryKpis.push({
label: 'Skills analizadas',
label: 'Skills analyzed',
value: String(numSkills),
});
}
@@ -1027,13 +1027,13 @@ export function mapBackendResultsToAnalysisData(
if (totalAnnual) {
summaryKpis.push({
label: 'Coste anual actual (backend)',
label: 'Current annual cost (backend)',
value: `${totalAnnual.toFixed(0)}`,
});
}
if (annualSavings) {
summaryKpis.push({
label: 'Ahorro potencial anual (backend)',
label: 'Annual potential savings (backend)',
value: `${annualSavings.toFixed(0)}`,
});
}
@@ -1043,22 +1043,22 @@ export function mapBackendResultsToAnalysisData(
const economicModel = buildEconomicModel(raw);
const benchmarkData = buildBenchmarkData(raw);
// Generar findings y recommendations basados en volumetría
// Generate findings and recommendations based on volumetry
const findings: Finding[] = [];
const recommendations: Recommendation[] = [];
// Extraer offHoursPct de la dimensión de volumetría
const offHoursPct = volumetryDimension?.distribution_data?.off_hours_pct ?? 0;
const offHoursPctValue = offHoursPct * 100; // Convertir de 0-1 a 0-100
const offHoursPctValue = offHoursPct * 100; // Convert from 0-1 a 0-100
if (offHoursPctValue > 20) {
const offHoursVolume = Math.round(totalVolume * offHoursPctValue / 100);
findings.push({
type: offHoursPctValue > 30 ? 'critical' : 'warning',
title: 'Alto Volumen Fuera de Horario',
text: `${offHoursPctValue.toFixed(0)}% de interacciones fuera de horario (8-19h)`,
title: 'High Off-Hours Volume',
text: `${offHoursPctValue.toFixed(0)}% of off-hours interactions (8-19h)`,
dimensionId: 'volumetry_distribution',
description: `${offHoursVolume.toLocaleString()} interacciones (${offHoursPctValue.toFixed(1)}%) ocurren fuera de horario laboral. Oportunidad ideal para implementar agentes virtuales 24/7.`,
description: `${offHoursVolume.toLocaleString()} interacciones (${offHoursPctValue.toFixed(1)}%) ocurren outside business hours. Ideal opportunity to implement 24/7 virtual agents.`,
impact: offHoursPctValue > 30 ? 'high' : 'medium'
});
@@ -1066,12 +1066,12 @@ export function mapBackendResultsToAnalysisData(
const estimatedSavings = Math.round(offHoursVolume * estimatedContainment / 100);
recommendations.push({
priority: 'high',
title: 'Implementar Agente Virtual 24/7',
text: `Desplegar agente virtual para atender ${offHoursPctValue.toFixed(0)}% de interacciones fuera de horario`,
description: `${offHoursVolume.toLocaleString()} interacciones ocurren fuera de horario laboral (19:00-08:00). Un agente virtual puede resolver ~${estimatedContainment}% de estas consultas automáticamente.`,
title: 'Implement 24/7 Virtual Agent',
text: `Deploy virtual agent to handle ${offHoursPctValue.toFixed(0)}% of off-hours interactions`,
description: `${offHoursVolume.toLocaleString()} interactions occur outside business hours (19:00-08:00). A virtual agent can resolve ~${estimatedContainment}% of these queries automatically.`,
dimensionId: 'volumetry_distribution',
impact: `Potencial de contención: ${estimatedSavings.toLocaleString()} interacciones/período`,
timeline: '1-3 meses'
impact: `Containment potential: ${estimatedSavings.toLocaleString()} interacciones/período`,
timeline: '1-3 months'
});
}
@@ -1080,7 +1080,7 @@ export function mapBackendResultsToAnalysisData(
overallHealthScore,
summaryKpis: mergedKpis,
dimensions,
heatmapData: [], // el heatmap por skill lo seguimos generando en el front
heatmapData: [], // skill heatmap still generated on frontend
findings,
recommendations,
opportunities: [],
@@ -1166,9 +1166,9 @@ export function buildHeatmapFromBackend(
abandonment_rate: number;
fcr_tecnico: number;
fcr_real: number;
aht_mean: number; // AHT promedio del backend (solo VALID - consistente con fresh path)
aht_total: number; // AHT total (ALL rows incluyendo NOISE/ZOMBIE/ABANDON) - solo informativo
hold_time_mean: number; // Hold time promedio (consistente con fresh path - MEAN, no P50)
aht_mean: number; // Average AHT del backend (only VALID - consistent with fresh path)
aht_total: number; // Total AHT (ALL rows incluyendo NOISE/ZOMBIE/ABANDON) - informational only
hold_time_mean: number; // Average Hold time (consistent with fresh path - MEAN, not P50)
}>();
for (const m of metricsBySkillRaw) {
@@ -1178,9 +1178,9 @@ export function buildHeatmapFromBackend(
abandonment_rate: safeNumber(m.abandonment_rate, NaN),
fcr_tecnico: safeNumber(m.fcr_tecnico, NaN),
fcr_real: safeNumber(m.fcr_real, NaN),
aht_mean: safeNumber(m.aht_mean, NaN), // AHT promedio (solo VALID)
aht_total: safeNumber(m.aht_total, NaN), // AHT total (ALL rows)
hold_time_mean: safeNumber(m.hold_time_mean, NaN), // Hold time promedio (MEAN)
aht_mean: safeNumber(m.aht_mean, NaN), // Average AHT (solo VALID)
aht_total: safeNumber(m.aht_total, NaN), // Total AHT (ALL rows)
hold_time_mean: safeNumber(m.hold_time_mean, NaN), // Average Hold time (MEAN)
});
}
}
@@ -1314,7 +1314,7 @@ export function buildHeatmapFromBackend(
// Dimensiones agentic similares a las que tenías en generateHeatmapData,
// pero usando valores reales en lugar de aleatorios.
// 1) Predictibilidad (menor CV => mayor puntuación)
// 1) Predictability (lower CV => higher score)
const predictability_score = Math.max(
0,
Math.min(
@@ -1347,14 +1347,14 @@ export function buildHeatmapFromBackend(
} else {
// NO usar estimación - usar valores globales del backend directamente
// Esto asegura consistencia con el fresh path que usa valores directos del CSV
skillTransferRate = globalEscalation; // Usar tasa global, sin estimación
skillTransferRate = globalEscalation; // Use global rate, no estimation
skillAbandonmentRate = abandonmentRateBackend;
skillFcrTecnico = 100 - skillTransferRate;
skillFcrReal = globalFcrPct;
console.warn(`⚠️ No metrics_by_skill for skill ${skill} - using global rates`);
}
// Complejidad inversa basada en transfer rate del skill
// Inverse complexity based on skill transfer rate
const complexity_inverse_score = Math.max(
0,
Math.min(
@@ -1446,10 +1446,10 @@ export function buildHeatmapFromBackend(
volume,
cost_volume: costVolume,
aht_seconds: aht_mean,
aht_total: aht_total, // AHT con TODAS las filas (solo informativo)
aht_total: aht_total, // AHT con TODAS las filas (informational only)
metrics: {
fcr: Math.round(skillFcrReal), // FCR Real (sin transfer Y sin recontacto 7d)
fcr_tecnico: Math.round(skillFcrTecnico), // FCR Técnico (comparable con benchmarks)
fcr_tecnico: Math.round(skillFcrTecnico), // Technical FCR (comparable con benchmarks)
aht: ahtMetric,
csat: csatMetric0_100,
hold_time: holdMetric,
@@ -1457,12 +1457,12 @@ export function buildHeatmapFromBackend(
abandonment_rate: Math.round(skillAbandonmentRate),
},
annual_cost,
cpi: skillCpi, // CPI real del backend (si disponible)
cpi: skillCpi, // Real CPI from backend (if available)
variability: {
cv_aht: Math.round(cv_aht * 100), // %
cv_talk_time: 0,
cv_hold_time: 0,
transfer_rate: skillTransferRate, // Transfer rate REAL o estimado
transfer_rate: skillTransferRate, // REAL or estimated transfer rate
},
automation_readiness,
dimensions: {
@@ -1491,19 +1491,19 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
const benchmarkData: AnalysisData['benchmarkData'] = [];
// Benchmarks hardcoded para sector aéreo
// Hardcoded benchmarks for airline sector
const AIRLINE_BENCHMARKS = {
aht_p50: 380, // segundos
aht_p50: 380, // seconds
fcr: 70, // % (rango 68-72%)
abandonment: 5, // % (rango 5-8%)
ratio_p90_p50: 2.0, // ratio saludable
cpi: 5.25 // € (rango €4.50-€6.00)
};
// 1. AHT Promedio (benchmark sector aéreo: 380s)
// 1. AHT Promedio (benchmark airline sector: 380s)
const ahtP50 = safeNumber(op?.aht_distribution?.p50, 0);
if (ahtP50 > 0) {
// Percentil: menor AHT = mejor. Si AHT <= benchmark = P75+
// Percentile: lower AHT = better. If AHT <= benchmark = P75+
const ahtPercentile = ahtP50 <= AIRLINE_BENCHMARKS.aht_p50
? Math.min(90, 75 + Math.round((AIRLINE_BENCHMARKS.aht_p50 - ahtP50) / 10))
: Math.max(10, 75 - Math.round((ahtP50 - AIRLINE_BENCHMARKS.aht_p50) / 5));
@@ -1521,15 +1521,15 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
});
}
// 2. Tasa FCR (benchmark sector aéreo: 70%)
// 2. FCR Rate (benchmark airline sector: 70%)
const fcrRate = safeNumber(op?.fcr_rate, NaN);
if (Number.isFinite(fcrRate) && fcrRate >= 0) {
// Percentil: mayor FCR = mejor
// Percentile: higher FCR = better
const fcrPercentile = fcrRate >= AIRLINE_BENCHMARKS.fcr
? Math.min(90, 50 + Math.round((fcrRate - AIRLINE_BENCHMARKS.fcr) * 2))
: Math.max(10, 50 - Math.round((AIRLINE_BENCHMARKS.fcr - fcrRate) * 2));
benchmarkData.push({
kpi: 'Tasa FCR',
kpi: 'FCR Rate',
userValue: fcrRate / 100,
userDisplay: `${Math.round(fcrRate)}%`,
industryValue: AIRLINE_BENCHMARKS.fcr / 100,
@@ -1560,15 +1560,15 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
});
}
// 4. Tasa de Abandono (benchmark sector aéreo: 5%)
// 4. Abandonment Rate (benchmark airline sector: 5%)
const abandonRate = safeNumber(op?.abandonment_rate, NaN);
if (Number.isFinite(abandonRate) && abandonRate >= 0) {
// Percentil: menor abandono = mejor
// Percentile: lower abandonment = better
const abandonPercentile = abandonRate <= AIRLINE_BENCHMARKS.abandonment
? Math.min(90, 75 + Math.round((AIRLINE_BENCHMARKS.abandonment - abandonRate) * 5))
: Math.max(10, 75 - Math.round((abandonRate - AIRLINE_BENCHMARKS.abandonment) * 5));
benchmarkData.push({
kpi: 'Tasa de Abandono',
kpi: 'Abandonment Rate',
userValue: abandonRate / 100,
userDisplay: `${abandonRate.toFixed(1)}%`,
industryValue: AIRLINE_BENCHMARKS.abandonment / 100,
@@ -1581,11 +1581,11 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
});
}
// 5. Ratio P90/P50 (benchmark sector aéreo: <2.0)
// 5. Ratio P90/P50 (benchmark airline sector: <2.0)
const ahtP90 = safeNumber(op?.aht_distribution?.p90, 0);
const ratio = ahtP50 > 0 && ahtP90 > 0 ? ahtP90 / ahtP50 : 0;
if (ratio > 0) {
// Percentil: menor ratio = mejor
// Percentile: lower ratio = better
const ratioPercentile = ratio <= AIRLINE_BENCHMARKS.ratio_p90_p50
? Math.min(90, 75 + Math.round((AIRLINE_BENCHMARKS.ratio_p90_p50 - ratio) * 30))
: Math.max(10, 75 - Math.round((ratio - AIRLINE_BENCHMARKS.ratio_p90_p50) * 30));
@@ -1603,13 +1603,13 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
});
}
// 6. Tasa de Transferencia/Escalación
// 6. Transfer/Escalation Rate
const escalationRate = safeNumber(op?.escalation_rate, NaN);
if (Number.isFinite(escalationRate) && escalationRate >= 0) {
// Menor escalación = mejor percentil
// Menor escalación = better percentil
const escalationPercentile = Math.max(10, Math.min(90, Math.round(100 - escalationRate * 5)));
benchmarkData.push({
kpi: 'Tasa de Transferencia',
kpi: 'Transfer Rate',
userValue: escalationRate / 100,
userDisplay: `${escalationRate.toFixed(1)}%`,
industryValue: 0.15,
@@ -1622,7 +1622,7 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
});
}
// 7. CPI - Coste por Interacción (benchmark sector aéreo: €4.50-€6.00)
// 7. CPI - Cost per Interaction (benchmark airline sector: €4.50-€6.00)
const econ = raw?.economy_costs;
const totalAnnualCost = safeNumber(econ?.cost_breakdown?.total_annual, 0);
const volumetry = raw?.volumetry;
@@ -1634,7 +1634,7 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
if (totalAnnualCost > 0 && totalInteractions > 0) {
const cpi = totalAnnualCost / totalInteractions;
// Menor CPI = mejor. Si CPI <= 4.50 = excelente (P90+), si CPI >= 6.00 = malo (P25-)
// Lower CPI = better. If CPI <= 4.50 = excellent (P90+), if CPI >= 6.00 = poor (P25-)
let cpiPercentile: number;
if (cpi <= 4.50) {
cpiPercentile = Math.min(95, 90 + Math.round((4.50 - cpi) * 10));
@@ -1647,7 +1647,7 @@ function buildBenchmarkData(raw: BackendRawResults): AnalysisData['benchmarkData
}
benchmarkData.push({
kpi: 'Coste por Interacción (CPI)',
kpi: 'Cost per Interaction (CPI)',
userValue: cpi,
userDisplay: `${cpi.toFixed(2)}`,
industryValue: AIRLINE_BENCHMARKS.cpi,