feat: Add Law 10/2025 compliance analysis tab

- Add new Law10Tab with compliance analysis for Spanish Law 10/2025
- Sections: LAW-01 (Response Speed), LAW-02 (Resolution Quality), LAW-07 (Time Coverage)
- Add Data Maturity Summary showing available/estimable/missing data
- Add Validation Questionnaire for manual data input
- Add Dimension Connections linking to other analysis tabs
- Fix KPI consistency: use correct field names (abandonment_rate, aht_seconds)
- Fix cache directory path for Windows compatibility
- Update economic calculations to use actual economicModel data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
sujucu70
2026-01-22 21:58:26 +01:00
parent 62454c6b6a
commit 88d7e4c10d
20 changed files with 5554 additions and 1285 deletions

View File

@@ -304,6 +304,111 @@ function KPIRedefinitionSection({ kpis }: { kpis: DataSummary['kpis'] }) {
);
}
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 (
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-emerald-600" />
Coste por Interacción (CPI)
</h3>
<p className="text-sm text-gray-600 mb-4">
El CPI se calcula dividiendo el <strong>coste total</strong> entre el <strong>volumen de interacciones</strong>.
El coste total incluye <em>todas</em> las interacciones (noise, zombie y válidas) porque todas se facturan,
y aplica un factor de productividad del {(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">Fórmula de Cálculo</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">CPI</span>
<span className="text-emerald-600">=</span>
<span className="px-2 py-1 bg-blue-100 rounded text-blue-800 text-sm">Coste Total</span>
<span className="text-emerald-600">÷</span>
<span className="px-2 py-1 bg-amber-100 rounded text-amber-800 text-sm">Volumen Total</span>
</div>
<p className="text-[10px] text-center text-emerald-600 mt-2">
El coste total usa (AHT segundos ÷ 3600) × coste/hora × volumen ÷ productividad
</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">¿Cómo se calcula el Coste Total?</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">Coste =</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">Volumen</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">
El <strong>AHT</strong> 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.
</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">Coste por Hora del Agente (Fully Loaded)</div>
<span className="text-xs bg-amber-200 text-amber-800 px-2 py-0.5 rounded-full font-medium">
Valor introducido: {costPerHour.toFixed(2)}/h
</span>
</div>
<p className="text-xs text-amber-700 mb-3">
Este valor fue configurado en la pantalla de entrada de datos y debe incluir todos los costes asociados al agente:
</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">Salario bruto del agente</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">Costes de seguridad social</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">Licencias de software</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">Infraestructura y puesto</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">Supervisión y QA</span>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-500"></span>
<span className="text-amber-700">Formación y overhead</span>
</div>
</div>
<p className="text-[10px] text-amber-600 mt-3 italic">
💡 Si necesita ajustar este valor, puede volver a la pantalla de entrada de datos y modificarlo.
</p>
</div>
</div>
);
}
function BeforeAfterSection({ kpis }: { kpis: DataSummary['kpis'] }) {
const rows = [
{
@@ -528,6 +633,9 @@ function GuaranteesSection() {
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;
@@ -633,6 +741,11 @@ export function MetodologiaDrawer({ isOpen, onClose, data }: MetodologiaDrawerPr
<SkillsMappingSection numSkillsNegocio={dataSummary.kpis.skillsNegocio} />
<TaxonomySection data={dataSummary.taxonomia} />
<KPIRedefinitionSection kpis={dataSummary.kpis} />
<CPICalculationSection
totalCost={totalCost}
totalVolume={totalCostVolume}
costPerHour={data.staticConfig?.cost_per_hour || 20}
/>
<BeforeAfterSection kpis={dataSummary.kpis} />
<GuaranteesSection />
</div>