Files
dashboard/src/screens/DashboardCarnet.jsx
T

245 lines
12 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, Cell, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid } from 'recharts';
import NavBar from '../components/NavBar';
const CHART_PALETTE = ['#6CC24A', '#4F758B', '#A4D65E', '#5B7F95', '#C4D600', '#6B8FA3', '#3E6B8A', '#8FBF5A'];
export default function DashboardCarnet() {
const [rawData, setRawData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filtroDepartamento, setFiltroDepartamento] = useState('');
const [filtroPosicion, setFiltroPosicion] = useState('');
const [filtroPais, setFiltroPais] = useState('');
const [filtroDivision, setFiltroDivision] = useState('');
useEffect(() => {
fetchData();
}, []);
// Helper para obtener valores únicos ordenados de una columna (obtiene todos los valores para la lista del filtro)
const opcionesUnicas = (campo) => [
...new Set(
rawData
.map(item => (item?.[campo] ?? '').toString().trim())
.filter(v => v !== '')
),
].sort((a, b) => a.localeCompare(b, 'es'));
const departamentos = useMemo(() => opcionesUnicas('departamento'), [rawData]);
const posiciones = useMemo(() => opcionesUnicas('posicion'), [rawData]);
const paises = useMemo(() => opcionesUnicas('pais'), [rawData]);
const divisiones = useMemo(() => opcionesUnicas('division'), [rawData]);
const fetchData = async () => {
setLoading(true);
setError(null);
console.log('[DashboardCarnet] Iniciando fetch...');
const { data, error: fetchError } = await supabaseData
.from('carnet_empleados_creados_glm')
.select('*')
.order('created_at', { ascending: false });
if (fetchError) {
console.error('[DashboardCarnet] Error de Supabase:', {
message: fetchError.message,
details: fetchError.details,
hint: fetchError.hint,
code: fetchError.code,
});
setError(fetchError.message || 'Error al cargar los datos de carnets.');
} else {
console.log('[DashboardCarnet] Datos recibidos:', data?.length ?? 0, 'registros');
setRawData(data ?? []);
}
setLoading(false);
};
const datosFiltrados = rawData.filter(item => {
const matchDepartamento = !filtroDepartamento || (item?.departamento ?? '').toString().trim() === filtroDepartamento;
const matchPosicion = !filtroPosicion || (item?.posicion ?? '').toString().trim() === filtroPosicion;
const matchPais = !filtroPais || (item?.pais ?? '').toString().trim() === filtroPais;
const matchDivision = !filtroDivision || (item?.division ?? '').toString().trim() === filtroDivision;
return matchDepartamento && matchPosicion && matchPais && matchDivision;
});
const totalCarnets = datosFiltrados.length;
const limpiarFiltros = () => {
setFiltroDepartamento('');
setFiltroPosicion('');
setFiltroPais('');
setFiltroDivision('');
};
const hayFiltrosActivos = filtroDepartamento || filtroPosicion || filtroPais || filtroDivision;
const conteoPorDivision = datosFiltrados.reduce((acc, curr) => {
const division = (curr.division || 'Sin división').toString().trim() || 'Sin división';
acc[division] = (acc[division] || 0) + 1;
return acc;
}, {});
const chartDataDivision = Object.keys(conteoPorDivision).map((division, index) => ({
name: division,
value: conteoPorDivision[division],
fill: CHART_PALETTE[index % CHART_PALETTE.length]
}));
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 Carnets</h1>
<p className="text-xs text-acero italic">Conteo de carnets creados por división</p>
</div>
</header>
{/* NAVEGACIÓN GLM */}
<NavBar />
{/* FILTROS GLM */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 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 Departamento</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={filtroDepartamento}
onChange={(e) => setFiltroDepartamento(e.target.value)}
>
<option value="">Todos los departamentos</option>
{departamentos.map((dep, idx) => (
<option key={idx} value={dep}>{dep}</option>
))}
</select>
</div>
<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 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 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>
{divisiones.map((division, idx) => (
<option key={idx} value={division}>{division}</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={limpiarFiltros}
disabled={!hayFiltrosActivos}
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 disabled:opacity-50 disabled:cursor-not-allowed"
>
Limpiar Filtros
</button>
</div>
</div>
{/* KPI 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 border-l-[6px] border-azul-glm">
<h2 className="text-xs text-acero uppercase font-bold tracking-wider">Total Carnets Creados</h2>
<p className="text-3xl font-extrabold text-azul-glm mt-1">{totalCarnets}</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">Divisiones</h2>
<p className="text-3xl font-extrabold text-verde-glm mt-1">{divisiones.length}</p>
<p className="text-xs text-acero mt-1">distintas en el histórico</p>
</div>
</div>
{/* GRÁFICA GLM */}
<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">Carnets por División</h3>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartDataDivision} margin={{ top: 10, right: 20, left: 20, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
<XAxis
dataKey="name"
angle={-30}
textAnchor="end"
interval={0}
height={70}
tick={{ fontSize: 11, fill: '#4F758B' }}
/>
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: '#4F758B' }} />
<Tooltip />
<Legend />
<Bar dataKey="value" name="Carnets" radius={[4, 4, 0, 0]}>
{chartDataDivision.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}