Book in functions

This commit is contained in:
2025-12-16 09:47:26 -05:00
parent d7eefdb652
commit 6209c7acce
8 changed files with 452 additions and 27 deletions

View File

@@ -91,7 +91,8 @@ def upgrade() -> None:
sa.Column('in_from', sa.String(length=64), nullable=False), sa.Column('in_from', sa.String(length=64), nullable=False),
sa.Column('status', sa.Enum('BOOKED_IN', 'LANDED', 'CANCELLED', name='arrivalsstatus'), nullable=False, server_default='BOOKED_IN'), sa.Column('status', sa.Enum('BOOKED_IN', 'LANDED', 'CANCELLED', name='arrivalsstatus'), nullable=False, server_default='BOOKED_IN'),
sa.Column('notes', sa.Text(), nullable=True), sa.Column('notes', sa.Text(), nullable=True),
sa.Column('booked_in_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), sa.Column('created_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('eta', sa.DateTime(), nullable=True),
sa.Column('landed_dt', sa.DateTime(), nullable=True), sa.Column('landed_dt', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.String(length=16), nullable=True), sa.Column('created_by', sa.String(length=16), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
@@ -104,7 +105,8 @@ def upgrade() -> None:
op.create_index('idx_arr_registration', 'arrivals', ['registration']) op.create_index('idx_arr_registration', 'arrivals', ['registration'])
op.create_index('idx_arr_in_from', 'arrivals', ['in_from']) op.create_index('idx_arr_in_from', 'arrivals', ['in_from'])
op.create_index('idx_arr_status', 'arrivals', ['status']) op.create_index('idx_arr_status', 'arrivals', ['status'])
op.create_index('idx_arr_booked_in_dt', 'arrivals', ['booked_in_dt']) op.create_index('idx_arr_created_dt', 'arrivals', ['created_dt'])
op.create_index('idx_arr_eta', 'arrivals', ['eta'])
op.create_index('idx_arr_created_by', 'arrivals', ['created_by']) op.create_index('idx_arr_created_by', 'arrivals', ['created_by'])

View File

@@ -63,6 +63,28 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
'flight_type': flight.flight_type.value 'flight_type': flight.flight_type.value
}) })
# Add booked-in arrivals
booked_in_arrivals = crud_arrival.get_multi(db, limit=1000)
for arrival in booked_in_arrivals:
# Only include BOOKED_IN and LANDED arrivals
if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.LANDED):
continue
# For BOOKED_IN, only include those from today; for LANDED, include all
if arrival.status == ArrivalStatus.BOOKED_IN:
if not (today_start <= arrival.created_dt < today_end):
continue
arrivals_list.append({
'registration': arrival.registration,
'callsign': arrival.callsign,
'type': arrival.type,
'in_from': arrival.in_from,
'eta': arrival.eta,
'landed_dt': arrival.landed_dt,
'status': arrival.status.value,
'isBookedIn': True
})
return arrivals_list return arrivals_list

View File

@@ -25,25 +25,25 @@ class CRUDArrival:
query = query.filter(Arrival.status == status) query = query.filter(Arrival.status == status)
if date_from: if date_from:
query = query.filter(func.date(Arrival.booked_in_dt) >= date_from) query = query.filter(func.date(Arrival.created_dt) >= date_from)
if date_to: if date_to:
query = query.filter(func.date(Arrival.booked_in_dt) <= date_to) query = query.filter(func.date(Arrival.created_dt) <= date_to)
return query.order_by(desc(Arrival.booked_in_dt)).offset(skip).limit(limit).all() return query.order_by(desc(Arrival.created_dt)).offset(skip).limit(limit).all()
def get_arrivals_today(self, db: Session) -> List[Arrival]: def get_arrivals_today(self, db: Session) -> List[Arrival]:
"""Get today's arrivals (booked in or landed)""" """Get today's arrivals (booked in or landed)"""
today = date.today() today = date.today()
return db.query(Arrival).filter( return db.query(Arrival).filter(
and_( and_(
func.date(Arrival.booked_in_dt) == today, func.date(Arrival.created_dt) == today,
or_( or_(
Arrival.status == ArrivalStatus.BOOKED_IN, Arrival.status == ArrivalStatus.BOOKED_IN,
Arrival.status == ArrivalStatus.LANDED Arrival.status == ArrivalStatus.LANDED
) )
) )
).order_by(Arrival.booked_in_dt).all() ).order_by(Arrival.created_dt).all()
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival: def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival:
db_obj = Arrival( db_obj = Arrival(

View File

@@ -23,7 +23,8 @@ class Arrival(Base):
in_from = Column(String(4), nullable=False, index=True) in_from = Column(String(4), nullable=False, index=True)
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True) status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True)
notes = Column(Text, nullable=True) notes = Column(Text, nullable=True)
booked_in_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True) created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
eta = Column(DateTime, nullable=True, index=True)
landed_dt = Column(DateTime, nullable=True) landed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True) created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)

