245 lines
12 KiB
React
245 lines
12 KiB
React
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>
|
||
);
|
||
}
|