feat: Nuevos filtros para dashboard de carnet

This commit is contained in:
2026-09-03 11:17:52 -04:00
parent 379342e696
commit 71b70e78ee
3 changed files with 117 additions and 48 deletions
+99 -34
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from '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';
@@ -9,17 +9,29 @@ export default function DashboardCarnet() {
const [rawData, setRawData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [clientes, setClientes] = useState([]);
const [filtroCliente, setFiltroCliente] = useState('');
const [filtroDepartamento, setFiltroDepartamento] = useState('');
const [filtroPosicion, setFiltroPosicion] = useState('');
const [filtroPais, setFiltroPais] = useState('');
const [filtroDivision, setFiltroDivision] = useState('');
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
const uniqueClientes = [...new Set(rawData.map(item => item.cliente_proyecto).filter(Boolean))];
setClientes(uniqueClientes);
}, [rawData]);
// 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);
@@ -46,21 +58,34 @@ export default function DashboardCarnet() {
setLoading(false);
};
const datosFiltrados = filtroCliente
? rawData.filter(item => item.cliente_proyecto === filtroCliente)
: rawData;
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 conteoPorCliente = rawData.reduce((acc, curr) => {
const cliente = curr.cliente_proyecto || 'Sin cliente';
acc[cliente] = (acc[cliente] || 0) + 1;
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 chartDataCliente = Object.keys(conteoPorCliente).map((cliente, index) => ({
name: cliente,
value: conteoPorCliente[cliente],
const chartDataDivision = Object.keys(conteoPorDivision).map((division, index) => ({
name: division,
value: conteoPorDivision[division],
fill: CHART_PALETTE[index % CHART_PALETTE.length]
}));
@@ -102,34 +127,74 @@ export default function DashboardCarnet() {
</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 cliente / proyecto</p>
<p className="text-xs text-acero italic">Conteo de carnets creados por división</p>
</div>
</header>
{/* NAVEGACIÓN GLM */}
<NavBar />
{/* FILTRO GLM */}
<div className="flex flex-col md:flex-row gap-4 mb-8 bg-white p-5 rounded-xl border border-gris-medio/30 shadow-sm">
{/* 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 Cliente / Proyecto</label>
<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={filtroCliente}
onChange={(e) => setFiltroCliente(e.target.value)}
value={filtroDepartamento}
onChange={(e) => setFiltroDepartamento(e.target.value)}
>
<option value="">Todos los clientes / proyectos</option>
{clientes.map((cliente, idx) => (
<option key={idx} value={cliente}>{cliente}</option>
<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={() => setFiltroCliente('')}
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"
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 Filtro
Limpiar Filtros
</button>
</div>
</div>
@@ -141,18 +206,18 @@ export default function DashboardCarnet() {
<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">Clientes / Proyectos</h2>
<p className="text-3xl font-extrabold text-verde-glm mt-1">{clientes.length}</p>
<p className="text-xs text-acero mt-1">distintos en el histórico</p>
<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 Cliente / Proyecto</h3>
<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={chartDataCliente} margin={{ top: 10, right: 20, left: 20, bottom: 60 }}>
<BarChart data={chartDataDivision} margin={{ top: 10, right: 20, left: 20, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0" />
<XAxis
dataKey="name"
@@ -166,7 +231,7 @@ export default function DashboardCarnet() {
<Tooltip />
<Legend />
<Bar dataKey="value" name="Carnets" radius={[4, 4, 0, 0]}>
{chartDataCliente.map((entry, index) => (
{chartDataDivision.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Bar>