Compare commits
2 Commits
ea35de5eb5
...
98d0e3cfd7
| Author | SHA1 | Date | |
|---|---|---|---|
| 98d0e3cfd7 | |||
| d2e7d3c3dd |
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
282
web/admin.html
282
web/admin.html
@@ -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()">×</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()">×</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,10 +1200,18 @@
|
||||
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 => ({
|
||||
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
|
||||
}));
|
||||
@@ -1164,10 +1260,18 @@
|
||||
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 => ({
|
||||
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
|
||||
}));
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user