const { useState, useMemo, useCallback, useEffect, useRef } = React; // ────────────────────────────────────────────────────────────────── // helpers // ────────────────────────────────────────────────────────────────── const Icon = ({name, size=20}) => { const C = window[name]; if (!C) return null; return ; }; const flatById = (() => { const map = {}; for (const m of MODULES) { map[m.id] = { kind: "module", node: m }; for (const g of allGroups(m)) { map[g.id] = { kind: "group", node: g, module: m }; for (const i of g.items) { map[i.id] = { kind: "item", node: i, group: g, module: m }; } } } return map; })(); // ────────────────────────────────────────────────────────────────── // MENÚ POR SISTEMA — la fuente de verdad son las tablas // sc.sistema una caja en la portada por fila // sc.usr_sistema_menu_titulo los títulos del menú lateral (cuelgan de un sistema) // sc.usr_sistema_menu_item los ítems de cada título // // `MODULES` (data.jsx) ya NO decide el menú: queda como REGISTRO DE VENTANAS — // aporta el ícono y, sobre todo, la clave de ruteo con la que el
elige el // componente. El puente es `usr_sistema_menu_item.id_modulo`, que vale exactamente // ese id. Un ítem sin `id_modulo` es un ítem cuya ventana no existe todavía: se // dibuja deshabilitado, no se esconde. // // Se lee de localStorage y no de /api/*: el preloader de index.html ya hidrató // LS con un XHR SÍNCRONO antes de que Babel evaluara este archivo, así que los // datos están disponibles en el primer render (mismo contrato que usan los CRUD). // ────────────────────────────────────────────────────────────────── const readTable = (key) => { try { const raw = localStorage.getItem(key); const arr = raw ? JSON.parse(raw) : []; return Array.isArray(arr) ? arr : []; } catch (e) { return []; } }; const esActivo = (r) => r && r.activo !== false; // Los ids son secuenciales y con relleno de ceros (SMT01…, SMI001…), así que // ordenar por id reproduce el orden con el que se cargó el árbol. Sigue siendo el // criterio de los TÍTULOS; los ítems ya tienen su propia columna `orden` // (35_rol_menu.sql) y el id sólo los desempata. const porId = (a, b) => String(a.id).localeCompare(String(b.id)); // Cada aplicación (directorio de un sistema) declara su id en su index.html: // // Con eso el router entra directo a ese sistema y no dibuja la portada. Sin eso // —la portada de la raíz— se comporta igual que siempre. const SISTEMA_FIJO = (typeof window !== "undefined" && window.__SC_SISTEMA) || null; // Ruta de cada sistema. Ya no es un directorio: cada sistema es su propio servicio // de Cloud Run, y el balanceador los junta bajo un solo dominio por prefijo // (/comercial/, /agencia/…). Los nombres son los del servicio y del repo, así que // la ruta que se ve en la barra dice exactamente qué se está ejecutando. // // El dominio es UNO SOLO a propósito: la sesión y el catálogo viven en // localStorage, que es por origen. Un dominio por módulo sería una sesión por // módulo, y eso no se puede relajar. // // Elegir un sistema en la portada tiene que NAVEGAR a su aplicación, no cambiar el // menú en la misma página: cada index.html carga SÓLO los