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:
@@ -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 <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">▁ <8%</span>
|
||||
<span className="text-amber-400">▃ 8-15%</span>
|
||||
<span className="text-red-500">█ >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 <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">▁ <45%</span>
|
||||
<span className="text-amber-400">▃ 45-65%</span>
|
||||
<span className="text-emerald-500">█ >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 (
|
||||
|
||||
@@ -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 <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 <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 < 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 >75%, Transfer >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>
|
||||
|
||||
Reference in New Issue
Block a user