Menu cleanup

This commit is contained in:
2026-06-20 08:04:37 -04:00
parent a9b5ec67ba
commit 10ab215396
8 changed files with 279 additions and 2 deletions
+1
View File
@@ -6,6 +6,7 @@
<title>PPR Admin Interface</title>
<link rel="stylesheet" href="admin.css">
<script src="lookups.js"></script>
<script src="topbar.js"></script>
</head>
<body>
<div class="top-bar">
+1
View File
@@ -6,6 +6,7 @@
<title>ATC Management Interface</title>
<link rel="stylesheet" href="admin.css">
<script src="lookups.js"></script>
<script src="topbar.js"></script>
<style>
/* ATC-specific styles */
.atc-container {
+1
View File
@@ -373,6 +373,7 @@
}
}
</style>
<script src="topbar.js"></script>
</head>
<body>
<div class="top-bar">
+1
View File
@@ -209,6 +209,7 @@
}
}
</style>
<script src="topbar.js"></script>
</head>
<body>
<div class="top-bar">
+1
View File
@@ -240,6 +240,7 @@
background: #1976D2;
}
</style>
<script src="topbar.js"></script>
</head>
<body>
<div class="top-bar">
+2 -1
View File
@@ -358,6 +358,7 @@
min-width: 1200px;
}
</style>
<script src="topbar.js"></script>
</head>
<body>
<div class="top-bar">
@@ -1274,4 +1275,4 @@
}
</style>
</body>
</html>
</html>
+2 -1
View File
@@ -358,6 +358,7 @@
min-width: 1200px;
}
</style>
<script src="topbar.js"></script>
</head>
<body>
<div class="top-bar">
@@ -1386,4 +1387,4 @@
document.addEventListener('DOMContentLoaded', initializePage);
</script>
</body>
</html>
</html>
+270
View File
@@ -0,0 +1,270 @@
(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();
});
})();