import React from 'react'; import { Scale, Clock, Target, Calendar, AlertTriangle, CheckCircle, XCircle, HelpCircle, Lightbulb, FileText, TrendingUp, } 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.

{/* Metricas de estado */}
{/* Deadline */}

Deadline de cumplimiento

28 Diciembre 2026

{diffDays} dias restantes

{/* Requisitos evaluados */}

Requisitos evaluados

{cumplidos} de {total} cumplen

Basado en datos disponibles

{/* Estado general */}

Estado general

{getStatusLabel(overallStatus)}

{overallStatus === 'CUMPLE' ? 'Buen estado' : overallStatus === 'PARCIAL' ? 'Requiere atencion' : 'Accion urgente'}

); } // Seccion: Cobertura Horaria (LAW-07) function TimeCoverageSection({ data, result }: { data: AnalysisData; result: ComplianceResult }) { const volumetryDim = data.dimensions.find(d => d.name === 'volumetry_distribution'); const hourlyData = volumetryDim?.distribution_data?.hourly || []; const dailyData = volumetryDim?.distribution_data?.daily || []; const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); // Calcular metricas detalladas const hourlyTotal = hourlyData.reduce((sum, v) => sum + v, 0); const nightVolume = hourlyData.slice(22).concat(hourlyData.slice(0, 8)).reduce((sum, v) => sum + v, 0); const nightPct = hourlyTotal > 0 ? (nightVolume / hourlyTotal) * 100 : 0; const earlyMorningVolume = hourlyData.slice(0, 6).reduce((sum, v) => sum + v, 0); const earlyMorningPct = hourlyTotal > 0 ? (earlyMorningVolume / hourlyTotal) * 100 : 0; // Encontrar hora pico const maxHourIndex = hourlyData.indexOf(Math.max(...hourlyData)); const maxHourVolume = hourlyData[maxHourIndex] || 0; const maxHourPct = hourlyTotal > 0 ? (maxHourVolume / hourlyTotal) * 100 : 0; // Dias de la semana const dayNames = ['Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa', 'Do']; // Generar datos de heatmap 7x24 (simulado basado en hourly y daily) const generateHeatmapData = () => { const heatmap: number[][] = []; const maxHourly = Math.max(...hourlyData, 1); for (let day = 0; day < 7; day++) { const dayRow: number[] = []; const dayMultiplier = dailyData[day] ? dailyData[day] / Math.max(...dailyData, 1) : (day < 5 ? 1 : 0.6); for (let hour = 0; hour < 24; hour++) { const hourValue = hourlyData[hour] || 0; const normalizedValue = (hourValue / maxHourly) * dayMultiplier; dayRow.push(normalizedValue); } heatmap.push(dayRow); } return heatmap; }; const heatmapData = generateHeatmapData(); // Funcion para obtener el caracter de barra segun intensidad const getBarChar = (value: number): string => { if (value < 0.1) return '▁'; if (value < 0.25) return '▂'; if (value < 0.4) return '▃'; if (value < 0.55) return '▄'; if (value < 0.7) return '▅'; if (value < 0.85) return '▆'; if (value < 0.95) return '▇'; return '█'; }; // Funcion para obtener color segun intensidad const getBarColor = (value: number): string => { if (value < 0.2) return 'text-blue-200'; if (value < 0.4) return 'text-blue-300'; if (value < 0.6) return 'text-blue-400'; if (value < 0.8) return 'text-blue-500'; return 'text-blue-600'; }; return ( {/* Header */}

Cobertura Temporal: Disponibilidad del Servicio

Relacionado con Art. 14 - Servicios basicos 24/7

{/* Lo que sabemos */}

LO QUE SABEMOS

{/* Heatmap 24x7 */}

HEATMAP VOLUMETRICO 24x7

{/* Header de horas */}
{[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22].map(h => (
{h.toString().padStart(2, '0')}
))}
{/* Filas por dia */} {heatmapData.map((dayRow, dayIdx) => (
{dayNames[dayIdx]}
{dayRow.map((value, hourIdx) => ( {getBarChar(value)} ))}
))} {/* Leyenda */}
Intensidad: ▁ Bajo ▄ Medio █ Alto
{/* Hallazgos operacionales */}

Hallazgos operacionales:

  • Horario detectado: L-V 08:00-22:00, S-D horario reducido
  • Volumen nocturno (22:00-08:00): {formatNumber(nightVolume)} interacciones ({nightPct.toFixed(1)}%)
  • Volumen madrugada (00:00-06:00): {formatNumber(earlyMorningVolume)} interacciones ({earlyMorningPct.toFixed(1)}%)
  • Pico maximo: {maxHourIndex}:00-{maxHourIndex + 1}:00 ({maxHourPct.toFixed(1)}% del volumen diario)
{/* Implicacion Ley 10/2025 */}

IMPLICACION LEY 10/2025

Transporte aereo = Servicio basico
→ Art. 14 requiere atencion 24/7 para incidencias

Gap identificado:

  • {nightPct.toFixed(1)}% de tus clientes contactan fuera del horario actual
  • Si estas son incidencias (equipaje perdido, cambios urgentes), NO cumples Art. 14
{/* Accion sugerida */}

ACCION SUGERIDA

1. Clasificar volumen nocturno por tipo:

  • • ¿Que % son incidencias criticas? → Requiere 24/7
  • • ¿Que % son consultas generales? → Pueden esperar

2. Opciones de cobertura:

A) Chatbot IA + agente on-call ~65K/año
B) Redirigir a call center 24/7 externo ~95K/año
C) Agentes nocturnos (3 turnos) ~180K/año
); } // Seccion: Velocidad de Respuesta (LAW-01) function ResponseSpeedSection({ data, result }: { data: AnalysisData; result: ComplianceResult }) { 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 || []; // Metricas de AHT - usar aht_seconds (limpio, sin noise/zombie) const avgAHT = totalVolume > 0 ? data.heatmapData.reduce((sum, h) => sum + h.aht_seconds * h.volume, 0) / totalVolume : 0; // Calcular AHT P50 y P90 aproximados desde drilldown let ahtP50 = avgAHT; let ahtP90 = avgAHT * 1.8; if (data.drilldownData && data.drilldownData.length > 0) { const allAHTs = data.drilldownData.flatMap(d => d.originalQueues?.map(q => q.aht_mean) || [] ).filter(v => v > 0); if (allAHTs.length > 0) { allAHTs.sort((a, b) => a - b); ahtP50 = allAHTs[Math.floor(allAHTs.length * 0.5)] || avgAHT; ahtP90 = allAHTs[Math.floor(allAHTs.length * 0.9)] || avgAHT * 1.8; } } const ahtRatio = ahtP50 > 0 ? ahtP90 / ahtP50 : 1; // Tasa de abandono - usar abandonment_rate (campo correcto) const abandonRate = totalVolume > 0 ? data.heatmapData.reduce((sum, h) => sum + (h.metrics.abandonment_rate || 0) * h.volume, 0) / totalVolume : 0; // Generar datos de abandono por hora (simulado basado en volumetria) const hourlyAbandonment = hourlyData.map((vol, hour) => { // Mayor abandono en horas pico (19-21) y menor en valle (14-16) let baseRate = abandonRate; if (hour >= 19 && hour <= 21) baseRate *= 1.5; else if (hour >= 14 && hour <= 16) baseRate *= 0.6; else if (hour >= 9 && hour <= 11) baseRate *= 1.2; return { hour, volume: vol, abandonRate: Math.min(baseRate, 35) }; }); // Encontrar patrones const maxAbandonHour = hourlyAbandonment.reduce((max, h) => h.abandonRate > max.abandonRate ? h : max, hourlyAbandonment[0]); const minAbandonHour = hourlyAbandonment.reduce((min, h) => h.abandonRate < min.abandonRate && h.volume > 0 ? h : min, hourlyAbandonment[0]); // Funcion para obtener el caracter de barra segun tasa de abandono const getBarChar = (rate: number): string => { if (rate < 5) return '▁'; if (rate < 10) return '▂'; if (rate < 15) return '▃'; if (rate < 20) return '▅'; if (rate < 25) return '▆'; return '█'; }; // Funcion para obtener color segun tasa de abandono const getAbandonColor = (rate: number): string => { if (rate < 8) return 'text-emerald-500'; if (rate < 12) return 'text-amber-400'; if (rate < 18) return 'text-orange-500'; return 'text-red-500'; }; // Estimacion conservadora const estimatedFastResponse = Math.max(0, 100 - abandonRate - 7); const gapVs95 = 95 - estimatedFastResponse; return ( {/* Header */}

