Initial commit — multi-tenant filtering, port constraints, chart bbox
This commit is contained in:
+111
-19
@@ -60,7 +60,15 @@ document.querySelectorAll('.dd-item').forEach(item => {
|
||||
openRecordingsModal();
|
||||
break;
|
||||
case 'ais-history':
|
||||
openRecordingsModal();
|
||||
if (window.openTrackHistoryModal) openTrackHistoryModal();
|
||||
else openRecordingsModal();
|
||||
break;
|
||||
case 'org-management':
|
||||
if (window.Auth?.isSuperAdmin() || window.Auth?.isAdmin()) {
|
||||
if (window.openOrgModal) openOrgModal();
|
||||
} else {
|
||||
alert('Admin access required.');
|
||||
}
|
||||
break;
|
||||
case 'export-data':
|
||||
alert('CSV export coming soon.');
|
||||
@@ -86,12 +94,24 @@ async function loadUsersList() {
|
||||
if (res.status === 401) { body.innerHTML = '<div style="color:var(--red);padding:8px">Session expired — please log in again (F5).</div>'; return; }
|
||||
const data = await res.json();
|
||||
const users = Array.isArray(data) ? data : [];
|
||||
// Also fetch companies to resolve company_id → name
|
||||
let companyMap = {};
|
||||
try {
|
||||
const cr = await fetch(`${API}/org/companies`,
|
||||
{ headers: { Authorization: `Bearer ${window.Auth.token()}` } });
|
||||
if (cr.ok) {
|
||||
const cs = await cr.json();
|
||||
cs.forEach(c => { companyMap[c.id] = c.name; });
|
||||
window._orgPortsCache = window._orgPortsCache || []; // used by company hint
|
||||
}
|
||||
} catch {}
|
||||
|
||||
body.innerHTML = `
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>USERNAME</th><th>FULL NAME</th><th>EMAIL</th>
|
||||
<th>ROLE</th><th>STATUS</th><th></th>
|
||||
<th>ROLE</th><th>COMPANY</th><th>STATUS</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -101,9 +121,10 @@ async function loadUsersList() {
|
||||
<td>${u.nombre}</td>
|
||||
<td class="mono" style="font-size:0.72rem">${u.email || '--'}</td>
|
||||
<td><span class="session-role ${roleClass(u.role)}">${u.role}</span></td>
|
||||
<td style="font-size:0.72rem;color:var(--text-muted)">${u.company_id ? (companyMap[u.company_id] || u.company_id) : '--'}</td>
|
||||
<td><span style="color:${u.activo ? 'var(--green)' : 'var(--red)'}">${u.activo ? 'ACTIVE' : 'INACTIVE'}</span></td>
|
||||
<td style="display:flex;gap:4px;justify-content:flex-end">
|
||||
<button class="chart-row-btn" onclick="editUser('${u.username}','${u.nombre}','${u.email||''}','${u.role}',${u.activo})">EDIT</button>
|
||||
<button class="chart-row-btn" onclick="editUser('${u.username}','${u.nombre}','${u.email||''}','${u.role}','${u.company_id||''}',${u.activo})">EDIT</button>
|
||||
${u.username !== 'admin' ? `<button class="chart-row-btn danger" onclick="deleteUser('${u.username}')">DELETE</button>` : ''}
|
||||
</td>
|
||||
</tr>`).join('')}
|
||||
@@ -116,11 +137,65 @@ async function loadUsersList() {
|
||||
}
|
||||
|
||||
function roleClass(role) {
|
||||
return { SUPERADMIN:'role-superadmin', ADMIN:'role-admin', USER:'role-user' }[role] || 'role-user';
|
||||
return {
|
||||
SUPERADMIN: 'role-superadmin',
|
||||
ADMIN: 'role-admin',
|
||||
CLIENT_ADMIN: 'role-admin', // same cyan badge as admin but label differs
|
||||
USER: 'role-user',
|
||||
}[role] || 'role-user';
|
||||
}
|
||||
|
||||
// ── MODAL CREAR USUARIO ───────────────────────────────────────────────────────
|
||||
function openUserForm() {
|
||||
let _ufCompanies = []; // cached list from /org/companies
|
||||
|
||||
async function _loadCompaniesForForm() {
|
||||
try {
|
||||
const r = await fetch(`${API}/org/companies`,
|
||||
{ headers: { Authorization: `Bearer ${window.Auth?.token()}` } });
|
||||
_ufCompanies = await r.json();
|
||||
} catch { _ufCompanies = []; }
|
||||
|
||||
const sel = document.getElementById('uf-company');
|
||||
sel.innerHTML = '<option value="">— Select company —</option>' +
|
||||
_ufCompanies.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
||||
}
|
||||
|
||||
function _ufToggleCompanyRow() {
|
||||
const role = document.getElementById('uf-role').value;
|
||||
const row = document.getElementById('uf-company-row');
|
||||
// Company field required for client roles (USER and CLIENT_ADMIN)
|
||||
row.style.display = (role === 'USER' || role === 'CLIENT_ADMIN') ? '' : 'none';
|
||||
}
|
||||
|
||||
document.getElementById('uf-role').addEventListener('change', _ufToggleCompanyRow);
|
||||
|
||||
// Auto-suggest username & full-name when a company is selected
|
||||
document.getElementById('uf-company').addEventListener('change', () => {
|
||||
const cid = document.getElementById('uf-company').value;
|
||||
const company = _ufCompanies.find(c => c.id === cid);
|
||||
const hint = document.getElementById('uf-company-hint');
|
||||
if (!company) { hint.textContent = ''; return; }
|
||||
|
||||
// Port name for the hint
|
||||
const portName = company.port_id
|
||||
? (window._orgPortsCache?.find?.(p => p.id === company.port_id)?.name || company.port_id)
|
||||
: '';
|
||||
hint.textContent = portName ? `Home port: ${portName}` : '';
|
||||
|
||||
// Auto-fill username: slug from company name (lowercase, no spaces)
|
||||
const slug = company.name.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
|
||||
const usernameEl = document.getElementById('uf-username');
|
||||
if (!usernameEl.readOnly && !usernameEl.value) {
|
||||
usernameEl.value = slug;
|
||||
}
|
||||
// Auto-fill full name if empty
|
||||
const nombreEl = document.getElementById('uf-nombre');
|
||||
if (!nombreEl.value) {
|
||||
nombreEl.value = company.name + (portName ? ` — ${portName}` : '');
|
||||
}
|
||||
});
|
||||
|
||||
async function openUserForm() {
|
||||
document.getElementById('user-form-title').textContent = 'CREATE USER';
|
||||
document.getElementById('uf-username').value = '';
|
||||
document.getElementById('uf-username').readOnly = false;
|
||||
@@ -128,23 +203,33 @@ function openUserForm() {
|
||||
document.getElementById('uf-email').value = '';
|
||||
document.getElementById('uf-password').value = '';
|
||||
document.getElementById('uf-role').value = 'USER';
|
||||
document.getElementById('uf-company').value = '';
|
||||
document.getElementById('uf-company-hint').textContent = '';
|
||||
document.getElementById('uf-error').classList.add('hidden');
|
||||
document.getElementById('btn-uf-save').dataset.editMode = '';
|
||||
_ufToggleCompanyRow();
|
||||
await _loadCompaniesForForm();
|
||||
_showModal('modal-user-form');
|
||||
}
|
||||
|
||||
document.getElementById('btn-user-new').addEventListener('click', openUserForm);
|
||||
|
||||
window.editUser = function(username, nombre, email, role, activo) {
|
||||
window.editUser = function(username, nombre, email, role, company_id, activo) {
|
||||
document.getElementById('user-form-title').textContent = `EDIT — ${username}`;
|
||||
document.getElementById('uf-username').value = username;
|
||||
document.getElementById('uf-username').value = username;
|
||||
document.getElementById('uf-username').readOnly = true;
|
||||
document.getElementById('uf-nombre').value = nombre;
|
||||
document.getElementById('uf-email').value = email;
|
||||
document.getElementById('uf-password').value = '';
|
||||
document.getElementById('uf-role').value = role;
|
||||
document.getElementById('uf-nombre').value = nombre;
|
||||
document.getElementById('uf-email').value = email;
|
||||
document.getElementById('uf-password').value = '';
|
||||
document.getElementById('uf-role').value = role;
|
||||
document.getElementById('uf-company').value = company_id || '';
|
||||
document.getElementById('uf-company-hint').textContent = '';
|
||||
document.getElementById('uf-error').classList.add('hidden');
|
||||
document.getElementById('btn-uf-save').dataset.editMode = '1';
|
||||
_ufToggleCompanyRow();
|
||||
_loadCompaniesForForm().then(() => {
|
||||
document.getElementById('uf-company').value = company_id || '';
|
||||
});
|
||||
_showModal('modal-user-form');
|
||||
};
|
||||
|
||||
@@ -161,15 +246,22 @@ window.deleteUser = async function(username) {
|
||||
};
|
||||
|
||||
document.getElementById('btn-uf-save').addEventListener('click', async () => {
|
||||
const username = document.getElementById('uf-username').value.trim();
|
||||
const nombre = document.getElementById('uf-nombre').value.trim();
|
||||
const email = document.getElementById('uf-email').value.trim();
|
||||
const password = document.getElementById('uf-password').value;
|
||||
const role = document.getElementById('uf-role').value;
|
||||
const errEl = document.getElementById('uf-error');
|
||||
const username = document.getElementById('uf-username').value.trim();
|
||||
const nombre = document.getElementById('uf-nombre').value.trim();
|
||||
const email = document.getElementById('uf-email').value.trim();
|
||||
const password = document.getElementById('uf-password').value;
|
||||
const role = document.getElementById('uf-role').value;
|
||||
const company_id = document.getElementById('uf-company').value || null;
|
||||
const errEl = document.getElementById('uf-error');
|
||||
|
||||
const isEdit = document.getElementById('btn-uf-save').dataset.editMode === '1';
|
||||
|
||||
// Company required for USER role
|
||||
if (role === 'USER' && !company_id) {
|
||||
errEl.textContent = 'Select a company for USER accounts.';
|
||||
errEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
if (!isEdit && (!username || !nombre || !password)) {
|
||||
errEl.textContent = 'Username, full name and password are required.';
|
||||
errEl.classList.remove('hidden');
|
||||
@@ -184,7 +276,7 @@ document.getElementById('btn-uf-save').addEventListener('click', async () => {
|
||||
try {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
const body = { nombre, email: email || null, role };
|
||||
const body = { nombre, email: email || null, role, company_id };
|
||||
if (password) body.password = password;
|
||||
res = await fetch(`${API}/auth/users/${username}`, {
|
||||
method: 'PUT',
|
||||
@@ -195,7 +287,7 @@ document.getElementById('btn-uf-save').addEventListener('click', async () => {
|
||||
res = await fetch(`${API}/auth/users`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.Auth.token()}` },
|
||||
body: JSON.stringify({ username, nombre, email: email || null, password, role }),
|
||||
body: JSON.stringify({ username, nombre, email: email || null, password, role, company_id }),
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user