0f2ea59754
Rol docente:
- profiles.rol acepta 'alumno'|'docente'|'admin' (migración 0006)
- Docente en checkout: no elige maestro; RPC usa su propio nombre como maestro_responsable
- Perfil/onboarding condicionales por rol: docente sin semestre ni tutor, label matrícula → "Número de empleado"
- catalogo.astro: perfilCompleto para docente solo requiere matrícula
Combobox maestros + separar tutores:
- maestros.es_tutor bool: subset marcado que aparece en el select de tutor del perfil
- MaestroForm: nuevo checkbox "Es tutor"; endpoints POST/PATCH aceptan es_tutor
- admin/maestros.astro: columna Tutor con SVG check
- perfil/onboarding queries filtran es_tutor=true, activo=true
- Checkout SolicitudCart: <select> combobox de maestros activos (reemplaza textarea);
option vacía usa el tutor por defecto; endpoint recibe maestro_id numeric
Fecha default 1 día:
- AccionesSolicitud: enDias(7) → enDias(1) por default al aprobar
Sonido notificación:
- Movido src/audio/sonido_notificacion.mp3 → public/audio/
- BadgeSolicitudes: new Audio('/audio/sonido_notificacion.mp3').play() en INSERT
Issues:
- Límite motivo 250 chars (era 500) en checkout + backend
- break-words en Maestro/Motivo de las 3 vistas admin y VerDetalles
119 lines
4.3 KiB
Plaintext
119 lines
4.3 KiB
Plaintext
---
|
|
import AppLayout from '@/layouts/AppLayout.astro';
|
|
import CartProvider from '@/components/alumno/SolicitudCart.tsx';
|
|
import FiltroCategorias from '@/components/alumno/FiltroCategorias.tsx';
|
|
import BuscadorCatalogo from '@/components/alumno/BuscadorCatalogo.tsx';
|
|
|
|
const qInitial = Astro.url.searchParams.get('q') ?? '';
|
|
|
|
const supabase = Astro.locals.supabase;
|
|
const userId = Astro.locals.user?.id;
|
|
const rol = Astro.locals.profile?.rol ?? 'alumno';
|
|
const esDocente = rol === 'docente';
|
|
|
|
// Trae matricula + tutor_id directo (el middleware sólo hidrata columnas antiguas).
|
|
let matricula: string | null = null;
|
|
let tutorId: number | null = null;
|
|
if (userId) {
|
|
const { data: p } = await supabase
|
|
.from('profiles')
|
|
.select('matricula, tutor_id')
|
|
.eq('id', userId)
|
|
.maybeSingle();
|
|
matricula = p?.matricula ?? null;
|
|
tutorId = (p as { tutor_id?: number | null } | null)?.tutor_id ?? null;
|
|
}
|
|
|
|
let tutorNombre: string | null = null;
|
|
if (tutorId) {
|
|
const { data } = await supabase.from('maestros').select('nombre').eq('id', tutorId).maybeSingle();
|
|
tutorNombre = data?.nombre ?? null;
|
|
}
|
|
|
|
// Docente: solo requiere matrícula (número de empleado). Alumno: matrícula + tutor.
|
|
const perfilCompleto = esDocente ? !!matricula : !!(matricula && tutorId);
|
|
|
|
// Lista completa de maestros activos para el combobox del checkout (solo alumno lo usa).
|
|
type MaestroOpt = { id: number; nombre: string };
|
|
let maestros: MaestroOpt[] = [];
|
|
if (!esDocente) {
|
|
const { data: ms } = await supabase
|
|
.from('maestros')
|
|
.select('id, nombre')
|
|
.eq('activo', true)
|
|
.order('nombre');
|
|
maestros = (ms ?? []) as MaestroOpt[];
|
|
}
|
|
|
|
type Material = {
|
|
id: number;
|
|
nombre: string;
|
|
descripcion: string | null;
|
|
cantidad_disponible: number;
|
|
cantidad_total: number;
|
|
numero_inventario: string | null;
|
|
imagen_path: string | null;
|
|
categoria: { id: number; nombre: string } | null;
|
|
};
|
|
|
|
const { data, error } = await Astro.locals.supabase
|
|
.from('materiales')
|
|
.select('id, nombre, descripcion, cantidad_disponible, cantidad_total, numero_inventario, imagen_path, categoria:categorias(id, nombre)')
|
|
.eq('estado', 'disponible')
|
|
.order('nombre');
|
|
|
|
const materiales = (data ?? []) as unknown as Material[];
|
|
|
|
const grupos = new Map<string, { id: number | null; nombre: string; items: Material[] }>();
|
|
for (const m of materiales) {
|
|
const key = m.categoria ? String(m.categoria.id) : 'sin';
|
|
const nombre = m.categoria?.nombre ?? 'Sin categoría';
|
|
if (!grupos.has(key)) grupos.set(key, { id: m.categoria?.id ?? null, nombre, items: [] });
|
|
grupos.get(key)!.items.push(m);
|
|
}
|
|
const grupoList = Array.from(grupos.values()).sort((a, b) => a.nombre.localeCompare(b.nombre, 'es'));
|
|
const categoriasFiltro = grupoList
|
|
.filter((g) => g.id !== null)
|
|
.map((g) => ({ id: g.id as number, nombre: g.nombre }));
|
|
---
|
|
<AppLayout title="Catálogo — LabPréstamos">
|
|
<div class="max-w-6xl">
|
|
<header class="mb-6">
|
|
<h1 class="text-2xl md:text-3xl font-semibold">Catálogo</h1>
|
|
<p class="text-sm opacity-70 mt-1">Material disponible en el Laboratorio de Sistemas.</p>
|
|
</header>
|
|
|
|
{error && (
|
|
<div class="card mb-6" role="alert" style="border-color: color-mix(in oklab, var(--color-danger) 30%, transparent);">
|
|
<p class="text-sm" style="color: var(--color-danger-text);">
|
|
No se pudo cargar el catálogo. Recarga la página o intenta más tarde.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{materiales.length === 0 && !error && (
|
|
<div class="card text-center">
|
|
<h2 class="text-lg font-semibold mb-1">Aún no hay material publicado</h2>
|
|
<p class="text-sm opacity-70">Vuelve pronto o contacta al encargado del laboratorio.</p>
|
|
</div>
|
|
)}
|
|
|
|
{materiales.length > 0 && (
|
|
<>
|
|
<div class="mb-6 flex flex-col gap-3">
|
|
<BuscadorCatalogo q={qInitial} client:load />
|
|
{categoriasFiltro.length > 0 && (
|
|
<FiltroCategorias categorias={categoriasFiltro} client:idle />
|
|
)}
|
|
</div>
|
|
|
|
<p id="catalogo-empty-filter" hidden class="text-sm opacity-70 mb-4">
|
|
Ningún material en esta categoría. Prueba con otra.
|
|
</p>
|
|
|
|
<CartProvider materiales={materiales} tutorNombre={tutorNombre} perfilCompleto={perfilCompleto} esDocente={esDocente} maestros={maestros} client:load />
|
|
</>
|
|
)}
|
|
</div>
|
|
</AppLayout>
|