feat: Mejorar visualización del resumen ejecutivo
- KPIs compactos en una sola tarjeta horizontal (4 métricas) - Health Score con desglose de factores (FCR, AHT, Transferencias, CSAT) - Barras de progreso por cada factor con estado y insight - Insight contextual según el score - Diseño más profesional y menos espacio vacío Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,124 @@
|
|||||||
import { TrendingUp, TrendingDown, Minus, AlertTriangle, CheckCircle, Target } from 'lucide-react';
|
import { TrendingUp, TrendingDown, AlertTriangle, CheckCircle, Target, Activity, Clock, PhoneForwarded, Users } from 'lucide-react';
|
||||||
import { BulletChart } from '../charts/BulletChart';
|
|
||||||
import type { AnalysisData, Finding } from '../../types';
|
import type { AnalysisData, Finding } from '../../types';
|
||||||
|
|
||||||
interface ExecutiveSummaryTabProps {
|
interface ExecutiveSummaryTabProps {
|
||||||
data: AnalysisData;
|
data: AnalysisData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health Score Gauge Component
|
// Compact KPI Row Component
|
||||||
function HealthScoreGauge({ score }: { score: number }) {
|
function KeyMetricsCard({
|
||||||
|
totalInteractions,
|
||||||
|
avgAHT,
|
||||||
|
avgFCR,
|
||||||
|
avgTransferRate,
|
||||||
|
ahtBenchmark,
|
||||||
|
fcrBenchmark
|
||||||
|
}: {
|
||||||
|
totalInteractions: number;
|
||||||
|
avgAHT: number;
|
||||||
|
avgFCR: number;
|
||||||
|
avgTransferRate: number;
|
||||||
|
ahtBenchmark?: number;
|
||||||
|
fcrBenchmark?: number;
|
||||||
|
}) {
|
||||||
|
const formatNumber = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : n.toString();
|
||||||
|
|
||||||
|
const getAHTStatus = (aht: number) => {
|
||||||
|
if (aht <= 300) return { color: 'text-emerald-600', bg: 'bg-emerald-50', label: 'Excelente' };
|
||||||
|
if (aht <= 420) return { color: 'text-emerald-600', bg: 'bg-emerald-50', label: 'Bueno' };
|
||||||
|
if (aht <= 480) return { color: 'text-amber-600', bg: 'bg-amber-50', label: 'Aceptable' };
|
||||||
|
return { color: 'text-red-600', bg: 'bg-red-50', label: 'Alto' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFCRStatus = (fcr: number) => {
|
||||||
|
if (fcr >= 85) return { color: 'text-emerald-600', bg: 'bg-emerald-50', label: 'Excelente' };
|
||||||
|
if (fcr >= 75) return { color: 'text-emerald-600', bg: 'bg-emerald-50', label: 'Bueno' };
|
||||||
|
if (fcr >= 65) return { color: 'text-amber-600', bg: 'bg-amber-50', label: 'Mejorable' };
|
||||||
|
return { color: 'text-red-600', bg: 'bg-red-50', label: 'Crítico' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ahtStatus = getAHTStatus(avgAHT);
|
||||||
|
const fcrStatus = getFCRStatus(avgFCR);
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
label: 'Interacciones',
|
||||||
|
value: formatNumber(totalInteractions),
|
||||||
|
sublabel: 'mensuales',
|
||||||
|
status: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Clock,
|
||||||
|
label: 'AHT Promedio',
|
||||||
|
value: `${Math.floor(avgAHT / 60)}:${String(avgAHT % 60).padStart(2, '0')}`,
|
||||||
|
sublabel: ahtBenchmark ? `Benchmark: ${Math.floor(ahtBenchmark / 60)}:${String(Math.round(ahtBenchmark) % 60).padStart(2, '0')}` : 'min:seg',
|
||||||
|
status: ahtStatus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: CheckCircle,
|
||||||
|
label: 'FCR',
|
||||||
|
value: `${avgFCR}%`,
|
||||||
|
sublabel: fcrBenchmark ? `Benchmark: ${fcrBenchmark}%` : 'Resolución 1er contacto',
|
||||||
|
status: fcrStatus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PhoneForwarded,
|
||||||
|
label: 'Transferencias',
|
||||||
|
value: `${avgTransferRate}%`,
|
||||||
|
sublabel: avgTransferRate > 20 ? 'Requiere atención' : 'Bajo control',
|
||||||
|
status: avgTransferRate > 20
|
||||||
|
? { color: 'text-amber-600', bg: 'bg-amber-50', label: 'Alto' }
|
||||||
|
: { color: 'text-emerald-600', bg: 'bg-emerald-50', label: 'OK' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 divide-x divide-y lg:divide-y-0 divide-slate-100">
|
||||||
|
{metrics.map((metric) => {
|
||||||
|
const Icon = metric.icon;
|
||||||
|
return (
|
||||||
|
<div key={metric.label} className="p-4 hover:bg-slate-50 transition-colors">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Icon className="w-4 h-4 text-[#6D84E3]" />
|
||||||
|
<span className="text-xs font-medium text-slate-500 uppercase tracking-wide">{metric.label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-2xl font-bold text-slate-800">{metric.value}</span>
|
||||||
|
{metric.status && (
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded ${metric.status.bg} ${metric.status.color}`}>
|
||||||
|
{metric.status.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">{metric.sublabel}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health Score with Breakdown
|
||||||
|
function HealthScoreDetailed({
|
||||||
|
score,
|
||||||
|
avgFCR,
|
||||||
|
avgAHT,
|
||||||
|
avgTransferRate,
|
||||||
|
avgCSAT
|
||||||
|
}: {
|
||||||
|
score: number;
|
||||||
|
avgFCR: number;
|
||||||
|
avgAHT: number;
|
||||||
|
avgTransferRate: number;
|
||||||
|
avgCSAT: number;
|
||||||
|
}) {
|
||||||
const getColor = (s: number) => {
|
const getColor = (s: number) => {
|
||||||
if (s >= 80) return '#059669'; // emerald-600
|
if (s >= 80) return '#059669';
|
||||||
if (s >= 60) return '#D97706'; // amber-600
|
if (s >= 60) return '#D97706';
|
||||||
return '#DC2626'; // red-600
|
return '#DC2626';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLabel = (s: number) => {
|
const getLabel = (s: number) => {
|
||||||
@@ -22,79 +129,129 @@ function HealthScoreGauge({ score }: { score: number }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const color = getColor(score);
|
const color = getColor(score);
|
||||||
const circumference = 2 * Math.PI * 45;
|
const circumference = 2 * Math.PI * 40;
|
||||||
const strokeDasharray = `${(score / 100) * circumference} ${circumference}`;
|
const strokeDasharray = `${(score / 100) * circumference} ${circumference}`;
|
||||||
|
|
||||||
|
// Calculate individual factor scores (0-100)
|
||||||
|
const fcrScore = Math.min(100, Math.round((avgFCR / 85) * 100));
|
||||||
|
const ahtScore = Math.min(100, Math.round(Math.max(0, (1 - (avgAHT - 240) / 360) * 100)));
|
||||||
|
const transferScore = Math.min(100, Math.round(Math.max(0, (1 - avgTransferRate / 30) * 100)));
|
||||||
|
const csatScore = avgCSAT;
|
||||||
|
|
||||||
|
const factors = [
|
||||||
|
{
|
||||||
|
name: 'FCR',
|
||||||
|
score: fcrScore,
|
||||||
|
weight: '30%',
|
||||||
|
status: fcrScore >= 80 ? 'good' : fcrScore >= 60 ? 'warning' : 'critical',
|
||||||
|
insight: fcrScore >= 80 ? 'Óptimo' : fcrScore >= 60 ? 'Mejorable' : 'Requiere acción'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Eficiencia (AHT)',
|
||||||
|
score: ahtScore,
|
||||||
|
weight: '25%',
|
||||||
|
status: ahtScore >= 80 ? 'good' : ahtScore >= 60 ? 'warning' : 'critical',
|
||||||
|
insight: ahtScore >= 80 ? 'Óptimo' : ahtScore >= 60 ? 'En rango' : 'Muy alto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Transferencias',
|
||||||
|
score: transferScore,
|
||||||
|
weight: '25%',
|
||||||
|
status: transferScore >= 80 ? 'good' : transferScore >= 60 ? 'warning' : 'critical',
|
||||||
|
insight: transferScore >= 80 ? 'Bajo' : transferScore >= 60 ? 'Moderado' : 'Excesivo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CSAT',
|
||||||
|
score: csatScore,
|
||||||
|
weight: '20%',
|
||||||
|
status: csatScore >= 80 ? 'good' : csatScore >= 60 ? 'warning' : 'critical',
|
||||||
|
insight: csatScore >= 80 ? 'Óptimo' : csatScore >= 60 ? 'Aceptable' : 'Bajo'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
good: 'bg-emerald-500',
|
||||||
|
warning: 'bg-amber-500',
|
||||||
|
critical: 'bg-red-500'
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMainInsight = () => {
|
||||||
|
const weakest = factors.reduce((min, f) => f.score < min.score ? f : min, factors[0]);
|
||||||
|
const strongest = factors.reduce((max, f) => f.score > max.score ? f : max, factors[0]);
|
||||||
|
|
||||||
|
if (score >= 80) {
|
||||||
|
return `Rendimiento destacado en ${strongest.name}. Mantener estándares actuales.`;
|
||||||
|
} else if (score >= 60) {
|
||||||
|
return `Oportunidad de mejora en ${weakest.name} (${weakest.insight.toLowerCase()}).`;
|
||||||
|
} else {
|
||||||
|
return `Priorizar mejora en ${weakest.name}: impacto directo en satisfacción del cliente.`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg p-6 border border-slate-200">
|
<div className="bg-white rounded-lg border border-slate-200 p-5">
|
||||||
<h3 className="font-semibold text-slate-800 mb-4 text-center">Health Score General</h3>
|
<div className="flex items-start gap-5">
|
||||||
<div className="relative w-32 h-32 mx-auto">
|
{/* Gauge */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="relative w-24 h-24">
|
||||||
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
|
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
|
||||||
{/* Background circle */}
|
<circle cx="50" cy="50" r="40" fill="none" stroke="#E2E8F0" strokeWidth="8" />
|
||||||
<circle
|
<circle
|
||||||
cx="50"
|
cx="50" cy="50" r="40" fill="none" stroke={color} strokeWidth="8"
|
||||||
cy="50"
|
strokeLinecap="round" strokeDasharray={strokeDasharray}
|
||||||
r="45"
|
className="transition-all duration-1000"
|
||||||
fill="none"
|
|
||||||
stroke="#E2E8F0"
|
|
||||||
strokeWidth="8"
|
|
||||||
/>
|
|
||||||
{/* Progress circle */}
|
|
||||||
<circle
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="45"
|
|
||||||
fill="none"
|
|
||||||
stroke={color}
|
|
||||||
strokeWidth="8"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
className="transition-all duration-1000 ease-out"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
<span className="text-3xl font-bold" style={{ color }}>{score}</span>
|
<span className="text-2xl font-bold" style={{ color }}>{score}</span>
|
||||||
<span className="text-xs text-slate-500">/100</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-sm font-semibold mt-1" style={{ color }}>{getLabel(score)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Breakdown */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-slate-800 mb-3">Health Score - Desglose</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{factors.map((factor) => (
|
||||||
|
<div key={factor.name} className="flex items-center gap-3">
|
||||||
|
<div className="w-24 text-xs text-slate-600 truncate">{factor.name}</div>
|
||||||
|
<div className="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all ${statusColors[factor.status]}`}
|
||||||
|
style={{ width: `${factor.score}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-10 text-xs text-slate-500 text-right">{factor.score}</div>
|
||||||
|
<div className={`w-16 text-xs ${
|
||||||
|
factor.status === 'good' ? 'text-emerald-600' :
|
||||||
|
factor.status === 'warning' ? 'text-amber-600' : 'text-red-600'
|
||||||
|
}`}>
|
||||||
|
{factor.insight}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Insight */}
|
||||||
|
<div className="mt-4 p-2.5 bg-slate-50 rounded-lg border-l-3 border-[#6D84E3]">
|
||||||
|
<p className="text-xs text-slate-600">
|
||||||
|
<span className="font-semibold text-slate-700">Insight: </span>
|
||||||
|
{getMainInsight()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center mt-3 text-sm font-medium" style={{ color }}>{getLabel(score)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KPI Card Component
|
// Top Opportunities Component
|
||||||
function KpiCard({ label, value, change, changeType }: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
change?: string;
|
|
||||||
changeType?: 'positive' | 'negative' | 'neutral';
|
|
||||||
}) {
|
|
||||||
const ChangeIcon = changeType === 'positive' ? TrendingUp :
|
|
||||||
changeType === 'negative' ? TrendingDown : Minus;
|
|
||||||
|
|
||||||
const changeColor = changeType === 'positive' ? 'text-emerald-600' :
|
|
||||||
changeType === 'negative' ? 'text-red-600' : 'text-slate-500';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg p-4 border border-slate-200">
|
|
||||||
<p className="text-sm text-slate-500 mb-1">{label}</p>
|
|
||||||
<p className="text-2xl font-bold text-slate-800">{value}</p>
|
|
||||||
{change && (
|
|
||||||
<div className={`flex items-center gap-1 mt-1 text-sm ${changeColor}`}>
|
|
||||||
<ChangeIcon className="w-4 h-4" />
|
|
||||||
<span>{change}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top Opportunities Component (McKinsey style)
|
|
||||||
function TopOpportunities({ findings, opportunities }: {
|
function TopOpportunities({ findings, opportunities }: {
|
||||||
findings: Finding[];
|
findings: Finding[];
|
||||||
opportunities: { name: string; impact: number; savings: number }[];
|
opportunities: { name: string; impact: number; savings: number }[];
|
||||||
}) {
|
}) {
|
||||||
// Combine critical findings and high-impact opportunities
|
|
||||||
const items = [
|
const items = [
|
||||||
...findings
|
...findings
|
||||||
.filter(f => f.type === 'critical' || f.type === 'warning')
|
.filter(f => f.type === 'critical' || f.type === 'warning')
|
||||||
@@ -108,13 +265,14 @@ function TopOpportunities({ findings, opportunities }: {
|
|||||||
})),
|
})),
|
||||||
].slice(0, 3);
|
].slice(0, 3);
|
||||||
|
|
||||||
// Fill with opportunities if not enough findings
|
|
||||||
if (items.length < 3) {
|
if (items.length < 3) {
|
||||||
const remaining = 3 - items.length;
|
const remaining = 3 - items.length;
|
||||||
opportunities
|
opportunities
|
||||||
.sort((a, b) => b.savings - a.savings)
|
.sort((a, b) => b.savings - a.savings)
|
||||||
.slice(0, remaining)
|
.slice(0, remaining)
|
||||||
.forEach((opp, i) => {
|
.forEach(() => {
|
||||||
|
const opp = opportunities[items.length];
|
||||||
|
if (opp) {
|
||||||
items.push({
|
items.push({
|
||||||
rank: items.length + 1,
|
rank: items.length + 1,
|
||||||
title: opp.name,
|
title: opp.name,
|
||||||
@@ -122,35 +280,34 @@ function TopOpportunities({ findings, opportunities }: {
|
|||||||
action: 'Implementar',
|
action: 'Implementar',
|
||||||
type: 'info' as const
|
type: 'info' as const
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIcon = (type: string) => {
|
const getIcon = (type: string) => {
|
||||||
if (type === 'critical') return <AlertTriangle className="w-5 h-5 text-red-500" />;
|
if (type === 'critical') return <AlertTriangle className="w-4 h-4 text-red-500" />;
|
||||||
if (type === 'warning') return <Target className="w-5 h-5 text-amber-500" />;
|
if (type === 'warning') return <Target className="w-4 h-4 text-amber-500" />;
|
||||||
return <CheckCircle className="w-5 h-5 text-emerald-500" />;
|
return <CheckCircle className="w-4 h-4 text-emerald-500" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg p-4 border border-slate-200">
|
<div className="bg-white rounded-lg p-4 border border-slate-200">
|
||||||
<h3 className="font-semibold text-slate-800 mb-4">Top 3 Oportunidades</h3>
|
<h3 className="font-semibold text-slate-800 mb-3">Top 3 Oportunidades</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.rank} className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
|
<div key={item.rank} className="flex items-start gap-2 p-2.5 bg-slate-50 rounded-lg">
|
||||||
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center text-sm font-bold text-slate-700">
|
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-slate-200 flex items-center justify-center text-xs font-bold text-slate-600">
|
||||||
{item.rank}
|
{item.rank}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1.5">
|
||||||
{getIcon(item.type)}
|
{getIcon(item.type)}
|
||||||
<span className="font-medium text-slate-800">{item.title}</span>
|
<span className="text-sm font-medium text-slate-700">{item.title}</span>
|
||||||
</div>
|
</div>
|
||||||
{item.metric && (
|
{item.metric && (
|
||||||
<p className="text-sm text-slate-500 mt-0.5">{item.metric}</p>
|
<p className="text-xs text-slate-500 mt-0.5">{item.metric}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-[#6D84E3] mt-1 font-medium">
|
<p className="text-xs text-[#6D84E3] mt-0.5 font-medium">→ {item.action}</p>
|
||||||
→ {item.action}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -159,8 +316,38 @@ function TopOpportunities({ findings, opportunities }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Economic Summary Compact
|
||||||
|
function EconomicSummary({ economicModel }: { economicModel: AnalysisData['economicModel'] }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-4 border border-slate-200">
|
||||||
|
<h3 className="font-semibold text-slate-800 mb-3">Impacto Económico</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||||
|
<div className="p-2.5 bg-slate-50 rounded-lg">
|
||||||
|
<p className="text-xs text-slate-500">Coste Anual</p>
|
||||||
|
<p className="text-lg font-bold text-slate-800">€{(economicModel.currentAnnualCost / 1000).toFixed(0)}K</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 bg-emerald-50 rounded-lg">
|
||||||
|
<p className="text-xs text-emerald-600">Ahorro Potencial</p>
|
||||||
|
<p className="text-lg font-bold text-emerald-700">€{(economicModel.annualSavings / 1000).toFixed(0)}K</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-2.5 bg-[#6D84E3]/10 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-[#6D84E3]">ROI 3 años</p>
|
||||||
|
<p className="text-lg font-bold text-[#6D84E3]">{economicModel.roi3yr}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-slate-500">Payback</p>
|
||||||
|
<p className="text-lg font-bold text-slate-700">{economicModel.paybackMonths}m</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExecutiveSummaryTab({ data }: ExecutiveSummaryTabProps) {
|
export function ExecutiveSummaryTab({ data }: ExecutiveSummaryTabProps) {
|
||||||
// Extract key KPIs for bullet charts
|
|
||||||
const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
|
const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
|
||||||
const avgAHT = data.heatmapData.length > 0
|
const avgAHT = data.heatmapData.length > 0
|
||||||
? Math.round(data.heatmapData.reduce((sum, h) => sum + h.aht_seconds, 0) / data.heatmapData.length)
|
? Math.round(data.heatmapData.reduce((sum, h) => sum + h.aht_seconds, 0) / data.heatmapData.length)
|
||||||
@@ -171,119 +358,41 @@ export function ExecutiveSummaryTab({ data }: ExecutiveSummaryTabProps) {
|
|||||||
const avgTransferRate = data.heatmapData.length > 0
|
const avgTransferRate = data.heatmapData.length > 0
|
||||||
? Math.round(data.heatmapData.reduce((sum, h) => sum + h.metrics.transfer_rate, 0) / data.heatmapData.length)
|
? Math.round(data.heatmapData.reduce((sum, h) => sum + h.metrics.transfer_rate, 0) / data.heatmapData.length)
|
||||||
: 0;
|
: 0;
|
||||||
|
const avgCSAT = data.heatmapData.length > 0
|
||||||
|
? Math.round(data.heatmapData.reduce((sum, h) => sum + h.metrics.csat, 0) / data.heatmapData.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
// Find benchmark data
|
|
||||||
const ahtBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('aht'));
|
const ahtBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('aht'));
|
||||||
const fcrBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('fcr'));
|
const fcrBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('fcr'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* Main Grid: KPIs + Health Score */}
|
{/* Key Metrics Bar */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
<KeyMetricsCard
|
||||||
{/* Summary KPIs */}
|
totalInteractions={totalInteractions}
|
||||||
{data.summaryKpis.slice(0, 3).map((kpi) => (
|
avgAHT={avgAHT}
|
||||||
<KpiCard
|
avgFCR={avgFCR}
|
||||||
key={kpi.label}
|
avgTransferRate={avgTransferRate}
|
||||||
label={kpi.label}
|
ahtBenchmark={ahtBenchmark?.industryValue}
|
||||||
value={kpi.value}
|
fcrBenchmark={fcrBenchmark?.industryValue}
|
||||||
change={kpi.change}
|
|
||||||
changeType={kpi.changeType}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Health Score Gauge */}
|
|
||||||
<HealthScoreGauge score={data.overallHealthScore} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bullet Charts Row */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<BulletChart
|
|
||||||
label="Total Interacciones"
|
|
||||||
actual={totalInteractions}
|
|
||||||
target={totalInteractions * 1.1}
|
|
||||||
ranges={[totalInteractions * 0.7, totalInteractions * 0.9, totalInteractions * 1.3]}
|
|
||||||
formatValue={(v) => v >= 1000 ? `${(v / 1000).toFixed(1)}K` : v.toString()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BulletChart
|
{/* Health Score with Breakdown */}
|
||||||
label="AHT"
|
<HealthScoreDetailed
|
||||||
actual={avgAHT}
|
score={data.overallHealthScore}
|
||||||
target={ahtBenchmark?.industryValue || 360}
|
avgFCR={avgFCR}
|
||||||
ranges={[480, 420, 600]} // >480s poor, 420-480 ok, <420 good
|
avgAHT={avgAHT}
|
||||||
unit="s"
|
avgTransferRate={avgTransferRate}
|
||||||
percentile={ahtBenchmark?.percentile}
|
avgCSAT={avgCSAT}
|
||||||
inverse={true}
|
|
||||||
formatValue={(v) => v.toString()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BulletChart
|
{/* Bottom Row */}
|
||||||
label="FCR"
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
actual={avgFCR}
|
|
||||||
target={fcrBenchmark?.industryValue || 75}
|
|
||||||
ranges={[65, 75, 100]} // <65 poor, 65-75 ok, >75 good
|
|
||||||
unit="%"
|
|
||||||
percentile={fcrBenchmark?.percentile}
|
|
||||||
formatValue={(v) => v.toString()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BulletChart
|
|
||||||
label="Tasa Transferencia"
|
|
||||||
actual={avgTransferRate}
|
|
||||||
target={15}
|
|
||||||
ranges={[25, 15, 40]} // >25% poor, 15-25 ok, <15 good
|
|
||||||
unit="%"
|
|
||||||
inverse={true}
|
|
||||||
formatValue={(v) => v.toString()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Row: Top Opportunities + Economic Summary */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<TopOpportunities
|
<TopOpportunities
|
||||||
findings={data.findings}
|
findings={data.findings}
|
||||||
opportunities={data.opportunities}
|
opportunities={data.opportunities}
|
||||||
/>
|
/>
|
||||||
|
<EconomicSummary economicModel={data.economicModel} />
|
||||||
{/* Economic Impact Summary */}
|
|
||||||
<div className="bg-white rounded-lg p-4 border border-slate-200">
|
|
||||||
<h3 className="font-semibold text-slate-800 mb-4">Impacto Económico</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="p-3 bg-slate-50 rounded-lg">
|
|
||||||
<p className="text-sm text-slate-500">Coste Anual Actual</p>
|
|
||||||
<p className="text-xl font-bold text-slate-800">
|
|
||||||
€{data.economicModel.currentAnnualCost.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-emerald-50 rounded-lg">
|
|
||||||
<p className="text-sm text-emerald-600">Ahorro Potencial</p>
|
|
||||||
<p className="text-xl font-bold text-emerald-700">
|
|
||||||
€{data.economicModel.annualSavings.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-slate-50 rounded-lg">
|
|
||||||
<p className="text-sm text-slate-500">Inversión Inicial</p>
|
|
||||||
<p className="text-xl font-bold text-slate-800">
|
|
||||||
€{data.economicModel.initialInvestment.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-[#6D84E3]/10 rounded-lg">
|
|
||||||
<p className="text-sm text-[#6D84E3]">ROI a 3 Años</p>
|
|
||||||
<p className="text-xl font-bold text-[#6D84E3]">
|
|
||||||
{data.economicModel.roi3yr}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payback indicator */}
|
|
||||||
<div className="mt-4 p-3 bg-gradient-to-r from-emerald-50 to-emerald-100 rounded-lg">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm text-emerald-700">Payback</span>
|
|
||||||
<span className="font-bold text-emerald-800">
|
|
||||||
{data.economicModel.paybackMonths} meses
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user