5 features: buscador catálogo, grid+fotos+unidades, estadísticas separada, badge realtime
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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Layout from './Layout.astro';
|
||||
import Toaster from '@/components/Toaster.tsx';
|
||||
import BadgeSolicitudes from '@/components/admin/BadgeSolicitudes.tsx';
|
||||
import BrandMark from '@/components/BrandMark.astro';
|
||||
|
||||
interface Props {
|
||||
@@ -10,6 +11,15 @@ const { title } = Astro.props;
|
||||
|
||||
const profile = Astro.locals.profile;
|
||||
const isAdmin = profile?.rol === 'admin';
|
||||
|
||||
let pendientesCount = 0;
|
||||
if (isAdmin) {
|
||||
const { count } = await Astro.locals.supabase
|
||||
.from('solicitudes')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('estado', 'pendiente');
|
||||
pendientesCount = count ?? 0;
|
||||
}
|
||||
const path = Astro.url.pathname;
|
||||
const isActive = (href: string) => path === href || path.startsWith(href + '/');
|
||||
|
||||
@@ -37,7 +47,8 @@ const alumnoNav: NavItem[] = [
|
||||
const adminNav: NavItem[] = [
|
||||
{ href: '/admin', label: 'Panel', icon: 'dashboard' },
|
||||
{ href: '/admin/solicitudes', label: 'Solicitudes', icon: 'inbox' },
|
||||
{ href: '/admin/inventario', label: 'Estadísticas', icon: 'stats' },
|
||||
{ href: '/admin/inventario', label: 'Inventario', icon: 'box' },
|
||||
{ href: '/admin/estadisticas', label: 'Estadísticas', icon: 'stats' },
|
||||
{ href: '/admin/reportes', label: 'Reportes', icon: 'chart' },
|
||||
];
|
||||
const nav = isAdmin ? adminNav : alumnoNav;
|
||||
@@ -76,15 +87,26 @@ const activeCheck = (item: NavItem) => {
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-[2px] text-sm font-medium uppercase tracking-wide whitespace-nowrap transition-colors',
|
||||
'relative flex items-center gap-3 px-3 py-2.5 rounded-[2px] text-sm font-medium uppercase tracking-wide whitespace-nowrap transition-colors',
|
||||
activeCheck(item) ? 'bg-[color:var(--color-primary)] text-white' : 'hover:bg-[color:var(--color-chalk)]'
|
||||
]}
|
||||
style={activeCheck(item) ? 'border: 2px solid var(--color-ink);' : 'border: 2px solid transparent;'}
|
||||
aria-current={activeCheck(item) ? 'page' : undefined}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS[item.icon as keyof typeof ICONS]} />
|
||||
</svg>
|
||||
<span class="relative inline-grid place-items-center">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS[item.icon as keyof typeof ICONS]} />
|
||||
</svg>
|
||||
{item.href === '/admin/solicitudes' && (
|
||||
<span
|
||||
data-badge="pendientes"
|
||||
hidden={pendientesCount === 0}
|
||||
aria-label={`${pendientesCount} solicitudes pendientes`}
|
||||
class="absolute -top-2 -right-3 min-w-[18px] h-[18px] px-1 grid place-items-center rounded-[2px] font-mono font-bold text-[11px] leading-none"
|
||||
style="background: var(--color-danger); color: var(--color-ink); border: 2px solid var(--color-ink);"
|
||||
>{pendientesCount}</span>
|
||||
)}
|
||||
</span>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
@@ -149,11 +171,11 @@ const activeCheck = (item: NavItem) => {
|
||||
href={item.href}
|
||||
aria-label={item.label}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
class="flex-1 grid place-items-center py-2.5"
|
||||
class="relative flex-1 grid place-items-center py-2.5"
|
||||
>
|
||||
<span
|
||||
class:list={[
|
||||
'grid place-items-center rounded-[2px] w-11 h-11 transition-[background-color,transform] duration-100 motion-safe:active:scale-95',
|
||||
'relative grid place-items-center rounded-[2px] w-11 h-11 transition-[background-color,transform] duration-100 motion-safe:active:scale-95',
|
||||
active ? 'bg-[color:var(--color-primary)] text-white' : ''
|
||||
]}
|
||||
style={active ? 'border: 2px solid var(--color-ink);' : ''}
|
||||
@@ -161,6 +183,15 @@ const activeCheck = (item: NavItem) => {
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS[item.icon as keyof typeof ICONS]} />
|
||||
</svg>
|
||||
{item.href === '/admin/solicitudes' && (
|
||||
<span
|
||||
data-badge="pendientes"
|
||||
hidden={pendientesCount === 0}
|
||||
aria-label={`${pendientesCount} solicitudes pendientes`}
|
||||
class="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 grid place-items-center rounded-[2px] font-mono font-bold text-[11px] leading-none"
|
||||
style="background: var(--color-danger); color: var(--color-ink); border: 2px solid var(--color-ink);"
|
||||
>{pendientesCount}</span>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
@@ -181,6 +212,7 @@ const activeCheck = (item: NavItem) => {
|
||||
</nav>
|
||||
|
||||
<Toaster client:idle />
|
||||
{isAdmin && <BadgeSolicitudes client:idle />}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user