esp/assets/js/layout.js

228 lines
9.4 KiB
JavaScript

// Layout común: navbar superior + footer + funcionalidades globales (Vanilla JS)
(function () {
'use strict';
const LAYOUT_VERSION = 'layout-v1054-vanilla';
const CSS_VERSION = '20251023-1000';
try { console.log('[ESP]', LAYOUT_VERSION); } catch {}
// ===== Theme (light/dark) =====
const THEME_KEY = 'esp_theme';
function applyTheme(theme) {
const t = (theme === 'dark') ? 'dark' : 'light';
document.documentElement.setAttribute('data-bs-theme', t);
try { localStorage.setItem(THEME_KEY, t); } catch {}
// Toggle de tema removido
}
function initTheme() {
// Forzar tema claro y limpiar preferencia guardada
applyTheme('light');
try { localStorage.removeItem(THEME_KEY); } catch {}
}
function renderNavbar() {
// No mostrar navbar en la página de login
try { if (location.pathname.endsWith('login.html') || location.pathname.endsWith('login.php')) return ''; } catch {}
return `
<nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="/">ESP Admin</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMain">
<ul class="navbar-nav ms-auto align-items-center gap-2">
<li class="nav-item">
<a class="btn btn-outline-primary btn-sm" href="/flash">Flashear ESP8266</a>
</li>
<li class="nav-item">
<button id="navCreateDevice" class="btn btn-primary btn-sm" type="button">Crear dispositivo</button>
</li>
<li class="nav-item">
<button id="navCreateSector" class="btn btn-secondary btn-sm" type="button">Crear sector</button>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#" id="userMenuToggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<span aria-hidden="true">👤</span>
<span id="navbarUsername">Usuario</span>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuToggle">
<li><a class="dropdown-item" href="#" id="menuPerfil">Perfil</a></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" type="button" id="logoutDropdownBtn">Salir</button></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>`;
}
function renderFooter() {
const year = new Date().getFullYear();
return `
<footer class="site-footer text-center py-3">
<div class="container small">ESP Project • v1 • ${year}</div>
</footer>`;
}
function setActiveNav() {
const isActive = (href) => href && (location.pathname.endsWith(href) || location.href.endsWith(href));
document.querySelectorAll('.navbar .nav-link').forEach(link => {
if (isActive(link.getAttribute('href'))) link.classList.add('active');
});
}
function ensureStylesheet() {
try {
const exists = document.querySelector('link[href*="/assets/css/layout.css"]');
if (exists) return;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `/assets/css/layout.css?v=${CSS_VERSION}`;
document.head.appendChild(link);
} catch {}
}
// Inyectar CSS lo antes posible para evitar FOUC
ensureStylesheet();
document.addEventListener('DOMContentLoaded', function () {
// Asegurar CSS base del layout
ensureStylesheet();
const usingPhpLayout = (document.body && document.body.getAttribute('data-layout') === 'php');
// Insertar navbar y footer solo si no se usa layout.php
if (!usingPhpLayout) {
document.body.insertAdjacentHTML('afterbegin', renderNavbar());
document.body.insertAdjacentHTML('beforeend', renderFooter());
}
try {
console.log('[ESP] DOMContentLoaded, navbar insertado');
} catch {}
// Indicador global de estado MQTT WebSocket (posicionado por CSS en esquina inferior derecha)
if (!usingPhpLayout && !document.getElementById('wsStatusIndicator')) {
const wsIndicatorHtml = `
<div id="wsStatusIndicator" class="ws-indicator inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded-full bg-slate-100 text-slate-700 border border-slate-200 disconnected" title="Estado de conexion MQTT WebSocket">
<span class="dot w-2.5 h-2.5 rounded-full bg-red-500"></span>
<span class="label">WS: Desconectado</span>
</div>`;
document.body.insertAdjacentHTML('afterbegin', wsIndicatorHtml);
}
// Función global para actualizar el indicador desde cualquier página
window.updateWSIndicator = function(connected) {
try {
const el = document.getElementById('wsStatusIndicator');
if (!el) return;
const dot = el.querySelector('.dot');
const label = el.querySelector('.label');
el.classList.remove('connected', 'disconnected');
if (connected) {
el.classList.add('connected');
el.classList.remove('bg-slate-100', 'text-slate-700');
el.classList.add('bg-emerald-50', 'text-emerald-700', 'border-emerald-200');
if (dot) {
dot.classList.remove('bg-red-500');
dot.classList.add('bg-emerald-500');
}
if (label) label.textContent = 'WS: Conectado';
} else {
el.classList.add('disconnected');
el.classList.remove('bg-emerald-50', 'text-emerald-700', 'border-emerald-200');
el.classList.add('bg-slate-100', 'text-slate-700');
if (dot) {
dot.classList.remove('bg-emerald-500');
dot.classList.add('bg-red-500');
}
if (label) label.textContent = 'WS: Desconectado';
}
} catch {}
};
const onLoginPage = location.pathname.endsWith('login.html') || location.pathname.endsWith('login.php');
// Inicializar tema (se fija en claro por defecto y no hay toggle)
initTheme();
setActiveNav();
// Handlers de navbar para abrir modales en index.html o redirigir
const onIndex = location.pathname.endsWith('index.html') || location.pathname.endsWith('index.php') || /\/paginas\/$/.test(location.pathname) || location.pathname === '/';
// Helpers globales de modales (Tailwind-only)
window.openModal = function(id) {
const m = document.getElementById(id);
if (!m) return;
m.classList.remove('hidden');
// foco overlay click-close
const overlay = m.querySelector('[data-modal-overlay]');
if (overlay) overlay.addEventListener('click', () => window.closeModal(id), { once: true });
if (typeof window.onOpenModal === 'function') {
try { window.onOpenModal(id); } catch {}
}
};
window.closeModal = function(id) {
const m = document.getElementById(id);
if (!m) return;
m.classList.add('hidden');
};
// Cierre por botones con atributo data-modal-close
document.addEventListener('click', function(e){
const btn = e.target.closest('[data-modal-close]');
if (!btn) return;
const modal = btn.closest('[id^="modal"]');
if (modal && modal.id) window.closeModal(modal.id);
});
const navCreateDevice = document.getElementById('navCreateDevice');
if (navCreateDevice) navCreateDevice.addEventListener('click', function() {
const modal = document.getElementById('modalCrearDispositivo');
if (onIndex && modal) { window.openModal('modalCrearDispositivo'); }
else { location.href = '/'; }
});
const navCreateSector = document.getElementById('navCreateSector');
if (navCreateSector) navCreateSector.addEventListener('click', function() {
const modal = document.getElementById('modalGestionSectores');
if (onIndex && modal) { window.openModal('modalGestionSectores'); }
else { location.href = '/'; }
});
// Manejadores de logout
function doLogout() {
fetch('/api/logout', { method: 'POST', credentials: 'same-origin' })
.catch(() => {})
.finally(() => { window.location.href = '/'; });
}
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) logoutBtn.addEventListener('click', doLogout);
const logoutDropdownBtn = document.getElementById('logoutDropdownBtn');
if (logoutDropdownBtn) logoutDropdownBtn.addEventListener('click', doLogout);
// Comprobación de sesión en páginas protegidas
if (!onLoginPage) {
fetch('/api/me', { credentials: 'same-origin' })
.then(async (res) => {
if (res.status === 401) {
window.location.href = '/';
return;
}
try {
const json = await res.json().catch(() => null);
const name =
(json?.data?.user?.nombre) ||
(json?.data?.user?.name) ||
(json?.data?.user?.username) ||
(typeof json?.data?.user === 'string' ? json.data.user : '') ||
(json?.user?.name) ||
(json?.user?.username) ||
(typeof json?.user === 'string' ? json.user : '') ||
'';
const el = document.getElementById('navbarUsername');
if (el && name) el.textContent = name;
} catch {}
})
.catch(() => { /* Si la API no responde, permanecer en la página */ });
}
});
})();