Getting there

This commit is contained in:
2025-12-12 11:18:28 -05:00
parent f7467690e4
commit dbb285fa20
15 changed files with 1080 additions and 36 deletions

View File

@@ -524,7 +524,7 @@
}
/* Airport Lookup Styles */
#arrival-airport-lookup-results, #departure-airport-lookup-results {
#arrival-airport-lookup-results, #departure-airport-lookup-results, #local-out-to-lookup-results {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f8f9fa;
@@ -986,12 +986,12 @@
<div id="local-aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="local_type">Aircraft Type *</label>
<input type="text" id="local_type" name="type" required tabindex="-1" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
<label for="local_type">Aircraft Type</label>
<input type="text" id="local_type" name="type" tabindex="4" placeholder="e.g., C172, PA34, AA5">
</div>
<div class="form-group">
<label for="local_callsign">Callsign (optional)</label>
<input type="text" id="local_callsign" name="callsign" placeholder="If different from registration" tabindex="4">
<input type="text" id="local_callsign" name="callsign" placeholder="If different from registration" tabindex="6">
</div>
<div class="form-group">
<label for="local_pob">Persons on Board *</label>
@@ -999,12 +999,17 @@
</div>
<div class="form-group">
<label for="local_flight_type">Flight Type *</label>
<select id="local_flight_type" name="flight_type" required tabindex="3">
<select id="local_flight_type" name="flight_type" required tabindex="5" onchange="handleFlightTypeChange(this.value)">
<option value="LOCAL">Local Flight</option>
<option value="CIRCUITS">Circuits</option>
<option value="DEPARTURE">Departure</option>
<option value="DEPARTURE">Departure to Other Airport</option>
</select>
</div>
<div class="form-group" id="departure-destination-group" style="display: none;">
<label for="local_out_to" id="departure-destination-label">Destination Airport</label>
<input type="text" id="local_out_to" name="out_to" placeholder="ICAO Code or Airport Name" oninput="handleLocalOutToAirportLookup(this.value)" tabindex="3">
<div id="local-out-to-lookup-results"></div>
</div>
<div class="form-group full-width">
<label for="local_notes">Notes</label>
<textarea id="local_notes" name="notes" rows="3" placeholder="e.g., destination, any special requirements"></textarea>
@@ -1304,6 +1309,27 @@
loadPPRs();
showNotification('Data updated');
}
// Refresh local flights when any local flight event occurs
if (data.type && (data.type.includes('local_flight_'))) {
console.log('Local flight update detected, refreshing...');
loadLocalFlights();
showNotification('Local flight updated');
}
// Refresh departures when any departure event occurs
if (data.type && (data.type.includes('departure_'))) {
console.log('Departure update detected, refreshing...');
loadDepartures();
showNotification('Departure updated');
}
// Refresh arrivals when any arrival event occurs
if (data.type && (data.type.includes('arrival_'))) {
console.log('Arrival update detected, refreshing...');
loadArrivals();
showNotification('Arrival updated');
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
@@ -1521,8 +1547,8 @@
openNewPPRModal();
}
// Press 'b' to book out local flight (LOCAL type)
if (e.key === 'b' || e.key === 'B') {
// Press 'o' to book out local flight (LOCAL type)
if (e.key === 'o' || e.key === 'O') {
e.preventDefault();
openLocalFlightModal('LOCAL');
}
@@ -1724,10 +1750,11 @@
document.getElementById('departures-no-data').style.display = 'none';
try {
// Load PPR departures and local flight departures (BOOKED_OUT only) simultaneously
const [pprResponse, localResponse] = await Promise.all([
// Load PPR departures, local flight departures, and airport departures simultaneously
const [pprResponse, localResponse, depResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000')
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000')
]);
if (!pprResponse.ok) {
@@ -1757,6 +1784,16 @@
departures.push(...localDepartures);
}
// Add departures to other airports (BOOKED_OUT status)
if (depResponse.ok) {
const depFlights = await depResponse.json();
const depDepartures = depFlights.map(flight => ({
...flight,
isDeparture: true // Flag to distinguish from PPR
}));
departures.push(...depDepartures);
}
displayDepartures(departures);
} catch (error) {
console.error('Error loading departures:', error);
@@ -2223,11 +2260,14 @@
for (const flight of departures) {
const row = document.createElement('tr');
const isLocal = flight.isLocalFlight;
const isDeparture = flight.isDeparture;
// Click handler that routes to correct modal
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else if (isDeparture) {
// TODO: Open departure edit modal
} else {
openPPRModal(flight.id);
}
@@ -2277,6 +2317,37 @@
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
} else if (isDeparture) {
// Departure to other airport display
if (flight.callsign && flight.callsign.trim()) {
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
} else {
aircraftDisplay = `<strong>${flight.registration}</strong>`;
}
toDisplay = flight.out_to || '-';
if (flight.out_to && flight.out_to.length === 4 && /^[A-Z]{4}$/.test(flight.out_to)) {
toDisplay = await getAirportDisplay(flight.out_to);
}
etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
pob = flight.pob || '-';
fuel = '-';
landedDt = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
// Action buttons for departure
if (flight.status === 'BOOKED_OUT') {
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
TAKE OFF
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateDepartureStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel">
CANCEL
</button>
`;
} else if (flight.status === 'DEPARTED') {
actionButtons = '<span style="color: #999;">Departed</span>';
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
} else {
// PPR display
if (flight.ac_call && flight.ac_call.trim()) {
@@ -2305,7 +2376,7 @@
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${isLocal ? flight.type : flight.ac_type}</td>
<td>${isLocal ? flight.type : isDeparture ? flight.type : flight.ac_type}</td>
<td>${toDisplay}</td>
<td>${etd}</td>
<td>${pob}</td>
@@ -2575,11 +2646,11 @@
}
// Timestamp modal functions
function showTimestampModal(status, pprId = null, isLocalFlight = false) {
function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false) {
const targetId = pprId || (isLocalFlight ? currentLocalFlightId : currentPPRId);
if (!targetId) return;
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight };
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture };
const modalTitle = document.getElementById('timestamp-modal-title');
const submitBtn = document.getElementById('timestamp-submit-btn');
@@ -2627,9 +2698,16 @@
try {
// Determine the correct API endpoint based on flight type
const isLocal = pendingStatusUpdate.isLocalFlight;
const endpoint = isLocal ?
`/api/v1/local-flights/${pendingStatusUpdate.pprId}/status` :
`/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
const isDeparture = pendingStatusUpdate.isDeparture;
let endpoint;
if (isLocal) {
endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`;
} else if (isDeparture) {
endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`;
} else {
endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
}
const response = await fetch(endpoint, {
method: 'PATCH',
@@ -3405,6 +3483,79 @@
document.getElementById('departure-airport-lookup-results').innerHTML = '';
}
function clearLocalOutToAirportLookup() {
document.getElementById('local-out-to-lookup-results').innerHTML = '';
}
// Airport lookup for Book Out modal departure field
function handleLocalOutToAirportLookup(value) {
if (!value.trim()) {
clearLocalOutToAirportLookup();
return;
}
performLocalOutToAirportLookup(value);
}
async function performLocalOutToAirportLookup(codeOrName) {
try {
const cleanInput = codeOrName.trim();
if (cleanInput.length < 2) {
clearLocalOutToAirportLookup();
return;
}
// Call the airport lookup API using same endpoint as PPR modal
const response = await authenticatedFetch(`/api/v1/airport/lookup/${encodeURIComponent(cleanInput)}`);
if (!response.ok) {
throw new Error('Failed to fetch airport data');
}
const matches = await response.json();
displayLocalOutToAirportLookupResults(matches, cleanInput);
} catch (error) {
console.error('Local out-to airport lookup error:', error);
document.getElementById('local-out-to-lookup-results').innerHTML =
'<div class="airport-no-match">Lookup failed - will use as entered</div>';
}
}
function displayLocalOutToAirportLookupResults(matches, searchTerm) {
const resultsDiv = document.getElementById('local-out-to-lookup-results');
if (!matches || matches.length === 0) {
resultsDiv.innerHTML = '<div class="airport-no-match">No matches found - will use as entered</div>';
} else {
// Show matches as clickable options (single or multiple)
const matchText = matches.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:';
const listHtml = matches.map(airport => `
<div class="airport-option" onclick="selectLocalOutToAirport('${airport.icao}')">
<div>
<div class="airport-code">${airport.icao}</div>
<div class="airport-name">${airport.name}</div>
${airport.city ? `<div class="airport-location">${airport.city}, ${airport.country}</div>` : ''}
</div>
</div>
`).join('');
resultsDiv.innerHTML = `
<div class="airport-no-match" style="margin-bottom: 0.5rem;">
${matchText}
</div>
<div class="airport-list">
${listHtml}
</div>
`;
}
}
function selectLocalOutToAirport(icaoCode) {
document.getElementById('local_out_to').value = icaoCode;
clearLocalOutToAirportLookup();
}
// Airport selection functions
function selectArrivalAirport(icaoCode) {
document.getElementById('in_from').value = icaoCode;
@@ -3461,6 +3612,9 @@
// Clear aircraft lookup results
clearLocalAircraftLookup();
// Update destination field visibility based on flight type
handleFlightTypeChange(flightType);
// Auto-focus on registration field
setTimeout(() => {
document.getElementById('local_registration').focus();
@@ -3471,6 +3625,24 @@
document.getElementById('localFlightModal').style.display = 'none';
}
// Handle flight type change to show/hide destination field
function handleFlightTypeChange(flightType) {
const destGroup = document.getElementById('departure-destination-group');
const destInput = document.getElementById('local_out_to');
const destLabel = document.getElementById('departure-destination-label');
if (flightType === 'DEPARTURE') {
destGroup.style.display = 'block';
destInput.required = true;
destLabel.textContent = 'Destination Airport *';
} else {
destGroup.style.display = 'none';
destInput.required = false;
destInput.value = '';
destLabel.textContent = 'Destination Airport';
}
}
// Handle aircraft lookup for local flights
let localAircraftLookupTimeout;
function handleLocalAircraftLookup(registration) {
@@ -3632,6 +3804,30 @@
}
}
// Update status from table for departures
async function updateDepartureStatusFromTable(departureId, status) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/departures/${departureId}/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');
loadPPRs(); // Refresh display
showNotification(`Departure marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating departure status', true);
}
}
// Update status from modal (uses currentLocalFlightId)
async function updateLocalFlightStatus(status) {
if (!currentLocalFlightId || !accessToken) return;
@@ -3719,7 +3915,9 @@
if (!accessToken) return;
const formData = new FormData(this);
const flightType = formData.get('flight_type');
const flightData = {};
let endpoint = '/api/v1/local-flights/';
formData.forEach((value, key) => {
// Skip the hidden id field and empty values
@@ -3747,10 +3945,17 @@
}
});
console.log('Submitting flight data:', flightData);
// If DEPARTURE flight type, use departures endpoint instead
if (flightType === 'DEPARTURE') {
endpoint = '/api/v1/departures/';
// Remove flight_type from data and use out_to instead
delete flightData.flight_type;
}
console.log(`Submitting ${endpoint} data:`, flightData);
try {
const response = await fetch('/api/v1/local-flights/', {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -3763,7 +3968,11 @@
let errorMessage = 'Failed to book out flight';
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorMessage;
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);