602 lines
31 KiB
PHP
602 lines
31 KiB
PHP
<?php
|
|
/**
|
|
* Gestión de Usuarios y Roles
|
|
* Solo accesible para administradores.
|
|
*/
|
|
require_once 'config/config.php';
|
|
if (!defined('APP_ROOT')) {
|
|
define('APP_ROOT', __DIR__);
|
|
}
|
|
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; }
|
|
|
|
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Admin';
|
|
|
|
// 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">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Usuarios y Roles — Módulo Lab</title>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
|
|
<style>
|
|
.role-badge { display:inline-block; padding:.25em .65em; border-radius:6px; font-size:.78rem; font-weight:600; color:#fff; }
|
|
.module-chip { display:inline-block; padding:.15em .55em; border-radius:4px; font-size:.72rem; background:#e9ecef; color:#495057; margin:1px; }
|
|
.module-check-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:.5rem; }
|
|
.module-check-grid .form-check { background:#f8f9fa; border-radius:6px; padding:.5rem .75rem .5rem 2rem; border:1px solid #dee2e6; }
|
|
.module-check-grid .form-check:has(input:checked) { background:#e7f1ff; border-color:#0d6efd; }
|
|
.color-preview { width:28px; height:28px; border-radius:50%; display:inline-block; vertical-align:middle; border:2px solid #dee2e6; }
|
|
.table-actions { white-space:nowrap; }
|
|
.user-avatar { width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:.85rem; color:#fff; }
|
|
.badge-active { background:#198754; }
|
|
.badge-inactive { background:#6c757d; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<?php
|
|
$SIDEBAR_TITLE = 'Panel Lab';
|
|
$SIDEBAR_ICON = 'fas fa-flask';
|
|
require_once __DIR__ . '/shared/components/sidebar.php';
|
|
?>
|
|
|
|
<main class="main-content">
|
|
<header class="content-header d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h1><i class="fas fa-users-cog text-primary"></i> Usuarios & Roles</h1>
|
|
<small class="text-muted"><i class="fas fa-user"></i> <?= htmlspecialchars($adminNombre) ?> — <?= date('d/m/Y') ?></small>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container-fluid py-3">
|
|
|
|
<!-- Tabs -->
|
|
<ul class="nav nav-tabs mb-3" id="mainTabs">
|
|
<li class="nav-item">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-usuarios">
|
|
<i class="fas fa-user me-1"></i> Usuarios
|
|
</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-roles">
|
|
<i class="fas fa-shield-alt me-1"></i> Roles
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content" style="display:block;padding:0">
|
|
|
|
<!-- ═══════════════ TAB USUARIOS ═══════════════ -->
|
|
<div class="tab-pane fade show active" id="tab-usuarios">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="mb-0 fw-semibold">Listado de usuarios del sistema</h6>
|
|
<button class="btn btn-primary btn-sm" onclick="usuarios.abrirNuevo()">
|
|
<i class="fas fa-plus me-1"></i> Nuevo Usuario
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0" id="tablaUsuarios">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:44px"></th>
|
|
<th>Usuario</th>
|
|
<th>Nombre</th>
|
|
<th>Rol</th>
|
|
<th>Estado</th>
|
|
<th>Último acceso</th>
|
|
<th class="text-end">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="usuariosTbody">
|
|
<tr><td colspan="7" class="text-center py-4 text-muted">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>Cargando…
|
|
</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- /tab-usuarios -->
|
|
|
|
<!-- ═══════════════ TAB ROLES ═══════════════ -->
|
|
<div class="tab-pane fade" id="tab-roles">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="mb-0 fw-semibold">Roles y permisos de módulo</h6>
|
|
<button class="btn btn-primary btn-sm" onclick="roles.abrirNuevo()">
|
|
<i class="fas fa-plus me-1"></i> Nuevo Rol
|
|
</button>
|
|
</div>
|
|
|
|
<div id="rolesGrid" class="row g-3">
|
|
<div class="col-12 text-center py-4 text-muted">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>Cargando…
|
|
</div>
|
|
</div>
|
|
</div><!-- /tab-roles -->
|
|
|
|
</div><!-- /tab-content -->
|
|
</div><!-- /container -->
|
|
</main>
|
|
|
|
<!-- ══════════════════════════════════════════════════════════
|
|
MODAL — USUARIO
|
|
══════════════════════════════════════════════════════════ -->
|
|
<div class="modal fade" id="modalUsuario" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="modalUsuarioTitulo">Nuevo Usuario</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="u_id">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Username <span class="text-danger">*</span></label>
|
|
<input type="text" id="u_username" class="form-control" placeholder="ej. juan.perez" autocomplete="off">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Nombre completo <span class="text-danger">*</span></label>
|
|
<input type="text" id="u_fullname" class="form-control" placeholder="Juan Pérez">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Correo electrónico</label>
|
|
<input type="email" id="u_email" class="form-control" placeholder="correo@dominio.com">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold" id="u_pass_label">Contraseña <span class="text-danger">*</span></label>
|
|
<input type="password" id="u_password" class="form-control" autocomplete="new-password">
|
|
<div class="form-text text-muted" id="u_pass_hint" style="display:none">
|
|
Deja en blanco para conservar la contraseña actual
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Rol <span class="text-danger">*</span></label>
|
|
<select id="u_role_id" class="form-select" onchange="usuarios.onRoleChange(this.value)">
|
|
<option value="">Seleccionar rol…</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Estado</label>
|
|
<select id="u_is_active" class="form-select">
|
|
<option value="1">Activo</option>
|
|
<option value="0">Inactivo</option>
|
|
</select>
|
|
</div>
|
|
<!-- Solo visible cuando es rol enfermero -->
|
|
<div class="col-12" id="u_enf_row" style="display:none">
|
|
<label class="form-label fw-semibold">Enfermera vinculada</label>
|
|
<select id="u_enfermera_id" class="form-select">
|
|
<option value="">Sin vincular</option>
|
|
</select>
|
|
<div class="form-text">Vincula este usuario con un registro de enfermera para que vea sus domicilios</div>
|
|
</div>
|
|
</div>
|
|
<div class="alert alert-danger mt-3 d-none" id="u_error"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
|
<button type="button" class="btn btn-primary" id="u_btn_guardar" onclick="usuarios.guardar()">
|
|
<i class="fas fa-save me-1"></i> Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ══════════════════════════════════════════════════════════
|
|
MODAL — ROL
|
|
══════════════════════════════════════════════════════════ -->
|
|
<div class="modal fade" id="modalRol" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="modalRolTitulo">Nuevo Rol</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="r_id">
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-5">
|
|
<label class="form-label fw-semibold">Nombre del rol <span class="text-danger">*</span></label>
|
|
<input type="text" id="r_name" class="form-control" placeholder="ej. Supervisor" oninput="roles.autoSlug()">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold">Slug <span class="text-danger">*</span></label>
|
|
<input type="text" id="r_slug" class="form-control" placeholder="supervisor">
|
|
<div class="form-text">Solo letras, números y guiones</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-semibold">Color</label>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<input type="color" id="r_color" class="form-control form-control-color" value="#0d6efd" style="width:56px;height:38px">
|
|
<span class="text-muted small">Insignia del rol</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<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>
|
|
<div class="module-check-grid" id="r_modules_grid">
|
|
<!-- Se inyecta por JS -->
|
|
</div>
|
|
<div class="alert alert-danger mt-3 d-none" id="r_error"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
|
<button type="button" class="btn btn-primary" onclick="roles.guardar()">
|
|
<i class="fas fa-save me-1"></i> Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
// ── Módulos del sistema (desde PHP) ──────────────────────────────────────────
|
|
const SYSTEM_MODULES = <?= json_encode($systemModules, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
// ── Datos en memoria ─────────────────────────────────────────────────────────
|
|
let allRoles = [];
|
|
let allUsers = [];
|
|
let enfermeras = [];
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// GESTIÓN DE ROLES
|
|
// ════════════════════════════════════════════════════════════════════
|
|
const roles = {
|
|
|
|
async cargar() {
|
|
const r = await fetch('api/lab/get_roles.php');
|
|
const res = await r.json();
|
|
console.log('[roles] API respuesta:', res);
|
|
allRoles = res.roles ?? [];
|
|
this.render();
|
|
// Refrescar selector de rol en modal de usuario
|
|
usuarios.refrescarSelectRoles();
|
|
},
|
|
|
|
render() {
|
|
const grid = document.getElementById('rolesGrid');
|
|
if (!allRoles.length) {
|
|
grid.innerHTML = '<div class="col-12 text-center text-muted py-4">No hay roles creados.</div>';
|
|
return;
|
|
}
|
|
grid.innerHTML = allRoles.map(r => {
|
|
const modChips = r.modules.map(m =>
|
|
`<span class="module-chip">${SYSTEM_MODULES[m] ?? m}</span>`
|
|
).join('');
|
|
const editBtn = `<button class="btn btn-sm btn-outline-primary" onclick="roles.editar(${r.id})"><i class="fas fa-edit"></i> Editar</button>`;
|
|
const deleteBtn = r.is_system ? '' :
|
|
`<button class="btn btn-sm btn-outline-danger ms-1" onclick="roles.eliminar(${r.id},'${esc(r.name)}')"><i class="fas fa-trash-alt"></i></button>`;
|
|
return `
|
|
<div class="col-md-6 col-xl-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<span class="role-badge" style="background:${r.color ?? '#6c757d'}">${esc(r.name)}</span>
|
|
${r.is_system ? '<span class="badge bg-secondary">sistema</span>':''}
|
|
<span class="ms-auto text-muted small">${r.user_count} usuario${r.user_count!==1?'s':''}</span>
|
|
</div>
|
|
<p class="text-muted small mb-2">${esc(r.description ?? '')}</p>
|
|
<div class="mb-3">${modChips || '<span class="text-muted small">Sin módulos</span>'}</div>
|
|
<div class="d-flex">${editBtn}${deleteBtn}</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
},
|
|
|
|
abrirNuevo() {
|
|
document.getElementById('r_id').value = '';
|
|
document.getElementById('r_name').value = '';
|
|
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';
|
|
new bootstrap.Modal('#modalRol').show();
|
|
},
|
|
|
|
editar(id) {
|
|
const r = allRoles.find(x => x.id === id);
|
|
if (!r) return;
|
|
document.getElementById('r_id').value = r.id;
|
|
document.getElementById('r_name').value = r.name;
|
|
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;
|
|
new bootstrap.Modal('#modalRol').show();
|
|
},
|
|
|
|
_buildModuleGrid(activeModules) {
|
|
const grid = document.getElementById('r_modules_grid');
|
|
grid.innerHTML = Object.entries(SYSTEM_MODULES).map(([slug, label]) => {
|
|
const checked = activeModules.includes(slug) ? 'checked' : '';
|
|
return `<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="mod_${slug}" value="${slug}" ${checked}>
|
|
<label class="form-check-label small" for="mod_${slug}">${label}</label>
|
|
</div>`;
|
|
}).join('');
|
|
},
|
|
|
|
autoSlug() {
|
|
const name = document.getElementById('r_name').value;
|
|
const slugEl = document.getElementById('r_slug');
|
|
if (!slugEl.dataset.manual) {
|
|
slugEl.value = name.toLowerCase()
|
|
.normalize('NFD').replace(/[\u0300-\u036f]/g,'')
|
|
.replace(/[^a-z0-9]+/g,'_').replace(/^_|_$/g,'');
|
|
}
|
|
},
|
|
|
|
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 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, home_page: homePage, modules };
|
|
if (id) body.id = parseInt(id);
|
|
|
|
const res = await fetch('api/lab/save_role.php', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify(body)
|
|
}).then(r => r.json());
|
|
|
|
if (!res.ok) { errEl.textContent = res.error; errEl.classList.remove('d-none'); return; }
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('modalRol'))?.hide();
|
|
await this.cargar();
|
|
},
|
|
|
|
async eliminar(id, nombre) {
|
|
if (!confirm(`¿Eliminar el rol "${nombre}"?\nLos usuarios con este rol pasarán al rol Administrador.`)) return;
|
|
const res = await fetch('api/lab/delete_role.php', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({id})
|
|
}).then(r => r.json());
|
|
if (!res.ok) { alert(res.error); return; }
|
|
await this.cargar();
|
|
await usuarios.cargar();
|
|
}
|
|
};
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// GESTIÓN DE USUARIOS
|
|
// ════════════════════════════════════════════════════════════════════
|
|
const usuarios = {
|
|
|
|
async cargar() {
|
|
const r = await fetch('api/lab/get_lab_users.php');
|
|
const res = await r.json();
|
|
console.log('[usuarios] API respuesta:', res);
|
|
allUsers = res.users ?? [];
|
|
console.log('[usuarios] Total a renderizar:', allUsers.length);
|
|
this.render();
|
|
},
|
|
|
|
render() {
|
|
const tbody = document.getElementById('usuariosTbody');
|
|
console.log('[usuarios.render] tbody:', tbody, '— allUsers:', allUsers.length);
|
|
if (!allUsers.length) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4 text-muted">No hay usuarios.</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = allUsers.map(u => {
|
|
const initials = (u.full_name ?? u.username).split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase();
|
|
const color = u.role_color ?? '#0d6efd';
|
|
const avatar = `<span class="user-avatar" style="background:${color}">${initials}</span>`;
|
|
const roleTag = `<span class="role-badge" style="background:${color}">${esc(u.role_name ?? u.role)}</span>`;
|
|
const statusTag= u.is_active
|
|
? '<span class="badge badge-active">Activo</span>'
|
|
: '<span class="badge badge-inactive">Inactivo</span>';
|
|
const lastLogin = u.last_login ? u.last_login.slice(0,16).replace('T',' ') : '—';
|
|
return `<tr>
|
|
<td>${avatar}</td>
|
|
<td><strong>${esc(u.username)}</strong></td>
|
|
<td>${esc(u.full_name ?? '—')}</td>
|
|
<td>${roleTag}</td>
|
|
<td>${statusTag}</td>
|
|
<td class="text-muted small">${lastLogin}</td>
|
|
<td class="text-end table-actions">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="usuarios.editar(${u.id})"><i class="fas fa-edit"></i></button>
|
|
<button class="btn btn-sm btn-outline-danger ms-1" onclick="usuarios.eliminar(${u.id},'${esc(u.username)}')"><i class="fas fa-trash-alt"></i></button>
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
},
|
|
|
|
refrescarSelectRoles() {
|
|
const sel = document.getElementById('u_role_id');
|
|
const curr = sel.value;
|
|
sel.innerHTML = '<option value="">Seleccionar rol…</option>' +
|
|
allRoles.map(r => `<option value="${r.id}">${esc(r.name)}</option>`).join('');
|
|
if (curr) sel.value = curr;
|
|
},
|
|
|
|
abrirNuevo() {
|
|
document.getElementById('u_id').value = '';
|
|
document.getElementById('u_username').value = '';
|
|
document.getElementById('u_fullname').value = '';
|
|
document.getElementById('u_email').value = '';
|
|
document.getElementById('u_password').value = '';
|
|
document.getElementById('u_role_id').value = '';
|
|
document.getElementById('u_is_active').value = '1';
|
|
document.getElementById('u_pass_label').innerHTML = 'Contraseña <span class="text-danger">*</span>';
|
|
document.getElementById('u_pass_hint').style.display = 'none';
|
|
document.getElementById('u_error').classList.add('d-none');
|
|
document.getElementById('u_enf_row').style.display = 'none';
|
|
document.getElementById('modalUsuarioTitulo').textContent = 'Nuevo Usuario';
|
|
new bootstrap.Modal('#modalUsuario').show();
|
|
},
|
|
|
|
editar(id) {
|
|
const u = allUsers.find(x => x.id === id);
|
|
if (!u) return;
|
|
document.getElementById('u_id').value = u.id;
|
|
document.getElementById('u_username').value = u.username;
|
|
document.getElementById('u_fullname').value = u.full_name ?? '';
|
|
document.getElementById('u_email').value = u.email ?? '';
|
|
document.getElementById('u_password').value = '';
|
|
document.getElementById('u_role_id').value = u.role_id ?? '';
|
|
document.getElementById('u_is_active').value = u.is_active ? '1' : '0';
|
|
document.getElementById('u_pass_label').innerHTML = 'Nueva contraseña';
|
|
document.getElementById('u_pass_hint').style.display = 'block';
|
|
document.getElementById('u_error').classList.add('d-none');
|
|
document.getElementById('modalUsuarioTitulo').textContent = 'Editar: ' + u.username;
|
|
this.onRoleChange(u.role_id, u.enfermera_id);
|
|
new bootstrap.Modal('#modalUsuario').show();
|
|
},
|
|
|
|
onRoleChange(roleId, currentEnfId = null) {
|
|
const role = allRoles.find(r => r.id === parseInt(roleId));
|
|
const enfRow = document.getElementById('u_enf_row');
|
|
if (role?.slug === 'enfermero') {
|
|
enfRow.style.display = '';
|
|
this._cargarEnfermeras(currentEnfId);
|
|
} else {
|
|
enfRow.style.display = 'none';
|
|
}
|
|
},
|
|
|
|
async _cargarEnfermeras(selectedId = null) {
|
|
if (!enfermeras.length) {
|
|
const res = await fetch('api/lab/get_enfermeras.php').then(r => r.json());
|
|
enfermeras = res.data ?? res.enfermeras ?? [];
|
|
}
|
|
const sel = document.getElementById('u_enfermera_id');
|
|
sel.innerHTML = '<option value="">Sin vincular</option>' +
|
|
enfermeras.map(e => `<option value="${e.id}" ${e.id == selectedId ? 'selected':''}>${esc(e.nombre_completo)}</option>`).join('');
|
|
},
|
|
|
|
async guardar() {
|
|
const id = document.getElementById('u_id').value;
|
|
const errEl = document.getElementById('u_error');
|
|
errEl.classList.add('d-none');
|
|
|
|
const body = {
|
|
username : document.getElementById('u_username').value.trim(),
|
|
full_name : document.getElementById('u_fullname').value.trim(),
|
|
email : document.getElementById('u_email').value.trim(),
|
|
password : document.getElementById('u_password').value,
|
|
role_id : parseInt(document.getElementById('u_role_id').value) || null,
|
|
is_active : parseInt(document.getElementById('u_is_active').value),
|
|
enfermera_id: document.getElementById('u_enfermera_id').value || null,
|
|
};
|
|
if (id) body.id = parseInt(id);
|
|
|
|
const res = await fetch('api/lab/save_lab_user.php', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify(body)
|
|
}).then(r => r.json());
|
|
|
|
if (!res.ok) { errEl.textContent = res.error; errEl.classList.remove('d-none'); return; }
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('modalUsuario'))?.hide();
|
|
await this.cargar();
|
|
},
|
|
|
|
async eliminar(id, username) {
|
|
if (!confirm(`¿Eliminar el usuario "${username}"? Esta acción no se puede deshacer.`)) return;
|
|
const res = await fetch('api/lab/delete_lab_user.php', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({id})
|
|
}).then(r => r.json());
|
|
if (!res.ok) { alert(res.error); return; }
|
|
await this.cargar();
|
|
}
|
|
};
|
|
|
|
// ── Utilidad ─────────────────────────────────────────────────────────────────
|
|
function esc(str) {
|
|
return String(str ?? '').replace(/[&<>"']/g, c =>
|
|
({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
}
|
|
|
|
// Marca manual en slug para no sobreescribir si el usuario lo editó
|
|
document.getElementById('r_slug').addEventListener('input', function() {
|
|
this.dataset.manual = this.value ? '1' : '';
|
|
});
|
|
|
|
// ── Inicialización ────────────────────────────────────────────────────────────
|
|
(async () => {
|
|
try {
|
|
await roles.cargar();
|
|
} catch (err) {
|
|
console.error('[roles.cargar] Error:', err);
|
|
document.getElementById('rolesGrid').innerHTML =
|
|
`<div class="col-12 alert alert-danger">Error cargando roles: ${esc(String(err))}</div>`;
|
|
}
|
|
try {
|
|
await usuarios.cargar();
|
|
} catch (err) {
|
|
console.error('[usuarios.cargar] Error:', err);
|
|
document.getElementById('usuariosTbody').innerHTML =
|
|
`<tr><td colspan="7" class="text-danger text-center py-3">Error cargando usuarios: ${esc(String(err))}</td></tr>`;
|
|
}
|
|
})();
|
|
</script>
|
|
<script src="assets/js/lab-sidebar.js"></script>
|
|
</body>
|
|
</html>
|