Files
labre-web/src/pages/admin/solicitudes/index.astro
T
LakG 7bab90b408 UI: navegación SPA, tabs sin bordes, patrón de fondo fijo
- ClientRouter (view transitions) para navegación sin recarga entera;
  Toaster y BadgeSolicitudes marcados con transition:persist para no
  rearmar la suscripción realtime en cada nav.
- Overlay de carga ahora activa a los 250ms del swap (evita parpadeo).
- Tabs de subnav (inventario/categorías/maestros, solicitudes, reportes)
  reescritos: sin border-b-2 ni underline verde; activo usa bg-linen
  como el sidebar. Coherente y sin patrón AI-slop.
- Marco horizontal debajo de todos los tab strips removido.
- Patrón de puntos verde del canvas ahora con background-attachment:
  fixed — estático en scroll.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xaxs8cGcQe9NwbMqA1gvn
2026-09-21 14:52:56 -07:00

162 lines
7.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import AppLayout from '@/layouts/AppLayout.astro';
import AccionesSolicitud from '@/components/admin/solicitudes/AccionesSolicitud.tsx';
import VerDetalles from '@/components/admin/solicitudes/VerDetalles.tsx';
import { truncate } from '@/lib/text';
const supabase = Astro.locals.supabase;
const { data, error } = await supabase
.from('solicitudes')
.select(
'id, fecha_solicitud, notas, maestro_responsable, alumno:profiles!alumno_id(id, nombre, email, matricula), items:solicitud_items(cantidad, descripcion, material:materiales(id, nombre, numero_inventario, cantidad_disponible))'
)
.eq('estado', 'pendiente')
.order('fecha_solicitud', { ascending: true });
const filas = data ?? [];
const fmtFecha = new Intl.DateTimeFormat('es-MX', { dateStyle: 'medium', timeStyle: 'short' });
const path = Astro.url.pathname;
const tabs = [
{ href: '/admin/solicitudes', label: 'Pendientes' },
{ href: '/admin/solicitudes/activos', label: 'En préstamo' },
{ href: '/admin/solicitudes/historial', label: 'Historial' },
];
---
<AppLayout title="Solicitudes pendientes — LabPréstamos">
<div class="max-w-6xl">
<header class="mb-6">
<div class="flex items-center gap-3 flex-wrap">
<h1 class="text-2xl md:text-3xl font-light">Solicitudes pendientes</h1>
<span
class="badge"
aria-label={`${filas.length} pendientes`}
>
{filas.length}
</span>
</div>
</header>
<nav aria-label="Vistas de solicitudes" class="mb-6 flex gap-2 overflow-x-auto">
{tabs.map((t) => {
const active = path === t.href || (t.href === '/admin/solicitudes' && path === '/admin/solicitudes/');
return (
<a
href={t.href}
class:list={[
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors rounded-[8px]',
active ? 'bg-linen text-verde-institucional' : 'text-graphite/70 hover:bg-linen hover:text-verde-institucional'
]}
aria-current={active ? 'page' : undefined}
>
{t.label}
</a>
);
})}
</nav>
{error ? (
<div role="alert" class="card text-center py-10">
<p class="font-medium text-graphite">No se pudo cargar la lista.</p>
<p class="text-sm text-graphite/70 mt-1">Recarga la página o contacta a soporte si persiste.</p>
</div>
) : filas.length === 0 ? (
<div class="card text-center py-12">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-3 text-verde-institucional" aria-hidden="true">
<path d="M4.5 12.75l6 6 9-13.5" />
</svg>
<p class="font-medium text-graphite">No hay solicitudes por revisar.</p>
<p class="text-sm text-graphite/70 mt-1">Todo al día.</p>
</div>
) : (
<>
{/* Desktop: tabla */}
<div class="card p-0 overflow-hidden hidden md:block">
<table class="w-full text-sm">
<thead class="text-left bg-linen text-graphite border-b border-hairline-gray">
<tr>
<th class="p-3 font-medium">Alumno</th>
<th class="p-3 font-medium">Material</th>
<th class="p-3 font-medium">Estado</th>
<th class="p-3 font-medium">Solicitado</th>
<th class="p-3 font-medium text-right">Acciones</th>
</tr>
</thead>
<tbody>
{filas.map((r: any) => (
<tr class="border-b border-hairline-gray last:border-b-0 align-top">
<td class="p-3">
<div class="font-medium text-graphite">{r.alumno?.nombre ?? '—'}</div>
<div class="text-xs text-graphite/70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
<div class="text-xs text-graphite/70 mt-1 break-words">Maestro: {r.maestro_responsable}</div>
</td>
<td class="p-3">
<div class="flex flex-col gap-1.5">
{(r.items ?? []).map((i: any) => (
<div>
<div>{i.cantidad}× {i.material?.nombre ?? '—'}</div>
<div class="text-xs text-graphite/70">
Inv. {i.material?.numero_inventario ?? '—'} · disp. <span style="font-variant-numeric: tabular-nums;">{i.material?.cantidad_disponible ?? 0}</span>
</div>
</div>
))}
</div>
{r.notas && (
<div class="text-xs mt-1.5 text-graphite/80 border-l border-hairline-gray pl-2">
<span class="opacity-60">Motivo:</span> <span class="break-words">{truncate(r.notas, 50)}</span>
</div>
)}
</td>
<td class="p-3">
<span class="chip chip-pendiente">Pendiente</span>
</td>
<td class="p-3 text-graphite/80">{fmtFecha.format(new Date(r.fecha_solicitud))}</td>
<td class="p-3">
<div class="flex items-center justify-end gap-2 flex-wrap">
<VerDetalles client:load prestamoId={r.id} />
<AccionesSolicitud client:load prestamo={r} />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: tarjetas */}
<ul class="md:hidden flex flex-col gap-3">
{filas.map((r: any) => (
<li class="card">
<div class="flex items-baseline justify-between gap-2">
<div class="font-medium text-graphite">{r.alumno?.nombre ?? '—'}</div>
<span class="chip chip-pendiente">Pendiente</span>
</div>
<div class="text-xs text-graphite/70 mt-0.5">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
<div class="text-xs text-graphite/70 mb-2 break-words">Maestro: {r.maestro_responsable}</div>
<div class="flex flex-col gap-1.5">
{(r.items ?? []).map((i: any) => (
<div class="text-sm">
<strong style="font-variant-numeric: tabular-nums;">{i.cantidad}×</strong> {i.material?.nombre ?? '—'}
<span class="text-graphite/70"> · Inv. {i.material?.numero_inventario ?? '—'} · disp. {i.material?.cantidad_disponible ?? 0}</span>
</div>
))}
</div>
{r.notas && (
<p class="text-sm mt-2 text-graphite/80 border-l border-hairline-gray pl-2">
<span class="opacity-60">Motivo:</span> <span class="break-words">{truncate(r.notas, 50)}</span>
</p>
)}
<div class="text-xs text-graphite/60 mt-2">
Solicitado {fmtFecha.format(new Date(r.fecha_solicitud))}
</div>
<div class="mt-3 pt-3 border-t border-hairline-gray flex items-center gap-2 flex-wrap">
<VerDetalles client:load prestamoId={r.id} />
<AccionesSolicitud client:load prestamo={r} />
</div>
</li>
))}
</ul>
</>
)}
</div>
</AppLayout>