From 522b4b6caaebe9dca7e2b4a7aec98af693213b03 Mon Sep 17 00:00:00 2001 From: Susana Date: Mon, 12 Jan 2026 09:03:47 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Mejorar=20visualizaci=C3=B3n=20del=20re?= =?UTF-8?q?sumen=20ejecutivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../components/tabs/ExecutiveSummaryTab.tsx | 493 +++++++++++------- 1 file changed, 301 insertions(+), 192 deletions(-) diff --git a/frontend/components/tabs/ExecutiveSummaryTab.tsx b/frontend/components/tabs/ExecutiveSummaryTab.tsx index f48b91f..43e783d 100644 --- a/frontend/components/tabs/ExecutiveSummaryTab.tsx +++ b/frontend/components/tabs/ExecutiveSummaryTab.tsx @@ -1,17 +1,124 @@ -import { TrendingUp, TrendingDown, Minus, AlertTriangle, CheckCircle, Target } from 'lucide-react'; -import { BulletChart } from '../charts/BulletChart'; +import { TrendingUp, TrendingDown, AlertTriangle, CheckCircle, Target, Activity, Clock, PhoneForwarded, Users } from 'lucide-react'; import type { AnalysisData, Finding } from '../../types'; interface ExecutiveSummaryTabProps { data: AnalysisData; } -// Health Score Gauge Component -function HealthScoreGauge({ score }: { score: number }) { +// Compact KPI Row Component +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 ( +
+
+ {metrics.map((metric) => { + const Icon = metric.icon; + return ( +
+
+ + {metric.label} +
+
+ {metric.value} + {metric.status && ( + + {metric.status.label} + + )} +
+

{metric.sublabel}

+
+ ); + })} +
+
+ ); +} + +// 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) => { - if (s >= 80) return '#059669'; // emerald-600 - if (s >= 60) return '#D97706'; // amber-600 - return '#DC2626'; // red-600 + if (s >= 80) return '#059669'; + if (s >= 60) return '#D97706'; + return '#DC2626'; }; const getLabel = (s: number) => { @@ -22,79 +129,129 @@ function HealthScoreGauge({ score }: { score: number }) { }; const color = getColor(score); - const circumference = 2 * Math.PI * 45; + const circumference = 2 * Math.PI * 40; 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 ( -
-

Health Score General

-
- - {/* Background circle */} - - {/* Progress circle */} - - -
- {score} - /100 +
+
+ {/* Gauge */} +
+
+ + + + +
+ {score} +
+
+

{getLabel(score)}

+
+ + {/* Breakdown */} +
+

Health Score - Desglose

+ +
+ {factors.map((factor) => ( +
+
{factor.name}
+
+
+
+
{factor.score}
+
+ {factor.insight} +
+
+ ))} +
+ + {/* Key Insight */} +
+

+ Insight: + {getMainInsight()} +

+
-

{getLabel(score)}

); } -// KPI Card 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 ( -
-

{label}

-

{value}

- {change && ( -
- - {change} -
- )} -
- ); -} - -// Top Opportunities Component (McKinsey style) +// Top Opportunities Component function TopOpportunities({ findings, opportunities }: { findings: Finding[]; opportunities: { name: string; impact: number; savings: number }[]; }) { - // Combine critical findings and high-impact opportunities const items = [ ...findings .filter(f => f.type === 'critical' || f.type === 'warning') @@ -108,49 +265,49 @@ function TopOpportunities({ findings, opportunities }: { })), ].slice(0, 3); - // Fill with opportunities if not enough findings if (items.length < 3) { const remaining = 3 - items.length; opportunities .sort((a, b) => b.savings - a.savings) .slice(0, remaining) - .forEach((opp, i) => { - items.push({ - rank: items.length + 1, - title: opp.name, - metric: `€${opp.savings.toLocaleString()} ahorro potencial`, - action: 'Implementar', - type: 'info' as const - }); + .forEach(() => { + const opp = opportunities[items.length]; + if (opp) { + items.push({ + rank: items.length + 1, + title: opp.name, + metric: `€${opp.savings.toLocaleString()} ahorro potencial`, + action: 'Implementar', + type: 'info' as const + }); + } }); } const getIcon = (type: string) => { - if (type === 'critical') return ; - if (type === 'warning') return ; - return ; + if (type === 'critical') return ; + if (type === 'warning') return ; + return ; }; return (
-

Top 3 Oportunidades

-
+

Top 3 Oportunidades

+
{items.map((item) => ( -
-
+
+
{item.rank}
-
+
{getIcon(item.type)} - {item.title} + {item.title}
{item.metric && ( -

{item.metric}

+

{item.metric}

)} -

- → {item.action} -

+

→ {item.action}

))} @@ -159,8 +316,38 @@ function TopOpportunities({ findings, opportunities }: { ); } +// Economic Summary Compact +function EconomicSummary({ economicModel }: { economicModel: AnalysisData['economicModel'] }) { + return ( +
+

Impacto Económico

+ +
+
+

Coste Anual

+

€{(economicModel.currentAnnualCost / 1000).toFixed(0)}K

+
+
+

Ahorro Potencial

+

€{(economicModel.annualSavings / 1000).toFixed(0)}K

+
+
+ +
+
+

ROI 3 años

+

{economicModel.roi3yr}%

+
+
+

Payback

+

{economicModel.paybackMonths}m

+
+
+
+ ); +} + export function ExecutiveSummaryTab({ data }: ExecutiveSummaryTabProps) { - // Extract key KPIs for bullet charts const totalInteractions = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); const avgAHT = data.heatmapData.length > 0 ? 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 ? Math.round(data.heatmapData.reduce((sum, h) => sum + h.metrics.transfer_rate, 0) / data.heatmapData.length) : 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 fcrBenchmark = data.benchmarkData.find(b => b.kpi.toLowerCase().includes('fcr')); return ( -
- {/* Main Grid: KPIs + Health Score */} -
- {/* Summary KPIs */} - {data.summaryKpis.slice(0, 3).map((kpi) => ( - - ))} +
+ {/* Key Metrics Bar */} + - {/* Health Score Gauge */} - -
+ {/* Health Score with Breakdown */} + - {/* Bullet Charts Row */} -
- v >= 1000 ? `${(v / 1000).toFixed(1)}K` : v.toString()} - /> - - 480s poor, 420-480 ok, <420 good - unit="s" - percentile={ahtBenchmark?.percentile} - inverse={true} - formatValue={(v) => v.toString()} - /> - - 75 good - unit="%" - percentile={fcrBenchmark?.percentile} - formatValue={(v) => v.toString()} - /> - - 25% poor, 15-25 ok, <15 good - unit="%" - inverse={true} - formatValue={(v) => v.toString()} - /> -
- - {/* Bottom Row: Top Opportunities + Economic Summary */} -
+ {/* Bottom Row */} +
- - {/* Economic Impact Summary */} -
-

Impacto Económico

-
-
-

Coste Anual Actual

-

- €{data.economicModel.currentAnnualCost.toLocaleString()} -

-
-
-

Ahorro Potencial

-

- €{data.economicModel.annualSavings.toLocaleString()} -

-
-
-

Inversión Inicial

-

- €{data.economicModel.initialInvestment.toLocaleString()} -

-
-
-

ROI a 3 Años

-

- {data.economicModel.roi3yr}% -

-
-
- - {/* Payback indicator */} -
-
- Payback - - {data.economicModel.paybackMonths} meses - -
-
-
+
);