Files
ppr-ng/web/topbar.js
T
2026-06-20 08:04:37 -04:00

271 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
const actionMap = {
'new-ppr': "openNewPPRModal",
'book-out': "openLocalFlightModal",
'book-in': "openBookInModal",
overflight: "openOverflightModal",
'user-aircraft': "openUserAircraftModal",
'user-management': "openUserManagementModal"
};
function injectTopbarStyles() {
if (document.getElementById('shared-topbar-styles')) return;
const style = document.createElement('style');
style.id = 'shared-topbar-styles';
style.textContent = `
.top-bar {
background: linear-gradient(135deg, #2c3e50, #3498db);
color: white;
padding: 0.5rem 2rem;
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: relative;
z-index: 100;
}
.top-bar .title { order: 2; flex: 1; text-align: center; }
.top-bar .title h1 { margin: 0; font-size: 1.5rem; }
.top-bar .menu-buttons {
order: 1;
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.top-bar .user-info {
order: 3;
font-size: 0.9rem;
opacity: 0.9;
display: flex;
align-items: center;
gap: 0.3rem;
}
.top-bar .btn {
padding: 0.7rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
text-decoration: none;
display: inline-block;
}
.top-bar .btn-success { background-color: #27ae60; color: white; }
.top-bar .btn-warning { background-color: #f39c12; color: white; }
.dropdown { position: relative; display: inline-block; }
.dropdown-toggle { white-space: nowrap; }
.dropdown-menu {
display: none;
position: absolute;
background-color: white;
min-width: 220px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
border-radius: 5px;
z-index: 1000;
top: 100%;
left: 0;
margin-top: 0.5rem;
padding: 0;
}
.dropdown-menu.active { display: block; }
.dropdown-menu a {
color: #333;
padding: 0.75rem 1.2rem;
text-decoration: none;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
transition: background-color 0.2s ease;
white-space: nowrap;
}
.dropdown-menu a:hover { background-color: #f5f5f5; }
.dropdown-menu a:first-child { border-radius: 5px 5px 0 0; }
.dropdown-menu a:last-child { border-radius: 0 0 5px 5px; }
.shortcut { font-size: 0.8rem; color: #999; margin-left: 1rem; }
`;
document.head.appendChild(style);
}
function actionLink(label, action, shortcut, id = '') {
const shortcutText = shortcut ? `<span class="shortcut">(${shortcut})</span>` : '';
const idAttr = id ? ` id="${id}"` : '';
const hidden = id === 'user-management-dropdown' ? ' style="display: none;"' : '';
return `<a href="#" data-topbar-action="${action}"${idAttr}${hidden}>${label} ${shortcutText}</a>`;
}
function navLink(label, href) {
return `<a href="${href}">${label}</a>`;
}
function normalizeTopbar() {
const topbar = document.querySelector('.top-bar');
if (!topbar || topbar.dataset.sharedTopbar === 'true') return;
injectTopbarStyles();
const existingTitle = topbar.querySelector('#tower-title, .title h1, h1');
const path = window.location.pathname.replace(/\/$/, '') || '/';
const titleByPath = {
'/admin': '✈️ Swansea Tower',
'/atc': '✈️ Swansea Tower - ATC View',
'/reports': '📊 PPR Reports',
'/movements': '📈 Movements',
'/drone-requests': 'Drone Requests',
'/bulk-log': '🧾 Bulk Flight Log',
'/journal': '📔 Journal Log'
};
const titleText = titleByPath[path] || (existingTitle ? existingTitle.textContent.trim() : 'Tower Ops');
const existingUser = topbar.querySelector('#current-user');
const username = existingUser ? existingUser.textContent.trim() : 'Loading...';
topbar.dataset.sharedTopbar = 'true';
topbar.innerHTML = `
<div class="title">
<h1 id="tower-title">${titleText}</h1>
</div>
<div class="menu-buttons">
<div class="dropdown">
<button class="btn btn-success dropdown-toggle" id="actionsDropdownBtn">📋 Actions</button>
<div class="dropdown-menu" id="actionsDropdownMenu">
${actionLink(' New PPR', 'new-ppr', 'N')}
${actionLink('🛫 Book Out', 'book-out', 'L')}
${actionLink('🛬 Book In', 'book-in', 'I')}
${actionLink('🔄 Overflight', 'overflight', 'O')}
</div>
</div>
<div class="dropdown">
<button class="btn btn-warning dropdown-toggle" id="adminDropdownBtn">⚙️ Menu</button>
<div class="dropdown-menu" id="adminDropdownMenu">
${navLink('🏠 Admin View', '/admin')}
${navLink('🎛️ ATC View', '/atc')}
${navLink('📊 Reports', '/reports')}
${navLink('🛸 Drone Requests', '/drone-requests')}
${navLink('📔 Journal Log', '/journal')}
${actionLink('✈️ User Aircraft', 'user-aircraft', '', 'user-aircraft-dropdown')}
${actionLink('👥 User Management', 'user-management', '', 'user-management-dropdown')}
</div>
</div>
</div>
<div class="user-info">
Logged in as: <span id="current-user">${username || 'Loading...'}</span> |
<a href="#" data-topbar-logout style="color: white;">Logout</a>
</div>
`;
}
function closeDropdowns(except = null) {
document.querySelectorAll('.dropdown-menu.active').forEach(menu => {
if (menu !== except) menu.classList.remove('active');
});
}
function runAction(action) {
const fnName = actionMap[action];
if (fnName && typeof window[fnName] === 'function') {
if (action === 'book-out') {
window[fnName]('LOCAL');
} else {
window[fnName]();
}
return;
}
window.location.href = `/admin?action=${encodeURIComponent(action)}`;
}
function handleDeferredAction() {
const params = new URLSearchParams(window.location.search);
const action = params.get('action');
if (!action) return;
window.setTimeout(() => {
runAction(action);
const cleanUrl = window.location.pathname + window.location.hash;
window.history.replaceState({}, document.title, cleanUrl);
}, 300);
}
async function updateRoleVisibility() {
const token = localStorage.getItem('ppr_access_token');
const userManagement = document.getElementById('user-management-dropdown');
const userAircraft = document.getElementById('user-aircraft-dropdown');
if (!token || (!userManagement && !userAircraft)) return;
try {
const response = await fetch('/api/v1/auth/test-token', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) return;
const user = await response.json();
const role = (user.role || '').toUpperCase();
if (role === 'ADMINISTRATOR') {
if (userManagement) userManagement.style.display = 'flex';
if (userAircraft) userAircraft.style.display = 'flex';
} else if (role === 'OPERATOR') {
if (userManagement) userManagement.style.display = 'none';
if (userAircraft) userAircraft.style.display = 'flex';
} else {
if (userManagement) userManagement.style.display = 'none';
if (userAircraft) userAircraft.style.display = 'none';
}
} catch (error) {
if (userManagement) userManagement.style.display = 'none';
}
}
document.addEventListener('click', event => {
const toggle = event.target.closest('.dropdown-toggle');
const action = event.target.closest('[data-topbar-action]');
const logout = event.target.closest('[data-topbar-logout]');
if (toggle) {
event.preventDefault();
event.stopImmediatePropagation();
const menu = toggle.parentElement.querySelector('.dropdown-menu');
const willOpen = !menu.classList.contains('active');
closeDropdowns(menu);
menu.classList.toggle('active', willOpen);
return;
}
if (action) {
event.preventDefault();
event.stopImmediatePropagation();
closeDropdowns();
runAction(action.dataset.topbarAction);
return;
}
if (logout) {
event.preventDefault();
event.stopImmediatePropagation();
if (typeof window.logout === 'function') {
window.logout();
} else {
localStorage.removeItem('ppr_access_token');
localStorage.removeItem('ppr_username');
localStorage.removeItem('ppr_token_expiry');
window.location.href = '/admin';
}
return;
}
if (!event.target.closest('.dropdown')) {
closeDropdowns();
}
});
document.addEventListener('DOMContentLoaded', () => {
normalizeTopbar();
updateRoleVisibility();
handleDeferredAction();
});
})();