import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
X, ShieldCheck, Database, RefreshCw, Tag, BarChart3,
ArrowRight, BadgeCheck, Download, ArrowLeftRight, Layers
} from 'lucide-react';
import type { AnalysisData, HeatmapDataPoint } from '../types';
interface MetodologiaDrawerProps {
isOpen: boolean;
onClose: () => void;
data: AnalysisData;
}
interface DataSummary {
totalRegistros: number;
mesesHistorico: number;
periodo: string;
fuente: string;
taxonomia: {
valid: number;
noise: number;
zombie: number;
abandon: number;
};
kpis: {
fcrTecnico: number;
fcrReal: number;
abandonoTradicional: number;
abandonoReal: number;
ahtLimpio: number;
skillsTecnicos: number;
skillsNegocio: number;
};
}
// ========== SUBSECCIONES ==========
function DataSummarySection({ data }: { data: DataSummary }) {
return (
Datos Procesados
{data.totalRegistros.toLocaleString('es-ES')}
Registros analizados
{data.mesesHistorico}
Meses de histórico
{data.fuente}
Sistema origen
Periodo: {data.periodo}
);
}
function PipelineSection() {
const steps = [
{
layer: 'Layer 0',
name: 'Raw Data',
desc: 'Ingesta y Normalización',
color: 'bg-gray-100 border-gray-300'
},
{
layer: 'Layer 1',
name: 'Trusted Data',
desc: 'Higiene y Clasificación',
color: 'bg-yellow-50 border-yellow-300'
},
{
layer: 'Layer 2',
name: 'Business Insights',
desc: 'Enriquecimiento',
color: 'bg-green-50 border-green-300'
},
{
layer: 'Output',
name: 'Dashboard',
desc: 'Visualización',
color: 'bg-blue-50 border-blue-300'
}
];
return (
Pipeline de Transformación
{steps.map((step, index) => (
{step.layer}
{step.name}
{step.desc}
{index < steps.length - 1 && (
)}
))}
Arquitectura modular de 3 capas para garantizar trazabilidad y escalabilidad.
);
}
function TaxonomySection({ data }: { data: DataSummary['taxonomia'] }) {
const rows = [
{
status: 'VALID',
pct: data.valid,
def: 'Duración 10s - 3h. Interacciones reales.',
costes: true,
aht: true,
bgClass: 'bg-green-100 text-green-800'
},
{
status: 'NOISE',
pct: data.noise,
def: 'Duración <10s (no abandono). Ruido técnico.',
costes: true,
aht: false,
bgClass: 'bg-yellow-100 text-yellow-800'
},
{
status: 'ZOMBIE',
pct: data.zombie,
def: 'Duración >3h. Error de sistema.',
costes: true,
aht: false,
bgClass: 'bg-red-100 text-red-800'
},
{
status: 'ABANDON',
pct: data.abandon,
def: 'Desconexión externa + Talk ≤5s.',
costes: false,
aht: false,
bgClass: 'bg-gray-100 text-gray-800'
}
];
return (
Taxonomía de Calidad de Datos
En lugar de eliminar registros, aplicamos "Soft Delete" con etiquetado de calidad
para permitir doble visión: financiera (todos los costes) y operativa (KPIs limpios).
Estado
%
Definición
Costes
AHT
{rows.map((row, idx) => (
{row.status}
{row.pct.toFixed(1)}%
{row.def}
{row.costes ? (
✓ Suma
) : (
✗ No
)}
{row.aht ? (
✓ Promedio
) : (
✗ Excluye
)}
))}
);
}
function KPIRedefinitionSection({ kpis }: { kpis: DataSummary['kpis'] }) {
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
KPIs Redefinidos
Hemos redefinido los KPIs para eliminar los "puntos ciegos" de las métricas tradicionales.
{/* FCR */}
FCR Real vs FCR Técnico
El hallazgo más crítico del diagnóstico.
{kpis.fcrReal}%
FCR Técnico (sin transferencia):
~{kpis.fcrTecnico}%
FCR Real (sin recontacto 7 días):
{kpis.fcrReal}%
💡 ~{kpis.fcrTecnico - kpis.fcrReal}% de "casos resueltos" generan segunda llamada, disparando costes ocultos.
{/* Abandono */}
Tasa de Abandono Real
Fórmula: Desconexión Externa + Talk ≤5 segundos
{kpis.abandonoReal.toFixed(1)}%
💡 El umbral de 5s captura al cliente que cuelga al escuchar la locución o en el timbre.
{/* AHT */}
AHT Limpio
Excluye NOISE (<10s) y ZOMBIE (>3h) del promedio.
{formatTime(kpis.ahtLimpio)}
💡 El AHT sin filtrar estaba distorsionado por errores de sistema.
);
}
function CPICalculationSection({ totalCost, totalVolume, costPerHour = 20 }: { totalCost: number; totalVolume: number; costPerHour?: number }) {
// Productivity factor: agents are ~70% productive (rest is breaks, training, after-call work, etc.)
const effectiveProductivity = 0.70;
// CPI = Total Cost / Total Volume
// El coste total ya incluye: TODOS los registros (noise + zombie + valid) y el factor de productividad
const cpi = totalVolume > 0 ? totalCost / totalVolume : 0;
return (
Coste por Interacción (CPI)
El CPI se calcula dividiendo el coste total entre el volumen de interacciones .
El coste total incluye todas las interacciones (noise, zombie y válidas) porque todas se facturan,
y aplica un factor de productividad del {(effectiveProductivity * 100).toFixed(0)}%.
{/* Fórmula visual */}
Fórmula de Cálculo
CPI
=
Coste Total
÷
Volumen Total
El coste total usa (AHT segundos ÷ 3600) × coste/hora × volumen ÷ productividad
{/* Cómo se calcula el coste total */}
¿Cómo se calcula el Coste Total?
Coste =
(AHT seg ÷ 3600)
×
€{costPerHour}/h
×
Volumen
÷
{(effectiveProductivity * 100).toFixed(0)}%
El AHT está en segundos, se convierte a horas dividiendo por 3600.
Incluye todas las interacciones que generan coste (noise + zombie + válidas).
Solo se excluyen los abandonos porque no consumen tiempo de agente.
{/* Componentes del coste horario */}
Coste por Hora del Agente (Fully Loaded)
Valor introducido: €{costPerHour.toFixed(2)}/h
Este valor fue configurado en la pantalla de entrada de datos y debe incluir todos los costes asociados al agente:
•
Salario bruto del agente
•
Costes de seguridad social
•
Licencias de software
•
Infraestructura y puesto
•
Supervisión y QA
•
Formación y overhead
💡 Si necesita ajustar este valor, puede volver a la pantalla de entrada de datos y modificarlo.
);
}
function BeforeAfterSection({ kpis }: { kpis: DataSummary['kpis'] }) {
const rows = [
{
metric: 'FCR',
tradicional: `${kpis.fcrTecnico}%`,
beyond: `${kpis.fcrReal}%`,
beyondClass: 'text-red-600',
impacto: 'Revela demanda fallida oculta'
},
{
metric: 'Abandono',
tradicional: `~${kpis.abandonoTradicional}%`,
beyond: `${kpis.abandonoReal.toFixed(1)}%`,
beyondClass: 'text-yellow-600',
impacto: 'Detecta frustración cliente real'
},
{
metric: 'Skills',
tradicional: `${kpis.skillsTecnicos} técnicos`,
beyond: `${kpis.skillsNegocio} líneas negocio`,
beyondClass: 'text-blue-600',
impacto: 'Visión ejecutiva accionable'
},
{
metric: 'AHT',
tradicional: 'Distorsionado',
beyond: 'Limpio',
beyondClass: 'text-green-600',
impacto: 'KPIs reflejan desempeño real'
}
];
return (
Impacto de la Transformación
Métrica
Visión Tradicional
Visión Beyond
Impacto
{rows.map((row, idx) => (
{row.metric}
{row.tradicional}
{row.beyond}
{row.impacto}
))}
💡 Sin esta transformación, las decisiones de automatización
se basarían en datos incorrectos, generando inversiones en los procesos equivocados.
);
}
function SkillsMappingSection({ numSkillsNegocio }: { numSkillsNegocio: number }) {
const mappings = [
{
lineaNegocio: 'Baggage & Handling',
keywords: 'HANDLING, EQUIPAJE, AHL (Lost & Found), DPR (Daños)',
color: 'bg-amber-100 text-amber-800'
},
{
lineaNegocio: 'Sales & Booking',
keywords: 'COMPRA, VENTA, RESERVA, PAGO',
color: 'bg-blue-100 text-blue-800'
},
{
lineaNegocio: 'Loyalty (SUMA)',
keywords: 'SUMA (Programa de Fidelización)',
color: 'bg-purple-100 text-purple-800'
},
{
lineaNegocio: 'B2B & Agencies',
keywords: 'AGENCIAS, AAVV, EMPRESAS, AVORIS, TOUROPERACION',
color: 'bg-cyan-100 text-cyan-800'
},
{
lineaNegocio: 'Changes & Post-Sales',
keywords: 'MODIFICACION, CAMBIO, POSTVENTA, REFUND, REEMBOLSO',
color: 'bg-orange-100 text-orange-800'
},
{
lineaNegocio: 'Digital Support',
keywords: 'WEB (Soporte a navegación)',
color: 'bg-indigo-100 text-indigo-800'
},
{
lineaNegocio: 'Customer Service',
keywords: 'ATENCION, INFO, OTROS, GENERAL, PREMIUM',
color: 'bg-green-100 text-green-800'
},
{
lineaNegocio: 'Internal / Backoffice',
keywords: 'COORD, BO_, HELPDESK, BACKOFFICE',
color: 'bg-slate-100 text-slate-800'
}
];
return (
Mapeo de Skills a Líneas de Negocio
{/* Resumen del mapeo */}
Se redujo la complejidad de 980 skills técnicos a {numSkillsNegocio} Líneas de Negocio .
Esta simplificación es vital para la visualización ejecutiva y la toma de decisiones estratégicas.
{/* Tabla de mapeo */}
Línea de Negocio
Keywords Detectadas (Lógica Fuzzy)
{mappings.map((m, idx) => (
{m.lineaNegocio}
{m.keywords}
))}
💡 El mapeo utiliza lógica fuzzy para clasificar automáticamente cada skill técnico
según las keywords detectadas en su nombre. Los skills no clasificados se asignan a "Customer Service".
);
}
function GuaranteesSection() {
const guarantees = [
{
icon: '✓',
title: '100% Trazabilidad',
desc: 'Todos los registros conservados (soft delete)'
},
{
icon: '✓',
title: 'Fórmulas Documentadas',
desc: 'Cada KPI tiene metodología auditable'
},
{
icon: '✓',
title: 'Reconciliación Financiera',
desc: 'Dataset original disponible para auditoría'
},
{
icon: '✓',
title: 'Metodología Replicable',
desc: 'Proceso reproducible para actualizaciones'
}
];
return (
Garantías de Calidad
{guarantees.map((item, i) => (
))}
);
}
// ========== COMPONENTE PRINCIPAL ==========
export function MetodologiaDrawer({ isOpen, onClose, data }: MetodologiaDrawerProps) {
// Calcular datos del resumen desde AnalysisData
const totalRegistros = data.heatmapData?.reduce((sum, h) => sum + h.volume, 0) || 0;
const totalCost = data.heatmapData?.reduce((sum, h) => sum + (h.annual_cost || 0), 0) || 0;
// cost_volume: volumen usado para calcular coste (non-abandon), fallback a volume si no existe
const totalCostVolume = data.heatmapData?.reduce((sum, h) => sum + (h.cost_volume || h.volume), 0) || totalRegistros;
// Calcular meses de histórico desde dateRange
let mesesHistorico = 1;
if (data.dateRange?.min && data.dateRange?.max) {
const minDate = new Date(data.dateRange.min);
const maxDate = new Date(data.dateRange.max);
mesesHistorico = Math.max(1, Math.round((maxDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24 * 30)));
}
// Calcular FCR promedio
const avgFCR = data.heatmapData?.length > 0
? Math.round(data.heatmapData.reduce((sum, h) => sum + (h.metrics?.fcr || 0), 0) / data.heatmapData.length)
: 46;
// Calcular abandono promedio
const avgAbandonment = data.heatmapData?.length > 0
? data.heatmapData.reduce((sum, h) => sum + (h.metrics?.abandonment_rate || 0), 0) / data.heatmapData.length
: 11;
// Calcular AHT promedio
const avgAHT = data.heatmapData?.length > 0
? Math.round(data.heatmapData.reduce((sum, h) => sum + (h.aht_seconds || 0), 0) / data.heatmapData.length)
: 289;
const dataSummary: DataSummary = {
totalRegistros,
mesesHistorico,
periodo: data.dateRange
? `${data.dateRange.min} - ${data.dateRange.max}`
: 'Enero - Diciembre 2025',
fuente: data.source === 'backend' ? 'Genesys Cloud CX' : 'Dataset cargado',
taxonomia: {
valid: 94.2,
noise: 3.1,
zombie: 0.8,
abandon: 1.9
},
kpis: {
fcrTecnico: Math.min(87, avgFCR + 30),
fcrReal: avgFCR,
abandonoTradicional: 0,
abandonoReal: avgAbandonment,
ahtLimpio: avgAHT,
skillsTecnicos: 980,
skillsNegocio: data.heatmapData?.length || 9
}
};
const handleDownloadPDF = () => {
// Por ahora, abrir una URL placeholder o mostrar alert
alert('Funcionalidad de descarga PDF en desarrollo. El documento estará disponible próximamente.');
// En producción: window.open('/documents/Beyond_Diagnostic_Protocolo_Datos.pdf', '_blank');
};
const formatDate = (): string => {
const now = new Date();
const months = [
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
];
return `${months[now.getMonth()]} ${now.getFullYear()}`;
};
return (
{isOpen && (
<>
{/* Overlay */}
{/* Drawer */}
{/* Header */}
Metodología de Transformación de Datos
{/* Body - Scrollable */}
{/* Footer */}
Descargar Protocolo Completo (PDF)
Beyond Diagnosis - Data Strategy Unit │ Certificado: {formatDate()}
>
)}
);
}
export default MetodologiaDrawer;