Compare commits

...

2 Commits

Author SHA1 Message Date
f7467690e4 Public board for local fligts 2025-12-12 06:50:38 -05:00
1d1c504f91 Callsign fix 2025-12-12 06:24:23 -05:00
3 changed files with 237 additions and 69 deletions

View File

@@ -3,20 +3,95 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_db from app.api.deps import get_db
from app.crud.crud_ppr import ppr as crud_ppr from app.crud.crud_ppr import ppr as crud_ppr
from app.crud.crud_local_flight import local_flight as crud_local_flight
from app.schemas.ppr import PPRPublic from app.schemas.ppr import PPRPublic
from app.models.local_flight import LocalFlightStatus
from datetime import date
router = APIRouter() router = APIRouter()
@router.get("/arrivals", response_model=List[PPRPublic]) @router.get("/arrivals")
async def get_public_arrivals(db: Session = Depends(get_db)): async def get_public_arrivals(db: Session = Depends(get_db)):
"""Get today's arrivals for public display""" """Get today's arrivals for public display (PPR and local flights)"""
arrivals = crud_ppr.get_arrivals_today(db) arrivals = crud_ppr.get_arrivals_today(db)
return arrivals
# Convert PPR arrivals to dictionaries
arrivals_list = []
for arrival in arrivals:
arrivals_list.append({
'ac_call': arrival.ac_call,
'ac_reg': arrival.ac_reg,
'ac_type': arrival.ac_type,
'in_from': arrival.in_from,
'eta': arrival.eta,
'landed_dt': arrival.landed_dt,
'status': arrival.status.value,
'isLocalFlight': False
})
# Add local flights with DEPARTED status
local_flights = crud_local_flight.get_multi(
db,
status=LocalFlightStatus.DEPARTED,
limit=1000
)
# Convert local flights to match the PPR format for display
for flight in local_flights:
arrivals_list.append({
'ac_call': flight.callsign or flight.registration,
'ac_reg': flight.registration,
'ac_type': flight.type,
'in_from': None,
'eta': flight.departure_dt,
'landed_dt': None,
'status': 'DEPARTED',
'isLocalFlight': True,
'flight_type': flight.flight_type.value
})
return arrivals_list
@router.get("/departures", response_model=List[PPRPublic]) @router.get("/departures")
async def get_public_departures(db: Session = Depends(get_db)): async def get_public_departures(db: Session = Depends(get_db)):
"""Get today's departures for public display""" """Get today's departures for public display (PPR and local flights)"""
departures = crud_ppr.get_departures_today(db) departures = crud_ppr.get_departures_today(db)
return departures
# Convert PPR departures to dictionaries
departures_list = []
for departure in departures:
departures_list.append({
'ac_call': departure.ac_call,
'ac_reg': departure.ac_reg,
'ac_type': departure.ac_type,
'out_to': departure.out_to,
'etd': departure.etd,
'departed_dt': departure.departed_dt,
'status': departure.status.value,
'isLocalFlight': False
})
# Add local flights with BOOKED_OUT status
local_flights = crud_local_flight.get_multi(
db,
status=LocalFlightStatus.BOOKED_OUT,
limit=1000
)
# Convert local flights to match the PPR format for display
for flight in local_flights:
departures_list.append({
'ac_call': flight.callsign or flight.registration,
'ac_reg': flight.registration,
'ac_type': flight.type,
'out_to': None,
'etd': flight.booked_out_dt,
'departed_dt': None,
'status': 'BOOKED_OUT',
'isLocalFlight': True,
'flight_type': flight.flight_type.value
})
return departures_list

View File

