Getting there
This commit is contained in:
249
web/admin.html
249
web/admin.html
@@ -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);
|
||||
|
||||
@@ -234,9 +234,9 @@
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket message received:', data);
|
||||
|
||||
// Refresh display when any PPR-related event occurs
|
||||
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) {
|
||||
console.log('PPR update detected, refreshing display...');
|
||||
// Refresh display when any PPR-related or local flight event occurs
|
||||
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_'))) {
|
||||
console.log('Flight update detected, refreshing display...');
|
||||
loadArrivals();
|
||||
loadDepartures();
|
||||
}
|
||||
@@ -375,6 +375,7 @@
|
||||
// Build rows asynchronously to lookup airport names
|
||||
const rows = await Promise.all(departures.map(async (departure) => {
|
||||
const isLocal = departure.isLocalFlight;
|
||||
const isDeparture = departure.isDeparture;
|
||||
|
||||
if (isLocal) {
|
||||
// Local flight
|
||||
@@ -384,6 +385,21 @@
|
||||
const time = convertToLocalTime(departure.etd);
|
||||
const timeDisplay = `<div>${escapeHtml(time)}</div>`;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${aircraftDisplay}</td>
|
||||
<td>${toDisplay}</td>
|
||||
<td>${timeDisplay}</td>
|
||||
</tr>
|
||||
`;
|
||||
} else if (isDeparture) {
|
||||
// Departure to other airport
|
||||
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 time = convertToLocalTime(departure.etd);
|
||||
const timeDisplay = `<div>${escapeHtml(time)}</div>`;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${aircraftDisplay}</td>
|
||||
|
||||
Reference in New Issue
Block a user