Compare commits
2 Commits
bd1200f377
...
5f2aa82e36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f2aa82e36 | ||
|
|
e8bd30aadc |
263
web/admin.html
263
web/admin.html
@@ -45,6 +45,7 @@
|
||||
opacity: 0.9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -145,6 +146,49 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-header-collapsible {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header-collapsible:hover {
|
||||
background: #3d5a6e;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.collapse-icon.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.footer-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #34495e;
|
||||
color: white;
|
||||
padding: 0.5rem 2rem;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
z-index: 50;
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 40px; /* Make room for footer */
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
@@ -241,17 +285,14 @@
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 50%;
|
||||
left: 100%;
|
||||
margin-left: 10px;
|
||||
margin-bottom: -20px;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notes-tooltip .tooltip-text::after {
|
||||
@@ -270,6 +311,22 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notes-tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notes-indicator {
|
||||
display: inline-block;
|
||||
background-color: #ffc107;
|
||||
color: #856404;
|
||||
font-size: 0.8rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
margin-left: 5px;
|
||||
cursor: help;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
@@ -587,7 +644,7 @@
|
||||
<!-- Arrivals Table -->
|
||||
<div class="ppr-table">
|
||||
<div class="table-header">
|
||||
🛬 Pending Arrivals - <span id="arrivals-count">0</span>
|
||||
🛬 Today's Pending Arrivals - <span id="arrivals-count">0</span>
|
||||
</div>
|
||||
|
||||
<div id="arrivals-loading" class="loading">
|
||||
@@ -621,7 +678,7 @@
|
||||
<!-- Departures Table -->
|
||||
<div class="ppr-table" style="margin-top: 2rem;">
|
||||
<div class="table-header">
|
||||
🛫 Pending Departures - <span id="departures-count">0</span>
|
||||
🛫 Today's Pending Departures - <span id="departures-count">0</span>
|
||||
</div>
|
||||
|
||||
<div id="departures-loading" class="loading">
|
||||
@@ -718,6 +775,40 @@
|
||||
<p>No parked visitors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming PPRs (Future Days) -->
|
||||
<div class="ppr-table" style="margin-top: 1rem;">
|
||||
<div class="table-header-collapsible" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;" onclick="toggleUpcomingTable()">
|
||||
<span>📅 Future PPRs - <span id="upcoming-count">0</span></span>
|
||||
<span class="collapse-icon collapsed" id="upcoming-collapse-icon">▼</span>
|
||||
</div>
|
||||
|
||||
<div id="upcoming-loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
Loading upcoming PPRs...
|
||||
</div>
|
||||
|
||||
<div id="upcoming-table-content" style="display: none;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr style="font-size: 0.85rem !important;">
|
||||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Date</th>
|
||||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Registration</th>
|
||||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Type</th>
|
||||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">From</th>
|
||||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">ETA</th>
|
||||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="upcoming-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="upcoming-no-data" class="no-data" style="display: none; padding: 1rem; font-size: 0.9rem;">
|
||||
<p>No upcoming PPRs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1380,7 +1471,7 @@
|
||||
if (!accessToken) return;
|
||||
|
||||
// Load all tables simultaneously
|
||||
await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked()]);
|
||||
await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked(), loadUpcoming()]);
|
||||
}
|
||||
|
||||
// Load arrivals (NEW and CONFIRMED status)
|
||||
@@ -1626,6 +1717,112 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Load upcoming PPRs (future days with NEW or CONFIRMED status)
|
||||
async function loadUpcoming() {
|
||||
document.getElementById('upcoming-loading').style.display = 'block';
|
||||
document.getElementById('upcoming-table-content').style.display = 'none';
|
||||
document.getElementById('upcoming-no-data').style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch upcoming PPRs');
|
||||
}
|
||||
|
||||
const allPPRs = await response.json();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Filter for PPRs with ETA in the future (not today) and NEW or CONFIRMED status
|
||||
const upcoming = allPPRs.filter(ppr => {
|
||||
if (!ppr.eta || (ppr.status !== 'NEW' && ppr.status !== 'CONFIRMED')) {
|
||||
return false;
|
||||
}
|
||||
const etaDate = ppr.eta.split('T')[0];
|
||||
return etaDate > today;
|
||||
});
|
||||
|
||||
displayUpcoming(upcoming);
|
||||
} catch (error) {
|
||||
console.error('Error loading upcoming PPRs:', error);
|
||||
if (error.message !== 'Session expired. Please log in again.') {
|
||||
showNotification('Error loading upcoming PPRs', true);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('upcoming-loading').style.display = 'none';
|
||||
}
|
||||
|
||||
function displayUpcoming(upcoming) {
|
||||
const tbody = document.getElementById('upcoming-table-body');
|
||||
document.getElementById('upcoming-count').textContent = upcoming.length;
|
||||
|
||||
if (upcoming.length === 0) {
|
||||
// Don't show anything if collapsed by default
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by ETA date and time
|
||||
upcoming.sort((a, b) => new Date(a.eta) - new Date(b.eta));
|
||||
|
||||
tbody.innerHTML = '';
|
||||
// Don't auto-expand, keep collapsed by default
|
||||
|
||||
for (const ppr of upcoming) {
|
||||
const row = document.createElement('tr');
|
||||
row.onclick = () => openPPRModal(ppr.id);
|
||||
row.style.cssText = 'font-size: 0.85rem !important;';
|
||||
|
||||
// Format date as Day DD/MM (e.g., Wed 11/12)
|
||||
const etaDate = new Date(ppr.eta);
|
||||
const dayName = etaDate.toLocaleDateString('en-GB', { weekday: 'short' });
|
||||
const dateStr = etaDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
|
||||
const dateDisplay = `${dayName} ${dateStr}`;
|
||||
|
||||
// Create notes indicator if notes exist
|
||||
const notesIndicator = ppr.notes && ppr.notes.trim() ?
|
||||
`<span class="notes-tooltip">
|
||||
<span class="notes-indicator">📝</span>
|
||||
<span class="tooltip-text">${ppr.notes}</span>
|
||||
</span>` : '';
|
||||
|
||||
row.innerHTML = `
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${dateDisplay}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_reg || '-'}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_type || '-'}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.in_from || '-'}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(ppr.eta)}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${notesIndicator}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
// Setup tooltips after rendering
|
||||
setupTooltips();
|
||||
}
|
||||
|
||||
// Toggle upcoming table collapse/expand
|
||||
function toggleUpcomingTable() {
|
||||
const content = document.getElementById('upcoming-table-content');
|
||||
const noData = document.getElementById('upcoming-no-data');
|
||||
const icon = document.getElementById('upcoming-collapse-icon');
|
||||
|
||||
const isVisible = content.style.display === 'block' || noData.style.display === 'block';
|
||||
|
||||
if (isVisible) {
|
||||
content.style.display = 'none';
|
||||
noData.style.display = 'none';
|
||||
icon.classList.add('collapsed');
|
||||
} else {
|
||||
const count = parseInt(document.getElementById('upcoming-count').textContent);
|
||||
if (count > 0) {
|
||||
content.style.display = 'block';
|
||||
} else {
|
||||
noData.style.display = 'block';
|
||||
}
|
||||
icon.classList.remove('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
// ICAO code to airport name cache
|
||||
const airportNameCache = {};
|
||||
|
||||
@@ -1698,6 +1895,8 @@
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
// Setup tooltips after rendering
|
||||
setupTooltips();
|
||||
}
|
||||
|
||||
async function displayDepartures(departures) {
|
||||
@@ -2782,6 +2981,47 @@
|
||||
clearDepartureAirportLookup();
|
||||
}
|
||||
|
||||
// Position tooltip dynamically to avoid being cut off
|
||||
function positionTooltip(event) {
|
||||
const indicator = event.currentTarget;
|
||||
const tooltip = indicator.querySelector('.tooltip-text');
|
||||
if (!tooltip) return;
|
||||
|
||||
const rect = indicator.getBoundingClientRect();
|
||||
const tooltipWidth = 300;
|
||||
const tooltipHeight = tooltip.offsetHeight || 100;
|
||||
|
||||
// Position to the right of the indicator by default
|
||||
let left = rect.right + 10;
|
||||
let top = rect.top + (rect.height / 2) - (tooltipHeight / 2);
|
||||
|
||||
// Check if tooltip would go off the right edge
|
||||
if (left + tooltipWidth > window.innerWidth) {
|
||||
// Position to the left instead
|
||||
left = rect.left - tooltipWidth - 10;
|
||||
}
|
||||
|
||||
// Check if tooltip would go off the bottom
|
||||
if (top + tooltipHeight > window.innerHeight) {
|
||||
top = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
|
||||
// Check if tooltip would go off the top
|
||||
if (top < 10) {
|
||||
top = 10;
|
||||
}
|
||||
|
||||
tooltip.style.left = left + 'px';
|
||||
tooltip.style.top = top + 'px';
|
||||
}
|
||||
|
||||
// Add hover listeners to all notes tooltips
|
||||
function setupTooltips() {
|
||||
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
||||
tooltip.addEventListener('mouseenter', positionTooltip);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the page when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupLoginForm();
|
||||
@@ -2798,5 +3038,10 @@
|
||||
document.getElementById('etd-time').addEventListener('change', markETDAsManuallyEdited);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Footer Bar -->
|
||||
<div class="footer-bar">
|
||||
Please contact James Pattinson if you have any ideas about or problems with this system
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -304,7 +304,9 @@
|
||||
|
||||
// Build rows asynchronously to lookup airport names
|
||||
const rows = await Promise.all(arrivals.map(async (arrival) => {
|
||||
const aircraftDisplay = `${escapeHtml(arrival.ac_reg || '')} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.ac_type || '')})</span>`;
|
||||
// Show callsign if available, otherwise registration
|
||||
const aircraftId = arrival.ac_call || arrival.ac_reg || '';
|
||||
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.ac_type || '')})</span>`;
|
||||
const fromDisplay = await getAirportName(arrival.in_from || '');
|
||||
|
||||
// Show landed time if available, otherwise ETA
|
||||
@@ -353,7 +355,9 @@
|
||||
|
||||
// Build rows asynchronously to lookup airport names
|
||||
const rows = await Promise.all(departures.map(async (departure) => {
|
||||
const aircraftDisplay = `${escapeHtml(departure.ac_reg || '')} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`;
|
||||
// Show callsign if available, otherwise registration
|
||||
const aircraftId = departure.ac_call || departure.ac_reg || '';
|
||||
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`;
|
||||
const toDisplay = await getAirportName(departure.out_to || '');
|
||||
|
||||
// Show departed time if available, otherwise ETD
|
||||
|
||||
Reference in New Issue
Block a user