@@ -982,25 +982,24 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label for="local_registration">Aircraft Registration *</label> <label for="local_registration">Aircraft Registration *</label>
<input type="text" id="local_registration" name="registration" required oninput="handleLocalAircraftLookup(this.value)"> <input type="text" id="local_registration" name="registration" required oninput="handleLocalAircraftLookup(this.value)" tabindex="1">
<div id="local-aircraft-lookup-results"></div> <div id="local-aircraft-lookup-results"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="local_type">Aircraft Type *</label> <label for="local_type">Aircraft Type *</label>
<input type="text" id="local_type" name="type" required tabindex="-1"> <input type="text" id="local_type" name="type" required tabindex="-1" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="local_callsign">Callsign (optional)</label> <label for="local_callsign">Callsign (optional)</label>
<input type="text" id="local_callsign" name="callsign" placeholder="If different from registration"> <input type="text" id="local_callsign" name="callsign" placeholder="If different from registration" tabindex="4">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="local_pob">Persons on Board *</label> <label for="local_pob">Persons on Board *</label>
<input type="number" id="local_pob" name="pob" required min="1"> <input type="number" id="local_pob" name="pob" required min="1" tabindex="2">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="local_flight_type">Flight Type *</label> <label for="local_flight_type">Flight Type *</label>
<select id="local_flight_type" name="flight_type" required> <select id="local_flight_type" name="flight_type" required tabindex="3">
<option value="">Select Type</option>
<option value="LOCAL">Local Flight</option> <option value="LOCAL">Local Flight</option>
<option value="CIRCUITS">Circuits</option> <option value="CIRCUITS">Circuits</option>
<option value="DEPARTURE">Departure</option> <option value="DEPARTURE">Departure</option>
@@ -1504,6 +1503,13 @@
return; return;
} }
// Press 'Escape' to close Book Out modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('localFlightModal').style.display === 'block') {
e.preventDefault();
closeLocalFlightModal();
return;
}
// Only trigger other shortcuts when not typing in input fields // Only trigger other shortcuts when not typing in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
return; return;
@@ -1514,6 +1520,24 @@
e.preventDefault(); e.preventDefault();
openNewPPRModal(); openNewPPRModal();
} }
// Press 'b' to book out local flight (LOCAL type)
if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
openLocalFlightModal('LOCAL');
}
// Press 'c' to book out circuits
if (e.key === 'c' || e.key === 'C') {
e.preventDefault();
openLocalFlightModal('CIRCUITS');
}
// Press 'd' to book out departure
if (e.key === 'd' || e.key === 'D') {
e.preventDefault();
openLocalFlightModal('DEPARTURE');
}
}); });
} }
@@ -2115,15 +2139,18 @@
if (isLocal) { if (isLocal) {
// Local flight display // Local flight display
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration; if (flight.callsign && flight.callsign.trim()) {
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`; aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
} else {
aircraftDisplay = `<strong>${flight.registration}</strong>`;
}
acType = flight.type; acType = flight.type;
fromDisplay = '-'; fromDisplay = `<i>${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}</i>`;
eta = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-'; eta = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
pob = flight.pob || '-'; pob = flight.pob || '-';
fuel = '-'; fuel = '-';
actionButtons = ` actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed"> <button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
LAND LAND
</button> </button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight"> <button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
@@ -2132,8 +2159,11 @@
`; `;
} else { } else {
// PPR display // PPR display
const callsign = flight.ac_call && flight.ac_call.trim() ? flight.ac_call : flight.ac_reg; if (flight.ac_call && flight.ac_call.trim()) {
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`; aircraftDisplay = `<strong>${flight.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
} else {
aircraftDisplay = `<strong>${flight.ac_reg}</strong>`;
}
acType = flight.ac_type; acType = flight.ac_type;
// Lookup airport name for in_from // Lookup airport name for in_from
@@ -2214,9 +2244,12 @@
if (isLocal) { if (isLocal) {
// Local flight display // Local flight display
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration; if (flight.callsign && flight.callsign.trim()) {
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`; aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
toDisplay = '-'; } else {
aircraftDisplay = `<strong>${flight.registration}</strong>`;
}
toDisplay = `<i>${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}</i>`;
etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-'; etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
pob = flight.pob || '-'; pob = flight.pob || '-';
fuel = '-'; fuel = '-';
@@ -2225,7 +2258,7 @@
// Action buttons for local flight // Action buttons for local flight
if (flight.status === 'BOOKED_OUT') { if (flight.status === 'BOOKED_OUT') {
actionButtons = ` actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'DEPARTED')" title="Mark as Departed"> <button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
TAKE OFF TAKE OFF
</button> </button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight"> <button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
@@ -2234,7 +2267,7 @@
`; `;
} else if (flight.status === 'DEPARTED') { } else if (flight.status === 'DEPARTED') {
actionButtons = ` actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed"> <button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
LAND LAND
</button> </button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight"> <button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
@@ -2246,8 +2279,11 @@
} }
} else { } else {
// PPR display // PPR display
const callsign = flight.ac_call && flight.ac_call.trim() ? flight.ac_call : flight.ac_reg; if (flight.ac_call && flight.ac_call.trim()) {
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`; aircraftDisplay = `<strong>${flight.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
} else {
aircraftDisplay = `<strong>${flight.ac_reg}</strong>`;
}
toDisplay = flight.out_to || '-'; toDisplay = flight.out_to || '-';
if (flight.out_to && flight.out_to.length === 4 && /^[A-Z]{4}$/.test(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); toDisplay = await getAirportDisplay(flight.out_to);
@@ -2539,11 +2575,11 @@
} }
// Timestamp modal functions // Timestamp modal functions
function showTimestampModal(status, pprId = null) { function showTimestampModal(status, pprId = null, isLocalFlight = false) {
const targetPprId = pprId || currentPPRId; const targetId = pprId || (isLocalFlight ? currentLocalFlightId : currentPPRId);
if (!targetPprId) return; if (!targetId) return;
pendingStatusUpdate = { status: status, pprId: targetPprId }; pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight };
const modalTitle = document.getElementById('timestamp-modal-title'); const modalTitle = document.getElementById('timestamp-modal-title');
const submitBtn = document.getElementById('timestamp-submit-btn'); const submitBtn = document.getElementById('timestamp-submit-btn');
@@ -2589,7 +2625,13 @@
} }
try { try {
const response = await fetch(`/api/v1/pprs/${pendingStatusUpdate.pprId}/status`, { // Determine the correct API endpoint based on flight type
const isLocal = pendingStatusUpdate.isLocalFlight;
const endpoint = isLocal ?
`/api/v1/local-flights/${pendingStatusUpdate.pprId}/status` :
`/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
const response = await fetch(endpoint, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -2608,9 +2650,11 @@
const updatedStatus = pendingStatusUpdate.status; const updatedStatus = pendingStatusUpdate.status;
closeTimestampModal(); closeTimestampModal();
loadPPRs(); // Refresh both tables loadPPRs(); // Refresh all tables
showNotification(`Status updated to ${updatedStatus}`); showNotification(`Status updated to ${updatedStatus}`);
if (!isLocal) {
closePPRModal(); // Close PPR modal after successful status update closePPRModal(); // Close PPR modal after successful status update
}
} catch (error) { } catch (error) {
console.error('Error updating status:', error); console.error('Error updating status:', error);
showNotification(`Error updating status: ${error.message}`, true); showNotification(`Error updating status: ${error.message}`, true);
@@ -3407,10 +3451,11 @@
} }
// Local Flight (Book Out) Modal Functions // Local Flight (Book Out) Modal Functions
function openLocalFlightModal() { function openLocalFlightModal(flightType = 'LOCAL') {
document.getElementById('local-flight-form').reset(); document.getElementById('local-flight-form').reset();
document.getElementById('local-flight-id').value = ''; document.getElementById('local-flight-id').value = '';
document.getElementById('local-flight-modal-title').textContent = 'Book Out'; document.getElementById('local-flight-modal-title').textContent = 'Book Out';
document.getElementById('local_flight_type').value = flightType;
document.getElementById('localFlightModal').style.display = 'block'; document.getElementById('localFlightModal').style.display = 'block';
// Clear aircraft lookup results // Clear aircraft lookup results

View File

@@ -304,7 +304,25 @@
// Build rows asynchronously to lookup airport names // Build rows asynchronously to lookup airport names
const rows = await Promise.all(arrivals.map(async (arrival) => { const rows = await Promise.all(arrivals.map(async (arrival) => {
// Show callsign if available, otherwise registration const isLocal = arrival.isLocalFlight;
if (isLocal) {
// Local flight
const aircraftId = arrival.ac_call || arrival.ac_reg || '';
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.ac_type || '')})</span>`;
const fromDisplay = `<i>${getFlightTypeDisplay(arrival.flight_type)}</i>`;
const time = convertToLocalTime(arrival.eta);
const timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">IN AIR</span></div>`;
return `
<tr>
<td>${aircraftDisplay}</td>
<td>${fromDisplay}</td>
<td>${timeDisplay}</td>
</tr>
`;
} else {
// PPR
const aircraftId = arrival.ac_call || arrival.ac_reg || ''; const aircraftId = arrival.ac_call || arrival.ac_reg || '';
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.ac_type || '')})</span>`; const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.ac_type || '')})</span>`;
const fromDisplay = await getAirportName(arrival.in_from || ''); const fromDisplay = await getAirportName(arrival.in_from || '');
@@ -325,6 +343,7 @@
<td>${timeDisplay}</td> <td>${timeDisplay}</td>
</tr> </tr>
`; `;
}
})); }));
tbody.innerHTML = rows.join(''); tbody.innerHTML = rows.join('');
@@ -355,7 +374,25 @@
// Build rows asynchronously to lookup airport names // Build rows asynchronously to lookup airport names
const rows = await Promise.all(departures.map(async (departure) => { const rows = await Promise.all(departures.map(async (departure) => {
// Show callsign if available, otherwise registration const isLocal = departure.isLocalFlight;
if (isLocal) {
// Local flight
const aircraftId = departure.ac_call || departure.ac_reg || '';
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`;
const toDisplay = `<i>${getFlightTypeDisplay(departure.flight_type)}</i>`;
const time = convertToLocalTime(departure.etd);
const timeDisplay = `<div>${escapeHtml(time)}</div>`;
return `
<tr>
<td>${aircraftDisplay}</td>
<td>${toDisplay}</td>
<td>${timeDisplay}</td>
</tr>
`;
} else {
// PPR
const aircraftId = departure.ac_call || departure.ac_reg || ''; const aircraftId = departure.ac_call || departure.ac_reg || '';
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`; const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`;
const toDisplay = await getAirportName(departure.out_to || ''); const toDisplay = await getAirportName(departure.out_to || '');
@@ -376,6 +413,7 @@
<td>${timeDisplay}</td> <td>${timeDisplay}</td>
</tr> </tr>
`; `;
}
})); }));
tbody.innerHTML = rows.join(''); tbody.innerHTML = rows.join('');
@@ -393,6 +431,16 @@
return div.innerHTML; return div.innerHTML;
} }
// Map flight type enum to friendly name
function getFlightTypeDisplay(flightType) {
const typeMap = {
'CIRCUITS': 'Circuit Traffic',
'LOCAL': 'Local Area',
'DEPARTURE': 'Departure'
};
return typeMap[flightType] || flightType;
}
// Load data on page load // Load data on page load
window.addEventListener('load', function() { window.addEventListener('load', function() {
loadArrivals(); loadArrivals();