v1.6: rol docente, combobox maestros, sonido notif, fecha 1 día, wrap admin

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
This commit is contained in:
2026-08-25 11:37:26 -07:00
parent 44fe7527be
commit 0f2ea59754
18 changed files with 180 additions and 94 deletions
+16 -4
View File
@@ -7,18 +7,20 @@ type MaestroRow = {
id: number;
nombre: string;
activo: boolean;
es_tutor: boolean;
alumnos: { count: number }[];
};
const { data, error } = await Astro.locals.supabase
.from('maestros')
.select('id, nombre, activo, alumnos:profiles!tutor_id(count)')
.select('id, nombre, activo, es_tutor, alumnos:profiles!tutor_id(count)')
.order('nombre');
const maestros = ((data ?? []) as unknown as MaestroRow[]).map((m) => ({
id: m.id,
nombre: m.nombre,
activo: m.activo,
es_tutor: m.es_tutor,
count: m.alumnos?.[0]?.count ?? 0,
}));
---
@@ -67,6 +69,7 @@ const maestros = ((data ?? []) as unknown as MaestroRow[]).map((m) => ({
<tr>
<th class="px-4 py-3 font-medium">Nombre</th>
<th class="px-4 py-3 font-medium">Estado</th>
<th class="px-4 py-3 font-medium text-center">Tutor</th>
<th class="px-4 py-3 font-medium text-right"># Alumnos</th>
<th class="px-4 py-3 font-medium text-right">Acciones</th>
</tr>
@@ -82,12 +85,21 @@ const maestros = ((data ?? []) as unknown as MaestroRow[]).map((m) => ({
<span class="text-xs uppercase tracking-wide opacity-60">Inactivo</span>
)}
</td>
<td class="px-4 py-3 text-center">
{m.es_tutor ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-label="Sí" role="img" style="display:inline-block; color: var(--color-primary);">
<polyline points="20 6 9 17 4 12" />
</svg>
) : (
<span class="opacity-40" aria-label="No">—</span>
)}
</td>
<td class="px-4 py-3 text-right" style="font-variant-numeric: tabular-nums;">
{m.count > 0 ? m.count : <span class="opacity-60">0</span>}
</td>
<td class="px-4 py-3">
<div class="flex gap-2 justify-end">
<MaestroForm mode="edit" maestro={{ id: m.id, nombre: m.nombre, activo: m.activo }} client:load />
<MaestroForm mode="edit" maestro={{ id: m.id, nombre: m.nombre, activo: m.activo, es_tutor: m.es_tutor }} client:load />
<EliminarMaestro id={m.id} nombre={m.nombre} alumnosCount={m.count} client:load />
</div>
</td>
@@ -103,11 +115,11 @@ const maestros = ((data ?? []) as unknown as MaestroRow[]).map((m) => ({
<div>
<h3 class="font-semibold">{m.nombre}</h3>
<p class="text-xs opacity-70 mt-1">
{m.activo ? 'Activo' : 'Inactivo'} · {m.count} alumno{m.count === 1 ? '' : 's'}
{m.activo ? 'Activo' : 'Inactivo'} · {m.es_tutor ? 'Tutor' : 'No tutor'} · {m.count} alumno{m.count === 1 ? '' : 's'}
</p>
</div>
<div class="flex flex-col gap-2 shrink-0">
<MaestroForm mode="edit" maestro={{ id: m.id, nombre: m.nombre, activo: m.activo }} client:load />
<MaestroForm mode="edit" maestro={{ id: m.id, nombre: m.nombre, activo: m.activo, es_tutor: m.es_tutor }} client:load />
<EliminarMaestro id={m.id} nombre={m.nombre} alumnosCount={m.count} client:load />
</div>
</article>