// analysisGenerator.ts - v2.0 with 6 dimensions import type { AnalysisData, Kpi, DimensionAnalysis, HeatmapDataPoint, Opportunity, RoadmapInitiative, EconomicModelData, BenchmarkDataPoint, Finding, Recommendation, TierKey, CustomerSegment, RawInteraction, DrilldownDataPoint, AgenticTier } from '../types'; import { generateAnalysisFromRealData, calculateDrilldownMetrics, generateOpportunitiesFromDrilldown, generateRoadmapFromDrilldown, calculateSkillMetrics, generateHeatmapFromMetrics, clasificarTierSimple } from './realDataAnalysis'; import { RoadmapPhase } from '../types'; import { BarChartHorizontal, Zap, Target, Brain, Bot } from 'lucide-react'; import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2'; import { callAnalysisApiRaw } from './apiClient'; import { mapBackendResultsToAnalysisData, buildHeatmapFromBackend, } from './backendMapper'; import { saveFileToServerCache, saveDrilldownToServerCache, getCachedDrilldown, downloadCachedFile } from './serverCache'; const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; const randomFloat = (min: number, max: number, decimals: number) => parseFloat((Math.random() * (max - min) + min).toFixed(decimals)); const randomFromList = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; // Normal distribution (Box-Muller transform) const normalRandom = (mean: number, std: number): number => { const u1 = Math.random(); const u2 = Math.random(); const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); return mean + std * z0; }; const getScoreColor = (score: number): 'green' | 'yellow' | 'red' => { if (score >= 80) return 'green'; if (score >= 60) return 'yellow'; return 'red'; }; // v3.0: 5 DIMENSIONES VIABLES const DIMENSIONS_CONTENT = { volumetry_distribution: { icon: BarChartHorizontal, titles: ["Volumetry & Distribution", "Demand Analysis"], summaries: { good: ["Interaction volume aligns with forecasts, enabling precise staff planning.", "Hourly distribution is uniform with predictable peaks. Balanced Pareto concentration."], medium: ["There are unforeseen demand peaks that cause service level drops.", "High concentration in few queues (>80% in 20% of queues), bottleneck risk."], bad: ["Chronic mismatch between forecast and actual volume, resulting in cost overruns or poor service.", "Very irregular hourly distribution with multiple unpredictable peaks."] }, kpis: [ { label: "Monthly Volume", value: `${randomInt(5000, 25000).toLocaleString('es-ES')}` }, { label: "% After Hours", value: `${randomInt(15, 45)}%` }, ], }, operational_efficiency: { icon: Zap, titles: ["Operational Efficiency", "Time Optimization"], summaries: { good: ["P90/P50 ratio is low (<1.5), indicating consistent times and standardized processes.", "Wait, hold and ACW times well controlled, maximizing productivity."], medium: ["P90/P50 ratio is moderate (1.5-2.0), outlier cases exist that affect efficiency.", "Hold time is slightly high, suggesting improvements in information access."], bad: ["High P90/P50 ratio (>2.0), indicating high variability in handling times.", "Extended ACW and hold times indicate inefficient manual processes."] }, kpis: [ { label: "AHT P50", value: `${randomInt(280, 450)}s` }, { label: "Ratio P90/P50", value: `${randomFloat(1.2, 2.5, 2)}` }, ], }, effectiveness_resolution: { icon: Target, titles: ["Effectiveness & Resolution", "Service Quality"], summaries: { good: ["FCR proxy >85%, minimal contact repetition within 7 days.", "Low transfer rate (<10%) and problematic calls (<5%)."], medium: ["FCR proxy 70-85%, opportunity to reduce re-contacts.", "Moderate transfer rate (10-20%), concentrated in certain queues."], bad: ["FCR proxy <70%, high volume of re-contacts within 7 days.", "High rate of problematic calls (>15%) and excessive transfers."] }, kpis: [ { label: "FCR Proxy 7d", value: `${randomInt(65, 92)}%` }, { label: "Transfer Rate", value: `${randomInt(5, 25)}%` }, ], }, complexity_predictability: { icon: Brain, titles: ["Complexity & Predictability", "Variability Analysis"], summaries: { good: ["Low AHT variability (P90/P50 ratio <1.5), highly predictable process.", "Controlled classification diversity, low % of calls with multiple holds."], medium: ["Moderate AHT variability, some outlier cases affect predictability.", "% calls with multiple holds high (15-30%), indicating complexity."], bad: ["High AHT variability (ratio >2.0), unpredictable process difficult to automate.", "High classification diversity and % transfers, indicating high complexity."] }, kpis: [ { label: "Ratio P90/P50", value: `${randomFloat(1.2, 2.5, 2)}` }, { label: "% Transferencias", value: `${randomInt(5, 30)}%` }, ], }, agentic_readiness: { icon: Bot, titles: ["Agentic Readiness", "Automation Potential"], summaries: { good: ["Score 8-10: Excellent candidate for full automation with AI agents.", "High volume, low variability, few transfers. Repetitive and predictable process."], medium: ["Score 5-7: Candidate for AI assistance (copilot) or partial automation.", "Moderate volume with some complexities requiring human supervision."], bad: ["Score 0-4: Requires prior optimization before automating.", "High complexity, low repeatability or excessive variability."] }, kpis: [ { label: "Overall Score", value: `${randomFloat(3.0, 9.5, 1)}/10` }, { label: "Category", value: randomFromList(['Automate', 'Assist', 'Optimize']) }, ], }, }; // Generic findings - specific ones are generated in realDataAnalysis.ts from calculated data const KEY_FINDINGS: Finding[] = [ { text: "The P90/P50 ratio of AHT is high (>2.0), indicating high variability in handling times.", dimensionId: 'operational_efficiency', type: 'warning', title: 'High Variability in Times', description: 'Poorly standardized processes generate unpredictable times and affect planning.', impact: 'high' }, { text: "High transfer rate indicates opportunity for improvement in routing or training.", dimensionId: 'effectiveness_resolution', type: 'warning', title: 'High Transfers', description: 'Frequent transfers affect customer experience and operational efficiency.', impact: 'high' }, { text: "Volume concentration in specific time slots generates demand peaks.", dimensionId: 'volumetry_distribution', type: 'info', title: 'Demand Concentration', description: 'Review capacity in high-volume time slots to optimize service level.', impact: 'medium' }, { text: "Significant percentage of interactions outside standard business hours (8-19h).", dimensionId: 'volumetry_distribution', type: 'info', title: 'After-Hours Demand', description: 'Evaluate extended coverage or self-service channels for after-hours demand.', impact: 'medium' }, { text: "Automation opportunities identified in high-volume repetitive queries.", dimensionId: 'agentic_readiness', type: 'info', title: 'Automation Opportunity', description: 'Skills with high repeatability and low complexity are ideal candidates for AI agents.', impact: 'high' }, ]; const RECOMMENDATIONS: Recommendation[] = [ { text: "Standardize processes in queues with high P90/P50 ratio to reduce variability.", dimensionId: 'operational_efficiency', priority: 'high', title: 'Process Standardization', description: 'Implement scripts and step-by-step guides to reduce variability in handling times.', impact: 'P90/P50 ratio reduction: 20-30%, Improved predictability', timeline: '3-4 weeks' }, { text: "Develop an order status bot for WhatsApp to deflect 30% of queries.", dimensionId: 'agentic_readiness', priority: 'high', title: 'Automated Order Tracking Bot', description: 'Implement ChatBot on WhatsApp for queries with high Agentic Score (>8).', impact: 'Volume reduction: 20-30%, Annual savings: €40-60K', timeline: '1-2 meses' }, { text: "Review workforce planning (WFM) for Mondays, adding flexible resources.", dimensionId: 'volumetry_distribution', priority: 'high', title: 'Workforce Adjustment (WFM)', description: 'Reposition agents and add part-time resources for Mondays 8-11h.', impact: 'SL improvement: +15-20%, Additional cost: €5-8K/month', timeline: '1 mes' }, { text: "Create a more robust Knowledge Base to reduce hold time and improve FCR.", dimensionId: 'effectiveness_resolution', priority: 'high', title: 'Information Access Improvement', description: 'Develop a centralized KB to reduce searches and improve first contact resolution.', impact: 'Hold time reduction: 15-25%, FCR improvement: 5-10%', timeline: '6-8 weeks' }, { text: "Implement 24/7 coverage with virtual agents for 28% of after-hours interactions.", dimensionId: 'volumetry_distribution', priority: 'medium', title: '24/7 Coverage with AI', description: 'Deploy virtual agents to handle nighttime and weekend interactions.', impact: 'Demand capture: 20-25%, Incremental cost: €15-20K/month', timeline: '2-3 meses' }, { text: "Simplify classifications and reduce complexity in problematic queues.", dimensionId: 'complexity_predictability', priority: 'medium', title: 'Complexity Reduction', description: 'Consolidate classifications and simplify flows to improve predictability.', impact: 'Complexity reduction: 20-30%, Improved Agentic Score', timeline: '4-6 weeks' }, ]; // === RECOMMENDATIONS BASED ON REAL DATA === const MAX_RECOMMENDATIONS = 4; const generateRecommendationsFromData = ( analysis: AnalysisData ): Recommendation[] => { const dimensions = analysis.dimensions || []; const dimScoreMap = new Map(); dimensions.forEach((d) => { if (d.id && typeof d.score === 'number') { dimScoreMap.set(d.id, d.score); } }); const overallScore = typeof analysis.overallHealthScore === 'number' ? analysis.overallHealthScore : 70; const econ = analysis.economicModel; const annualSavings = econ?.annualSavings ?? 0; const currentCost = econ?.currentAnnualCost ?? 0; // Relevance by recommendation const scoredTemplates = RECOMMENDATIONS.map((tpl, index) => { const dimId = tpl.dimensionId || 'overall'; const dimScore = dimScoreMap.get(dimId) ?? overallScore; let relevance = 0; // 1) Weak dimensions => more relevance if (dimScore < 60) relevance += 3; else if (dimScore < 75) relevance += 2; else if (dimScore < 85) relevance += 1; // 2) Priority declared in the template if (tpl.priority === 'high') relevance += 2; else if (tpl.priority === 'medium') relevance += 1; // 3) Reinforcement based on economic potential if ( annualSavings > 0 && currentCost > 0 && annualSavings / currentCost > 0.15 && dimId === 'economy' ) { relevance += 2; } // 4) Slight penalty if dimension is already very good (>85) if (dimScore > 85) relevance -= 1; return { tpl, relevance, index, // por si queremos desempatar }; }); // Filter out those that contribute nothing (relevance <= 0) let filtered = scoredTemplates.filter((s) => s.relevance > 0); // Si ninguna pasa el filtro (por ejemplo, todo muy bien), // we keep at least 2-3 of the highest priority ones if (filtered.length === 0) { filtered = scoredTemplates .slice() .sort((a, b) => { const prioWeight = (p?: 'high' | 'medium' | 'low') => { if (p === 'high') return 3; if (p === 'medium') return 2; return 1; }; return ( prioWeight(b.tpl.priority) - prioWeight(a.tpl.priority) ); }) .slice(0, MAX_RECOMMENDATIONS); } else { // Ordenamos por relevancia (desc), y en empate, por orden original filtered.sort((a, b) => { if (b.relevance !== a.relevance) { return b.relevance - a.relevance; } return a.index - b.index; }); } const selected = filtered.slice(0, MAX_RECOMMENDATIONS).map((s) => s.tpl); // Mapear a tipo Recommendation completo return selected.map((rec, i): Recommendation => ({ priority: rec.priority || (i === 0 ? ('high' as const) : ('medium' as const)), title: rec.title || 'Recommendation', description: rec.description || rec.text, impact: rec.impact || 'Estimated 10-20% improvement in key KPIs.', timeline: rec.timeline || '4-8 weeks', // required fields: text: rec.text || rec.description || 'Priority recommendation based on data analysis.', dimensionId: rec.dimensionId || 'overall', })); }; // === FINDINGS BASED ON REAL DATA === const MAX_FINDINGS = 5; const generateFindingsFromData = ( analysis: AnalysisData ): Finding[] => { const dimensions = analysis.dimensions || []; const dimScoreMap = new Map(); dimensions.forEach((d) => { if (d.id && typeof d.score === 'number') { dimScoreMap.set(d.id, d.score); } }); const overallScore = typeof analysis.overallHealthScore === 'number' ? analysis.overallHealthScore : 70; // We look at volumetry to reinforce some findings const volumetryDim = dimensions.find( (d) => d.id === 'volumetry_distribution' ); const offHoursPct = volumetryDim?.distribution_data?.off_hours_pct ?? 0; // Relevancia por finding const scoredTemplates = KEY_FINDINGS.map((tpl, index) => { const dimId = tpl.dimensionId || 'overall'; const dimScore = dimScoreMap.get(dimId) ?? overallScore; let relevance = 0; // 1) Weak dimensions => more relevance if (dimScore < 60) relevance += 3; else if (dimScore < 75) relevance += 2; else if (dimScore < 85) relevance += 1; // 2) Type of finding (critical > warning > info) if (tpl.type === 'critical') relevance += 3; else if (tpl.type === 'warning') relevance += 2; else relevance += 1; // 3) Impacto (high > medium > low) if (tpl.impact === 'high') relevance += 2; else if (tpl.impact === 'medium') relevance += 1; // 4) Reinforcement in volumetry if there is high after-hours demand if ( offHoursPct > 0.25 && tpl.dimensionId === 'volumetry_distribution' ) { relevance += 2; if ( tpl.title?.toLowerCase().includes('after hours') || tpl.text ?.toLowerCase() .includes('outside business hours') ) { relevance += 1; } } return { tpl, relevance, index, }; }); // Filtramos los que no aportan nada (relevance <= 0) let filtered = scoredTemplates.filter((s) => s.relevance > 0); // Si nada pasa el filtro, cogemos al menos algunos por prioridad/tipo if (filtered.length === 0) { filtered = scoredTemplates .slice() .sort((a, b) => { const typeWeight = (t?: Finding['type']) => { if (t === 'critical') return 3; if (t === 'warning') return 2; return 1; }; const impactWeight = (imp?: string) => { if (imp === 'high') return 3; if (imp === 'medium') return 2; return 1; }; const scoreA = typeWeight(a.tpl.type) + impactWeight(a.tpl.impact); const scoreB = typeWeight(b.tpl.type) + impactWeight(b.tpl.impact); return scoreB - scoreA; }) .slice(0, MAX_FINDINGS); } else { // Ordenamos por relevancia (desc), y en empate, por orden original filtered.sort((a, b) => { if (b.relevance !== a.relevance) { return b.relevance - a.relevance; } return a.index - b.index; }); } const selected = filtered.slice(0, MAX_FINDINGS).map((s) => s.tpl); // Mapear a tipo Finding completo return selected.map((finding, i): Finding => ({ type: finding.type || (i === 0 ? ('warning' as const) : ('info' as const)), title: finding.title || 'Hallazgo', description: finding.description || finding.text, // required fields: text: finding.text || finding.description || 'Relevant finding based on data.', dimensionId: finding.dimensionId || 'overall', impact: finding.impact, })); }; const generateFindingsFromTemplates = (): Finding[] => { return [ ...new Set( Array.from({ length: 3 }, () => randomFromList(KEY_FINDINGS)) ), ].map((finding, i): Finding => ({ type: finding.type || (i === 0 ? 'warning' : 'info'), title: finding.title || 'Hallazgo', description: finding.description || finding.text, // required fields: text: finding.text || finding.description || 'Hallazgo relevante', dimensionId: finding.dimensionId || 'overall', impact: finding.impact, })); }; const generateRecommendationsFromTemplates = (): Recommendation[] => { return [ ...new Set( Array.from({ length: 3 }, () => randomFromList(RECOMMENDATIONS)) ), ].map((rec, i): Recommendation => ({ priority: rec.priority || (i === 0 ? 'high' : 'medium'), title: rec.title || 'Recommendation', description: rec.description || rec.text, impact: rec.impact || 'Estimated improvement of 20-30%', timeline: rec.timeline || '1-2 weeks', // required fields: text: rec.text || rec.description || 'Priority recommendation', dimensionId: rec.dimensionId || 'overall', })); }; // v2.0: Generate realistic hourly distribution const generateHourlyDistribution = (): number[] => { // Distribution with peaks at 9-11h and 14-17h const distribution = Array(24).fill(0).map((_, hour) => { if (hour >= 9 && hour <= 11) return randomInt(800, 1200); // Morning peak if (hour >= 14 && hour <= 17) return randomInt(700, 1000); // Afternoon peak if (hour >= 8 && hour <= 18) return randomInt(300, 600); // Business hours return randomInt(50, 200); // After hours }); return distribution; }; // v2.0: Calculate % after hours const calculateOffHoursPct = (hourly_distribution: number[]): number => { const total = hourly_distribution.reduce((a, b) => a + b, 0); if (total === 0) return 0; // Avoid division by zero const off_hours = hourly_distribution.slice(0, 8).reduce((a, b) => a + b, 0) + hourly_distribution.slice(19, 24).reduce((a, b) => a + b, 0); return off_hours / total; }; // v2.0: Identificar horas pico const identifyPeakHours = (hourly_distribution: number[]): number[] => { if (!hourly_distribution || hourly_distribution.length === 0) return []; const sorted = [...hourly_distribution].sort((a, b) => b - a); const threshold = sorted[Math.min(2, sorted.length - 1)] || 0; // Top 3 or maximum available return hourly_distribution .map((val, idx) => val >= threshold ? idx : -1) .filter(idx => idx !== -1); }; // v2.1: Generate heatmap with new transformation logic (3 dimensions) const generateHeatmapData = ( costPerHour: number = 20, avgCsat: number = 85, segmentMapping?: { high_value_queues: string[]; medium_value_queues: string[]; low_value_queues: string[] } ): HeatmapDataPoint[] => { const skills = ['Ventas Inbound', 'Soporte Técnico N1', 'Facturación', 'Retención', 'VIP Support', 'Trial Support']; const COST_PER_SECOND = costPerHour / 3600; return skills.map(skill => { const volume = randomInt(800, 5500); // Monthly volume (expanded to cover repeatability range) // Simular raw data: duration_talk, hold_time, wrap_up_time const avg_talk_time = randomInt(240, 450); // segundos const avg_hold_time = randomInt(15, 80); // segundos const avg_wrap_up = randomInt(10, 50); // segundos const aht_mean = avg_talk_time + avg_hold_time + avg_wrap_up; // Average AHT // Simular desviación estándar del AHT (para CV) const aht_std = randomInt(Math.round(aht_mean * 0.15), Math.round(aht_mean * 0.60)); // 15-60% del AHT const cv_aht = aht_std / aht_mean; // Coeficiente de Variación // Transfer rate (for inverse complexity) const transfer_rate = randomInt(5, 35); // % const fcr_approx = 100 - transfer_rate; // FCR aproximado // Period cost (monthly) - with 70% productivity factor const effectiveProductivity = 0.70; const period_cost = Math.round((aht_mean / 3600) * costPerHour * volume / effectiveProductivity); const annual_cost = period_cost; // Renamed for compatibility, but it is monthly cost // CPI = cost per interaction const cpi = volume > 0 ? period_cost / volume : 0; // === NUEVA LÓGICA: 3 DIMENSIONES === // Dimension 1: Predictability (Proxy: AHT CV) // Fórmula: MAX(0, MIN(10, 10 - ((CV - 0.3) / 1.2 * 10))) const predictability_score = Math.max(0, Math.min(10, 10 - ((cv_aht - 0.3) / 1.2 * 10) )); // Dimension 2: Inverse Complexity (Proxy: Transfer Rate) // Fórmula: MAX(0, MIN(10, 10 - ((T - 0.05) / 0.25 * 10))) const complexity_inverse_score = Math.max(0, Math.min(10, 10 - ((transfer_rate / 100 - 0.05) / 0.25 * 10) )); // Dimension 3: Repeatability/Impact (Proxy: Volume) // > 5,000 = 10, < 100 = 0, linear interpolation between 100-5000 let repetitivity_score: number; if (volume >= 5000) { repetitivity_score = 10; } else if (volume <= 100) { repetitivity_score = 0; } else { repetitivity_score = ((volume - 100) / (5000 - 100)) * 10; } // Agentic Readiness Score (Weighted average) // Weights: Predictability 40%, Complexity 35%, Repeatability 25% const agentic_readiness_score = predictability_score * 0.40 + complexity_inverse_score * 0.35 + repetitivity_score * 0.25; // Categoría de readiness let readiness_category: 'automate_now' | 'assist_copilot' | 'optimize_first'; if (agentic_readiness_score >= 8.0) { readiness_category = 'automate_now'; } else if (agentic_readiness_score >= 5.0) { readiness_category = 'assist_copilot'; } else { readiness_category = 'optimize_first'; } const automation_readiness = Math.round(agentic_readiness_score * 10); // Escala 0-100 para compatibilidad // Clasificar segmento si hay mapeo let segment: CustomerSegment | undefined; if (segmentMapping) { const normalizedSkill = skill.toLowerCase(); if (segmentMapping.high_value_queues.some(q => normalizedSkill.includes(q.toLowerCase()))) { segment = 'high'; } else if (segmentMapping.low_value_queues.some(q => normalizedSkill.includes(q.toLowerCase()))) { segment = 'low'; } else { segment = 'medium'; } } return { skill, segment, volume, cost_volume: volume, // In synthetic data, we assume all are non-abandon aht_seconds: aht_mean, // Renombrado para compatibilidad metrics: { fcr: isNaN(fcr_approx) ? 0 : Math.max(0, Math.min(100, Math.round(fcr_approx))), aht: isNaN(aht_mean) ? 0 : Math.max(0, Math.min(100, Math.round(100 - ((aht_mean - 240) / 310) * 100))), csat: isNaN(avgCsat) ? 0 : Math.max(0, Math.min(100, Math.round(avgCsat))), hold_time: isNaN(avg_hold_time) ? 0 : Math.max(0, Math.min(100, Math.round(100 - (avg_hold_time / 120) * 100))), transfer_rate: isNaN(transfer_rate) ? 0 : Math.max(0, Math.min(100, Math.round(transfer_rate * 100))) }, annual_cost, cpi, variability: { cv_aht: Math.round(cv_aht * 100), // Convertir a porcentaje cv_talk_time: 0, // Deprecado en v2.1 cv_hold_time: 0, // Deprecado en v2.1 transfer_rate }, automation_readiness, // New dimensions (v2.1) dimensions: { predictability: Math.round(predictability_score * 10) / 10, complexity_inverse: Math.round(complexity_inverse_score * 10) / 10, repetitivity: Math.round(repetitivity_score * 10) / 10 }, readiness_category }; }); }; // v2.0: Add NPV and costBreakdown const generateEconomicModelData = (): EconomicModelData => { const currentAnnualCost = randomInt(800000, 2500000); const annualSavings = randomInt(150000, 500000); const futureAnnualCost = currentAnnualCost - annualSavings; const initialInvestment = randomInt(40000, 150000); 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: 'Operational efficiency', amount: annualSavings * 0.30, percentage: 30 }, { category: 'FCR Improvement', amount: annualSavings * 0.15, percentage: 15 }, { category: 'Attrition reduction', 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, futureAnnualCost, annualSavings, initialInvestment, paybackMonths, roi3yr: parseFloat(roi3yr.toFixed(1)), npv: Math.round(npv), savingsBreakdown, costBreakdown }; }; // v2.0: Add multiple percentiles const generateBenchmarkData = (): BenchmarkDataPoint[] => { const userAHT = randomInt(380, 450); const industryAHT = 420; const userFCR = randomFloat(0.65, 0.78, 2); const industryFCR = 0.72; const userCSAT = randomFloat(4.1, 4.6, 1); const industryCSAT = 4.3; const userCPI = randomFloat(2.8, 4.5, 2); const industryCPI = 3.5; return [ { kpi: 'Average AHT', userValue: userAHT, userDisplay: `${userAHT}s`, industryValue: industryAHT, industryDisplay: `${industryAHT}s`, percentile: randomInt(40, 75), p25: 380, p50: 420, p75: 460, p90: 510 }, { kpi: 'Tasa FCR', userValue: userFCR, userDisplay: `${(userFCR * 100).toFixed(0)}%`, industryValue: industryFCR, industryDisplay: `${(industryFCR * 100).toFixed(0)}%`, percentile: randomInt(30, 65), p25: 0.65, p50: 0.72, p75: 0.82, p90: 0.88 }, { kpi: 'CSAT', userValue: userCSAT, userDisplay: `${userCSAT}/5`, industryValue: industryCSAT, industryDisplay: `${industryCSAT}/5`, percentile: randomInt(45, 80), p25: 4.0, p50: 4.3, p75: 4.6, p90: 4.8 }, { kpi: 'Coste por Interacción (Voz)', userValue: userCPI, userDisplay: `€${userCPI.toFixed(2)}`, industryValue: industryCPI, industryDisplay: `€${industryCPI.toFixed(2)}`, percentile: randomInt(50, 85), p25: 2.8, p50: 3.5, p75: 4.2, p90: 5.0 }, ]; }; export const generateAnalysis = async ( tier: TierKey, costPerHour: number = 20, avgCsat: number = 85, segmentMapping?: { high_value_queues: string[]; medium_value_queues: string[]; low_value_queues: string[] }, file?: File, sheetUrl?: string, useSynthetic?: boolean, authHeaderOverride?: string ): Promise => { // If there is a file, process it // If there is a file, first try to use the backend if (file && !useSynthetic) { console.log('📡 Processing file (API first):', file.name); // Pre-parse file to get dateRange and interactions (used in both routes) let dateRange: { min: string; max: string } | undefined; let parsedInteractions: RawInteraction[] | undefined; try { const { parseFile, validateInteractions } = await import('./fileParser'); const interactions = await parseFile(file); const validation = validateInteractions(interactions); dateRange = validation.stats.dateRange || undefined; parsedInteractions = interactions; // Save to use in drilldownData console.log(`📅 Date range extracted: ${dateRange?.min} to ${dateRange?.max}`); console.log(`📊 Parsed ${interactions.length} interactions for drilldown`); // Cache the CSV file on the server for future use try { if (authHeaderOverride && file) { await saveFileToServerCache(authHeaderOverride, file, costPerHour); console.log(`💾 CSV file cached on server for future use`); } else { console.warn('⚠️ No se pudo cachear: falta authHeader o file'); } } catch (cacheError) { console.warn('⚠️ Could not cache file:', cacheError); } } catch (e) { console.warn('⚠️ Could not extract dateRange from file:', e); } // 1) Intentar backend + mapeo try { const raw = await callAnalysisApiRaw({ tier, costPerHour, avgCsat, segmentMapping, file, authHeaderOverride, }); const mapped = mapBackendResultsToAnalysisData(raw, tier); // Add dateRange extracted from file mapped.dateRange = dateRange; // Heatmap: use frontend calculations (parsedInteractions) for consistency // This ensures dashboard shows the same values as realDataAnalysis logs if (parsedInteractions && parsedInteractions.length > 0) { const skillMetrics = calculateSkillMetrics(parsedInteractions, costPerHour); mapped.heatmapData = generateHeatmapFromMetrics(skillMetrics, avgCsat, segmentMapping); console.log('📊 Heatmap generated from frontend (parsedInteractions) - consistent metrics'); } else { // Fallback: use backend if there are no parsedInteractions mapped.heatmapData = buildHeatmapFromBackend( raw, costPerHour, avgCsat, segmentMapping ); console.log('📊 Heatmap generated from backend (fallback - without parsedInteractions)'); } // v4.5: SYNCHRONIZE CPI from economy dimension with heatmapData for consistency between tabs // The heatmapData contains the correctly calculated CPI (with weighted cost_volume) // The economy dimension was calculated in mapBackendResultsToAnalysisData with another formula // We update the dimension to show the same value as Executive Summary if (mapped.heatmapData && mapped.heatmapData.length > 0) { const heatmapData = mapped.heatmapData; const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0); const hasCpiField = heatmapData.some(h => h.cpi !== undefined && h.cpi > 0); let globalCPI: number; if (hasCpiField) { // Real CPI available: weighted average by cost_volume globalCPI = totalCostVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.cpi || 0) * (h.cost_volume || h.volume), 0) / totalCostVolume : 0; } else { // Fallback: annual_cost / cost_volume const totalAnnualCost = heatmapData.reduce((sum, h) => sum + (h.annual_cost || 0), 0); globalCPI = totalCostVolume > 0 ? totalAnnualCost / totalCostVolume : 0; } // Update economy dimension with CPI calculated from heatmap // Search for both economy_costs (backend) and economy_cpi (frontend fallback) const economyDimIdx = mapped.dimensions.findIndex(d => d.id === 'economy_costs' || d.name === 'economy_costs' || d.id === 'economy_cpi' || d.name === 'economy_cpi' ); if (economyDimIdx >= 0 && globalCPI > 0) { // Use airline benchmark (€3.50) for consistency with ExecutiveSummaryTab // Percentiles: p25=2.20, p50=3.50, p75=4.50, p90=5.50 const CPI_BENCHMARK = 3.50; const cpiDiff = globalCPI - CPI_BENCHMARK; // For inverted CPI: lower is better const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative'; // Calculate score based on airline percentiles let newScore: number; if (globalCPI <= 2.20) newScore = 100; else if (globalCPI <= 3.50) newScore = 80; else if (globalCPI <= 4.50) newScore = 60; else if (globalCPI <= 5.50) newScore = 40; else newScore = 20; mapped.dimensions[economyDimIdx].score = newScore; mapped.dimensions[economyDimIdx].kpi = { label: 'Coste por Interacción', value: `€${globalCPI.toFixed(2)}`, change: `vs benchmark €${CPI_BENCHMARK.toFixed(2)}`, changeType: cpiStatus as 'positive' | 'neutral' | 'negative' }; console.log(`💰 CPI sincronizado: €${globalCPI.toFixed(2)}, score: ${newScore}`); } } // v3.5: Calculate drilldownData FIRST (necessary for opportunities and roadmap) if (parsedInteractions && parsedInteractions.length > 0) { mapped.drilldownData = calculateDrilldownMetrics(parsedInteractions, costPerHour); console.log(`📊 Drill-down calculated: ${mapped.drilldownData.length} skills, ${mapped.drilldownData.filter(d => d.isPriorityCandidate).length} priority candidates`); // v4.4: Cache drilldownData on server BEFORE returning (fix: was fire-and-forget) // This ensures cache is available when user clicks "Use Cache" if (authHeaderOverride && mapped.drilldownData.length > 0) { try { const cacheSuccess = await saveDrilldownToServerCache(authHeaderOverride, mapped.drilldownData); if (cacheSuccess) { console.log('💾 DrilldownData cached on server successfully'); } else { console.warn('⚠️ Could not cache drilldownData - fallback to heatmap on next use'); } } catch (cacheErr) { console.warn('⚠️ Error cacheando drilldownData:', cacheErr); } } // Use opportunities and roadmap based on drilldownData (real data) mapped.opportunities = generateOpportunitiesFromDrilldown(mapped.drilldownData, costPerHour); mapped.roadmap = generateRoadmapFromDrilldown(mapped.drilldownData, costPerHour); console.log(`📊 Opportunities: ${mapped.opportunities.length}, Roadmap: ${mapped.roadmap.length}`); } else { console.warn('⚠️ No hay interacciones parseadas, usando heatmap para drilldown'); // v4.3: Generate drilldownData from heatmap to use same functions mapped.drilldownData = generateDrilldownFromHeatmap(mapped.heatmapData, costPerHour); mapped.opportunities = generateOpportunitiesFromDrilldown(mapped.drilldownData, costPerHour); mapped.roadmap = generateRoadmapFromDrilldown(mapped.drilldownData, costPerHour); } // Findings y recommendations mapped.findings = generateFindingsFromData(mapped); mapped.recommendations = generateRecommendationsFromData(mapped); // Benchmark: for now we do not have real data mapped.benchmarkData = []; console.log( '✅ Usando resultados del backend mapeados (heatmap + opportunities + drilldown reales)' ); return mapped; } catch (apiError: any) { const status = apiError?.status; const msg = (apiError as Error).message || ''; // 🔐 If it is an authentication error (401), we do NOT fallback if (status === 401 || msg.includes('401')) { console.error( '❌ Authentication error in backend, aborting analysis (no fallback).' ); throw apiError; } console.error( '❌ Backend /analysis no disponible o mapeo incompleto, fallback a lógica local:', apiError ); } // 2) Fallback completo: lógica antigua del frontend try { const { parseFile, validateInteractions } = await import('./fileParser'); const interactions = await parseFile(file); const validation = validateInteractions(interactions); if (!validation.valid) { console.error('❌ Validation errors:', validation.errors); throw new Error( `Validación fallida: ${validation.errors.join(', ')}` ); } if (validation.warnings.length > 0) { console.warn('⚠️ Warnings:', validation.warnings); } return generateAnalysisFromRealData( tier, interactions, costPerHour, avgCsat, segmentMapping ); } catch (error) { console.error('❌ Error processing file:', error); throw new Error( `Error processing file: ${(error as Error).message}` ); } } // If there is a Google Sheets URL, process it (TODO: implement) if (sheetUrl && !useSynthetic) { console.warn('🔗 Google Sheets URL processing not implemented yet, using synthetic data'); } // Generate synthetic data (fallback) console.log('✨ Generating synthetic data'); return generateSyntheticAnalysis(tier, costPerHour, avgCsat, segmentMapping); }; /** * Generates analysis using the CSV file cached on the server * Allows re-analysis without needing to upload the file again * Funciona entre diferentes navegadores y dispositivos * * v3.5: Downloads the cached CSV to parse locally and obtain * all original queues (original_queue_id) instead of only * las 9 categorías agregadas (queue_skill) */ export const generateAnalysisFromCache = async ( tier: TierKey, costPerHour: number = 20, avgCsat: number = 85, segmentMapping?: { high_value_queues: string[]; medium_value_queues: string[]; low_value_queues: string[] }, authHeaderOverride?: string ): Promise => { console.log('💾 Analyzing from server-cached file...'); // Verify that we have authHeader if (!authHeaderOverride) { throw new Error('Authentication required to access server cache.'); } const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; // Prepare economic data const economyData = { costPerHour, avgCsat, segmentMapping, }; // Crear FormData para el endpoint const formData = new FormData(); formData.append('economy_json', JSON.stringify(economyData)); formData.append('analysis', 'premium'); console.log('📡 Running backend analysis and drilldown fetch in parallel...'); // === EJECUTAR EN PARALELO: Backend analysis + DrilldownData fetch === const backendAnalysisPromise = fetch(`${API_BASE_URL}/analysis/cached`, { method: 'POST', headers: { Authorization: authHeaderOverride, }, body: formData, }); // Get cached drilldownData (small JSON, very fast) const drilldownPromise = getCachedDrilldown(authHeaderOverride); // Esperar ambas operaciones en paralelo const [response, cachedDrilldownData] = await Promise.all([backendAnalysisPromise, drilldownPromise]); if (cachedDrilldownData) { console.log(`✅ Got cached drilldownData: ${cachedDrilldownData.length} skills`); } else { console.warn('⚠️ No cached drilldownData found, will use heatmap fallback'); } try { if (response.status === 404) { throw new Error('No file cached on server. Please upload a CSV file first.'); } if (!response.ok) { const errorText = await response.text(); console.error('❌ Backend error:', response.status, errorText); throw new Error(`Server error (${response.status}): ${errorText}`); } const rawResponse = await response.json(); const raw = rawResponse.results; const dateRangeFromBackend = rawResponse.dateRange; const uniqueQueuesFromBackend = rawResponse.uniqueQueues; console.log('✅ Backend analysis from cache completed'); console.log('📅 Date range from backend:', dateRangeFromBackend); console.log('📊 Unique queues from backend:', uniqueQueuesFromBackend); // Mapear resultados del backend a AnalysisData (solo 2 parámetros) console.log('📦 Raw backend results keys:', Object.keys(raw || {})); console.log('📦 volumetry:', raw?.volumetry ? 'present' : 'missing'); console.log('📦 operational_performance:', raw?.operational_performance ? 'present' : 'missing'); console.log('📦 agentic_readiness:', raw?.agentic_readiness ? 'present' : 'missing'); const mapped = mapBackendResultsToAnalysisData(raw, tier); console.log('📊 Mapped data summaryKpis:', mapped.summaryKpis?.length || 0); console.log('📊 Mapped data dimensions:', mapped.dimensions?.length || 0); // Add dateRange from backend if (dateRangeFromBackend && dateRangeFromBackend.min && dateRangeFromBackend.max) { mapped.dateRange = dateRangeFromBackend; } // Heatmap: build from real backend data mapped.heatmapData = buildHeatmapFromBackend( raw, costPerHour, avgCsat, segmentMapping ); console.log('📊 Heatmap data points:', mapped.heatmapData?.length || 0); // v4.6: SYNCHRONIZE CPI from economy dimension with heatmapData for consistency between tabs // (Mismo fix que en generateAnalysis - necesario para path de cache) if (mapped.heatmapData && mapped.heatmapData.length > 0) { const heatmapData = mapped.heatmapData; const totalCostVolume = heatmapData.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0); const hasCpiField = heatmapData.some(h => h.cpi !== undefined && h.cpi > 0); // DEBUG: Log CPI calculation details console.log('🔍 CPI SYNC DEBUG (cache):'); console.log(' - heatmapData length:', heatmapData.length); console.log(' - hasCpiField:', hasCpiField); console.log(' - totalCostVolume:', totalCostVolume); if (hasCpiField) { console.log(' - Sample CPIs:', heatmapData.slice(0, 3).map(h => ({ skill: h.skill, cpi: h.cpi, cost_volume: h.cost_volume }))); } let globalCPI: number; if (hasCpiField) { globalCPI = totalCostVolume > 0 ? heatmapData.reduce((sum, h) => sum + (h.cpi || 0) * (h.cost_volume || h.volume), 0) / totalCostVolume : 0; } else { const totalAnnualCost = heatmapData.reduce((sum, h) => sum + (h.annual_cost || 0), 0); console.log(' - totalAnnualCost (fallback):', totalAnnualCost); globalCPI = totalCostVolume > 0 ? totalAnnualCost / totalCostVolume : 0; } console.log(' - globalCPI calculated:', globalCPI.toFixed(4)); // Search for both economy_costs (backend) and economy_cpi (frontend fallback) const dimensionIds = mapped.dimensions.map(d => ({ id: d.id, name: d.name })); console.log(' - Available dimensions:', dimensionIds); const economyDimIdx = mapped.dimensions.findIndex(d => d.id === 'economy_costs' || d.name === 'economy_costs' || d.id === 'economy_cpi' || d.name === 'economy_cpi' ); console.log(' - economyDimIdx:', economyDimIdx); if (economyDimIdx >= 0 && globalCPI > 0) { const oldKpi = mapped.dimensions[economyDimIdx].kpi; console.log(' - OLD KPI value:', oldKpi?.value); // Use airline benchmark (€3.50) for consistency with ExecutiveSummaryTab // Percentiles: p25=2.20, p50=3.50, p75=4.50, p90=5.50 const CPI_BENCHMARK = 3.50; const cpiDiff = globalCPI - CPI_BENCHMARK; // For inverted CPI: lower is better const cpiStatus = cpiDiff <= 0 ? 'positive' : cpiDiff <= 0.5 ? 'neutral' : 'negative'; // Calculate score based on airline percentiles let newScore: number; if (globalCPI <= 2.20) newScore = 100; else if (globalCPI <= 3.50) newScore = 80; else if (globalCPI <= 4.50) newScore = 60; else if (globalCPI <= 5.50) newScore = 40; else newScore = 20; mapped.dimensions[economyDimIdx].score = newScore; mapped.dimensions[economyDimIdx].kpi = { label: 'Coste por Interacción', value: `€${globalCPI.toFixed(2)}`, change: `vs benchmark €${CPI_BENCHMARK.toFixed(2)}`, changeType: cpiStatus as 'positive' | 'neutral' | 'negative' }; console.log(' - NEW KPI value:', mapped.dimensions[economyDimIdx].kpi.value); console.log(' - NEW score:', newScore); console.log(`💰 CPI sincronizado (cache): €${globalCPI.toFixed(2)}`); } else { console.warn('⚠️ CPI sync skipped: economyDimIdx=', economyDimIdx, 'globalCPI=', globalCPI); } } // === DrilldownData: use cached (fast) or fallback to heatmap === if (cachedDrilldownData && cachedDrilldownData.length > 0) { // Use cached drilldownData directly (already calculated when uploading file) mapped.drilldownData = cachedDrilldownData; console.log(`📊 Using cached drilldownData: ${mapped.drilldownData.length} skills`); // Count original queues for log const uniqueOriginalQueues = new Set( mapped.drilldownData.flatMap((d: any) => (d.originalQueues || []).map((q: any) => q.original_queue_id) ).filter((q: string) => q && q.trim() !== '') ).size; console.log(`📊 Total original queues: ${uniqueOriginalQueues}`); // Usar oportunidades y roadmap basados en drilldownData real mapped.opportunities = generateOpportunitiesFromDrilldown(mapped.drilldownData, costPerHour); mapped.roadmap = generateRoadmapFromDrilldown(mapped.drilldownData, costPerHour); console.log(`📊 Opportunities: ${mapped.opportunities.length}, Roadmap: ${mapped.roadmap.length}`); } else if (mapped.heatmapData && mapped.heatmapData.length > 0) { // v4.5: No cached drilldownData - try to calculate it from cached CSV console.log('⚠️ No cached drilldownData found, attempting to calculate from cached CSV...'); let calculatedDrilldown = false; try { // Download and parse cached CSV to calculate real drilldown const cachedFile = await downloadCachedFile(authHeaderOverride); if (cachedFile) { console.log(`📥 Downloaded cached CSV: ${(cachedFile.size / 1024 / 1024).toFixed(2)} MB`); const { parseFile } = await import('./fileParser'); const parsedInteractions = await parseFile(cachedFile); if (parsedInteractions && parsedInteractions.length > 0) { console.log(`📊 Parsed ${parsedInteractions.length} interactions from cached CSV`); // Calculate real drilldown from interactions mapped.drilldownData = calculateDrilldownMetrics(parsedInteractions, costPerHour); console.log(`📊 Calculated drilldown: ${mapped.drilldownData.length} skills`); // Save drilldown in cache for next use try { const saveSuccess = await saveDrilldownToServerCache(authHeaderOverride, mapped.drilldownData); if (saveSuccess) { console.log('💾 DrilldownData saved to cache for future use'); } else { console.warn('⚠️ Failed to save drilldownData to cache'); } } catch (saveErr) { console.warn('⚠️ Error saving drilldownData to cache:', saveErr); } calculatedDrilldown = true; } } } catch (csvErr) { console.warn('⚠️ Could not calculate drilldown from cached CSV:', csvErr); } if (!calculatedDrilldown) { // Final fallback: use heatmap (approximate data) console.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.warn('⚠️ FALLBACK ACTIVE: No cached drilldownData'); console.warn(' Probable cause: CSV was not uploaded correctly or cache expired'); console.warn(' Consequence: Using aggregated heatmap data (less precise)'); console.warn(' Solution: Re-upload the CSV file to obtain complete data'); console.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); mapped.drilldownData = generateDrilldownFromHeatmap(mapped.heatmapData, costPerHour); console.log(`📊 Drill-down from heatmap (fallback): ${mapped.drilldownData.length} aggregated skills`); } // Use same functions as fresh route for consistency mapped.opportunities = generateOpportunitiesFromDrilldown(mapped.drilldownData, costPerHour); mapped.roadmap = generateRoadmapFromDrilldown(mapped.drilldownData, costPerHour); } // Findings y recommendations mapped.findings = generateFindingsFromData(mapped); mapped.recommendations = generateRecommendationsFromData(mapped); // Benchmark: empty for now mapped.benchmarkData = []; // Marcar que viene del backend/caché mapped.source = 'backend'; console.log('✅ Analysis generated from server-cached file'); return mapped; } catch (error) { console.error('❌ Error analyzing from cache:', error); throw error; } }; // Helper function to generate drilldownData from heatmapData when we do not have parsedInteractions function generateDrilldownFromHeatmap( heatmapData: HeatmapDataPoint[], costPerHour: number ): DrilldownDataPoint[] { return heatmapData.map(hp => { const cvAht = hp.variability?.cv_aht || 0; const transferRate = hp.variability?.transfer_rate || hp.metrics?.transfer_rate || 0; const fcrRate = hp.metrics?.fcr || 0; // FCR Técnico: usar el campo si existe, sino calcular como 100 - transfer_rate const fcrTecnico = hp.metrics?.fcr_tecnico ?? (100 - transferRate); const agenticScore = hp.dimensions ? (hp.dimensions.predictability * 0.4 + hp.dimensions.complexity_inverse * 0.35 + hp.dimensions.repetitivity * 0.25) : (hp.automation_readiness || 0) / 10; // v4.4: Use clasificarTierSimple with ALL available heatmap data // cvAht, transferRate y fcrRate están en % (ej: 75), clasificarTierSimple espera decimal (ej: 0.75) const tier = clasificarTierSimple( agenticScore, cvAht / 100, // CV como decimal transferRate / 100, // Transfer como decimal fcrRate / 100, // FCR como decimal (nuevo en v4.4) hp.volume // Volumen para red flag check (nuevo en v4.4) ); return { skill: hp.skill, volume: hp.volume, volumeValid: hp.volume, aht_mean: hp.aht_seconds, cv_aht: cvAht, transfer_rate: transferRate, fcr_rate: fcrRate, fcr_tecnico: fcrTecnico, // FCR Técnico para consistencia con Summary agenticScore: agenticScore, isPriorityCandidate: cvAht < 75, originalQueues: [{ original_queue_id: hp.skill, volume: hp.volume, volumeValid: hp.volume, aht_mean: hp.aht_seconds, cv_aht: cvAht, transfer_rate: transferRate, fcr_rate: fcrRate, fcr_tecnico: fcrTecnico, // FCR Técnico para consistencia con Summary agenticScore: agenticScore, tier: tier, isPriorityCandidate: cvAht < 75, }], }; }); } // Helper function to generate analysis with synthetic data const generateSyntheticAnalysis = ( tier: TierKey, costPerHour: number = 20, avgCsat: number = 85, segmentMapping?: { high_value_queues: string[]; medium_value_queues: string[]; low_value_queues: string[] } ): AnalysisData => { const overallHealthScore = randomInt(55, 95); const summaryKpis: Kpi[] = [ { label: "Interacciones Totales", value: randomInt(15000, 50000).toLocaleString('es-ES') }, { label: "AHT Promedio", value: `${randomInt(300, 480)}s`, change: `-${randomInt(5, 20)}s`, changeType: 'positive' }, { label: "Tasa FCR", value: `${randomInt(70, 88)}%`, change: `+${randomFloat(0.5, 2, 1)}%`, changeType: 'positive' }, { label: "CSAT", value: `${randomFloat(4.1, 4.8, 1)}/5`, change: `-${randomFloat(0.1, 0.3, 1)}`, changeType: 'negative' }, ]; // v3.0: 5 viable dimensions const dimensionKeys = ['volumetry_distribution', 'operational_efficiency', 'effectiveness_resolution', 'complexity_predictability', 'agentic_readiness']; const dimensions: DimensionAnalysis[] = dimensionKeys.map(key => { const content = DIMENSIONS_CONTENT[key as keyof typeof DIMENSIONS_CONTENT]; const score = randomInt(50, 98); const status = getScoreColor(score); const dimension: DimensionAnalysis = { id: key, name: key as any, title: randomFromList(content.titles), score, percentile: randomInt(30, 85), summary: randomFromList(content.summaries[status === 'green' ? 'good' : status === 'yellow' ? 'medium' : 'bad']), kpi: randomFromList(content.kpis), icon: content.icon, }; // Add distribution_data for volumetry_distribution if (key === 'volumetry_distribution') { const hourly = generateHourlyDistribution(); dimension.distribution_data = { hourly, off_hours_pct: calculateOffHoursPct(hourly), peak_hours: identifyPeakHours(hourly) }; } return dimension; }); // v2.0: Calcular Agentic Readiness Score let agenticReadiness = undefined; if (tier === 'gold' || tier === 'silver') { // Generate synthetic data for the algorithm const volumen_mes = randomInt(5000, 25000); const aht_values = Array.from({ length: 100 }, () => Math.max(180, normalRandom(420, 120)) // Media 420s, std 120s ); const escalation_rate = randomFloat(0.05, 0.25, 2); const cpi_humano = randomFloat(2.5, 5.0, 2); const volumen_anual = volumen_mes * 12; const agenticInput: AgenticReadinessInput = { volumen_mes, aht_values, escalation_rate, cpi_humano, volumen_anual, tier }; // Additional data for GOLD if (tier === 'gold') { const hourly_distribution = dimensions.find(d => d.name === 'volumetry_distribution')?.distribution_data?.hourly; const off_hours_pct = dimensions.find(d => d.name === 'volumetry_distribution')?.distribution_data?.off_hours_pct; agenticInput.structured_fields_pct = randomFloat(0.4, 0.9, 2); agenticInput.exception_rate = randomFloat(0.05, 0.25, 2); agenticInput.hourly_distribution = hourly_distribution; agenticInput.off_hours_pct = off_hours_pct; agenticInput.csat_values = Array.from({ length: 100 }, () => Math.max(1, Math.min(5, normalRandom(4.3, 0.8))) ); } agenticReadiness = calculateAgenticReadinessScore(agenticInput); } const heatmapData = generateHeatmapData(costPerHour, avgCsat, segmentMapping); console.log('📊 Heatmap data generated:', { length: heatmapData.length, firstItem: heatmapData[0], metricsKeys: heatmapData[0] ? Object.keys(heatmapData[0].metrics) : [], metricsValues: heatmapData[0] ? heatmapData[0].metrics : {}, hasNaN: heatmapData.some(item => Object.values(item.metrics).some(v => isNaN(v)) ) }); // v4.3: Generate drilldownData from heatmap to use same functions const drilldownData = generateDrilldownFromHeatmap(heatmapData, costPerHour); return { tier, overallHealthScore, summaryKpis, dimensions, heatmapData, drilldownData, agenticReadiness, findings: generateFindingsFromTemplates(), recommendations: generateRecommendationsFromTemplates(), opportunities: generateOpportunitiesFromDrilldown(drilldownData, costPerHour), economicModel: generateEconomicModelData(), roadmap: generateRoadmapFromDrilldown(drilldownData, costPerHour), benchmarkData: generateBenchmarkData(), source: 'synthetic', }; };