6de2455917
- 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
451 lines
14 KiB
HTML
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>
|