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>
150 lines
4.9 KiB
PHP
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);
|
|
}
|
|
}
|