View File

@@ -38,7 +38,7 @@ class ArrivalBase(BaseModel):
class ArrivalCreate(ArrivalBase): class ArrivalCreate(ArrivalBase):
pass eta: Optional[datetime] = None
class ArrivalUpdate(BaseModel): class ArrivalUpdate(BaseModel):
@@ -57,7 +57,8 @@ class ArrivalStatusUpdate(BaseModel):
class Arrival(ArrivalBase): class Arrival(ArrivalBase):
id: int id: int
status: ArrivalStatus status: ArrivalStatus
booked_in_dt: datetime created_dt: datetime
eta: Optional[datetime] = None
landed_dt: Optional[datetime] = None landed_dt: Optional[datetime] = None
created_by: Optional[str] = None created_by: Optional[str] = None
updated_at: datetime updated_at: datetime

View File

@@ -19,6 +19,9 @@
<button class="btn btn-info" onclick="openLocalFlightModal()"> <button class="btn btn-info" onclick="openLocalFlightModal()">
🛫 Book Out 🛫 Book Out
</button> </button>
<button class="btn btn-info" onclick="openBookInModal()">
🛬 Book In
</button>
<button class="btn btn-primary" onclick="window.open('reports.html', '_blank')"> <button class="btn btn-primary" onclick="window.open('reports.html', '_blank')">
📊 Reports 📊 Reports
</button> </button>
@@ -505,6 +508,65 @@
</div> </div>
</div> </div>
<!-- Book In Modal -->
<div id="bookInModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Book In</h2>
<button class="close" onclick="closeBookInModal()">&times;</button>
</div>
<div class="modal-body">
<form id="book-in-form">
<input type="hidden" id="book-in-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="book_in_registration">Aircraft Registration *</label>
<input type="text" id="book_in_registration" name="registration" required oninput="handleBookInAircraftLookup(this.value)" tabindex="1">
<div id="book-in-aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="book_in_type">Aircraft Type</label>
<input type="text" id="book_in_type" name="type" tabindex="4" placeholder="e.g., C172, PA34, AA5">
</div>
<div class="form-group">
<label for="book_in_callsign">Callsign (optional)</label>
<input type="text" id="book_in_callsign" name="callsign" placeholder="If different from registration" tabindex="5">
</div>
<div class="form-group">
<label for="book_in_pob">Persons on Board *</label>
<input type="number" id="book_in_pob" name="pob" required min="1" tabindex="2">
</div>
<div class="form-group">
<label for="book_in_from">Coming From (Airport) *</label>
<input type="text" id="book_in_from" name="in_from" placeholder="ICAO Code or Airport Name" required oninput="handleBookInArrivalAirportLookup(this.value)" tabindex="3">
<div id="book-in-arrival-airport-lookup-results"></div>
</div>
<div class="form-group">
<label for="book_in_eta_time">ETA (Estimated Time of Arrival) *</label>
<select id="book_in_eta_time" name="eta_time" required>
<option value="">Select Time</option>
</select>
</div>
<div class="form-group full-width">
<label for="book_in_notes">Notes</label>
<textarea id="book_in_notes" name="notes" rows="3" placeholder="e.g., any special requirements"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeBookInModal()">
Cancel
</button>
<button type="submit" class="btn btn-success">
🛬 Book In
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Departure Edit Modal --> <!-- Departure Edit Modal -->
<div id="departureEditModal" class="modal"> <div id="departureEditModal" class="modal">
<div class="modal-content"> <div class="modal-content">
@@ -1014,6 +1076,13 @@
return; return;
} }
// Press 'Escape' to close Book In modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('bookInModal').style.display === 'block') {
e.preventDefault();
closeBookInModal();
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;
@@ -1042,6 +1111,12 @@
e.preventDefault(); e.preventDefault();
openLocalFlightModal('DEPARTURE'); openLocalFlightModal('DEPARTURE');
} }
// Press 'i' to book in arrival
if (e.key === 'i' || e.key === 'I') {
e.preventDefault();
openBookInModal();
}
}); });
} }
@@ -1177,10 +1252,11 @@
document.getElementById('arrivals-no-data').style.display = 'none'; document.getElementById('arrivals-no-data').style.display = 'none';
try { try {
// Load PPRs and local flights that are in the air // Load PPRs, local flights, and booked-in arrivals
const [pprResponse, localResponse] = await Promise.all([ const [pprResponse, localResponse, bookInResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'), 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/arrivals/?limit=1000')
]); ]);
if (!pprResponse.ok) { if (!pprResponse.ok) {
@@ -1218,6 +1294,24 @@
arrivals.push(...localInAir); arrivals.push(...localInAir);
} }
// Add booked-in arrivals from the arrivals table
if (bookInResponse.ok) {
const bookedInArrivals = await bookInResponse.json();
const today = new Date().toISOString().split('T')[0];
const bookedInToday = bookedInArrivals
.filter(arrival => {
// Only include arrivals booked in today (created_dt) with BOOKED_IN status
if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false;
const bookedDate = arrival.created_dt.split('T')[0];
return bookedDate === today;
})
.map(arrival => ({
...arrival,
isBookedIn: true // Flag to distinguish from PPR and local
}));
arrivals.push(...bookedInToday);
}
displayArrivals(arrivals); displayArrivals(arrivals);
} catch (error) { } catch (error) {
console.error('Error loading arrivals:', error); console.error('Error loading arrivals:', error);
@@ -1435,13 +1529,17 @@
document.getElementById('parked-no-data').style.display = 'none'; document.getElementById('parked-no-data').style.display = 'none';
try { try {
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000'); // Load both PPRs and booked-in arrivals
const [pprResponse, bookedInResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/arrivals/?limit=1000')
]);
if (!response.ok) { if (!pprResponse.ok) {
throw new Error('Failed to fetch parked visitors'); throw new Error('Failed to fetch parked visitors');
} }
const allPPRs = await response.json(); const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
// Filter for parked visitors: LANDED status and (no ETD or ETD not today) // Filter for parked visitors: LANDED status and (no ETD or ETD not today)
@@ -1459,6 +1557,18 @@
return etdDate !== today; return etdDate !== today;
}); });
// Add booked-in arrivals with LANDED status
if (bookedInResponse.ok) {
const bookedInArrivals = await bookedInResponse.json();
const bookedInParked = bookedInArrivals
.filter(arrival => arrival.status === 'LANDED')
.map(arrival => ({
...arrival,
isBookedIn: true // Flag to distinguish from PPR
}));
parked.push(...bookedInParked);
}
displayParked(parked); displayParked(parked);
} catch (error) { } catch (error) {
console.error('Error loading parked visitors:', error); console.error('Error loading parked visitors:', error);
@@ -1491,9 +1601,25 @@
for (const ppr of parked) { for (const ppr of parked) {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id); const isBookedIn = ppr.isBookedIn;
// Click handler that routes to correct modal/display
if (isBookedIn) {
row.style.cursor = 'default'; // Booked-in arrivals don't have a modal yet
} else {
row.onclick = () => openPPRModal(ppr.id);
}
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;'; row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
// Get registration based on type (PPR vs booked-in)
const registration = ppr.ac_reg || ppr.registration || '-';
// Get aircraft type based on type (PPR vs booked-in)
const acType = ppr.ac_type || ppr.type || '-';
// Get from airport
const fromAirport = ppr.in_from || '-';
// Format arrival: time if today, date if not // Format arrival: time if today, date if not
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
let arrivedDisplay = '-'; let arrivedDisplay = '-';
@@ -1517,9 +1643,9 @@
} }
row.innerHTML = ` 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;">${registration}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_type || '-'}</td> <td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${acType}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.in_from || '-'}</td> <td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${fromAirport}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${arrivedDisplay}</td> <td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${arrivedDisplay}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${etdDisplay}</td> <td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${etdDisplay}</td>
`; `;
@@ -1674,11 +1800,16 @@
for (const flight of arrivals) { for (const flight of arrivals) {
const row = document.createElement('tr'); const row = document.createElement('tr');
const isLocal = flight.isLocalFlight; const isLocal = flight.isLocalFlight;
const isBookedIn = flight.isBookedIn;
// Click handler that routes to correct modal // Click handler that routes to correct modal
row.onclick = () => { row.onclick = () => {
if (isLocal) { if (isLocal) {
openLocalFlightEditModal(flight.id); openLocalFlightEditModal(flight.id);
} else if (isBookedIn) {
// For booked-in flights, we might add a view modal later
// For now, just show a message
showNotification(`Booked-in flight: ${flight.registration}`, false);
} else { } else {
openPPRModal(flight.id); openPPRModal(flight.id);
} }
@@ -1713,6 +1844,34 @@
CANCEL CANCEL
</button> </button>
`; `;
} else if (isBookedIn) {
// Booked-in arrival display
if (flight.callsign && flight.callsign.trim()) {
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;
// 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;
// Show ETA if available, otherwise show landed_dt
eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
pob = flight.pob || '-';
fuel = '-';
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
CANCEL
</button>
`;
} else { } else {
// PPR display // PPR display
if (flight.ac_call && flight.ac_call.trim()) { if (flight.ac_call && flight.ac_call.trim()) {
@@ -2165,11 +2324,11 @@
} }
// Timestamp modal functions // Timestamp modal functions
function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false) { function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false, isBookedIn = false) {
const targetId = pprId || (isLocalFlight ? currentLocalFlightId : currentPPRId); const targetId = pprId || (isLocalFlight ? currentLocalFlightId : (isBookedIn ? currentBookedInArrivalId : currentPPRId));
if (!targetId) return; if (!targetId) return;
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture }; pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture, isBookedIn: isBookedIn };
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');
@@ -2218,12 +2377,15 @@
// Determine the correct API endpoint based on flight type // Determine the correct API endpoint based on flight type
const isLocal = pendingStatusUpdate.isLocalFlight; const isLocal = pendingStatusUpdate.isLocalFlight;
const isDeparture = pendingStatusUpdate.isDeparture; const isDeparture = pendingStatusUpdate.isDeparture;
const isBookedIn = pendingStatusUpdate.isBookedIn;
let endpoint; let endpoint;
if (isLocal) { if (isLocal) {
endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`; endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`;
} else if (isDeparture) { } else if (isDeparture) {
endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`; endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`;
} else if (isBookedIn) {
endpoint = `/api/v1/arrivals/${pendingStatusUpdate.pprId}/status`;
} else { } else {
endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`; endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
} }
@@ -2247,9 +2409,14 @@
const updatedStatus = pendingStatusUpdate.status; const updatedStatus = pendingStatusUpdate.status;
closeTimestampModal(); closeTimestampModal();
loadPPRs(); // Refresh all tables // Refresh appropriate table based on flight type
if (isBookedIn) {
loadArrivals(); // Refresh arrivals table
} else {
loadPPRs(); // Refresh all tables (PPR, local, departures)
}
showNotification(`Status updated to ${updatedStatus}`); showNotification(`Status updated to ${updatedStatus}`);
if (!isLocal) { if (!isLocal && !isBookedIn) {
closePPRModal(); // Close PPR modal after successful status update closePPRModal(); // Close PPR modal after successful status update
} }
} catch (error) { } catch (error) {
@@ -2730,6 +2897,7 @@
const userManagementModal = document.getElementById('userManagementModal'); const userManagementModal = document.getElementById('userManagementModal');
const userModal = document.getElementById('userModal'); const userModal = document.getElementById('userModal');
const tableHelpModal = document.getElementById('tableHelpModal'); const tableHelpModal = document.getElementById('tableHelpModal');
const bookInModal = document.getElementById('bookInModal');
if (event.target === pprModal) { if (event.target === pprModal) {
closePPRModal(); closePPRModal();
@@ -2746,6 +2914,9 @@
if (event.target === tableHelpModal) { if (event.target === tableHelpModal) {
closeTableHelp(); closeTableHelp();
} }
if (event.target === bookInModal) {
closeBookInModal();
}
} }
function clearArrivalAirportLookup() { function clearArrivalAirportLookup() {
@@ -2787,6 +2958,50 @@
document.getElementById('localFlightModal').style.display = 'none'; document.getElementById('localFlightModal').style.display = 'none';
} }
function openBookInModal() {
document.getElementById('book-in-form').reset();
document.getElementById('book-in-id').value = '';
document.getElementById('bookInModal').style.display = 'block';
// Clear aircraft lookup results
clearBookInAircraftLookup();
clearBookInArrivalAirportLookup();
// Populate ETA time slots
populateETATimeSlots();
// Auto-focus on registration field
setTimeout(() => {
document.getElementById('book_in_registration').focus();
}, 100);
}
function closeBookInModal() {
document.getElementById('bookInModal').style.display = 'none';
}
function populateETATimeSlots() {
const select = document.getElementById('book_in_eta_time');
const next15MinSlot = getNext10MinuteSlot();
select.innerHTML = '';
for (let i = 0; i < 14; i++) {
const time = new Date(next15MinSlot.getTime() + i * 10 * 60 * 1000);
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
const timeStr = `${hours}:${minutes}`;
const option = document.createElement('option');
option.value = timeStr;
option.textContent = timeStr;
if (i === 0) {
option.selected = true;
}
select.appendChild(option);
}
}
// Handle flight type change to show/hide destination field // Handle flight type change to show/hide destination field
function handleFlightTypeChange(flightType) { function handleFlightTypeChange(flightType) {
const destGroup = document.getElementById('departure-destination-group'); const destGroup = document.getElementById('departure-destination-group');
@@ -2870,6 +3085,7 @@
// Local Flight Edit Modal Functions // Local Flight Edit Modal Functions
let currentLocalFlightId = null; let currentLocalFlightId = null;
let currentBookedInArrivalId = null;
async function openLocalFlightEditModal(flightId) { async function openLocalFlightEditModal(flightId) {
if (!accessToken) return; if (!accessToken) return;
@@ -3026,6 +3242,37 @@
} }
} }
// Update status from table for booked-in arrivals
async function updateArrivalStatusFromTable(arrivalId, status) {
if (!accessToken) return;
// Show confirmation for cancel actions
if (status === 'CANCELLED') {
if (!confirm('Are you sure you want to cancel this arrival? This action cannot be easily undone.')) {
return;
}
}
try {
const response = await fetch(`/api/v1/arrivals/${arrivalId}/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');
loadArrivals(); // Refresh arrivals table
showNotification(`Arrival marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating arrival status', true);
}
}
// Update status from modal (uses currentLocalFlightId) // Update status from modal (uses currentLocalFlightId)
async function updateLocalFlightStatus(status) { async function updateLocalFlightStatus(status) {
if (!currentLocalFlightId || !accessToken) return; if (!currentLocalFlightId || !accessToken) return;
@@ -3267,6 +3514,86 @@
} }
}); });
document.getElementById('book-in-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!accessToken) return;
const formData = new FormData(this);
const arrivalData = {};
formData.forEach((value, key) => {
// Skip the hidden id field and empty values
if (key === 'id') return;
// Handle time-only ETA (always today)
if (key === 'eta_time') {
if (value.trim()) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
// Store ETA in the eta field
arrivalData.eta = new Date(`${dateStr}T${value}`).toISOString();
}
return;
}
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (key === 'pob') {
arrivalData[key] = parseInt(value);
} else if (value.trim) {
arrivalData[key] = value.trim();
} else {
arrivalData[key] = value;
}
}
});
// Book In uses LANDED status (they're arriving now)
arrivalData.status = 'LANDED';
console.log('Submitting arrivals data:', arrivalData);
try {
const response = await fetch('/api/v1/arrivals/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(arrivalData)
});
if (!response.ok) {
let errorMessage = 'Failed to book in arrival';
try {
const errorData = await response.json();
if (errorData.detail) {
errorMessage = typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail);
} else if (errorData.errors) {
errorMessage = errorData.errors.map(e => e.msg).join(', ');
}
} 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();
closeBookInModal();
loadPPRs(); // Refresh tables
showNotification(`Aircraft ${result.registration} booked in successfully!`);
} catch (error) {
console.error('Error booking in arrival:', error);
showNotification(`Error: ${error.message}`, true);
}
});
// Add hover listeners to all notes tooltips // Add hover listeners to all notes tooltips
function setupTooltips() { function setupTooltips() {
document.querySelectorAll('.notes-tooltip').forEach(tooltip => { document.querySelectorAll('.notes-tooltip').forEach(tooltip => {

View File

@@ -234,8 +234,8 @@
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('WebSocket message received:', data); console.log('WebSocket message received:', data);
// Refresh display when any PPR-related, local flight, or departure event occurs // Refresh display when any PPR-related, local flight, departure, or arrival event occurs
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_') || data.type.includes('departure_'))) { if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_') || data.type.includes('departure_') || data.type.includes('arrival_'))) {
console.log('Flight update detected, refreshing display...'); console.log('Flight update detected, refreshing display...');
loadArrivals(); loadArrivals();
loadDepartures(); loadDepartures();
@@ -305,6 +305,7 @@
// 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) => {
const isLocal = arrival.isLocalFlight; const isLocal = arrival.isLocalFlight;
const isBookedIn = arrival.isBookedIn;
if (isLocal) { if (isLocal) {
// Local flight // Local flight
@@ -321,6 +322,30 @@
<td>${timeDisplay}</td> <td>${timeDisplay}</td>
</tr> </tr>
`; `;
} else if (isBookedIn) {
// Booked-in arrival
const aircraftId = arrival.callsign || arrival.registration || '';
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.type || '')})</span>`;
const fromDisplay = await getAirportName(arrival.in_from || '');
let timeDisplay;
if (arrival.status === 'LANDED' && arrival.landed_dt) {
// Show landed time if LANDED
const time = convertToLocalTime(arrival.landed_dt);
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;">LANDED</span></div>`;
} else {
// Show ETA if BOOKED_IN
const time = convertToLocalTime(arrival.eta);
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;">IN AIR</span></div>`;
}
return `
<tr>
<td>${aircraftDisplay}</td>
<td>${escapeHtml(fromDisplay)}</td>
<td>${timeDisplay}</td>
</tr>
`;
} else { } else {
// PPR // PPR
const aircraftId = arrival.ac_call || arrival.ac_reg || ''; const aircraftId = arrival.ac_call || arrival.ac_reg || '';

View File

@@ -191,6 +191,8 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
typeFieldId = 'ac_type'; typeFieldId = 'ac_type';
} else if (fieldId === 'local_registration') { } else if (fieldId === 'local_registration') {
typeFieldId = 'local_type'; typeFieldId = 'local_type';
} else if (fieldId === 'book_in_registration') {
typeFieldId = 'book_in_type';
} }
if (typeFieldId) { if (typeFieldId) {
@@ -342,12 +344,29 @@ function initializeLookups() {
{ isAircraft: true, minLength: 4, debounceMs: 300 } { isAircraft: true, minLength: 4, debounceMs: 300 }
); );
lookupManager.register('local-aircraft', localAircraftLookup); lookupManager.register('local-aircraft', localAircraftLookup);
const bookInAircraftLookup = createLookup(
'book_in_registration',
'book-in-aircraft-lookup-results',
null,
{ isAircraft: true, minLength: 4, debounceMs: 300 }
);
lookupManager.register('book-in-aircraft', bookInAircraftLookup);
const bookInArrivalAirportLookup = createLookup(
'book_in_from',
'book-in-arrival-airport-lookup-results',
null,
{ isAirport: true, minLength: 2 }
);
lookupManager.register('book-in-arrival-airport', bookInArrivalAirportLookup);
// Attach keyboard handlers to airport input fields // Attach keyboard handlers to airport input fields
setTimeout(() => { setTimeout(() => {
if (arrivalAirportLookup.attachKeyboardHandler) arrivalAirportLookup.attachKeyboardHandler(); if (arrivalAirportLookup.attachKeyboardHandler) arrivalAirportLookup.attachKeyboardHandler();
if (departureAirportLookup.attachKeyboardHandler) departureAirportLookup.attachKeyboardHandler(); if (departureAirportLookup.attachKeyboardHandler) departureAirportLookup.attachKeyboardHandler();
if (localOutToLookup.attachKeyboardHandler) localOutToLookup.attachKeyboardHandler(); if (localOutToLookup.attachKeyboardHandler) localOutToLookup.attachKeyboardHandler();
if (bookInArrivalAirportLookup.attachKeyboardHandler) bookInArrivalAirportLookup.attachKeyboardHandler();
}, 100); }, 100);
} }
@@ -426,3 +445,31 @@ function selectLocalOutToAirport(icaoCode) {
function selectLocalAircraft(registration) { function selectLocalAircraft(registration) {
lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration); lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration);
} }
function handleBookInAircraftLookup(value) {
const lookup = lookupManager.lookups['book-in-aircraft'];
if (lookup) lookup.handle(value);
}
function handleBookInArrivalAirportLookup(value) {
const lookup = lookupManager.lookups['book-in-arrival-airport'];
if (lookup) lookup.handle(value);
}
function clearBookInAircraftLookup() {
const lookup = lookupManager.lookups['book-in-aircraft'];
if (lookup) lookup.clear();
}
function clearBookInArrivalAirportLookup() {
const lookup = lookupManager.lookups['book-in-arrival-airport'];
if (lookup) lookup.clear();
}
function selectBookInAircraft(registration) {
lookupManager.selectItem('book-in-aircraft-lookup-results', 'book_in_registration', registration);
}
function selectBookInArrivalAirport(icaoCode) {
lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode);
}