Files
dashboard/src/screens/DashboardGLMWAY.jsx
T

347 lines
18 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo } from 'react';
import { supabaseData } from '../services/supabaseClient';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell } from 'recharts';
import NavBar from '../components/NavBar';
const CHART_PALETTE = ['#6CC24A', '#4F758B', '#A4D65E', '#5B7F95', '#C4D600', '#6B8FA3', '#3E6B8A', '#8FBF5A'];
export default function DashboardGLMWAY() {
const [rawData, setRawData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filtroAnio, setFiltroAnio] = useState('');
const [filtroMes, setFiltroMes] = useState('');
const [filtroPais, setFiltroPais] = useState('');
const [filtroDivision, setFiltroDivision] = useState('');
const [filtroCoordinador, setFiltroCoordinador] = useState('');
const [filtroFacilitador, setFiltroFacilitador] = useState('');
const [evaluaciones, setEvaluaciones] = useState([]);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
setError(null);
console.log('[DashboardGLMWAY] Iniciando fetch...');
const [inscritosResult, evaluacionesResult] = await Promise.all([
supabaseData
.from('inscritos_glmway')
.select('*')
.order('fecha_ingreso', { ascending: false }),
supabaseData
.from('glmway_evaluacion_kpi')
.select('evaluaciones_enviadas, evaluaciones_resueltas'),
]);
if (inscritosResult.error) {
console.error('[DashboardGLMWAY] Error de Supabase:', {
message: inscritosResult.error.message,
details: inscritosResult.error.details,
hint: inscritosResult.error.hint,
code: inscritosResult.error.code,
});
setError(inscritosResult.error.message || 'Error al cargar los datos de empleados.');
} else {
console.log('[DashboardGLMWAY] Datos recibidos:', inscritosResult.data?.length ?? 0, 'registros');
setRawData(inscritosResult.data ?? []);
}
if (evaluacionesResult.error) {
console.error('[DashboardGLMWAY] Error evaluaciones:', evaluacionesResult.error);
} else {
setEvaluaciones(evaluacionesResult.data ?? []);
}
setLoading(false);
};
const aniosDisponibles = useMemo(() => {
const anios = new Set();
rawData.forEach(item => {
if (item.fecha_ingreso) {
const anio = item.fecha_ingreso.substring(0, 4);
anios.add(anio);
}
});
return Array.from(anios).sort((a, b) => b - a);
}, [rawData]);
const mesesDisponibles = useMemo(() => {
let datosFiltrados = rawData;
if (filtroAnio) {
datosFiltrados = datosFiltrados.filter(item => item.fecha_ingreso?.startsWith(filtroAnio));
}
const meses = new Set();
datosFiltrados.forEach(item => {
if (item.fecha_ingreso) {
const mes = item.fecha_ingreso.substring(5, 7);
meses.add(mes);
}
});
return Array.from(meses).sort();
}, [rawData, filtroAnio]);
const paisesDisponibles = useMemo(() => {
const paises = new Set();
rawData.forEach(item => { if (item.pais) paises.add(item.pais); });
return Array.from(paises).sort();
}, [rawData]);
const divisionesDisponibles = useMemo(() => {
const divisiones = new Set();
rawData.forEach(item => { if (item.division) divisiones.add(item.division); });
return Array.from(divisiones).sort();
}, [rawData]);
const coordinadoresDisponibles = useMemo(() => {
const coordinadores = new Set();
rawData.forEach(item => { if (item.coordinador_rrhh) coordinadores.add(item.coordinador_rrhh); });
return Array.from(coordinadores).sort();
}, [rawData]);
const facilitadoresDisponibles = useMemo(() => {
const facilitadores = new Set();
rawData.forEach(item => { if (item.instructor) facilitadores.add(item.instructor); });
return Array.from(facilitadores).sort();
}, [rawData]);
const datosFiltrados = useMemo(() => {
return rawData.filter(item => {
if (!item.fecha_ingreso) return false;
if (filtroAnio && !item.fecha_ingreso.startsWith(filtroAnio)) return false;
if (filtroMes && !item.fecha_ingreso.substring(5, 7).startsWith(filtroMes)) return false;
if (filtroPais && item.pais !== filtroPais) return false;
if (filtroDivision && item.division !== filtroDivision) return false;
if (filtroCoordinador && item.coordinador_rrhh !== filtroCoordinador) return false;
if (filtroFacilitador && item.instructor !== filtroFacilitador) return false;
return true;
});
}, [rawData, filtroAnio, filtroMes, filtroPais, filtroDivision, filtroCoordinador, filtroFacilitador]);
const mesesNombres = {
'01': 'Enero', '02': 'Febrero', '03': 'Marzo', '04': 'Abril',
'05': 'Mayo', '06': 'Junio', '07': 'Julio', '08': 'Agosto',
'09': 'Septiembre', '10': 'Octubre', '11': 'Noviembre', '12': 'Diciembre'
};
const totalEmpleados = datosFiltrados.length;
const totalParticipantes = useMemo(() => {
return datosFiltrados.filter(item => item.asistio_evento_glmway === true).length;
}, [datosFiltrados]);
const totalEvaluacionesEnviadas = useMemo(() => {
return evaluaciones.reduce((sum, item) => sum + (item.evaluaciones_enviadas || 0), 0);
}, [evaluaciones]);
const totalEvaluacionesResueltas = useMemo(() => {
return evaluaciones.reduce((sum, item) => sum + (item.evaluaciones_resueltas || 0), 0);
}, [evaluaciones]);
const rankingCoordinadores = useMemo(() => {
const conteo = {};
datosFiltrados.forEach(item => {
if (item.coordinador_rrhh && item.asistio_evento_glmway === true) {
conteo[item.coordinador_rrhh] = (conteo[item.coordinador_rrhh] || 0) + 1;
}
});
return Object.entries(conteo)
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
}, [datosFiltrados]);
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 GLMWAY</h1>
<p className="text-xs text-acero italic">Fecha de ingreso de empleados</p>
</div>
</header>
{/* NAVEGACIÓN GLM */}
<NavBar />
{/* FILTROS GLM */}
<div className="flex flex-wrap gap-4 mb-8 bg-white p-5 rounded-xl border border-gris-medio/30 shadow-sm">
<div className="flex-1 min-w-[150px]">
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por Año</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={filtroAnio}
onChange={(e) => { setFiltroAnio(e.target.value); setFiltroMes(''); }}
>
<option value="">Todos los años</option>
{aniosDisponibles.map((anio, idx) => (
<option key={idx} value={anio}>{anio}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[150px]">
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Filtrar por Mes</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={filtroMes}
onChange={(e) => setFiltroMes(e.target.value)}
>
<option value="">Todos los meses</option>
{mesesDisponibles.map((mes, idx) => (
<option key={idx} value={mes}>{mesesNombres[mes]}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[150px]">
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">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>
{paisesDisponibles.map((p, idx) => (
<option key={idx} value={p}>{p}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[150px]">
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Divisió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={filtroDivision}
onChange={(e) => setFiltroDivision(e.target.value)}
>
<option value="">Todas las divisiones</option>
{divisionesDisponibles.map((d, idx) => (
<option key={idx} value={d}>{d}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[150px]">
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Coordinador RH</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={filtroCoordinador}
onChange={(e) => setFiltroCoordinador(e.target.value)}
>
<option value="">Todos los coordinadores</option>
{coordinadoresDisponibles.map((c, idx) => (
<option key={idx} value={c}>{c}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[150px]">
<label className="block text-xs font-bold text-azul-glm uppercase tracking-wider mb-1">Facilitador</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={filtroFacilitador}
onChange={(e) => setFiltroFacilitador(e.target.value)}
>
<option value="">Todos los facilitadores</option>
{facilitadoresDisponibles.map((f, idx) => (
<option key={idx} value={f}>{f}</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={() => { setFiltroAnio(''); setFiltroMes(''); setFiltroPais(''); setFiltroDivision(''); setFiltroCoordinador(''); setFiltroFacilitador(''); }}
className="w-full md:w-auto 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>
{/* KPI 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-azul-glm">
<h2 className="text-xs text-acero uppercase font-bold tracking-wider">Total Empleados</h2>
<p className="text-3xl font-extrabold text-azul-glm mt-1">{totalEmpleados}</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">Participaron en Sesiones</h2>
<p className="text-3xl font-extrabold text-verde-glm mt-1">
{totalEmpleados > 0 ? Math.round((totalParticipantes / totalEmpleados) * 100) : 0}%
</p>
<p className="text-xs text-acero mt-1">{totalParticipantes} de {totalEmpleados} inscritos</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">Evaluaciones Enviadas</h2>
<p className="text-3xl font-extrabold text-azul-medio mt-1">{totalEvaluacionesEnviadas}</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">Evaluaciones Completadas</h2>
<p className="text-3xl font-extrabold text-verde-glm mt-1">{totalEvaluacionesResueltas}</p>
</div>
</div>
{/* RANKING COORDINADORES RH */}
<div className="bg-white p-6 rounded-xl border border-gris-medio/30 shadow-sm mb-8">
<h3 className="text-base font-bold text-azul-glm mb-4 pb-2 border-b-2 border-verde-glm">
Ranking de Coordinadores RH
</h3>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={rankingCoordinadores} margin={{ top: 10, right: 20, left: 20, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
<XAxis
dataKey="name"
interval={0}
angle={-30}
textAnchor="end"
height={70}
tick={{ fontSize: 11, fill: '#4F758B' }}
/>
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
<Tooltip
formatter={(value) => [value, 'Empleados']}
/>
<Bar dataKey="value" name="Empleados" radius={[4, 4, 0, 0]}>
{rankingCoordinadores.map((entry, index) => (
<Cell key={index} fill={CHART_PALETTE[index % CHART_PALETTE.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}