Download web/users.html from CYV28/RetinAI: direct link, hf CLI and curl.
- Browser
- Download file 33.3 kB
-
https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/users.html
- Command line
-
hf download hf://spaces/CYV28/RetinAI/web/users.html
-
curl -L -o users.html https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/users.html
33.3 kB
| <html lang="es"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
| <meta name="description" content="Gestión de Usuarios"> | |
| <meta name="author" content="Sistema Médico"> | |
| <link rel="icon" href="favicon.ico"> | |
| <title>Usuarios</title> | |
| <link rel="stylesheet" href="css/simplebar.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script> | |
| (function() { | |
| var dark = localStorage.getItem('theme') === 'dark'; | |
| document.write( | |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'dark' : 'light') + '.css" id="' + (dark ? 'dark' : 'light') + 'Theme">' + | |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'light' : 'dark') + '.css" id="' + (dark ? 'light' : 'dark') + 'Theme" disabled>' | |
| ); | |
| })(); | |
| </script> | |
| <style> | |
| /* ── TOAST NOTIFICATIONS ─────────────────────────── */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 1.5rem; | |
| right: 1.5rem; | |
| z-index: 99999; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.6rem; | |
| pointer-events: none; | |
| } | |
| .toast-notify { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 12px; | |
| min-width: 300px; | |
| max-width: 380px; | |
| padding: 14px 16px; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.15); | |
| background: #fff; | |
| border-left: 5px solid #ccc; | |
| pointer-events: all; | |
| opacity: 0; | |
| transform: translateX(60px); | |
| transition: opacity 0.3s ease, transform 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .toast-notify.show { opacity: 1; transform: translateX(0); } | |
| .toast-notify.hide { opacity: 0; transform: translateX(60px); } | |
| .toast-notify.toast-success { border-left-color: #28a745; } | |
| .toast-notify.toast-error { border-left-color: #dc3545; } | |
| .toast-notify.toast-warning { border-left-color: #ffc107; } | |
| .toast-notify.toast-info { border-left-color: #17a2b8; } | |
| .toast-icon { flex-shrink: 0; margin-top: 1px; } | |
| .toast-success .toast-icon svg { stroke: #28a745; } | |
| .toast-error .toast-icon svg { stroke: #dc3545; } | |
| .toast-warning .toast-icon svg { stroke: #ffc107; } | |
| .toast-info .toast-icon svg { stroke: #17a2b8; } | |
| .toast-body { flex: 1; } | |
| .toast-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 2px; } | |
| .toast-success .toast-title { color: #155724; } | |
| .toast-error .toast-title { color: #721c24; } | |
| .toast-warning .toast-title { color: #856404; } | |
| .toast-info .toast-title { color: #0c5460; } | |
| .toast-msg { font-size: 0.825rem; color: #555; line-height: 1.4; } | |
| .toast-close { | |
| background: none; border: none; font-size: 1rem; | |
| line-height: 1; color: #aaa; cursor: pointer; padding: 0; flex-shrink: 0; | |
| } | |
| .toast-close:hover { color: #333; } | |
| .toast-progress { | |
| position: absolute; | |
| bottom: 0; left: 0; | |
| height: 3px; | |
| border-radius: 0 0 0 10px; | |
| } | |
| .toast-success .toast-progress { background: #28a745; } | |
| .toast-error .toast-progress { background: #dc3545; } | |
| .toast-warning .toast-progress { background: #ffc107; } | |
| .toast-info .toast-progress { background: #17a2b8; } | |
| </style> | |
| </head> | |
| <body class="vertical light"> | |
| <div class="wrapper"> | |
| <!-- Top Navigation --> | |
| <nav class="topnav navbar navbar-light"> | |
| <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar"> | |
| <i data-feather="menu" class="navbar-toggler-icon"></i> | |
| </button> | |
| <ul class="nav"> | |
| <li class="nav-item"> | |
| <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light"> | |
| <i data-feather="sun"></i> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="#" onclick="confirmLogout()" class="nav-link text-muted" title="Cerrar sesión" style="display: flex; align-items: center; gap: 6px;"> | |
| <span class="avatar avatar-sm mt-2"> | |
| <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle"> | |
| </span> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Cerrar sesión"> | |
| <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> | |
| <polyline points="16 17 21 12 16 7"/> | |
| <line x1="21" y1="12" x2="9" y2="12"/> | |
| </svg> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| <!-- Sidebar --> | |
| <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar> | |
| <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle"> | |
| <i data-feather="x"><span class="sr-only"></span></i> | |
| </a> | |
| <nav class="vertnav navbar navbar-light"> | |
| <!-- nav bar --> | |
| <a class="navbar-brand mx-auto mt-2 flex-fill text-center"> | |
| <img src="assets/images/RetinAILogo.png" alt="Logo" id="sidebarLogo" style="max-width:140px; transition: opacity 0.3s ease;"> | |
| <img src="assets/images/LogoMini.png" alt="Logo Mini" id="sidebarLogoMini" style="max-width:44px; display:none; transition: opacity 0.3s ease;"> | |
| </a> | |
| <ul class="navbar-nav flex-fill w-100 mb-2"> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./index.html"> | |
| <i data-feather="home" class="fe-16"></i> | |
| <span class="ml-3 item-text">Dashboard</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./diagnosis.html"> | |
| <i data-feather="eye" class="fe-16"></i> | |
| <span class="ml-3 item-text">Diagnóstico</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./patients.html"> | |
| <i data-feather="users" class="fe-16"></i> | |
| <span class="ml-3 item-text">Pacientes</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./historial-consultas.html"> | |
| <i data-feather="file-text" class="fe-16"></i> | |
| <span class="ml-3 item-text">Consultas</span> | |
| </a> | |
| </li> | |
| <li class="nav-item active"> | |
| <a class="nav-link" href="./users.html"> | |
| <i data-feather="user" class="fe-16"></i> | |
| <span class="ml-3 item-text">Usuarios</span> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| </aside> | |
| <!-- Main Content --> | |
| <main role="main" class="main-content"> | |
| <div class="container-fluid"> | |
| <div class="row justify-content-center"> | |
| <div class="col-12"> | |
| <!-- Header --> | |
| <div class="row align-items-center mb-2"> | |
| <div class="col"> | |
| <h2 class="h5 page-title">Gestión de Usuarios</h2> | |
| </div> | |
| <div class="col-auto"> | |
| <button class="btn btn-primary" onclick="loadUsers()"> | |
| <i data-feather="refresh-cw" class="fe-16 mr-2"></i>Actualizar | |
| </button> | |
| <button class="btn btn-success ml-2" onclick="showAddUserModal()"> | |
| <i data-feather="plus" class="fe-16 mr-2"></i>Nuevo Usuario | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Users Table --> | |
| <div class="card shadow"> | |
| <div class="card-header"> | |
| <h6 class="card-title mb-0"> | |
| <i data-feather="users" class="mr-2"></i>Lista de Usuarios | |
| </h6> | |
| </div> | |
| <div class="card-body"> | |
| <div id="loadingSpinner" class="text-center py-5"> | |
| <div class="spinner-border text-primary" role="status"> | |
| <span class="sr-only">Cargando...</span> | |
| </div> | |
| <p class="text-muted mt-2">Cargando usuarios...</p> | |
| </div> | |
| <div id="usersTableContainer" style="display: none;"> | |
| <div class="table-responsive"> | |
| <table class="table table-striped table-hover"> | |
| <thead class="thead-light"> | |
| <tr> | |
| <th>ID</th> | |
| <th>Nombre de Usuario</th> | |
| <th>Rol</th> | |
| <th>Fecha de Creación</th> | |
| <th class="text-center">Acciones</th> | |
| </tr> | |
| </thead> | |
| <tbody id="usersTableBody"> | |
| <!-- Datos se cargarán aquí dinámicamente --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div id="emptyState" class="text-center py-5" style="display: none;"> | |
| <i data-feather="users" class="fe-48 text-muted mb-3"></i> | |
| <h6 class="text-muted">No se encontraron usuarios</h6> | |
| <p class="text-muted">No hay usuarios registrados en el sistema.</p> | |
| <button class="btn btn-primary" onclick="showAddUserModal()"> | |
| <i data-feather="plus" class="mr-1"></i>Crear Primer Usuario | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Modal para agregar/editar usuario --> | |
| <div class="modal fade" id="userModal" tabindex="-1" role="dialog" aria-labelledby="userModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog" role="document"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title" id="userModalLabel"> | |
| <i data-feather="user" class="mr-2"></i>Nuevo Usuario | |
| </h5> | |
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |
| <span aria-hidden="true">×</span> | |
| </button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="userForm"> | |
| <input type="hidden" id="userId"> | |
| <div class="form-group"> | |
| <label for="username">Nombre de Usuario</label> | |
| <input type="text" class="form-control" id="username" required> | |
| </div> | |
| <div class="form-group"> | |
| <label for="password">Contraseña</label> | |
| <input type="password" class="form-control" id="password" required> | |
| </div> | |
| <div class="form-group"> | |
| <label for="confirmPassword">Confirmar Contraseña</label> | |
| <input type="password" class="form-control" id="confirmPassword" required> | |
| </div> | |
| <div class="form-group"> | |
| <label for="role">Rol</label> | |
| <select class="form-control" id="role" required> | |
| <option value="Doctor">Doctor</option> | |
| <option value="Admin">Administrador</option> | |
| </select> | |
| </div> | |
| </form> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button> | |
| <button type="button" class="btn btn-primary" id="saveUserBtn" onclick="saveUser()">Guardar</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="js/jquery.min.js"></script> | |
| <script src="js/popper.min.js"></script> | |
| <script src="js/bootstrap.min.js"></script> | |
| <script src="js/simplebar.min.js"></script> | |
| <script src="js/config.js"></script> | |
| <script src="api.js"></script> | |
| <script> | |
| $(document).ready(function() { | |
| checkAuthentication(); | |
| loadUsers(); | |
| $('#logoutBtn').on('click', function(e) { | |
| e.preventDefault(); | |
| if (confirm('¿Estás seguro que deseas cerrar sesión?')) { | |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| }); | |
| }); | |
| function checkAuthentication() { | |
| api.getSession().then(r => { if (!r.success) window.location.href = 'auth-login.html'; }).catch(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| function loadUsers() { | |
| $('#loadingSpinner').show(); | |
| $('#usersTableContainer').hide(); | |
| $('#emptyState').hide(); | |
| api.getUsers().then(function(response) { | |
| $('#loadingSpinner').hide(); | |
| if (response.success) { | |
| if (response.users.length > 0) { | |
| renderUsersTable(response.users); | |
| $('#usersTableContainer').show(); | |
| } else { | |
| $('#emptyState').show(); | |
| } | |
| } else { | |
| console.error('Error cargando usuarios:', response.message); | |
| showAlert('error', 'Error cargando usuarios: ' + response.message); | |
| $('#emptyState').show(); | |
| } | |
| }); | |
| } | |
| function renderUsersTable(users) { | |
| const tbody = $('#usersTableBody'); | |
| tbody.empty(); | |
| users.forEach(user => { | |
| const row = ` | |
| <tr> | |
| <td>${user.userID}</td> | |
| <td>${user.username}</td> | |
| <td> | |
| <span class="badge ${user.role === 'Admin' ? 'badge-primary' : 'badge-secondary'}"> | |
| ${user.role} | |
| </span> | |
| </td> | |
| <td>${user.creationDate}</td> | |
| <td class="text-center"> | |
| <div class="btn-group" role="group"> | |
| <button class="btn btn-sm btn-outline-primary" | |
| onclick="editUser(${user.userID})" | |
| title="Editar"> | |
| <i data-feather="edit" class="fe-12"></i> | |
| </button> | |
| <button class="btn btn-sm btn-outline-danger" | |
| onclick="confirmDeleteUser(${user.userID})" | |
| title="Eliminar"> | |
| <i data-feather="trash-2" class="fe-12"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| tbody.append(row); | |
| }); | |
| } | |
| function showAddUserModal() { | |
| $('#userForm')[0].reset(); | |
| $('#userId').val(''); | |
| $('#userModalLabel').html('<i data-feather="user-plus" class="mr-2"></i>Nuevo Usuario'); | |
| $('#userModal').modal('show'); | |
| } | |
| function editUser(userId) { | |
| api.getUsers().then(function(response) { | |
| if (response.success) { | |
| const user = response.users.find(u => u.userID == userId); | |
| if (user) { | |
| $('#userId').val(user.userID); | |
| $('#username').val(user.username); | |
| $('#role').val(user.role); | |
| $('#password').val(''); | |
| $('#confirmPassword').val(''); | |
| $('#userModalLabel').html('<i data-feather="edit" class="mr-2"></i>Editar Usuario'); | |
| $('#userModal').modal('show'); | |
| } | |
| } | |
| }); | |
| } | |
| function saveUser() { | |
| const userId = $('#userId').val(); | |
| const username = $('#username').val().trim(); | |
| const password = $('#password').val(); | |
| const confirmPassword = $('#confirmPassword').val(); | |
| const role = $('#role').val(); | |
| if (!username) { | |
| showAlert('error', 'El nombre de usuario es requerido'); | |
| return; | |
| } | |
| if (!userId && (!password || !confirmPassword)) { | |
| showAlert('error', 'La contraseña es requerida para nuevos usuarios'); | |
| return; | |
| } | |
| if (password !== confirmPassword) { | |
| showAlert('error', 'Las contraseñas no coinciden'); | |
| return; | |
| } | |
| if (userId) { | |
| const userData = { | |
| userId: userId, | |
| username: username, | |
| role: role | |
| }; | |
| if (password) { | |
| userData.password = password; | |
| } | |
| api.updateUser(userId, userData).then(function(response) { | |
| if (response.success) { | |
| showAlert('success', 'Usuario actualizado exitosamente'); | |
| $('#userModal').modal('hide'); | |
| loadUsers(); | |
| } else { | |
| showAlert('error', response.message || 'Error actualizando usuario'); | |
| } | |
| }); | |
| } else { | |
| api.createUser(username, password, role).then(function(response) { | |
| if (response.success) { | |
| showAlert('success', 'Usuario creado exitosamente'); | |
| $('#userModal').modal('hide'); | |
| loadUsers(); | |
| } else { | |
| showAlert('error', response.message || 'Error creando usuario'); | |
| } | |
| }); | |
| } | |
| } | |
| function confirmDeleteUser(userId) { | |
| if (confirm('¿Estás seguro que deseas eliminar este usuario? Esta acción no se puede deshacer.')) { | |
| deleteUser(userId); | |
| } | |
| } | |
| function deleteUser(userId) { | |
| api.deleteUser(userId).then(function(response) { | |
| if (response.success) { | |
| showAlert('success', 'Usuario eliminado exitosamente'); | |
| loadUsers(); | |
| } else { | |
| showAlert('error', response.message || 'Error eliminando usuario'); | |
| } | |
| }); | |
| } | |
| function showAlert(type, message) { | |
| if (!document.getElementById('toast-container')) { | |
| const container = document.createElement('div'); | |
| container.id = 'toast-container'; | |
| document.body.appendChild(container); | |
| } | |
| const container = document.getElementById('toast-container'); | |
| const icons = { | |
| success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>', | |
| error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', | |
| warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' | |
| }; | |
| const titles = { success: 'Éxito', error: 'Error', warning: 'Advertencia', info: 'Información' }; | |
| const duration = type === 'error' ? 6000 : 4000; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast-notify toast-${type}`; | |
| toast.innerHTML = ` | |
| <div class="toast-icon">${icons[type] || icons.info}</div> | |
| <div class="toast-body"> | |
| <div class="toast-title">${titles[type] || 'Aviso'}</div> | |
| <div class="toast-msg">${message}</div> | |
| </div> | |
| <button class="toast-close" onclick="this.closest('.toast-notify').remove()">×</button> | |
| <div class="toast-progress" style="width:100%;transition:width ${duration}ms linear;"></div> | |
| `; | |
| container.appendChild(toast); | |
| requestAnimationFrame(() => requestAnimationFrame(() => { | |
| toast.classList.add('show'); | |
| const bar = toast.querySelector('.toast-progress'); | |
| if (bar) requestAnimationFrame(() => { bar.style.width = '0%'; }); | |
| })); | |
| const timer = setTimeout(() => { | |
| toast.classList.add('hide'); | |
| toast.addEventListener('transitionend', () => toast.remove(), { once: true }); | |
| }, duration); | |
| toast.querySelector('.toast-close').addEventListener('click', () => clearTimeout(timer)); | |
| } | |
| </script> | |
| <script> | |
| function confirmLogout() { | |
| executeLogout(); | |
| } | |
| function executeLogout() { | |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| </script> | |
| <script> | |
| function checkAdminAccess() { | |
| api.getSession().then(r => { if (!r.success) { window.location.href = 'auth-login.html'; } else if (r.user.role !== 'Admin') { alert('Acceso denegado: Solo administradores'); window.location.href = 'index.html'; } }).catch(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| function renderUsersTableSecure(users) { | |
| const tbody = $('#usersTableBody'); | |
| tbody.empty(); | |
| api.getSession().then(function(currentUserResponse) { if (!currentUserResponse.success) return; const currentUserId = currentUserResponse.user.userID; | |
| users.forEach(user => { | |
| const canDelete = user.userID !== currentUserId; | |
| const deleteButton = canDelete ? | |
| `<button class="btn btn-sm btn-outline-danger" | |
| onclick="confirmDeleteUser(${user.userID})" | |
| title="Eliminar"> | |
| <i data-feather="trash-2" class="fe-12"></i> | |
| </button>` : | |
| `<button class="btn btn-sm btn-outline-secondary" | |
| disabled | |
| title="No puedes eliminar tu propia cuenta"> | |
| <i data-feather="user-x" class="fe-12"></i> | |
| </button>`; | |
| const row = ` | |
| <tr> | |
| <td>${user.userID}</td> | |
| <td> | |
| ${user.username} | |
| ${user.userID === currentUserId ? '<span class="badge badge-info ml-2">Tú</span>' : ''} | |
| </td> | |
| <td> | |
| <span class="badge ${user.role === 'Admin' ? 'badge-primary' : 'badge-secondary'}"> | |
| ${user.role} | |
| </span> | |
| </td> | |
| <td>${user.creationDate}</td> | |
| <td class="text-center"> | |
| <div class="btn-group" role="group"> | |
| <button class="btn btn-sm btn-outline-primary" | |
| onclick="editUser(${user.userID})" | |
| title="Editar"> | |
| <i data-feather="edit" class="fe-12"></i> | |
| </button> | |
| ${deleteButton} | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| tbody.append(row); | |
| }); | |
| }); | |
| } | |
| function confirmDeleteUserSecure(userId) { | |
| api.getSession().then(function(response) { if (!response.success) { showAlert('error', 'Error de autenticación'); return; } if (response.user.userID === userId) { | |
| showAlert('error', 'No puedes eliminar tu propia cuenta'); | |
| return; | |
| } | |
| const confirmation = confirm( | |
| 'ELIMINAR USUARIO\n\n' + | |
| '¿Estás seguro que deseas eliminar este usuario?\n\n' + | |
| '• Esta acción NO se puede deshacer\n' + | |
| '• Se perderán todos los datos asociados\n' + | |
| '• El usuario no podrá acceder al sistema\n\n' + | |
| 'Escribe "CONFIRMAR" para continuar:' | |
| ); | |
| if (confirmation) { | |
| const secondConfirmation = prompt( | |
| 'Para confirmar la eliminación, escribe: CONFIRMAR' | |
| ); | |
| if (secondConfirmation === 'CONFIRMAR') { | |
| deleteUser(userId); | |
| } else { | |
| showAlert('info', 'Eliminación cancelada - Confirmación incorrecta'); | |
| } | |
| } | |
| }); | |
| } | |
| $(document).ready(function() { | |
| checkAdminAccess(); | |
| setTimeout(() => { | |
| checkAuthentication(); | |
| loadUsers(); | |
| $('#logoutBtn').on('click', function(e) { | |
| e.preventDefault(); | |
| if (confirm('¿Estás seguro que deseas cerrar sesión?')) { | |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| }); | |
| }, 500); // Pequeño delay para asegurar que la verificación de admin se complete | |
| }); | |
| window.renderUsersTable = renderUsersTableSecure; | |
| window.confirmDeleteUser = confirmDeleteUserSecure; | |
| </script> | |
| <script> | |
| (function() { | |
| function centerWindow() { | |
| try { | |
| const screenWidth = window.screen.availWidth; | |
| const screenHeight = window.screen.availHeight; | |
| const x = Math.floor((screenWidth - 1200) / 2); | |
| const y = Math.floor((screenHeight - 800) / 2); | |
| if (window.moveTo) { | |
| window.moveTo(Math.max(0, x), Math.max(0, y)); | |
| } | |
| } catch (e) { | |
| console.warn('No se pudo centrar ventana:', e); | |
| } | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', centerWindow); | |
| } else { | |
| centerWindow(); | |
| } | |
| window.addEventListener('load', centerWindow); | |
| window.addEventListener('focus', centerWindow); | |
| })(); | |
| </script> | |
| <script> | |
| // Feather Icons: reemplaza <i data-feather="X"> con SVG inline | |
| // Inmune a problemas de fuentes locales | |
| function _initFeather() { | |
| if (typeof feather !== 'undefined') feather.replace({ 'stroke-width': 1.8 }); | |
| } | |
| document.addEventListener('DOMContentLoaded', _initFeather); | |
| // Re-render al inyectar HTML dinámico (modals, cards, tablas) | |
| var _fObserver = new MutationObserver(function(mutations) { | |
| var needsRefresh = mutations.some(function(m) { | |
| return m.addedNodes.length > 0; | |
| }); | |
| if (needsRefresh) _initFeather(); | |
| }); | |
| document.addEventListener('DOMContentLoaded', function() { | |
| _fObserver.observe(document.body, { childList: true, subtree: true }); | |
| }); | |
| </script> | |
| <script> | |
| (function () { | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'theme-overlay'; | |
| overlay.style.cssText = [ | |
| 'position:fixed','inset:0','z-index:99999', | |
| 'pointer-events:none','opacity:0', | |
| 'transition:opacity 0.18s ease','background:#fff' | |
| ].join(';'); | |
| document.body.appendChild(overlay); | |
| function switchTheme() { | |
| const lightTheme = document.getElementById('lightTheme'); | |
| const darkTheme = document.getElementById('darkTheme'); | |
| const goingDark = !darkTheme.disabled; | |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; | |
| overlay.style.opacity = '1'; | |
| setTimeout(function () { | |
| lightTheme.disabled = !lightTheme.disabled; | |
| darkTheme.disabled = !darkTheme.disabled; | |
| setTimeout(function () { overlay.style.opacity = '0'; }, 50); | |
| }, 180); | |
| } | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const btn = document.getElementById('modeSwitcher'); | |
| if (!btn) return; | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| switchTheme(); | |
| }); | |
| }); | |
| })(); | |
| </script> | |
| <script> | |
| (function () { | |
| // 1. Aplicar tema guardado al cargar | |
| function applyTheme(theme) { | |
| const lightTheme = document.getElementById('lightTheme'); | |
| const darkTheme = document.getElementById('darkTheme'); | |
| if (theme === 'dark') { | |
| lightTheme.disabled = true; | |
| darkTheme.disabled = false; | |
| } else { | |
| lightTheme.disabled = false; | |
| darkTheme.disabled = true; | |
| } | |
| } | |
| applyTheme(localStorage.getItem('theme') || 'light'); | |
| // 2. Escuchar cambios desde OTRAS pestañas | |
| window.addEventListener('storage', function(e) { | |
| if (e.key === 'theme') { | |
| applyTheme(e.newValue); | |
| } | |
| }); | |
| // 3. Overlay de transición | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'theme-overlay'; | |
| overlay.style.cssText = [ | |
| 'position:fixed','inset:0','z-index:99999', | |
| 'pointer-events:none','opacity:0', | |
| 'transition:opacity 0.18s ease','background:#fff' | |
| ].join(';'); | |
| document.body.appendChild(overlay); | |
| function switchTheme() { | |
| const darkTheme = document.getElementById('darkTheme'); | |
| const goingDark = darkTheme.disabled; // si está disabled, vamos a dark | |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; | |
| overlay.style.opacity = '1'; | |
| setTimeout(function () { | |
| const newTheme = goingDark ? 'dark' : 'light'; | |
| applyTheme(newTheme); | |
| localStorage.setItem('theme', newTheme); // 👈 esto dispara el evento en otras pestañas | |
| setTimeout(function () { overlay.style.opacity = '0'; }, 50); | |
| }, 180); | |
| } | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const btn = document.getElementById('modeSwitcher'); | |
| if (!btn) return; | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| switchTheme(); | |
| }); | |
| }); | |
| })(); | |
| (function() { | |
| var sidebar = document.getElementById('leftSidebar'); | |
| var logo = document.getElementById('sidebarLogo'); | |
| var logoMini = document.getElementById('sidebarLogoMini'); | |
| if (!sidebar || !logo || !logoMini) return; | |
| function updateLogo() { | |
| var isCollapsed = sidebar.classList.contains('collapsed') | |
| || document.body.classList.contains('collapsed') | |
| || sidebar.offsetWidth < 100; | |
| logo.style.display = isCollapsed ? 'none' : ''; | |
| logoMini.style.display = isCollapsed ? 'block' : 'none'; | |
| } | |
| var observer = new MutationObserver(updateLogo); | |
| observer.observe(sidebar, { attributes: true, attributeFilter: ['class', 'style'] }); | |
| observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); | |
| updateLogo(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |