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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff