Initial commit: frontend + backend integration

This commit is contained in:
Ignacio
2025-12-29 18:12:32 +01:00
commit 2cd6d6b95c
146 changed files with 31503 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
import React from 'react';
import { motion } from 'framer-motion';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import { Clock, AlertCircle, TrendingUp } from 'lucide-react';
interface HourlyDistributionChartProps {
hourly: number[];
off_hours_pct: number;
peak_hours: number[];
}
export function HourlyDistributionChart({ hourly, off_hours_pct, peak_hours }: HourlyDistributionChartProps) {
// Preparar datos para el gráfico
const chartData = hourly.map((value, hour) => ({
hour: `${hour}:00`,
hourNum: hour,
volume: value,
isPeak: peak_hours.includes(hour),
isOffHours: hour < 8 || hour >= 19
}));
const totalVolume = hourly.reduce((a, b) => a + b, 0);
const peakVolume = Math.max(...hourly);
const avgVolume = totalVolume / 24;
// Custom tooltip
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-white p-3 rounded-lg shadow-lg border border-slate-200">
<p className="font-semibold text-slate-900 mb-1">{data.hour}</p>
<p className="text-sm text-slate-600">
Volumen: <span className="font-medium text-slate-900">{data.volume.toLocaleString('es-ES')}</span>
</p>
<p className="text-sm text-slate-600">
% del total: <span className="font-medium text-slate-900">
{((data.volume / totalVolume) * 100).toFixed(1)}%
</span>
</p>
{data.isPeak && (
<p className="text-xs text-amber-600 mt-1"> Hora pico</p>
)}
{data.isOffHours && (
<p className="text-xs text-red-600 mt-1">🌙 Fuera de horario</p>
)}
</div>
);
}
return null;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="bg-white rounded-xl p-6 shadow-sm border border-slate-200"
>
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<Clock className="w-5 h-5 text-slate-600" />
<h3 className="text-lg font-semibold text-slate-900">
Distribución Horaria de Interacciones
</h3>
</div>
<p className="text-sm text-slate-600">
Análisis del volumen de interacciones por hora del día
</p>
</div>
{/* KPIs */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-slate-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-green-600" />
<span className="text-xs text-slate-600">Volumen Pico</span>
</div>
<div className="text-2xl font-bold text-slate-900">
{peakVolume.toLocaleString('es-ES')}
</div>
<div className="text-xs text-slate-500 mt-1">
{peak_hours.map(h => `${h}:00`).join(', ')}
</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-4 h-4 text-blue-600" />
<span className="text-xs text-slate-600">Promedio/Hora</span>
</div>
<div className="text-2xl font-bold text-slate-900">
{Math.round(avgVolume).toLocaleString('es-ES')}
</div>
<div className="text-xs text-slate-500 mt-1">
24 horas
</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-4 h-4 text-red-600" />
<span className="text-xs text-slate-600">Fuera de Horario</span>
</div>
<div className="text-2xl font-bold text-slate-900">
{(off_hours_pct * 100).toFixed(1)}%
</div>
<div className="text-xs text-slate-500 mt-1">
19:00 - 08:00
</div>
</div>
</div>
{/* Chart */}
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
<XAxis
dataKey="hour"
tick={{ fontSize: 11, fill: '#64748B' }}
interval={1}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
tick={{ fontSize: 12, fill: '#64748B' }}
tickFormatter={(value) => value.toLocaleString('es-ES')}
/>
<Tooltip content={<CustomTooltip />} />
<ReferenceLine
y={avgVolume}
stroke="#6D84E3"
strokeDasharray="5 5"
label={{ value: 'Promedio', position: 'right', fill: '#6D84E3', fontSize: 12 }}
/>
<Bar
dataKey="volume"
fill="#6D84E3"
radius={[4, 4, 0, 0]}
animationDuration={1000}
>
{chartData.map((entry, index) => (
<motion.rect
key={`bar-${index}`}
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ duration: 0.5, delay: index * 0.02 }}
fill={
entry.isPeak ? '#F59E0B' : // Amber for peaks
entry.isOffHours ? '#EF4444' : // Red for off-hours
'#6D84E3' // Corporate blue for normal
}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="flex items-center justify-center gap-6 mt-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-[#6D84E3]"></div>
<span className="text-slate-600">Horario laboral (8-19h)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-[#F59E0B]"></div>
<span className="text-slate-600">Horas pico</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-[#EF4444]"></div>
<span className="text-slate-600">Fuera de horario</span>
</div>
</div>
{/* Insight */}
{off_hours_pct > 0.25 && (
<div className="mt-6 p-4 bg-amber-50 rounded-lg border border-amber-200">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-900 mb-1">
Alto volumen fuera de horario laboral
</p>
<p className="text-sm text-amber-800">
El {(off_hours_pct * 100).toFixed(0)}% de las interacciones ocurren fuera del horario
laboral estándar (19:00-08:00). Considera implementar cobertura 24/7 con agentes virtuales
para mejorar la experiencia del cliente y reducir costes.
</p>
</div>
</div>
</div>
)}
</motion.div>
);
}