Files
boc/aamos-admin-upgrade/dist/users.html
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

451 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AAMOS Admin — Users</title>
<link rel="stylesheet" href="assets/admin.css">
<style>
.users-toolbar {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.search-box {
flex: 1;
min-width: 200px;
padding: 10px 14px;
border: 1px solid var(--separator);
border-radius: var(--radius-sm);
background: var(--bg-input);
color: var(--text-primary);
font-size: 15px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: var(--radius-sm);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.btn:hover { opacity: 0.85; }
.btn-primary { background: var(--primary); color: white; }
.btn-danger { background: var(--danger); color: white; }
.users-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.users-table th {
text-align: left;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--text-tertiary);
padding: 12px 16px;
border-bottom: 1px solid var(--separator);
background: var(--bg-input);
}
.users-table td {
padding: 14px 16px;
border-bottom: 1px solid var(--separator);
}
.users-table tr:hover { background: var(--bg-input); }
.user-info-cell {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar-sm {
width: 36px; height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--purple));
display: flex; align-items: center; justify-content: center;
color: white; font-weight: 700; font-size: 13px;
}
.user-details { line-height: 1.3; }
.user-name-sm { font-weight: 600; }
.user-email-sm { font-size: 12px; color: var(--text-tertiary); }
.role-badge {
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
display: inline-block;
}
.role-admin { background: rgba(0,122,255,0.12); color: var(--primary); }
.role-finance { background: rgba(255,149,0,0.12); color: var(--warning); }
.role-operations { background: rgba(52,199,89,0.12); color: var(--success); }
.role-viewer { background: var(--bg-input); color: var(--text-tertiary); }
.actions-cell { display: flex; gap: 8px; }
.icon-btn-sm {
width: 32px; height: 32px;
border: none;
border-radius: 8px;
background: var(--bg-input);
cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.icon-btn-sm:hover { background: var(--separator); }
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 20px;
}
.page-btn {
padding: 8px 14px;
border: 1px solid var(--separator);
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-primary);
cursor: pointer;
}
.page-btn.active { background: var(--primary); color: white; border-color: var(--primary); }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
}
.modal-overlay.active { display: flex; }
.modal {
background: var(--bg-card);
border-radius: var(--radius);
padding: 24px;
width: 90%;
max-width: 480px;
box-shadow: var(--shadow-md);
}
.modal-title { font-size: 20px; font-weight: 700; margin-bottom: 20px; }
.form-group { margin-bottom: 16px; }
.form-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--text-secondary); }
.form-input {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--separator);
border-radius: var(--radius-sm);
background: var(--bg-input);
color: var(--text-primary);
font-size: 15px;
}
.form-select {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--separator);
border-radius: var(--radius-sm);
background: var(--bg-input);
color: var(--text-primary);
font-size: 15px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.btn-secondary {
background: var(--bg-input);
color: var(--text-primary);
}
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18L20 8.5v7L12 19.82 4 15.5v-7l8-4.32z"/></svg>
</div>
<div class="brand-text">
<div class="brand-name">AAMOS Admin</div>
<div class="brand-sub">Unified v2.0</div>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a class="nav-item" href="unified-dashboard.html">
<span class="nav-icon">📊</span>
Dashboard
</a>
<a class="nav-item active" href="users.html">
<span class="nav-icon">👥</span>
Users
</a>
<a class="nav-item" href="modules.html">
<span class="nav-icon">🔧</span>
Modules
</a>
<div class="nav-section-label">Systems</div>
<a class="nav-item" href="quixzoom.html">
<span class="nav-icon">📸</span>
quiXzoom
</a>
<a class="nav-item" href="landvex.html">
<span class="nav-icon">🌍</span>
LandveX
</a>
<a class="nav-item" href="economy.html">
<span class="nav-icon">💰</span>
Economy
</a>
<a class="nav-item" href="crm.html">
<span class="nav-icon">🤝</span>
CRM
</a>
<div class="nav-section-label">System</div>
<a class="nav-item" href="audit.html">
<span class="nav-icon">📋</span>
Audit Log
</a>
<a class="nav-item" href="settings.html">
<span class="nav-icon">⚙️</span>
Settings
</a>
</nav>
</aside>
<div class="main">
<header class="topbar">
<div class="topbar-title">Users</div>
<div class="topbar-actions">
<button class="btn btn-primary" id="add-user-btn">+ Add User</button>
</div>
</header>
<section class="page-content active">
<div class="users-toolbar">
<input type="text" class="search-box" id="search-input" placeholder="Search users...">
</div>
<div class="card">
<table class="users-table">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Created</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-tbody">
<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-tertiary)">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</section>
</div>
<!-- Add/Edit Modal -->
<div class="modal-overlay" id="user-modal">
<div class="modal">
<div class="modal-title" id="modal-title">Add User</div>
<form id="user-form">
<input type="hidden" id="user-id">
<div class="form-group">
<label class="form-label">Name</label>
<input type="text" class="form-input" id="user-name" required>
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" id="user-email" required>
</div>
<div class="form-group">
<label class="form-label">Role</label>
<select class="form-select" id="user-role">
<option value="admin">Admin</option>
<option value="finance">Finance</option>
<option value="operations">Operations</option>
<option value="viewer">Viewer</option>
</select>
</div>
<div class="form-group" id="password-group">
<label class="form-label">Password</label>
<input type="password" class="form-input" id="user-password" placeholder="Min 8 characters">
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="cancel-btn">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<script src="assets/api.js"></script>
<script>
'use strict';
const api = createAdminAPI(window.location.origin);
let currentPage = 1;
let currentUsers = [];
function formatDate(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('sv-SE');
}
function getInitials(name) {
return name?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) || '??';
}
function getRoleClass(role) {
const map = { admin: 'role-admin', finance: 'role-finance', operations: 'role-operations', viewer: 'role-viewer' };
return map[role] || 'role-viewer';
}
async function loadUsers(page = 1, search = '') {
try {
const data = await api.getUsers(page, 25, search);
currentUsers = data.users || [];
renderUsers(currentUsers);
renderPagination(data.total, data.page, data.limit);
} catch (err) {
document.getElementById('users-tbody').innerHTML =
`<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--danger)">Error: ${err.message}</td></tr>`;
}
}
function renderUsers(users) {
const tbody = document.getElementById('users-tbody');
if (!users?.length) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-tertiary)">No users found</td></tr>`;
return;
}
tbody.innerHTML = users.map(u => `
<tr data-id="${u.id}">
<td>
<div class="user-info-cell">
<div class="user-avatar-sm">${getInitials(u.name)}</div>
<div class="user-details">
<div class="user-name-sm">${u.name || 'Unnamed'}</div>
<div class="user-email-sm">${u.email}</div>
</div>
</div>
</td>
<td><span class="role-badge ${getRoleClass(u.role)}">${u.role}</span></td>
<td>${formatDate(u.created_at)}</td>
<td>${formatDate(u.last_login)}</td>
<td>
<div class="actions-cell">
<button class="icon-btn-sm" onclick="editUser('${u.id}')" title="Edit">✏️</button>
<button class="icon-btn-sm" onclick="deleteUser('${u.id}')" title="Delete">🗑️</button>
</div>
</td>
</tr>
`).join('');
}
function renderPagination(total, page, limit) {
const totalPages = Math.ceil(total / limit) || 1;
const container = document.getElementById('pagination');
let html = `
<button class="page-btn" ${page <= 1 ? 'disabled' : ''} onclick="loadUsers(${page - 1})">←</button>
`;
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) {
html += `<button class="page-btn ${i === page ? 'active' : ''}" onclick="loadUsers(${i})">${i}</button>`;
} else if (i === page - 2 || i === page + 2) {
html += `<span style="padding:8px;color:var(--text-tertiary)">…</span>`;
}
}
html += `<button class="page-btn" ${page >= totalPages ? 'disabled' : ''} onclick="loadUsers(${page + 1})">→</button>`;
container.innerHTML = html;
}
// Modal
const modal = document.getElementById('user-modal');
const form = document.getElementById('user-form');
document.getElementById('add-user-btn').addEventListener('click', () => {
document.getElementById('modal-title').textContent = 'Add User';
document.getElementById('user-id').value = '';
form.reset();
document.getElementById('password-group').style.display = '';
modal.classList.add('active');
});
document.getElementById('cancel-btn').addEventListener('click', () => {
modal.classList.remove('active');
});
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.classList.remove('active');
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('user-id').value;
const data = {
name: document.getElementById('user-name').value,
email: document.getElementById('user-email').value,
role: document.getElementById('user-role').value,
};
try {
if (id) {
await api.updateUser(id, data);
} else {
data.password = document.getElementById('user-password').value;
await api.createUser(data);
}
modal.classList.remove('active');
loadUsers(currentPage);
} catch (err) {
alert('Error: ' + err.message);
}
});
window.editUser = (id) => {
const u = currentUsers.find(x => x.id === id);
if (!u) return;
document.getElementById('modal-title').textContent = 'Edit User';
document.getElementById('user-id').value = u.id;
document.getElementById('user-name').value = u.name || '';
document.getElementById('user-email').value = u.email;
document.getElementById('user-role').value = u.role;
document.getElementById('password-group').style.display = 'none';
modal.classList.add('active');
};
window.deleteUser = async (id) => {
if (!confirm('Delete this user?')) return;
try {
await api.deleteUser(id);
loadUsers(currentPage);
} catch (err) {
alert('Error: ' + err.message);
}
};
document.getElementById('search-input').addEventListener('input', (e) => {
loadUsers(1, e.target.value);
});
loadUsers();
</script>
</body>
</html>