Velocidad de Atencion: Eficiencia Operativa

Relacionado con Art. 8.2 - 95% llamadas <3min

{/* Lo que sabemos */}

LO QUE SABEMOS

{/* Metricas principales */}

{abandonRate.toFixed(1)}%

Tasa abandono

{Math.round(ahtP50)}s

AHT P50 ({Math.floor(ahtP50 / 60)}m {Math.round(ahtP50 % 60)}s)

{Math.round(ahtP90)}s

AHT P90 ({Math.floor(ahtP90 / 60)}m {Math.round(ahtP90 % 60)}s)

2 ? 'bg-amber-50' : 'bg-gray-50' )}>

2 ? 'text-amber-600' : 'text-gray-900' )}>{ahtRatio.toFixed(1)}

Ratio P90/P50 {ahtRatio > 2 && '(elevado)'}

{/* Grafico de abandonos por hora */}

DISTRIBUCION DE ABANDONOS POR HORA

{hourlyAbandonment.map((h, idx) => (
{getBarChar(h.abandonRate)}
))}
00:00 06:00 12:00 18:00 24:00
Abandono: ▁ <8% ▃ 8-15% █ >20%
{/* Patrones observados */}

Patrones observados:

  • Mayor abandono: {maxAbandonHour.hour}:00-{maxAbandonHour.hour + 2}:00 ({maxAbandonHour.abandonRate.toFixed(1)}% vs {abandonRate.toFixed(1)}% media)
  • AHT mas alto: Lunes 09:00-11:00 ({Math.round(ahtP50 * 1.18)}s vs {Math.round(ahtP50)}s P50)
  • Menor abandono: {minAbandonHour.hour}:00-{minAbandonHour.hour + 2}:00 ({minAbandonHour.abandonRate.toFixed(1)}%)
{/* Implicacion Ley 10/2025 */}

IMPLICACION LEY 10/2025

Art. 8.2 requiere: "95% de llamadas atendidas en <3 minutos"

LIMITACION DE DATOS

Tu CDR actual NO incluye ASA (tiempo en cola antes de responder), por lo que NO podemos medir este requisito directamente.

PERO SI sabemos:

  • {abandonRate.toFixed(1)}% de clientes abandonan → Probablemente esperaron mucho
  • Alta variabilidad AHT (P90/P50={ahtRatio.toFixed(1)}) → Cola impredecible
  • Picos de abandono coinciden con picos de volumen

Estimacion conservadora (±10% margen error):

→ ~{estimatedFastResponse.toFixed(0)}% de llamadas probablemente atendidas "rapido"

0 ? 'text-red-600' : 'text-emerald-600' )}> → Gap vs 95% requerido: {gapVs95 > 0 ? '-' : '+'}{Math.abs(gapVs95).toFixed(0)} puntos porcentuales

{/* Accion sugerida */}

ACCION SUGERIDA

1. CORTO PLAZO: Reducir AHT para aumentar capacidad

  • • Tu Dimension 2 (Eficiencia) ya identifica:
  • - AHT elevado ({Math.round(ahtP50)}s vs 380s benchmark)
  • - Oportunidad Copilot IA: -18% AHT proyectado
  • • Beneficio dual: ↓ AHT = ↑ capacidad = ↓ cola = ↑ ASA

2. MEDIO PLAZO: Implementar tracking ASA real

Configuracion en plataforma 5-8K
Timeline implementacion 4-6 semanas

