Files
BeyondCXAnalytics-Demo/frontend/components/MetodologiaDrawer.tsx
Claude 92931ea2dd refactor: implement i18n in MetodologiaDrawer (phase 4)
Refactored MetodologiaDrawer to use react-i18next translations:
- All 8 subsections (DataSummary, Pipeline, Taxonomy, KPI, CPI, BeforeAfter, SkillsMapping, Guarantees)
- 100+ text strings replaced with t() calls
- Month names in date formatting

Added translation keys to es.json and en.json:
- methodology section with 40+ new keys
- CPI calculation components
- Impact analysis labels
- Skill mapping explanations

Build verified successfully.

https://claude.ai/code/session_4f888c33-8937-4db8-8a9d-ddc9ac51a725
2026-02-06 19:11:38 +00:00

769 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useTranslation } from 'react-i18next';
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, t }: { data: DataSummary; t: any }) {
return (
<div className="bg-slate-50 rounded-lg p-5">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Database className="w-5 h-5 text-blue-600" />
{t('methodology.dataProcessed')}
</h3>
<div className="grid grid-cols-3 gap-4">
<div className="bg-white rounded-lg p-4 text-center shadow-sm">
<div className="text-3xl font-bold text-blue-600">
{data.totalRegistros.toLocaleString('es-ES')}
</div>
<div className="text-sm text-gray-600">{t('methodology.recordsAnalyzed')}</div>
</div>
<div className="bg-white rounded-lg p-4 text-center shadow-sm">
<div className="text-3xl font-bold text-blue-600">
{data.mesesHistorico}
</div>
<div className="text-sm text-gray-600">{t('methodology.monthsHistory')}</div>
</div>
<div className="bg-white rounded-lg p-4 text-center shadow-sm">
<div className="text-2xl font-bold text-blue-600">
{data.fuente}
</div>
<div className="text-sm text-gray-600">{t('methodology.sourceSystem')}</div>
</div>
</div>
<p className="text-xs text-slate-500 mt-3 text-center">
{t('methodology.periodRange', { period: data.periodo })}
</p>
</div>
);
}
function PipelineSection({ t }: { t: any }) {
const steps = [
{
layer: 'Layer 0',
name: t('methodology.pipeline.layer1'),
desc: t('methodology.pipeline.layer1Desc'),
color: 'bg-gray-100 border-gray-300'
},
{
layer: 'Layer 1',
name: t('methodology.pipeline.layer2'),
desc: t('methodology.pipeline.layer2Desc'),
color: 'bg-yellow-50 border-yellow-300'
},
{
layer: 'Layer 2',
name: t('methodology.pipeline.layer3'),
desc: t('methodology.pipeline.layer3Desc'),
color: 'bg-green-50 border-green-300'
},
{
layer: 'Output',
name: t('methodology.pipeline.layer4'),
desc: t('methodology.pipeline.layer4Desc'),
color: 'bg-blue-50 border-blue-300'
}
];
return (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<RefreshCw className="w-5 h-5 text-purple-600" />
{t('methodology.pipeline.title')}
</h3>
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<React.Fragment key={step.layer}>
<div className={`flex-1 p-3 rounded-lg border-2 ${step.color} text-center`}>
<div className="text-[10px] text-gray-500 uppercase">{step.layer}</div>
<div className="font-semibold text-sm">{step.name}</div>
<div className="text-[10px] text-gray-600 mt-1">{step.desc}</div>
</div>
{index < steps.length - 1 && (
<ArrowRight className="w-5 h-5 text-gray-400 mx-1 flex-shrink-0" />
)}
</React.Fragment>
))}
</div>
<p className="text-xs text-gray-500 mt-3 italic">
{t('methodology.pipeline.description')}
</p>
</div>
);
}
function TaxonomySection({ data, t }: { data: DataSummary['taxonomia']; t: any }) {
const rows = [
{
status: t('methodology.taxonomy.valid'),
pct: data.valid,
def: t('methodology.taxonomy.validDef'),
costes: true,
aht: true,
bgClass: 'bg-green-100 text-green-800'
},
{
status: t('methodology.taxonomy.noise'),
pct: data.noise,
def: t('methodology.taxonomy.noiseDef'),
costes: true,
aht: false,
bgClass: 'bg-yellow-100 text-yellow-800'
},
{
status: t('methodology.taxonomy.zombie'),
pct: data.zombie,
def: t('methodology.taxonomy.zombieDef'),
costes: true,
aht: false,
bgClass: 'bg-red-100 text-red-800'
},
{
status: t('methodology.taxonomy.abandon'),
pct: data.abandon,
def: t('methodology.taxonomy.abandonDef'),
costes: false,
aht: false,
bgClass: 'bg-gray-100 text-gray-800'
}
];
return (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Tag className="w-5 h-5 text-orange-600" />
{t('methodology.taxonomy.title')}
</h3>
<p className="text-sm text-gray-600 mb-4">
{t('methodology.taxonomy.description')}
</p>
<div className="overflow-hidden rounded-lg border border-slate-200">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-semibold">{t('methodology.taxonomy.state')}</th>
<th className="px-3 py-2 text-right font-semibold">{t('methodology.taxonomy.percentage')}</th>
<th className="px-3 py-2 text-left font-semibold">{t('methodology.taxonomy.definition')}</th>
<th className="px-3 py-2 text-center font-semibold">{t('methodology.taxonomy.costs')}</th>
<th className="px-3 py-2 text-center font-semibold">{t('methodology.taxonomy.aht')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((row, idx) => (
<tr key={row.status} className={idx % 2 === 1 ? 'bg-gray-50' : ''}>
<td className="px-3 py-2">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${row.bgClass}`}>
{row.status}
</span>
</td>
<td className="px-3 py-2 text-right font-semibold">{row.pct.toFixed(1)}%</td>
<td className="px-3 py-2 text-xs text-gray-600">{row.def}</td>
<td className="px-3 py-2 text-center">
{row.costes ? (
<span className="text-green-600">{t('methodology.taxonomy.sumYes')}</span>
) : (
<span className="text-red-600">{t('methodology.taxonomy.sumNo')}</span>
)}
</td>
<td className="px-3 py-2 text-center">
{row.aht ? (
<span className="text-green-600">{t('methodology.taxonomy.avgYes')}</span>
) : (
<span className="text-red-600">{t('methodology.taxonomy.avgExclude')}</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
function KPIRedefinitionSection({ kpis, t }: { kpis: DataSummary['kpis']; t: any }) {
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-indigo-600" />
{t('methodology.kpis.title')}
</h3>
<p className="text-sm text-gray-600 mb-4">
{t('methodology.kpis.description')}
</p>
<div className="space-y-3">
{/* FCR */}
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-red-800">{t('methodology.kpis.fcrTitle')}</h4>
<p className="text-xs text-red-700 mt-1">
{t('methodology.kpis.fcrSubtitle')}
</p>
</div>
<span className="text-2xl font-bold text-red-600">{kpis.fcrReal}%</span>
</div>
<div className="mt-3 text-xs">
<div className="flex justify-between py-1 border-b border-red-200">
<span className="text-gray-600">{t('methodology.kpis.fcrTechnical')}</span>
<span className="font-medium">~{kpis.fcrTecnico}%</span>
</div>
<div className="flex justify-between py-1">
<span className="text-gray-600">{t('methodology.kpis.fcrReal')}</span>
<span className="font-medium text-red-600">{kpis.fcrReal}%</span>
</div>
</div>
<p className="text-[10px] text-red-600 mt-2 italic">
💡 {t('methodology.kpis.fcrGap', { diff: kpis.fcrTecnico - kpis.fcrReal })}
</p>
</div>
{/* Abandono */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-yellow-800">{t('methodology.kpis.abandonTitle')}</h4>
<p className="text-xs text-yellow-700 mt-1">
{t('methodology.kpis.abandonFormula')}
</p>
</div>
<span className="text-2xl font-bold text-yellow-600">{kpis.abandonoReal.toFixed(1)}%</span>
</div>
<p className="text-[10px] text-yellow-600 mt-2 italic">
💡 {t('methodology.kpis.abandonDesc')}
</p>
</div>
{/* AHT */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-blue-800">{t('methodology.kpis.ahtTitle')}</h4>
<p className="text-xs text-blue-700 mt-1">
{t('methodology.kpis.ahtDesc')}
</p>
</div>
<span className="text-2xl font-bold text-blue-600">{formatTime(kpis.ahtLimpio)}</span>
</div>
<p className="text-[10px] text-blue-600 mt-2 italic">
💡 {t('methodology.kpis.ahtNote')}
</p>
</div>
</div>
</div>
);
}
function CPICalculationSection({ totalCost, totalVolume, costPerHour = 20, t }: { totalCost: number; totalVolume: number; costPerHour?: number; t: any }) {
// 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 (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-emerald-600" />
{t('methodology.kpis.cpiTitle')}
</h3>
<p className="text-sm text-gray-600 mb-4">
{t('methodology.cpi.description', { productivity: (effectiveProductivity * 100).toFixed(0) })}
</p>
{/* Fórmula visual */}
<div className="bg-emerald-50 border border-emerald-200 rounded-lg p-4 mb-4">
<div className="text-center mb-3">
<span className="text-xs text-emerald-700 uppercase tracking-wider font-medium">{t('methodology.kpis.cpiFormulaTitle')}</span>
</div>
<div className="flex items-center justify-center gap-2 text-lg font-mono flex-wrap">
<span className="px-3 py-1 bg-white rounded border border-emerald-300">{t('methodology.kpis.cpiLabel')}</span>
<span className="text-emerald-600">=</span>
<span className="px-2 py-1 bg-blue-100 rounded text-blue-800 text-sm">{t('methodology.kpis.totalCost')}</span>
<span className="text-emerald-600">{t('methodology.kpis.divide')}</span>
<span className="px-2 py-1 bg-amber-100 rounded text-amber-800 text-sm">{t('methodology.kpis.totalVolume')}</span>
</div>
<p className="text-[10px] text-center text-emerald-600 mt-2">
{t('methodology.kpis.cpiNote')}
</p>
</div>
{/* Cómo se calcula el coste total */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4">
<div className="text-sm font-semibold text-slate-700 mb-2">{t('methodology.kpis.howCalculate')}</div>
<div className="bg-white rounded p-3 mb-3">
<div className="flex items-center justify-center gap-2 text-sm font-mono flex-wrap">
<span className="text-slate-600">{t('methodology.kpis.costEquals')}</span>
<span className="px-2 py-1 bg-blue-100 rounded text-blue-800 text-xs">(AHT seg ÷ 3600)</span>
<span className="text-slate-400">×</span>
<span className="px-2 py-1 bg-amber-100 rounded text-amber-800 text-xs">{costPerHour}/h</span>
<span className="text-slate-400">×</span>
<span className="px-2 py-1 bg-gray-100 rounded text-gray-800 text-xs">{t('methodology.cpi.volume')}</span>
<span className="text-slate-400">÷</span>
<span className="px-2 py-1 bg-purple-100 rounded text-purple-800 text-xs">{(effectiveProductivity * 100).toFixed(0)}%</span>
</div>
</div>
<p className="text-xs text-slate-600">
{t('methodology.cpi.ahtExplanation')}
</p>
</div>
{/* Componentes del coste horario */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-semibold text-amber-800">{t('methodology.cpi.hourlyRate')}</div>
<span className="text-xs bg-amber-200 text-amber-800 px-2 py-0.5 rounded-full font-medium">
{t('methodology.cpi.configuredValue', { value: costPerHour.toFixed(2) })}
</span>
</div>
<p className="text-xs text-amber-700 mb-3">
{t('methodology.cpi.includesAllCosts')}
</p>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">{t('methodology.cpi.cost1')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">{t('methodology.cpi.cost2')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">{t('methodology.cpi.cost3')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">{t('methodology.cpi.cost4')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">{t('methodology.cpi.cost5')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">{t('methodology.cpi.cost6')}</span>
</div>
</div>
<p className="text-[10px] text-amber-600 mt-3 italic">
💡 {t('methodology.cpi.adjustNote')}
</p>
</div>
</div>
);
}
function BeforeAfterSection({ kpis, t }: { kpis: DataSummary['kpis']; t: any }) {
const rows = [
{
metric: t('methodology.impact.fcr'),
tradicional: `${kpis.fcrTecnico}%`,
beyond: `${kpis.fcrReal}%`,
beyondClass: 'text-red-600',
impacto: t('methodology.impact.revealsDemand')
},
{
metric: t('methodology.impact.abandon'),
tradicional: `~${kpis.abandonoTradicional}%`,
beyond: `${kpis.abandonoReal.toFixed(1)}%`,
beyondClass: 'text-yellow-600',
impacto: t('methodology.impact.detectsFrustration')
},
{
metric: t('methodology.impact.skills'),
tradicional: t('methodology.impact.technicalSkills', { count: kpis.skillsTecnicos }),
beyond: t('methodology.impact.businessLines', { count: kpis.skillsNegocio }),
beyondClass: 'text-blue-600',
impacto: t('methodology.impact.executiveVision')
},
{
metric: t('methodology.impact.aht'),
tradicional: t('methodology.impact.distorted'),
beyond: t('methodology.impact.clean'),
beyondClass: 'text-green-600',
impacto: t('methodology.impact.reflectsPerformance')
}
];
return (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<ArrowLeftRight className="w-5 h-5 text-teal-600" />
{t('methodology.impact.title')}
</h3>
<div className="overflow-hidden rounded-lg border border-slate-200">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-semibold">{t('methodology.impact.metric')}</th>
<th className="px-3 py-2 text-center font-semibold">{t('methodology.impact.traditional')}</th>
<th className="px-3 py-2 text-center font-semibold">{t('methodology.impact.beyond')}</th>
<th className="px-3 py-2 text-left font-semibold">{t('methodology.impact.impact')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((row, idx) => (
<tr key={row.metric} className={idx % 2 === 1 ? 'bg-gray-50' : ''}>
<td className="px-3 py-2 font-medium">{row.metric}</td>
<td className="px-3 py-2 text-center">{row.tradicional}</td>
<td className={`px-3 py-2 text-center font-semibold ${row.beyondClass}`}>{row.beyond}</td>
<td className="px-3 py-2 text-xs text-gray-600">{row.impacto}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 p-3 bg-indigo-50 border border-indigo-200 rounded-lg">
<p className="text-xs text-indigo-800">
<strong>💡 {t('methodology.impact.withoutTransformation')}</strong> {t('methodology.impact.wrongInvestments')}
</p>
</div>
</div>
);
}
function SkillsMappingSection({ numSkillsNegocio, t }: { numSkillsNegocio: number; t: any }) {
const mappings = [
{
lineaNegocio: t('methodology.skillMapping.baggage'),
keywords: 'HANDLING, EQUIPAJE, AHL (Lost & Found), DPR (Daños)',
color: 'bg-amber-100 text-amber-800'
},
{
lineaNegocio: t('methodology.skillMapping.sales'),
keywords: 'COMPRA, VENTA, RESERVA, PAGO',
color: 'bg-blue-100 text-blue-800'
},
{
lineaNegocio: t('methodology.skillMapping.loyalty'),
keywords: 'SUMA (Programa de Fidelización)',
color: 'bg-purple-100 text-purple-800'
},
{
lineaNegocio: t('methodology.skillMapping.b2b'),
keywords: 'AGENCIAS, AAVV, EMPRESAS, AVORIS, TOUROPERACION',
color: 'bg-cyan-100 text-cyan-800'
},
{
lineaNegocio: t('methodology.skillMapping.changes'),
keywords: 'MODIFICACION, CAMBIO, POSTVENTA, REFUND, REEMBOLSO',
color: 'bg-orange-100 text-orange-800'
},
{
lineaNegocio: t('methodology.skillMapping.digital'),
keywords: 'WEB (Soporte a navegación)',
color: 'bg-indigo-100 text-indigo-800'
},
{
lineaNegocio: t('methodology.skillMapping.customer'),
keywords: 'ATENCION, INFO, OTROS, GENERAL, PREMIUM',
color: 'bg-green-100 text-green-800'
},
{
lineaNegocio: t('methodology.skillMapping.internal'),
keywords: 'COORD, BO_, HELPDESK, BACKOFFICE',
color: 'bg-slate-100 text-slate-800'
}
];
return (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Layers className="w-5 h-5 text-violet-600" />
{t('methodology.skillMapping.title')}
</h3>
{/* Resumen del mapeo */}
<div className="bg-violet-50 border border-violet-200 rounded-lg p-4 mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-violet-800">{t('methodology.skillMapping.simplificationApplied')}</span>
<div className="flex items-center gap-2">
<span className="text-2xl font-bold text-violet-600">980</span>
<ArrowRight className="w-4 h-4 text-violet-400" />
<span className="text-2xl font-bold text-violet-600">{numSkillsNegocio}</span>
</div>
</div>
<p className="text-xs text-violet-700">
{t('methodology.skillMapping.reductionDesc', { count: numSkillsNegocio })}
</p>
</div>
{/* Tabla de mapeo */}
<div className="overflow-hidden rounded-lg border border-slate-200">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-semibold">{t('methodology.skillMapping.businessLine')}</th>
<th className="px-3 py-2 text-left font-semibold">{t('methodology.skillMapping.keywords')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{mappings.map((m, idx) => (
<tr key={m.lineaNegocio} className={idx % 2 === 1 ? 'bg-gray-50' : ''}>
<td className="px-3 py-2">
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${m.color}`}>
{m.lineaNegocio}
</span>
</td>
<td className="px-3 py-2 text-xs text-gray-600 font-mono">
{m.keywords}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-xs text-gray-500 mt-3 italic">
💡 {t('methodology.skillMapping.fuzzyLogicNote')}
</p>
</div>
);
}
function GuaranteesSection({ t }: { t: any }) {
const guarantees = [
{
icon: '✓',
title: t('methodology.quality.traceability'),
desc: t('methodology.quality.traceabilityDesc')
},
{
icon: '✓',
title: t('methodology.quality.formulas'),
desc: t('methodology.quality.formulasDesc')
},
{
icon: '✓',
title: t('methodology.quality.reconciliation'),
desc: t('methodology.quality.reconciliationDesc')
},
{
icon: '✓',
title: t('methodology.quality.replicable'),
desc: t('methodology.quality.replicableDesc')
}
];
return (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<BadgeCheck className="w-5 h-5 text-green-600" />
{t('methodology.quality.title')}
</h3>
<div className="grid grid-cols-2 gap-3">
{guarantees.map((item, i) => (
<div key={i} className="flex items-start gap-3 p-3 bg-green-50 rounded-lg">
<span className="text-green-600 font-bold text-lg">{item.icon}</span>
<div>
<div className="font-medium text-green-800 text-sm">{item.title}</div>
<div className="text-xs text-green-700">{item.desc}</div>
</div>
</div>
))}
</div>
</div>
);
}
// ========== COMPONENTE PRINCIPAL ==========
export function MetodologiaDrawer({ isOpen, onClose, data }: MetodologiaDrawerProps) {
const { t } = useTranslation();
// 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}`
: t('methodology.defaultPeriod'),
fuente: data.source === 'backend' ? t('methodology.sourceGenesys') : t('methodology.sourceDataset'),
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(t('methodology.pdfDevelopment'));
// En producción: window.open('/documents/Beyond_Diagnostic_Protocolo_Datos.pdf', '_blank');
};
const formatDate = (): string => {
const now = new Date();
const monthKey = `methodology.months.${['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'][now.getMonth()]}`;
return `${t(monthKey)} ${now.getFullYear()}`;
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Overlay */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-40"
onClick={onClose}
/>
{/* Drawer */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className="fixed right-0 top-0 h-full w-full max-w-2xl bg-white shadow-xl z-50 overflow-hidden flex flex-col"
>
{/* Header */}
<div className="sticky top-0 bg-white border-b border-slate-200 px-6 py-4 flex justify-between items-center flex-shrink-0">
<div className="flex items-center gap-2">
<ShieldCheck className="text-green-600 w-6 h-6" />
<h2 className="text-lg font-bold text-slate-800">{t('methodology.fullTitle')}</h2>
</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 p-1 rounded-lg hover:bg-slate-100 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Body - Scrollable */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<DataSummarySection data={dataSummary} t={t} />
<PipelineSection t={t} />
<SkillsMappingSection numSkillsNegocio={dataSummary.kpis.skillsNegocio} t={t} />
<TaxonomySection data={dataSummary.taxonomia} t={t} />
<KPIRedefinitionSection kpis={dataSummary.kpis} t={t} />
<CPICalculationSection
totalCost={totalCost}
totalVolume={totalCostVolume}
costPerHour={data.staticConfig?.cost_per_hour || 20}
t={t}
/>
<BeforeAfterSection kpis={dataSummary.kpis} t={t} />
<GuaranteesSection t={t} />
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-gray-50 border-t border-slate-200 px-6 py-4 flex-shrink-0">
<div className="flex justify-between items-center">
<button
onClick={handleDownloadPDF}
className="flex items-center gap-2 px-4 py-2 bg-[#6D84E3] text-white rounded-lg hover:bg-[#5A70C7] transition-colors text-sm font-medium"
>
<Download className="w-4 h-4" />
{t('methodology.downloadProtocol')}
</button>
<span className="text-xs text-gray-500">
{t('methodology.certificate', { date: formatDate() })}
</span>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
export default MetodologiaDrawer;