import React, { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { HelpCircle, ArrowUpDown, TrendingUp, AlertTriangle, CheckCircle, Activity, ChevronDown, ChevronUp } from 'lucide-react'; import { HeatmapDataPoint } from '../types'; import clsx from 'clsx'; import MethodologyFooter from './MethodologyFooter'; import { getConsolidatedCategory, skillsConsolidationConfig } from '../config/skillsConsolidation'; interface VariabilityHeatmapProps { data: HeatmapDataPoint[]; } type SortKey = 'skill' | 'cv_aht' | 'cv_talk_time' | 'cv_hold_time' | 'transfer_rate' | 'automation_readiness' | 'volume'; type SortOrder = 'asc' | 'desc'; interface TooltipData { skill: string; metric: string; value: number; x: number; y: number; } interface Insight { type: 'quick_win' | 'standardize' | 'consult'; skill: string; volume: number; automation_readiness: number; recommendation: string; roi: number; } interface ConsolidatedDataPoint { categoryKey: string; categoryName: string; volume: number; originalSkills: string[]; variability: { cv_aht: number; cv_talk_time: number; cv_hold_time: number; transfer_rate: number; }; automation_readiness: number; } // Colores invertidos: Verde = bajo CV (bueno), Rojo = alto CV (malo) // Escala RELATIVA: Ajusta a los datos reales (45-75%) para mejor diferenciación const getCellColor = (value: number, minValue: number = 45, maxValue: number = 75) => { // Normalizar valor al rango 0-100 relativo al min/max actual const normalized = ((value - minValue) / (maxValue - minValue)) * 100; // Escala relativa a datos reales if (normalized < 20) return 'bg-emerald-600 text-white'; // Bajo en rango if (normalized < 35) return 'bg-green-500 text-white'; // Bajo-medio if (normalized < 50) return 'bg-yellow-400 text-yellow-900'; // Medio if (normalized < 70) return 'bg-amber-500 text-white'; // Alto-medio return 'bg-red-500 text-white'; // Alto en rango }; const getReadinessColor = (score: number) => { if (score >= 80) return 'bg-emerald-600 text-white'; if (score >= 60) return 'bg-yellow-400 text-yellow-900'; return 'bg-red-500 text-white'; }; const getReadinessLabel = (score: number): string => { if (score >= 80) return 'Listo para automatizar'; if (score >= 60) return 'Estandarizar primero'; return 'Consultoría recomendada'; }; const getCellIcon = (value: number) => { if (value < 25) return ; if (value >= 55) return ; return null; }; // Función para consolidar skills por categoría const consolidateVariabilityData = (data: HeatmapDataPoint[]): ConsolidatedDataPoint[] => { const consolidationMap = new Map(); data.forEach(item => { const category = getConsolidatedCategory(item.skill); if (!category) return; const key = category.category; if (!consolidationMap.has(key)) { consolidationMap.set(key, { category: key, displayName: category.displayName, volume: 0, skills: [], cvAhtSum: 0, cvTalkSum: 0, cvHoldSum: 0, transferRateSum: 0, readinessSum: 0, count: 0 }); } const entry = consolidationMap.get(key)!; entry.volume += item.volume || 0; entry.skills.push(item.skill); entry.cvAhtSum += item.variability?.cv_aht || 0; entry.cvTalkSum += item.variability?.cv_talk_time || 0; entry.cvHoldSum += item.variability?.cv_hold_time || 0; entry.transferRateSum += item.variability?.transfer_rate || 0; entry.readinessSum += item.automation_readiness || 0; entry.count += 1; }); return Array.from(consolidationMap.values()).map(entry => ({ categoryKey: entry.category, categoryName: entry.displayName, volume: entry.volume, originalSkills: [...new Set(entry.skills)], variability: { cv_aht: Math.round(entry.cvAhtSum / entry.count), cv_talk_time: Math.round(entry.cvTalkSum / entry.count), cv_hold_time: Math.round(entry.cvHoldSum / entry.count), transfer_rate: Math.round(entry.transferRateSum / entry.count) }, automation_readiness: Math.round(entry.readinessSum / entry.count) })); }; const VariabilityHeatmap: React.FC = ({ data }) => { const [sortKey, setSortKey] = useState('automation_readiness'); const [sortOrder, setSortOrder] = useState('desc'); const [hoveredRow, setHoveredRow] = useState(null); const [tooltip, setTooltip] = useState(null); const [expandedRows, setExpandedRows] = useState>(new Set()); const metrics: Array<{ key: keyof HeatmapDataPoint['variability']; label: string }> = [ { key: 'cv_aht', label: 'CV AHT' }, { key: 'cv_talk_time', label: 'CV Talk Time' }, { key: 'cv_hold_time', label: 'CV Hold Time' }, { key: 'transfer_rate', label: 'Transfer Rate' }, ]; // Calculate insights with consolidated data const insights = useMemo(() => { try { const consolidated = consolidateVariabilityData(data); const sortedByReadiness = [...consolidated].sort((a, b) => b.automation_readiness - a.automation_readiness); // Calculate simple ROI estimate: based on volume and variability reduction potential const getRoiEstimate = (cat: ConsolidatedDataPoint): number => { const volumeFactor = Math.min(cat.volume / 1000, 10); // Max 10K impact const variabilityReduction = Math.max(0, 75 - cat.variability.cv_aht); // Potential improvement return Math.round(volumeFactor * variabilityReduction * 1.5); // Rough EU multiplier }; const quickWins: Insight[] = sortedByReadiness .filter(item => item.automation_readiness >= 80) .slice(0, 5) .map(item => ({ type: 'quick_win', skill: item.categoryName, volume: item.volume, automation_readiness: item.automation_readiness, roi: getRoiEstimate(item), recommendation: `CV AHT ${item.variability.cv_aht}% → Listo para automatización` })); const standardize: Insight[] = sortedByReadiness .filter(item => item.automation_readiness >= 60 && item.automation_readiness < 80) .slice(0, 5) .map(item => ({ type: 'standardize', skill: item.categoryName, volume: item.volume, automation_readiness: item.automation_readiness, roi: getRoiEstimate(item), recommendation: `Estandarizar antes de automatizar` })); const consult: Insight[] = sortedByReadiness .filter(item => item.automation_readiness < 60) .slice(0, 5) .map(item => ({ type: 'consult', skill: item.categoryName, volume: item.volume, automation_readiness: item.automation_readiness, roi: getRoiEstimate(item), recommendation: `Consultoría para identificar causas raíz` })); return { quickWins, standardize, consult }; } catch (error) { console.error('❌ Error calculating insights (VariabilityHeatmap):', error); return { quickWins: [], standardize: [], consult: [] }; } }, [data]); // Calculate dynamic title const dynamicTitle = useMemo(() => { try { if (!data || !Array.isArray(data)) return 'Análisis de variabilidad interna'; const highVariability = data.filter(item => (item?.automation_readiness || 0) < 60).length; const total = data.length; if (highVariability === 0) { return `Todas las skills muestran baja variabilidad (>60), listas para automatización`; } else if (highVariability === total) { return `${highVariability} de ${total} skills muestran alta variabilidad (CV>40%), sugiriendo necesidad de estandarización antes de automatizar`; } else { return `${highVariability} de ${total} skills muestran alta variabilidad (CV>40%), sugiriendo necesidad de estandarización antes de automatizar`; } } catch (error) { console.error('❌ Error in dynamicTitle useMemo (VariabilityHeatmap):', error); return 'Análisis de variabilidad interna'; } }, [data]); // Consolidate data once for reuse const consolidatedData = useMemo(() => consolidateVariabilityData(data), [data]); // Get min/max values for relative color scaling const colorScaleValues = useMemo(() => { const cvValues = consolidatedData.flatMap(item => [ item.variability.cv_aht, item.variability.cv_talk_time, item.variability.cv_hold_time ]); return { min: Math.min(...cvValues, 45), max: Math.max(...cvValues, 75) }; }, [consolidatedData]); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortKey(key); setSortOrder(key === 'automation_readiness' ? 'desc' : key === 'volume' ? 'desc' : 'asc'); } }; const sortedData = [...consolidatedData].sort((a, b) => { let aValue: number | string; let bValue: number | string; if (sortKey === 'skill') { aValue = a.categoryName; bValue = b.categoryName; } else if (sortKey === 'automation_readiness') { aValue = a.automation_readiness; bValue = b.automation_readiness; } else if (sortKey === 'volume') { aValue = a.volume; bValue = b.volume; } else { aValue = a.variability?.[sortKey] || 0; bValue = b.variability?.[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); }); 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); }; return (
{/* Header with Dynamic Title */}

Heatmap de Variabilidad Interna™

Mide la consistencia y predictibilidad interna de cada skill. Baja variabilidad indica procesos maduros listos para automatización. Alta variabilidad sugiere necesidad de estandarización o consultoría.

{dynamicTitle}

{/* Insights Panel - Improved with Volume & ROI */}
{/* Quick Wins */}

✓ Quick Wins ({insights.quickWins.length})

{insights.quickWins.map((insight, idx) => (
{idx + 1}. {insight.skill}
Vol: {(insight.volume / 1000).toFixed(1)}K/mes | ROI: €{insight.roi}K/año
{insight.recommendation}
))} {insights.quickWins.length === 0 && (

No hay skills con readiness >80

)}
{/* Standardize - Top 5 */}

📈 Estandarizar ({insights.standardize.length})

{insights.standardize.map((insight, idx) => (
{idx + 1}. {insight.skill}
Vol: {(insight.volume / 1000).toFixed(1)}K/mes | ROI: €{insight.roi}K/año
{insight.recommendation}
))} {insights.standardize.length === 0 && (

No hay skills con readiness 60-79

)}
{/* Consult */}

⚠️ Consultoría ({insights.consult.length})

{insights.consult.map((insight, idx) => (
{idx + 1}. {insight.skill}
Vol: {(insight.volume / 1000).toFixed(1)}K/mes | ROI: €{insight.roi}K/año
{insight.recommendation}
))} {insights.consult.length === 0 && (

No hay skills con readiness <60

)}
{/* Heatmap Table */}
{metrics.map(({ key, label }) => ( ))} {sortedData.map((item, index) => ( setHoveredRow(item.categoryKey)} onMouseLeave={() => setHoveredRow(null)} className={clsx( 'border-b border-slate-200 transition-colors', hoveredRow === item.categoryKey && 'bg-blue-50' )} > {metrics.map(({ key }) => { const value = item.variability[key]; 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" >
Categoría/Skill
handleSort('volume')} className="p-4 font-semibold text-slate-700 text-center cursor-pointer hover:bg-slate-100 transition-colors border-b-2 border-slate-300 bg-blue-50" >
VOLUMEN
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('automation_readiness')} className="p-4 font-semibold text-slate-700 text-center cursor-pointer hover:bg-slate-100 transition-colors border-b-2 border-slate-300" >
READINESS
{item.categoryName} {item.originalSkills.length > 1 && ( ({item.originalSkills.length} skills) )}
{(item.volume / 1000).toFixed(1)}K/mes
handleCellHover(item.categoryName, key.toUpperCase(), value, e)} onMouseLeave={handleCellLeave} > {value}% {getCellIcon(value)}
{item.automation_readiness} {getReadinessLabel(item.automation_readiness)}
{/* Enhanced Legend - Relative Scale */}
Escala de Variabilidad (escala relativa a datos actuales):
Bajo (Mejor en rango)
Bajo-Medio
Medio
Alto-Medio
Alto (Peor en rango)
Automation Readiness (0-100):
80-100 - Listo para automatizar
60-79 - Estandarizar primero
<60 - Consultoría recomendada
💡 Nota: Los datos se han consolidado de 44 skills a 12 categorías para mayor claridad. Las métricas muestran promedios por categoría.
{/* Tooltip */} {tooltip && (
{tooltip.skill}
{tooltip.metric}: {tooltip.value}%
)}
{/* Methodology Footer */}
); }; export default VariabilityHeatmap;