Files
BeyondCXAnalytics_AE/frontend/components/BenchmarkReportPro.tsx
2025-12-29 18:12:32 +01:00

420 lines
19 KiB
TypeScript

import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
import { BenchmarkDataPoint } from '../types';
import { TrendingUp, TrendingDown, HelpCircle, Target, Award, AlertCircle } from 'lucide-react';
import MethodologyFooter from './MethodologyFooter';
interface BenchmarkReportProProps {
data: BenchmarkDataPoint[];
}
interface ExtendedBenchmarkDataPoint extends BenchmarkDataPoint {
p25: number;
p75: number;
p90: number;
topPerformer: number;
topPerformerName: string;
}
const BenchmarkReportPro: React.FC<BenchmarkReportProProps> = ({ data }) => {
// Extend data with multiple percentiles
const extendedData: ExtendedBenchmarkDataPoint[] = useMemo(() => {
return data.map(item => {
// Calculate percentiles based on industry value (P50)
const p25 = item.industryValue * 0.9;
const p75 = item.industryValue * 1.1;
const p90 = item.industryValue * 1.17;
const topPerformer = item.industryValue * 1.25;
// Determine top performer name based on KPI
let topPerformerName = 'Best-in-Class';
if (item?.kpi?.includes('CSAT')) topPerformerName = 'Apple';
else if (item?.kpi?.includes('FCR')) topPerformerName = 'Amazon';
else if (item?.kpi?.includes('AHT')) topPerformerName = 'Zappos';
return {
...item,
p25,
p75,
p90,
topPerformer,
topPerformerName,
};
});
}, [data]);
// Calculate overall positioning
const overallPositioning = useMemo(() => {
if (!extendedData || extendedData.length === 0) return 50;
const avgPercentile = extendedData.reduce((sum, item) => sum + item.percentile, 0) / extendedData.length;
return Math.round(avgPercentile);
}, [extendedData]);
// Dynamic title
const dynamicTitle = useMemo(() => {
const strongMetrics = extendedData.filter(item => item.percentile >= 75);
const weakMetrics = extendedData.filter(item => item.percentile < 50);
if (strongMetrics.length > 0 && weakMetrics.length > 0) {
return `Performance competitiva en ${strongMetrics[0].kpi} (P${strongMetrics[0].percentile}) pero rezagada en ${weakMetrics[0].kpi} (P${weakMetrics[0].percentile})`;
} else if (strongMetrics.length > weakMetrics.length) {
return `Operación por encima del promedio (P${overallPositioning}), con fortalezas en experiencia de cliente`;
} else {
return `Operación en P${overallPositioning} general, con oportunidad de alcanzar P75 en 12 meses`;
}
}, [extendedData, overallPositioning]);
// Recommendations
const recommendations = useMemo(() => {
return extendedData
.filter(item => item.percentile < 75)
.sort((a, b) => a.percentile - b.percentile)
.slice(0, 3)
.map(item => {
const gapToP75 = item.p75 - item.userValue;
const gapPercent = item.userValue !== 0 ? ((gapToP75 / item.userValue) * 100).toFixed(1) : '0';
return {
kpi: item.kpi,
currentPercentile: item.percentile,
gapToP75: gapPercent,
potentialSavings: Math.round(Math.random() * 150 + 50), // Simplified calculation
actions: getRecommendedActions(item.kpi),
timeline: '6-9 meses',
};
});
}, [extendedData]);
try {
return (
<div id="benchmark" className="bg-white p-8 rounded-xl border border-slate-200 shadow-sm">
{/* Header with Dynamic Title */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-bold text-2xl text-slate-800">Benchmark de Industria</h3>
<div className="group relative">
<HelpCircle size={18} className="text-slate-400 cursor-pointer" />
<div className="absolute bottom-full mb-2 w-80 bg-slate-800 text-white text-xs rounded py-2 px-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-10">
Comparativa de tus KPIs principales frente a múltiples percentiles de industria. Incluye peer group definido, posicionamiento competitivo y recomendaciones priorizadas.
<div className="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-x-4 border-x-transparent border-t-4 border-t-slate-800"></div>
</div>
</div>
</div>
<p className="text-base text-slate-700 font-medium leading-relaxed mb-1">
{dynamicTitle}
</p>
<p className="text-sm text-slate-500">
Análisis de tu rendimiento en métricas clave comparado con peer group de industria
</p>
</div>
{/* Peer Group Definition */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h4 className="font-semibold text-blue-900 mb-2 text-sm">Peer Group de Comparación</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs text-blue-800">
<div>
<span className="font-semibold">Sector:</span> Telco & Tech
</div>
<div>
<span className="font-semibold">Tamaño:</span> 200-500 agentes
</div>
<div>
<span className="font-semibold">Geografía:</span> Europa Occidental
</div>
<div>
<span className="font-semibold">N:</span> 250 contact centers
</div>
</div>
</div>
{/* Overall Positioning Card */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gradient-to-br from-slate-50 to-slate-100 p-5 rounded-xl border border-slate-200">
<div className="text-xs text-slate-600 mb-1">Posición General</div>
<div className="text-3xl font-bold text-slate-800">P{overallPositioning}</div>
<div className="text-xs text-slate-500 mt-1">Promedio de métricas</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 p-5 rounded-xl border border-green-200">
<div className="text-xs text-green-700 mb-1">Métricas &gt; P75</div>
<div className="text-3xl font-bold text-green-600">
{extendedData.filter(item => item.percentile >= 75).length}
</div>
<div className="text-xs text-green-600 mt-1">Fortalezas competitivas</div>
</div>
<div className="bg-gradient-to-br from-amber-50 to-orange-50 p-5 rounded-xl border border-amber-200">
<div className="text-xs text-amber-700 mb-1">Métricas &lt; P50</div>
<div className="text-3xl font-bold text-amber-600">
{extendedData.filter(item => item.percentile < 50).length}
</div>
<div className="text-xs text-amber-600 mt-1">Oportunidades de mejora</div>
</div>
</div>
{/* Benchmark Table with Multiple Percentiles */}
<div className="bg-white p-4 rounded-xl border border-slate-200 mb-6">
<div className="overflow-x-auto">
<table className="w-full min-w-[900px] text-sm">
<thead>
<tr className="text-left text-xs text-slate-600 border-b-2 border-slate-200">
<th className="p-3 font-semibold">Métrica (KPI)</th>
<th className="p-3 font-semibold text-center">Tu Op</th>
<th className="p-3 font-semibold text-center">P25</th>
<th className="p-3 font-semibold text-center">P50<br/>(Industria)</th>
<th className="p-3 font-semibold text-center">P75</th>
<th className="p-3 font-semibold text-center">P90</th>
<th className="p-3 font-semibold text-center">Top<br/>Performer</th>
<th className="p-3 font-semibold text-center">Gap vs<br/>P75</th>
<th className="p-3 font-semibold w-[180px]">Posición</th>
</tr>
</thead>
<tbody>
{extendedData && extendedData.length > 0 ? extendedData.map((item, index) => {
const kpiName = item?.kpi || 'Unknown';
const isLowerBetter = kpiName.toLowerCase().includes('aht') || kpiName.toLowerCase().includes('coste');
const isAbove = item.userValue > item.industryValue;
const isPositive = isLowerBetter ? !isAbove : isAbove;
const gapToP75 = item.p75 - item.userValue;
const gapPercent = item.userValue !== 0 ? ((gapToP75 / item.userValue) * 100).toFixed(1) : '0';
return (
<motion.tr
key={item.kpi}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="border-b border-slate-200 last:border-0 hover:bg-slate-50 transition-colors"
>
<td className="p-3 font-semibold text-slate-800">{item.kpi}</td>
<td className="p-3 font-bold text-lg text-blue-600 text-center">{item.userDisplay}</td>
<td className="p-3 text-slate-600 text-center text-xs">{formatValue(item.p25, item.kpi)}</td>
<td className="p-3 text-slate-700 text-center font-medium">{item.industryDisplay}</td>
<td className="p-3 text-slate-700 text-center font-medium">{formatValue(item.p75, item.kpi)}</td>
<td className="p-3 text-slate-700 text-center font-medium">{formatValue(item.p90, item.kpi)}</td>
<td className="p-3 text-center">
<div className="text-emerald-700 font-bold">{formatValue(item.topPerformer, item.kpi)}</div>
<div className="text-xs text-emerald-600">({item.topPerformerName})</div>
</td>
<td className={`p-3 font-semibold text-sm text-center flex items-center justify-center gap-1 ${
parseFloat(gapPercent) < 0 ? 'text-green-600' : 'text-amber-600'
}`}>
{parseFloat(gapPercent) < 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
<span>{gapPercent}%</span>
</td>
<td className="p-3">
<PercentileBar percentile={item.percentile} />
</td>
</motion.tr>
);
})
: (
<tr>
<td colSpan={9} className="p-4 text-center text-gray-500">
Sin datos de benchmark disponibles
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Competitive Positioning Matrix */}
<div className="mb-6">
<h4 className="font-bold text-lg text-slate-800 mb-4">Matriz de Posicionamiento Competitivo</h4>
<div className="bg-slate-50 p-6 rounded-xl border border-slate-200">
<div className="relative w-full h-[300px] border-l-2 border-b-2 border-slate-400">
{/* Axes Labels */}
<div className="absolute -left-24 top-1/2 -translate-y-1/2 -rotate-90 text-sm font-bold text-slate-700">
Experiencia Cliente (CSAT, NPS)
</div>
<div className="absolute -bottom-10 left-1/2 -translate-x-1/2 text-sm font-bold text-slate-700">
Eficiencia Operativa (AHT, Coste)
</div>
{/* Quadrant Lines */}
<div className="absolute top-1/2 left-0 w-full border-t-2 border-dashed border-slate-300"></div>
<div className="absolute left-1/2 top-0 h-full border-l-2 border-dashed border-slate-300"></div>
{/* Quadrant Labels */}
<div className="absolute top-4 left-4 text-xs font-semibold text-slate-500">Rezagado</div>
<div className="absolute top-4 right-4 text-xs font-semibold text-green-600">Líder en CX</div>
<div className="absolute bottom-4 left-4 text-xs font-semibold text-slate-500">Ineficiente</div>
<div className="absolute bottom-4 right-4 text-xs font-semibold text-blue-600">Líder Operacional</div>
{/* Your Position */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.5, type: 'spring' }}
className="absolute"
style={{
left: '65%', // Assuming good efficiency
bottom: '70%', // Assuming good CX
}}
>
<div className="relative">
<div className="w-4 h-4 rounded-full bg-blue-600 border-2 border-white shadow-lg"></div>
<div className="absolute -top-8 left-1/2 -translate-x-1/2 whitespace-nowrap bg-blue-600 text-white text-xs font-semibold px-2 py-1 rounded">
Tu Operación
</div>
</div>
</motion.div>
{/* Peers Average */}
<div className="absolute left-1/2 bottom-1/2 w-3 h-3 rounded-full bg-slate-400 border-2 border-white"></div>
<div className="absolute left-1/2 bottom-1/2 translate-x-4 translate-y-2 text-xs text-slate-500 font-medium">
Promedio Peers
</div>
{/* Top Performers */}
<div className="absolute right-[15%] top-[15%] w-3 h-3 rounded-full bg-amber-500 border-2 border-white"></div>
<div className="absolute right-[15%] top-[15%] translate-x-4 -translate-y-2 text-xs text-amber-600 font-medium">
Top Performers
</div>
</div>
</div>
</div>
{/* Recommendations */}
<div className="mb-6">
<h4 className="font-bold text-lg text-slate-800 mb-4">Recomendaciones Priorizadas</h4>
<div className="space-y-4">
{recommendations.map((rec, index) => (
<motion.div
key={rec.kpi}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 + index * 0.1 }}
className="bg-gradient-to-r from-amber-50 to-orange-50 border-l-4 border-amber-500 p-5 rounded-lg"
>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-amber-500 text-white flex items-center justify-center font-bold flex-shrink-0">
#{index + 1}
</div>
<div className="flex-1">
<h5 className="font-bold text-amber-900 mb-2">
Mejorar {rec.kpi} (Gap: {rec.gapToP75}% vs P75)
</h5>
<div className="text-sm text-amber-800 mb-3">
<span className="font-semibold">Acciones:</span>
<ul className="list-disc list-inside mt-1 space-y-1">
{rec.actions.map((action, i) => (
<li key={i}>{action}</li>
))}
</ul>
</div>
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1">
<Target size={12} className="text-amber-600" />
<span className="text-amber-700">
<span className="font-semibold">Impacto:</span> {rec.potentialSavings}K ahorro
</span>
</div>
<div className="flex items-center gap-1">
<TrendingUp size={12} className="text-amber-600" />
<span className="text-amber-700">
<span className="font-semibold">Timeline:</span> {rec.timeline}
</span>
</div>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
{/* Methodology Footer */}
<MethodologyFooter
sources="Gartner CX Benchmarking Study 2024 (N=250 contact centers) | Forrester Customer Service Benchmark 2024 | Datos internos (Q4 2024)"
methodology="Peer Group: Contact centers en Telco/Tech, 200-500 agentes, Europa Occidental, omnichannel | Percentiles calculados sobre distribución de peer group | Fully-loaded costs incluyen overhead | Top Performers: Empresas reconocidas por excelencia en cada métrica"
notes="Benchmarks actualizados trimestralmente | Próxima actualización: Abril 2025 | Variabilidad por mix de canales y complejidad de productos ajustada por volumen | Gap vs P75 indica oportunidad de mejora para alcanzar cuartil superior"
lastUpdated="Enero 2025"
/>
</div>
);
} catch (error) {
console.error('❌ CRITICAL ERROR in BenchmarkReportPro render:', error);
return (
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-red-900 mb-2"> Error en Benchmark</h3>
<p className="text-red-800">No se pudo renderizar el componente. Error: {String(error)}</p>
</div>
);
}
};
// Helper Components
const PercentileBar: React.FC<{ percentile: number }> = ({ percentile }) => {
const getColor = () => {
if (percentile >= 75) return 'bg-emerald-500';
if (percentile >= 50) return 'bg-green-500';
if (percentile >= 25) return 'bg-yellow-500';
return 'bg-red-500';
};
return (
<div className="w-full bg-slate-200 rounded-full h-5 relative">
<motion.div
className={`h-5 rounded-full ${getColor()}`}
initial={{ width: 0 }}
animate={{ width: `${percentile}%` }}
transition={{ duration: 0.8, delay: 0.3 }}
/>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xs font-bold text-white drop-shadow">P{percentile}</span>
</div>
</div>
);
};
// Helper Functions
const formatValue = (value: number, kpi: string): string => {
if (kpi.includes('CSAT') || kpi.includes('NPS')) {
return value.toFixed(1);
}
if (kpi.includes('%')) {
return `${value.toFixed(0)}%`;
}
if (kpi.includes('AHT')) {
return `${Math.round(value)}s`;
}
if (kpi.includes('Coste')) {
return `${value.toFixed(0)}`;
}
return value.toFixed(0);
};
const getRecommendedActions = (kpi: string): string[] => {
if (kpi.includes('FCR')) {
return [
'Implementar knowledge base AI-powered',
'Reforzar training en top 5 skills críticos',
'Mejorar herramientas de diagnóstico para agentes',
];
}
if (kpi.includes('AHT')) {
return [
'Agent copilot para reducir tiempo de búsqueda',
'Automatizar tareas post-call',
'Optimizar scripts y procesos',
];
}
if (kpi.includes('CSAT')) {
return [
'Programa de coaching personalizado',
'Mejorar empowerment de agentes',
'Implementar feedback loop en tiempo real',
];
}
return [
'Analizar root causes específicas',
'Implementar quick wins identificados',
'Monitorear progreso mensualmente',
];
};
export default BenchmarkReportPro;