Nuevo módulo `soporte` con la documentación del proyecto en cuatro secciones:
manual de usuario (visible para todos), y documentación técnica, arquitectura
y operación (solo administradores).
- Markdown.php: renderizador propio del subconjunto que usa la documentación
(encabezados, listas anidadas, tablas, código, citas). Escapa todo el texto
antes de aplicar formato, así que los .md no pueden inyectar HTML. Se
prefirió un archivo auditable a incorporar una dependencia externa.
- DocIndex.php: descubre los .md, arma el árbol, resuelve acceso por sección
y construye el índice del buscador.
- Generadores.php: expande marcadores {{modulos}}, {{endpoints}}, {{tablas}},
{{roles}} y {{servicios}} leyendo el código y la base en cada carga, para
que los inventarios no puedan quedar desactualizados.
Se registra en SYSTEM_MODULES y se concede a los 12 roles con permission=read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.5 KiB
Enrutamiento y módulos
Cómo una URL termina ejecutando una pantalla concreta, y qué hace falta para agregar un módulo nuevo.
El recorrido de una petición
GET /erp.php?m=turnero&v=historial
│
├── erp.php define APP_ROOT y llama App::run()
│
├── App::boot() carga config, abre sesión, fija zona horaria
│
├── Router decide módulo y vista
│ ├── 1º intenta la ruta limpia: /turnero/historial
│ └── 2º cae a los parámetros: ?m=turnero&v=historial
│
├── Router::resolveFile()
│ └── modules/turnero/views/historial.php ¿existe? si no → 404
│
├── Rbac::hasModule('turnero') ¿tiene acceso? si no → 403
│
└── include del archivo de la vista
El punto clave: la ruta es literalmente la ubicación del archivo. ?m=turnero&v=historial carga modules/turnero/views/historial.php. No hay tabla de rutas ni configuración intermedia.
Validación de la URL
Router acepta como módulo y vista solo [a-zA-Z0-9_], máximo 64 caracteres. Cualquier cosa fuera de ese patrón se descarta silenciosamente y se reemplaza por el valor por defecto (dashboard / index). Eso cierra la puerta a recorrer directorios con ../.
Rutas públicas
Casi todo exige sesión. Las excepciones están fijas en core/Router.php:
| Ruta | Por qué es pública |
|---|---|
turnero/display |
Pantalla de TV en sala de espera; no hay quién inicie sesión |
turnero/kiosko |
El paciente saca su turno solo |
Cualquier otra combinación pasa por el control de acceso.
Ojo:
isPublic()solo omite la verificación de módulo. La sesión se maneja aparte, dentro de cada vista.
Registrar un módulo nuevo
Hacen falta tres cosas. Si falta alguna, el módulo no aparece o da 403.
1. La carpeta y al menos una vista
modules/mimodulo/
module.php
views/index.php
api/ (opcional)
2. El descriptor module.php — devuelve un arreglo:
<?php return [
'slug' => 'mimodulo',
'name' => 'Mi Módulo',
'icon' => 'fas fa-cube',
'category' => 'lab',
'route' => '/erp.php?m=mimodulo&v=index',
'is_active' => true,
'sort_order' => 50,
'description' => 'Para qué sirve',
'links' => [
['name' => 'Inicio', 'icon' => 'fas fa-home', 'route' => '/erp.php?m=mimodulo&v=index'],
],
];
links son las entradas que salen en el menú lateral. El descriptor se ejecuta como PHP, así que puede armar los enlaces según el rol de quien mira — el turnero lo hace: muestra escritorios distintos a recepcionistas y bacteriólogos.
3. El registro en SYSTEM_MODULES (config/config.php)
define('SYSTEM_MODULES', [
...
'mimodulo' => 'Mi Módulo',
]);
Estar acá es lo que activa la verificación de permisos. Un módulo ausente de esta lista no se valida y queda accesible para cualquier sesión.
4. Dar acceso a los roles — sin esto, todos reciben 403:
INSERT INTO role_modules (role_id, module_slug, permission, can_view)
SELECT id, 'mimodulo', 'write', 1 FROM roles WHERE slug IN ('admin','superadmin');
Los módulos de la sesión se cargan al iniciar sesión, desde
role_id. Después de tocarrole_modules, el usuario afectado tiene que volver a entrar para que el cambio surta efecto.
Módulos actuales
{{modulos}}
Vistas y layout
Una vista se escribe así:
require_once APP_ROOT . '/config/config.php';
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
Layout::open('Título de la pantalla', 'fas fa-icono');
// HTML, CSS y JS de la pantalla
Layout::close();
Layout::open() emite el <head>, la barra superior y el menú lateral — que construye leyendo los module.php de los módulos a los que el usuario tiene acceso. Layout::close() cierra el documento.
Endpoints
Cada módulo puede tener su carpeta api/. Son archivos PHP sueltos que devuelven JSON y se consumen por fetch desde las vistas. No pasan por Router: se invocan por su ruta real (modules/turnero/api/get_historial.php).
Por convención, api/_helpers.php de cada módulo concentra lo común — conexión, lectura del cuerpo JSON, respuestas jsonOk() / jsonError() y la verificación de acceso.
Los archivos que empiezan con guión bajo son de uso interno y no se llaman directamente desde el navegador.