420 lines
19 KiB
TypeScript
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 > 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 < 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;
|