544bbf38a9
Buscador catálogo alumno: - BuscadorCatalogo.tsx con debounce 120ms, normaliza tildes, ?q= en URL - FiltroCategorias usa data-* attributes, coordinación via window.__labreFiltrar - Cards con data-nombre y data-numero-inventario para filtro combinable AND Grid + imágenes + unidades individuales (admin): - Inventario ahora es grid 2/3/4/5 col con fotos aspect-square - MaterialForm: input file con preview, checkbox trackeado_por_unidad - UnidadesManager: agregar/editar/eliminar unidades por material - Reasignar unidad desde detalles de solicitud (aprobada/activa) - Alumno ve etiqueta de unidad asignada en mis-préstamos - Helper materialImg.imgUrl(path) para URL pública del bucket - Auto-submit debounced del filtro de inventario (input + selects) Estadísticas separada: - Nueva ruta /admin/estadisticas con 6 KPIs (movidos de /admin/inventario) - 3 gráficas recharts: top 10 materiales, solicitudes/día, distribución estados - Chart.tsx wrapper con paleta UABC + respeta prefers-reduced-motion - Nav admin: Panel · Solicitudes · Inventario (box) · Estadísticas (stats) · Reportes - admin/index.astro usa todayMX() (fix inconsistencia) Notificaciones pendientes (admin): - Badge SSR con conteo de pendientes sobre ícono Solicitudes (sidebar + dock) - BadgeSolicitudes.tsx: canal Realtime en prestamos.solicitudes, +1 en INSERT con toast, refetch en UPDATE (replica identity default no trae old.estado) - Recharts añadido a package.json
101 lines
3.0 KiB
TypeScript
101 lines
3.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
type Cat = { id: number; nombre: string };
|
|
type Props = { categorias: Cat[] };
|
|
|
|
const ALL = 'all';
|
|
|
|
function applyFilter(value: string) {
|
|
const cards = document.querySelectorAll<HTMLElement>('[data-cat]');
|
|
cards.forEach((el) => {
|
|
const cat = el.dataset.cat;
|
|
const show = value === ALL || cat === value;
|
|
if (show) delete el.dataset.hiddenByCat;
|
|
else el.dataset.hiddenByCat = 'true';
|
|
});
|
|
// El buscador (BuscadorCatalogo, client:load) define este helper y unifica
|
|
// ambos flags (cat + search). Fallback si aún no montó: aplicar directo.
|
|
if (typeof window !== 'undefined' && window.__labreFiltrar) {
|
|
window.__labreFiltrar();
|
|
} else {
|
|
let anyVisible = false;
|
|
cards.forEach((el) => {
|
|
el.hidden = el.dataset.hiddenByCat === 'true';
|
|
if (!el.hidden) anyVisible = true;
|
|
});
|
|
const empty = document.getElementById('catalogo-empty-filter');
|
|
if (empty) empty.hidden = anyVisible || cards.length === 0;
|
|
}
|
|
}
|
|
|
|
export default function FiltroCategorias({ categorias }: Props) {
|
|
const [active, setActive] = useState<string>(ALL);
|
|
|
|
useEffect(() => {
|
|
const url = new URL(location.href);
|
|
const cat = url.searchParams.get('cat');
|
|
const initial = cat && categorias.some((c) => String(c.id) === cat) ? cat : ALL;
|
|
setActive(initial);
|
|
applyFilter(initial);
|
|
}, [categorias]);
|
|
|
|
const pick = (value: string) => {
|
|
setActive(value);
|
|
const url = new URL(location.href);
|
|
if (value === ALL) url.searchParams.delete('cat');
|
|
else url.searchParams.set('cat', value);
|
|
history.replaceState(null, '', url.toString());
|
|
applyFilter(value);
|
|
};
|
|
|
|
const chipClass = (v: string) =>
|
|
v === active
|
|
? 'btn'
|
|
: 'btn btn-ghost';
|
|
|
|
const chipStyle = (v: string): React.CSSProperties =>
|
|
v === active
|
|
? { background: 'var(--color-primary)', color: 'white', minHeight: '40px', paddingBlock: '0.375rem' }
|
|
: { minHeight: '40px', paddingBlock: '0.375rem' };
|
|
|
|
return (
|
|
<div
|
|
role="tablist"
|
|
aria-label="Filtrar por categoría"
|
|
className="flex gap-2 overflow-x-auto -mx-4 px-4 pb-1"
|
|
style={{ scrollbarWidth: 'thin' }}
|
|
>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={active === ALL}
|
|
className={chipClass(ALL)}
|
|
style={chipStyle(ALL)}
|
|
data-cat-value={ALL}
|
|
data-cat-active={active === ALL ? 'true' : undefined}
|
|
onClick={() => pick(ALL)}
|
|
>
|
|
Todas
|
|
</button>
|
|
{categorias.map((c) => {
|
|
const v = String(c.id);
|
|
return (
|
|
<button
|
|
key={c.id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={active === v}
|
|
className={chipClass(v)}
|
|
style={chipStyle(v)}
|
|
data-cat-value={v}
|
|
data-cat-active={active === v ? 'true' : undefined}
|
|
onClick={() => pick(v)}
|
|
>
|
|
{c.nombre}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|