up
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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', {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 @@
|
||||
<?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 @@
|
||||
<?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']]];
|
||||
|
||||
Reference in New Issue
Block a user