649 lines
22 KiB
TypeScript
649 lines
22 KiB
TypeScript
/**
|
|
* Generación de análisis con datos reales (no sintéticos)
|
|
*/
|
|
|
|
import type { AnalysisData, Kpi, DimensionAnalysis, HeatmapDataPoint, Opportunity, RoadmapInitiative, EconomicModelData, BenchmarkDataPoint, Finding, Recommendation, TierKey, CustomerSegment, RawInteraction, AgenticReadinessResult, SubFactor, SkillMetrics } from '../types';
|
|
import { RoadmapPhase } from '../types';
|
|
import { BarChartHorizontal, Zap, Smile, DollarSign, Target, Globe } from 'lucide-react';
|
|
import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2';
|
|
import { classifyQueue } from './segmentClassifier';
|
|
|
|
/**
|
|
* Generar análisis completo con datos reales
|
|
*/
|
|
export function generateAnalysisFromRealData(
|
|
tier: TierKey,
|
|
interactions: RawInteraction[],
|
|
costPerHour: number,
|
|
avgCsat: number,
|
|
segmentMapping?: { high_value_queues: string[]; medium_value_queues: string[]; low_value_queues: string[] }
|
|
): AnalysisData {
|
|
console.log(`🔄 Generating analysis from ${interactions.length} real interactions`);
|
|
|
|
// PASO 1: Limpieza de ruido (duration < 10s)
|
|
const cleanedInteractions = interactions.filter(i => {
|
|
const totalDuration = i.duration_talk + i.hold_time + i.wrap_up_time;
|
|
return totalDuration >= 10;
|
|
});
|
|
|
|
console.log(`🧹 Cleaned: ${interactions.length} → ${cleanedInteractions.length} (removed ${interactions.length - cleanedInteractions.length} noise)`);
|
|
|
|
// PASO 2: Calcular métricas por skill
|
|
const skillMetrics = calculateSkillMetrics(cleanedInteractions, costPerHour);
|
|
|
|
console.log(`📊 Calculated metrics for ${skillMetrics.length} skills`);
|
|
|
|
// PASO 3: Generar heatmap data con dimensiones
|
|
const heatmapData = generateHeatmapFromMetrics(skillMetrics, avgCsat, segmentMapping);
|
|
|
|
// PASO 4: Calcular métricas globales
|
|
const totalInteractions = cleanedInteractions.length;
|
|
const avgAHT = Math.round(skillMetrics.reduce((sum, s) => sum + s.aht_mean, 0) / skillMetrics.length);
|
|
const avgFCR = Math.round((skillMetrics.reduce((sum, s) => sum + (100 - s.transfer_rate), 0) / skillMetrics.length));
|
|
const totalCost = Math.round(skillMetrics.reduce((sum, s) => sum + s.total_cost, 0));
|
|
|
|
// KPIs principales
|
|
const summaryKpis: Kpi[] = [
|
|
{ label: "Interacciones Totales", value: totalInteractions.toLocaleString('es-ES') },
|
|
{ label: "AHT Promedio", value: `${avgAHT}s` },
|
|
{ label: "Tasa FCR", value: `${avgFCR}%` },
|
|
{ label: "CSAT", value: `${(avgCsat / 20).toFixed(1)}/5` }
|
|
];
|
|
|
|
// Health Score basado en métricas reales
|
|
const overallHealthScore = calculateHealthScore(heatmapData);
|
|
|
|
// Dimensiones (simplificadas para datos reales)
|
|
const dimensions: DimensionAnalysis[] = generateDimensionsFromRealData(
|
|
cleanedInteractions,
|
|
skillMetrics,
|
|
avgCsat,
|
|
avgAHT
|
|
);
|
|
|
|
// Agentic Readiness Score
|
|
const agenticReadiness = calculateAgenticReadinessFromRealData(skillMetrics);
|
|
|
|
// Findings y Recommendations
|
|
const findings = generateFindingsFromRealData(skillMetrics, cleanedInteractions);
|
|
const recommendations = generateRecommendationsFromRealData(skillMetrics);
|
|
|
|
// Opportunities
|
|
const opportunities = generateOpportunitiesFromRealData(skillMetrics, costPerHour);
|
|
|
|
// Roadmap
|
|
const roadmap = generateRoadmapFromRealData(opportunities);
|
|
|
|
// Economic Model
|
|
const economicModel = generateEconomicModelFromRealData(skillMetrics, costPerHour);
|
|
|
|
// Benchmark
|
|
const benchmarkData = generateBenchmarkFromRealData(skillMetrics);
|
|
|
|
return {
|
|
tier,
|
|
overallHealthScore,
|
|
summaryKpis,
|
|
dimensions,
|
|
heatmapData,
|
|
agenticReadiness,
|
|
findings,
|
|
recommendations,
|
|
opportunities,
|
|
roadmap,
|
|
economicModel,
|
|
benchmarkData
|
|
};
|
|
}
|
|
|
|
/**
|
|
* PASO 2: Calcular métricas base por skill
|
|
*/
|
|
interface SkillMetrics {
|
|
skill: string;
|
|
volume: number;
|
|
aht_mean: number;
|
|
aht_std: number;
|
|
cv_aht: number;
|
|
transfer_rate: number;
|
|
total_cost: number;
|
|
hold_time_mean: number;
|
|
cv_talk_time: number;
|
|
}
|
|
|
|
function calculateSkillMetrics(interactions: RawInteraction[], costPerHour: number): SkillMetrics[] {
|
|
// Agrupar por skill
|
|
const skillGroups = new Map<string, RawInteraction[]>();
|
|
|
|
interactions.forEach(i => {
|
|
if (!skillGroups.has(i.queue_skill)) {
|
|
skillGroups.set(i.queue_skill, []);
|
|
}
|
|
skillGroups.get(i.queue_skill)!.push(i);
|
|
});
|
|
|
|
// Calcular métricas para cada skill
|
|
const metrics: SkillMetrics[] = [];
|
|
|
|
skillGroups.forEach((group, skill) => {
|
|
const volume = group.length;
|
|
if (volume === 0) return; // Evitar división por cero
|
|
|
|
// AHT = duration_talk + hold_time + wrap_up_time
|
|
const ahts = group.map(i => i.duration_talk + i.hold_time + i.wrap_up_time);
|
|
const aht_mean = ahts.reduce((sum, v) => sum + v, 0) / volume;
|
|
const aht_variance = ahts.reduce((sum, v) => sum + Math.pow(v - aht_mean, 2), 0) / volume;
|
|
const aht_std = Math.sqrt(aht_variance);
|
|
const cv_aht = aht_mean > 0 ? aht_std / aht_mean : 0;
|
|
|
|
// Talk time CV
|
|
const talkTimes = group.map(i => i.duration_talk);
|
|
const talk_mean = talkTimes.reduce((sum, v) => sum + v, 0) / volume;
|
|
const talk_std = Math.sqrt(talkTimes.reduce((sum, v) => sum + Math.pow(v - talk_mean, 2), 0) / volume);
|
|
const cv_talk_time = talk_mean > 0 ? talk_std / talk_mean : 0;
|
|
|
|
// Transfer rate
|
|
const transfers = group.filter(i => i.transfer_flag).length;
|
|
const transfer_rate = (transfers / volume) * 100;
|
|
|
|
// Hold time promedio
|
|
const hold_time_mean = group.reduce((sum, i) => sum + i.hold_time, 0) / volume;
|
|
|
|
// Coste total (AHT en horas * coste por hora * volumen)
|
|
const total_cost = (aht_mean / 3600) * costPerHour * volume;
|
|
|
|
metrics.push({
|
|
skill,
|
|
volume,
|
|
aht_mean,
|
|
aht_std,
|
|
cv_aht,
|
|
transfer_rate,
|
|
total_cost,
|
|
hold_time_mean,
|
|
cv_talk_time
|
|
});
|
|
});
|
|
|
|
return metrics.sort((a, b) => b.volume - a.volume); // Ordenar por volumen descendente
|
|
}
|
|
|
|
/**
|
|
* PASO 3: Transformar métricas a dimensiones (0-10)
|
|
*/
|
|
function generateHeatmapFromMetrics(
|
|
metrics: SkillMetrics[],
|
|
avgCsat: number,
|
|
segmentMapping?: { high_value_queues: string[]; medium_value_queues: string[]; low_value_queues: string[] }
|
|
): HeatmapDataPoint[] {
|
|
console.log('🔍 generateHeatmapFromMetrics called with:', {
|
|
metricsLength: metrics.length,
|
|
firstMetric: metrics[0],
|
|
avgCsat,
|
|
hasSegmentMapping: !!segmentMapping
|
|
});
|
|
|
|
const result = metrics.map(m => {
|
|
// Dimensión 1: Predictibilidad (CV AHT)
|
|
const predictability = Math.max(0, Math.min(10, 10 - ((m.cv_aht - 0.3) / 1.2 * 10)));
|
|
|
|
// Dimensión 2: Complejidad Inversa (Transfer Rate)
|
|
const complexity_inverse = Math.max(0, Math.min(10, 10 - ((m.transfer_rate / 100 - 0.05) / 0.25 * 10)));
|
|
|
|
// Dimensión 3: Repetitividad (Volumen)
|
|
let repetitiveness = 0;
|
|
if (m.volume >= 5000) {
|
|
repetitiveness = 10;
|
|
} else if (m.volume <= 100) {
|
|
repetitiveness = 0;
|
|
} else {
|
|
// Interpolación lineal entre 100 y 5000
|
|
repetitiveness = ((m.volume - 100) / (5000 - 100)) * 10;
|
|
}
|
|
|
|
// Agentic Readiness Score (promedio ponderado)
|
|
const agentic_readiness = (
|
|
predictability * 0.40 +
|
|
complexity_inverse * 0.35 +
|
|
repetitiveness * 0.25
|
|
);
|
|
|
|
// Categoría
|
|
let category: 'automate' | 'assist' | 'optimize';
|
|
if (agentic_readiness >= 8.0) {
|
|
category = 'automate';
|
|
} else if (agentic_readiness >= 5.0) {
|
|
category = 'assist';
|
|
} else {
|
|
category = 'optimize';
|
|
}
|
|
|
|
// Segmentación
|
|
const segment = segmentMapping
|
|
? classifyQueue(m.skill, segmentMapping.high_value_queues, segmentMapping.medium_value_queues, segmentMapping.low_value_queues)
|
|
: 'medium' as CustomerSegment;
|
|
|
|
// Scores de performance (normalizados 0-100)
|
|
const fcr_score = Math.round(100 - m.transfer_rate);
|
|
const aht_score = Math.round(Math.max(0, Math.min(100, 100 - ((m.aht_mean - 240) / 310) * 100)));
|
|
const csat_score = avgCsat;
|
|
const hold_time_score = Math.round(Math.max(0, Math.min(100, 100 - (m.hold_time_mean / 60) * 10)));
|
|
const transfer_rate_score = Math.round(100 - m.transfer_rate);
|
|
|
|
return {
|
|
skill: m.skill,
|
|
volume: m.volume,
|
|
aht_seconds: Math.round(m.aht_mean),
|
|
metrics: {
|
|
fcr: fcr_score,
|
|
aht: aht_score,
|
|
csat: csat_score,
|
|
hold_time: hold_time_score,
|
|
transfer_rate: transfer_rate_score
|
|
},
|
|
automation_readiness: Math.round(agentic_readiness * 10),
|
|
variability: {
|
|
cv_aht: Math.round(m.cv_aht * 100),
|
|
cv_talk_time: Math.round(m.cv_talk_time * 100),
|
|
cv_hold_time: Math.round(m.cv_talk_time * 80), // Aproximación
|
|
transfer_rate: Math.round(m.transfer_rate)
|
|
},
|
|
dimensions: {
|
|
predictability: Math.round(predictability * 10) / 10,
|
|
complexity_inverse: Math.round(complexity_inverse * 10) / 10,
|
|
repetitiveness: Math.round(repetitiveness * 10) / 10
|
|
},
|
|
agentic_readiness: Math.round(agentic_readiness * 10) / 10,
|
|
category,
|
|
segment
|
|
};
|
|
});
|
|
|
|
console.log('📊 Heatmap data generated from real data:', {
|
|
length: result.length,
|
|
firstItem: result[0],
|
|
objectKeys: result[0] ? Object.keys(result[0]) : [],
|
|
hasMetricsObject: result[0] && typeof result[0].metrics !== 'undefined',
|
|
metricsKeys: result[0] && result[0].metrics ? Object.keys(result[0].metrics) : [],
|
|
firstMetrics: result[0] && result[0].metrics ? result[0].metrics : null,
|
|
automation_readiness: result[0] ? result[0].automation_readiness : null
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calcular Health Score global
|
|
*/
|
|
function calculateHealthScore(heatmapData: HeatmapDataPoint[]): number {
|
|
if (heatmapData.length === 0) return 50;
|
|
|
|
const avgFCR = heatmapData.reduce((sum, d) => sum + (d.metrics?.fcr || 0), 0) / heatmapData.length;
|
|
const avgAHT = heatmapData.reduce((sum, d) => sum + (d.metrics?.aht || 0), 0) / heatmapData.length;
|
|
const avgCSAT = heatmapData.reduce((sum, d) => sum + (d.metrics?.csat || 0), 0) / heatmapData.length;
|
|
const avgVariability = heatmapData.reduce((sum, d) => sum + (100 - (d.variability?.cv_aht || 0)), 0) / heatmapData.length;
|
|
|
|
return Math.round((avgFCR + avgAHT + avgCSAT + avgVariability) / 4);
|
|
}
|
|
|
|
/**
|
|
* Generar dimensiones desde datos reales
|
|
*/
|
|
function generateDimensionsFromRealData(
|
|
interactions: RawInteraction[],
|
|
metrics: SkillMetrics[],
|
|
avgCsat: number,
|
|
avgAHT: number
|
|
): DimensionAnalysis[] {
|
|
const totalVolume = interactions.length;
|
|
const avgCV = metrics.reduce((sum, m) => sum + m.cv_aht, 0) / metrics.length;
|
|
const avgTransferRate = metrics.reduce((sum, m) => sum + m.transfer_rate, 0) / metrics.length;
|
|
|
|
return [
|
|
{
|
|
id: 'volumetry_distribution',
|
|
name: 'volumetry_distribution',
|
|
title: 'Análisis de la Demanda',
|
|
score: Math.min(100, Math.round((totalVolume / 200))), // Score basado en volumen
|
|
percentile: 65,
|
|
summary: `Se procesaron ${totalVolume.toLocaleString('es-ES')} interacciones distribuidas en ${metrics.length} skills diferentes.`,
|
|
kpi: { label: 'Volumen Total', value: totalVolume.toLocaleString('es-ES') },
|
|
icon: BarChartHorizontal
|
|
},
|
|
{
|
|
id: 'performance',
|
|
name: 'performance',
|
|
title: 'Rendimiento Operativo',
|
|
score: Math.round(100 - (avgCV * 100)),
|
|
percentile: 70,
|
|
summary: avgCV < 0.4
|
|
? 'El AHT muestra baja variabilidad, indicando procesos estandarizados.'
|
|
: 'La variabilidad del AHT es alta, sugiriendo inconsistencia en procesos.',
|
|
kpi: { label: 'AHT Promedio', value: `${avgAHT}s` },
|
|
icon: Zap
|
|
},
|
|
{
|
|
id: 'satisfaction',
|
|
name: 'satisfaction',
|
|
title: 'Voz del Cliente',
|
|
score: avgCsat,
|
|
percentile: 60,
|
|
summary: `CSAT promedio de ${(avgCsat / 20).toFixed(1)}/5.`,
|
|
kpi: { label: 'CSAT', value: `${(avgCsat / 20).toFixed(1)}/5` },
|
|
icon: Smile
|
|
},
|
|
{
|
|
id: 'economy',
|
|
name: 'economy',
|
|
title: 'Rentabilidad del Servicio',
|
|
score: Math.round(100 - avgTransferRate),
|
|
percentile: 55,
|
|
summary: `Tasa de transferencia del ${avgTransferRate.toFixed(1)}%.`,
|
|
kpi: { label: 'Transfer Rate', value: `${avgTransferRate.toFixed(1)}%` },
|
|
icon: DollarSign
|
|
},
|
|
{
|
|
id: 'efficiency',
|
|
name: 'efficiency',
|
|
title: 'Resolución y Calidad',
|
|
score: Math.round(100 - avgTransferRate),
|
|
percentile: 68,
|
|
summary: `FCR estimado del ${(100 - avgTransferRate).toFixed(1)}%.`,
|
|
kpi: { label: 'FCR', value: `${(100 - avgTransferRate).toFixed(1)}%` },
|
|
icon: Target
|
|
},
|
|
{
|
|
id: 'benchmark',
|
|
name: 'benchmark',
|
|
title: 'Contexto Competitivo',
|
|
score: 75,
|
|
percentile: 65,
|
|
summary: 'Métricas alineadas con benchmarks de la industria.',
|
|
kpi: { label: 'Benchmark', value: 'P65' },
|
|
icon: Globe
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular Agentic Readiness desde datos reales
|
|
*/
|
|
function calculateAgenticReadinessFromRealData(metrics: SkillMetrics[]): AgenticReadinessResult {
|
|
const totalVolume = metrics.reduce((sum, m) => sum + m.volume, 0);
|
|
const avgCV = metrics.reduce((sum, m) => sum + m.cv_aht, 0) / metrics.length;
|
|
const avgTransferRate = metrics.reduce((sum, m) => sum + m.transfer_rate, 0) / metrics.length;
|
|
|
|
// Predictibilidad
|
|
const predictability = Math.max(0, Math.min(10, 10 - ((avgCV - 0.3) / 1.2 * 10)));
|
|
|
|
// Complejidad Inversa
|
|
const complexity_inverse = Math.max(0, Math.min(10, 10 - (avgTransferRate / 10)));
|
|
|
|
// ROI (simplificado)
|
|
const roi = Math.min(10, totalVolume / 1000);
|
|
|
|
// Repetitividad (basada en volumen)
|
|
const repetitiveness = Math.min(10, totalVolume / 500);
|
|
|
|
// Score final
|
|
const score = Math.round((predictability * 0.4 + complexity_inverse * 0.35 + repetitiveness * 0.25) * 10) / 10;
|
|
|
|
// Tier basado en score
|
|
let tier: TierKey;
|
|
if (score >= 8) tier = 'gold';
|
|
else if (score >= 5) tier = 'silver';
|
|
else tier = 'bronze';
|
|
|
|
// Sub-factors
|
|
const sub_factors: SubFactor[] = [
|
|
{
|
|
name: 'predictibilidad',
|
|
displayName: 'Predictibilidad',
|
|
score: Math.round(predictability * 10) / 10,
|
|
weight: 0.4,
|
|
description: `CV AHT promedio: ${Math.round(avgCV * 100)}%`
|
|
},
|
|
{
|
|
name: 'complejidad_inversa',
|
|
displayName: 'Complejidad Inversa',
|
|
score: Math.round(complexity_inverse * 10) / 10,
|
|
weight: 0.35,
|
|
description: `Tasa de transferencia promedio: ${Math.round(avgTransferRate)}%`
|
|
},
|
|
{
|
|
name: 'repetitividad',
|
|
displayName: 'Repetitividad',
|
|
score: Math.round(repetitiveness * 10) / 10,
|
|
weight: 0.25,
|
|
description: `Volumen total: ${totalVolume.toLocaleString('es-ES')} interacciones`
|
|
}
|
|
];
|
|
|
|
// Interpretation
|
|
let interpretation: string;
|
|
if (score >= 8) {
|
|
interpretation = 'Excelente candidato para automatización. Alta predictibilidad, baja complejidad y volumen significativo.';
|
|
} else if (score >= 5) {
|
|
interpretation = 'Buen candidato para asistencia con IA. Considere implementar copilots o asistentes virtuales.';
|
|
} else {
|
|
interpretation = 'Requiere optimización previa. Enfóquese en estandarizar procesos y reducir variabilidad antes de automatizar.';
|
|
}
|
|
|
|
return {
|
|
score,
|
|
sub_factors,
|
|
tier,
|
|
confidence: totalVolume > 1000 ? 'high' as const : totalVolume > 500 ? 'medium' as const : 'low' as const,
|
|
interpretation
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generar findings desde datos reales
|
|
*/
|
|
function generateFindingsFromRealData(metrics: SkillMetrics[], interactions: RawInteraction[]): Finding[] {
|
|
const findings: Finding[] = [];
|
|
|
|
// Finding 1: Variabilidad
|
|
const highVariabilitySkills = metrics.filter(m => m.cv_aht > 0.45);
|
|
if (highVariabilitySkills.length > 0) {
|
|
findings.push({
|
|
type: 'warning',
|
|
title: 'Alta Variabilidad de AHT',
|
|
description: `${highVariabilitySkills.length} skills muestran CV > 45%, sugiriendo procesos poco estandarizados.`
|
|
});
|
|
}
|
|
|
|
// Finding 2: Transferencias
|
|
const highTransferSkills = metrics.filter(m => m.transfer_rate > 20);
|
|
if (highTransferSkills.length > 0) {
|
|
findings.push({
|
|
type: 'warning',
|
|
title: 'Alta Tasa de Transferencia',
|
|
description: `${highTransferSkills.length} skills con transfer rate > 20%.`
|
|
});
|
|
}
|
|
|
|
// Finding 3: Volumen
|
|
const topSkill = metrics[0];
|
|
findings.push({
|
|
type: 'info',
|
|
title: 'Skill de Mayor Volumen',
|
|
description: `"${topSkill.skill}" representa el ${Math.round(topSkill.volume / interactions.length * 100)}% del volumen total.`
|
|
});
|
|
|
|
return findings;
|
|
}
|
|
|
|
/**
|
|
* Generar recomendaciones desde datos reales
|
|
*/
|
|
function generateRecommendationsFromRealData(metrics: SkillMetrics[]): Recommendation[] {
|
|
const recommendations: Recommendation[] = [];
|
|
|
|
const highVariabilitySkills = metrics.filter(m => m.cv_aht > 0.45);
|
|
if (highVariabilitySkills.length > 0) {
|
|
recommendations.push({
|
|
priority: 'high',
|
|
title: 'Estandarizar Procesos',
|
|
description: `Crear guías y scripts para los ${highVariabilitySkills.length} skills con alta variabilidad.`,
|
|
impact: 'Reducción del 20-30% en AHT'
|
|
});
|
|
}
|
|
|
|
const highVolumeSkills = metrics.filter(m => m.volume > 500);
|
|
if (highVolumeSkills.length > 0) {
|
|
recommendations.push({
|
|
priority: 'high',
|
|
title: 'Automatizar Skills de Alto Volumen',
|
|
description: `Implementar bots para los ${highVolumeSkills.length} skills con > 500 interacciones.`,
|
|
impact: 'Ahorro estimado del 40-60%'
|
|
});
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
/**
|
|
* Generar opportunities desde datos reales
|
|
*/
|
|
function generateOpportunitiesFromRealData(metrics: SkillMetrics[], costPerHour: number): Opportunity[] {
|
|
return metrics.slice(0, 10).map((m, index) => {
|
|
const potentialSavings = m.total_cost * 0.4; // 40% de ahorro potencial
|
|
|
|
return {
|
|
id: `opp-${index + 1}`,
|
|
skill: m.skill,
|
|
currentVolume: m.volume,
|
|
currentAHT: Math.round(m.aht_mean),
|
|
currentCost: Math.round(m.total_cost),
|
|
potentialSavings: Math.round(potentialSavings),
|
|
automationPotential: m.cv_aht < 0.3 && m.transfer_rate < 15 ? 'high' : m.cv_aht < 0.5 ? 'medium' : 'low',
|
|
priority: index < 3 ? 'high' : index < 7 ? 'medium' : 'low'
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generar roadmap desde opportunities
|
|
*/
|
|
function generateRoadmapFromRealData(opportunities: Opportunity[]): RoadmapInitiative[] {
|
|
const highPriority = opportunities.filter(o => o.priority === 'high');
|
|
|
|
return highPriority.slice(0, 5).map((opp, index) => ({
|
|
id: `init-${index + 1}`,
|
|
title: `Automatizar ${opp.skill}`,
|
|
description: `Implementar bot para reducir AHT y coste`,
|
|
phase: index < 2 ? RoadmapPhase.QUICK_WINS : RoadmapPhase.STRATEGIC,
|
|
effort: opp.currentVolume > 1000 ? 'high' : 'medium',
|
|
impact: opp.potentialSavings > 10000 ? 'high' : 'medium',
|
|
timeline: `${index * 2 + 1}-${index * 2 + 3} meses`
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Generar economic model desde datos reales
|
|
*/
|
|
function generateEconomicModelFromRealData(metrics: SkillMetrics[], costPerHour: number): EconomicModelData {
|
|
const totalCost = metrics.reduce((sum, m) => sum + m.total_cost, 0);
|
|
const annualSavings = Math.round(totalCost * 0.35);
|
|
const initialInvestment = Math.round(totalCost * 0.1);
|
|
const paybackMonths = Math.ceil((initialInvestment / annualSavings) * 12);
|
|
const roi3yr = (((annualSavings * 3) - initialInvestment) / initialInvestment) * 100;
|
|
|
|
// NPV con tasa de descuento 10%
|
|
const discountRate = 0.10;
|
|
const npv = -initialInvestment +
|
|
(annualSavings / (1 + discountRate)) +
|
|
(annualSavings / Math.pow(1 + discountRate, 2)) +
|
|
(annualSavings / Math.pow(1 + discountRate, 3));
|
|
|
|
const savingsBreakdown = [
|
|
{ category: 'Automatización de tareas', amount: annualSavings * 0.45, percentage: 45 },
|
|
{ category: 'Eficiencia operativa', amount: annualSavings * 0.30, percentage: 30 },
|
|
{ category: 'Mejora FCR', amount: annualSavings * 0.15, percentage: 15 },
|
|
{ category: 'Reducción attrition', amount: annualSavings * 0.075, percentage: 7.5 },
|
|
{ category: 'Otros', amount: annualSavings * 0.025, percentage: 2.5 },
|
|
];
|
|
|
|
const costBreakdown = [
|
|
{ category: 'Software y licencias', amount: initialInvestment * 0.43, percentage: 43 },
|
|
{ category: 'Implementación', amount: initialInvestment * 0.29, percentage: 29 },
|
|
{ category: 'Training y change mgmt', amount: initialInvestment * 0.18, percentage: 18 },
|
|
{ category: 'Contingencia', amount: initialInvestment * 0.10, percentage: 10 },
|
|
];
|
|
|
|
return {
|
|
currentAnnualCost: Math.round(totalCost),
|
|
futureAnnualCost: Math.round(totalCost - annualSavings),
|
|
annualSavings,
|
|
initialInvestment,
|
|
paybackMonths,
|
|
roi3yr: parseFloat(roi3yr.toFixed(1)),
|
|
npv: Math.round(npv),
|
|
savingsBreakdown,
|
|
costBreakdown
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generar benchmark desde datos reales
|
|
*/
|
|
function generateBenchmarkFromRealData(metrics: SkillMetrics[]): BenchmarkDataPoint[] {
|
|
const avgAHT = metrics.reduce((sum, m) => sum + m.aht_mean, 0) / (metrics.length || 1);
|
|
const avgFCR = 100 - (metrics.reduce((sum, m) => sum + m.transfer_rate, 0) / (metrics.length || 1));
|
|
const avgCSAT = 4.3; // Default CSAT
|
|
const avgCPI = 3.5; // Default CPI
|
|
|
|
return [
|
|
{
|
|
kpi: 'AHT Promedio',
|
|
userValue: Math.round(avgAHT),
|
|
userDisplay: `${Math.round(avgAHT)}s`,
|
|
industryValue: 420,
|
|
industryDisplay: `420s`,
|
|
percentile: Math.max(10, Math.min(90, Math.round(100 - (avgAHT / 420) * 100))),
|
|
p25: 380,
|
|
p50: 420,
|
|
p75: 460,
|
|
p90: 510
|
|
},
|
|
{
|
|
kpi: 'Tasa FCR',
|
|
userValue: avgFCR / 100,
|
|
userDisplay: `${Math.round(avgFCR)}%`,
|
|
industryValue: 0.72,
|
|
industryDisplay: `72%`,
|
|
percentile: Math.max(10, Math.min(90, Math.round((avgFCR / 100) * 100))),
|
|
p25: 0.65,
|
|
p50: 0.72,
|
|
p75: 0.82,
|
|
p90: 0.88
|
|
},
|
|
{
|
|
kpi: 'CSAT',
|
|
userValue: avgCSAT,
|
|
userDisplay: `${avgCSAT}/5`,
|
|
industryValue: 4.3,
|
|
industryDisplay: `4.3/5`,
|
|
percentile: 65,
|
|
p25: 3.8,
|
|
p50: 4.3,
|
|
p75: 4.6,
|
|
p90: 4.8
|
|
},
|
|
{
|
|
kpi: 'Coste por Interacción',
|
|
userValue: avgCPI,
|
|
userDisplay: `€${avgCPI.toFixed(2)}`,
|
|
industryValue: 3.5,
|
|
industryDisplay: `€3.50`,
|
|
percentile: 55,
|
|
p25: 2.8,
|
|
p50: 3.5,
|
|
p75: 4.2,
|
|
p90: 4.8
|
|
}
|
|
];
|
|
}
|