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 {
|
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
Reference in New Issue
Block a user