refactor: implement i18n in RoadmapTab and Law10Tab (phase 5)

Refactored two major tab components to use react-i18next:

RoadmapTab (2,719 lines):
- Opportunity bubble chart with all quadrants and methodology
- Wave cards with risk levels and strategic recommendations
- Tier classification (TIER 1-4) with distribution analysis
- Economic impact and feasibility axis labels
- 100+ translation keys for visualization and strategy content

Law10Tab (1,533 lines):
- Spanish regulatory compliance (Ley 10/2025) analysis
- All 12 requirements with status evaluations
- Time coverage, response speed, resolution quality sections
- Compliance messages and action recommendations
- 80+ translation keys for regulatory content

Added comprehensive translation keys to es.json and en.json:
- roadmap section: 100+ keys
- law10 section: 80+ keys

Build verified successfully.

https://claude.ai/code/session_4f888c33-8937-4db8-8a9d-ddc9ac51a725
This commit is contained in:
Claude
2026-02-06 19:43:01 +00:00
parent 92931ea2dd
commit cbe074f43c
4 changed files with 1230 additions and 315 deletions

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Scale,
Clock,
@@ -65,7 +66,7 @@ const LAW_10_2025 = {
// FUNCIONES DE EVALUACION DE COMPLIANCE
// ============================================
function evaluateLaw07Compliance(data: AnalysisData): ComplianceResult {
function evaluateLaw07Compliance(data: AnalysisData, t: (key: string, options?: any) => string): ComplianceResult {
// Evaluar cobertura horaria basado en off_hours_pct
const volumetryDim = data.dimensions.find(d => d.name === 'volumetry_distribution');
const offHoursPct = volumetryDim?.distribution_data?.off_hours_pct ?? null;
@@ -74,39 +75,39 @@ function evaluateLaw07Compliance(data: AnalysisData): ComplianceResult {
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Sin datos de distribucion horaria',
details: ['No se encontraron datos de distribucion horaria en el analisis'],
gap: t('law10.compliance.law07.noData'),
details: [t('law10.compliance.law07.noDataDetails')],
};
}
const details: string[] = [];
details.push(`${offHoursPct.toFixed(1)}% de interacciones fuera de horario laboral`);
details.push(t('law10.compliance.law07.offHoursPercent', { percent: offHoursPct.toFixed(1) }));
if (offHoursPct < 5) {
return {
status: 'CUMPLE',
score: 100,
gap: '-',
details: [...details, 'Cobertura horaria adecuada'],
details: [...details, t('law10.compliance.law07.adequateCoverage')],
};
} else if (offHoursPct <= 15) {
return {
status: 'PARCIAL',
score: Math.round(100 - ((offHoursPct - 5) / 10) * 50),
gap: `${(offHoursPct - 5).toFixed(1)}pp sobre optimo`,
details: [...details, 'Cobertura horaria mejorable - considerar ampliar horarios'],
gap: t('law10.compliance.law07.gapOverOptimal', { gap: (offHoursPct - 5).toFixed(1) }),
details: [...details, t('law10.compliance.law07.improvableCoverage')],
};
} else {
return {
status: 'NO_CUMPLE',
score: Math.max(0, Math.round(50 - ((offHoursPct - 15) / 10) * 50)),
gap: `${(offHoursPct - 15).toFixed(1)}pp sobre limite`,
details: [...details, 'Cobertura horaria insuficiente - requiere accion inmediata'],
gap: t('law10.compliance.law07.gapOverLimit', { gap: (offHoursPct - 15).toFixed(1) }),
details: [...details, t('law10.compliance.law07.insufficientCoverage')],
};
}
}
function evaluateLaw01Compliance(data: AnalysisData): ComplianceResult {
function evaluateLaw01Compliance(data: AnalysisData, t: (key: string, options?: any) => string): ComplianceResult {
// Evaluar tiempo de espera (hold_time) vs limite de 180 segundos
const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
@@ -114,8 +115,8 @@ function evaluateLaw01Compliance(data: AnalysisData): ComplianceResult {
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Sin datos de tiempos de espera',
details: ['No se encontraron datos de hold_time en el analisis'],
gap: t('law10.compliance.law01.noData'),
details: [t('law10.compliance.law01.noDataDetails')],
};
}
@@ -135,35 +136,35 @@ function evaluateLaw01Compliance(data: AnalysisData): ComplianceResult {
const pctDentroLimite = (volDentroLimite / totalVolume) * 100;
const details: string[] = [];
details.push(`Tiempo de espera promedio: ${Math.round(avgHoldTime)}s (limite: 180s)`);
details.push(`${pctDentroLimite.toFixed(1)}% de interacciones dentro del limite`);
details.push(`${colasExceden.length} de ${data.heatmapData.length} colas exceden el limite`);
details.push(t('law10.compliance.law01.avgHoldTime', { time: Math.round(avgHoldTime) }));
details.push(t('law10.compliance.law01.withinLimit', { percent: pctDentroLimite.toFixed(1) }));
details.push(t('law10.compliance.law01.queuesExceedLimit', { count: colasExceden.length, total: data.heatmapData.length }));
if (avgHoldTime < 180 && pctColasExceden < 10) {
return {
status: 'CUMPLE',
score: 100,
gap: `-${Math.round(180 - avgHoldTime)}s`,
gap: t('law10.compliance.law01.gapNegative', { gap: Math.round(180 - avgHoldTime) }),
details,
};
} else if (avgHoldTime < 180) {
return {
status: 'PARCIAL',
score: Math.round(90 - pctColasExceden),
gap: `${colasExceden.length} colas fuera`,
gap: t('law10.compliance.law01.queuesOutside', { count: colasExceden.length }),
details,
};
} else {
return {
status: 'NO_CUMPLE',
score: Math.max(0, Math.round(50 - ((avgHoldTime - 180) / 60) * 25)),
gap: `+${Math.round(avgHoldTime - 180)}s`,
gap: t('law10.compliance.law01.gapPositive', { gap: Math.round(avgHoldTime - 180) }),
details,
};
}
}
function evaluateLaw02Compliance(data: AnalysisData): ComplianceResult {
function evaluateLaw02Compliance(data: AnalysisData, t: (key: string, options?: any) => string): ComplianceResult {
// Evaluar FCR y tasa de transferencia
const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
@@ -171,8 +172,8 @@ function evaluateLaw02Compliance(data: AnalysisData): ComplianceResult {
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Sin datos de resolucion',
details: ['No se encontraron datos de FCR o transferencias'],
gap: t('law10.compliance.law02.noData'),
details: [t('law10.compliance.law02.noDataDetails')],
};
}
@@ -187,13 +188,13 @@ function evaluateLaw02Compliance(data: AnalysisData): ComplianceResult {
) / totalVolume;
const details: string[] = [];
details.push(`FCR Tecnico: ${avgFCR.toFixed(1)}% (objetivo: >75%)`);
details.push(`Tasa de transferencia: ${avgTransfer.toFixed(1)}% (objetivo: <15%)`);
details.push(t('law10.compliance.law02.fcrTechnical', { fcr: avgFCR.toFixed(1) }));
details.push(t('law10.compliance.law02.transferRate', { rate: avgTransfer.toFixed(1) }));
// Colas con alto transfer
const colasAltoTransfer = data.heatmapData.filter(h => h.metrics.transfer_rate > 25);
if (colasAltoTransfer.length > 0) {
details.push(`${colasAltoTransfer.length} colas con transfer >25%`);
details.push(t('law10.compliance.law02.highTransferQueues', { count: colasAltoTransfer.length }));
}
const cumpleFCR = avgFCR >= 75;
@@ -205,7 +206,7 @@ function evaluateLaw02Compliance(data: AnalysisData): ComplianceResult {
return {
status: 'CUMPLE',
score: 100,
gap: '-',
gap: t('law10.compliance.law02.gapDash'),
details,
};
} else if (parcialFCR && parcialTransfer) {
@@ -213,31 +214,33 @@ function evaluateLaw02Compliance(data: AnalysisData): ComplianceResult {
(Math.min(avgFCR, 75) / 75 * 50) +
(Math.max(0, 25 - avgTransfer) / 25 * 50)
);
const fcrGap = avgFCR < 75 ? t('law10.compliance.law02.gapNegative', { gap: (75 - avgFCR).toFixed(0) }) : t('law10.compliance.law02.gapOk');
const transferGap = avgTransfer > 15 ? t('law10.compliance.law02.gapPositive', { gap: (avgTransfer - 15).toFixed(0) }) : t('law10.compliance.law02.gapOk');
return {
status: 'PARCIAL',
score,
gap: `FCR ${avgFCR < 75 ? `-${(75 - avgFCR).toFixed(0)}pp` : 'OK'}, Transfer ${avgTransfer > 15 ? `+${(avgTransfer - 15).toFixed(0)}pp` : 'OK'}`,
gap: t('law10.compliance.law02.gapFcr', { fcrGap, transferGap }),
details,
};
} else {
return {
status: 'NO_CUMPLE',
score: Math.max(0, Math.round((avgFCR / 75 * 30) + ((30 - avgTransfer) / 30 * 20))),
gap: `FCR -${(75 - avgFCR).toFixed(0)}pp, Transfer +${(avgTransfer - 15).toFixed(0)}pp`,
gap: `FCR ${t('law10.compliance.law02.gapNegative', { gap: (75 - avgFCR).toFixed(0) })}, Transfer ${t('law10.compliance.law02.gapPositive', { gap: (avgTransfer - 15).toFixed(0) })}`,
details,
};
}
}
function evaluateLaw09Compliance(_data: AnalysisData): ComplianceResult {
function evaluateLaw09Compliance(_data: AnalysisData, t: (key: string, options?: any) => string): ComplianceResult {
// Los datos de idioma no estan disponibles en el modelo actual
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Requiere datos',
gap: t('law10.compliance.law09.noData'),
details: [
'No se dispone de datos de idioma en las interacciones',
'Para evaluar este requisito se necesita el campo "language" en el CSV',
t('law10.compliance.law09.noLanguageData'),
t('law10.compliance.law09.needsLanguageField'),
],
};
}
@@ -273,12 +276,12 @@ function getStatusBadgeVariant(status: ComplianceStatus): 'success' | 'warning'
}
}
function getStatusLabel(status: ComplianceStatus): string {
function getStatusLabel(status: ComplianceStatus, t: (key: string) => string): string {
switch (status) {
case 'CUMPLE': return 'Cumple';
case 'PARCIAL': return 'Parcial';
case 'NO_CUMPLE': return 'No Cumple';
default: return 'Sin Datos';
case 'CUMPLE': return t('law10.status.CUMPLE');
case 'PARCIAL': return t('law10.status.PARCIAL');
case 'NO_CUMPLE': return t('law10.status.NO_CUMPLE');
default: return t('law10.status.SIN_DATOS');
}
}
@@ -288,6 +291,7 @@ function Law10HeaderCountdown({
}: {
complianceResults: { law07: ComplianceResult; law01: ComplianceResult; law02: ComplianceResult; law09: ComplianceResult };
}) {
const { t } = useTranslation();
const now = new Date();
const deadline = LAW_10_2025.deadline;
const diffTime = deadline.getTime() - now.getTime();
@@ -314,18 +318,14 @@ function Law10HeaderCountdown({
<Lightbulb className="w-5 h-5 text-amber-600" />
</div>
<div>
<h2 className="font-semibold text-gray-900">Sobre este Analisis</h2>
<p className="text-sm text-gray-500">Ley 10/2025 de Atencion al Cliente</p>
<h2 className="font-semibold text-gray-900">{t('law10.header.aboutThisAnalysis')}</h2>
<p className="text-sm text-gray-500">{t('law10.header.lawTitle')}</p>
</div>
</div>
{/* Descripcion */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
<p className="text-sm text-gray-700 leading-relaxed">
Este modulo conecta tus <strong>metricas operacionales actuales</strong> con los requisitos de la
Ley 10/2025. No mide compliance directamente (requeriria datos adicionales), pero <strong>SI
identifica patrones</strong> que impactan en tu capacidad de cumplir con la normativa.
</p>
<p className="text-sm text-gray-700 leading-relaxed" dangerouslySetInnerHTML={{ __html: t('law10.header.description') }} />
</div>
{/* Metricas de estado */}
@@ -334,9 +334,9 @@ function Law10HeaderCountdown({
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<Calendar className="w-5 h-5 text-gray-400" />
<div>
<p className="text-xs text-gray-500">Deadline de cumplimiento</p>
<p className="text-sm font-semibold text-gray-900">28 Diciembre 2026</p>
<p className="text-xs text-gray-500">{diffDays} dias restantes</p>
<p className="text-xs text-gray-500">{t('law10.header.complianceDeadline')}</p>
<p className="text-sm font-semibold text-gray-900">{t('law10.header.december282026')}</p>
<p className="text-xs text-gray-500">{t('law10.header.daysRemaining', { days: diffDays })}</p>
</div>
</div>
@@ -344,9 +344,9 @@ function Law10HeaderCountdown({
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<Scale className="w-5 h-5 text-gray-400" />
<div>
<p className="text-xs text-gray-500">Requisitos evaluados</p>
<p className="text-sm font-semibold text-gray-900">{cumplidos} de {total} cumplen</p>
<p className="text-xs text-gray-500">Basado en datos disponibles</p>
<p className="text-xs text-gray-500">{t('law10.header.requirementsEvaluated')}</p>
<p className="text-sm font-semibold text-gray-900">{t('law10.header.requirementsMet', { met: cumplidos, total })}</p>
<p className="text-xs text-gray-500">{t('law10.header.basedOnData')}</p>
</div>
</div>
@@ -354,18 +354,18 @@ function Law10HeaderCountdown({
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<StatusIcon status={overallStatus} />
<div>
<p className="text-xs text-gray-500">Estado general</p>
<p className="text-xs text-gray-500">{t('law10.header.overallStatus')}</p>
<p className={cn(
'text-sm font-semibold',
overallStatus === 'CUMPLE' && 'text-emerald-600',
overallStatus === 'PARCIAL' && 'text-amber-600',
overallStatus === 'NO_CUMPLE' && 'text-red-600',
)}>
{getStatusLabel(overallStatus)}
{getStatusLabel(overallStatus, t)}
</p>
<p className="text-xs text-gray-500">
{overallStatus === 'CUMPLE' ? 'Buen estado' :
overallStatus === 'PARCIAL' ? 'Requiere atencion' : 'Accion urgente'}
{overallStatus === 'CUMPLE' ? t('law10.header.goodState') :
overallStatus === 'PARCIAL' ? t('law10.header.requiresAttention') : t('law10.header.urgentAction')}
</p>
</div>
</div>
@@ -376,6 +376,7 @@ function Law10HeaderCountdown({
// Seccion: Cobertura Horaria (LAW-07)
function TimeCoverageSection({ data, result }: { data: AnalysisData; result: ComplianceResult }) {
const { t } = useTranslation();
const volumetryDim = data.dimensions.find(d => d.name === 'volumetry_distribution');
const hourlyData = volumetryDim?.distribution_data?.hourly || [];
const dailyData = volumetryDim?.distribution_data?.daily || [];
@@ -447,13 +448,13 @@ function TimeCoverageSection({ data, result }: { data: AnalysisData; result: Com
<Target className="w-5 h-5 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">Cobertura Temporal: Disponibilidad del Servicio</h3>
<p className="text-sm text-gray-500">Relacionado con Art. 14 - Servicios basicos 24/7</p>
<h3 className="font-semibold text-gray-900">{t('law10.timeCoverage.title')}</h3>
<p className="text-sm text-gray-500">{t('law10.timeCoverage.article')}</p>
</div>
</div>
<div className="flex items-center gap-2">
<StatusIcon status={result.status} />
<Badge label={getStatusLabel(result.status)} variant={getStatusBadgeVariant(result.status)} />
<Badge label={getStatusLabel(result.status, t)} variant={getStatusBadgeVariant(result.status)} />
</div>
</div>
@@ -461,12 +462,12 @@ function TimeCoverageSection({ data, result }: { data: AnalysisData; result: Com
<div className="mb-5">
<h4 className="text-sm font-semibold text-emerald-700 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
LO QUE SABEMOS
{t('law10.timeCoverage.whatWeKnow')}
</h4>
{/* Heatmap 24x7 */}
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<p className="text-xs text-gray-500 mb-3 font-medium">HEATMAP VOLUMETRICO 24x7</p>
<p className="text-xs text-gray-500 mb-3 font-medium">{t('law10.timeCoverage.heatmap247')}</p>
{/* Header de horas */}
<div className="flex items-center mb-1">
@@ -500,32 +501,32 @@ function TimeCoverageSection({ data, result }: { data: AnalysisData; result: Com
{/* Leyenda */}
<div className="flex items-center gap-4 mt-3 text-[10px] text-gray-500">
<span>Intensidad:</span>
<span className="text-blue-200"> Bajo</span>
<span className="text-blue-400"> Medio</span>
<span className="text-blue-600"> Alto</span>
<span>{t('law10.timeCoverage.intensity')}</span>
<span className="text-blue-200"> {t('law10.timeCoverage.intensityLow')}</span>
<span className="text-blue-400"> {t('law10.timeCoverage.intensityMedium')}</span>
<span className="text-blue-600"> {t('law10.timeCoverage.intensityHigh')}</span>
</div>
</div>
{/* Hallazgos operacionales */}
<div className="space-y-2 text-sm">
<p className="font-medium text-gray-700 mb-2">Hallazgos operacionales:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.timeCoverage.operationalFindings')}</p>
<ul className="space-y-1.5 text-gray-600">
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>Horario detectado: <strong>L-V 08:00-22:00</strong>, S-D horario reducido</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.timeCoverage.detectedSchedule') }} />
</li>
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>Volumen nocturno (22:00-08:00): <strong>{formatNumber(nightVolume)}</strong> interacciones ({nightPct.toFixed(1)}%)</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.timeCoverage.nightVolume', { volume: formatNumber(nightVolume), percent: nightPct.toFixed(1) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>Volumen madrugada (00:00-06:00): <strong>{formatNumber(earlyMorningVolume)}</strong> interacciones ({earlyMorningPct.toFixed(1)}%)</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.timeCoverage.earlyMorningVolume', { volume: formatNumber(earlyMorningVolume), percent: earlyMorningPct.toFixed(1) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>Pico maximo: <strong>{maxHourIndex}:00-{maxHourIndex + 1}:00</strong> ({maxHourPct.toFixed(1)}% del volumen diario)</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.timeCoverage.peakHour', { hour: maxHourIndex, hourEnd: maxHourIndex + 1, percent: maxHourPct.toFixed(1) }) }} />
</li>
</ul>
</div>
@@ -535,25 +536,25 @@ function TimeCoverageSection({ data, result }: { data: AnalysisData; result: Com
<div className="mb-5 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<h4 className="text-sm font-semibold text-amber-800 mb-3 flex items-center gap-2">
<Scale className="w-4 h-4" />
IMPLICACION LEY 10/2025
{t('law10.timeCoverage.lawImplication')}
</h4>
<div className="space-y-3 text-sm text-gray-700">
<p>
<strong>Transporte aereo = Servicio basico</strong><br />
<span className="text-gray-600"> Art. 14 requiere atencion 24/7 para incidencias</span>
<strong>{t('law10.timeCoverage.basicServiceRequirement')}</strong><br />
<span className="text-gray-600">{t('law10.timeCoverage.article14Requirement')}</span>
</p>
<div className="border-t border-amber-200 pt-3">
<p className="font-medium text-amber-900 mb-2">Gap identificado:</p>
<p className="font-medium text-amber-900 mb-2">{t('law10.timeCoverage.gapIdentified')}</p>
<ul className="space-y-1 text-gray-600">
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span><strong>{nightPct.toFixed(1)}%</strong> de tus clientes contactan fuera del horario actual</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.timeCoverage.clientsOutsideHours', { percent: nightPct.toFixed(1) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span>Si estas son incidencias (equipaje perdido, cambios urgentes), <strong>NO cumples Art. 14</strong></span>
<span dangerouslySetInnerHTML={{ __html: t('law10.timeCoverage.complianceIssue') }} />
</li>
</ul>
</div>
@@ -564,32 +565,32 @@ function TimeCoverageSection({ data, result }: { data: AnalysisData; result: Com
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-semibold text-blue-800 mb-3 flex items-center gap-2">
<Target className="w-4 h-4" />
ACCION SUGERIDA
{t('law10.timeCoverage.suggestedAction')}
</h4>
<div className="space-y-4 text-sm">
<div>
<p className="font-medium text-gray-700 mb-2">1. Clasificar volumen nocturno por tipo:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.timeCoverage.classifyNightVolume')}</p>
<ul className="space-y-1 text-gray-600 ml-4">
<li> ¿Que % son incidencias criticas? Requiere 24/7</li>
<li> ¿Que % son consultas generales? Pueden esperar</li>
<li> {t('law10.timeCoverage.criticalIncidents')}</li>
<li> {t('law10.timeCoverage.generalQueries')}</li>
</ul>
</div>
<div>
<p className="font-medium text-gray-700 mb-2">2. Opciones de cobertura:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.timeCoverage.coverageOptions')}</p>
<div className="space-y-2 ml-4">
<div className="flex items-center justify-between p-2 bg-white rounded border border-gray-200">
<span className="text-gray-700">A) Chatbot IA + agente on-call</span>
<span className="font-semibold text-emerald-600">~65K/año</span>
<span className="text-gray-700">{t('law10.timeCoverage.optionAChatbot')}</span>
<span className="font-semibold text-emerald-600">{t('law10.timeCoverage.costPerYear', { cost: '65K' })}</span>
</div>
<div className="flex items-center justify-between p-2 bg-white rounded border border-gray-200">
<span className="text-gray-700">B) Redirigir a call center 24/7 externo</span>
<span className="font-semibold text-amber-600">~95K/año</span>
<span className="text-gray-700">{t('law10.timeCoverage.optionBExternal')}</span>
<span className="font-semibold text-amber-600">{t('law10.timeCoverage.costPerYear', { cost: '95K' })}</span>
</div>
<div className="flex items-center justify-between p-2 bg-white rounded border border-gray-200">
<span className="text-gray-700">C) Agentes nocturnos (3 turnos)</span>
<span className="font-semibold text-red-600">~180K/año</span>
<span className="text-gray-700">{t('law10.timeCoverage.optionCNight')}</span>
<span className="font-semibold text-red-600">{t('law10.timeCoverage.costPerYear', { cost: '180K' })}</span>
</div>
</div>
</div>
@@ -601,6 +602,7 @@ function TimeCoverageSection({ data, result }: { data: AnalysisData; result: Com
// Seccion: Velocidad de Respuesta (LAW-01)
function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: ComplianceResult }) {
const { t } = useTranslation();
const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
const volumetryDim = data.dimensions.find(d => d.name === 'volumetry_distribution');
const hourlyData = volumetryDim?.distribution_data?.hourly || [];
@@ -677,13 +679,13 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
<Clock className="w-5 h-5 text-purple-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">Velocidad de Atencion: Eficiencia Operativa</h3>
<p className="text-sm text-gray-500">Relacionado con Art. 8.2 - 95% llamadas &lt;3min</p>
<h3 className="font-semibold text-gray-900">{t('law10.responseSpeed.title')}</h3>
<p className="text-sm text-gray-500">{t('law10.responseSpeed.article')}</p>
</div>
</div>
<div className="flex items-center gap-2">
<StatusIcon status={result.status} />
<Badge label={getStatusLabel(result.status)} variant={getStatusBadgeVariant(result.status)} />
<Badge label={getStatusLabel(result.status, t)} variant={getStatusBadgeVariant(result.status)} />
</div>
</div>
@@ -691,22 +693,22 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
<div className="mb-5">
<h4 className="text-sm font-semibold text-emerald-700 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
LO QUE SABEMOS
{t('law10.responseSpeed.whatWeKnow')}
</h4>
{/* Metricas principales */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">{abandonRate.toFixed(1)}%</p>
<p className="text-xs text-gray-600">Tasa abandono</p>
<p className="text-xs text-gray-600">{t('law10.responseSpeed.abandonmentRate')}</p>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">{Math.round(ahtP50)}s</p>
<p className="text-xs text-gray-600">AHT P50 ({Math.floor(ahtP50 / 60)}m {Math.round(ahtP50 % 60)}s)</p>
<p className="text-xs text-gray-600">{t('law10.responseSpeed.ahtP50', { min: Math.floor(ahtP50 / 60), sec: Math.round(ahtP50 % 60) })}</p>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">{Math.round(ahtP90)}s</p>
<p className="text-xs text-gray-600">AHT P90 ({Math.floor(ahtP90 / 60)}m {Math.round(ahtP90 % 60)}s)</p>
<p className="text-xs text-gray-600">{t('law10.responseSpeed.ahtP90', { min: Math.floor(ahtP90 / 60), sec: Math.round(ahtP90 % 60) })}</p>
</div>
<div className={cn(
'p-3 rounded-lg',
@@ -716,13 +718,13 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
'text-2xl font-bold',
ahtRatio > 2 ? 'text-amber-600' : 'text-gray-900'
)}>{ahtRatio.toFixed(1)}</p>
<p className="text-xs text-gray-600">Ratio P90/P50 {ahtRatio > 2 && '(elevado)'}</p>
<p className="text-xs text-gray-600">{t('law10.responseSpeed.ratioP90P50', { elevated: ahtRatio > 2 ? t('law10.responseSpeed.elevated') : '' })}</p>
</div>
</div>
{/* Grafico de abandonos por hora */}
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<p className="text-xs text-gray-500 mb-3 font-medium">DISTRIBUCION DE ABANDONOS POR HORA</p>
<p className="text-xs text-gray-500 mb-3 font-medium">{t('law10.responseSpeed.abandonmentByHour')}</p>
<div className="flex items-end gap-0.5 h-16 mb-2">
{hourlyAbandonment.map((h, idx) => (
<div
@@ -744,28 +746,28 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
<span>24:00</span>
</div>
<div className="flex items-center gap-4 mt-3 text-[10px] text-gray-500">
<span>Abandono:</span>
<span className="text-emerald-500"> &lt;8%</span>
<span className="text-amber-400"> 8-15%</span>
<span className="text-red-500"> &gt;20%</span>
<span>{t('law10.responseSpeed.abandonmentLegend')}</span>
<span className="text-emerald-500" dangerouslySetInnerHTML={{ __html: `${t('law10.responseSpeed.abandonmentLow')}` }} />
<span className="text-amber-400" dangerouslySetInnerHTML={{ __html: `${t('law10.responseSpeed.abandonmentMedium')}` }} />
<span className="text-red-500" dangerouslySetInnerHTML={{ __html: `${t('law10.responseSpeed.abandonmentHigh')}` }} />
</div>
</div>
{/* Patrones observados */}
<div className="space-y-2 text-sm">
<p className="font-medium text-gray-700 mb-2">Patrones observados:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.responseSpeed.patternsObserved')}</p>
<ul className="space-y-1.5 text-gray-600">
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>Mayor abandono: <strong>{maxAbandonHour.hour}:00-{maxAbandonHour.hour + 2}:00</strong> ({maxAbandonHour.abandonRate.toFixed(1)}% vs {abandonRate.toFixed(1)}% media)</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.maxAbandonment', { hourStart: maxAbandonHour.hour, hourEnd: maxAbandonHour.hour + 2, rate: maxAbandonHour.abandonRate.toFixed(1), avg: abandonRate.toFixed(1) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>AHT mas alto: <strong>Lunes 09:00-11:00</strong> ({Math.round(ahtP50 * 1.18)}s vs {Math.round(ahtP50)}s P50)</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.highestAht', { high: Math.round(ahtP50 * 1.18), p50: Math.round(ahtP50) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>Menor abandono: <strong>{minAbandonHour.hour}:00-{minAbandonHour.hour + 2}:00</strong> ({minAbandonHour.abandonRate.toFixed(1)}%)</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.minAbandonment', { hourStart: minAbandonHour.hour, hourEnd: minAbandonHour.hour + 2, rate: minAbandonHour.abandonRate.toFixed(1) }) }} />
</li>
</ul>
</div>
@@ -775,54 +777,47 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
<div className="mb-5 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<h4 className="text-sm font-semibold text-amber-800 mb-3 flex items-center gap-2">
<Scale className="w-4 h-4" />
IMPLICACION LEY 10/2025
{t('law10.responseSpeed.lawImplication')}
</h4>
<div className="space-y-3 text-sm text-gray-700">
<p>
<strong>Art. 8.2 requiere:</strong> "95% de llamadas atendidas en &lt;3 minutos"
</p>
<p dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.article82Requirement') }} />
<div className="p-3 bg-amber-100/50 rounded border border-amber-300">
<p className="font-medium text-amber-900 mb-1 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
LIMITACION DE DATOS
{t('law10.responseSpeed.dataLimitation')}
</p>
<p className="text-gray-600 text-xs">
Tu CDR actual NO incluye ASA (tiempo en cola antes de responder),
por lo que NO podemos medir este requisito directamente.
{t('law10.responseSpeed.noAsaData')}
</p>
</div>
<div className="border-t border-amber-200 pt-3">
<p className="font-medium text-gray-700 mb-2">PERO SI sabemos:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.responseSpeed.butWeKnow')}</p>
<ul className="space-y-1 text-gray-600">
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span><strong>{abandonRate.toFixed(1)}%</strong> de clientes abandonan Probablemente esperaron mucho</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.customersAbandon', { rate: abandonRate.toFixed(1) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span>Alta variabilidad AHT (P90/P50={ahtRatio.toFixed(1)}) Cola impredecible</span>
<span>{t('law10.responseSpeed.highVariability', { ratio: ahtRatio.toFixed(1) })}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span>Picos de abandono coinciden con picos de volumen</span>
<span>{t('law10.responseSpeed.peaksCoincide')}</span>
</li>
</ul>
</div>
<div className="p-3 bg-white rounded border border-gray-200">
<p className="text-xs text-gray-500 mb-1">Estimacion conservadora (±10% margen error):</p>
<p className="font-medium">
~<strong>{estimatedFastResponse.toFixed(0)}%</strong> de llamadas probablemente atendidas "rapido"
</p>
<p className="text-xs text-gray-500 mb-1">{t('law10.responseSpeed.conservativeEstimate')}</p>
<p className="font-medium" dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.likelyFastResponse', { percent: estimatedFastResponse.toFixed(0) }) }} />
<p className={cn(
'font-medium',
gapVs95 > 0 ? 'text-red-600' : 'text-emerald-600'
)}>
Gap vs 95% requerido: <strong>{gapVs95 > 0 ? '-' : '+'}{Math.abs(gapVs95).toFixed(0)}</strong> puntos porcentuales
</p>
)} dangerouslySetInnerHTML={{ __html: t('law10.responseSpeed.gapVs95', { operator: gapVs95 > 0 ? '-' : '+', gap: Math.abs(gapVs95).toFixed(0) }) }} />
</div>
</div>
</div>
@@ -831,32 +826,32 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
<div className="p-4 bg-purple-50 border border-purple-200 rounded-lg">
<h4 className="text-sm font-semibold text-purple-800 mb-3 flex items-center gap-2">
<Target className="w-4 h-4" />
ACCION SUGERIDA
{t('law10.responseSpeed.suggestedAction')}
</h4>
<div className="space-y-4 text-sm">
<div>
<p className="font-medium text-gray-700 mb-2">1. CORTO PLAZO: Reducir AHT para aumentar capacidad</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.responseSpeed.shortTerm')}</p>
<ul className="space-y-1 text-gray-600 ml-4">
<li> Tu Dimension 2 (Eficiencia) ya identifica:</li>
<li className="ml-4 text-xs">- AHT elevado ({Math.round(ahtP50)}s vs 380s benchmark)</li>
<li className="ml-4 text-xs">- Oportunidad Copilot IA: -18% AHT proyectado</li>
<li> Beneficio dual: AHT = capacidad = cola = ASA</li>
<li> {t('law10.responseSpeed.dimension2Identifies')}</li>
<li className="ml-4 text-xs">{t('law10.responseSpeed.highAht', { aht: Math.round(ahtP50) })}</li>
<li className="ml-4 text-xs">{t('law10.responseSpeed.copilotOpportunity')}</li>
<li> {t('law10.responseSpeed.dualBenefit')}</li>
</ul>
</div>
<div>
<p className="font-medium text-gray-700 mb-2">2. MEDIO PLAZO: Implementar tracking ASA real</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.responseSpeed.mediumTerm')}</p>
<div className="space-y-2 ml-4">
<div className="flex items-center justify-between p-2 bg-white rounded border border-gray-200">
<span className="text-gray-700">Configuracion en plataforma</span>
<span className="text-gray-700">{t('law10.responseSpeed.platformConfig')}</span>
<span className="font-semibold text-purple-600">5-8K</span>
</div>
<div className="flex items-center justify-between p-2 bg-white rounded border border-gray-200">
<span className="text-gray-700">Timeline implementacion</span>
<span className="font-semibold text-gray-600">4-6 semanas</span>
<span className="text-gray-700">{t('law10.responseSpeed.implementationTimeline')}</span>
<span className="font-semibold text-gray-600">{t('law10.responseSpeed.implementationWeeks')}</span>
</div>
<p className="text-xs text-gray-500">Beneficio: Medicion precisa para auditoria ENAC</p>
<p className="text-xs text-gray-500">{t('law10.responseSpeed.benefit')}</p>
</div>
</div>
</div>
@@ -867,6 +862,7 @@ function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: Co
// Seccion: Calidad de Resolucion (LAW-02)
function ResolutionQualitySection({ data, result }: { data: AnalysisData; result: ComplianceResult }) {
const { t } = useTranslation();
const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
// FCR Tecnico y Real
@@ -925,13 +921,13 @@ function ResolutionQualitySection({ data, result }: { data: AnalysisData; result
<Target className="w-5 h-5 text-emerald-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">Calidad de Resolucion: Efectividad</h3>
<p className="text-sm text-gray-500">Relacionado con Art. 17 - Resolucion en 15 dias</p>
<h3 className="font-semibold text-gray-900">{t('law10.resolutionQuality.title')}</h3>
<p className="text-sm text-gray-500">{t('law10.resolutionQuality.article')}</p>
</div>
</div>
<div className="flex items-center gap-2">
<StatusIcon status={result.status} />
<Badge label={getStatusLabel(result.status)} variant={getStatusBadgeVariant(result.status)} />
<Badge label={getStatusLabel(result.status, t)} variant={getStatusBadgeVariant(result.status)} />
</div>
</div>
@@ -939,7 +935,7 @@ function ResolutionQualitySection({ data, result }: { data: AnalysisData; result
<div className="mb-5">
<h4 className="text-sm font-semibold text-emerald-700 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
LO QUE SABEMOS
{t('law10.resolutionQuality.whatWeKnow')}
</h4>
{/* Metricas principales */}
@@ -952,21 +948,21 @@ function ResolutionQualitySection({ data, result }: { data: AnalysisData; result
'text-2xl font-bold',
avgFCRReal >= 60 ? 'text-gray-900' : 'text-red-600'
)}>{avgFCRReal.toFixed(0)}%</p>
<p className="text-xs text-gray-600">FCR Real (fcr_real_flag)</p>
<p className="text-xs text-gray-600">{t('law10.resolutionQuality.fcrReal')}</p>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">{recontactRate7d.toFixed(0)}%</p>
<p className="text-xs text-gray-600">Tasa recontacto 7 dias</p>
<p className="text-xs text-gray-600">{t('law10.resolutionQuality.recontactRate7d')}</p>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">{repeatCallsPct.toFixed(0)}%</p>
<p className="text-xs text-gray-600">Llamadas repetidas</p>
<p className="text-xs text-gray-600">{t('law10.resolutionQuality.repeatCalls')}</p>
</div>
</div>
{/* Grafico FCR por skill */}
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<p className="text-xs text-gray-500 mb-3 font-medium">FCR POR SKILL/QUEUE</p>
<p className="text-xs text-gray-500 mb-3 font-medium">{t('law10.resolutionQuality.fcrBySkill')}</p>
<div className="space-y-2">
{skillFCRData.slice(0, 8).map((s, idx) => (
<div key={idx} className="flex items-center gap-2">
@@ -994,22 +990,22 @@ function ResolutionQualitySection({ data, result }: { data: AnalysisData; result
))}
</div>
<div className="flex items-center gap-4 mt-3 text-[10px] text-gray-500">
<span>FCR:</span>
<span className="text-red-500"> &lt;45%</span>
<span className="text-amber-400"> 45-65%</span>
<span className="text-emerald-500"> &gt;75%</span>
<span>{t('law10.resolutionQuality.fcrLegend')}</span>
<span className="text-red-500" dangerouslySetInnerHTML={{ __html: `${t('law10.resolutionQuality.fcrLow')}` }} />
<span className="text-amber-400" dangerouslySetInnerHTML={{ __html: `${t('law10.resolutionQuality.fcrMedium')}` }} />
<span className="text-emerald-500" dangerouslySetInnerHTML={{ __html: `${t('law10.resolutionQuality.fcrHigh')}` }} />
</div>
</div>
{/* Top skills con FCR bajo */}
{lowFCRSkills.length > 0 && (
<div className="space-y-2 text-sm">
<p className="font-medium text-gray-700 mb-2">Top skills con FCR bajo:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.resolutionQuality.topLowFcr')}</p>
<ul className="space-y-1.5 text-gray-600">
{lowFCRSkills.map((s, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-gray-400">{idx + 1}.</span>
<span><strong>{s.skill}</strong>: {s.fcrReal.toFixed(0)}% FCR</span>
<span><strong>{s.skill}</strong>: {t('law10.resolutionQuality.fcrValue', { fcr: s.fcrReal.toFixed(0) })}</span>
</li>
))}
</ul>
@@ -1021,48 +1017,44 @@ function ResolutionQualitySection({ data, result }: { data: AnalysisData; result
<div className="mb-5 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<h4 className="text-sm font-semibold text-amber-800 mb-3 flex items-center gap-2">
<Scale className="w-4 h-4" />
IMPLICACION LEY 10/2025
{t('law10.resolutionQuality.lawImplication')}
</h4>
<div className="space-y-3 text-sm text-gray-700">
<p>
<strong>Art. 17 requiere:</strong> "Resolucion de reclamaciones ≤15 dias"
</p>
<p dangerouslySetInnerHTML={{ __html: t('law10.resolutionQuality.article17Requirement') }} />
<div className="p-3 bg-amber-100/50 rounded border border-amber-300">
<p className="font-medium text-amber-900 mb-1 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
LIMITACION DE DATOS
{t('law10.resolutionQuality.dataLimitation')}
</p>
<p className="text-gray-600 text-xs">
Tu CDR solo registra interacciones individuales, NO casos multi-touch
ni tiempo total de resolucion.
{t('law10.resolutionQuality.noCaseTracking')}
</p>
</div>
<div className="border-t border-amber-200 pt-3">
<p className="font-medium text-gray-700 mb-2">PERO SI sabemos:</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.resolutionQuality.butWeKnow')}</p>
<ul className="space-y-1 text-gray-600">
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span><strong>{recontactRate7d.toFixed(0)}%</strong> de casos requieren multiples contactos</span>
<span dangerouslySetInnerHTML={{ __html: t('law10.resolutionQuality.multipleContactsRequired', { percent: recontactRate7d.toFixed(0) }) }} />
</li>
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span>FCR {avgFCRReal.toFixed(0)}% = {recontactRate7d.toFixed(0)}% NO resuelto en primera interaccion</span>
<span>{t('law10.resolutionQuality.fcrGap', { fcr: avgFCRReal.toFixed(0), gap: recontactRate7d.toFixed(0) })}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span>Esto sugiere procesos complejos o informacion fragmentada</span>
<span>{t('law10.resolutionQuality.complexProcesses')}</span>
</li>
</ul>
</div>
<div className="p-3 bg-red-50 rounded border border-red-200">
<p className="font-medium text-red-800 mb-1">Senal de alerta:</p>
<p className="font-medium text-red-800 mb-1">{t('law10.resolutionQuality.alertSignal')}</p>
<p className="text-gray-600 text-xs">
Si los clientes recontactan multiples veces por el mismo tema, es probable
que el tiempo TOTAL de resolucion supere los 15 dias requeridos por ley.
{t('law10.resolutionQuality.resolutionTimeRisk')}
</p>
</div>
</div>
@@ -1072,30 +1064,30 @@ function ResolutionQualitySection({ data, result }: { data: AnalysisData; result
<div className="p-4 bg-emerald-50 border border-emerald-200 rounded-lg">
<h4 className="text-sm font-semibold text-emerald-800 mb-3 flex items-center gap-2">
<Target className="w-4 h-4" />
ACCION SUGERIDA
{t('law10.resolutionQuality.suggestedAction')}
</h4>
<div className="space-y-4 text-sm">
<div>
<p className="font-medium text-gray-700 mb-2">1. DIAGNOSTICO: Implementar sistema de casos/tickets</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.resolutionQuality.diagnosis')}</p>
<ul className="space-y-1 text-gray-600 ml-4">
<li> Registrar fecha apertura + cierre</li>
<li> Vincular multiples interacciones al mismo caso</li>
<li> Tipologia: consulta / reclamacion / incidencia</li>
<li> {t('law10.resolutionQuality.registerOpenClose')}</li>
<li> {t('law10.resolutionQuality.linkInteractions')}</li>
<li> {t('law10.resolutionQuality.typology')}</li>
</ul>
<div className="flex items-center justify-between p-2 bg-white rounded border border-gray-200 mt-2 ml-4">
<span className="text-gray-700">Inversion CRM/Ticketing</span>
<span className="text-gray-700">{t('law10.resolutionQuality.crmInvestment')}</span>
<span className="font-semibold text-emerald-600">15-25K</span>
</div>
</div>
<div>
<p className="font-medium text-gray-700 mb-2">2. MEJORA OPERATIVA: Aumentar FCR</p>
<p className="font-medium text-gray-700 mb-2">{t('law10.resolutionQuality.operationalImprovement')}</p>
<ul className="space-y-1 text-gray-600 ml-4">
<li> Tu Dimension 3 (Efectividad) ya identifica:</li>
<li className="ml-4 text-xs">- Root causes: info fragmentada, falta empowerment</li>
<li className="ml-4 text-xs">- Solucion: Knowledge base + decision trees</li>
<li> Beneficio: FCR = recontactos = tiempo total</li>
<li> {t('law10.resolutionQuality.dimension3Identifies')}</li>
<li className="ml-4 text-xs">{t('law10.resolutionQuality.rootCauses')}</li>
<li className="ml-4 text-xs">{t('law10.resolutionQuality.solution')}</li>
<li> {t('law10.resolutionQuality.fcrBenefit')}</li>
</ul>
</div>
</div>
@@ -1112,86 +1104,88 @@ function Law10SummaryRoadmap({
complianceResults: { law07: ComplianceResult; law01: ComplianceResult; law02: ComplianceResult; law09: ComplianceResult };
data: AnalysisData;
}) {
const { t } = useTranslation();
// Resultado por defecto para requisitos sin datos
const sinDatos: ComplianceResult = {
status: 'SIN_DATOS',
score: 0,
gap: 'Requiere datos',
details: ['No se dispone de datos para evaluar este requisito'],
gap: t('law10.common.requiredData'),
details: [t('law10.common.noData')],
};
// Todos los requisitos de la Ley 10/2025 con descripciones
const allRequirements = [
{
id: 'LAW-01',
name: 'Tiempo de Espera',
description: 'Tiempo maximo de espera de 3 minutos para atencion telefonica',
name: t('law10.summary.requirements.LAW-01.name'),
description: t('law10.summary.requirements.LAW-01.description'),
result: complianceResults.law01,
},
{
id: 'LAW-02',
name: 'Resolucion Efectiva',
description: 'Resolucion en primera contacto sin transferencias innecesarias',
name: t('law10.summary.requirements.LAW-02.name'),
description: t('law10.summary.requirements.LAW-02.description'),
result: complianceResults.law02,
},
{
id: 'LAW-03',
name: 'Acceso a Agente Humano',
description: 'Derecho a hablar con un agente humano en cualquier momento',
name: t('law10.summary.requirements.LAW-03.name'),
description: t('law10.summary.requirements.LAW-03.description'),
result: sinDatos,
},
{
id: 'LAW-04',
name: 'Grabacion de Llamadas',
description: 'Notificacion previa de grabacion y acceso a la misma',
name: t('law10.summary.requirements.LAW-04.name'),
description: t('law10.summary.requirements.LAW-04.description'),
result: sinDatos,
},
{
id: 'LAW-05',
name: 'Accesibilidad',
description: 'Canales accesibles para personas con discapacidad',
name: t('law10.summary.requirements.LAW-05.name'),
description: t('law10.summary.requirements.LAW-05.description'),
result: sinDatos,
},
{
id: 'LAW-06',
name: 'Confirmacion Escrita',
description: 'Confirmacion por escrito de reclamaciones y gestiones',
name: t('law10.summary.requirements.LAW-06.name'),
description: t('law10.summary.requirements.LAW-06.description'),
result: sinDatos,
},
{
id: 'LAW-07',
name: 'Cobertura Horaria',
description: 'Atencion 24/7 para servicios esenciales o horario ampliado',
name: t('law10.summary.requirements.LAW-07.name'),
description: t('law10.summary.requirements.LAW-07.description'),
result: complianceResults.law07,
},
{
id: 'LAW-08',
name: 'Formacion de Agentes',
description: 'Personal cualificado y formado en atencion al cliente',
name: t('law10.summary.requirements.LAW-08.name'),
description: t('law10.summary.requirements.LAW-08.description'),
result: sinDatos,
},
{
id: 'LAW-09',
name: 'Idiomas Cooficiales',
description: 'Atencion en catalan, euskera, gallego y valenciano',
name: t('law10.summary.requirements.LAW-09.name'),
description: t('law10.summary.requirements.LAW-09.description'),
result: complianceResults.law09,
},
{
id: 'LAW-10',
name: 'Plazos de Resolucion',
description: 'Resolucion de reclamaciones en maximo 15 dias habiles',
name: t('law10.summary.requirements.LAW-10.name'),
description: t('law10.summary.requirements.LAW-10.description'),
result: sinDatos,
},
{
id: 'LAW-11',
name: 'Gratuidad del Servicio',
description: 'Atencion telefonica sin coste adicional (numeros 900)',
name: t('law10.summary.requirements.LAW-11.name'),
description: t('law10.summary.requirements.LAW-11.description'),
result: sinDatos,
},
{
id: 'LAW-12',
name: 'Trazabilidad',
description: 'Numero de referencia para seguimiento de gestiones',
name: t('law10.summary.requirements.LAW-12.name'),
description: t('law10.summary.requirements.LAW-12.description'),
result: sinDatos,
},
];
@@ -1496,12 +1490,14 @@ function DataMaturitySummary({ data }: { data: AnalysisData }) {
// ============================================
export function Law10Tab({ data }: Law10TabProps) {
const { t } = useTranslation();
// Evaluar compliance para cada requisito
const complianceResults = {
law07: evaluateLaw07Compliance(data),
law01: evaluateLaw01Compliance(data),
law02: evaluateLaw02Compliance(data),
law09: evaluateLaw09Compliance(data),
law07: evaluateLaw07Compliance(data, t),
law01: evaluateLaw01Compliance(data, t),
law02: evaluateLaw02Compliance(data, t),
law09: evaluateLaw09Compliance(data, t),
};
return (

View File

@@ -5,6 +5,7 @@ import {
ArrowRight, Info, Users, Target, Zap, Shield,
ChevronDown, ChevronUp, BookOpen, Bot, Settings, Rocket, AlertCircle
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { RoadmapPhase } from '../../types';
import type { AnalysisData, RoadmapInitiative, HeatmapDataPoint, DrilldownDataPoint, OriginalQueueMetrics, AgenticTier } from '../../types';
import {
@@ -450,6 +451,8 @@ function OpportunityBubbleChart({
heatmapData: HeatmapDataPoint[];
drilldownData?: DrilldownDataPoint[]
}) {
const { t } = useTranslation();
// v3.5: Usar drilldownData si está disponible para tener info de Tier por cola
let chartData: BubbleDataPoint[] = [];
@@ -517,10 +520,10 @@ function OpportunityBubbleChart({
<div>
<h3 className="font-semibold text-gray-800 flex items-center gap-2">
<Target className="w-5 h-5 text-[#6D84E3]" />
Mapa de Oportunidades por Tier
{t('roadmap.opportunityMapTitle')}
</h3>
<p className="text-xs text-gray-500 mt-1">
Factibilidad (Score) vs Impacto Económico (Ahorro TCO) Tamaño = Volumen Color = Tier
{t('roadmap.opportunityMapSubtitle')}
</p>
</div>
</div>
@@ -529,12 +532,12 @@ function OpportunityBubbleChart({
<div className="relative" style={{ height: '340px' }}>
{/* Y-axis label */}
<div className="absolute -left-2 top-1/2 -translate-y-1/2 -rotate-90 text-xs text-gray-600 font-semibold whitespace-nowrap">
IMPACTO ECONÓMICO (Ahorro TCO /año)
{t('roadmap.economicImpactAxis')}
</div>
{/* X-axis label */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 text-xs text-gray-600 font-semibold">
FACTIBILIDAD (Agentic Readiness Score 0-10)
{t('roadmap.feasibilityAxis')}
</div>
{/* Chart area */}
@@ -553,7 +556,7 @@ function OpportunityBubbleChart({
style={{ left: `${(7.5 / 10) * 100}%` }}
>
<span className="absolute -top-5 left-1/2 -translate-x-1/2 text-[9px] text-emerald-600 font-medium whitespace-nowrap">
Tier AUTOMATE 7.5
{t('roadmap.tierAutomateThreshold')}
</span>
</div>
@@ -566,27 +569,27 @@ function OpportunityBubbleChart({
{/* Quadrant labels - basados en Score (X) y Ahorro (Y) */}
{/* Top-right: High Score + High Savings = QUICK WINS */}
<div className="absolute top-2 right-2 text-xs bg-emerald-100 px-2.5 py-1.5 rounded-lg border-2 border-emerald-400 shadow-sm">
<div className="font-bold text-emerald-700">🎯 QUICK WINS</div>
<div className="text-[9px] text-emerald-600">Score 7.5 + Ahorro alto</div>
<div className="text-[9px] text-emerald-500 font-medium"> Prioridad 1</div>
<div className="font-bold text-emerald-700">🎯 {t('roadmap.quadrantQuickWins')}</div>
<div className="text-[9px] text-emerald-600">{t('roadmap.quadrantQuickWinsDesc')}</div>
<div className="text-[9px] text-emerald-500 font-medium">{t('roadmap.quadrantQuickWinsPriority')}</div>
</div>
{/* Top-left: Low Score + High Savings = OPTIMIZE */}
<div className="absolute top-2 left-2 text-xs bg-amber-100 px-2.5 py-1.5 rounded-lg border-2 border-amber-400 shadow-sm">
<div className="font-bold text-amber-700"> OPTIMIZE</div>
<div className="text-[9px] text-amber-600">Score &lt;7.5 + Ahorro alto</div>
<div className="text-[9px] text-amber-500 font-medium"> Wave 1 primero</div>
<div className="font-bold text-amber-700"> {t('roadmap.quadrantOptimize')}</div>
<div className="text-[9px] text-amber-600">{t('roadmap.quadrantOptimizeDesc')}</div>
<div className="text-[9px] text-amber-500 font-medium">{t('roadmap.quadrantOptimizePriority')}</div>
</div>
{/* Bottom-right: High Score + Low Savings = STRATEGIC */}
<div className="absolute bottom-10 right-2 text-xs bg-blue-100 px-2.5 py-1.5 rounded-lg border-2 border-blue-400 shadow-sm">
<div className="font-bold text-blue-700">📊 STRATEGIC</div>
<div className="text-[9px] text-blue-600">Score 7.5 + Ahorro bajo</div>
<div className="text-[9px] text-blue-500 font-medium"> Evaluar ROI</div>
<div className="font-bold text-blue-700">📊 {t('roadmap.quadrantStrategic')}</div>
<div className="text-[9px] text-blue-600">{t('roadmap.quadrantStrategicDesc')}</div>
<div className="text-[9px] text-blue-500 font-medium">{t('roadmap.quadrantStrategicPriority')}</div>
</div>
{/* Bottom-left: Low Score + Low Savings = DEFER */}
<div className="absolute bottom-10 left-2 text-xs bg-gray-100 px-2.5 py-1.5 rounded-lg border-2 border-gray-300 shadow-sm">
<div className="font-bold text-gray-600">📋 DEFER</div>
<div className="text-[9px] text-gray-500">Score &lt;7.5 + Ahorro bajo</div>
<div className="text-[9px] text-gray-400 font-medium"> Backlog</div>
<div className="font-bold text-gray-600">📋 {t('roadmap.quadrantDefer')}</div>
<div className="text-[9px] text-gray-500">{t('roadmap.quadrantDeferDesc')}</div>
<div className="text-[9px] text-gray-400 font-medium">{t('roadmap.quadrantDeferPriority')}</div>
</div>
{/* Bubbles */}
@@ -629,19 +632,19 @@ function OpportunityBubbleChart({
<div className="font-semibold text-sm">{item.name}</div>
<div className="mt-1 space-y-0.5">
<div className="flex items-center gap-2">
<span className="text-gray-400">Score:</span>
<span className="text-gray-400">{t('roadmap.tooltipScore')}</span>
<span className="font-medium">{item.feasibility.toFixed(1)}/10</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400">Volumen:</span>
<span className="font-medium">{item.volume.toLocaleString()}/mes</span>
<span className="text-gray-400">{t('roadmap.tooltipVolume')}</span>
<span className="font-medium">{item.volume.toLocaleString()}{t('roadmap.perMonth')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400">Ahorro TCO:</span>
<span className="font-medium text-emerald-400">{formatCurrency(item.economicImpact)}/año</span>
<span className="text-gray-400">{t('roadmap.tooltipSavingsTco')}</span>
<span className="font-medium text-emerald-400">{formatCurrency(item.economicImpact)}{t('roadmap.perYear')}</span>
</div>
<div className="flex items-center gap-2 pt-1 border-t border-gray-600">
<span className="text-gray-400">Tier:</span>
<span className="text-gray-400">{t('roadmap.tooltipTier')}</span>
<span
className="px-1.5 py-0.5 rounded text-[10px] font-medium"
style={{ backgroundColor: tierColor.fill }}
@@ -705,12 +708,11 @@ function OpportunityBubbleChart({
{/* Leyenda por Tier */}
<div className="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
<p className="text-xs text-gray-700 mb-2">
<span className="font-semibold" style={{ color: '#6d84e3' }}>Interpretación:</span> Las burbujas en el cuadrante superior derecho (Score alto + Ahorro alto)
son Quick Wins para automatización. El tamaño indica volumen de interacciones.
<span className="font-semibold" style={{ color: '#6d84e3' }}>{t('roadmap.interpretation')}</span> {t('roadmap.opportunityMapInterpretation')}
</p>
<div className="flex flex-wrap items-center gap-4 text-[10px] text-gray-600 pt-2 border-t border-gray-200">
<div className="flex items-center gap-1">
<span className="font-medium">Tamaño:</span> Volumen
<span className="font-medium">{t('roadmap.bubbleSize')}</span> {t('roadmap.volume')}
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: TIER_COLORS.AUTOMATE.fill }} />
@@ -735,29 +737,29 @@ function OpportunityBubbleChart({
<div className="mt-4 p-4 bg-white rounded-lg border border-gray-300 shadow-sm">
<h4 className="text-sm font-semibold text-gray-800 mb-3 flex items-center gap-2">
<Info className="w-4 h-4 text-[#6D84E3]" />
Metodología de Cálculo
{t('roadmap.methodologyTitle')}
</h4>
{/* Ejes */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="p-3 bg-gray-50 rounded border border-gray-200">
<h5 className="text-xs font-semibold text-gray-700 mb-2">📊 Eje X: FACTIBILIDAD (Score 0-10)</h5>
<h5 className="text-xs font-semibold text-gray-700 mb-2">📊 {t('roadmap.axisXFactibility')}</h5>
<p className="text-[11px] text-gray-600 mb-2">
Score Agentic Readiness calculado con 5 factores ponderados:
{t('roadmap.axisXFactibilityDesc')}
</p>
<ul className="text-[10px] text-gray-500 space-y-1 ml-2">
<li> <strong>Predictibilidad (30%)</strong>: basado en CV AHT</li>
<li> <strong>Resolutividad (25%)</strong>: FCR (60%) + Transfer (40%)</li>
<li> <strong>Volumen (25%)</strong>: escala logarítmica del volumen</li>
<li> <strong>Calidad Datos (10%)</strong>: % registros válidos</li>
<li> <strong>Simplicidad (10%)</strong>: basado en AHT</li>
<li> <strong>{t('roadmap.factorPredictability')}</strong>: {t('roadmap.factorPredictabilityDesc')}</li>
<li> <strong>{t('roadmap.factorResolution')}</strong>: {t('roadmap.factorResolutionDesc')}</li>
<li> <strong>{t('roadmap.factorVolumeWeight')}</strong>: {t('roadmap.factorVolumeDesc')}</li>
<li> <strong>{t('roadmap.factorDataQuality')}</strong>: {t('roadmap.factorDataQualityDesc')}</li>
<li> <strong>{t('roadmap.factorSimplicity')}</strong>: {t('roadmap.factorSimplicityDesc')}</li>
</ul>
</div>
<div className="p-3 bg-gray-50 rounded border border-gray-200">
<h5 className="text-xs font-semibold text-gray-700 mb-2">💰 Eje Y: IMPACTO ECONÓMICO (/año)</h5>
<h5 className="text-xs font-semibold text-gray-700 mb-2">💰 {t('roadmap.axisYEconomicImpact')}</h5>
<p className="text-[11px] text-gray-600 mb-2">
Ahorro TCO calculado según tier con CPI diferencial:
{t('roadmap.axisYEconomicImpactDesc')}
</p>
<div className="text-[10px] text-gray-500 space-y-1 ml-2">
<p className="font-mono bg-gray-100 px-1 py-0.5 rounded text-[9px]">
@@ -778,49 +780,49 @@ function OpportunityBubbleChart({
{/* Fórmulas por Tier */}
<div className="p-3 bg-gradient-to-r from-slate-50 to-white rounded border border-gray-200">
<h5 className="text-xs font-semibold text-gray-700 mb-3">🧮 Fórmulas de Ahorro por Tier</h5>
<h5 className="text-xs font-semibold text-gray-700 mb-3">🧮 {t('roadmap.savingsFormulas')}</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-[10px]">
<div className="flex items-start gap-2">
<div className="w-3 h-3 rounded-full flex-shrink-0 mt-0.5" style={{ backgroundColor: TIER_COLORS.AUTOMATE.fill }} />
<div>
<p className="font-semibold text-emerald-700">AUTOMATE (Score 7.5)</p>
<p className="font-semibold text-emerald-700">{t('roadmap.formulaAutomate')}</p>
<p className="font-mono text-gray-600 mt-0.5">
Ahorro = Vol × 12 × <strong>70%</strong> × (2.33 - 0.15)
{t('roadmap.formulaAutomateCalc')}
</p>
<p className="text-gray-500">= Vol × 12 × 0.70 × 2.18</p>
<p className="text-gray-500">{t('roadmap.formulaAutomateResult')}</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="w-3 h-3 rounded-full flex-shrink-0 mt-0.5" style={{ backgroundColor: TIER_COLORS.ASSIST.fill }} />
<div>
<p className="font-semibold text-blue-700">ASSIST (Score 5.5)</p>
<p className="font-semibold text-blue-700">{t('roadmap.formulaAssist')}</p>
<p className="font-mono text-gray-600 mt-0.5">
Ahorro = Vol × 12 × <strong>30%</strong> × (2.33 - 1.50)
{t('roadmap.formulaAssistCalc')}
</p>
<p className="text-gray-500">= Vol × 12 × 0.30 × 0.83</p>
<p className="text-gray-500">{t('roadmap.formulaAssistResult')}</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="w-3 h-3 rounded-full flex-shrink-0 mt-0.5" style={{ backgroundColor: TIER_COLORS.AUGMENT.fill }} />
<div>
<p className="font-semibold text-amber-700">AUGMENT (Score 3.5)</p>
<p className="font-semibold text-amber-700">{t('roadmap.formulaAugment')}</p>
<p className="font-mono text-gray-600 mt-0.5">
Ahorro = Vol × 12 × <strong>15%</strong> × (2.33 - 2.00)
{t('roadmap.formulaAugmentCalc')}
</p>
<p className="text-gray-500">= Vol × 12 × 0.15 × 0.33</p>
<p className="text-gray-500">{t('roadmap.formulaAugmentResult')}</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="w-3 h-3 rounded-full flex-shrink-0 mt-0.5" style={{ backgroundColor: TIER_COLORS['HUMAN-ONLY'].fill }} />
<div>
<p className="font-semibold text-red-700">HUMAN-ONLY (Score &lt; 3.5 o Red Flags)</p>
<p className="font-semibold text-red-700">{t('roadmap.formulaHumanOnly')}</p>
<p className="font-mono text-gray-600 mt-0.5">
Ahorro = <strong>0</strong>
{t('roadmap.formulaHumanOnlyCalc')}
</p>
<p className="text-gray-500">Requiere estandarización previa</p>
<p className="text-gray-500">{t('roadmap.formulaHumanOnlyRequires')}</p>
</div>
</div>
</div>
@@ -828,7 +830,7 @@ function OpportunityBubbleChart({
{/* Clasificación de Tier */}
<div className="mt-3 p-3 bg-gray-50 rounded border border-gray-200">
<h5 className="text-xs font-semibold text-gray-700 mb-2">🏷 Criterios de Clasificación de Tier</h5>
<h5 className="text-xs font-semibold text-gray-700 mb-2">🏷 {t('roadmap.tierClassificationCriteria')}</h5>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-[10px]">
<div className="p-2 bg-emerald-50 rounded border border-emerald-200">
<p className="font-semibold text-emerald-700">AUTOMATE</p>
@@ -871,9 +873,7 @@ function OpportunityBubbleChart({
{/* Nota metodológica */}
<p className="text-[10px] text-gray-500 mt-3 italic">
<strong>Nota:</strong> El tamaño de las burbujas representa el volumen de interacciones.
Las colas clasificadas como HUMAN-ONLY no aparecen en el gráfico (ahorro = 0).
Los ahorros son proyecciones basadas en benchmarks de industria y deben validarse con pilotos.
<strong>{t('roadmap.methodologicalNote')}</strong> {t('roadmap.methodologicalNoteText')}
</p>
</div>
</div>
@@ -954,6 +954,7 @@ function WaveCard({
exitCriteria?: WaveExitCriteria;
priorityQueues?: PriorityQueue[];
}) {
const { t } = useTranslation();
const [expanded, setExpanded] = React.useState(false);
const margenAnual = wave.ahorroAnual - wave.costoRecurrenteAnual;
@@ -990,7 +991,7 @@ function WaveCard({
<h3 className="font-bold text-gray-800">{wave.titulo}</h3>
{wave.esCondicional && (
<span className="text-[10px] bg-amber-200 text-amber-800 px-2 py-0.5 rounded-full font-medium">
Condicional
{t('roadmap.conditional')}
</span>
)}
</div>
@@ -998,7 +999,7 @@ function WaveCard({
</div>
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${riesgoColors[wave.riesgo]}`}>
{riesgoIcons[wave.riesgo]} Riesgo {wave.riesgo}
{riesgoIcons[wave.riesgo]} {t('roadmap.risk')} {t(`roadmap.risk${wave.riesgo.charAt(0).toUpperCase() + wave.riesgo.slice(1)}`)}
</span>
</div>
</div>
@@ -1056,7 +1057,7 @@ function WaveCard({
{/* Por qué es necesario */}
<div className="p-3 bg-gray-50 rounded-lg border border-gray-100">
<p className="text-xs text-gray-500 font-medium mb-1">🎯 Por qué es necesario:</p>
<p className="text-xs text-gray-500 font-medium mb-1">🎯 {t('roadmap.whyNecessary')}</p>
<p className="text-sm text-gray-700">{wave.porQueNecesario}</p>
</div>
@@ -1169,7 +1170,7 @@ function WaveCard({
onClick={() => setExpanded(!expanded)}
className="w-full text-sm text-gray-500 hover:text-gray-700 flex items-center justify-center gap-1 py-2 border-t border-gray-100"
>
{expanded ? 'Ocultar detalles' : 'Ver iniciativas y criterios'}
{expanded ? t('roadmap.hideDetails') : t('roadmap.viewInitiatives')}
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
@@ -1678,6 +1679,8 @@ function RoadmapTimeline({ waves }: { waves: WaveData[] }) {
// ========== COMPONENTE PRINCIPAL: ROADMAP TAB ==========
export function RoadmapTab({ data }: RoadmapTabProps) {
const { t } = useTranslation();
// Analizar datos de heatmap para determinar skills listos
const heatmapData = data.heatmapData || [];
@@ -2266,10 +2269,10 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<div className="px-5 py-4 border-b border-gray-200">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<Target className="w-5 h-5 text-blue-600" />
Clasificación por Potencial de Automatización
{t('roadmap.classificationByAutomationTier')}
</h3>
<p className="text-xs text-gray-500 mt-1">
{totalQueues} colas clasificadas en 4 Tiers según su preparación para IA {totalVolume.toLocaleString()} interacciones/mes
{t('roadmap.queuesClassifiedDescription', { count: totalQueues, volume: totalVolume.toLocaleString() })}
</p>
</div>
@@ -2283,20 +2286,20 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<Rocket className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-[10px] text-emerald-600 font-medium">TIER 1</p>
<p className="text-[10px] text-emerald-600 font-medium">{t('roadmap.tier1')}</p>
<p className="text-xs font-bold text-emerald-800">AUTOMATE</p>
</div>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-emerald-700">{tierCounts.AUTOMATE.length}</p>
<p className="text-[10px] text-emerald-600">
{tierVolumes.AUTOMATE.toLocaleString()} int/mes
{tierVolumes.AUTOMATE.toLocaleString()} {t('roadmap.intPerMonth')}
</p>
<p className="text-[10px] text-emerald-500">
({Math.round((tierVolumes.AUTOMATE / totalVolume) * 100)}% volumen)
{t('roadmap.volumePercentage', { pct: Math.round((tierVolumes.AUTOMATE / totalVolume) * 100) })}
</p>
<p className="text-xs font-semibold text-emerald-700 pt-1 border-t border-emerald-200">
{formatCurrency(potentialSavings.AUTOMATE)}/año
{formatCurrency(potentialSavings.AUTOMATE)}{t('roadmap.perYear')}
</p>
</div>
</div>
@@ -2308,20 +2311,20 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<Bot className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-[10px] text-blue-600 font-medium">TIER 2</p>
<p className="text-[10px] text-blue-600 font-medium">{t('roadmap.tier2')}</p>
<p className="text-xs font-bold text-blue-800">ASSIST</p>
</div>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-blue-700">{tierCounts.ASSIST.length}</p>
<p className="text-[10px] text-blue-600">
{tierVolumes.ASSIST.toLocaleString()} int/mes
{tierVolumes.ASSIST.toLocaleString()} {t('roadmap.intPerMonth')}
</p>
<p className="text-[10px] text-blue-500">
({Math.round((tierVolumes.ASSIST / totalVolume) * 100)}% volumen)
{t('roadmap.volumePercentage', { pct: Math.round((tierVolumes.ASSIST / totalVolume) * 100) })}
</p>
<p className="text-xs font-semibold text-blue-700 pt-1 border-t border-blue-200">
{formatCurrency(potentialSavings.ASSIST)}/año
{formatCurrency(potentialSavings.ASSIST)}{t('roadmap.perYear')}
</p>
</div>
</div>
@@ -2333,20 +2336,20 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<TrendingUp className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-[10px] text-amber-600 font-medium">TIER 3</p>
<p className="text-[10px] text-amber-600 font-medium">{t('roadmap.tier3')}</p>
<p className="text-xs font-bold text-amber-800">AUGMENT</p>
</div>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-amber-700">{tierCounts.AUGMENT.length}</p>
<p className="text-[10px] text-amber-600">
{tierVolumes.AUGMENT.toLocaleString()} int/mes
{tierVolumes.AUGMENT.toLocaleString()} {t('roadmap.intPerMonth')}
</p>
<p className="text-[10px] text-amber-500">
({Math.round((tierVolumes.AUGMENT / totalVolume) * 100)}% volumen)
{t('roadmap.volumePercentage', { pct: Math.round((tierVolumes.AUGMENT / totalVolume) * 100) })}
</p>
<p className="text-xs font-semibold text-amber-700 pt-1 border-t border-amber-200">
{formatCurrency(potentialSavings.AUGMENT)}/año
{formatCurrency(potentialSavings.AUGMENT)}{t('roadmap.perYear')}
</p>
</div>
</div>
@@ -2358,20 +2361,20 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<Users className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-[10px] text-red-600 font-medium">TIER 4</p>
<p className="text-[10px] text-red-600 font-medium">{t('roadmap.tier4')}</p>
<p className="text-xs font-bold text-red-800">HUMAN-ONLY</p>
</div>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-red-700">{tierCounts['HUMAN-ONLY'].length}</p>
<p className="text-[10px] text-red-600">
{tierVolumes['HUMAN-ONLY'].toLocaleString()} int/mes
{tierVolumes['HUMAN-ONLY'].toLocaleString()} {t('roadmap.intPerMonth')}
</p>
<p className="text-[10px] text-red-500">
({Math.round((tierVolumes['HUMAN-ONLY'] / totalVolume) * 100)}% volumen)
{t('roadmap.volumePercentage', { pct: Math.round((tierVolumes['HUMAN-ONLY'] / totalVolume) * 100) })}
</p>
<p className="text-xs font-semibold text-red-700 pt-1 border-t border-red-200">
0/año (Red flags)
{t('roadmap.noSavingsRedFlags')}
</p>
</div>
</div>
@@ -2379,7 +2382,7 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
{/* Barra de distribución visual */}
<div className="bg-white rounded-lg p-3 border border-gray-200">
<p className="text-xs text-gray-500 font-medium mb-2">Distribución del volumen por tier:</p>
<p className="text-xs text-gray-500 font-medium mb-2">{t('roadmap.volumeDistributionByTier')}</p>
<div className="h-6 rounded-full overflow-hidden flex">
{tierVolumes.AUTOMATE > 0 && (
<div
@@ -2501,16 +2504,16 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
// Configuración simplificada por tipo
const typeConfig = {
DUAL: {
label: 'Nuestra Recomendación: Estrategia Dual',
sublabel: 'Ejecutar dos líneas de trabajo en paralelo para maximizar el impacto'
label: t('roadmap.dualStrategyLabel'),
sublabel: t('roadmap.dualStrategySublabel')
},
FOUNDATION: {
label: 'Nuestra Recomendación: Foundation First',
sublabel: 'Preparar la operación antes de automatizar'
label: t('roadmap.foundationFirstLabel'),
sublabel: t('roadmap.foundationFirstSublabel')
},
STANDARDIZATION: {
label: 'Nuestra Recomendación: Estandarización',
sublabel: 'Resolver problemas operativos críticos antes de invertir en IA'
label: t('roadmap.standardizationLabel'),
sublabel: t('roadmap.standardizationSublabel')
}
};
@@ -2543,35 +2546,32 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<>
{/* Explicación */}
<div className="p-3 bg-gray-50 rounded-lg mb-3">
<p className="font-semibold text-gray-800 mb-1">¿Qué significa Foundation?</p>
<p className="font-semibold text-gray-800 mb-1">{t('roadmap.whatIsFoundation')}</p>
<p className="text-xs text-gray-600">
La operación actual no tiene colas listas para automatizar directamente.
Foundation es la fase de preparación: estandarizar procesos, reducir variabilidad
y mejorar la calidad de datos para que la automatización posterior sea efectiva.
{t('roadmap.foundationExplanation')}
</p>
</div>
<p className="text-sm text-gray-600 mb-3">
{tierCounts.ASSIST.length} colas ASSIST ({Math.round(assistPct)}% del volumen)
pueden elevarse a Tier AUTOMATE tras completar Wave 1-2.
{t('roadmap.assistQueuesCanElevate', { count: tierCounts.ASSIST.length, pct: Math.round(assistPct) })}
</p>
<div className="grid grid-cols-3 gap-4 text-sm border-t border-gray-100 pt-3">
<div>
<p className="text-xs text-gray-500">Inversión</p>
<p className="text-xs text-gray-500">{t('roadmap.investment')}</p>
<p className="font-semibold text-gray-800">{formatCurrency(wave1Setup + wave2Setup)}</p>
</div>
<div>
<p className="text-xs text-gray-500">Timeline</p>
<p className="font-semibold text-gray-800">6-9 meses</p>
<p className="text-xs text-gray-500">{t('roadmap.timeline')}</p>
<p className="font-semibold text-gray-800">6-9 {t('roadmap.months', { count: 6 }).toLowerCase()}</p>
</div>
<div>
<p className="text-xs text-gray-500">Ahorro habilitado</p>
<p className="font-semibold text-gray-800">{formatCurrency(potentialSavings.ASSIST)}/año</p>
<p className="text-xs text-gray-500">{t('roadmap.enabledSavings')}</p>
<p className="font-semibold text-gray-800">{formatCurrency(potentialSavings.ASSIST)}{t('roadmap.perYear')}</p>
</div>
</div>
<div className="text-xs text-gray-500 border-t border-gray-100 pt-3 mt-3">
<strong className="text-gray-700">Criterios para pasar a automatización:</strong> CV 90% · Transfer 30% · AHT -15%
<strong className="text-gray-700">{t('roadmap.criteriaForAutomation')}</strong> {t('roadmap.criteriaForAutomationValues')}
</div>
</>
)}
@@ -2581,51 +2581,48 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<>
{/* Explicación */}
<div className="p-3 bg-gray-50 rounded-lg mb-3">
<p className="font-semibold text-gray-800 mb-1">¿Por qué estandarización primero?</p>
<p className="font-semibold text-gray-800 mb-1">{t('roadmap.whyStandardizationFirst')}</p>
<p className="text-xs text-gray-600">
Se han detectado "red flags" operativos críticos (alta variabilidad, muchas transferencias)
que harían fracasar cualquier proyecto de automatización. Invertir en IA ahora sería
malgastar recursos. Primero hay que estabilizar la operación.
{t('roadmap.standardizationExplanation')}
</p>
</div>
<p className="text-sm text-gray-600 mb-3">
{Math.round(humanOnlyPct + augmentPct)}% del volumen presenta red flags (CV &gt;75%, Transfer &gt;20%).
Wave 1 es una inversión habilitadora sin retorno directo inmediato.
{t('roadmap.volumeWithRedFlags', { pct: Math.round(humanOnlyPct + augmentPct) })}
</p>
<div className="grid grid-cols-3 gap-4 text-sm border-t border-gray-100 pt-3">
<div>
<p className="text-xs text-gray-500">Inversión Wave 1</p>
<p className="text-xs text-gray-500">{t('roadmap.investmentWave1')}</p>
<p className="font-semibold text-gray-800">{formatCurrency(wave1Setup)}</p>
</div>
<div>
<p className="text-xs text-gray-500">Timeline</p>
<p className="font-semibold text-gray-800">3-4 meses</p>
<p className="text-xs text-gray-500">{t('roadmap.timeline')}</p>
<p className="font-semibold text-gray-800">3-4 {t('roadmap.months', { count: 3 }).toLowerCase()}</p>
</div>
<div>
<p className="text-xs text-gray-500">Ahorro directo</p>
<p className="font-semibold text-gray-500">0 (habilitador)</p>
<p className="text-xs text-gray-500">{t('roadmap.directSavings')}</p>
<p className="font-semibold text-gray-500">{t('roadmap.enablingNoDirectSavings')}</p>
</div>
</div>
<div className="text-xs text-gray-500 border-t border-gray-100 pt-3 mt-3">
<strong className="text-gray-700">Objetivo:</strong> Reducir red flags en las {Math.min(10, tierCounts['HUMAN-ONLY'].length + tierCounts.AUGMENT.length)} colas principales. Reevaluar tras completar.
<strong className="text-gray-700">{t('roadmap.objective')}</strong> {t('roadmap.objectiveReduceRedFlags', { count: Math.min(10, tierCounts['HUMAN-ONLY'].length + tierCounts.AUGMENT.length) })}
</div>
</>
)}
{/* Siguiente paso */}
<div className="border-t border-gray-200 pt-3 mt-3">
<p className="text-xs text-gray-500 mb-1">Siguiente paso recomendado:</p>
<p className="text-xs text-gray-500 mb-1">{t('roadmap.nextRecommendedStep')}</p>
<p className="text-sm text-gray-700">
{recType === 'DUAL' && (
<>Iniciar piloto de automatización con las {pilotQueues.length} colas AUTOMATE, mientras se ejecuta Wave 1 (Foundation) en paralelo para preparar el resto.</>
t('roadmap.nextStepDual', { count: pilotQueues.length })
)}
{recType === 'FOUNDATION' && (
<>Comenzar Wave 1 focalizando en las {Math.min(10, tierCounts['HUMAN-ONLY'].length)} colas de mayor volumen. Medir progreso mensual en CV y Transfer.</>
t('roadmap.nextStepFoundation', { count: Math.min(10, tierCounts['HUMAN-ONLY'].length) })
)}
{recType === 'STANDARDIZATION' && (
<>Realizar workshop de diagnóstico operacional para identificar las causas raíz de los red flags antes de planificar inversiones.</>
t('roadmap.nextStepStandardization')
)}
</p>
</div>
@@ -2653,13 +2650,13 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
<div className="flex items-center gap-3">
<BookOpen className="w-5 h-5 text-blue-500" />
<div className="text-left">
<h3 className="font-semibold text-gray-800">Detalle por Wave</h3>
<p className="text-xs text-gray-500">Iniciativas, criterios de entrada/salida, inversión por fase</p>
<h3 className="font-semibold text-gray-800">{t('roadmap.waveDetail')}</h3>
<p className="text-xs text-gray-500">{t('roadmap.waveDetailDescription')}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400">
{waveDetailExpanded ? 'Ocultar detalle' : 'Ver detalle'}
{waveDetailExpanded ? t('roadmap.hideDetail') : t('roadmap.viewDetail')}
</span>
{waveDetailExpanded ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
@@ -2678,7 +2675,7 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
onClick={() => setShowAllWaves(!showAllWaves)}
className="text-xs text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1"
>
{showAllWaves ? 'Colapsar todas' : 'Expandir todas'}
{showAllWaves ? t('roadmap.collapseAll') : t('roadmap.expandAll')}
{showAllWaves ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button>
</div>