Files
Lizandro GuarnizoandClaude Opus 5 018fb13332 Módulo Soporte: visor de documentación con renderizado Markdown y buscador
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>
2026-08-04 10:07:06 -05:00

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 tocar role_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.