@@ -1014,6 +1076,13 @@
return;
}
+ // Press 'Escape' to close Book In modal if it's open (allow even when typing in inputs)
+ if (e.key === 'Escape' && document.getElementById('bookInModal').style.display === 'block') {
+ e.preventDefault();
+ closeBookInModal();
+ return;
+ }
+
// Only trigger other shortcuts when not typing in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
return;
@@ -1042,6 +1111,12 @@
e.preventDefault();
openLocalFlightModal('DEPARTURE');
}
+
+ // Press 'i' to book in arrival
+ if (e.key === 'i' || e.key === 'I') {
+ e.preventDefault();
+ openBookInModal();
+ }
});
}
@@ -1177,10 +1252,11 @@
document.getElementById('arrivals-no-data').style.display = 'none';
try {
- // Load PPRs and local flights that are in the air
- const [pprResponse, localResponse] = await Promise.all([
+ // Load PPRs, local flights, and booked-in arrivals
+ const [pprResponse, localResponse, bookInResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
- authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
+ authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000'),
+ authenticatedFetch('/api/v1/arrivals/?limit=1000')
]);
if (!pprResponse.ok) {
@@ -1218,6 +1294,24 @@
arrivals.push(...localInAir);
}
+ // Add booked-in arrivals from the arrivals table
+ if (bookInResponse.ok) {
+ const bookedInArrivals = await bookInResponse.json();
+ const today = new Date().toISOString().split('T')[0];
+ const bookedInToday = bookedInArrivals
+ .filter(arrival => {
+ // Only include arrivals booked in today (created_dt) with BOOKED_IN status
+ if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false;
+ const bookedDate = arrival.created_dt.split('T')[0];
+ return bookedDate === today;
+ })
+ .map(arrival => ({
+ ...arrival,
+ isBookedIn: true // Flag to distinguish from PPR and local
+ }));
+ arrivals.push(...bookedInToday);
+ }
+
displayArrivals(arrivals);
} catch (error) {
console.error('Error loading arrivals:', error);
@@ -1435,13 +1529,17 @@
document.getElementById('parked-no-data').style.display = 'none';
try {
- const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
+ // Load both PPRs and booked-in arrivals
+ const [pprResponse, bookedInResponse] = await Promise.all([
+ authenticatedFetch('/api/v1/pprs/?limit=1000'),
+ authenticatedFetch('/api/v1/arrivals/?limit=1000')
+ ]);
- if (!response.ok) {
+ if (!pprResponse.ok) {
throw new Error('Failed to fetch parked visitors');
}
- const allPPRs = await response.json();
+ const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for parked visitors: LANDED status and (no ETD or ETD not today)
@@ -1459,6 +1557,18 @@
return etdDate !== today;
});
+ // Add booked-in arrivals with LANDED status
+ if (bookedInResponse.ok) {
+ const bookedInArrivals = await bookedInResponse.json();
+ const bookedInParked = bookedInArrivals
+ .filter(arrival => arrival.status === 'LANDED')
+ .map(arrival => ({
+ ...arrival,
+ isBookedIn: true // Flag to distinguish from PPR
+ }));
+ parked.push(...bookedInParked);
+ }
+
displayParked(parked);
} catch (error) {
console.error('Error loading parked visitors:', error);
@@ -1491,9 +1601,25 @@
for (const ppr of parked) {
const row = document.createElement('tr');
- row.onclick = () => openPPRModal(ppr.id);
+ const isBookedIn = ppr.isBookedIn;
+
+ // Click handler that routes to correct modal/display
+ if (isBookedIn) {
+ row.style.cursor = 'default'; // Booked-in arrivals don't have a modal yet
+ } else {
+ row.onclick = () => openPPRModal(ppr.id);
+ }
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
+ // Get registration based on type (PPR vs booked-in)
+ const registration = ppr.ac_reg || ppr.registration || '-';
+
+ // Get aircraft type based on type (PPR vs booked-in)
+ const acType = ppr.ac_type || ppr.type || '-';
+
+ // Get from airport
+ const fromAirport = ppr.in_from || '-';
+
// Format arrival: time if today, date if not
const today = new Date().toISOString().split('T')[0];
let arrivedDisplay = '-';
@@ -1517,9 +1643,9 @@
}
row.innerHTML = `
-
${ppr.ac_reg || '-'} |
-
${ppr.ac_type || '-'} |
-
${ppr.in_from || '-'} |
+
${registration} |
+
${acType} |
+
${fromAirport} |
${arrivedDisplay} |
${etdDisplay} |
`;
@@ -1674,11 +1800,16 @@
for (const flight of arrivals) {
const row = document.createElement('tr');
const isLocal = flight.isLocalFlight;
+ const isBookedIn = flight.isBookedIn;
// Click handler that routes to correct modal
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
+ } else if (isBookedIn) {
+ // For booked-in flights, we might add a view modal later
+ // For now, just show a message
+ showNotification(`Booked-in flight: ${flight.registration}`, false);
} else {
openPPRModal(flight.id);
}
@@ -1713,6 +1844,34 @@
CANCEL
`;
+ } else if (isBookedIn) {
+ // Booked-in arrival display
+ if (flight.callsign && flight.callsign.trim()) {
+ aircraftDisplay = `
${flight.callsign}${flight.registration}`;
+ } else {
+ aircraftDisplay = `
${flight.registration}`;
+ }
+ acType = flight.type;
+
+ // Lookup airport name for in_from
+ let fromDisplay_temp = flight.in_from;
+ if (flight.in_from && flight.in_from.length === 4 && /^[A-Z]{4}$/.test(flight.in_from)) {
+ fromDisplay_temp = await getAirportDisplay(flight.in_from);
+ }
+ fromDisplay = fromDisplay_temp;
+
+ // Show ETA if available, otherwise show landed_dt
+ eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
+ pob = flight.pob || '-';
+ fuel = '-';
+ actionButtons = `
+
+
+ `;
} else {
// PPR display
if (flight.ac_call && flight.ac_call.trim()) {
@@ -2165,11 +2324,11 @@
}
// Timestamp modal functions
- function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false) {
- const targetId = pprId || (isLocalFlight ? currentLocalFlightId : currentPPRId);
+ function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false, isBookedIn = false) {
+ const targetId = pprId || (isLocalFlight ? currentLocalFlightId : (isBookedIn ? currentBookedInArrivalId : currentPPRId));
if (!targetId) return;
- pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture };
+ pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture, isBookedIn: isBookedIn };
const modalTitle = document.getElementById('timestamp-modal-title');
const submitBtn = document.getElementById('timestamp-submit-btn');
@@ -2218,12 +2377,15 @@
// Determine the correct API endpoint based on flight type
const isLocal = pendingStatusUpdate.isLocalFlight;
const isDeparture = pendingStatusUpdate.isDeparture;
+ const isBookedIn = pendingStatusUpdate.isBookedIn;
let endpoint;
if (isLocal) {
endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`;
} else if (isDeparture) {
endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`;
+ } else if (isBookedIn) {
+ endpoint = `/api/v1/arrivals/${pendingStatusUpdate.pprId}/status`;
} else {
endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
}
@@ -2247,9 +2409,14 @@
const updatedStatus = pendingStatusUpdate.status;
closeTimestampModal();
- loadPPRs(); // Refresh all tables
+ // Refresh appropriate table based on flight type
+ if (isBookedIn) {
+ loadArrivals(); // Refresh arrivals table
+ } else {
+ loadPPRs(); // Refresh all tables (PPR, local, departures)
+ }
showNotification(`Status updated to ${updatedStatus}`);
- if (!isLocal) {
+ if (!isLocal && !isBookedIn) {
closePPRModal(); // Close PPR modal after successful status update
}
} catch (error) {
@@ -2730,6 +2897,7 @@
const userManagementModal = document.getElementById('userManagementModal');
const userModal = document.getElementById('userModal');
const tableHelpModal = document.getElementById('tableHelpModal');
+ const bookInModal = document.getElementById('bookInModal');
if (event.target === pprModal) {
closePPRModal();
@@ -2746,6 +2914,9 @@
if (event.target === tableHelpModal) {
closeTableHelp();
}
+ if (event.target === bookInModal) {
+ closeBookInModal();
+ }
}
function clearArrivalAirportLookup() {
@@ -2787,6 +2958,50 @@
document.getElementById('localFlightModal').style.display = 'none';
}
+ function openBookInModal() {
+ document.getElementById('book-in-form').reset();
+ document.getElementById('book-in-id').value = '';
+ document.getElementById('bookInModal').style.display = 'block';
+
+ // Clear aircraft lookup results
+ clearBookInAircraftLookup();
+ clearBookInArrivalAirportLookup();
+
+ // Populate ETA time slots
+ populateETATimeSlots();
+
+ // Auto-focus on registration field
+ setTimeout(() => {
+ document.getElementById('book_in_registration').focus();
+ }, 100);
+ }
+
+ function closeBookInModal() {
+ document.getElementById('bookInModal').style.display = 'none';
+ }
+
+ function populateETATimeSlots() {
+ const select = document.getElementById('book_in_eta_time');
+ const next15MinSlot = getNext10MinuteSlot();
+
+ select.innerHTML = '';
+
+ for (let i = 0; i < 14; i++) {
+ const time = new Date(next15MinSlot.getTime() + i * 10 * 60 * 1000);
+ const hours = time.getHours().toString().padStart(2, '0');
+ const minutes = time.getMinutes().toString().padStart(2, '0');
+ const timeStr = `${hours}:${minutes}`;
+
+ const option = document.createElement('option');
+ option.value = timeStr;
+ option.textContent = timeStr;
+ if (i === 0) {
+ option.selected = true;
+ }
+ select.appendChild(option);
+ }
+ }
+
// Handle flight type change to show/hide destination field
function handleFlightTypeChange(flightType) {
const destGroup = document.getElementById('departure-destination-group');
@@ -2870,6 +3085,7 @@
// Local Flight Edit Modal Functions
let currentLocalFlightId = null;
+ let currentBookedInArrivalId = null;
async function openLocalFlightEditModal(flightId) {
if (!accessToken) return;
@@ -3026,6 +3242,37 @@
}
}
+ // Update status from table for booked-in arrivals
+ async function updateArrivalStatusFromTable(arrivalId, status) {
+ if (!accessToken) return;
+
+ // Show confirmation for cancel actions
+ if (status === 'CANCELLED') {
+ if (!confirm('Are you sure you want to cancel this arrival? This action cannot be easily undone.')) {
+ return;
+ }
+ }
+
+ try {
+ const response = await fetch(`/api/v1/arrivals/${arrivalId}/status`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`
+ },
+ body: JSON.stringify({ status: status })
+ });
+
+ if (!response.ok) throw new Error('Failed to update status');
+
+ loadArrivals(); // Refresh arrivals table
+ showNotification(`Arrival marked as ${status.toLowerCase()}`);
+ } catch (error) {
+ console.error('Error updating status:', error);
+ showNotification('Error updating arrival status', true);
+ }
+ }
+
// Update status from modal (uses currentLocalFlightId)
async function updateLocalFlightStatus(status) {
if (!currentLocalFlightId || !accessToken) return;
@@ -3267,6 +3514,86 @@
}
});
+ document.getElementById('book-in-form').addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ if (!accessToken) return;
+
+ const formData = new FormData(this);
+ const arrivalData = {};
+
+ formData.forEach((value, key) => {
+ // Skip the hidden id field and empty values
+ if (key === 'id') return;
+
+ // Handle time-only ETA (always today)
+ if (key === 'eta_time') {
+ if (value.trim()) {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, '0');
+ const day = String(today.getDate()).padStart(2, '0');
+ const dateStr = `${year}-${month}-${day}`;
+ // Store ETA in the eta field
+ arrivalData.eta = new Date(`${dateStr}T${value}`).toISOString();
+ }
+ return;
+ }
+
+ // Only include non-empty values
+ if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
+ if (key === 'pob') {
+ arrivalData[key] = parseInt(value);
+ } else if (value.trim) {
+ arrivalData[key] = value.trim();
+ } else {
+ arrivalData[key] = value;
+ }
+ }
+ });
+
+ // Book In uses LANDED status (they're arriving now)
+ arrivalData.status = 'LANDED';
+
+ console.log('Submitting arrivals data:', arrivalData);
+
+ try {
+ const response = await fetch('/api/v1/arrivals/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`
+ },
+ body: JSON.stringify(arrivalData)
+ });
+
+ if (!response.ok) {
+ let errorMessage = 'Failed to book in arrival';
+ try {
+ const errorData = await response.json();
+ if (errorData.detail) {
+ errorMessage = typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail);
+ } else if (errorData.errors) {
+ errorMessage = errorData.errors.map(e => e.msg).join(', ');
+ }
+ } catch (e) {
+ const text = await response.text();
+ console.error('Server response:', text);
+ errorMessage = `Server error (${response.status})`;
+ }
+ throw new Error(errorMessage);
+ }
+
+ const result = await response.json();
+ closeBookInModal();
+ loadPPRs(); // Refresh tables
+ showNotification(`Aircraft ${result.registration} booked in successfully!`);
+ } catch (error) {
+ console.error('Error booking in arrival:', error);
+ showNotification(`Error: ${error.message}`, true);
+ }
+ });
+
// Add hover listeners to all notes tooltips
function setupTooltips() {
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
diff --git a/web/index.html b/web/index.html
index d35a5bd..81d4a0d 100644
--- a/web/index.html
+++ b/web/index.html
@@ -234,8 +234,8 @@
const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
- // Refresh display when any PPR-related, local flight, or departure event occurs
- if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_') || data.type.includes('departure_'))) {
+ // Refresh display when any PPR-related, local flight, departure, or arrival event occurs
+ if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_') || data.type.includes('departure_') || data.type.includes('arrival_'))) {
console.log('Flight update detected, refreshing display...');
loadArrivals();
loadDepartures();
@@ -305,6 +305,7 @@
// Build rows asynchronously to lookup airport names
const rows = await Promise.all(arrivals.map(async (arrival) => {
const isLocal = arrival.isLocalFlight;
+ const isBookedIn = arrival.isBookedIn;
if (isLocal) {
// Local flight
@@ -321,6 +322,30 @@
${timeDisplay} |
`;
+ } else if (isBookedIn) {
+ // Booked-in arrival
+ const aircraftId = arrival.callsign || arrival.registration || '';
+ const aircraftDisplay = `${escapeHtml(aircraftId)}
(${escapeHtml(arrival.type || '')})`;
+ const fromDisplay = await getAirportName(arrival.in_from || '');
+
+ let timeDisplay;
+ if (arrival.status === 'LANDED' && arrival.landed_dt) {
+ // Show landed time if LANDED
+ const time = convertToLocalTime(arrival.landed_dt);
+ timeDisplay = `
${time}LANDED
`;
+ } else {
+ // Show ETA if BOOKED_IN
+ const time = convertToLocalTime(arrival.eta);
+ timeDisplay = `
${time}IN AIR
`;
+ }
+
+ return `
+
+ | ${aircraftDisplay} |
+ ${escapeHtml(fromDisplay)} |
+ ${timeDisplay} |
+
+ `;
} else {
// PPR
const aircraftId = arrival.ac_call || arrival.ac_reg || '';
diff --git a/web/lookups.js b/web/lookups.js
index 7aefae7..1e53ae5 100644
--- a/web/lookups.js
+++ b/web/lookups.js
@@ -191,6 +191,8 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
typeFieldId = 'ac_type';
} else if (fieldId === 'local_registration') {
typeFieldId = 'local_type';
+ } else if (fieldId === 'book_in_registration') {
+ typeFieldId = 'book_in_type';
}
if (typeFieldId) {
@@ -342,12 +344,29 @@ function initializeLookups() {
{ isAircraft: true, minLength: 4, debounceMs: 300 }
);
lookupManager.register('local-aircraft', localAircraftLookup);
+
+ const bookInAircraftLookup = createLookup(
+ 'book_in_registration',
+ 'book-in-aircraft-lookup-results',
+ null,
+ { isAircraft: true, minLength: 4, debounceMs: 300 }
+ );
+ lookupManager.register('book-in-aircraft', bookInAircraftLookup);
+
+ const bookInArrivalAirportLookup = createLookup(
+ 'book_in_from',
+ 'book-in-arrival-airport-lookup-results',
+ null,
+ { isAirport: true, minLength: 2 }
+ );
+ lookupManager.register('book-in-arrival-airport', bookInArrivalAirportLookup);
// Attach keyboard handlers to airport input fields
setTimeout(() => {
if (arrivalAirportLookup.attachKeyboardHandler) arrivalAirportLookup.attachKeyboardHandler();
if (departureAirportLookup.attachKeyboardHandler) departureAirportLookup.attachKeyboardHandler();
if (localOutToLookup.attachKeyboardHandler) localOutToLookup.attachKeyboardHandler();
+ if (bookInArrivalAirportLookup.attachKeyboardHandler) bookInArrivalAirportLookup.attachKeyboardHandler();
}, 100);
}
@@ -426,3 +445,31 @@ function selectLocalOutToAirport(icaoCode) {
function selectLocalAircraft(registration) {
lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration);
}
+
+function handleBookInAircraftLookup(value) {
+ const lookup = lookupManager.lookups['book-in-aircraft'];
+ if (lookup) lookup.handle(value);
+}
+
+function handleBookInArrivalAirportLookup(value) {
+ const lookup = lookupManager.lookups['book-in-arrival-airport'];
+ if (lookup) lookup.handle(value);
+}
+
+function clearBookInAircraftLookup() {
+ const lookup = lookupManager.lookups['book-in-aircraft'];
+ if (lookup) lookup.clear();
+}
+
+function clearBookInArrivalAirportLookup() {
+ const lookup = lookupManager.lookups['book-in-arrival-airport'];
+ if (lookup) lookup.clear();
+}
+
+function selectBookInAircraft(registration) {
+ lookupManager.selectItem('book-in-aircraft-lookup-results', 'book_in_registration', registration);
+}
+
+function selectBookInArrivalAirport(icaoCode) {
+ lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode);
+}