diff --git a/api/lab/get_roles.php b/api/lab/get_roles.php index 380de56..36b61f0 100644 --- a/api/lab/get_roles.php +++ b/api/lab/get_roles.php @@ -9,7 +9,7 @@ requireAdmin(); $db = Database::getInstance(); -$roles = $db->fetchAll("SELECT id, name, slug, description, color, is_system, created_at FROM roles ORDER BY id ASC"); +$roles = $db->fetchAll("SELECT id, name, slug, description, color, home_page, is_system, created_at FROM roles ORDER BY id ASC"); foreach ($roles as &$role) { $mods = $db->fetchAll( diff --git a/api/lab/save_role.php b/api/lab/save_role.php index db19d53..a8657de 100644 --- a/api/lab/save_role.php +++ b/api/lab/save_role.php @@ -18,6 +18,7 @@ $name = trim($body['name'] ?? ''); $slug = trim($body['slug'] ?? ''); $description = trim($body['description'] ?? ''); $color = trim($body['color'] ?? '#6c757d'); +$home_page = trim($body['home_page'] ?? '') ?: null; $modules = $body['modules'] ?? []; if (!$name) jsonError('El nombre del rol es obligatorio'); @@ -39,16 +40,16 @@ if ($id) { if (!$role) jsonError('Rol no encontrado', 404); $db->query( - "UPDATE roles SET name=?, slug=?, description=?, color=?, updated_at=NOW() WHERE id=?", - [$name, $slug, $description, $color, $id] + "UPDATE roles SET name=?, slug=?, description=?, color=?, home_page=?, updated_at=NOW() WHERE id=?", + [$name, $slug, $description, $color, $home_page, $id] ); // Reconstruir módulos solo si no es sistema, o siempre (admin puede editar módulos) $db->query("DELETE FROM role_modules WHERE role_id = ?", [$id]); } else { // Crear $db->query( - "INSERT INTO roles (name, slug, description, color) VALUES (?,?,?,?)", - [$name, $slug, $description, $color] + "INSERT INTO roles (name, slug, description, color, home_page) VALUES (?,?,?,?,?)", + [$name, $slug, $description, $color, $home_page] ); $id = $db->lastInsertId(); } diff --git a/assets/css/styles.css b/assets/css/styles.css index c2fb113..012b8ef 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -57,19 +57,6 @@ body { box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); } -.sidebar-header { - padding: 20px; - text-align: center; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.05); -} - -.sidebar-header h4 { - font-weight: 600; - margin: 0; - font-size: 1.2rem; -} - .sidebar-menu { list-style: none; padding: 0; @@ -82,12 +69,13 @@ body { .sidebar-menu .nav-link { display: block; - padding: 15px 20px; - color: rgba(255, 255, 255, 0.8); + padding: 11px 20px; + color: rgba(255, 255, 255, 0.82); text-decoration: none; - transition: all 0.3s ease; + transition: all 0.25s ease; position: relative; overflow: hidden; + font-size: 0.9rem; } .sidebar-menu .nav-link::before { @@ -155,6 +143,90 @@ body { background: #dc3545; } +/* ── Sidebar – cabecera mejorada ─────────────────────────────────── */ +.sidebar-header { + padding: 18px 20px; + text-align: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.2); +} + +.sidebar-header h4 { + font-size: 1.05rem; + font-weight: 700; + letter-spacing: 0.02em; + margin: 0; +} + +/* ── Sidebar – etiquetas de categoría ────────────────────────────── */ +.sidebar-menu li.sidebar-section { + border-bottom: none; + padding: 12px 18px 4px; + font-size: 0.67rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.13em; + color: rgba(255, 255, 255, 0.38); + margin-top: 4px; + pointer-events: none; + display: flex; + align-items: center; + gap: 5px; + position: relative; +} + +.sidebar-menu li.sidebar-section::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin-left: 4px; +} + +/* ── Sidebar – usuario al pie ────────────────────────────────────── */ +.sidebar-user { + padding: 12px 18px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.18); + display: flex; + flex-direction: column; + gap: 2px; +} + +.sidebar-user-name { + font-size: 0.85rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-user-role { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.48); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +/* ── Sidebar – scrollbar ─────────────────────────────────────────── */ +.sidebar::-webkit-scrollbar { + width: 4px; +} + +.sidebar::-webkit-scrollbar-track { + background: transparent; +} + +.sidebar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); + border-radius: 2px; +} + +.sidebar::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.32); +} + /* Main Content */ .main-content { margin-left: 280px; diff --git a/config/config.php b/config/config.php index 20f4957..3f5d47d 100644 --- a/config/config.php +++ b/config/config.php @@ -499,7 +499,8 @@ function authenticateUser($username, $password) { // Cargar módulos del rol (slug y permiso) $modules = []; $modulePermissions = []; - $roleSlug = $admin['role'] ?? 'admin'; + $roleSlug = $admin['role'] ?? 'admin'; + $homePage = null; if (!empty($admin['role_id'])) { $modStmt = $pdo->prepare( "SELECT module_slug, COALESCE(permission,'write') AS permission @@ -510,6 +511,11 @@ function authenticateUser($username, $password) { $modules[] = $row['module_slug']; $modulePermissions[$row['module_slug']] = $row['permission']; } + // Leer home_page del rol + $rpStmt = $pdo->prepare("SELECT home_page FROM roles WHERE id = ?"); + $rpStmt->execute([$admin['role_id']]); + $rp = $rpStmt->fetch(PDO::FETCH_ASSOC); + $homePage = $rp['home_page'] ?? null; } elseif ($roleSlug === 'admin') { // Fallback: admin sin role_id tiene todos los módulos $modules = array_keys(SYSTEM_MODULES); @@ -524,6 +530,7 @@ function authenticateUser($username, $password) { 'email' => $admin['email'], 'role' => $roleSlug, 'role_id' => $admin['role_id'] ?? null, + 'home_page' => $homePage, 'enfermera_id' => $admin['enfermera_id'] ?? null, 'modules' => $modules, 'module_permissions' => $modulePermissions, diff --git a/core/ModuleRegistry.php b/core/ModuleRegistry.php index aaaf98d..5ac68af 100644 --- a/core/ModuleRegistry.php +++ b/core/ModuleRegistry.php @@ -20,16 +20,17 @@ class ModuleRegistry private static ?array $registry = null; /** Orden de visualización de categorías en el sidebar */ - private const CATEGORY_ORDER = ['bot', 'lab', 'turnero', 'clinico', 'reportes', 'sistema']; + private const CATEGORY_ORDER = ['bot', 'lab', 'formularios', 'turnero', 'clinico', 'reportes', 'sistema']; /** Etiquetas e íconos de cada categoría */ private const CATEGORIES = [ - 'bot' => ['label' => 'WhatsApp', 'icon' => 'fab fa-whatsapp'], - 'lab' => ['label' => 'Laboratorio', 'icon' => 'fas fa-flask'], - 'turnero' => ['label' => 'Turnero', 'icon' => 'fas fa-ticket-alt'], - 'clinico' => ['label' => 'Clínico', 'icon' => 'fas fa-vials'], - 'reportes'=> ['label' => 'Reportes', 'icon' => 'fas fa-chart-bar'], - 'sistema' => ['label' => 'Sistema', 'icon' => 'fas fa-cog'], + 'bot' => ['label' => 'WhatsApp', 'icon' => 'fab fa-whatsapp'], + 'lab' => ['label' => 'Domicilios', 'icon' => 'fas fa-house-medical'], + 'formularios'=> ['label' => 'Formulario', 'icon' => 'fas fa-file-invoice'], + 'turnero' => ['label' => 'Turnero', 'icon' => 'fas fa-ticket-alt'], + 'clinico' => ['label' => 'Clínico', 'icon' => 'fas fa-vials'], + 'reportes' => ['label' => 'Reportes', 'icon' => 'fas fa-chart-bar'], + 'sistema' => ['label' => 'Sistema', 'icon' => 'fas fa-cog'], ]; // ─── API pública ──────────────────────────────────────────────────────── @@ -47,20 +48,26 @@ class ModuleRegistry /** * Retorna solo los módulos ACTIVOS que el usuario actual puede ver. - * Si el usuario es admin sin role_id → todos los activos. + * + * IMPORTANTE: consulta role_modules desde BD en tiempo real para que los + * cambios hechos en lab_usuarios.php se reflejen sin necesidad de re-login. + * + * Excepción: superadmin y admin sin role_id tienen acceso total (retrocompat). * * @return array Indexado por slug. */ public static function forCurrentUser(): array { self::load(); + $allowed = self::liveUserModules(); // null = acceso total + $result = []; foreach (self::$registry as $slug => $mod) { if (!$mod['is_active']) { continue; } - // hasModule() definida en config.php / core/Rbac.php (retrocompat) - if (function_exists('hasModule') && !hasModule($slug)) { + // null = superadmin / admin legacy → ve todo + if ($allowed !== null && !in_array($slug, $allowed, true)) { continue; } $result[$slug] = $mod; @@ -68,6 +75,50 @@ class ModuleRegistry return $result; } + /** + * Obtiene los slugs de módulos permitidos para el usuario actual consultando + * role_modules en BD (ignora la caché de sesión para ser siempre fresco). + * + * Retorna null si el usuario tiene acceso total (superadmin / admin legacy). + * + * @return string[]|null + */ + private static function liveUserModules(): ?array + { + $role = $_SESSION['admin_user']['role'] ?? 'admin'; + $roleId = $_SESSION['admin_user']['role_id'] ?? null; + + // Superadmin → acceso total siempre + if ($role === 'superadmin') { + return null; + } + + // Admin sin role_id asignado → retrocompat, acceso total + if ($role === 'admin' && !$roleId) { + return null; + } + + // Sin role_id en cualquier otro rol → usar sesión como fallback + if (!$roleId) { + return $_SESSION['admin_user']['modules'] ?? []; + } + + // Consulta en tiempo real: reflejan cambios en lab_usuarios.php al instante + try { + $db = Database::getInstance(); + $pdo = $db->getConnection(); + $stmt = $pdo->prepare( + "SELECT module_slug FROM role_modules WHERE role_id = ?" + ); + $stmt->execute([(int) $roleId]); + return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), 'module_slug'); + } catch (Exception $e) { + error_log("ModuleRegistry::liveUserModules error: " . $e->getMessage()); + // Fallback a sesión si la BD no responde + return $_SESSION['admin_user']['modules'] ?? []; + } + } + /** * Retorna módulos activos agrupados por categoría para el sidebar. * Solo incluye módulos accesibles por el usuario actual. diff --git a/lab_usuarios.php b/lab_usuarios.php index 426ac1d..b61cf7f 100644 --- a/lab_usuarios.php +++ b/lab_usuarios.php @@ -4,6 +4,9 @@ * Solo accesible para administradores. */ require_once 'config/config.php'; +if (file_exists(__DIR__ . '/core/ModuleRegistry.php')) { + require_once __DIR__ . '/core/ModuleRegistry.php'; +} if (!isUserLoggedIn()) { header('Location: login.php'); exit; } if (isEnfermero()) { header('Location: enfermero_portal.php'); exit; } @@ -12,6 +15,20 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user'][' // Catálogo de módulos disponible desde PHP (para el