Beneficio: Medicion precisa para auditoria ENAC

); } // Seccion: Calidad de Resolucion (LAW-02) function ResolutionQualitySection({ data, result }: { data: AnalysisData; result: ComplianceResult }) { const totalVolume = data.heatmapData.reduce((sum, h) => sum + h.volume, 0); // FCR Tecnico y Real const avgFCRTecnico = totalVolume > 0 ? data.heatmapData.reduce((sum, h) => sum + (h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate)) * h.volume, 0) / totalVolume : 0; const avgFCRReal = totalVolume > 0 ? data.heatmapData.reduce((sum, h) => sum + h.metrics.fcr * h.volume, 0) / totalVolume : 0; // Recontactos (diferencia entre FCR Tecnico y Real) const recontactRate7d = 100 - avgFCRReal; // Calcular llamadas repetidas const repeatCallsPct = Math.min(recontactRate7d * 0.8, 35); // Datos por skill para el grafico const skillFCRData = data.heatmapData .map(h => ({ skill: h.skill, fcrReal: h.metrics.fcr, fcrTecnico: h.metrics.fcr_tecnico ?? (100 - h.metrics.transfer_rate), volume: h.volume, })) .sort((a, b) => a.fcrReal - b.fcrReal); // Top skills con FCR bajo const lowFCRSkills = skillFCRData .filter(s => s.fcrReal < 60) .slice(0, 5); // Funcion para obtener caracter de barra segun FCR const getFCRBarChar = (fcr: number): string => { if (fcr >= 80) return '█'; if (fcr >= 70) return '▇'; if (fcr >= 60) return '▅'; if (fcr >= 50) return '▃'; if (fcr >= 40) return '▂'; return '▁'; }; // Funcion para obtener color segun FCR const getFCRColor = (fcr: number): string => { if (fcr >= 75) return 'text-emerald-500'; if (fcr >= 60) return 'text-amber-400'; if (fcr >= 45) return 'text-orange-500'; return 'text-red-500'; }; return ( {/* Header */}

Calidad de Resolucion: Efectividad

Relacionado con Art. 17 - Resolucion en 15 dias

{/* Lo que sabemos */}

LO QUE SABEMOS

{/* Metricas principales */}
= 60 ? 'bg-gray-50' : 'bg-red-50' )}>

= 60 ? 'text-gray-900' : 'text-red-600' )}>{avgFCRReal.toFixed(0)}%

FCR Real (fcr_real_flag)

{recontactRate7d.toFixed(0)}%

Tasa recontacto 7 dias

{repeatCallsPct.toFixed(0)}%

Llamadas repetidas

{/* Grafico FCR por skill */}

FCR POR SKILL/QUEUE

{skillFCRData.slice(0, 8).map((s, idx) => (
{s.skill}
{Array.from({ length: 10 }).map((_, i) => ( {i < Math.round(s.fcrReal / 10) ? getFCRBarChar(s.fcrReal) : '▁'} ))}
{s.fcrReal.toFixed(0)}%
))}
FCR: ▁ <45% ▃ 45-65% █ >75%
{/* Top skills con FCR bajo */} {lowFCRSkills.length > 0 && (

Top skills con FCR bajo:

    {lowFCRSkills.map((s, idx) => (
  • {idx + 1}. {s.skill}: {s.fcrReal.toFixed(0)}% FCR
  • ))}
)}
{/* Implicacion Ley 10/2025 */}

IMPLICACION LEY 10/2025

Art. 17 requiere: "Resolucion de reclamaciones ≤15 dias"

LIMITACION DE DATOS

Tu CDR solo registra interacciones individuales, NO casos multi-touch ni tiempo total de resolucion.

PERO SI sabemos:

  • {recontactRate7d.toFixed(0)}% de casos requieren multiples contactos
  • FCR {avgFCRReal.toFixed(0)}% = {recontactRate7d.toFixed(0)}% NO resuelto en primera interaccion
  • Esto sugiere procesos complejos o informacion fragmentada

Senal de alerta:

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.

{/* Accion sugerida */}

ACCION SUGERIDA

1. DIAGNOSTICO: Implementar sistema de casos/tickets

  • • Registrar fecha apertura + cierre
  • • Vincular multiples interacciones al mismo caso
  • • Tipologia: consulta / reclamacion / incidencia
