587 lines
34 KiB
React
587 lines
34 KiB
React
import React, { useState, useEffect, useMemo } from 'react';
|
||
import { supabaseData } from '../services/supabaseClient';
|
||
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts';
|
||
import NavBar from '../components/NavBar';
|
||
|
||
const GLM_COLORS = { positivo: '#6CC24A', neutro: '#F0C75E', negativo: '#FF6A13' };
|
||
const CHART_PALETTE = ['#6CC24A', '#4F758B', '#A4D65E', '#5B7F95', '#C4D600', '#6B8FA3'];
|
||
|
||
export default function Dashboard() {
|
||
const [rawData, setRawData] = useState([]);
|
||
const [filteredData, setFilteredData] = useState([]);
|
||
const [encuestasEnviadas, setEncuestasEnviadas] = useState(0);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState(null);
|
||
|
||
const opcionesUnicas = (campo) => [
|
||
...new Set(
|
||
rawData
|
||
.map(item => (item?.[campo] ?? '').toString().trim())
|
||
.filter(v => v !== '')
|
||
),
|
||
].sort((a, b) => a.localeCompare(b, 'es'));
|
||
|
||
const posiciones = useMemo(() => opcionesUnicas('posicion'), [rawData]);
|
||
const homologaciones = useMemo(() => opcionesUnicas('homologacion_ia'), [rawData]);
|
||
const paises = useMemo(() => opcionesUnicas('pais'), [rawData]);
|
||
const cuentas = useMemo(() => opcionesUnicas('cuenta_proyecto'), [rawData]);
|
||
const jefes = useMemo(() => opcionesUnicas('jefe_directo'), [rawData]);
|
||
|
||
const [filtroPosicion, setFiltroPosicion] = useState('');
|
||
const [filtroHomologacion, setFiltroHomologacion] = useState('');
|
||
const [filtroPais, setFiltroPais] = useState('');
|
||
const [filtroCuenta, setFiltroCuenta] = useState('');
|
||
const [filtroJefe, setFiltroJefe] = useState('');
|
||
const [busquedaNombre, setBusquedaNombre] = useState('');
|
||
|
||
const [paginaActual, setPaginaActual] = useState(1);
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
aplicarFiltros();
|
||
setPaginaActual(1);
|
||
}, [filtroPosicion, filtroHomologacion, filtroPais, filtroCuenta, filtroJefe, rawData]);
|
||
|
||
const fetchData = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
console.log('[DashboardTerminacion] Iniciando fetch...');
|
||
|
||
const { data, error: fetchError } = await supabaseData
|
||
.from('encuesta_terminacion_glm')
|
||
.select('*')
|
||
.order('timestamp', { ascending: false });
|
||
|
||
if (fetchError) {
|
||
console.error('[DashboardTerminacion] Error de Supabase:', {
|
||
message: fetchError.message,
|
||
details: fetchError.details,
|
||
hint: fetchError.hint,
|
||
code: fetchError.code,
|
||
});
|
||
setError(fetchError.message || 'Error al cargar los datos de encuestas.');
|
||
} else {
|
||
console.log('[DashboardTerminacion] Datos recibidos:', data?.length ?? 0, 'registros');
|
||
setRawData(data ?? []);
|
||
}
|
||
|
||
const { data: kpiData, error: kpiError } = await supabaseData
|
||
.from('encuesta_terminacion_kpi_glm')
|
||
.select('encuestas_enviadas');
|
||
|
||
if (kpiError) {
|
||
console.error('[DashboardTerminacion] Error al cargar KPI:', kpiError.message);
|
||
} else {
|
||
const totalEnviadas = (kpiData ?? []).reduce(
|
||
(acc, curr) => acc + (Number(curr.encuestas_enviadas) || 0),
|
||
0
|
||
);
|
||
setEncuestasEnviadas(totalEnviadas);
|
||
}
|
||
|
||
setLoading(false);
|
||
};
|
||
|
||
const aplicarFiltros = () => {
|
||
let result = rawData;
|
||
if (filtroPosicion) result = result.filter(item => item.posicion === filtroPosicion);
|
||
if (filtroHomologacion) result = result.filter(item => item.homologacion_ia === filtroHomologacion);
|
||
if (filtroPais) result = result.filter(item => item.pais === filtroPais);
|
||
if (filtroCuenta) result = result.filter(item => item.cuenta_proyecto === filtroCuenta);
|
||
if (filtroJefe) result = result.filter(item => item.jefe_directo === filtroJefe);
|
||
|
||
// Ordenar por fecha de más reciente a más vieja
|
||
result = [...result].sort((a, b) => {
|
||
const dateA = a.timestamp ? new Date(a.timestamp) : 0;
|
||
const dateB = b.timestamp ? new Date(b.timestamp) : 0;
|
||
return dateB - dateA;
|
||
});
|
||
|
||
setFilteredData(result);
|
||
};
|
||
|
||
const filasPorPagina = 10;
|
||
const totalPaginas = Math.ceil(filteredData.length / filasPorPagina) || 1;
|
||
const indiceUltimoItem = paginaActual * filasPorPagina;
|
||
const indicePrimerItem = indiceUltimoItem - filasPorPagina;
|
||
const datosFiltradosTabla = busquedaNombre.trim()
|
||
? filteredData.filter(item =>
|
||
(item?.nombre ?? '').toString().toLowerCase().includes(busquedaNombre.trim().toLowerCase())
|
||
)
|
||
: filteredData;
|
||
|
||
const totalRegistrosTabla = datosFiltradosTabla.length;
|
||
const datosPaginados = datosFiltradosTabla.slice(indicePrimerItem, indiceUltimoItem);
|
||
|
||
const totalRegistros = filteredData.length;
|
||
const porcentajeAprobacion = encuestasEnviadas > 0
|
||
? Math.round((totalRegistros / encuestasEnviadas) * 100)
|
||
: 0;
|
||
const esVerdadero = (v) => v === true || String(v).toLowerCase().trim() === 'true';
|
||
const regresarian = filteredData.filter(d => esVerdadero(d.considerar_regresar)).length;
|
||
const recomendarian = filteredData.filter(d => esVerdadero(d.siento_comodo_recomendando)).length;
|
||
|
||
const sentimientoCount = filteredData.reduce((acc, curr) => {
|
||
const sent = curr.sentimiento_ia || 'neutro';
|
||
acc[sent] = (acc[sent] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
|
||
const chartDataSentimiento = Object.keys(sentimientoCount).map(key => ({
|
||
name: key,
|
||
value: sentimientoCount[key]
|
||
}));
|
||
|
||
const pendientes = Math.max(encuestasEnviadas - totalRegistros, 0);
|
||
const chartDataAprobacion = [
|
||
{ name: 'Completadas', value: totalRegistros },
|
||
{ name: 'Pendientes', value: pendientes },
|
||
];
|
||
|
||
const esFalso = (v) => v === false || String(v).toLowerCase().trim() === 'false';
|
||
|
||
const chartDataRegresar = [
|
||
{ name: 'Sí', value: filteredData.filter(d => esVerdadero(d.considerar_regresar)).length },
|
||
{ name: 'No', value: filteredData.filter(d => esFalso(d.considerar_regresar)).length },
|
||
];
|
||
|
||
const chartDataRecomendar = [
|
||
{ name: 'Sí', value: filteredData.filter(d => esVerdadero(d.siento_comodo_recomendando)).length },
|
||
{ name: 'No', value: filteredData.filter(d => esFalso(d.siento_comodo_recomendando)).length },
|
||
];
|
||
|
||
const contarPorCampo = (campo) => {
|
||
const conteo = filteredData.reduce((acc, curr) => {
|
||
const valor = (curr?.[campo] ?? '').toString().trim() || 'Sin especificar';
|
||
acc[valor] = (acc[valor] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
return Object.keys(conteo)
|
||
.sort((a, b) => conteo[b] - conteo[a])
|
||
.map((key, index) => ({
|
||
name: key,
|
||
value: conteo[key],
|
||
fill: CHART_PALETTE[index % CHART_PALETTE.length],
|
||
}));
|
||
};
|
||
|
||
const chartDataMotivoSalida = contarPorCampo('homologacion_ia');
|
||
const chartDataPorPais = contarPorCampo('pais');
|
||
const chartDataPorProyecto = contarPorCampo('cuenta_proyecto');
|
||
|
||
if (loading) return (
|
||
<div className="p-10 text-center">
|
||
<div className="inline-block w-10 h-10 border-4 border-verde-glm border-t-transparent rounded-full animate-spin mb-4" />
|
||
<p className="text-xl font-semibold text-azul-glm">Cargando datos...</p>
|
||
</div>
|
||
);
|
||
|
||
if (error) return (
|
||
<div className="p-10 max-w-xl mx-auto min-h-screen flex items-center justify-center">
|
||
<div className="bg-white p-8 rounded-2xl border border-red-200 shadow-sm text-center w-full">
|
||
<div className="text-red-500 text-4xl mb-4">⚠️</div>
|
||
<h2 className="text-xl font-bold text-red-700 mb-2">Error al cargar datos</h2>
|
||
<p className="text-sm text-gray-600 mb-1">{error}</p>
|
||
<p className="text-xs text-gray-400 mb-6">Revisa la consola del navegador (F12) para más detalles.</p>
|
||
<button
|
||
onClick={fetchData}
|
||
className="bg-azul-glm hover:bg-azul-medio text-white font-semibold py-2.5 px-6 rounded-lg transition-colors"
|
||
>
|
||
Reintentar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div className="p-6 max-w-7xl mx-auto min-h-screen">
|
||
|
||
{/* HEADER GLM */}
|
||
<header className="flex flex-col sm:flex-row items-center gap-4 mb-8 bg-white p-4 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<div className="flex-shrink-0 bg-verde-bg p-2 rounded-lg">
|
||
<img
|
||
src="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png"
|
||
alt="Logo GLM"
|
||
className="h-12 w-auto object-contain max-w-[180px]"
|
||
/>
|
||
</div>
|
||
<div className="text-center sm:text-left flex-1">
|
||
<h1 className="text-2xl font-bold text-azul-glm tracking-tight">Dashboard de Encuestas de Terminación</h1>
|
||
<p className="text-xs text-acero italic">Métricas analíticas de entrevistas de salida</p>
|
||
</div>
|
||
</header>
|
||
|
||
{/* NAVEGACIÓN GLM */}
|
||
<NavBar />
|
||
|
||
{/* FILTROS GLM */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8 bg-white p-5 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<div className="flex-1">
|
||
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por Posición</label>
|
||
<select
|
||
className="w-full rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none"
|
||
value={filtroPosicion}
|
||
onChange={(e) => setFiltroPosicion(e.target.value)}
|
||
>
|
||
<option value="">Todas las posiciones</option>
|
||
{posiciones.map((pos, idx) => (
|
||
<option key={idx} value={pos}>{pos}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por Motivo</label>
|
||
<select
|
||
className="w-full rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none"
|
||
value={filtroHomologacion}
|
||
onChange={(e) => setFiltroHomologacion(e.target.value)}
|
||
>
|
||
<option value="">Todos los motivos</option>
|
||
{homologaciones.map((hom, idx) => (
|
||
<option key={idx} value={hom}>{hom}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por País</label>
|
||
<select
|
||
className="w-full rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none"
|
||
value={filtroPais}
|
||
onChange={(e) => setFiltroPais(e.target.value)}
|
||
>
|
||
<option value="">Todos los países</option>
|
||
{paises.map((pais, idx) => (
|
||
<option key={idx} value={pais}>{pais}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por Cuenta/Proyecto</label>
|
||
<select
|
||
className="w-full rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none"
|
||
value={filtroCuenta}
|
||
onChange={(e) => setFiltroCuenta(e.target.value)}
|
||
>
|
||
<option value="">Todas las cuentas/proyectos</option>
|
||
{cuentas.map((cuenta, idx) => (
|
||
<option key={idx} value={cuenta}>{cuenta}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por Jefe Directo</label>
|
||
<select
|
||
className="w-full rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none"
|
||
value={filtroJefe}
|
||
onChange={(e) => setFiltroJefe(e.target.value)}
|
||
>
|
||
<option value="">Todos los jefes directos</option>
|
||
{jefes.map((jefe, idx) => (
|
||
<option key={idx} value={jefe}>{jefe}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex items-end">
|
||
<button
|
||
onClick={() => { setFiltroPosicion(''); setFiltroHomologacion(''); setFiltroPais(''); setFiltroCuenta(''); setFiltroJefe(''); }}
|
||
className="w-full bg-verde-bg hover:bg-verde-glm hover:text-white text-azul-glm font-semibold text-sm py-2.5 px-5 rounded-lg border border-verde-glm/40 hover:border-verde-glm transition-colors"
|
||
>
|
||
Limpiar Filtros
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* KPIs GLM */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm border-l-[6px] border-naranja">
|
||
<h2 className="text-xs text-acero uppercase font-bold tracking-wider">Total Encuestas Enviadas</h2>
|
||
<p className="text-3xl font-extrabold text-naranja mt-1">{encuestasEnviadas}</p>
|
||
</div>
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm border-l-[6px] border-azul-glm">
|
||
<h2 className="text-xs text-acero uppercase font-bold tracking-wider">Total Encuestas Completadas</h2>
|
||
<p className="text-3xl font-extrabold text-azul-glm mt-1">{totalRegistros}</p>
|
||
</div>
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm border-l-[6px] border-verde-glm">
|
||
<h2 className="text-xs text-acero uppercase font-bold tracking-wider">Consideraría Regresar</h2>
|
||
<p className="text-3xl font-extrabold text-verde-glm mt-1">
|
||
{totalRegistros > 0 ? Math.round((regresarian / totalRegistros) * 100) : 0}%
|
||
</p>
|
||
<p className="text-xs text-acero mt-1">{regresarian} empleados interesados</p>
|
||
</div>
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm border-l-[6px] border-azul-medio">
|
||
<h2 className="text-xs text-acero uppercase font-bold tracking-wider">Recomendaría la Empresa</h2>
|
||
<p className="text-3xl font-extrabold text-azul-medio mt-1">
|
||
{totalRegistros > 0 ? Math.round((recomendarian / totalRegistros) * 100) : 0}%
|
||
</p>
|
||
<p className="text-xs text-acero mt-1">{recomendarian} empleados satisfechos</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* GRÁFICA GLM */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">Sentimiento del Motivo</h3>
|
||
<div className="h-64">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<PieChart>
|
||
<Pie
|
||
data={chartDataSentimiento}
|
||
cx="50%"
|
||
cy="50%"
|
||
outerRadius={80}
|
||
fill="#8884d8"
|
||
dataKey="value"
|
||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||
>
|
||
{chartDataSentimiento.map((entry, index) => (
|
||
<Cell key={`cell-${index}`} fill={GLM_COLORS[entry.name.toLowerCase()] || CHART_PALETTE[index % CHART_PALETTE.length]} />
|
||
))}
|
||
</Pie>
|
||
<Tooltip />
|
||
<Legend />
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">% de Aprobación</h3>
|
||
<div className="h-64 relative">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<PieChart>
|
||
<Pie
|
||
data={chartDataAprobacion}
|
||
cx="50%"
|
||
cy="50%"
|
||
innerRadius={60}
|
||
outerRadius={80}
|
||
dataKey="value"
|
||
startAngle={90}
|
||
endAngle={-270}
|
||
>
|
||
<Cell fill="#6CC24A" />
|
||
<Cell fill="#E2E8F0" />
|
||
</Pie>
|
||
<Tooltip />
|
||
<Legend />
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none" style={{ top: '-2rem' }}>
|
||
<span className="text-4xl font-extrabold text-verde-glm">{porcentajeAprobacion}%</span>
|
||
<span className="text-xs text-acero">{totalRegistros} de {encuestasEnviadas}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* GRÁFICAS SÍ / NO */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">¿Regresaría a la organización en el futuro?</h3>
|
||
<div className="h-64">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={chartDataRegresar} layout="vertical" margin={{ top: 10, right: 30, left: 10, bottom: 10 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
|
||
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
|
||
<YAxis type="category" dataKey="name" tick={{ fontSize: 13, fill: '#4F758B' }} width={40} />
|
||
<Tooltip />
|
||
<Bar dataKey="value" name="Respuestas" radius={[0, 4, 4, 0]}>
|
||
{chartDataRegresar.map((entry) => (
|
||
<Cell key={entry.name} fill={entry.name === 'Sí' ? '#6CC24A' : '#FF6A13'} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">Recomendaría a otra persona para trabajar en la organización</h3>
|
||
<div className="h-64">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={chartDataRecomendar} layout="vertical" margin={{ top: 10, right: 30, left: 10, bottom: 10 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
|
||
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
|
||
<YAxis type="category" dataKey="name" tick={{ fontSize: 13, fill: '#4F758B' }} width={40} />
|
||
<Tooltip />
|
||
<Bar dataKey="value" name="Respuestas" radius={[0, 4, 4, 0]}>
|
||
{chartDataRecomendar.map((entry) => (
|
||
<Cell key={entry.name} fill={entry.name === 'Sí' ? '#6CC24A' : '#FF6A13'} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* GRÁFICAS DE SALIDA LABORAL */}
|
||
<div className="grid grid-cols-1 gap-6 mb-8">
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">Motivo de Salida Laboral</h3>
|
||
<div className="h-72">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={chartDataMotivoSalida} margin={{ top: 10, right: 20, left: 10, bottom: 22 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
|
||
<XAxis dataKey="name" tick={{ fontSize: 11, fill: '#4F758B' }} interval={0} angle={-20} textAnchor="end" height={60} />
|
||
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
|
||
<Tooltip />
|
||
<Bar dataKey="value" name="Salidas" radius={[4, 4, 0, 0]}>
|
||
{chartDataMotivoSalida.map((entry, index) => (
|
||
<Cell key={`motivo-${index}`} fill={entry.fill} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">Salida Laboral Según País</h3>
|
||
<div className="h-72">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={chartDataPorPais} margin={{ top: 10, right: 20, left: 10, bottom: 10 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
|
||
<XAxis dataKey="name" tick={{ fontSize: 11, fill: '#4F758B' }} interval={0} angle={-20} textAnchor="end" height={60} />
|
||
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
|
||
<Tooltip />
|
||
<Bar dataKey="value" name="Salidas" radius={[4, 4, 0, 0]}>
|
||
{chartDataPorPais.map((entry, index) => (
|
||
<Cell key={`pais-${index}`} fill={entry.fill} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm">
|
||
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">Salida Laboral Según Proyecto</h3>
|
||
<div className="h-72">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={chartDataPorProyecto} margin={{ top: 10, right: 20, left: 10, bottom: 10 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
|
||
<XAxis dataKey="name" tick={{ fontSize: 11, fill: '#4F758B' }} interval={0} angle={-20} textAnchor="end" height={60} />
|
||
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
|
||
<Tooltip />
|
||
<Bar dataKey="value" name="Salidas" radius={[4, 4, 0, 0]}>
|
||
{chartDataPorProyecto.map((entry, index) => (
|
||
<Cell key={`proyecto-${index}`} fill={entry.fill} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* TABLA GLM */}
|
||
<div className="bg-white rounded-xl border border-gris-medio/30 shadow-sm overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gris-medio/30 bg-verde-bg flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||
<h3 className="text-base font-bold text-azul-glm">Detalle de Registros</h3>
|
||
<div className="flex items-center gap-2">
|
||
<label className="text-xs font-bold text-azul-glm uppercase tracking-wider">Buscar empleado</label>
|
||
<input
|
||
type="text"
|
||
value={busquedaNombre}
|
||
onChange={(e) => { setBusquedaNombre(e.target.value); setPaginaActual(1); }}
|
||
placeholder="Escriba el nombre..."
|
||
className="w-56 rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="min-w-full divide-y divide-gris-medio/50 text-sm">
|
||
<thead className="bg-azul-glm">
|
||
<tr>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Fecha</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Nombre</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">País</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Cliente/Proyecto</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Posición</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Jefe Directo</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">PDV</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Trade Partner</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Motivo</th>
|
||
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Sentimiento</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="bg-white divide-y divide-gris-medio/30">
|
||
{datosPaginados.map((item, idx) => (
|
||
<tr key={idx} className={`${idx % 2 === 0 ? 'bg-white' : 'bg-gris-claro'} hover:bg-verde-bg/50 transition-colors`}>
|
||
<td className="px-6 py-4 whitespace-nowrap text-acero">
|
||
{new Date(item.timestamp).toLocaleDateString()}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap font-semibold text-gris-oscuro">{item.nombre}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-gris-oscuro">{item.pais}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-gris-oscuro">{item.cuenta_proyecto}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-gris-oscuro">{item.posicion}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-gris-oscuro">{item.jefe_directo}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-gris-oscuro">{item.pdv}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-gris-oscuro">{item.trade_partner}</td>
|
||
<td className="px-6 py-4 text-gris-oscuro max-w-xs truncate">{item.homologacion_ia}</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
{(() => {
|
||
const sent = String(item.sentimiento_ia || '').toLowerCase().trim();
|
||
let classes = 'bg-gray-100 text-gray-800';
|
||
if (sent === 'positivo') {
|
||
classes = 'bg-verde-ok text-green-700';
|
||
} else if (sent === 'negativo') {
|
||
classes = 'bg-rojo-suave text-red-600';
|
||
} else if (sent === 'neutro') {
|
||
classes = 'bg-amarillo-medio text-yellow-700';
|
||
}
|
||
return (
|
||
<span className={`px-2.5 py-1 inline-flex text-xs leading-5 font-bold rounded-full ${classes}`}>
|
||
{item.sentimiento_ia}
|
||
</span>
|
||
);
|
||
})()}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Controles de Pagina */}
|
||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-6 py-4 border-t border-gris-medio/30 bg-gris-claro/50">
|
||
<div className="text-xs text-acero font-medium">
|
||
{totalRegistros > 0
|
||
? `Mostrando registros ${indicePrimerItem + 1} - ${Math.min(indiceUltimoItem, totalRegistros)} de ${totalRegistros}`
|
||
: 'No hay registros para mostrar'
|
||
}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setPaginaActual(prev => Math.max(prev - 1, 1))}
|
||
disabled={paginaActual === 1}
|
||
className="px-3 py-1.5 rounded-lg border border-gris-medio text-xs font-semibold text-azul-glm bg-white hover:bg-gris-claro disabled:opacity-50 disabled:cursor-not-allowed transition-all cursor-pointer"
|
||
>
|
||
Atrás
|
||
</button>
|
||
<span className="text-xs text-gris-oscuro font-bold">
|
||
Página {paginaActual} de {totalPaginas}
|
||
</span>
|
||
<button
|
||
onClick={() => setPaginaActual(prev => Math.min(prev + 1, totalPaginas))}
|
||
disabled={paginaActual === totalPaginas}
|
||
className="px-3 py-1.5 rounded-lg border border-gris-medio text-xs font-semibold text-azul-glm bg-white hover:bg-gris-claro disabled:opacity-50 disabled:cursor-not-allowed transition-all cursor-pointer"
|
||
>
|
||
Siguiente
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|