// =====================================================================
// Shell del portal: sesión, menú según permisos y ruteo por hash.
// El menú se pinta con lo que el servidor autoriza, no con una lista
// escrita a mano: así no puede quedar desfasado de la API.
// =====================================================================
import { api, esc, icono, ficha, escalonar, activarCopiado, cargando } from './ui.mjs';
import * as V from './vistas.mjs';
const VISTAS = {
inicio: V.inicio,
oferta: V.oferta,
requisitos: V.requisitos,
becas: V.becas,
clases: V.clases,
preguntas: V.preguntas,
mensajes: V.mensajes,
contacto: V.contacto,
equipo: V.equipo,
bitacora: V.bitacora,
};
const $ = (s) => document.querySelector(s);
// El contenedor de la vista se reemplaza por uno nuevo en cada render.
// Si se reusara el mismo nodo, cada montar() volvería a registrar sus
// listeners sobre él y al recargar una sección se acumularían: un clic en
// "Editar" abriría tantas ventanas como veces se hubiera recargado.
let vista = $('#vista');
function lienzoNuevo() {
const el = document.createElement('div');
el.className = 'lienzo__interno';
el.id = 'vista';
el.setAttribute('aria-live', 'polite');
vista.replaceWith(el);
vista = el;
return el;
}
// Qué bloques plegables dejó abiertos la persona. Sin esto, al guardar un
// cambio la sección se repinta y el bloque en el que estaba trabajando se
// vuelve a plegar, así que pierde su lugar.
const plegablesAbiertos = new Map();
function recordarPlegables(caja) {
caja.querySelectorAll('details.plegable[data-plegable]').forEach((d) => {
const guardado = plegablesAbiertos.get(d.dataset.plegable);
if (guardado !== undefined) d.open = guardado;
});
// `toggle` no burbujea, pero un listener en captura sí lo recibe.
caja.addEventListener('toggle', (ev) => {
const d = ev.target.closest?.('details.plegable[data-plegable]');
if (d) plegablesAbiertos.set(d.dataset.plegable, d.open);
}, true);
}
// ---- sesión ----------------------------------------------------------
let ctx;
try {
const yo = await api('/auth/yo');
ctx = {
usuario: yo.usuario,
permisos: new Set(yo.permisos),
navegacion: yo.navegacion,
grupos: yo.grupos,
recargar: () => pintar(actual, { forzar: true }),
};
} catch {
location.href = '/';
throw new Error('sin sesión');
}
// ---- menú ------------------------------------------------------------
function botonNav(s) {
return ``;
}
function pintarMenu() {
const sueltas = ctx.navegacion.filter((s) => !s.grupo);
const bloques = ctx.grupos
.map((g) => ({ ...g, items: ctx.navegacion.filter((s) => s.grupo === g.id) }))
.filter((g) => g.items.length);
$('#nav').innerHTML = [
sueltas.map(botonNav).join(''),
...bloques.map((g) => `
`;
$('#ficha-movil').innerHTML = ficha(ctx.usuario.iniciales, ctx.usuario.rol_color);
$('#salir').addEventListener('click', async () => {
await api('/auth/salir', { method: 'POST' }).catch(() => {});
location.href = '/';
});
}
// ---- ruteo -----------------------------------------------------------
let actual = null;
// Cada render lleva su número. Si la persona cambia de sección mientras una
// carga, el render viejo se descarta al llegar en lugar de bloquear el menú:
// antes, con una bandera de "ocupado", el clic se ignoraba y el menú parecía
// muerto hasta ir a otra sección y volver.
let renderActual = 0;
function seccionValida(id) {
return ctx.navegacion.some((s) => s.id === id) && VISTAS[id];
}
async function pintar(id, { forzar = false } = {}) {
if (!seccionValida(id)) id = ctx.navegacion[0]?.id ?? 'inicio';
if (id === actual && !forzar) return;
const miRender = ++renderActual;
actual = id;
document.querySelectorAll('.nav-item[data-seccion]').forEach((b) => {
if (b.dataset.seccion === id) b.setAttribute('aria-current', 'page');
else b.removeAttribute('aria-current');
});
cerrarCajon();
lienzoNuevo().innerHTML = cargando(3, 120);
try {
const v = await VISTAS[id](ctx);
if (miRender !== renderActual) return; // llegó tarde: ya se pidió otra sección
const caja = lienzoNuevo();
caja.innerHTML = `
${esc(v.ceja ?? '')}