Initial commit — multi-tenant filtering, port constraints, chart bbox

This commit is contained in:
2026-05-04 22:41:09 -04:00
parent c3b07be67e
commit fcf1d2787a
1102 changed files with 7353 additions and 1166 deletions
+111 -19
View File
@@ -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) {