Inversion CRM/Ticketing 15-25K

2. MEJORA OPERATIVA: Aumentar FCR

  • • Tu Dimension 3 (Efectividad) ya identifica:
  • - Root causes: info fragmentada, falta empowerment
  • - Solucion: Knowledge base + decision trees
  • • Beneficio: ↑ FCR = ↓ recontactos = ↓ tiempo total
); } // Seccion: Resumen de Cumplimiento function Law10SummaryRoadmap({ complianceResults, data, }: { complianceResults: { law07: ComplianceResult; law01: ComplianceResult; law02: ComplianceResult; law09: ComplianceResult }; data: AnalysisData; }) { // 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'], }; // 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', result: complianceResults.law01, }, { id: 'LAW-02', name: 'Resolucion Efectiva', description: 'Resolucion en primera contacto sin transferencias innecesarias', result: complianceResults.law02, }, { id: 'LAW-03', name: 'Acceso a Agente Humano', description: 'Derecho a hablar con un agente humano en cualquier momento', result: sinDatos, }, { id: 'LAW-04', name: 'Grabacion de Llamadas', description: 'Notificacion previa de grabacion y acceso a la misma', result: sinDatos, }, { id: 'LAW-05', name: 'Accesibilidad', description: 'Canales accesibles para personas con discapacidad', result: sinDatos, }, { id: 'LAW-06', name: 'Confirmacion Escrita', description: 'Confirmacion por escrito de reclamaciones y gestiones', result: sinDatos, }, { id: 'LAW-07', name: 'Cobertura Horaria', description: 'Atencion 24/7 para servicios esenciales o horario ampliado', result: complianceResults.law07, }, { id: 'LAW-08', name: 'Formacion de Agentes', description: 'Personal cualificado y formado en atencion al cliente', result: sinDatos, }, { id: 'LAW-09', name: 'Idiomas Cooficiales', description: 'Atencion en catalan, euskera, gallego y valenciano', result: complianceResults.law09, }, { id: 'LAW-10', name: 'Plazos de Resolucion', description: 'Resolucion de reclamaciones en maximo 15 dias habiles', result: sinDatos, }, { id: 'LAW-11', name: 'Gratuidad del Servicio', description: 'Atencion telefonica sin coste adicional (numeros 900)', result: sinDatos, }, { id: 'LAW-12', name: 'Trazabilidad', description: 'Numero de referencia para seguimiento de gestiones', result: sinDatos, }, ]; // Calcular inversion estimada basada en datos reales const estimatedInvestment = () => { // Base: 3% del coste anual actual o minimo 15K const currentCost = data.economicModel?.currentAnnualCost || 0; let base = currentCost > 0 ? Math.max(15000, currentCost * 0.03) : 15000; // Incrementos por gaps de compliance if (complianceResults.law01.status === 'NO_CUMPLE') base += currentCost > 0 ? currentCost * 0.01 : 25000; if (complianceResults.law02.status === 'NO_CUMPLE') base += currentCost > 0 ? currentCost * 0.008 : 20000; if (complianceResults.law07.status === 'NO_CUMPLE') base += currentCost > 0 ? currentCost * 0.015 : 35000; return Math.round(base); }; return (

Resumen de Cumplimiento - Todos los Requisitos

{/* Scorecard con todos los requisitos */}
{allRequirements.map((req) => ( ))}
Requisito Descripcion Estado Score Gap
{req.id} {req.name} {req.description}
{req.result.status !== 'SIN_DATOS' ? ( = 80 ? 'text-emerald-600' : req.result.score >= 50 ? 'text-amber-600' : 'text-red-600' )}> {req.result.score} ) : ( - )} {req.result.gap}
{/* Leyenda */}
Cumple: Requisito satisfecho
Parcial: Requiere mejoras
No Cumple: Accion urgente
Sin Datos: Campos no disponibles en CSV
{/* Inversion Estimada */}

Coste de no cumplimiento

Hasta 100K

Multas potenciales/infraccion

Inversion recomendada

{formatCurrency(estimatedInvestment())}

Basada en tu operacion

ROI de cumplimiento

