Future PPRs
This commit is contained in:
239
web/admin.html
239
web/admin.html
@@ -145,6 +145,31 @@
|
|||||||
font-weight: 500;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -241,17 +266,14 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 10000;
|
||||||
bottom: 50%;
|
|
||||||
left: 100%;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-bottom: -20px;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes-tooltip .tooltip-text::after {
|
.notes-tooltip .tooltip-text::after {
|
||||||
@@ -270,6 +292,22 @@
|
|||||||
opacity: 1;
|
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 Styles */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -587,7 +625,7 @@
|
|||||||
<!-- Arrivals Table -->
|
<!-- Arrivals Table -->
|
||||||
<div class="ppr-table">
|
<div class="ppr-table">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
🛬 Pending Arrivals - <span id="arrivals-count">0</span>
|
🛬 Today's Pending Arrivals - <span id="arrivals-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="arrivals-loading" class="loading">
|
<div id="arrivals-loading" class="loading">
|
||||||
@@ -621,7 +659,7 @@
|
|||||||
<!-- Departures Table -->
|
<!-- Departures Table -->
|
||||||
<div class="ppr-table" style="margin-top: 2rem;">
|
<div class="ppr-table" style="margin-top: 2rem;">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
🛫 Pending Departures - <span id="departures-count">0</span>
|
🛫 Today's Pending Departures - <span id="departures-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="departures-loading" class="loading">
|
<div id="departures-loading" class="loading">
|
||||||
@@ -718,6 +756,40 @@
|
|||||||
<p>No parked visitors.</p>
|
<p>No parked visitors.</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1380,7 +1452,7 @@
|
|||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
// Load all tables simultaneously
|
// 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)
|
// Load arrivals (NEW and CONFIRMED status)
|
||||||
@@ -1626,6 +1698,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
|
// ICAO code to airport name cache
|
||||||
const airportNameCache = {};
|
const airportNameCache = {};
|
||||||
|
|
||||||
@@ -1698,6 +1876,8 @@
|
|||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
// Setup tooltips after rendering
|
||||||
|
setupTooltips();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function displayDepartures(departures) {
|
async function displayDepartures(departures) {
|
||||||
@@ -2782,6 +2962,47 @@
|
|||||||
clearDepartureAirportLookup();
|
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
|
// Initialize the page when DOM is loaded
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
setupLoginForm();
|
setupLoginForm();
|
||||||
|
|||||||
@@ -304,7 +304,9 @@
|
|||||||
|
|
||||||
// Build rows asynchronously to lookup airport names
|
// Build rows asynchronously to lookup airport names
|
||||||
const rows = await Promise.all(arrivals.map(async (arrival) => {
|
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 || '');
|
const fromDisplay = await getAirportName(arrival.in_from || '');
|
||||||
|
|
||||||
// Show landed time if available, otherwise ETA
|
// Show landed time if available, otherwise ETA
|
||||||
@@ -353,7 +355,9 @@
|
|||||||
|
|
||||||
// Build rows asynchronously to lookup airport names
|
// Build rows asynchronously to lookup airport names
|
||||||
const rows = await Promise.all(departures.map(async (departure) => {
|
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 || '');
|
const toDisplay = await getAirportName(departure.out_to || '');
|
||||||
|
|
||||||
// Show departed time if available, otherwise ETD
|
// Show departed time if available, otherwise ETD
|
||||||
|
|||||||
Reference in New Issue
Block a user