Future PPRs

This commit is contained in:
James Pattinson
2025-12-10 15:44:26 +00:00
parent bd1200f377
commit e8bd30aadc
2 changed files with 236 additions and 11 deletions

View File

@@ -145,6 +145,31 @@
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 {
text-align: center;
padding: 2rem;
@@ -241,17 +266,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 +292,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 +625,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 +659,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 +756,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 +1452,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 +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
const airportNameCache = {};
@@ -1698,6 +1876,8 @@
`;
tbody.appendChild(row);
}
// Setup tooltips after rendering
setupTooltips();
}
async function displayDepartures(departures) {
@@ -2782,6 +2962,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();

View File

@@ -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