Files
whatsapp/core/Router.php
T
Lizandro GuarnizoandClaude Opus 5 6589503c57 Tablet de firma para el paciente en Recepción 4
Hasta ahora, para que el paciente firmara el consentimiento de bienvenida
había que girarle el monitor a la recepcionista o pasarle el mouse: el botón
Firmar abre el formulario en la pantalla de ella.

Se agrega una pantalla aparte (/erp.php?m=turnero&v=firma) para poner frente
al paciente. Sola, sin que la recepcionista haga nada: cuando el turno entra
a ese puesto y le falta el F-LAB-01, aparece su nombre y un botón grande de
firmar; al terminar dice gracias y vuelve a reposo.

Reutiliza lo que ya existía: el formulario de ver_formulario_enviado.php, el
aviso 'turneroFirmado' que ya emite al firmar, y el sondeo de recepción, que
seguirá poniendo el renglón en verde sin cambios.

Sobre el acceso: va en PUBLIC_ROUTES porque nadie va a iniciar sesión cada
mañana en una tablet que manipula el público. No queda abierta: se identifica
por la cookie del dispositivo, y sin ella no muestra ningún dato. El endpoint
devuelve solo el turno que está en ese puesto en ese instante —no permite
buscar, ni ver otros, ni consultar historial— y descarta turnos de días
anteriores, igual que el televisor. La tablet debe quedar en modo kiosco.

Falta registrar el dispositivo de Recepción 4 desde Configuración; hoy no
tiene ninguno.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:35:20 -05:00

150 lines
4.9 KiB
PHP

<?php
/**
* core/Router.php
* Enrutador central del ERP.
*
* Mapea la URL entrante a modules/{m}/views/{v}.php.
* Soporta rutas limpias (/turnero/dashboard) y parámetros explícitos (?m=&v=).
*
* RUTAS PÚBLICAS (sin autenticación):
* turnero/display → pantalla TV para sala de espera
* turnero/kiosko → pantalla táctil para tomar turno
*/
class Router
{
/** Módulo por defecto cuando no se especifica ?m= */
private const DEFAULT_MODULE = 'dashboard';
/** Vista por defecto cuando no se especifica ?v= */
private const DEFAULT_VIEW = 'index';
/** Rutas que NO requieren sesión iniciada. Formato: 'modulo/vista' */
private const PUBLIC_ROUTES = [
'turnero/display',
'turnero/kiosko',
// Tablet de firma del paciente: la manipula el público y nadie va a
// iniciar sesión en ella cada mañana. No queda abierta: se identifica
// por la cookie del dispositivo y sin ella no muestra dato alguno.
'turnero/firma',
];
/** Patrón permitido para módulo y vista: solo letras, números y guión bajo */
private const SLUG_PATTERN = '/^[a-zA-Z0-9_]{1,64}$/';
private string $module;
private string $view;
// ─── Constructor ────────────────────────────────────────────────────────
public function __construct()
{
[$this->module, $this->view] = $this->resolveRoute();
}
// ─── API pública ────────────────────────────────────────────────────────
public function getModule(): string { return $this->module; }
public function getView(): string { return $this->view; }
/**
* Indica si la ruta actual es pública (no requiere login).
*/
public function isPublic(): bool
{
return in_array($this->module . '/' . $this->view, self::PUBLIC_ROUTES, true);
}
/**
* Resuelve la ruta y devuelve el archivo de vista correspondiente.
* Lanza una excepción si el archivo no existe.
*
* @return string Ruta absoluta al archivo de vista.
* @throws RuntimeException Si el módulo/vista no se encuentran.
*/
public function resolveFile(): string
{
$file = APP_ROOT . "/modules/{$this->module}/views/{$this->view}.php";
if (!file_exists($file)) {
throw new RuntimeException(
"Vista no encontrada: {$this->module}/{$this->view}",
404
);
}
return $file;
}
// ─── Internos ───────────────────────────────────────────────────────────
/**
* Determina módulo y vista a partir de la URL.
* Prioridad: PATH_INFO limpio → parámetros GET ?m= ?v=
*
* @return array{string, string}
*/
private function resolveRoute(): array
{
// 1. Intentar ruta limpia desde PATH_INFO o REQUEST_URI
// Formato: /modulo/vista → e.g. /turnero/dashboard
$path = $this->cleanPath();
if ($path !== null) {
[$m, $v] = $path;
if ($this->isValidSlug($m) && $this->isValidSlug($v)) {
return [$m, $v];
}
}
// 2. Parámetros GET ?m=module&v=view
$m = $_GET['m'] ?? self::DEFAULT_MODULE;
$v = $_GET['v'] ?? self::DEFAULT_VIEW;
// Sanitizar: solo caracteres seguros
$m = preg_replace('/[^a-zA-Z0-9_]/', '', $m);
$v = preg_replace('/[^a-zA-Z0-9_]/', '', $v);
if (!$this->isValidSlug($m)) $m = self::DEFAULT_MODULE;
if (!$this->isValidSlug($v)) $v = self::DEFAULT_VIEW;
return [$m, $v];
}
/**
* Extrae módulo y vista de la REQUEST_URI limpia.
* Retorna null si la URI no corresponde al formato /m/v.
*
* @return array{string,string}|null
*/
private function cleanPath(): ?array
{
$uri = $_SERVER['REQUEST_URI'] ?? '';
// Eliminar query string
$pos = strpos($uri, '?');
if ($pos !== false) {
$uri = substr($uri, 0, $pos);
}
// Eliminar el subdirectorio base si el proyecto no está en la raíz
$scriptDir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/');
if ($scriptDir !== '' && str_starts_with($uri, $scriptDir)) {
$uri = substr($uri, strlen($scriptDir));
}
// Normalizar y dividir
$parts = array_values(array_filter(explode('/', trim($uri, '/'))));
if (count($parts) >= 2) {
return [$parts[0], $parts[1]];
}
return null;
}
private function isValidSlug(string $slug): bool
{
return (bool) preg_match(self::SLUG_PATTERN, $slug);
}
}