import React from 'react';
import {
Scale,
Clock,
Target,
Calendar,
AlertTriangle,
CheckCircle,
XCircle,
HelpCircle,
TrendingUp,
FileText,
Lightbulb,
} from 'lucide-react';
import type { AnalysisData, HeatmapDataPoint, DrilldownDataPoint } from '../../types';
import {
Card,
Badge,
Stat,
} from '../ui';
import {
cn,
STATUS_CLASSES,
formatCurrency,
formatNumber,
} from '../../config/designSystem';
// ============================================
// TIPOS Y CONSTANTES
// ============================================
type ComplianceStatus = 'CUMPLE' | 'PARCIAL' | 'NO_CUMPLE' | 'SIN_DATOS';
interface ComplianceResult {
status: ComplianceStatus;
score: number; // 0-100
gap: string;
details: string[];
}
const LAW_10_2025 = {
deadline: new Date('2026-12-28'),
requirements: {
LAW_07: {
name: 'Cobertura Horaria',
maxOffHoursPct: 15,
},
LAW_01: {
name: 'Velocidad de Respuesta',
maxHoldTimeSeconds: 180,
},
LAW_02: {
name: 'Calidad de Resolucion',
minFCR: 75,
maxTransfer: 15,
},
LAW_09: {
name: 'Cobertura Linguistica',
languages: ['es', 'ca', 'eu', 'gl', 'va'],
},
},
};
// ============================================
// FUNCIONES DE EVALUACION DE COMPLIANCE
// ============================================
function evaluateLaw07Compliance(data: AnalysisData): 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;
if (offHoursPct === null) {
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Sin datos de distribucion horaria',
details: ['No se encontraron datos de distribucion horaria en el analisis'],
};
}
const details: string[] = [];
details.push(`${offHoursPct.toFixed(1)}% de interacciones fuera de horario laboral`);
if (offHoursPct < 5) {
return {
status: 'CUMPLE',
score: 100,
gap: '-',
details: [...details, 'Cobertura horaria adecuada'],
};
} 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'],
};
} 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'],
};
}
}
function evaluateLaw01Compliance(data: AnalysisData): ComplianceResult {
// Evaluar tiempo de espera (hold_time) vs limite de 180 segundos
const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
if (totalVolume === 0) {
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Sin datos de tiempos de espera',
details: ['No se encontraron datos de hold_time en el analisis'],
};
}
// Calcular hold_time promedio ponderado por volumen
const avgHoldTime = data.heatmapData.reduce(
(sum, h) => sum + h.metrics.hold_time * h.volume, 0
) / totalVolume;
// Contar colas que exceden el limite
const colasExceden = data.heatmapData.filter(h => h.metrics.hold_time > 180);
const pctColasExceden = (colasExceden.length / data.heatmapData.length) * 100;
// Calcular % de interacciones dentro del limite
const volDentroLimite = data.heatmapData
.filter(h => h.metrics.hold_time <= 180)
.reduce((sum, h) => sum + h.volume, 0);
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`);
if (avgHoldTime < 180 && pctColasExceden < 10) {
return {
status: 'CUMPLE',
score: 100,
gap: `-${Math.round(180 - avgHoldTime)}s`,
details,
};
} else if (avgHoldTime < 180) {
return {
status: 'PARCIAL',
score: Math.round(90 - pctColasExceden),
gap: `${colasExceden.length} colas fuera`,
details,
};
} else {
return {
status: 'NO_CUMPLE',
score: Math.max(0, Math.round(50 - ((avgHoldTime - 180) / 60) * 25)),
gap: `+${Math.round(avgHoldTime - 180)}s`,
details,
};
}
}
function evaluateLaw02Compliance(data: AnalysisData): ComplianceResult {
// Evaluar FCR y tasa de transferencia
const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0);
if (totalVolume === 0) {
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Sin datos de resolucion',
details: ['No se encontraron datos de FCR o transferencias'],
};
}
// FCR Tecnico ponderado (comparable con benchmarks)
const avgFCR = data.heatmapData.reduce(
(sum, h) => sum + (h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate)) * h.volume, 0
) / totalVolume;
// Transfer rate ponderado
const avgTransfer = data.heatmapData.reduce(
(sum, h) => sum + h.metrics.transfer_rate * h.volume, 0
) / totalVolume;
const details: string[] = [];
details.push(`FCR Tecnico: ${avgFCR.toFixed(1)}% (objetivo: >75%)`);
details.push(`Tasa de transferencia: ${avgTransfer.toFixed(1)}% (objetivo: <15%)`);
// 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%`);
}
const cumpleFCR = avgFCR >= 75;
const cumpleTransfer = avgTransfer <= 15;
const parcialFCR = avgFCR >= 60;
const parcialTransfer = avgTransfer <= 25;
if (cumpleFCR && cumpleTransfer) {
return {
status: 'CUMPLE',
score: 100,
gap: '-',
details,
};
} else if (parcialFCR && parcialTransfer) {
const score = Math.round(
(Math.min(avgFCR, 75) / 75 * 50) +
(Math.max(0, 25 - avgTransfer) / 25 * 50)
);
return {
status: 'PARCIAL',
score,
gap: `FCR ${avgFCR < 75 ? `-${(75 - avgFCR).toFixed(0)}pp` : 'OK'}, Transfer ${avgTransfer > 15 ? `+${(avgTransfer - 15).toFixed(0)}pp` : 'OK'}`,
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`,
details,
};
}
}
function evaluateLaw09Compliance(_data: AnalysisData): ComplianceResult {
// Los datos de idioma no estan disponibles en el modelo actual
return {
status: 'SIN_DATOS',
score: 0,
gap: 'Requiere datos',
details: [
'No se dispone de datos de idioma en las interacciones',
'Para evaluar este requisito se necesita el campo "language" en el CSV',
],
};
}
// ============================================
// COMPONENTES DE SECCION
// ============================================
interface Law10TabProps {
data: AnalysisData;
}
// Status Icon Component
function StatusIcon({ status }: { status: ComplianceStatus }) {
switch (status) {
case 'CUMPLE':
return ;
case 'PARCIAL':
return ;
case 'NO_CUMPLE':
return ;
default:
return ;
}
}
function getStatusBadgeVariant(status: ComplianceStatus): 'success' | 'warning' | 'critical' | 'default' {
switch (status) {
case 'CUMPLE': return 'success';
case 'PARCIAL': return 'warning';
case 'NO_CUMPLE': return 'critical';
default: return 'default';
}
}
function getStatusLabel(status: ComplianceStatus): string {
switch (status) {
case 'CUMPLE': return 'Cumple';
case 'PARCIAL': return 'Parcial';
case 'NO_CUMPLE': return 'No Cumple';
default: return 'Sin Datos';
}
}
// Header con descripcion del analisis
function Law10HeaderCountdown({
complianceResults,
}: {
complianceResults: { law07: ComplianceResult; law01: ComplianceResult; law02: ComplianceResult; law09: ComplianceResult };
}) {
const now = new Date();
const deadline = LAW_10_2025.deadline;
const diffTime = deadline.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Contar requisitos cumplidos
const results = [complianceResults.law07, complianceResults.law01, complianceResults.law02];
const cumplidos = results.filter(r => r.status === 'CUMPLE').length;
const total = results.length;
// Determinar estado general
const getOverallStatus = () => {
if (results.every(r => r.status === 'CUMPLE')) return 'CUMPLE';
if (results.some(r => r.status === 'NO_CUMPLE')) return 'NO_CUMPLE';
return 'PARCIAL';
};
const overallStatus = getOverallStatus();
return (
{/* Header */}
Sobre este Analisis
Ley 10/2025 de Atencion al Cliente
{/* Descripcion */}
Este modulo conecta tus metricas operacionales actuales con los requisitos de la
Ley 10/2025. No mide compliance directamente (requeriria datos adicionales), pero SI
identifica patrones que impactan en tu capacidad de cumplir con la normativa.