This commit is contained in:
Lizandro Guarnizo
2026-04-16 23:07:53 -05:00
parent aa12a87fbb
commit 0164c51c1a
10 changed files with 236 additions and 41 deletions
+1 -1
View File
@@ -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(
+5 -4
View File
@@ -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();
}
+88 -16
View File
@@ -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;
+8 -1
View File
@@ -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,
+61 -10
View File
@@ -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<string, 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.
+44 -7
View File
@@ -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 <script>)
$systemModules = SYSTEM_MODULES;
// Lista enriquecida de módulos para el select de home_page (slug → ruta)
$systemModulesRaw = [];
if (class_exists('ModuleRegistry')) {
foreach (ModuleRegistry::getAll() as $slug => $mod) {
if (!empty($mod['route'])) {
$systemModulesRaw[] = ['name' => $mod['name'], 'route' => $mod['route']];
}
}
} else {
foreach (SYSTEM_MODULES as $slug => $label) {
$systemModulesRaw[] = ['name' => $label, 'route' => '/' . $slug . '.php'];
}
}
?>
<!DOCTYPE html>
<html lang="es">
@@ -223,6 +240,23 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<label class="form-label fw-semibold">Descripción</label>
<textarea id="r_description" class="form-control" rows="2" placeholder="Descripción corta del rol"></textarea>
</div>
<div class="col-12">
<label class="form-label fw-semibold">
<i class="fas fa-home me-1 text-primary"></i>
Página de inicio al hacer login
</label>
<select id="r_home_page" class="form-select">
<option value="">— Automático (según módulos asignados) —</option>
<?php foreach ($systemModulesRaw as $sm): ?>
<option value="<?= htmlspecialchars($sm['route'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($sm['name'], ENT_QUOTES, 'UTF-8') ?>
(<?= htmlspecialchars($sm['route'], ENT_QUOTES, 'UTF-8') ?>)
</option>
<?php endforeach; ?>
<option value="/index.php">Panel principal (/index.php)</option>
</select>
<div class="form-text">Al iniciar sesión, el usuario con este rol será redirigido a la página seleccionada.</div>
</div>
</div>
<label class="form-label fw-semibold">Módulos permitidos</label>
@@ -303,6 +337,7 @@ const roles = {
document.getElementById('r_slug').value = '';
document.getElementById('r_description').value = '';
document.getElementById('r_color').value = '#0d6efd';
document.getElementById('r_home_page').value = '';
document.getElementById('r_error').classList.add('d-none');
this._buildModuleGrid([]);
document.getElementById('modalRolTitulo').textContent = 'Nuevo Rol';
@@ -317,6 +352,7 @@ const roles = {
document.getElementById('r_slug').value = r.slug;
document.getElementById('r_description').value = r.description ?? '';
document.getElementById('r_color').value = r.color ?? '#0d6efd';
document.getElementById('r_home_page').value = r.home_page ?? '';
document.getElementById('r_error').classList.add('d-none');
this._buildModuleGrid(r.modules);
document.getElementById('modalRolTitulo').textContent = 'Editar Rol: ' + r.name;
@@ -345,17 +381,18 @@ const roles = {
},
async guardar() {
const id = document.getElementById('r_id').value;
const name = document.getElementById('r_name').value.trim();
const slug = document.getElementById('r_slug').value.trim();
const desc = document.getElementById('r_description').value.trim();
const color = document.getElementById('r_color').value;
const errEl = document.getElementById('r_error');
const id = document.getElementById('r_id').value;
const name = document.getElementById('r_name').value.trim();
const slug = document.getElementById('r_slug').value.trim();
const desc = document.getElementById('r_description').value.trim();
const color = document.getElementById('r_color').value;
const homePage = document.getElementById('r_home_page').value.trim() || null;
const errEl = document.getElementById('r_error');
errEl.classList.add('d-none');
const modules = [...document.querySelectorAll('#r_modules_grid input:checked')].map(el => el.value);
const body = { name, slug, description: desc, color, modules };
const body = { name, slug, description: desc, color, home_page: homePage, modules };
if (id) body.id = parseInt(id);
const res = await fetch('api/lab/save_role.php', {
+12
View File
@@ -57,6 +57,18 @@ if ($_POST && !$loginBlocked) {
// Redirigir según el rol
$roleSlug = $adminUser['role'] ?? 'admin';
$modules = $adminUser['modules'] ?? [];
// 1. Página de inicio configurada explícitamente para el rol
if (!empty($adminUser['home_page'])) {
$safePath = filter_var($adminUser['home_page'], FILTER_SANITIZE_URL);
// Solo rutas relativas (evitar redirects abiertos)
if (str_starts_with($safePath, '/') && !str_starts_with($safePath, '//')) {
header('Location: ' . $safePath);
exit;
}
}
// 2. Reglas heredadas (compatibilidad hacia atrás)
if ($roleSlug === 'enfermero') {
header('Location: enfermero_portal.php');
} elseif (count($modules) === 1 && $modules[0] === 'lab_formularios') {
+15
View File
@@ -0,0 +1,15 @@
-- =============================================================================
-- Migración: 20260416_roles_home_page
-- Agrega la columna home_page a la tabla roles para definir la página de inicio
-- por rol. El valor es la ruta relativa (ej. /lab_dashboard.php, /index.php).
-- NULL = comportamiento heredado (login.php decide según reglas antiguas).
-- =============================================================================
ALTER TABLE `roles`
ADD COLUMN IF NOT EXISTS `home_page` VARCHAR(200) DEFAULT NULL
COMMENT 'Ruta de inicio al hacer login. NULL = usa reglas heredadas.'
AFTER `color`;
-- Registrar migración
INSERT IGNORE INTO migrations (migration, executed_at)
VALUES ('20260416_roles_home_page', NOW());
+1 -1
View File
@@ -1 +1 @@
<?php return ['slug'=>'lab_configuracion','name'=>'Configuración Lab','icon'=>'fas fa-sliders-h','category'=>'sistema','route'=>'/lab_configuracion.php','is_active'=>true,'sort_order'=>80,'oleada'=>0,'description'=>'Tarifas, laboratorio y configuraciones del sistema','links'=>[['name'=>'Configuración','icon'=>'fas fa-sliders-h','route'=>'/lab_configuracion.php']]];
<?php return ['slug'=>'lab_configuracion','name'=>'Identidad del Lab','icon'=>'fas fa-file-pdf','category'=>'formularios','route'=>'/lab_configuracion.php','is_active'=>true,'sort_order'=>32,'oleada'=>0,'description'=>'Personaliza el encabezado, logo y datos que aparecen en los documentos PDF','links'=>[['name'=>'Identidad del Lab','icon'=>'fas fa-file-pdf','route'=>'/lab_configuracion.php']]];
+1 -1
View File
@@ -1 +1 @@
<?php return ['slug'=>'lab_formularios','name'=>'Formularios','icon'=>'fas fa-wpforms','category'=>'lab','route'=>'/lab_formularios.php','is_active'=>true,'sort_order'=>24,'oleada'=>0,'description'=>'Builder de formularios drag-drop con firma digital','links'=>[['name'=>'Formularios','icon'=>'fas fa-wpforms','route'=>'/lab_formularios.php']]];
<?php return ['slug'=>'lab_formularios','name'=>'Formularios','icon'=>'fas fa-wpforms','category'=>'formularios','route'=>'/lab_formularios.php','is_active'=>true,'sort_order'=>30,'oleada'=>0,'description'=>'Builder de formularios drag-drop con firma digital','links'=>[['name'=>'Formularios','icon'=>'fas fa-wpforms','route'=>'/lab_formularios.php']]];