Compare commits

..

2 Commits

Author SHA1 Message Date
98d0e3cfd7 Help text 2025-12-16 07:15:48 -05:00
d2e7d3c3dd Booking out improvements 2025-12-16 06:41:18 -05:00
3 changed files with 313 additions and 25 deletions

View File

@@ -10,7 +10,7 @@ from app.schemas.ppr import PPRPublic
from app.models.local_flight import LocalFlightStatus
from app.models.departure import DepartureStatus
from app.models.arrival import ArrivalStatus
from datetime import date
from datetime import date, datetime, timedelta
router = APIRouter()
@@ -34,15 +34,23 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
'isLocalFlight': False
})
# Add local flights with DEPARTED status
# Add local flights with DEPARTED status that were booked out today
local_flights = crud_local_flight.get_multi(
db,
status=LocalFlightStatus.DEPARTED,
limit=1000
)
# Get today's date boundaries
today = date.today()
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today + timedelta(days=1), datetime.min.time())
# Convert local flights to match the PPR format for display
for flight in local_flights:
# Only include flights booked out today
if not (today_start <= flight.created_dt < today_end):
continue
arrivals_list.append({
'ac_call': flight.callsign or flight.registration,
'ac_reg': flight.registration,
@@ -78,15 +86,23 @@ async def get_public_departures(db: Session = Depends(get_db)):
'isDeparture': False
})
# Add local flights with BOOKED_OUT status
# Add local flights with BOOKED_OUT status that were booked out today
local_flights = crud_local_flight.get_multi(
db,
status=LocalFlightStatus.BOOKED_OUT,
limit=1000
)
# Get today's date boundaries
today = date.today()
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today + timedelta(days=1), datetime.min.time())
# Convert local flights to match the PPR format for display
for flight in local_flights:
# Only include flights booked out today
if not (today_start <= flight.created_dt < today_end):
continue
departures_list.append({
'ac_call': flight.callsign or flight.registration,
'ac_reg': flight.registration,

View File

@@ -147,6 +147,18 @@ body {
font-weight: 500;
}
.info-icon {
display: inline-block;
cursor: pointer;
font-size: 1.2rem;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.info-icon:hover {
opacity: 1;
}
.table-header-collapsible {
background: #34495e;
color: white;
@@ -364,6 +376,16 @@ tbody tr:hover {
padding: 1.5rem;
}
.modal-footer {
padding: 1rem;
text-align: right;
border-top: 1px solid #ddd;
}
.modal-footer .btn {
margin-left: 0.5rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@@ -40,7 +40,10 @@
<!-- Arrivals Table -->
<div class="ppr-table">
<div class="table-header">
🛬 Today's Pending Arrivals - <span id="arrivals-count">0</span>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🛬 Today's Pending Arrivals - <span id="arrivals-count">0</span></span>
<span class="info-icon" onclick="showTableHelp('arrivals')" title="What is this?"></span>
</div>
</div>
<div id="arrivals-loading" class="loading">
@@ -74,7 +77,10 @@
<!-- Departures Table -->
<div class="ppr-table" style="margin-top: 2rem;">
<div class="table-header">
🛫 Today's Pending Departures - <span id="departures-count">0</span>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🛫 Today's Pending Departures - <span id="departures-count">0</span></span>
<span class="info-icon" onclick="showTableHelp('departures')" title="What is this?"></span>
</div>
</div>
<div id="departures-loading" class="loading">
@@ -112,7 +118,10 @@
<!-- Departed Today -->
<div class="ppr-table">
<div class="table-header" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">
✈️ Departed Today - <span id="departed-count">0</span>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>✈️ Departed Today - <span id="departed-count">0</span></span>
<span class="info-icon" onclick="showTableHelp('departed')" title="What is this?" style="font-size: 1.1rem; cursor: pointer;"></span>
</div>
</div>
<div id="departed-loading" class="loading" style="display: none;">
@@ -143,7 +152,10 @@
<!-- Parked Visitors -->
<div class="ppr-table">
<div class="table-header" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">
🅿️ Parked Visitors - <span id="parked-count">0</span>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🅿️ Parked Visitors - <span id="parked-count">0</span></span>
<span class="info-icon" onclick="showTableHelp('parked')" title="What is this?" style="font-size: 1.1rem; cursor: pointer;"></span>
</div>
</div>
<div id="parked-loading" class="loading" style="display: none;">
@@ -493,6 +505,82 @@
</div>
</div>
<!-- Departure Edit Modal -->
<div id="departureEditModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="departure-edit-title">Departure Details</h2>
<button class="close" onclick="closeDepartureEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="quick-actions">
<button id="departure-btn-departed" class="btn btn-primary btn-sm" onclick="updateDepartureStatus('DEPARTED')" style="display: none;">
🛫 Mark Departed
</button>
<button id="departure-btn-cancel" class="btn btn-danger btn-sm" onclick="updateDepartureStatus('CANCELLED')" style="display: none;">
❌ Cancel
</button>
</div>
<form id="departure-edit-form">
<input type="hidden" id="departure-edit-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="departure_edit_registration">Aircraft Registration</label>
<input type="text" id="departure_edit_registration" name="registration" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="departure_edit_type">Aircraft Type</label>
<input type="text" id="departure_edit_type" name="type" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="departure_edit_callsign">Callsign</label>
<input type="text" id="departure_edit_callsign" name="callsign" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="departure_edit_out_to">Destination</label>
<input type="text" id="departure_edit_out_to" name="out_to" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="departure_edit_etd">ETD (Estimated Time of Departure)</label>
<div style="display: flex; gap: 0.5rem;">
<input type="date" id="departure_edit_etd_date" name="etd_date" readonly style="flex: 1; background-color: #f5f5f5; cursor: not-allowed;">
<input type="time" id="departure_edit_etd_time" name="etd_time" readonly style="flex: 1; background-color: #f5f5f5; cursor: not-allowed;">
</div>
</div>
<div class="form-group full-width">
<label for="departure_edit_notes">Notes</label>
<textarea id="departure_edit_notes" name="notes" rows="3" readonly style="background-color: #f5f5f5; cursor: not-allowed;"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeDepartureEditModal()">
Close
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Table Help Modal -->
<div id="tableHelpModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Table Information</h2>
<button class="close" onclick="closeTableHelp()">&times;</button>
</div>
<div class="modal-body" id="tableHelpContent">
<!-- Content will be populated by JavaScript -->
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeTableHelp()">Close</button>
</div>
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="modal">
<div class="modal-content">
@@ -1112,13 +1200,21 @@
return etaDate === today;
});
// Add local flights in DEPARTED status (in the air, heading back)
// Add local flights in DEPARTED status (in the air, heading back) - only those booked out today
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localInAir = localFlights.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
const today = new Date().toISOString().split('T')[0];
const localInAir = localFlights
.filter(flight => {
// Only include flights booked out today (created_dt)
if (!flight.created_dt) return false;
const createdDate = flight.created_dt.split('T')[0];
return createdDate === today;
})
.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
arrivals.push(...localInAir);
}
@@ -1164,13 +1260,21 @@
return etdDate === today;
});
// Add local flights (BOOKED_OUT status - ready to go)
// Add local flights (BOOKED_OUT status - ready to go) - only those booked out today
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localDepartures = localFlights.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
const today = new Date().toISOString().split('T')[0];
const localDepartures = localFlights
.filter(flight => {
// Only include flights booked out today (created_dt)
if (!flight.created_dt) return false;
const createdDate = flight.created_dt.split('T')[0];
return createdDate === today;
})
.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
departures.push(...localDepartures);
}
@@ -1202,9 +1306,10 @@
document.getElementById('departed-no-data').style.display = 'none';
try {
const [pprResponse, localResponse] = await Promise.all([
const [pprResponse, localResponse, depResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=DEPARTED&limit=1000')
]);
if (!pprResponse.ok) {
@@ -1227,8 +1332,8 @@
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];
if (!flight.departed_dt) return false;
const departedDate = flight.departed_dt.split('T')[0];
return departedDate === today;
}).map(flight => ({
...flight,
@@ -1237,6 +1342,20 @@
departed.push(...localDeparted);
}
// Add departures to other airports that departed today
if (depResponse.ok) {
const depFlights = await depResponse.json();
const depDeparted = depFlights.filter(flight => {
if (!flight.departed_dt) return false;
const departedDate = flight.departed_dt.split('T')[0];
return departedDate === today;
}).map(flight => ({
...flight,
isDeparture: true
}));
departed.push(...depDeparted);
}
displayDeparted(departed);
} catch (error) {
console.error('Error loading departed aircraft:', error);
@@ -1259,8 +1378,8 @@
// Sort by departed time
departed.sort((a, b) => {
const aTime = a.departed_dt || a.departure_dt;
const bTime = b.departed_dt || b.departure_dt;
const aTime = a.departed_dt;
const bTime = b.departed_dt;
return new Date(aTime) - new Date(bTime);
});
@@ -1270,10 +1389,13 @@
for (const flight of departed) {
const row = document.createElement('tr');
const isLocal = flight.isLocalFlight;
const isDeparture = flight.isDeparture;
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else if (isDeparture) {
openDepartureEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
@@ -1285,7 +1407,14 @@
<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>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
`;
} else if (isDeparture) {
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;">${flight.out_to || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
`;
} else {
row.innerHTML = `
@@ -1657,7 +1786,7 @@
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else if (isDeparture) {
// TODO: Open departure edit modal
openDepartureEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
@@ -2600,6 +2729,7 @@
const timestampModal = document.getElementById('timestampModal');
const userManagementModal = document.getElementById('userManagementModal');
const userModal = document.getElementById('userModal');
const tableHelpModal = document.getElementById('tableHelpModal');
if (event.target === pprModal) {
closePPRModal();
@@ -2613,6 +2743,9 @@
if (event.target === userModal) {
closeUserModal();
}
if (event.target === tableHelpModal) {
closeTableHelp();
}
}
function clearArrivalAirportLookup() {
@@ -2789,6 +2922,55 @@
currentLocalFlightId = null;
}
// Open departure edit modal
async function openDepartureEditModal(departureId) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/departures/${departureId}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) throw new Error('Failed to load departure');
const departure = await response.json();
currentDepartureId = departure.id;
// Populate form
document.getElementById('departure-edit-id').value = departure.id;
document.getElementById('departure_edit_registration').value = departure.registration;
document.getElementById('departure_edit_type').value = departure.type;
document.getElementById('departure_edit_callsign').value = departure.callsign || '';
document.getElementById('departure_edit_out_to').value = departure.out_to;
document.getElementById('departure_edit_notes').value = departure.notes || '';
// Parse and populate ETD if exists
if (departure.etd) {
const etd = new Date(departure.etd);
document.getElementById('departure_edit_etd_date').value = etd.toISOString().slice(0, 10);
document.getElementById('departure_edit_etd_time').value = etd.toISOString().slice(11, 16);
}
// Show/hide action buttons based on status
const deptBtn = document.getElementById('departure-btn-departed');
const cancelBtn = document.getElementById('departure-btn-cancel');
if (deptBtn) deptBtn.style.display = departure.status === 'BOOKED_OUT' ? 'inline-block' : 'none';
if (cancelBtn) cancelBtn.style.display = departure.status === 'BOOKED_OUT' ? 'inline-block' : 'none';
document.getElementById('departure-edit-title').textContent = `${departure.registration} to ${departure.out_to}`;
document.getElementById('departureEditModal').style.display = 'block';
} catch (error) {
console.error('Error loading departure:', error);
showNotification('Error loading departure details', true);
}
}
function closeDepartureEditModal() {
document.getElementById('departureEditModal').style.display = 'none';
currentDepartureId = null;
}
// Update status from table buttons (with flight ID passed)
async function updateLocalFlightStatusFromTable(flightId, status) {
if (!accessToken) return;
@@ -2876,6 +3058,74 @@
}
}
async function updateDepartureStatus(status) {
if (!currentDepartureId || !accessToken) return;
// Show confirmation for cancel actions
if (status === 'CANCELLED') {
if (!confirm('Are you sure you want to cancel this departure? This action cannot be easily undone.')) {
return;
}
}
try {
const response = await fetch(`/api/v1/departures/${currentDepartureId}/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');
closeDepartureEditModal();
loadPPRs(); // Refresh display
showNotification(`Departure marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating departure status', true);
}
}
// Table help modal functions
const tableHelpTexts = {
arrivals: {
title: "Today's Pending Arrivals",
text: "Displays aircraft that are expected to arrive at Swansea today. These are flights that have filed PPRs or have been booked in as arriving. Aircraft in this list are actively planning to land today."
},
departures: {
title: "Today's Pending Departures",
text: "Displays aircraft that are ready to depart from Swansea today. This includes flights with approved PPRs awaiting departure, local flights that have been booked out, and departures to other airfields. These aircraft will depart today."
},
departed: {
title: "Departed Today",
text: "Displays the flights which have departed today to other airfields, either by way of a PPR or by booking out."
},
parked: {
title: "Parked Visitors",
text: "Displays visiting aircraft that are currently parked at Swansea airport and are NOT expected to depart today. The ETD column shows the day the aircraft intends to depart."
}
};
function showTableHelp(tableType) {
const helpData = tableHelpTexts[tableType];
if (!helpData) return;
const modal = document.getElementById('tableHelpModal');
const content = document.getElementById('tableHelpContent');
const header = modal.querySelector('.modal-header h2');
header.textContent = helpData.title;
content.innerHTML = `<p>${helpData.text}</p>`;
modal.style.display = 'block';
}
function closeTableHelp() {
document.getElementById('tableHelpModal').style.display = 'none';
}
// Local flight edit form submission
document.getElementById('local-flight-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();