diff --git a/web/admin.html b/web/admin.html index e4d5d13..37b9038 100644 --- a/web/admin.html +++ b/web/admin.html @@ -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 @@
- 🛬 Pending Arrivals - 0 + 🛬 Today's Pending Arrivals - 0
@@ -621,7 +659,7 @@
- 🛫 Pending Departures - 0 + 🛫 Today's Pending Departures - 0
@@ -718,6 +756,40 @@

No parked visitors.

+ + +
+
+ 📅 Future PPRs - 0 + +
+ + + + + + +
@@ -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() ? + ` + 📝 + ${ppr.notes} + ` : ''; + + row.innerHTML = ` + ${dateDisplay} + ${ppr.ac_reg || '-'} + ${ppr.ac_type || '-'} + ${ppr.in_from || '-'} + ${formatTimeOnly(ppr.eta)} + ${notesIndicator} + `; + 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(); diff --git a/web/index.html b/web/index.html index b6a4bad..e4b9733 100644 --- a/web/index.html +++ b/web/index.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 || '')} (${escapeHtml(arrival.ac_type || '')})`; + // Show callsign if available, otherwise registration + const aircraftId = arrival.ac_call || arrival.ac_reg || ''; + const aircraftDisplay = `${escapeHtml(aircraftId)} (${escapeHtml(arrival.ac_type || '')})`; 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 || '')} (${escapeHtml(departure.ac_type || '')})`; + // Show callsign if available, otherwise registration + const aircraftId = departure.ac_call || departure.ac_reg || ''; + const aircraftDisplay = `${escapeHtml(aircraftId)} (${escapeHtml(departure.ac_type || '')})`; const toDisplay = await getAirportName(departure.out_to || ''); // Show departed time if available, otherwise ETD