# 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
<?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`)

```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:

```sql
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í:

```php
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.
