'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() ? `
` : ''; return `
${nominalBlock}
`; } // ── 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;