feat: add users management tab to admin panel

New Sistema > Usuarios section with user listing, role badges
(ADMIN=blue, OWNER=purple, TALLER=green, BODEGA=orange),
activate/deactivate toggle, and pending users badge count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 22:25:09 +00:00
parent 6c6a9eecd6
commit e5d074687a
2 changed files with 148 additions and 3 deletions

View File

@@ -668,6 +668,15 @@
<span>Exportar CSV</span> <span>Exportar CSV</span>
</div> </div>
</div> </div>
<div class="sidebar-section">
<h3>Sistema</h3>
<div class="sidebar-item" data-section="users">
<span class="icon">👤</span>
<span>Usuarios</span>
<span class="badge" id="pendingUsersBadge" style="display:none; background:var(--warning); color:#000; font-size:0.7rem; padding:2px 6px; border-radius:10px; margin-left:auto;"></span>
</div>
</div>
</aside> </aside>
<!-- Main Content --> <!-- Main Content -->
@@ -1207,6 +1216,35 @@
</div> </div>
</div> </div>
</section> </section>
<!-- Users Section -->
<section id="section-users" class="admin-section">
<div class="page-header">
<h1 class="page-title">Usuarios</h1>
</div>
<div class="card">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Email</th>
<th>Negocio</th>
<th>Rol</th>
<th>Activo</th>
<th>Último Login</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="usersTable">
<tr><td colspan="7" class="loading"><div class="spinner"></div></td></tr>
</tbody>
</table>
</div>
</div>
</section>
</main> </main>
</div> </div>

View File

@@ -118,6 +118,9 @@ function showSection(sectionId) {
case 'diagrams': case 'diagrams':
// Just show section, user uses search // Just show section, user uses search
break; break;
case 'users':
loadUsers();
break;
} }
} }
@@ -1226,18 +1229,18 @@ function renderPagination(containerId, pagination, pageKey, loadFunction) {
let html = ''; let html = '';
// Previous button // Previous button
html += `<button ${page <= 1 ? 'disabled' : ''} onclick="goToPage('${pageKey}', ${page - 1}, ${loadFunction.name})">← Anterior</button>`; html += `<button ${page <= 1 ? 'disabled' : ''} onclick="goToPage('${pageKey}', ${page - 1}, '${loadFunction.name}')">← Anterior</button>`;
// Page numbers // Page numbers
const startPage = Math.max(1, page - 2); const startPage = Math.max(1, page - 2);
const endPage = Math.min(total_pages, page + 2); const endPage = Math.min(total_pages, page + 2);
for (let i = startPage; i <= endPage; i++) { for (let i = startPage; i <= endPage; i++) {
html += `<button class="${i === page ? 'active' : ''}" onclick="goToPage('${pageKey}', ${i}, ${loadFunction.name})">${i}</button>`; html += `<button class="${i === page ? 'active' : ''}" onclick="goToPage('${pageKey}', ${i}, '${loadFunction.name}')">${i}</button>`;
} }
// Next button // Next button
html += `<button ${page >= total_pages ? 'disabled' : ''} onclick="goToPage('${pageKey}', ${page + 1}, ${loadFunction.name})">Siguiente →</button>`; html += `<button ${page >= total_pages ? 'disabled' : ''} onclick="goToPage('${pageKey}', ${page + 1}, '${loadFunction.name}')">Siguiente →</button>`;
container.innerHTML = html; container.innerHTML = html;
} }
@@ -1967,3 +1970,107 @@ async function deleteHotspot(hotspotId) {
showAlert(e.message, 'error'); showAlert(e.message, 'error');
} }
} }
// ============================================================================
// User Management
// ============================================================================
const roleBadgeColors = {
ADMIN: '#3b82f6',
OWNER: '#8b5cf6',
TALLER: '#22c55e',
BODEGA: '#f59e0b'
};
function formatDate(dateStr) {
if (!dateStr) return '<span style="color:var(--text-secondary)">Nunca</span>';
var d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
return d.toLocaleDateString('es-MX', { year: 'numeric', month: 'short', day: 'numeric' })
+ ' ' + d.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
}
function getRoleBadge(role) {
var color = roleBadgeColors[role] || '#6b7280';
return '<span style="background:' + color + '; color:#fff; padding:2px 8px; border-radius:10px; font-size:0.75rem; font-weight:600;">' + (role || 'N/A') + '</span>';
}
function getActiveBadge(isActive) {
if (isActive) {
return '<span style="background:var(--success); color:#000; padding:2px 8px; border-radius:10px; font-size:0.75rem; font-weight:600;">Activo</span>';
}
return '<span style="background:#ef4444; color:#fff; padding:2px 8px; border-radius:10px; font-size:0.75rem; font-weight:600;">Inactivo</span>';
}
async function loadUsers() {
var token = localStorage.getItem('access_token');
var tbody = document.getElementById('usersTable');
tbody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div></td></tr>';
try {
var res = await fetch('/api/admin/users', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (!res.ok) throw new Error('Error al cargar usuarios (' + res.status + ')');
var data = await res.json();
var users = Array.isArray(data) ? data : (data.data || []);
// Update pending badge
var pending = users.filter(function(u) { return !u.is_active; }).length;
var badge = document.getElementById('pendingUsersBadge');
if (badge) {
if (pending > 0) {
badge.textContent = pending;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
}
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; color:var(--text-secondary); padding:2rem;">No hay usuarios registrados</td></tr>';
return;
}
tbody.innerHTML = users.map(function(u) {
var toggleLabel = u.is_active ? 'Desactivar' : 'Activar';
var toggleClass = u.is_active ? 'btn-secondary' : 'btn-primary';
return '<tr>' +
'<td>' + (u.name || u.nombre || '-') + '</td>' +
'<td>' + (u.email || '-') + '</td>' +
'<td>' + (u.business_name || u.negocio || '-') + '</td>' +
'<td>' + getRoleBadge(u.role || u.rol) + '</td>' +
'<td>' + getActiveBadge(u.is_active) + '</td>' +
'<td>' + formatDate(u.last_login || u.ultimo_login) + '</td>' +
'<td><button class="btn ' + toggleClass + '" style="font-size:0.8rem; padding:4px 10px;" onclick="toggleUserActive(' + u.id + ', ' + u.is_active + ')">' + toggleLabel + '</button></td>' +
'</tr>';
}).join('');
} catch (e) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; color:#ef4444; padding:2rem;">' + e.message + '</td></tr>';
}
}
async function toggleUserActive(userId, currentActive) {
var token = localStorage.getItem('access_token');
var action = currentActive ? 'desactivar' : 'activar';
if (!confirm('¿Seguro que deseas ' + action + ' este usuario?')) return;
try {
var res = await fetch('/api/admin/users/' + userId + '/activate', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ is_active: !currentActive })
});
if (!res.ok) {
var err = await res.json();
throw new Error(err.error || 'Error al actualizar usuario');
}
showAlert('Usuario ' + (currentActive ? 'desactivado' : 'activado') + ' correctamente');
loadUsers();
} catch (e) {
showAlert(e.message, 'error');
}
}