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

@@ -118,6 +118,9 @@ function showSection(sectionId) {
case 'diagrams':
// Just show section, user uses search
break;
case 'users':
loadUsers();
break;
}
}
@@ -1226,18 +1229,18 @@ function renderPagination(containerId, pagination, pageKey, loadFunction) {
let html = '';
// 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
const startPage = Math.max(1, page - 2);
const endPage = Math.min(total_pages, page + 2);
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
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;
}
@@ -1967,3 +1970,107 @@ async function deleteHotspot(hotspotId) {
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');
}
}