isPublic()) { Auth::requireLogin(); // Enfermeros van a su portal propio, no al panel ERP if (Auth::isEnfermero()) { header('Location: ' . APP_ROOT . '/../enfermero_portal.php'); 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 ───────────────────────────────────────────────────── /** * 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 = $_SERVER['REMOTE_ADDR'] ?? ''; // ¿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 '
'; echo ''; echo '

404 — Módulo no encontrado

'; echo '

El módulo ' . esc($module) . ' / vista ' . esc($view) . ' no existe.

'; echo 'Ir al inicio'; echo '
'; Layout::close(); } private static function render403(string $module): void { http_response_code(403); Layout::open('Acceso denegado', 'fas fa-lock'); echo '
'; echo ''; echo '

403 — Sin permiso

'; echo '

Tu rol no tiene acceso al módulo ' . esc($module) . '.

'; echo 'Volver'; echo '
'; Layout::close(); } }