7bab90b408
- 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
162 lines
7.4 KiB
Plaintext
162 lines
7.4 KiB
Plaintext
---
|
||
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>
|