Inital stab at local flights

This commit is contained in:
2025-12-12 06:14:36 -05:00
parent 56e4ab6e3e
commit 0aeed2268a
8 changed files with 1217 additions and 84 deletions

View File

@@ -632,6 +632,9 @@
<button class="btn btn-success" onclick="openNewPPRModal()">
New PPR
</button>
<button class="btn btn-info" onclick="openLocalFlightModal()">
🛫 Book Out
</button>
<button class="btn btn-primary" onclick="window.open('reports.html', '_blank')">
📊 Reports
</button>
@@ -965,6 +968,137 @@
</div>
</div>
<!-- Local Flight (Book Out) Modal -->
<div id="localFlightModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="local-flight-modal-title">Book Out</h2>
<button class="close" onclick="closeLocalFlightModal()">&times;</button>
</div>
<div class="modal-body">
<form id="local-flight-form">
<input type="hidden" id="local-flight-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="local_registration">Aircraft Registration *</label>
<input type="text" id="local_registration" name="registration" required oninput="handleLocalAircraftLookup(this.value)">
<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">
</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">
</div>
<div class="form-group">
<label for="local_pob">Persons on Board *</label>
<input type="number" id="local_pob" name="pob" required min="1">
</div>
<div class="form-group">
<label for="local_flight_type">Flight Type *</label>
<select id="local_flight_type" name="flight_type" required>
<option value="">Select Type</option>
<option value="LOCAL">Local Flight</option>
<option value="CIRCUITS">Circuits</option>
<option value="DEPARTURE">Departure</option>
</select>
</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>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeLocalFlightModal()">
Cancel
</button>
<button type="submit" class="btn btn-success">
🛫 Book Out
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Local Flight Edit Modal -->
<div id="localFlightEditModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="local-flight-edit-title">Local Flight Details</h2>
<button class="close" onclick="closeLocalFlightEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="quick-actions">
<button id="local-btn-departed" class="btn btn-primary btn-sm" onclick="updateLocalFlightStatus('DEPARTED')" style="display: none;">
🛫 Mark Departed
</button>
<button id="local-btn-landed" class="btn btn-warning btn-sm" onclick="updateLocalFlightStatus('LANDED')" style="display: none;">
🛬 Land
</button>
<button id="local-btn-cancel" class="btn btn-danger btn-sm" onclick="updateLocalFlightStatus('CANCELLED')" style="display: none;">
❌ Cancel
</button>
</div>
<form id="local-flight-edit-form">
<input type="hidden" id="local-edit-flight-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="local_edit_registration">Aircraft Registration</label>
<input type="text" id="local_edit_registration" name="registration" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="local_edit_type">Aircraft Type</label>
<input type="text" id="local_edit_type" name="type" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="local_edit_callsign">Callsign</label>
<input type="text" id="local_edit_callsign" name="callsign">
</div>
<div class="form-group">
<label for="local_edit_pob">POB</label>
<input type="number" id="local_edit_pob" name="pob" min="1">
</div>
<div class="form-group">
<label for="local_edit_flight_type">Flight Type</label>
<select id="local_edit_flight_type" name="flight_type">
<option value="LOCAL">Local Flight</option>
<option value="CIRCUITS">Circuits</option>
<option value="DEPARTURE">Departure</option>
</select>
</div>
<div class="form-group">
<label for="local_edit_departure_dt">Departure Time</label>
<div style="display: flex; gap: 0.5rem;">
<input type="date" id="local_edit_departure_date" name="departure_date" style="flex: 1;">
<input type="time" id="local_edit_departure_time" name="departure_time" style="flex: 1;">
</div>
</div>
<div class="form-group full-width">
<label for="local_edit_notes">Notes</label>
<textarea id="local_edit_notes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeLocalFlightEditModal()">
Cancel
</button>
<button type="submit" class="btn btn-success">
💾 Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="modal">
<div class="modal-content">
@@ -1508,22 +1642,24 @@
await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked(), loadUpcoming()]);
}
// Load arrivals (NEW and CONFIRMED status)
// Load arrivals (NEW and CONFIRMED status for PPR, DEPARTED for local flights)
async function loadArrivals() {
document.getElementById('arrivals-loading').style.display = 'block';
document.getElementById('arrivals-table-content').style.display = 'none';
document.getElementById('arrivals-no-data').style.display = 'none';
try {
// Load all PPRs and filter client-side for today's arrivals
// We filter by ETA date (not ETD) and NEW/CONFIRMED status
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
// Load PPRs and local flights that are in the air
const [pprResponse, localResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
]);
if (!response.ok) {
if (!pprResponse.ok) {
throw new Error('Failed to fetch arrivals');
}
const allPPRs = await response.json();
const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for arrivals with ETA today and NEW or CONFIRMED status
@@ -1536,6 +1672,16 @@
return etaDate === today;
});
// Add local flights in DEPARTED status (in the air, heading back)
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localInAir = localFlights.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
arrivals.push(...localInAir);
}
displayArrivals(arrivals);
} catch (error) {
console.error('Error loading arrivals:', error);
@@ -1547,25 +1693,27 @@
document.getElementById('arrivals-loading').style.display = 'none';
}
// Load departures (LANDED status)
// Load departures (LANDED status for PPR, BOOKED_OUT only for local flights)
async function loadDepartures() {
document.getElementById('departures-loading').style.display = 'block';
document.getElementById('departures-table-content').style.display = 'none';
document.getElementById('departures-no-data').style.display = 'none';
try {
// Load all PPRs and filter client-side for today's departures
// We filter by ETD date and LANDED status only (exclude DEPARTED)
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
// Load PPR departures and local flight departures (BOOKED_OUT only) simultaneously
const [pprResponse, localResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000')
]);
if (!response.ok) {
throw new Error('Failed to fetch departures');
if (!pprResponse.ok) {
throw new Error('Failed to fetch PPR departures');
}
const allPPRs = await response.json();
const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for departures with ETD today and LANDED status only
// Filter for PPR departures with ETD today and LANDED status only
const departures = allPPRs.filter(ppr => {
if (!ppr.etd || ppr.status !== 'LANDED') {
return false;
@@ -1575,6 +1723,16 @@
return etdDate === today;
});
// Add local flights (BOOKED_OUT status - ready to go)
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localDepartures = localFlights.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
departures.push(...localDepartures);
}
displayDepartures(departures);
} catch (error) {
console.error('Error loading departures:', error);
@@ -1593,16 +1751,19 @@
document.getElementById('departed-no-data').style.display = 'none';
try {
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
const [pprResponse, localResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
]);
if (!response.ok) {
if (!pprResponse.ok) {
throw new Error('Failed to fetch departed aircraft');
}
const allPPRs = await response.json();
const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for aircraft departed today
// Filter for PPRs departed today
const departed = allPPRs.filter(ppr => {
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
return false;
@@ -1611,6 +1772,20 @@
return departedDate === today;
});
// Add local flights departed today
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localDeparted = localFlights.filter(flight => {
if (!flight.departure_dt) return false;
const departedDate = flight.departure_dt.split('T')[0];
return departedDate === today;
}).map(flight => ({
...flight,
isLocalFlight: true
}));
departed.push(...localDeparted);
}
displayDeparted(departed);
} catch (error) {
console.error('Error loading departed aircraft:', error);
@@ -1632,22 +1807,43 @@
}
// Sort by departed time
departed.sort((a, b) => new Date(a.departed_dt) - new Date(b.departed_dt));
departed.sort((a, b) => {
const aTime = a.departed_dt || a.departure_dt;
const bTime = b.departed_dt || b.departure_dt;
return new Date(aTime) - new Date(bTime);
});
tbody.innerHTML = '';
document.getElementById('departed-table-content').style.display = 'block';
for (const ppr of departed) {
for (const flight of departed) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
const isLocal = flight.isLocalFlight;
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
};
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
row.innerHTML = `
<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_call || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.out_to || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(ppr.departed_dt)}</td>
`;
if (isLocal) {
row.innerHTML = `
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.registration || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.callsign || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">-</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departure_dt)}</td>
`;
} else {
row.innerHTML = `
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_reg || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_call || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
`;
}
tbody.appendChild(row);
}
}
@@ -1885,47 +2081,89 @@
return;
}
// Sort arrivals by ETA (ascending)
// Sort arrivals by ETA/departure time (ascending)
arrivals.sort((a, b) => {
if (!a.eta) return 1;
if (!b.eta) return -1;
return new Date(a.eta) - new Date(b.eta);
const aTime = a.eta || a.departure_dt;
const bTime = b.eta || b.departure_dt;
if (!aTime) return 1;
if (!bTime) return -1;
return new Date(aTime) - new Date(bTime);
});
tbody.innerHTML = '';
document.getElementById('arrivals-table-content').style.display = 'block';
for (const ppr of arrivals) {
for (const flight of arrivals) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
const isLocal = flight.isLocalFlight;
// Click handler that routes to correct modal
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
};
// Create notes indicator if notes exist
const notesIndicator = ppr.notes && ppr.notes.trim() ?
const notesIndicator = flight.notes && flight.notes.trim() ?
`<span class="notes-tooltip">
<span class="notes-indicator">📝</span>
<span class="tooltip-text">${ppr.notes}</span>
<span class="tooltip-text">${flight.notes}</span>
</span>` : '';
// Display callsign as main item if present, registration below; otherwise show registration
const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
`<strong>${ppr.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${ppr.ac_reg}</span>` :
`<strong>${ppr.ac_reg}</strong>`;
// Lookup airport name for in_from
let fromDisplay = ppr.in_from;
if (ppr.in_from && ppr.in_from.length === 4 && /^[A-Z]{4}$/.test(ppr.in_from)) {
fromDisplay = await getAirportDisplay(ppr.in_from);
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${ppr.ac_type}</td>
<td>${fromDisplay}</td>
<td>${formatTimeOnly(ppr.eta)}</td>
<td>${ppr.pob_in}</td>
<td>${ppr.fuel || '-'}</td>
<td>
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${ppr.id})" title="Mark as Landed">
let aircraftDisplay, acType, fromDisplay, eta, pob, fuel, actionButtons;
if (isLocal) {
// Local flight display
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
acType = flight.type;
fromDisplay = '-';
eta = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
pob = flight.pob || '-';
fuel = '-';
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Arrival">
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
CANCEL
</button>
</td>
`;
} else {
// PPR display
const callsign = flight.ac_call && flight.ac_call.trim() ? flight.ac_call : flight.ac_reg;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
acType = flight.ac_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;
eta = formatTimeOnly(flight.eta);
pob = flight.pob_in;
fuel = flight.fuel || '-';
actionButtons = `
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${flight.id})" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${flight.id}, 'CANCELED')" title="Cancel Arrival">
CANCEL
</button>
`;
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${acType}</td>
<td>${fromDisplay}</td>
<td>${eta}</td>
<td>${pob}</td>
<td>${fuel}</td>
<td>${actionButtons}</td>
`;
tbody.appendChild(row);
}
@@ -1944,46 +2182,100 @@
// Sort departures by ETD (ascending), nulls last
departures.sort((a, b) => {
if (!a.etd) return 1;
if (!b.etd) return -1;
return new Date(a.etd) - new Date(b.etd);
const aTime = a.etd || a.booked_out_dt;
const bTime = b.etd || b.booked_out_dt;
if (!aTime) return 1;
if (!bTime) return -1;
return new Date(aTime) - new Date(bTime);
});
tbody.innerHTML = '';
document.getElementById('departures-table-content').style.display = 'block';
for (const ppr of departures) {
for (const flight of departures) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
const isLocal = flight.isLocalFlight;
// Click handler that routes to correct modal
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
};
// Create notes indicator if notes exist
const notesIndicator = ppr.notes && ppr.notes.trim() ?
const notesIndicator = flight.notes && flight.notes.trim() ?
`<span class="notes-tooltip">
<span class="notes-indicator">📝</span>
<span class="tooltip-text">${ppr.notes}</span>
<span class="tooltip-text">${flight.notes}</span>
</span>` : '';
// Display callsign as main item if present, registration below; otherwise show registration
const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
`<strong>${ppr.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${ppr.ac_reg}</span>` :
`<strong>${ppr.ac_reg}</strong>`;
// Lookup airport name for out_to
let toDisplay = ppr.out_to || '-';
if (ppr.out_to && ppr.out_to.length === 4 && /^[A-Z]{4}$/.test(ppr.out_to)) {
toDisplay = await getAirportDisplay(ppr.out_to);
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${ppr.ac_type}</td>
<td>${toDisplay}</td>
<td>${ppr.etd ? formatTimeOnly(ppr.etd) : '-'}</td>
<td>${ppr.pob_out || ppr.pob_in}</td>
<td>${ppr.fuel || '-'}</td>
<td>${ppr.landed_dt ? formatTimeOnly(ppr.landed_dt) : '-'}</td>
<td>
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ppr.id})" title="Mark as Departed">
let aircraftDisplay, toDisplay, etd, pob, fuel, landedDt, actionButtons;
if (isLocal) {
// Local flight display
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
toDisplay = '-';
etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
pob = flight.pob || '-';
fuel = '-';
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
// Action buttons for local flight
if (flight.status === 'BOOKED_OUT') {
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'DEPARTED')" title="Mark as Departed">
TAKE OFF
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
CANCEL
</button>
`;
} else if (flight.status === 'DEPARTED') {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
CANCEL
</button>
`;
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
} else {
// PPR display
const callsign = flight.ac_call && flight.ac_call.trim() ? flight.ac_call : flight.ac_reg;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
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.etd ? formatTimeOnly(flight.etd) : '-';
pob = flight.pob_out || flight.pob_in;
fuel = flight.fuel || '-';
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
TAKE OFF
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Departure">
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${flight.id}, 'CANCELED')" title="Cancel Departure">
CANCEL
</button>
</td>
`;
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${isLocal ? flight.type : flight.ac_type}</td>
<td>${toDisplay}</td>
<td>${etd}</td>
<td>${pob}</td>
<td>${fuel}</td>
<td>${landedDt}</td>
<td>${actionButtons}</td>
`;
tbody.appendChild(row);
}
@@ -3114,6 +3406,337 @@
tooltip.style.top = top + 'px';
}
// Local Flight (Book Out) Modal Functions
function openLocalFlightModal() {
document.getElementById('local-flight-form').reset();
document.getElementById('local-flight-id').value = '';
document.getElementById('local-flight-modal-title').textContent = 'Book Out';
document.getElementById('localFlightModal').style.display = 'block';
// Clear aircraft lookup results
clearLocalAircraftLookup();
// Auto-focus on registration field
setTimeout(() => {
document.getElementById('local_registration').focus();
}, 100);
}
function closeLocalFlightModal() {
document.getElementById('localFlightModal').style.display = 'none';
}
// Handle aircraft lookup for local flights
let localAircraftLookupTimeout;
function handleLocalAircraftLookup(registration) {
// Clear previous timeout
if (localAircraftLookupTimeout) {
clearTimeout(localAircraftLookupTimeout);
}
// Clear results if input is too short
if (registration.length < 4) {
clearLocalAircraftLookup();
return;
}
// Show searching indicator
document.getElementById('local-aircraft-lookup-results').innerHTML =
'<div class="aircraft-searching">Searching...</div>';
// Debounce the search - wait 300ms after user stops typing
localAircraftLookupTimeout = setTimeout(() => {
performLocalAircraftLookup(registration);
}, 300);
}
async function performLocalAircraftLookup(registration) {
try {
// Clean the input (remove non-alphanumeric characters and make uppercase)
const cleanInput = registration.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
if (cleanInput.length < 4) {
clearLocalAircraftLookup();
return;
}
// Call the API
const response = await authenticatedFetch(`/api/v1/aircraft/lookup/${cleanInput}`);
if (!response.ok) {
throw new Error('Failed to fetch aircraft data');
}
const matches = await response.json();
displayLocalAircraftLookupResults(matches, cleanInput);
} catch (error) {
console.error('Aircraft lookup error:', error);
document.getElementById('local-aircraft-lookup-results').innerHTML =
'<div class="aircraft-no-match">Lookup failed - please enter manually</div>';
}
}
function displayLocalAircraftLookupResults(matches, searchTerm) {
const resultsDiv = document.getElementById('local-aircraft-lookup-results');
if (matches.length === 0) {
resultsDiv.innerHTML = '<div class="aircraft-no-match">No matches found</div>';
} else if (matches.length === 1) {
// Unique match found - auto-populate
const aircraft = matches[0];
resultsDiv.innerHTML = `
<div class="aircraft-match">
${aircraft.manufacturer_name} ${aircraft.model} (${aircraft.type_code})
</div>
`;
// Auto-populate the form fields
document.getElementById('local_registration').value = aircraft.registration;
document.getElementById('local_type').value = aircraft.type_code;
} else {
// Multiple matches - show list but don't auto-populate
resultsDiv.innerHTML = `
<div class="aircraft-no-match">
Multiple matches found (${matches.length}) - please be more specific
</div>
`;
}
}
function clearLocalAircraftLookup() {
document.getElementById('local-aircraft-lookup-results').innerHTML = '';
}
// Local Flight Edit Modal Functions
let currentLocalFlightId = null;
async function openLocalFlightEditModal(flightId) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/local-flights/${flightId}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) throw new Error('Failed to load flight');
const flight = await response.json();
currentLocalFlightId = flight.id;
// Populate form
document.getElementById('local-edit-flight-id').value = flight.id;
document.getElementById('local_edit_registration').value = flight.registration;
document.getElementById('local_edit_type').value = flight.type;
document.getElementById('local_edit_callsign').value = flight.callsign || '';
document.getElementById('local_edit_pob').value = flight.pob;
document.getElementById('local_edit_flight_type').value = flight.flight_type;
document.getElementById('local_edit_notes').value = flight.notes || '';
// Parse and populate departure time if exists
if (flight.departure_dt) {
const dept = new Date(flight.departure_dt);
document.getElementById('local_edit_departure_date').value = dept.toISOString().slice(0, 10);
document.getElementById('local_edit_departure_time').value = dept.toISOString().slice(11, 16);
}
// Show/hide action buttons based on status
const deptBtn = document.getElementById('local-btn-departed');
const landBtn = document.getElementById('local-btn-landed');
const cancelBtn = document.getElementById('local-btn-cancel');
deptBtn.style.display = flight.status === 'BOOKED_OUT' ? 'inline-block' : 'none';
landBtn.style.display = flight.status === 'DEPARTED' ? 'inline-block' : 'none';
cancelBtn.style.display = (flight.status === 'BOOKED_OUT' || flight.status === 'DEPARTED') ? 'inline-block' : 'none';
document.getElementById('local-flight-edit-title').textContent = `${flight.registration} - ${flight.flight_type}`;
document.getElementById('localFlightEditModal').style.display = 'block';
} catch (error) {
console.error('Error loading flight:', error);
showNotification('Error loading flight details', true);
}
}
function closeLocalFlightEditModal() {
document.getElementById('localFlightEditModal').style.display = 'none';
currentLocalFlightId = null;
}
// Update status from table buttons (with flight ID passed)
async function updateLocalFlightStatusFromTable(flightId, status) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/local-flights/${flightId}/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(`Flight marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating flight status', true);
}
}
// Update status from modal (uses currentLocalFlightId)
async function updateLocalFlightStatus(status) {
if (!currentLocalFlightId || !accessToken) return;
try {
const response = await fetch(`/api/v1/local-flights/${currentLocalFlightId}/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');
closeLocalFlightEditModal();
loadPPRs(); // Refresh display
showNotification(`Flight marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating flight status', true);
}
}
// Local flight edit form submission
document.getElementById('local-flight-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!currentLocalFlightId || !accessToken) return;
const formData = new FormData(this);
const updateData = {};
formData.forEach((value, key) => {
if (key === 'id') return;
// Handle date/time combination for departure
if (key === 'departure_date' || key === 'departure_time') {
if (!updateData.departure_dt && formData.get('departure_date') && formData.get('departure_time')) {
const dateStr = formData.get('departure_date');
const timeStr = formData.get('departure_time');
updateData.departure_dt = new Date(`${dateStr}T${timeStr}`).toISOString();
}
return;
}
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (key === 'pob') {
updateData[key] = parseInt(value);
} else if (value.trim) {
updateData[key] = value.trim();
} else {
updateData[key] = value;
}
}
});
try {
const response = await fetch(`/api/v1/local-flights/${currentLocalFlightId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(updateData)
});
if (!response.ok) throw new Error('Failed to update flight');
closeLocalFlightEditModal();
loadPPRs(); // Refresh display
showNotification('Flight updated successfully');
} catch (error) {
console.error('Error updating flight:', error);
showNotification('Error updating flight', true);
}
});
// Add event listener for local flight form submission
document.getElementById('local-flight-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!accessToken) return;
const formData = new FormData(this);
const flightData = {};
formData.forEach((value, key) => {
// Skip the hidden id field and empty values
if (key === 'id') return;
// Handle date/time combination for departure
if (key === 'departure_date' || key === 'departure_time') {
if (!flightData.departure_dt && formData.get('departure_date') && formData.get('departure_time')) {
const dateStr = formData.get('departure_date');
const timeStr = formData.get('departure_time');
flightData.departure_dt = new Date(`${dateStr}T${timeStr}`).toISOString();
}
return;
}
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (key === 'pob') {
flightData[key] = parseInt(value);
} else if (value.trim) {
flightData[key] = value.trim();
} else {
flightData[key] = value;
}
}
});
console.log('Submitting flight data:', flightData);
try {
const response = await fetch('/api/v1/local-flights/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(flightData)
});
if (!response.ok) {
let errorMessage = 'Failed to book out flight';
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorMessage;
} 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();
closeLocalFlightModal();
loadPPRs(); // Refresh tables
showNotification(`Aircraft ${result.registration} booked out successfully!`);
} catch (error) {
console.error('Error booking out flight:', error);
showNotification(`Error: ${error.message}`, true);
}
});
// Add hover listeners to all notes tooltips
function setupTooltips() {
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {