import React, { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { HelpCircle, ArrowUpDown, TrendingUp, TrendingDown, AlertTriangle, Star, Award } from 'lucide-react'; import { HeatmapDataPoint } from '../types'; import clsx from 'clsx'; import MethodologyFooter from './MethodologyFooter'; interface HeatmapProProps { data: HeatmapDataPoint[]; } type SortKey = 'skill' | 'fcr' | 'aht' | 'csat' | 'hold_time' | 'transfer_rate' | 'average' | 'cost'; type SortOrder = 'asc' | 'desc'; interface TooltipData { skill: string; metric: string; value: number; x: number; y: number; } interface Insight { type: 'strength' | 'opportunity'; skill: string; metric: string; value: number; percentile: string; } const getCellColor = (value: number) => { if (value >= 95) return 'bg-emerald-600 text-white'; if (value >= 90) return 'bg-emerald-500 text-white'; if (value >= 85) return 'bg-green-400 text-green-900'; if (value >= 80) return 'bg-yellow-300 text-yellow-900'; if (value >= 70) return 'bg-amber-400 text-amber-900'; return 'bg-red-500 text-white'; }; const getPercentile = (value: number): string => { if (value >= 95) return 'P95+ (Best-in-Class)'; if (value >= 90) return 'P90-P95 (Excelente)'; if (value >= 85) return 'P75-P90 (Competitivo)'; if (value >= 70) return 'P50-P75 (Por debajo promedio)'; return ' { if (value >= 95) return ; if (value < 70) return ; return null; }; const HeatmapPro: React.FC = ({ data }) => { console.log('🔥 HeatmapPro received data:', { length: data?.length, firstItem: data?.[0], firstMetrics: data?.[0]?.metrics, metricsKeys: data?.[0] ? Object.keys(data[0].metrics) : [], metricsValues: data?.[0] ? Object.values(data[0].metrics) : [], hasUndefinedMetrics: data?.some(item => Object.values(item.metrics).some(v => v === undefined) ), hasNaNMetrics: data?.some(item => Object.values(item.metrics).some(v => isNaN(v)) ) }); const [sortKey, setSortKey] = useState('skill'); const [sortOrder, setSortOrder] = useState('asc'); const [hoveredRow, setHoveredRow] = useState(null); const [tooltip, setTooltip] = useState(null); const metrics: Array<{ key: keyof HeatmapDataPoint['metrics']; label: string }> = [ { key: 'fcr', label: 'FCR' }, { key: 'aht', label: 'AHT' }, { key: 'csat', label: 'CSAT' }, { key: 'hold_time', label: 'Hold Time' }, { key: 'transfer_rate', label: 'Transfer %' }, ]; // Calculate insights const insights = useMemo(() => { try { console.log('💡 insights useMemo called'); const allMetrics: Array<{ skill: string; metric: string; value: number }> = []; if (!data || !Array.isArray(data)) { console.log('⚠️ insights: data is invalid'); return { strengths: [], opportunities: [] }; } console.log(`✅ insights: processing ${data.length} items`); data.forEach(item => { if (!item?.metrics) return; metrics.forEach(({ key, label }) => { const value = item.metrics?.[key]; if (typeof value === 'number' && !isNaN(value)) { allMetrics.push({ skill: item?.skill || 'Unknown', metric: label, value: value, }); } }); }); allMetrics.sort((a, b) => b.value - a.value); const strengths: Insight[] = (allMetrics.slice(0, 3) || []).map(m => ({ type: 'strength' as const, skill: m?.skill || 'Unknown', metric: m?.metric || 'Unknown', value: m?.value || 0, percentile: getPercentile(m?.value || 0), })); const opportunities: Insight[] = (allMetrics.slice(-3).reverse() || []).map(m => ({ type: 'opportunity' as const, skill: m?.skill || 'Unknown', metric: m?.metric || 'Unknown', value: m?.value || 0, percentile: getPercentile(m?.value || 0), })); return { strengths, opportunities }; } catch (error) { console.error('❌ Error in insights useMemo:', error); return { strengths: [], opportunities: [] }; } }, [data]); // Calculate dynamic title const dynamicTitle = useMemo(() => { try { console.log('📊 dynamicTitle useMemo called'); if (!data || !Array.isArray(data) || data.length === 0) { console.log('⚠️ dynamicTitle: data is invalid or empty'); return 'Análisis de métricas de rendimiento'; } console.log(`✅ dynamicTitle: processing ${data.length} items`); const totalMetrics = data.length * metrics.length; const belowP75 = data.reduce((count, item) => { if (!item?.metrics) return count; return count + metrics.filter(m => { const value = item.metrics?.[m.key]; return typeof value === 'number' && !isNaN(value) && value < 85; }).length; }, 0); const percentage = Math.round((belowP75 / totalMetrics) * 100); const totalCost = data.reduce((sum, item) => sum + (item?.annual_cost || 0), 0); const costStr = `€${Math.round(totalCost / 1000)}K`; const metricCounts = metrics.map(({ key, label }) => ({ label, count: data.filter(item => { if (!item?.metrics) return false; const value = item.metrics?.[key]; return typeof value === 'number' && !isNaN(value) && value < 85; }).length, })); metricCounts.sort((a, b) => b.count - a.count); const topMetric = metricCounts?.[0]; return `${percentage}% de las métricas están por debajo de P75, representando ${costStr} en coste anual, con ${topMetric?.label || 'N/A'} mostrando la mayor oportunidad de mejora`; } catch (error) { console.error('❌ Error in dynamicTitle useMemo:', error); return 'Análisis de métricas de rendimiento'; } }, [data]); // Calculate averages const dataWithAverages = useMemo(() => { try { console.log('📋 dataWithAverages useMemo called'); if (!data || !Array.isArray(data)) { console.log('⚠️ dataWithAverages: data is invalid'); return []; } console.log(`✅ dataWithAverages: processing ${data.length} items`); return data.map((item, index) => { if (!item) { return { skill: 'Unknown', average: 0, metrics: {}, automation_readiness: 0, variability: {}, dimensions: {} }; } if (!item.metrics) { return { ...item, average: 0 }; } const values = metrics.map(m => item.metrics?.[m.key]).filter(v => typeof v === 'number' && !isNaN(v)); const average = values.length > 0 ? values.reduce((sum, v) => sum + v, 0) / values.length : 0; return { ...item, average }; }); } catch (error) { console.error('❌ Error in dataWithAverages useMemo:', error); return []; } }, [data]); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortKey(key); setSortOrder('desc'); } }; const sortedData = useMemo(() => { try { console.log('🔄 sortedData useMemo called', { hasDataWithAverages: !!dataWithAverages, isArray: Array.isArray(dataWithAverages), length: dataWithAverages?.length }); if (!dataWithAverages || !Array.isArray(dataWithAverages)) { console.log('⚠️ sortedData: dataWithAverages is invalid'); return []; } console.log(`✅ sortedData: sorting ${dataWithAverages.length} items`); console.log('About to spread and sort dataWithAverages'); const sorted = [...dataWithAverages].sort((a, b) => { try { if (!a || !b) { console.error('sort: a or b is null/undefined', { a, b }); return 0; } let aValue: number | string; let bValue: number | string; if (sortKey === 'skill') { aValue = a?.skill ?? ''; bValue = b?.skill ?? ''; } else if (sortKey === 'average') { aValue = a?.average ?? 0; bValue = b?.average ?? 0; } else if (sortKey === 'cost') { aValue = a?.annual_cost ?? 0; bValue = b?.annual_cost ?? 0; } else { aValue = a?.metrics?.[sortKey] ?? 0; bValue = b?.metrics?.[sortKey] ?? 0; } if (typeof aValue === 'string' && typeof bValue === 'string') { return sortOrder === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } return sortOrder === 'asc' ? (aValue as number) - (bValue as number) : (bValue as number) - (aValue as number); } catch (error) { console.error('Error in sort function:', error, { a, b, sortKey, sortOrder }); return 0; } }); console.log('✅ Sort completed successfully', { sortedLength: sorted.length }); return sorted; } catch (error) { console.error('❌ Error in sortedData useMemo:', error); return []; } }, [dataWithAverages, sortKey, sortOrder]); const handleCellHover = ( skill: string, metric: string, value: number, event: React.MouseEvent ) => { const rect = event.currentTarget.getBoundingClientRect(); setTooltip({ skill, metric, value, x: rect.left + rect.width / 2, y: rect.top, }); }; const handleCellLeave = () => { setTooltip(null); }; try { return (
{/* Header with Dynamic Title */}

Beyond CX Heatmap™

Mapa de calor de Readiness Agéntico por skill. Muestra el rendimiento en métricas clave comparado con benchmarks de industria (P75) para identificar fortalezas y áreas de mejora prioritarias.

{dynamicTitle}

Análisis de Performance Competitivo: Skills críticos vs. benchmarks de industria (P75) | Datos: Q4 2024 | N=15,000 interacciones

{/* Insights Panel */}
{/* Top Strengths */}

Top 3 Fortalezas

{insights.strengths.map((insight, idx) => (
{insight.skill} - {insight.metric} {insight.value}%
))}
{/* Top Opportunities */}

Top 3 Oportunidades de Mejora

{insights.opportunities.map((insight, idx) => (
{insight.skill} - {insight.metric} {insight.value}%
))}
{/* Heatmap Table */}
{metrics.map(({ key, label }) => ( ))} {sortedData.map((item, index) => { // Calculate average cost once const avgCost = sortedData.length > 0 ? sortedData.reduce((sum, d) => sum + (d?.annual_cost || 0), 0) / sortedData.length : 0; return ( setHoveredRow(item.skill)} onMouseLeave={() => setHoveredRow(null)} className={clsx( 'border-b border-slate-200 transition-colors', hoveredRow === item.skill && 'bg-blue-50' )} > {metrics.map(({ key }) => { const value = item?.metrics?.[key] ?? 0; return ( ); })} ); })}
handleSort('skill')} className="p-4 font-semibold text-slate-700 text-left cursor-pointer hover:bg-slate-100 transition-colors border-b-2 border-slate-300" >
Skill/Proceso
handleSort(key)} className="p-4 font-semibold text-slate-700 text-center cursor-pointer hover:bg-slate-100 transition-colors uppercase border-b-2 border-slate-300" >
{label}
handleSort('average')} className="p-4 font-semibold text-slate-700 text-center cursor-pointer hover:bg-slate-100 transition-colors border-b-2 border-slate-300" >
PROMEDIO
handleSort('cost')} className="p-4 font-semibold text-slate-700 text-center cursor-pointer hover:bg-slate-100 transition-colors border-b-2 border-slate-300" >
COSTE ANUAL
{item.skill} {item.segment && ( {item.segment === 'high' && '🟢 High'} {item.segment === 'medium' && '🟡 Medium'} {item.segment === 'low' && '🔴 Low'} )}
handleCellHover(item.skill, key.toUpperCase(), value, e)} onMouseLeave={handleCellLeave} > {value} {getCellIcon(value)} {item.average.toFixed(1)} {item.annual_cost ? (
€{Math.round(item.annual_cost / 1000)}K
= avgCost * 1.2 ? 'bg-red-500' // Alto coste (>120% del promedio) : (item?.annual_cost || 0) >= avgCost * 0.8 ? 'bg-amber-400' // Coste medio (80-120% del promedio) : 'bg-green-500' // Bajo coste (<80% del promedio) )} />
) : ( N/A )}
{/* Enhanced Legend */}
Escala de Performance vs. Industria:
<70 - Crítico (Por debajo P25)
70-80 - Oportunidad (P25-P50)
80-85 - Promedio (P50-P75)
85-90 - Competitivo (P75-P90)
90-95 - Excelente (P90-P95)
95+ - Best-in-Class (P95+)
{/* Tooltip */} {tooltip && (
{tooltip.skill}
{tooltip.metric}: {tooltip.value}%
Percentil: {getPercentile(tooltip.value)}
{tooltip.value >= 85 ? ( <> Por encima del promedio ) : ( <> Oportunidad de mejora )}
)}
{/* Methodology Footer */}
); } catch (error) { console.error('❌ CRITICAL ERROR in HeatmapPro render:', error); return (

❌ Error en Heatmap

No se pudo renderizar el componente. Error: {String(error)}

); } }; export default HeatmapPro;