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:
@@ -5,13 +5,13 @@ const supabase = Astro.locals.supabase;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const [pend, activos, vencidos, agotados, ultimas] = await Promise.all([
|
||||
supabase.from('prestamos').select('*', { count: 'exact', head: true }).eq('estado', 'pendiente'),
|
||||
supabase.from('prestamos').select('*', { count: 'exact', head: true }).in('estado', ['aprobado', 'activo']),
|
||||
supabase.from('prestamos').select('*', { count: 'exact', head: true }).in('estado', ['aprobado', 'activo']).lt('fecha_devolucion_estimada', today),
|
||||
supabase.from('solicitudes').select('*', { count: 'exact', head: true }).eq('estado', 'pendiente'),
|
||||
supabase.from('solicitudes').select('*', { count: 'exact', head: true }).in('estado', ['aprobado', 'activo']),
|
||||
supabase.from('solicitudes').select('*', { count: 'exact', head: true }).in('estado', ['aprobado', 'activo']).lt('fecha_devolucion_estimada', today),
|
||||
supabase.from('materiales').select('*', { count: 'exact', head: true }).eq('cantidad_disponible', 0),
|
||||
supabase
|
||||
.from('prestamos')
|
||||
.select('id, cantidad, fecha_solicitud, estado, alumno:profiles!alumno_id(nombre, email), material:materiales!material_id(nombre)')
|
||||
.from('solicitudes')
|
||||
.select('id, fecha_solicitud, estado, alumno:profiles!alumno_id(nombre, email), items:solicitud_items(cantidad, material:materiales(nombre))')
|
||||
.order('fecha_solicitud', { ascending: false })
|
||||
.limit(5),
|
||||
]);
|
||||
@@ -67,7 +67,6 @@ const estadoLabel: Record<string, string> = {
|
||||
<tr>
|
||||
<th class="p-3 font-medium">Alumno</th>
|
||||
<th class="p-3 font-medium">Material</th>
|
||||
<th class="p-3 font-medium">Cant.</th>
|
||||
<th class="p-3 font-medium">Estado</th>
|
||||
<th class="p-3 font-medium">Solicitado</th>
|
||||
</tr>
|
||||
@@ -76,8 +75,13 @@ const estadoLabel: Record<string, string> = {
|
||||
{ultimas.data.map((r: any) => (
|
||||
<tr class="border-t" style="border-color: color-mix(in oklab, var(--color-ink) 10%, transparent);">
|
||||
<td class="p-3">{r.alumno?.nombre ?? r.alumno?.email ?? '—'}</td>
|
||||
<td class="p-3">{r.material?.nombre ?? '—'}</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
{(r.items ?? []).map((i: any) => (
|
||||
<div>{i.cantidad}× {i.material?.nombre ?? '—'}</div>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3">{estadoLabel[r.estado] ?? r.estado}</td>
|
||||
<td class="p-3 opacity-80">{fmtFecha.format(new Date(r.fecha_solicitud))}</td>
|
||||
</tr>
|
||||
|
||||
@@ -30,12 +30,12 @@ const [
|
||||
{ count: nEnPrestamo },
|
||||
{ count: nVencidos },
|
||||
] = await Promise.all([
|
||||
supabase.from('prestamos').select('id', countHead).gte('fecha_solicitud', startISO).lt('fecha_solicitud', endISO),
|
||||
supabase.from('solicitudes').select('id', countHead).gte('fecha_solicitud', startISO).lt('fecha_solicitud', endISO),
|
||||
supabase.from('audit_log').select('id', countHead).eq('estado_nuevo', 'aprobado').gte('at', startISO).lt('at', endISO),
|
||||
supabase.from('audit_log').select('id', countHead).eq('estado_nuevo', 'rechazado').gte('at', startISO).lt('at', endISO),
|
||||
supabase.from('audit_log').select('id', countHead).eq('estado_nuevo', 'devuelto').gte('at', startISO).lt('at', endISO),
|
||||
supabase.from('prestamos').select('id', countHead).in('estado', ['aprobado', 'activo']),
|
||||
supabase.from('prestamos').select('id', countHead).in('estado', ['aprobado', 'activo']).lt('fecha_devolucion_estimada', dia.isoDate),
|
||||
supabase.from('solicitudes').select('id', countHead).in('estado', ['aprobado', 'activo']),
|
||||
supabase.from('solicitudes').select('id', countHead).in('estado', ['aprobado', 'activo']).lt('fecha_devolucion_estimada', dia.isoDate),
|
||||
]);
|
||||
|
||||
const kpis = [
|
||||
|
||||
@@ -63,12 +63,16 @@ let total = 0;
|
||||
let hitLimit = false;
|
||||
|
||||
if (vista === 'historial') {
|
||||
const itemsSelect = materialId
|
||||
? 'items:solicitud_items!inner(cantidad, descripcion, material:materiales(nombre, numero_inventario))'
|
||||
: 'items:solicitud_items(cantidad, descripcion, material:materiales(nombre, numero_inventario))';
|
||||
|
||||
let q = supabase
|
||||
.from('prestamos')
|
||||
.from('solicitudes')
|
||||
.select(`
|
||||
id, cantidad, fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, fecha_devolucion_real, estado,
|
||||
id, maestro_responsable, fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, fecha_devolucion_real, estado,
|
||||
alumno:profiles!alumno_id(nombre, email, matricula),
|
||||
material:materiales!material_id(nombre, numero_inventario)
|
||||
${itemsSelect}
|
||||
`)
|
||||
.order('fecha_solicitud', { ascending: false })
|
||||
// ponytail: 500, paginar cuando reportes rutinarios pasen de eso
|
||||
@@ -78,7 +82,7 @@ if (vista === 'historial') {
|
||||
// ponytail: bound de día en UTC; cambiar a bounds tz-aware si el reporte cruza medianoche PST/PDT
|
||||
if (hasta) q = q.lte('fecha_solicitud', `${hasta}T23:59:59.999Z`);
|
||||
if (estado && estado !== 'all') q = q.eq('estado', estado);
|
||||
if (materialId) q = q.eq('material_id', materialId);
|
||||
if (materialId) q = q.eq('items.material_id', materialId);
|
||||
if (alumnoId) q = q.eq('alumno_id', alumnoId);
|
||||
|
||||
const { data, error } = await q;
|
||||
@@ -92,11 +96,11 @@ if (vista === 'historial') {
|
||||
let vencidos: any[] = [];
|
||||
if (vista === 'vencidos') {
|
||||
const { data, error } = await supabase
|
||||
.from('prestamos')
|
||||
.from('solicitudes')
|
||||
.select(`
|
||||
id, cantidad, fecha_devolucion_estimada,
|
||||
id, fecha_devolucion_estimada,
|
||||
alumno:profiles!alumno_id(nombre, email, matricula),
|
||||
material:materiales!material_id(nombre, numero_inventario)
|
||||
items:solicitud_items(cantidad, material:materiales(nombre, numero_inventario))
|
||||
`)
|
||||
.in('estado', ['aprobado', 'activo'])
|
||||
.lt('fecha_devolucion_estimada', today)
|
||||
@@ -228,8 +232,8 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
<tr>
|
||||
<th class="p-3 font-medium">Fecha solicitud</th>
|
||||
<th class="p-3 font-medium">Alumno</th>
|
||||
<th class="p-3 font-medium">Maestro responsable</th>
|
||||
<th class="p-3 font-medium">Material</th>
|
||||
<th class="p-3 font-medium">Cant.</th>
|
||||
<th class="p-3 font-medium">Estado</th>
|
||||
<th class="p-3 font-medium">Fecha devolución</th>
|
||||
</tr>
|
||||
@@ -242,13 +246,17 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
<div class="font-medium">{r.alumno?.nombre ?? r.alumno?.email ?? '—'}</div>
|
||||
{r.alumno?.matricula && <div class="text-xs opacity-70">{r.alumno.matricula}</div>}
|
||||
</td>
|
||||
<td class="p-3 opacity-90">{r.maestro_responsable ?? '—'}</td>
|
||||
<td class="p-3">
|
||||
<div>{r.material?.nombre ?? '—'}</div>
|
||||
{r.material?.numero_inventario && (
|
||||
<div class="text-xs opacity-70">{r.material.numero_inventario}</div>
|
||||
)}
|
||||
{(r.items ?? []).map((i: any) => (
|
||||
<div>
|
||||
<span style="font-variant-numeric: tabular-nums;">{i.cantidad}×</span> {i.material?.nombre ?? '—'}
|
||||
{i.material?.numero_inventario && (
|
||||
<span class="text-xs opacity-70"> ({i.material.numero_inventario})</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3">{estadoLabel[r.estado] ?? r.estado}</td>
|
||||
<td class="p-3 opacity-90 whitespace-nowrap">
|
||||
{fmt(r.fecha_devolucion_real ?? r.fecha_devolucion_estimada)}
|
||||
@@ -278,13 +286,19 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
<span class="text-xs opacity-70">{fmt(r.fecha_solicitud)}</span>
|
||||
<span class="text-xs font-medium">{estadoLabel[r.estado] ?? r.estado}</span>
|
||||
</div>
|
||||
<div class="mt-1 font-medium">{r.material?.nombre ?? '—'}</div>
|
||||
<div class="mt-1 space-y-0.5">
|
||||
{(r.items ?? []).map((i: any) => (
|
||||
<div class="font-medium">
|
||||
<span style="font-variant-numeric: tabular-nums;">{i.cantidad}×</span> {i.material?.nombre ?? '—'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-sm opacity-80">
|
||||
{r.alumno?.nombre ?? r.alumno?.email ?? '—'}
|
||||
{r.alumno?.matricula && <span class="opacity-60"> · {r.alumno.matricula}</span>}
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between text-sm">
|
||||
<span style="font-variant-numeric: tabular-nums;">Cant. {r.cantidad}</span>
|
||||
<div class="text-xs opacity-70">Maestro: {r.maestro_responsable ?? '—'}</div>
|
||||
<div class="mt-2 flex items-center justify-end text-sm">
|
||||
<span class="opacity-80">
|
||||
Dev.: {fmt(r.fecha_devolucion_real ?? r.fecha_devolucion_estimada)}
|
||||
</span>
|
||||
@@ -313,7 +327,6 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
<tr>
|
||||
<th class="p-3 font-medium">Alumno</th>
|
||||
<th class="p-3 font-medium">Material</th>
|
||||
<th class="p-3 font-medium">Cant.</th>
|
||||
<th class="p-3 font-medium">Debía devolver el</th>
|
||||
<th class="p-3 font-medium">Días de atraso</th>
|
||||
</tr>
|
||||
@@ -331,12 +344,15 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
{r.alumno?.matricula && <div class="text-xs opacity-70">{r.alumno.matricula}</div>}
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<div>{r.material?.nombre ?? '—'}</div>
|
||||
{r.material?.numero_inventario && (
|
||||
<div class="text-xs opacity-70">{r.material.numero_inventario}</div>
|
||||
)}
|
||||
{(r.items ?? []).map((i: any) => (
|
||||
<div>
|
||||
<span style="font-variant-numeric: tabular-nums;">{i.cantidad}×</span> {i.material?.nombre ?? '—'}
|
||||
{i.material?.numero_inventario && (
|
||||
<span class="text-xs opacity-70"> ({i.material.numero_inventario})</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3 whitespace-nowrap">{fmt(r.fecha_devolucion_estimada)}</td>
|
||||
<td class="p-3 font-medium" style={`font-variant-numeric: tabular-nums; color: var(--color-danger-text);`}>
|
||||
{dias} {dias === 1 ? 'día' : 'días'}
|
||||
@@ -354,7 +370,13 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
const dias = diasAtraso(r.fecha_devolucion_estimada);
|
||||
return (
|
||||
<li class="card" style="background: color-mix(in oklab, var(--color-danger) 8%, transparent);">
|
||||
<div class="font-medium">{r.material?.nombre ?? '—'}</div>
|
||||
<div class="space-y-0.5">
|
||||
{(r.items ?? []).map((i: any) => (
|
||||
<div class="font-medium">
|
||||
<span style="font-variant-numeric: tabular-nums;">{i.cantidad}×</span> {i.material?.nombre ?? '—'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-sm opacity-80">
|
||||
{r.alumno?.nombre ?? r.alumno?.email ?? '—'}
|
||||
{r.alumno?.matricula && <span class="opacity-60"> · {r.alumno.matricula}</span>}
|
||||
|
||||
@@ -5,9 +5,9 @@ import VerDetalles from '@/components/admin/solicitudes/VerDetalles.tsx';
|
||||
|
||||
const supabase = Astro.locals.supabase;
|
||||
const { data, error } = await supabase
|
||||
.from('prestamos')
|
||||
.from('solicitudes')
|
||||
.select(
|
||||
'id, cantidad, fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, notas, alumno:profiles!alumno_id(id, nombre, email, matricula), material:materiales!material_id(id, nombre, numero_inventario, cantidad_disponible)'
|
||||
'id, fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, notas, maestro_responsable, alumno:profiles!alumno_id(id, nombre, email, matricula), items:solicitud_items(cantidad, descripcion, material:materiales(id, nombre, numero_inventario, cantidad_disponible))'
|
||||
)
|
||||
.in('estado', ['aprobado', 'activo'])
|
||||
.order('fecha_devolucion_estimada', { ascending: true });
|
||||
@@ -16,6 +16,11 @@ const filas = data ?? [];
|
||||
const fmtFecha = new Intl.DateTimeFormat('es-MX', { dateStyle: 'medium' });
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const isVencido = (f?: string | null) => !!f && f < today;
|
||||
const resumenMateriales = (items: any[]) => {
|
||||
const nombres = (items ?? []).map((i) => i.material?.nombre ?? '—');
|
||||
if (nombres.length <= 2) return nombres.join(', ') || '—';
|
||||
return `${nombres.slice(0, 2).join(', ')} +${nombres.length - 2} más`;
|
||||
};
|
||||
|
||||
const path = Astro.url.pathname;
|
||||
const tabs = [
|
||||
@@ -73,7 +78,6 @@ const tabs = [
|
||||
<tr>
|
||||
<th class="p-3 font-medium">Alumno</th>
|
||||
<th class="p-3 font-medium">Material</th>
|
||||
<th class="p-3 font-medium">Cant.</th>
|
||||
<th class="p-3 font-medium">Aprobado</th>
|
||||
<th class="p-3 font-medium">Devolver antes de</th>
|
||||
<th class="p-3 font-medium text-right">Acciones</th>
|
||||
@@ -90,17 +94,23 @@ const tabs = [
|
||||
<td class="p-3">
|
||||
<div class="font-medium">{r.alumno?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-xs opacity-70 mt-1">Maestro: {r.maestro_responsable}</div>
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<div>{r.material?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">Inv. {r.material?.numero_inventario ?? '—'}</div>
|
||||
<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 opacity-70">Inv. {i.material?.numero_inventario ?? '—'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{r.notas && (
|
||||
<div class="text-xs mt-1.5 opacity-80 border-l-2 pl-2" style="border-color: color-mix(in oklab, var(--color-primary) 40%, transparent);">
|
||||
<span class="opacity-60">Motivo:</span> {r.notas}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3 opacity-80">{r.fecha_aprobacion ? fmtFecha.format(new Date(r.fecha_aprobacion)) : '—'}</td>
|
||||
<td class="p-3">
|
||||
<span style="font-variant-numeric: tabular-nums;">
|
||||
@@ -118,7 +128,7 @@ const tabs = [
|
||||
<td class="p-3">
|
||||
<div class="flex justify-end gap-2 flex-wrap">
|
||||
<VerDetalles client:load prestamoId={r.id} />
|
||||
<MarcarDevuelto client:load prestamoId={r.id} nombreMaterial={r.material?.nombre ?? '—'} />
|
||||
<MarcarDevuelto client:load prestamoId={r.id} resumenMateriales={resumenMateriales(r.items)} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -139,9 +149,16 @@ const tabs = [
|
||||
<span class="inline-flex rounded-[2px] px-2 py-0.5 text-xs font-semibold" style="background: var(--color-danger); color: var(--color-ink); border: 1.5px solid var(--color-ink);">Vencido</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="text-xs opacity-70 mb-2">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-sm">{r.material?.nombre ?? '—'} <span class="opacity-70">· Inv. {r.material?.numero_inventario ?? '—'}</span></div>
|
||||
<div class="text-sm mt-1">Cantidad: <strong style="font-variant-numeric: tabular-nums;">{r.cantidad}</strong></div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-xs opacity-70 mb-2">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="opacity-70"> · Inv. {i.material?.numero_inventario ?? '—'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-sm mt-1 opacity-80">Devolver antes de <span style="font-variant-numeric: tabular-nums;">{r.fecha_devolucion_estimada ? fmtFecha.format(new Date(r.fecha_devolucion_estimada + 'T00:00:00')) : '—'}</span></div>
|
||||
{r.notas && (
|
||||
<p class="text-sm mt-2 opacity-80 border-l-2 pl-2" style="border-color: color-mix(in oklab, var(--color-primary) 40%, transparent);">
|
||||
@@ -150,7 +167,7 @@ const tabs = [
|
||||
)}
|
||||
<div class="mt-3 flex gap-2 flex-wrap">
|
||||
<VerDetalles client:load prestamoId={r.id} />
|
||||
<MarcarDevuelto client:load prestamoId={r.id} nombreMaterial={r.material?.nombre ?? '—'} />
|
||||
<MarcarDevuelto client:load prestamoId={r.id} resumenMateriales={resumenMateriales(r.items)} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -11,9 +11,9 @@ const estados = estadoFiltro ? [estadoFiltro] : Array.from(ALL);
|
||||
|
||||
// ponytail: limit 200, paginar cuando pase de 500 filas
|
||||
const { data, error } = await supabase
|
||||
.from('prestamos')
|
||||
.from('solicitudes')
|
||||
.select(
|
||||
'id, cantidad, fecha_solicitud, fecha_devolucion_real, estado, notas, alumno:profiles!alumno_id(id, nombre, email, matricula), material:materiales!material_id(id, nombre, numero_inventario)'
|
||||
'id, fecha_solicitud, fecha_devolucion_real, estado, notas, maestro_responsable, alumno:profiles!alumno_id(id, nombre, email, matricula), items:solicitud_items(cantidad, descripcion, material:materiales(id, nombre, numero_inventario))'
|
||||
)
|
||||
.in('estado', estados)
|
||||
.order('fecha_solicitud', { ascending: false })
|
||||
@@ -104,7 +104,6 @@ const estadoLabel: Record<string, string> = {
|
||||
<tr>
|
||||
<th class="p-3 font-medium">Alumno</th>
|
||||
<th class="p-3 font-medium">Material</th>
|
||||
<th class="p-3 font-medium">Cant.</th>
|
||||
<th class="p-3 font-medium">Estado</th>
|
||||
<th class="p-3 font-medium">Solicitado</th>
|
||||
<th class="p-3 font-medium">Devuelto</th>
|
||||
@@ -116,12 +115,18 @@ const estadoLabel: Record<string, string> = {
|
||||
<td class="p-3">
|
||||
<div class="font-medium">{r.alumno?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-xs opacity-70 mt-1">Maestro: {r.maestro_responsable}</div>
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<div>{r.material?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">Inv. {r.material?.numero_inventario ?? '—'}</div>
|
||||
<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 opacity-70">Inv. {i.material?.numero_inventario ?? '—'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3">{estadoLabel[r.estado] ?? r.estado}</td>
|
||||
<td class="p-3 opacity-80">{fmtFecha.format(new Date(r.fecha_solicitud))}</td>
|
||||
<td class="p-3 opacity-80">{r.fecha_devolucion_real ? fmtFecha.format(new Date(r.fecha_devolucion_real)) : '—'}</td>
|
||||
@@ -139,7 +144,12 @@ const estadoLabel: Record<string, string> = {
|
||||
<span class="text-xs rounded-[2px] px-2 py-0.5" style="background: color-mix(in oklab, var(--color-ink) 10%, transparent);">{estadoLabel[r.estado] ?? r.estado}</span>
|
||||
</div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-sm mt-2">{r.material?.nombre ?? '—'} · Cantidad <strong style="font-variant-numeric: tabular-nums;">{r.cantidad}</strong></div>
|
||||
<div class="text-xs opacity-70">Maestro: {r.maestro_responsable}</div>
|
||||
<div class="flex flex-col gap-1 mt-2">
|
||||
{(r.items ?? []).map((i: any) => (
|
||||
<div class="text-sm">{i.cantidad}× {i.material?.nombre ?? '—'}</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-xs opacity-70 mt-2">Solicitado {fmtFecha.format(new Date(r.fecha_solicitud))}</div>
|
||||
{r.fecha_devolucion_real && <div class="text-xs opacity-70">Devuelto {fmtFecha.format(new Date(r.fecha_devolucion_real))}</div>}
|
||||
</li>
|
||||
|
||||
@@ -4,9 +4,9 @@ import AccionesSolicitud from '@/components/admin/solicitudes/AccionesSolicitud.
|
||||
|
||||
const supabase = Astro.locals.supabase;
|
||||
const { data, error } = await supabase
|
||||
.from('prestamos')
|
||||
.from('solicitudes')
|
||||
.select(
|
||||
'id, cantidad, fecha_solicitud, notas, alumno:profiles!alumno_id(id, nombre, email, matricula), material:materiales!material_id(id, nombre, numero_inventario, cantidad_disponible)'
|
||||
'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 });
|
||||
@@ -27,7 +27,7 @@ const tabs = [
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">Solicitudes pendientes</h1>
|
||||
<span
|
||||
class="inline-flex items-center rounded-[2px] px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: var(--color-primary); color: var(--color-ink); border: 1.5px solid var(--color-ink); font-variant-numeric: tabular-nums;"
|
||||
style="background: var(--color-primary); color: white; border: 1.5px solid var(--color-ink); font-variant-numeric: tabular-nums;"
|
||||
aria-label={`${filas.length} pendientes`}
|
||||
>
|
||||
{filas.length}
|
||||
@@ -74,7 +74,6 @@ const tabs = [
|
||||
<tr>
|
||||
<th class="p-3 font-medium">Alumno</th>
|
||||
<th class="p-3 font-medium">Material</th>
|
||||
<th class="p-3 font-medium">Cantidad</th>
|
||||
<th class="p-3 font-medium">Solicitado</th>
|
||||
<th class="p-3 font-medium text-right">Acciones</th>
|
||||
</tr>
|
||||
@@ -85,11 +84,18 @@ const tabs = [
|
||||
<td class="p-3">
|
||||
<div class="font-medium">{r.alumno?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-xs opacity-70 mt-1">Maestro: {r.maestro_responsable}</div>
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<div>{r.material?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">
|
||||
Inv. {r.material?.numero_inventario ?? '—'} · disp. <span style="font-variant-numeric: tabular-nums;">{r.material?.cantidad_disponible ?? 0}</span>
|
||||
<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 opacity-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 opacity-80 border-l-2 pl-2" style="border-color: color-mix(in oklab, var(--color-primary) 40%, transparent);">
|
||||
@@ -97,7 +103,6 @@ const tabs = [
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3 opacity-80">{fmtFecha.format(new Date(r.fecha_solicitud))}</td>
|
||||
<td class="p-3">
|
||||
<div class="flex justify-end">
|
||||
@@ -118,9 +123,16 @@ const tabs = [
|
||||
<div class="font-medium">{r.alumno?.nombre ?? '—'}</div>
|
||||
<div class="text-xs opacity-70">{fmtFecha.format(new Date(r.fecha_solicitud))}</div>
|
||||
</div>
|
||||
<div class="text-xs opacity-70 mb-2">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-sm">{r.material?.nombre ?? '—'} <span class="opacity-70">· Inv. {r.material?.numero_inventario ?? '—'}</span></div>
|
||||
<div class="text-sm mt-1">Cantidad: <strong style="font-variant-numeric: tabular-nums;">{r.cantidad}</strong> <span class="opacity-70">· disp. {r.material?.cantidad_disponible ?? 0}</span></div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-xs opacity-70 mb-2">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="opacity-70"> · Inv. {i.material?.numero_inventario ?? '—'} · disp. {i.material?.cantidad_disponible ?? 0}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{r.notas && (
|
||||
<p class="text-sm mt-2 opacity-80 border-l-2 pl-2" style="border-color: color-mix(in oklab, var(--color-primary) 40%, transparent);">
|
||||
<span class="opacity-60">Motivo:</span> {r.notas}
|
||||
|
||||
Reference in New Issue
Block a user