Inital stab at local flights
This commit is contained in:
789
web/admin.html
789
web/admin.html
@@ -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()">×</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()">×</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 => {
|
||||
|
||||
Reference in New Issue
Block a user