Rediseño: vale de préstamo multi-ítem, fiel al formulario de papel
El vale físico del LSC permite pedir varios materiales en un solo trámite; el sistema modelaba 1 solicitud = 1 material. Migra prestamos.prestamos -> solicitudes (cabecera) + solicitud_items (renglones), vía RENAME + backfill (preserva ids/historial real). - RPC prestamos.crear_solicitud: transaccional, lockea materiales por fila, arregla la race condition del insert directo anterior. El alumno ya no inserta directo (RLS lo bloquea). - sync_stock/log_estado reescritos para iterar renglones por vale. - maestro_responsable (por vale) y profiles.semestre (perfil, nullable, sin UI todavía — onboarding queda para después). - Alumno: carrito (SolicitudCart/AgregarMaterial) reemplaza el modal de solicitud único por material. - Admin: 3 vistas de solicitudes, reportes y CSV export listan renglones por vale; api/admin/prestamos -> api/admin/solicitudes. De paso corrige un bug preexistente en detalles.ts (audit_log nunca se ordenaba por la columna correcta). Verificado: build limpio, ciclo RPC+triggers probado en una transacción revertida en prod (sin residuo), 9 vistas SSR smoke-testeadas contra el schema real. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,27 +3,33 @@ import AppLayout from '@/layouts/AppLayout.astro';
|
||||
|
||||
type Estado = 'pendiente' | 'aprobado' | 'rechazado' | 'activo' | 'devuelto' | 'vencido';
|
||||
|
||||
type Prestamo = {
|
||||
id: number;
|
||||
type Item = {
|
||||
cantidad: number;
|
||||
descripcion: string | null;
|
||||
material: { id: number; nombre: string; numero_inventario: string | null } | null;
|
||||
};
|
||||
|
||||
type Solicitud = {
|
||||
id: number;
|
||||
estado: Estado;
|
||||
fecha_solicitud: string;
|
||||
fecha_aprobacion: string | null;
|
||||
fecha_devolucion_estimada: string | null;
|
||||
fecha_devolucion_real: string | null;
|
||||
notas: string | null;
|
||||
material: { id: number; nombre: string; numero_inventario: string | null } | null;
|
||||
maestro_responsable: string;
|
||||
items: Item[];
|
||||
};
|
||||
|
||||
const user = Astro.locals.user!;
|
||||
|
||||
const { data, error } = await Astro.locals.supabase
|
||||
.from('prestamos')
|
||||
.select('id, cantidad, estado, fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, fecha_devolucion_real, notas, material:materiales(id, nombre, numero_inventario)')
|
||||
.from('solicitudes')
|
||||
.select('id, estado, fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, fecha_devolucion_real, notas, maestro_responsable, items:solicitud_items(cantidad, descripcion, material:materiales(id, nombre, numero_inventario))')
|
||||
.eq('alumno_id', user.id)
|
||||
.order('fecha_solicitud', { ascending: false });
|
||||
|
||||
const prestamos = (data ?? []) as unknown as Prestamo[];
|
||||
const prestamos = (data ?? []) as unknown as Solicitud[];
|
||||
|
||||
const activosSet = new Set<Estado>(['pendiente', 'aprobado', 'activo']);
|
||||
const activos = prestamos.filter((p) => activosSet.has(p.estado));
|
||||
@@ -87,19 +93,27 @@ const badgeStyle = (e: Estado) =>
|
||||
{activos.map((p) => (
|
||||
<li class="card">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<h3 class="font-semibold leading-snug truncate">{p.material?.nombre ?? 'Material eliminado'}</h3>
|
||||
<p class="text-xs opacity-60 font-mono mt-0.5">
|
||||
{p.material?.numero_inventario ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="min-w-0 space-y-1">
|
||||
{p.items.map((it) => (
|
||||
<li class="leading-snug">
|
||||
<span class="font-semibold" style="font-variant-numeric: tabular-nums;">{it.cantidad}×</span>{' '}
|
||||
<span class="font-semibold">{it.material?.nombre ?? 'Material eliminado'}</span>
|
||||
{it.material?.numero_inventario && (
|
||||
<span class="text-xs opacity-60 font-mono ml-1.5">{it.material.numero_inventario}</span>
|
||||
)}
|
||||
{it.descripcion && (
|
||||
<p class="text-xs opacity-60 ml-0.5">{it.descripcion}</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<span class="text-xs font-medium px-2 py-1 rounded-[2px] whitespace-nowrap" style={badgeStyle(p.estado)}>
|
||||
{estadoLabel[p.estado]}
|
||||
</span>
|
||||
</div>
|
||||
<dl class="mt-3 grid grid-cols-2 gap-y-1 text-sm">
|
||||
<dt class="opacity-60">Cantidad</dt>
|
||||
<dd class="text-right" style="font-variant-numeric: tabular-nums;">{p.cantidad}</dd>
|
||||
<dt class="opacity-60">Maestro responsable</dt>
|
||||
<dd class="text-right truncate">{p.maestro_responsable}</dd>
|
||||
<dt class="opacity-60">Solicitado</dt>
|
||||
<dd class="text-right">{fmt(p.fecha_solicitud)}</dd>
|
||||
{p.fecha_aprobacion && (
|
||||
@@ -141,19 +155,27 @@ const badgeStyle = (e: Estado) =>
|
||||
{historial.map((p) => (
|
||||
<li class="card" style="opacity: 0.92;">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<h3 class="font-semibold leading-snug truncate">{p.material?.nombre ?? 'Material eliminado'}</h3>
|
||||
<p class="text-xs opacity-60 font-mono mt-0.5">
|
||||
{p.material?.numero_inventario ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="min-w-0 space-y-1">
|
||||
{p.items.map((it) => (
|
||||
<li class="leading-snug">
|
||||
<span class="font-semibold" style="font-variant-numeric: tabular-nums;">{it.cantidad}×</span>{' '}
|
||||
<span class="font-semibold">{it.material?.nombre ?? 'Material eliminado'}</span>
|
||||
{it.material?.numero_inventario && (
|
||||
<span class="text-xs opacity-60 font-mono ml-1.5">{it.material.numero_inventario}</span>
|
||||
)}
|
||||
{it.descripcion && (
|
||||
<p class="text-xs opacity-60 ml-0.5">{it.descripcion}</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<span class="text-xs font-medium px-2 py-1 rounded-[2px] whitespace-nowrap" style={badgeStyle(p.estado)}>
|
||||
{estadoLabel[p.estado]}
|
||||
</span>
|
||||
</div>
|
||||
<dl class="mt-3 grid grid-cols-2 gap-y-1 text-sm">
|
||||
<dt class="opacity-60">Cantidad</dt>
|
||||
<dd class="text-right" style="font-variant-numeric: tabular-nums;">{p.cantidad}</dd>
|
||||
<dt class="opacity-60">Maestro responsable</dt>
|
||||
<dd class="text-right truncate">{p.maestro_responsable}</dd>
|
||||
<dt class="opacity-60">Solicitado</dt>
|
||||
<dd class="text-right">{fmt(p.fecha_solicitud)}</dd>
|
||||
{p.fecha_devolucion_real && (
|
||||
|
||||
Reference in New Issue
Block a user