Flow improvements

This commit is contained in:
2026-06-29 07:15:01 -04:00
parent 8d8cb9ccad
commit 0a49dfe219
16 changed files with 281 additions and 102 deletions
+94 -36
View File
@@ -49,11 +49,11 @@
<div class="container">
<!-- Local Flights Table -->
<!-- Local Traffic Table -->
<div class="ppr-table">
<div class="table-header">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🛩️ Today's Local Flights - <span id="local-flights-count">0</span></span>
<span>🛩️ Local Traffic - <span id="local-flights-count">0</span></span>
<span class="info-icon" onclick="showTableHelp('local-flights')" title="What is this?"></span>
</div>
</div>
@@ -84,7 +84,7 @@
</div>
<div id="local-flights-no-data" class="no-data" style="display: none;">
<h3>No Local Flights</h3>
<h3>No Local Traffic</h3>
</div>
</div>
@@ -447,12 +447,11 @@
document.getElementById('departures-no-data').style.display = 'none';
try {
// Load PPR departures and airport departures simultaneously
const [pprResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
// Load PPR departures and airport departures that are still pending departure
const [pprResponse, depBookedOutResponse, depOutGroundResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000')
]);
if (!pprResponse.ok) {
@@ -462,10 +461,9 @@
const allPPRs = await pprResponse.json();
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
// Combine departures
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
const allDepartures = [...depBookedOut, ...depOutGround];
const today = new Date().toISOString().split('T')[0];
// Filter for PPR departures with ETD today and LANDED status only
@@ -478,7 +476,7 @@
return etdDate === today;
});
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
// Add departures to other airports that are not yet airborne locally
const depDepartures = allDepartures.map(flight => ({
...flight,
isDeparture: true // Flag to distinguish from PPR
@@ -502,19 +500,41 @@
document.getElementById('local-flights-no-data').style.display = 'none';
try {
const response = await authenticatedFetch('/api/v1/local-flights/?limit=1000');
const [localResponse, pprResponse, depResponse] = await Promise.all([
authenticatedFetch('/api/v1/local-flights/?limit=1000'),
authenticatedFetch('/api/v1/pprs/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
]);
if (!response.ok) {
if (!localResponse.ok || !pprResponse.ok || !depResponse.ok) {
throw new Error('Failed to fetch local flights');
}
const today = new Date().toISOString().split('T')[0];
const localFlights = (await response.json()).filter(flight => {
const localFlights = (await localResponse.json()).filter(flight => {
if (!flight.created_dt || ['CANCELLED', 'LANDED'].includes(flight.status)) return false;
return flight.created_dt.split('T')[0] === today;
});
const pprLocalTraffic = (await pprResponse.json())
.filter(ppr => {
const dateFields = [ppr.etd, ppr.landed_dt, ppr.submitted_dt];
return dateFields.some(value => value && value.split('T')[0] === today);
})
.map(ppr => ({
...ppr,
isPPRLocalTraffic: true
}));
const departureLocalTraffic = (await depResponse.json())
.filter(departure => {
const dateFields = [departure.created_dt, departure.etd, departure.takeoff_dt, departure.departed_dt];
return dateFields.some(value => value && value.split('T')[0] === today);
})
.map(departure => ({
...departure,
isDepartureLocalTraffic: true
}));
displayLocalFlights(localFlights);
displayLocalFlights([...localFlights, ...pprLocalTraffic, ...departureLocalTraffic]);
} catch (error) {
console.error('Error loading local flights:', error);
if (error.message !== 'Session expired. Please log in again.') {
@@ -610,7 +630,7 @@
}
// Load departed aircraft (DEPARTED status with departed_dt today)
// Load departed aircraft (DEPARTED status with QSY/departed time today)
async function loadDeparted() {
document.getElementById('departed-loading').style.display = 'block';
document.getElementById('departed-table-content').style.display = 'none';
@@ -631,10 +651,10 @@
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
const departed = allPPRs.filter(ppr => {
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
if (!ppr.qsy_dt || ppr.status !== 'DEPARTED') {
return false;
}
const departedDate = ppr.departed_dt.split('T')[0];
const departedDate = ppr.qsy_dt.split('T')[0];
return departedDate === today;
});
@@ -675,8 +695,8 @@
// Sort by departed time
departed.sort((a, b) => {
const aTime = a.departed_dt;
const bTime = b.departed_dt;
const aTime = a.isDeparture ? a.departed_dt : a.qsy_dt;
const bTime = b.isDeparture ? b.departed_dt : b.qsy_dt;
return parseUtcDate(aTime) - parseUtcDate(bTime);
});
@@ -721,7 +741,7 @@
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></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>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.qsy_dt)}</td>
`;
}
tbody.appendChild(row);
@@ -1200,24 +1220,52 @@
tbody.innerHTML = '';
document.getElementById('local-flights-table-content').style.display = 'block';
const circuitCounts = await loadLocalFlightCircuitCounts(localFlights);
const circuitCounts = await loadLocalFlightCircuitCounts(localFlights.filter(flight => !flight.isPPRLocalTraffic && !flight.isDepartureLocalTraffic));
for (const flight of localFlights) {
const row = document.createElement('tr');
row.onclick = () => openLocalFlightEditModal(flight.id);
const isPPR = flight.isPPRLocalTraffic;
const isDeparture = flight.isDepartureLocalTraffic;
row.onclick = () => isPPR ? openPPRModal(flight.id) : (isDeparture ? openDepartureEditModal(flight.id) : openLocalFlightEditModal(flight.id));
const aircraftDisplay = flight.callsign && flight.callsign.trim()
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
: `<strong>${flight.registration}</strong>`;
const typeIcon = flight.submitted_via === 'PUBLIC'
const aircraftDisplay = isPPR
? (flight.ac_call && flight.ac_call.trim()
? `<strong>${flight.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`
: `<strong>${flight.ac_reg}</strong>`)
: isDeparture
? (flight.callsign && flight.callsign.trim()
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
: `<strong>${flight.registration}</strong>`)
: (flight.callsign && flight.callsign.trim()
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
: `<strong>${flight.registration}</strong>`);
const typeIcon = isPPR
? '<span style="color: #032cfc; font-weight: bold; font-size: 0.9em;" title="From PPR">P</span>'
: isDeparture
? (flight.submitted_via === 'PUBLIC'
? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>'
: '<span style="color: #6f42c1; font-weight: bold; font-size: 0.9em;" title="Airport departure">D</span>')
: flight.submitted_via === 'PUBLIC'
? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>'
: '<span style="color: #228b22; font-weight: bold; font-size: 0.9em;" title="Local flight">L</span>';
const flightType = flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
const flightType = isPPR ? (flight.out_to ? `To ${flight.out_to}` : 'PPR Departure') : isDeparture ? (flight.out_to ? `To ${flight.out_to}` : 'Departure') : flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
const etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
const circuits = circuitCounts[flight.id] ?? flight.circuits ?? 0;
const circuits = (isPPR || isDeparture) ? '-' : circuitCounts[flight.id] ?? flight.circuits ?? 0;
let actionButtons = '';
if (flight.status === 'BOOKED_OUT') {
if (isPPR) {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
QSY
</button>
`;
} else if (isDeparture) {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
QSY
</button>
`;
} else if (flight.status === 'BOOKED_OUT') {
actionButtons = `
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
CONTACT
@@ -1246,10 +1294,10 @@
row.innerHTML = `
<td>${aircraftDisplay}</td>
<td style="text-align: center; width: 30px;">${typeIcon}</td>
<td>${flight.type || '-'}</td>
<td>${isPPR ? flight.ac_type || '-' : flight.type || '-'}</td>
<td>${flightType}</td>
<td>${etd}</td>
<td>${flight.pob || '-'}</td>
<td>${isPPR ? (flight.pob_out || flight.pob_in || '-') : (flight.pob || '-')}</td>
<td>${localFlightStatusBadge(flight.status)}</td>
<td>${circuits}</td>
<td style="white-space: nowrap;">${actionButtons}</td>
@@ -1495,11 +1543,21 @@
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>
`;
if (flight.status === 'LANDED') {
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('LOCAL', ${flight.id})" title="Mark as Local">
TAKE OFF
</button>
`;
} else if (flight.status === 'LOCAL') {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
QSY
</button>
`;
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
}
row.innerHTML = `
+67 -41
View File
@@ -255,11 +255,11 @@
</div>
</div>
<!-- Row 1: Local Area -->
<!-- Row 1: Local Traffic -->
<div class="atc-section">
<h2>📍 Local Area <span class="count" id="local-count">0</span></h2>
<h2>📍 Local Traffic <span class="count" id="local-count">0</span></h2>
<div class="aircraft-list" id="local-list">
<div class="no-aircraft">No aircraft in local area</div>
<div class="no-aircraft">No local traffic</div>
</div>
</div>
@@ -445,15 +445,13 @@
document.getElementById('departures-no-data').style.display = 'none';
try {
// Load PPR departures, local flight departures, and airport departures simultaneously
const [pprResponse, localBookedOutResponse, localOutGroundResponse, localLocalResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
// Load PPR departures, local flight departures, and airport departures that are still pending departure
const [pprResponse, localBookedOutResponse, localOutGroundResponse, depBookedOutResponse, depOutGroundResponse] = 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=GROUND&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000')
]);
if (!pprResponse.ok) {
@@ -463,15 +461,13 @@
const allPPRs = await pprResponse.json();
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
const localOutGround = localOutGroundResponse.ok ? await localOutGroundResponse.json() : [];
const localLocal = localLocalResponse.ok ? await localLocalResponse.json() : [];
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
// Combine local flights
const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal];
const allLocalFlights = [...localBookedOut, ...localOutGround];
// Combine departures
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
const allDepartures = [...depBookedOut, ...depOutGround];
const today = new Date().toISOString().split('T')[0];
// Filter for PPR departures with ETD today and LANDED status only
@@ -484,7 +480,7 @@
return etdDate === today;
});
// Add local flights (GROUND and LOCAL status - ready to go) - only those booked out today
// Add local flights that are not yet airborne locally - only those booked out today
const localDepartures = allLocalFlights
.filter(flight => {
// Only include flights booked out today (created_dt)
@@ -498,7 +494,7 @@
}));
departures.push(...localDepartures);
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
// Add departures to other airports that are not yet airborne locally
const depDepartures = allDepartures.map(flight => ({
...flight,
isDeparture: true // Flag to distinguish from PPR
@@ -602,7 +598,7 @@
}
// Load departed aircraft (DEPARTED status with departed_dt today)
// Load departed aircraft (DEPARTED status with QSY/departed time today)
async function loadDeparted() {
document.getElementById('departed-loading').style.display = 'block';
document.getElementById('departed-table-content').style.display = 'none';
@@ -623,10 +619,10 @@
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
const departed = allPPRs.filter(ppr => {
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
if (!ppr.qsy_dt || ppr.status !== 'DEPARTED') {
return false;
}
const departedDate = ppr.departed_dt.split('T')[0];
const departedDate = ppr.qsy_dt.split('T')[0];
return departedDate === today;
});
@@ -667,8 +663,8 @@
// Sort by departed time
departed.sort((a, b) => {
const aTime = a.departed_dt;
const bTime = b.departed_dt;
const aTime = a.isDeparture ? a.departed_dt : a.qsy_dt;
const bTime = b.isDeparture ? b.departed_dt : b.qsy_dt;
return parseUtcDate(aTime) - parseUtcDate(bTime);
});
@@ -713,7 +709,7 @@
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></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>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.qsy_dt)}</td>
`;
}
tbody.appendChild(row);
@@ -1088,7 +1084,7 @@
T&G
</button>`;
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Traffic">
LOCAL
</button>
${circuitButton}
@@ -1216,12 +1212,13 @@
</button>
`;
} else if (flight.status === 'GROUND') {
const takeoffStatus = flight.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('${takeoffStatus}', ${flight.id}, true)" title="Mark as airborne">
TAKE OFF
</button>
`;
} else if (flight.status === 'DEPARTED') {
} else if (['DEPARTED', 'LOCAL', 'CIRCUIT', 'CIRCUIT_DOWNWIND', 'CIRCUIT_BASE', 'CIRCUIT_FINAL'].includes(flight.status)) {
// Allow touch and go for all local flight types
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
T&G
@@ -1293,11 +1290,21 @@
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>
`;
if (flight.status === 'LANDED') {
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('LOCAL', ${flight.id})" title="Mark as Local">
TAKE OFF
</button>
`;
} else if (flight.status === 'LOCAL') {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
QSY
</button>
`;
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
}
row.innerHTML = `
@@ -1412,19 +1419,26 @@
// Load departing aircraft (ready to take off)
async function loadDepartingAircraft() {
try {
const [groundDeparturesResponse, groundLocalResponse] = await Promise.all([
const [pprResponse, groundDeparturesResponse, groundLocalResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000')
]);
let groundAircraft = [];
if (groundDeparturesResponse.ok) groundAircraft = await groundDeparturesResponse.json();
if (pprResponse.ok) {
const today = getLocalDateString();
groundAircraft = (await pprResponse.json())
.filter(ppr => ppr.status === 'LANDED' && ppr.etd && ppr.etd.split('T')[0] === today)
.map(ppr => ({ ...ppr, isPPR: true }));
}
if (groundDeparturesResponse.ok) groundAircraft = groundAircraft.concat(await groundDeparturesResponse.json());
if (groundLocalResponse.ok) groundAircraft = groundAircraft.concat((await groundLocalResponse.json()).map(l => ({ ...l, isLocalFlight: true })));
groundAircraft = groundAircraft.filter(ac => isTodayRecord(ac, ['created_dt', 'etd']));
displayDepartingAircraft(groundAircraft.map(ac => ({
...ac,
isDeparture: !ac.isLocalFlight
isDeparture: !ac.isLocalFlight && !ac.isPPR
})));
} catch (error) {
console.error('Error loading departing aircraft:', error);
@@ -1447,6 +1461,7 @@
const type = ac.ac_type || ac.type;
const dest = ac.out_to;
const isLocal = ac.isLocalFlight;
const isPPR = ac.isPPR;
// All aircraft in awaiting departure are in GROUND status
let takeoffOnclick, buttonText, buttonTitle, clickType;
@@ -1457,13 +1472,18 @@
buttonText = 'TAKE OFF';
buttonTitle = takeoffTitle;
clickType = 'local';
} else if (isPPR) {
takeoffOnclick = `event.stopPropagation(); showTimestampModal('LOCAL', ${ac.id})`;
buttonText = 'TAKE OFF';
buttonTitle = 'Mark as Local';
clickType = 'ppr';
} else {
takeoffOnclick = `event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('LOCAL', ${ac.id}, false, true)`;
buttonText = 'TAKE OFF';
buttonTitle = 'Mark as Local';
clickType = 'departure';
}
const itemClass = isLocal ? 'local-flight' : 'departure';
const itemClass = isLocal ? 'local-flight' : (isPPR ? 'departure' : 'departure');
return `
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${clickType}')">
@@ -1481,6 +1501,7 @@
async function loadLocalAircraft() {
try {
const response = await Promise.all([
authenticatedFetch('/api/v1/pprs/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'),
@@ -1488,12 +1509,14 @@
]);
let locals = [];
if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true }));
if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true })));
if (response[2].ok) locals = locals.concat((await response[2].json()).map(a => ({ ...a, isArrival: true })));
if (response[3].ok) locals = locals.concat((await response[3].json()).map(o => ({ ...o, isOverflight: true })));
if (response[0].ok) locals = (await response[0].json()).map(p => ({ ...p, isPPR: true }));
if (response[1].ok) locals = locals.concat((await response[1].json()).map(l => ({ ...l, isLocalFlight: true })));
if (response[2].ok) locals = locals.concat((await response[2].json()).map(d => ({ ...d, isDeparture: true })));
if (response[3].ok) locals = locals.concat((await response[3].json()).map(a => ({ ...a, isArrival: true })));
if (response[4].ok) locals = locals.concat((await response[4].json()).map(o => ({ ...o, isOverflight: true })));
locals = locals.filter(ac => {
if (ac.isOverflight) return true;
if (ac.isPPR) return isTodayRecord(ac, ['etd', 'landed_dt', 'submitted_dt']);
if (ac.isLocalFlight) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
if (ac.isArrival) return isTodayRecord(ac, ['created_dt', 'eta']);
if (ac.isDeparture) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
@@ -1513,7 +1536,7 @@
countEl.textContent = aircraft.length;
if (aircraft.length === 0) {
container.innerHTML = '<div class="no-aircraft">No aircraft in local area</div>';
container.innerHTML = '<div class="no-aircraft">No local traffic</div>';
return;
}
@@ -1522,9 +1545,12 @@
const type = ac.type || ac.ac_type || ac.aircraft_type || '';
const dest = ac.out_to;
const isDeparture = ac.isDeparture;
const isPPR = ac.isPPR;
let buttons;
if (isDeparture) {
if (isPPR) {
buttons = `<button class="status-btn" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ac.id})">QSY</button>`;
} else if (isDeparture) {
// Departure in LOCAL status - show QSY and REJOIN buttons
buttons = `
<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>
@@ -1537,9 +1563,9 @@
// Overflight in ACTIVE status - show QSY button
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentOverflightId = '${ac.id}'; showOverflightQSYModal()">QSY</button>`;
}
const itemClass = isDeparture ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
const detailsText = isDeparture ? `${type}${dest ? `${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'}${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? `${dest}` : ` Local Flight`}`));
const entityType = isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local'));
const itemClass = isDeparture || isPPR ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
const detailsText = isDeparture || isPPR ? `${type}${dest ? `${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'}${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? `${dest}` : ` Local Flight`}`));
const entityType = isPPR ? 'ppr' : (isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local')));
return `
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
+1 -1
View File
@@ -472,7 +472,7 @@
document.getElementById('phone').value = ppr.phone || '';
document.getElementById('notes').value = ppr.notes || '';
if (['CANCELED', 'DELETED', 'LANDED', 'DEPARTED'].includes(ppr.status)) {
if (['CANCELED', 'DELETED', 'LANDED', 'LOCAL', 'DEPARTED'].includes(ppr.status)) {
document.getElementById('update-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
showNotification('This PPR can no longer be edited or cancelled online.', true);
+3 -3
View File
@@ -829,10 +829,10 @@
const toDisplay = await getAirportName(departure.out_to || '');
let timeDisplay, sortTime;
if (departure.status === 'DEPARTED' && departure.departed_dt) {
const time = convertToLocalTime(departure.departed_dt);
if (departure.status === 'DEPARTED' && departure.qsy_dt) {
const time = convertToLocalTime(departure.qsy_dt);
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">DEPARTED</span></div>`;
sortTime = departure.departed_dt;
sortTime = departure.qsy_dt;
} else {
timeDisplay = convertToLocalTime(departure.etd);
sortTime = departure.etd;
+7 -5
View File
@@ -1181,7 +1181,7 @@
row.className = 'clickable-row';
row.onclick = () => openReportDetail('PPR', ppr.id);
const takeoff = ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '-';
const takeoff = ppr.takeoff_dt ? formatDateTime(ppr.takeoff_dt) : '-';
const landing = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-';
@@ -1381,7 +1381,7 @@
}
function getPPRSortTime(ppr) {
return ppr.landed_dt || ppr.departed_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
return ppr.landed_dt || ppr.qsy_dt || ppr.takeoff_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
}
const detailConfig = {
@@ -1397,7 +1397,8 @@
['Captain', r => r.captain],
['From', r => r.in_from],
['To', r => r.out_to],
['Takeoff', r => formatOptionalDateTime(r.departed_dt)],
['Takeoff', r => formatOptionalDateTime(r.takeoff_dt)],
['QSY', r => formatOptionalDateTime(r.qsy_dt)],
['Landing', r => formatOptionalDateTime(r.landed_dt)],
['ETA', r => formatOptionalDateTime(r.eta)],
['ETD', r => formatOptionalDateTime(r.etd)],
@@ -1571,7 +1572,7 @@
const headers = [
'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain',
'From', 'To', 'Takeoff', 'Landing', 'POB In', 'POB Out', 'Fuel',
'From', 'To', 'Takeoff', 'QSY', 'Landing', 'POB In', 'POB Out', 'Fuel',
'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
];
@@ -1584,7 +1585,8 @@
ppr.captain,
ppr.in_from,
ppr.out_to || '',
ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '',
ppr.takeoff_dt ? formatDateTime(ppr.takeoff_dt) : '',
ppr.qsy_dt ? formatDateTime(ppr.qsy_dt) : '',
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
ppr.pob_in,
ppr.pob_out || '',
+14 -3
View File
@@ -777,6 +777,9 @@
const ppr = await response.json();
populateForm(ppr);
const departedBtn = document.getElementById('btn-departed');
departedBtn.textContent = '🛫 Depart';
departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
// Show/hide quick action buttons based on current status
if (ppr.status === 'NEW') {
@@ -789,8 +792,16 @@
document.getElementById('btn-cancel').style.display = 'inline-block';
} else if (ppr.status === 'LANDED') {
document.getElementById('btn-landed').style.display = 'none';
document.getElementById('btn-departed').style.display = 'inline-block';
departedBtn.style.display = 'inline-block';
departedBtn.textContent = '🛫 Take Off';
departedBtn.setAttribute('onclick', "showTimestampModal('LOCAL')");
document.getElementById('btn-cancel').style.display = 'inline-block';
} else if (ppr.status === 'LOCAL') {
document.getElementById('btn-landed').style.display = 'none';
departedBtn.style.display = 'inline-block';
departedBtn.textContent = 'QSY';
departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
document.getElementById('btn-cancel').style.display = 'none';
} else {
// DEPARTED, CANCELED, DELETED - hide all quick actions and cancel button
document.querySelector('.quick-actions').style.display = 'none';
@@ -2657,8 +2668,8 @@
text: "Displays visiting aircraft and airport departures that are ready to leave Swansea today. Local flights are shown in their own table."
},
"local-flights": {
title: "Today's Local Flights",
text: "Displays local and circuit flights booked out today, with shortcuts for contact, takeoff, circuit work, touch-and-go, and landing."
title: "Local Traffic",
text: "Displays local traffic booked out today, including local flights, circuits, and PPR departures that are airborne locally before QSY."
},
overflights: {
title: "Active Overflights",