'use strict';
const API = 'http://localhost:5503';
const SESSION_KEY = 'ams_session';
// ── Estado de sesión ────────────────────────────────────────────────────────
const Auth = {
session: null,
load() {
try {
const raw = localStorage.getItem(SESSION_KEY);
this.session = raw ? JSON.parse(raw) : null;
} catch { this.session = null; }
this._renderBadge();
},
save(data) {
this.session = data;
localStorage.setItem(SESSION_KEY, JSON.stringify(data));
this._renderBadge();
},
clear() {
this.session = null;
localStorage.removeItem(SESSION_KEY);
this._renderBadge();
},
isLoggedIn() { return !!this.session?.token; },
isAdmin() { return ['ADMIN','SUPERADMIN'].includes(this.session?.role); },
isSuperAdmin(){ return this.session?.role === 'SUPERADMIN'; },
token() { return this.session?.token || ''; },
_renderBadge() {
const el = document.getElementById('session-badge');
if (!el) return;
if (!this.session) {
el.innerHTML = `NOT LOGGED IN`;
el.title = 'Click to login';
el.onclick = () => Modal.openLogin(null);
} else {
const roleClass = {
SUPERADMIN: 'role-superadmin',
ADMIN: 'role-admin',
USER: 'role-user',
}[this.session.role] || 'role-user';
el.innerHTML = `
${this.session.nombre}
${this.session.role}
`;
el.title = 'Click to logout';
el.onclick = () => {
if (confirm(`Logout as ${this.session.nombre}?`)) Auth.clear();
};
}
},
};
// ── Modal controller ────────────────────────────────────────────────────────
const Modal = {
_pendingAid: null,
openLogin(aidData) {
this._pendingAid = aidData;
document.getElementById('login-user').value = '';
document.getElementById('login-pass').value = '';
document.getElementById('login-error').classList.add('hidden');
_showOverlay('modal-login');
setTimeout(() => document.getElementById('login-user').focus(), 100);
},
closeLogin() {
_hideOverlay();
this._pendingAid = null;
},
async submitLogin() {
const username = document.getElementById('login-user').value.trim();
const password = document.getElementById('login-pass').value;
const errEl = document.getElementById('login-error');
const btn = document.getElementById('btn-login-submit');
if (!username || !password) return;
btn.disabled = true;
btn.textContent = 'AUTHENTICATING...';
errEl.classList.add('hidden');
try {
const form = new URLSearchParams({ username, password });
const res = await fetch(`${API}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: form,
});
if (!res.ok) throw new Error('invalid');
const data = await res.json();
if (!['ADMIN','SUPERADMIN'].includes(data.role)) {
errEl.textContent = 'Your account does not have edit permissions.';
errEl.classList.remove('hidden');
return;
}
Auth.save({ token: data.access_token, username: data.username,
nombre: data.nombre, role: data.role });
_hideOverlay();
if (this._pendingAid) {
this.openEdit(this._pendingAid);
this._pendingAid = null;
}
} catch {
errEl.textContent = 'Invalid credentials or insufficient permissions.';
errEl.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = 'LOGIN';
}
},
async openEdit(aidData) {
// Si no tiene sesión con permisos → abrir login primero
if (!Auth.isAdmin()) {
this.openLogin(aidData);
return;
}
document.getElementById('edit-aid-name').textContent = aidData.nombre;
document.getElementById('edit-save-status').textContent = '';
document.getElementById('edit-save-status').className = 'save-status';
const body = document.getElementById('modal-edit-body');
body.innerHTML = '
Loading…
';
_showOverlay('modal-edit');
// Refresh caches so the dropdowns show up-to-date contacts and lamps
try {
await Promise.all([
window.loadContacts ? window.loadContacts() : Promise.resolve(),
window.loadLamps ? window.loadLamps() : Promise.resolve(),
]);
} catch {}
body.innerHTML = buildEditForm(aidData);
document.getElementById('btn-edit-submit').onclick = () => this.submitEdit(aidData);
},
closeEdit() { _hideOverlay(); },
async submitEdit(aidData) {
const btn = document.getElementById('btn-edit-submit');
const status = document.getElementById('edit-save-status');
const motivo = document.getElementById('ef-motivo')?.value?.trim();
if (!motivo) {
status.textContent = 'Reason for change is required.';
status.className = 'save-status err';
return;
}
btn.disabled = true;
btn.textContent = 'SAVING...';
status.textContent = '';
const payload = {
puerto_responsable: v('ef-puerto') || null,
empresa_responsable: v('ef-empresa') || null,
caracteristica_luz: v('ef-luz') || null,
alcance_nm: flt('ef-alcance'),
radio_borneo_m: flt('ef-borneo'),
observaciones: v('ef-obs') || null,
lamp_id: v('ef-lamp') || null,
modificado_por: Auth.session.nombre,
motivo_cambio: motivo,
};
// Posición nominal solo si superadmin y se modificó
if (Auth.isSuperAdmin()) {
const lat = flt('ef-lat');
const lon = flt('ef-lon');
if (lat) payload.lat_nominal = lat;
if (lon) payload.lon_nominal = lon;
}
try {
const res = await fetch(`${API}/aids/${aidData.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Auth.token()}`,
},
body: JSON.stringify(payload),
});
if (res.status === 401) {
Auth.clear();
_hideOverlay();
this.openLogin(aidData);
return;
}
if (!res.ok) throw new Error(await res.text());
const updated = await res.json();
// Actualizar feature en el mapa
const feature = window.aidsSource?.getFeatureById(aidData.id);
if (feature) feature.setProperties({ ...feature.getProperties(), ...updated });
// Refrescar panel info
if (window.refreshInfoPanel) window.refreshInfoPanel({ ...aidData, ...updated });
status.textContent = 'Saved successfully.';
status.className = 'save-status ok';
setTimeout(() => _hideOverlay(), 1200);
} catch (e) {
status.textContent = 'Error: ' + e.message;
status.className = 'save-status err';
} finally {
btn.disabled = false;
btn.textContent = 'SAVE CHANGES';
}
},
};
// ── Form builder ─────────────────────────────────────────────────────────────
function buildEditForm(p) {
// Port options from PORT_AUTHORITY contacts (deduplicated by port_name)
const contacts = window._contactsCache || [];
const portsFromContacts = [...new Set(contacts
.filter(c => c.role === 'PORT_AUTHORITY' && c.port_name)
.map(c => c.port_name))];
// Keep the current value as an option even if no contact uses it yet
if (p.puerto_responsable && !portsFromContacts.includes(p.puerto_responsable)) {
portsFromContacts.push(p.puerto_responsable);
}
const pOpts = portsFromContacts.map(x =>
``).join('');
// Company options from OWNER contacts (deduplicated by company_name)
const companiesFromContacts = [...new Set(contacts
.filter(c => c.role === 'OWNER' && c.company_name)
.map(c => c.company_name))];
if (p.empresa_responsable && !companiesFromContacts.includes(p.empresa_responsable)) {
companiesFromContacts.push(p.empresa_responsable);
}
const cOpts = companiesFromContacts.map(x =>
``).join('');
// Lamp options from the lamp catalog
const lamps = window._lampCache || [];
const lOpts = lamps.map(l =>
``).join('');
const nominalBlock = Auth.isSuperAdmin() ? `
NOMINAL POSITION — SUPERADMIN ONLY
` : '';
return `
GENERAL
TECHNICAL
${nominalBlock}
AUDIT
`;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function v(id) { return document.getElementById(id)?.value?.trim() || ''; }
function flt(id) { const n = parseFloat(v(id)); return isNaN(n) ? null : n; }
function _showOverlay(modalId) {
document.getElementById('modal-overlay').classList.remove('hidden');
['modal-login','modal-edit'].forEach(id => {
document.getElementById(id).classList.toggle('hidden', id !== modalId);
});
}
function _hideOverlay() {
document.getElementById('modal-overlay').classList.add('hidden');
document.getElementById('modal-login').classList.add('hidden');
document.getElementById('modal-edit').classList.add('hidden');
}
// ── Login de inicio ───────────────────────────────────────────────────────────
async function initStartupLogin() {
const screen = document.getElementById('login-screen');
const app = document.getElementById('app');
const btn = document.getElementById('ls-submit');
const errEl = document.getElementById('ls-error');
// Validate the cached token against /auth/me before auto-entering. The
// backend may have restarted / token may have expired — in that case we
// force the user back to the login screen instead of showing a broken UI.
Auth.load();
if (Auth.isLoggedIn()) {
try {
const r = await fetch(`${API}/auth/me`, {
headers: { Authorization: `Bearer ${Auth.token()}` },
});
if (r.ok) {
screen.classList.add('hidden');
app.classList.remove('hidden');
afterLogin();
return;
}
// 401 / network error → drop the stale session and show login
Auth.clear();
} catch {
Auth.clear();
}
}
// Make sure login screen is visible if we got here
screen.classList.remove('hidden');
app.classList.add('hidden');
async function doLogin() {
const username = document.getElementById('ls-user').value.trim();
const password = document.getElementById('ls-pass').value;
if (!username || !password) return;
btn.disabled = true;
btn.textContent = 'AUTHENTICATING...';
errEl.classList.add('hidden');
try {
const form = new URLSearchParams({ username, password });
const res = await fetch(`${API}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: form,
});
if (!res.ok) throw new Error('invalid');
const data = await res.json();
Auth.save({ token: data.access_token, username: data.username,
nombre: data.nombre, role: data.role });
screen.classList.add('hidden');
app.classList.remove('hidden');
afterLogin();
} catch {
errEl.textContent = 'Invalid username or password.';
errEl.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = 'LOGIN';
}
}
btn.addEventListener('click', doLogin);
document.getElementById('ls-pass').addEventListener('keydown', (e) => {
if (e.key === 'Enter') doLogin();
});
}
function afterLogin() {
if (window.PortSearch) window.PortSearch.init();
Auth._renderBadge();
// Sincronizar barra de estado inferior con usuario conectado
const sbUser = document.getElementById('sb-user');
if (sbUser && Auth.session) {
sbUser.textContent = `${Auth.session.nombre} · ${Auth.session.role}`;
}
}
// ── Event listeners ──────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
initStartupLogin();
document.getElementById('btn-login-submit')?.addEventListener('click', () => Modal.submitLogin());
document.getElementById('btn-login-cancel')?.addEventListener('click', () => Modal.closeLogin());
document.getElementById('btn-close-login')?.addEventListener('click', () => Modal.closeLogin());
document.getElementById('btn-close-edit')?.addEventListener('click', () => Modal.closeEdit());
document.getElementById('btn-edit-cancel')?.addEventListener('click', () => Modal.closeEdit());
document.getElementById('login-pass')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') Modal.submitLogin();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') _hideOverlay();
});
// (Click on the backdrop intentionally does NOT close modals — user must
// use the X or CLOSE button. Avoids losing unsaved Settings inputs.)
});
window.Auth = Auth;
window.Modal = Modal;