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
769 lines
29 KiB
TypeScript
769 lines
29 KiB
TypeScript
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;
|