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:
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user