isPublic()) { Auth::requireLogin(); // Enfermeros van a su portal propio, no al panel ERP if (Auth::isEnfermero()) { header('Location: ' . APP_ROOT . '/../enfermero_portal.php'); exit; } // Recepcionistas con IP registrada: solo pueden acceder a su desk if (Auth::role() === 'recepcionista') { $mod = $router->getModule(); $view = $router->getView(); if ($mod === 'turnero' && $view === 'recepcion') { $deskId = (int)($_GET['desk_id'] ?? 0); $assignedDesk = self::recepIpDesk(); if ($assignedDesk && $deskId !== $assignedDesk) { header('Location: /erp.php?m=turnero&v=recepcion&desk_id=' . $assignedDesk); exit; } } } // Bacteriólogos: sin dashboard ni historial; redirige según IP if (Auth::isBacteriologo()) { $mod = $router->getModule(); $view = $router->getView(); $blocked = ($mod === 'dashboard') || ($mod === 'turnero' && in_array($view, ['dashboard', 'historial'], true)); if ($blocked) { header('Location: ' . self::bacteDefaultUrl()); exit; } } } self::dispatch($router); } // ─── Bootstrap ────────────────────────────────────────────────────────── /** * Inicializa la sesión y la zona horaria. * Se puede llamar también desde archivos legacy para obtener sesión lista. */ public static function boot(): void { if (session_status() === PHP_SESSION_NONE) { // Cookies de sesión seguras session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } date_default_timezone_set(defined('TIMEZONE') ? TIMEZONE : 'America/Bogota'); } // ─── Dispatch ─────────────────────────────────────────────────────────── private static function dispatch(Router $router): void { try { $viewFile = $router->resolveFile(); } catch (RuntimeException $e) { self::render404($router->getModule(), $router->getView()); return; } // Verificar permiso de módulo (solo en rutas protegidas) if (!$router->isPublic()) { $module = $router->getModule(); // Si el módulo está registrado en SYSTEM_MODULES, verificar acceso if (defined('SYSTEM_MODULES') && array_key_exists($module, SYSTEM_MODULES)) { if (!Rbac::hasModule($module)) { self::render403($module); return; } } } // Ejecutar la vista del módulo include $viewFile; } // ─── Helpers de rol ───────────────────────────────────────────────────── /** IP real del cliente, soporta proxy con X-Forwarded-For. */ public static function clientIp(): string { $raw = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; return trim(explode(',', $raw)[0]); } /** * Devuelve el lugar_id de recepción asignado a la IP del cliente, o null si no está registrada. */ private static function recepIpDesk(): ?int { try { $pdo = Database::getInstance()->getConnection(); $st = $pdo->prepare( "SELECT d.lugar_id FROM turnero_dispositivos d JOIN turnero_lugares l ON l.id = d.lugar_id WHERE d.ip = ? AND d.activo = 1 AND l.tipo = 'recepcion' LIMIT 1" ); $st->execute([self::clientIp()]); $id = $st->fetchColumn(); return $id ? (int)$id : null; } catch (\Throwable $_) { return null; } } /** * URL de destino para bacteriólogo según IP del cliente. * IP registrada en turnero_dispositivos → ese lugar. * IP no registrada → primer lugar de tipo muestras. */ private static function bacteDefaultUrl(): string { $base = '/erp.php?m=turnero&v=lugar&lugar_id='; try { $pdo = Database::getInstance()->getConnection(); $ip = self::clientIp(); // ¿IP registrada? $dev = $pdo->prepare( "SELECT lugar_id FROM turnero_dispositivos WHERE ip = ? AND activo = 1 LIMIT 1" ); $dev->execute([$ip]); $row = $dev->fetch(PDO::FETCH_ASSOC); if ($row) { return $base . (int)$row['lugar_id']; } // Primer lugar de toma de muestras $first = $pdo->query( "SELECT id FROM turnero_lugares WHERE activo=1 AND tipo='muestras' ORDER BY sort_order LIMIT 1" )->fetch(PDO::FETCH_ASSOC); if ($first) { return $base . (int)$first['id']; } } catch (\Throwable $_) {} // Fallback: turnero sin vista específica return '/erp.php?m=turnero'; } // ─── Páginas de error ──────────────────────────────────────────────────── private static function render404(string $module, string $view): void { http_response_code(404); Layout::open('Página no encontrada', 'fas fa-exclamation-triangle'); echo '
El módulo ' . esc($module) . ' / vista ' . esc($view) . ' no existe.
Tu rol no tiene acceso al módulo ' . esc($module) . '.