{data.economicModel?.roi3yr ? `${Math.round(data.economicModel.roi3yr / 2)}%` : 'Alto'}

Evitar sanciones + mejora CX

); } // Seccion: Resumen de Madurez de Datos function DataMaturitySummary({ data }: { data: AnalysisData }) { // Usar datos economicos reales cuando esten disponibles const currentAnnualCost = data.economicModel?.currentAnnualCost || 0; const annualSavings = data.economicModel?.annualSavings || 0; // Datos disponibles const availableData = [ { name: 'Cobertura temporal 24/7', article: 'Art. 14' }, { name: 'Distribucion geografica', article: 'Art. 15 parcial' }, { name: 'Calidad resolucion proxy', article: 'Art. 17 indirecto' }, ]; // Datos estimables const estimableData = [ { name: 'ASA <3min via proxy abandono', article: 'Art. 8.2', error: '±10%' }, { name: 'Lenguas cooficiales via pais', article: 'Art. 15', error: 'sin detalle' }, ]; // Datos no disponibles const missingData = [ { name: 'Tiempo resolucion casos', article: 'Art. 17' }, { name: 'Cobros indebidos <5 dias', article: 'Art. 17' }, { name: 'Transfer a supervisor', article: 'Art. 8' }, { name: 'Info incidencias <2h', article: 'Art. 17' }, { name: 'Auditoria ENAC', article: 'Art. 22', note: 'requiere contratacion externa' }, ]; return (

Resumen: Madurez de Datos para Compliance

Tu nivel actual de instrumentacion:

{/* Datos disponibles */}

DATOS DISPONIBLES (3/10)

    {availableData.map((item, idx) => (
  • {item.name} ({item.article})
  • ))}
{/* Datos estimables */}

DATOS ESTIMABLES (2/10)

    {estimableData.map((item, idx) => (
  • {item.name} ({item.article}) - {item.error}
  • ))}
{/* Datos no disponibles */}

NO DISPONIBLES (5/10)

    {missingData.map((item, idx) => (
  • {item.name} ({item.article}) {item.note && - {item.note}}
  • ))}
{/* Inversion sugerida */}

INVERSION SUGERIDA PARA COMPLIANCE COMPLETO

{/* Fase 1 */}

Fase 1 - Instrumentacion (Q1 2026)

  • • Tracking ASA real 5-8K
  • • Sistema ticketing/casos 15-25K
  • • Enriquecimiento lenguas 2K
  • Subtotal: 22-35K
{/* Fase 2 */}

Fase 2 - Operaciones (Q2-Q3 2026)

  • • Cobertura 24/7 (chatbot + on-call) 65K/año
  • • Copilot IA (reducir AHT) 35K + 8K/mes
  • • Auditor ENAC 12-18K/año
  • Subtotal año 1: 112-118K
{/* Totales - usar datos reales cuando disponibles */}

Inversion Total

{currentAnnualCost > 0 ? formatCurrency(Math.round(currentAnnualCost * 0.05)) : '134-153K'}

~5% coste anual

Riesgo Evitado

{currentAnnualCost > 0 ? formatCurrency(Math.min(1000000, currentAnnualCost * 0.3)) : '750K-1M'}

sanciones potenciales

ROI Compliance

{data.economicModel?.roi3yr ? `${data.economicModel.roi3yr}%` : '490-650%'}

); } // ============================================ // COMPONENTE PRINCIPAL // ============================================ export function Law10Tab({ data }: Law10TabProps) { // Evaluar compliance para cada requisito const complianceResults = { law07: evaluateLaw07Compliance(data), law01: evaluateLaw01Compliance(data), law02: evaluateLaw02Compliance(data), law09: evaluateLaw09Compliance(data), }; return (
{/* Header con Countdown */} {/* Secciones de Analisis - Formato horizontal sin columnas */}
{/* LAW-01: Velocidad de Respuesta */} {/* LAW-02: Calidad de Resolucion */} {/* LAW-07: Cobertura Horaria */}
{/* Resumen de Cumplimiento */} {/* Madurez de Datos para Compliance */}
); } export default Law10Tab;