Menu cleanup
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
<title>PPR Admin Interface</title>
|
<title>PPR Admin Interface</title>
|
||||||
<link rel="stylesheet" href="admin.css">
|
<link rel="stylesheet" href="admin.css">
|
||||||
<script src="lookups.js"></script>
|
<script src="lookups.js"></script>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>ATC Management Interface</title>
|
<title>ATC Management Interface</title>
|
||||||
<link rel="stylesheet" href="admin.css">
|
<link rel="stylesheet" href="admin.css">
|
||||||
<script src="lookups.js"></script>
|
<script src="lookups.js"></script>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
<style>
|
<style>
|
||||||
/* ATC-specific styles */
|
/* ATC-specific styles */
|
||||||
.atc-container {
|
.atc-container {
|
||||||
|
|||||||
@@ -373,6 +373,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
@@ -240,6 +240,7 @@
|
|||||||
background: #1976D2;
|
background: #1976D2;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
min-width: 1200px;
|
min-width: 1200px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
min-width: 1200px;
|
min-width: 1200px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="topbar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
+270
@@ -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();
|
||||||
|
});
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user