Inital stab at local flights

This commit is contained in:
2025-12-12 06:14:36 -05:00
parent 56e4ab6e3e
commit 0aeed2268a
8 changed files with 1217 additions and 84 deletions

View File

@@ -0,0 +1,58 @@
"""Add local_flights table for tracking local flights
Revision ID: 002_local_flights
Revises: 001_initial_schema
Create Date: 2025-12-12 12:00:00.000000
This migration adds a new table for tracking local flights (circuits, local, departure)
that don't require PPR submissions.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '002_local_flights'
down_revision = '001_initial_schema'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""
Create local_flights table for tracking aircraft that book out locally.
"""
op.create_table('local_flights',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('registration', sa.String(length=16), nullable=False),
sa.Column('type', sa.String(length=32), nullable=False),
sa.Column('callsign', sa.String(length=16), nullable=True),
sa.Column('pob', sa.Integer(), nullable=False),
sa.Column('flight_type', sa.Enum('LOCAL', 'CIRCUITS', 'DEPARTURE', name='localflighttype'), nullable=False),
sa.Column('status', sa.Enum('BOOKED_OUT', 'DEPARTED', 'LANDED', 'CANCELLED', name='localflightstatus'), nullable=False, server_default='BOOKED_OUT'),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('booked_out_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('departure_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('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci'
)
# Create indexes for frequently queried columns
op.create_index('idx_registration', 'local_flights', ['registration'])
op.create_index('idx_flight_type', 'local_flights', ['flight_type'])
op.create_index('idx_status', 'local_flights', ['status'])
op.create_index('idx_booked_out_dt', 'local_flights', ['booked_out_dt'])
op.create_index('idx_created_by', 'local_flights', ['created_by'])
def downgrade() -> None:
"""
Drop the local_flights table.
"""
op.drop_table('local_flights')

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter
from app.api.endpoints import auth, pprs, public, aircraft, airport
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"])
api_router.include_router(local_flights.router, prefix="/local-flights", tags=["local_flights"])
api_router.include_router(public.router, prefix="/public", tags=["public"])
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])

View File

@@ -0,0 +1,195 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session
from datetime import date
from app.api.deps import get_db, get_current_read_user, get_current_operator_user
from app.crud.crud_local_flight import local_flight as crud_local_flight
from app.schemas.local_flight import LocalFlight, LocalFlightCreate, LocalFlightUpdate, LocalFlightStatus, LocalFlightType, LocalFlightStatusUpdate
from app.models.ppr import User
from app.core.utils import get_client_ip
router = APIRouter()
@router.get("/", response_model=List[LocalFlight])
async def get_local_flights(
request: Request,
skip: int = 0,
limit: int = 100,
status: Optional[LocalFlightStatus] = None,
flight_type: Optional[LocalFlightType] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get local flight records with optional filtering"""
flights = crud_local_flight.get_multi(
db, skip=skip, limit=limit, status=status,
flight_type=flight_type, date_from=date_from, date_to=date_to
)
return flights
@router.post("/", response_model=LocalFlight)
async def create_local_flight(
request: Request,
flight_in: LocalFlightCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Create a new local flight record (book out)"""
flight = crud_local_flight.create(db, obj_in=flight_in, created_by=current_user.username)
# Send real-time update via WebSocket
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_booked_out",
"data": {
"id": flight.id,
"registration": flight.registration,
"flight_type": flight.flight_type.value,
"status": flight.status.value
}
})
return flight
@router.get("/{flight_id}", response_model=LocalFlight)
async def get_local_flight(
flight_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get a specific local flight record"""
flight = crud_local_flight.get(db, flight_id=flight_id)
if not flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
return flight
@router.put("/{flight_id}", response_model=LocalFlight)
async def update_local_flight(
request: Request,
flight_id: int,
flight_in: LocalFlightUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update a local flight record"""
db_flight = crud_local_flight.get(db, flight_id=flight_id)
if not db_flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
flight = crud_local_flight.update(db, db_obj=db_flight, obj_in=flight_in)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_updated",
"data": {
"id": flight.id,
"registration": flight.registration,
"status": flight.status.value
}
})
return flight
@router.patch("/{flight_id}/status", response_model=LocalFlight)
async def update_local_flight_status(
request: Request,
flight_id: int,
status_update: LocalFlightStatusUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update local flight status (LANDED, CANCELLED, etc.)"""
flight = crud_local_flight.update_status(
db,
flight_id=flight_id,
status=status_update.status,
timestamp=status_update.timestamp
)
if not flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_status_update",
"data": {
"id": flight.id,
"registration": flight.registration,
"status": flight.status.value,
"landed_dt": flight.landed_dt.isoformat() if flight.landed_dt else None
}
})
return flight
@router.delete("/{flight_id}", response_model=LocalFlight)
async def cancel_local_flight(
request: Request,
flight_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Cancel a local flight record"""
flight = crud_local_flight.cancel(db, flight_id=flight_id)
if not flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_cancelled",
"data": {
"id": flight.id,
"registration": flight.registration
}
})
return flight
@router.get("/active/current", response_model=List[LocalFlight])
async def get_active_flights(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get currently active (booked out) flights"""
return crud_local_flight.get_active_flights(db)
@router.get("/today/departures", response_model=List[LocalFlight])
async def get_today_departures(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get today's departures (booked out or departed)"""
return crud_local_flight.get_departures_today(db)
@router.get("/today/booked-out", response_model=List[LocalFlight])
async def get_today_booked_out(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get all flights booked out today"""
return crud_local_flight.get_booked_out_today(db)

View File

@@ -0,0 +1,136 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func, desc
from datetime import date, datetime
from app.models.local_flight import LocalFlight, LocalFlightStatus, LocalFlightType
from app.schemas.local_flight import LocalFlightCreate, LocalFlightUpdate, LocalFlightStatusUpdate
class CRUDLocalFlight:
def get(self, db: Session, flight_id: int) -> Optional[LocalFlight]:
return db.query(LocalFlight).filter(LocalFlight.id == flight_id).first()
def get_multi(
self,
db: Session,
skip: int = 0,
limit: int = 100,
status: Optional[LocalFlightStatus] = None,
flight_type: Optional[LocalFlightType] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None
) -> List[LocalFlight]:
query = db.query(LocalFlight)
if status:
query = query.filter(LocalFlight.status == status)
if flight_type:
query = query.filter(LocalFlight.flight_type == flight_type)
if date_from:
query = query.filter(func.date(LocalFlight.booked_out_dt) >= date_from)
if date_to:
query = query.filter(func.date(LocalFlight.booked_out_dt) <= date_to)
return query.order_by(desc(LocalFlight.booked_out_dt)).offset(skip).limit(limit).all()
def get_active_flights(self, db: Session) -> List[LocalFlight]:
"""Get currently active (booked out or departed) flights"""
return db.query(LocalFlight).filter(
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.DEPARTED
)
).order_by(desc(LocalFlight.booked_out_dt)).all()
def get_departures_today(self, db: Session) -> List[LocalFlight]:
"""Get today's departures (booked out or departed)"""
today = date.today()
return db.query(LocalFlight).filter(
and_(
func.date(LocalFlight.booked_out_dt) == today,
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.DEPARTED
)
)
).order_by(LocalFlight.booked_out_dt).all()
def get_booked_out_today(self, db: Session) -> List[LocalFlight]:
"""Get all flights booked out today"""
today = date.today()
return db.query(LocalFlight).filter(
and_(
func.date(LocalFlight.booked_out_dt) == today,
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.LANDED
)
)
).order_by(LocalFlight.booked_out_dt).all()
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str) -> LocalFlight:
db_obj = LocalFlight(
**obj_in.dict(),
created_by=created_by,
status=LocalFlightStatus.BOOKED_OUT
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, db_obj: LocalFlight, obj_in: LocalFlightUpdate) -> LocalFlight:
update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items():
if value is not None:
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_status(
self,
db: Session,
flight_id: int,
status: LocalFlightStatus,
timestamp: Optional[datetime] = None
) -> Optional[LocalFlight]:
db_obj = self.get(db, flight_id)
if not db_obj:
return None
# Ensure status is a LocalFlightStatus enum
if isinstance(status, str):
status = LocalFlightStatus(status)
db_obj.status = status
# Set timestamps based on status
current_time = timestamp if timestamp is not None else datetime.utcnow()
if status == LocalFlightStatus.DEPARTED:
db_obj.departure_dt = current_time
elif status == LocalFlightStatus.LANDED:
db_obj.landed_dt = current_time
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def cancel(self, db: Session, flight_id: int) -> Optional[LocalFlight]:
db_obj = self.get(db, flight_id)
if db_obj:
db_obj.status = LocalFlightStatus.CANCELLED
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
local_flight = CRUDLocalFlight()

View File

@@ -8,6 +8,10 @@ import redis.asyncio as redis
from app.core.config import settings
from app.api.api import api_router
# Import models to ensure they're registered with SQLAlchemy
from app.models.ppr import PPRRecord, User, Journal, Airport, Aircraft
from app.models.local_flight import LocalFlight
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, Integer, String, DateTime, Text, Enum as SQLEnum, BigInteger
from sqlalchemy.sql import func
from enum import Enum
from app.db.session import Base
class LocalFlightType(str, Enum):
LOCAL = "LOCAL"
CIRCUITS = "CIRCUITS"
DEPARTURE = "DEPARTURE"
class LocalFlightStatus(str, Enum):
BOOKED_OUT = "BOOKED_OUT"
DEPARTED = "DEPARTED"
LANDED = "LANDED"
CANCELLED = "CANCELLED"
class LocalFlight(Base):
__tablename__ = "local_flights"
id = Column(BigInteger, primary_key=True, autoincrement=True)
registration = Column(String(16), nullable=False, index=True)
type = Column(String(32), nullable=False) # Aircraft type
callsign = Column(String(16), nullable=True)
pob = Column(Integer, nullable=False) # Persons on board
flight_type = Column(SQLEnum(LocalFlightType), nullable=False, index=True)
status = Column(SQLEnum(LocalFlightStatus), nullable=False, default=LocalFlightStatus.BOOKED_OUT, index=True)
notes = Column(Text, nullable=True)
booked_out_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
departure_dt = Column(DateTime, nullable=True) # Actual takeoff time
landed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())

View File

@@ -0,0 +1,81 @@
from pydantic import BaseModel, validator
from datetime import datetime
from typing import Optional
from enum import Enum
class LocalFlightType(str, Enum):
LOCAL = "LOCAL"
CIRCUITS = "CIRCUITS"
DEPARTURE = "DEPARTURE"
class LocalFlightStatus(str, Enum):
BOOKED_OUT = "BOOKED_OUT"
DEPARTED = "DEPARTED"
LANDED = "LANDED"
CANCELLED = "CANCELLED"
class LocalFlightBase(BaseModel):
registration: str
type: str # Aircraft type
callsign: Optional[str] = None
pob: int
flight_type: LocalFlightType
notes: Optional[str] = None
@validator('registration')
def validate_registration(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Aircraft registration is required')
return v.strip().upper()
@validator('type')
def validate_type(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Aircraft type is required')
return v.strip()
@validator('pob')
def validate_pob(cls, v):
if v is not None and v < 1:
raise ValueError('Persons on board must be at least 1')
return v
class LocalFlightCreate(LocalFlightBase):
pass
class LocalFlightUpdate(BaseModel):
registration: Optional[str] = None
type: Optional[str] = None
callsign: Optional[str] = None
pob: Optional[int] = None
flight_type: Optional[LocalFlightType] = None
status: Optional[LocalFlightStatus] = None
departure_dt: Optional[datetime] = None
notes: Optional[str] = None
class LocalFlightStatusUpdate(BaseModel):
status: LocalFlightStatus
timestamp: Optional[datetime] = None
class LocalFlightInDBBase(LocalFlightBase):
id: int
status: LocalFlightStatus
booked_out_dt: datetime
departure_dt: Optional[datetime] = None
landed_dt: Optional[datetime] = None
created_by: Optional[str] = None
updated_at: datetime
class Config:
from_attributes = True
class LocalFlight(LocalFlightInDBBase):
pass

View File

@@ -632,6 +632,9 @@
<button class="btn btn-success" onclick="openNewPPRModal()">
New PPR
</button>
<button class="btn btn-info" onclick="openLocalFlightModal()">
🛫 Book Out
</button>
<button class="btn btn-primary" onclick="window.open('reports.html', '_blank')">
📊 Reports
</button>
@@ -965,6 +968,137 @@
</div>
</div>
<!-- Local Flight (Book Out) Modal -->
<div id="localFlightModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="local-flight-modal-title">Book Out</h2>
<button class="close" onclick="closeLocalFlightModal()">&times;</button>
</div>
<div class="modal-body">
<form id="local-flight-form">
<input type="hidden" id="local-flight-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="local_registration">Aircraft Registration *</label>
<input type="text" id="local_registration" name="registration" required oninput="handleLocalAircraftLookup(this.value)">
<div id="local-aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="local_type">Aircraft Type *</label>
<input type="text" id="local_type" name="type" required tabindex="-1">
</div>
<div class="form-group">
<label for="local_callsign">Callsign (optional)</label>
<input type="text" id="local_callsign" name="callsign" placeholder="If different from registration">
</div>
<div class="form-group">
<label for="local_pob">Persons on Board *</label>
<input type="number" id="local_pob" name="pob" required min="1">
</div>
<div class="form-group">
<label for="local_flight_type">Flight Type *</label>
<select id="local_flight_type" name="flight_type" required>
<option value="">Select Type</option>
<option value="LOCAL">Local Flight</option>
<option value="CIRCUITS">Circuits</option>
<option value="DEPARTURE">Departure</option>
</select>
</div>
<div class="form-group full-width">
<label for="local_notes">Notes</label>
<textarea id="local_notes" name="notes" rows="3" placeholder="e.g., destination, any special requirements"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeLocalFlightModal()">
Cancel
</button>
<button type="submit" class="btn btn-success">
🛫 Book Out
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Local Flight Edit Modal -->
<div id="localFlightEditModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="local-flight-edit-title">Local Flight Details</h2>
<button class="close" onclick="closeLocalFlightEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="quick-actions">
<button id="local-btn-departed" class="btn btn-primary btn-sm" onclick="updateLocalFlightStatus('DEPARTED')" style="display: none;">
🛫 Mark Departed
</button>
<button id="local-btn-landed" class="btn btn-warning btn-sm" onclick="updateLocalFlightStatus('LANDED')" style="display: none;">
🛬 Land
</button>
<button id="local-btn-cancel" class="btn btn-danger btn-sm" onclick="updateLocalFlightStatus('CANCELLED')" style="display: none;">
❌ Cancel
</button>
</div>
<form id="local-flight-edit-form">
<input type="hidden" id="local-edit-flight-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="local_edit_registration">Aircraft Registration</label>
<input type="text" id="local_edit_registration" name="registration" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="local_edit_type">Aircraft Type</label>
<input type="text" id="local_edit_type" name="type" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group">
<label for="local_edit_callsign">Callsign</label>
<input type="text" id="local_edit_callsign" name="callsign">
</div>
<div class="form-group">
<label for="local_edit_pob">POB</label>
<input type="number" id="local_edit_pob" name="pob" min="1">
</div>
<div class="form-group">
<label for="local_edit_flight_type">Flight Type</label>
<select id="local_edit_flight_type" name="flight_type">
<option value="LOCAL">Local Flight</option>
<option value="CIRCUITS">Circuits</option>
<option value="DEPARTURE">Departure</option>
</select>
</div>
<div class="form-group">
<label for="local_edit_departure_dt">Departure Time</label>
<div style="display: flex; gap: 0.5rem;">
<input type="date" id="local_edit_departure_date" name="departure_date" style="flex: 1;">
<input type="time" id="local_edit_departure_time" name="departure_time" style="flex: 1;">
</div>
</div>
<div class="form-group full-width">
<label for="local_edit_notes">Notes</label>
<textarea id="local_edit_notes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeLocalFlightEditModal()">
Cancel
</button>
<button type="submit" class="btn btn-success">
💾 Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="modal">
<div class="modal-content">
@@ -1508,22 +1642,24 @@
await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked(), loadUpcoming()]);
}
// Load arrivals (NEW and CONFIRMED status)
// Load arrivals (NEW and CONFIRMED status for PPR, DEPARTED for local flights)
async function loadArrivals() {
document.getElementById('arrivals-loading').style.display = 'block';
document.getElementById('arrivals-table-content').style.display = 'none';
document.getElementById('arrivals-no-data').style.display = 'none';
try {
// Load all PPRs and filter client-side for today's arrivals
// We filter by ETA date (not ETD) and NEW/CONFIRMED status
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
// Load PPRs and local flights that are in the air
const [pprResponse, localResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
]);
if (!response.ok) {
if (!pprResponse.ok) {
throw new Error('Failed to fetch arrivals');
}
const allPPRs = await response.json();
const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for arrivals with ETA today and NEW or CONFIRMED status
@@ -1536,6 +1672,16 @@
return etaDate === today;
});
// Add local flights in DEPARTED status (in the air, heading back)
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localInAir = localFlights.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
arrivals.push(...localInAir);
}
displayArrivals(arrivals);
} catch (error) {
console.error('Error loading arrivals:', error);
@@ -1547,25 +1693,27 @@
document.getElementById('arrivals-loading').style.display = 'none';
}
// Load departures (LANDED status)
// Load departures (LANDED status for PPR, BOOKED_OUT only for local flights)
async function loadDepartures() {
document.getElementById('departures-loading').style.display = 'block';
document.getElementById('departures-table-content').style.display = 'none';
document.getElementById('departures-no-data').style.display = 'none';
try {
// Load all PPRs and filter client-side for today's departures
// We filter by ETD date and LANDED status only (exclude DEPARTED)
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
// Load PPR departures and local flight departures (BOOKED_OUT only) simultaneously
const [pprResponse, localResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000')
]);
if (!response.ok) {
throw new Error('Failed to fetch departures');
if (!pprResponse.ok) {
throw new Error('Failed to fetch PPR departures');
}
const allPPRs = await response.json();
const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for departures with ETD today and LANDED status only
// Filter for PPR departures with ETD today and LANDED status only
const departures = allPPRs.filter(ppr => {
if (!ppr.etd || ppr.status !== 'LANDED') {
return false;
@@ -1575,6 +1723,16 @@
return etdDate === today;
});
// Add local flights (BOOKED_OUT status - ready to go)
if (localResponse.ok) {
const localFlights = await localResponse.json();
const localDepartures = localFlights.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
departures.push(...localDepartures);
}
displayDepartures(departures);
} catch (error) {
console.error('Error loading departures:', error);
@@ -1593,16 +1751,19 @@
document.getElementById('departed-no-data').style.display = 'none';
try {
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
const [pprResponse, localResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
]);
if (!response.ok) {
if (!pprResponse.ok) {
throw new Error('Failed to fetch departed aircraft');
}
const allPPRs = await response.json();
const allPPRs = await pprResponse.json();
const today = new Date().toISOString().split('T')[0];
// Filter for aircraft departed today
// Filter for PPRs departed today
const departed = allPPRs.filter(ppr => {
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
return false;
@@ -1611,6 +1772,20 @@
return departedDate === today;
});
// Add local flights departed today
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];
return departedDate === today;
}).map(flight => ({
...flight,
isLocalFlight: true
}));
departed.push(...localDeparted);
}
displayDeparted(departed);
} catch (error) {
console.error('Error loading departed aircraft:', error);
@@ -1632,22 +1807,43 @@
}
// Sort by departed time
departed.sort((a, b) => new Date(a.departed_dt) - new Date(b.departed_dt));
departed.sort((a, b) => {
const aTime = a.departed_dt || a.departure_dt;
const bTime = b.departed_dt || b.departure_dt;
return new Date(aTime) - new Date(bTime);
});
tbody.innerHTML = '';
document.getElementById('departed-table-content').style.display = 'block';
for (const ppr of departed) {
for (const flight of departed) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
const isLocal = flight.isLocalFlight;
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
};
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
if (isLocal) {
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;">${ppr.ac_call || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.out_to || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(ppr.departed_dt)}</td>
<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>
`;
} else {
row.innerHTML = `
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_reg || '-'}</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>
`;
}
tbody.appendChild(row);
}
}
@@ -1885,47 +2081,89 @@
return;
}
// Sort arrivals by ETA (ascending)
// Sort arrivals by ETA/departure time (ascending)
arrivals.sort((a, b) => {
if (!a.eta) return 1;
if (!b.eta) return -1;
return new Date(a.eta) - new Date(b.eta);
const aTime = a.eta || a.departure_dt;
const bTime = b.eta || b.departure_dt;
if (!aTime) return 1;
if (!bTime) return -1;
return new Date(aTime) - new Date(bTime);
});
tbody.innerHTML = '';
document.getElementById('arrivals-table-content').style.display = 'block';
for (const ppr of arrivals) {
for (const flight of arrivals) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
const isLocal = flight.isLocalFlight;
// Click handler that routes to correct modal
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
};
// Create notes indicator if notes exist
const notesIndicator = ppr.notes && ppr.notes.trim() ?
const notesIndicator = flight.notes && flight.notes.trim() ?
`<span class="notes-tooltip">
<span class="notes-indicator">📝</span>
<span class="tooltip-text">${ppr.notes}</span>
<span class="tooltip-text">${flight.notes}</span>
</span>` : '';
// Display callsign as main item if present, registration below; otherwise show registration
const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
`<strong>${ppr.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${ppr.ac_reg}</span>` :
`<strong>${ppr.ac_reg}</strong>`;
// Lookup airport name for in_from
let fromDisplay = ppr.in_from;
if (ppr.in_from && ppr.in_from.length === 4 && /^[A-Z]{4}$/.test(ppr.in_from)) {
fromDisplay = await getAirportDisplay(ppr.in_from);
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${ppr.ac_type}</td>
<td>${fromDisplay}</td>
<td>${formatTimeOnly(ppr.eta)}</td>
<td>${ppr.pob_in}</td>
<td>${ppr.fuel || '-'}</td>
<td>
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${ppr.id})" title="Mark as Landed">
let aircraftDisplay, acType, fromDisplay, eta, pob, fuel, actionButtons;
if (isLocal) {
// Local flight display
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
acType = flight.type;
fromDisplay = '-';
eta = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
pob = flight.pob || '-';
fuel = '-';
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Arrival">
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
CANCEL
</button>
</td>
`;
} else {
// PPR display
const callsign = flight.ac_call && flight.ac_call.trim() ? flight.ac_call : flight.ac_reg;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
acType = flight.ac_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;
eta = formatTimeOnly(flight.eta);
pob = flight.pob_in;
fuel = flight.fuel || '-';
actionButtons = `
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${flight.id})" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${flight.id}, 'CANCELED')" title="Cancel Arrival">
CANCEL
</button>
`;
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${acType}</td>
<td>${fromDisplay}</td>
<td>${eta}</td>
<td>${pob}</td>
<td>${fuel}</td>
<td>${actionButtons}</td>
`;
tbody.appendChild(row);
}
@@ -1944,46 +2182,100 @@
// Sort departures by ETD (ascending), nulls last
departures.sort((a, b) => {
if (!a.etd) return 1;
if (!b.etd) return -1;
return new Date(a.etd) - new Date(b.etd);
const aTime = a.etd || a.booked_out_dt;
const bTime = b.etd || b.booked_out_dt;
if (!aTime) return 1;
if (!bTime) return -1;
return new Date(aTime) - new Date(bTime);
});
tbody.innerHTML = '';
document.getElementById('departures-table-content').style.display = 'block';
for (const ppr of departures) {
for (const flight of departures) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
const isLocal = flight.isLocalFlight;
// Click handler that routes to correct modal
row.onclick = () => {
if (isLocal) {
openLocalFlightEditModal(flight.id);
} else {
openPPRModal(flight.id);
}
};
// Create notes indicator if notes exist
const notesIndicator = ppr.notes && ppr.notes.trim() ?
const notesIndicator = flight.notes && flight.notes.trim() ?
`<span class="notes-tooltip">
<span class="notes-indicator">📝</span>
<span class="tooltip-text">${ppr.notes}</span>
<span class="tooltip-text">${flight.notes}</span>
</span>` : '';
// Display callsign as main item if present, registration below; otherwise show registration
const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
`<strong>${ppr.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${ppr.ac_reg}</span>` :
`<strong>${ppr.ac_reg}</strong>`;
// Lookup airport name for out_to
let toDisplay = ppr.out_to || '-';
if (ppr.out_to && ppr.out_to.length === 4 && /^[A-Z]{4}$/.test(ppr.out_to)) {
toDisplay = await getAirportDisplay(ppr.out_to);
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${ppr.ac_type}</td>
<td>${toDisplay}</td>
<td>${ppr.etd ? formatTimeOnly(ppr.etd) : '-'}</td>
<td>${ppr.pob_out || ppr.pob_in}</td>
<td>${ppr.fuel || '-'}</td>
<td>${ppr.landed_dt ? formatTimeOnly(ppr.landed_dt) : '-'}</td>
<td>
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ppr.id})" title="Mark as Departed">
let aircraftDisplay, toDisplay, etd, pob, fuel, landedDt, actionButtons;
if (isLocal) {
// Local flight display
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
toDisplay = '-';
etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
pob = flight.pob || '-';
fuel = '-';
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
// Action buttons for local flight
if (flight.status === 'BOOKED_OUT') {
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'DEPARTED')" title="Mark as Departed">
TAKE OFF
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Departure">
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
CANCEL
</button>
</td>
`;
} else if (flight.status === 'DEPARTED') {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
CANCEL
</button>
`;
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
} else {
// PPR display
const callsign = flight.ac_call && flight.ac_call.trim() ? flight.ac_call : flight.ac_reg;
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
toDisplay = 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);
}
etd = flight.etd ? formatTimeOnly(flight.etd) : '-';
pob = flight.pob_out || flight.pob_in;
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>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${flight.id}, 'CANCELED')" title="Cancel Departure">
CANCEL
</button>
`;
}
row.innerHTML = `
<td>${aircraftDisplay}${notesIndicator}</td>
<td>${isLocal ? flight.type : flight.ac_type}</td>
<td>${toDisplay}</td>
<td>${etd}</td>
<td>${pob}</td>
<td>${fuel}</td>
<td>${landedDt}</td>
<td>${actionButtons}</td>
`;
tbody.appendChild(row);
}
@@ -3114,6 +3406,337 @@
tooltip.style.top = top + 'px';
}
// Local Flight (Book Out) Modal Functions
function openLocalFlightModal() {
document.getElementById('local-flight-form').reset();
document.getElementById('local-flight-id').value = '';
document.getElementById('local-flight-modal-title').textContent = 'Book Out';
document.getElementById('localFlightModal').style.display = 'block';
// Clear aircraft lookup results
clearLocalAircraftLookup();
// Auto-focus on registration field
setTimeout(() => {
document.getElementById('local_registration').focus();
}, 100);
}
function closeLocalFlightModal() {
document.getElementById('localFlightModal').style.display = 'none';
}
// Handle aircraft lookup for local flights
let localAircraftLookupTimeout;
function handleLocalAircraftLookup(registration) {
// Clear previous timeout
if (localAircraftLookupTimeout) {
clearTimeout(localAircraftLookupTimeout);
}
// Clear results if input is too short
if (registration.length < 4) {
clearLocalAircraftLookup();
return;
}
// Show searching indicator
document.getElementById('local-aircraft-lookup-results').innerHTML =
'<div class="aircraft-searching">Searching...</div>';
// Debounce the search - wait 300ms after user stops typing
localAircraftLookupTimeout = setTimeout(() => {
performLocalAircraftLookup(registration);
}, 300);
}
async function performLocalAircraftLookup(registration) {
try {
// Clean the input (remove non-alphanumeric characters and make uppercase)
const cleanInput = registration.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
if (cleanInput.length < 4) {
clearLocalAircraftLookup();
return;
}
// Call the API
const response = await authenticatedFetch(`/api/v1/aircraft/lookup/${cleanInput}`);
if (!response.ok) {
throw new Error('Failed to fetch aircraft data');
}
const matches = await response.json();
displayLocalAircraftLookupResults(matches, cleanInput);
} catch (error) {
console.error('Aircraft lookup error:', error);
document.getElementById('local-aircraft-lookup-results').innerHTML =
'<div class="aircraft-no-match">Lookup failed - please enter manually</div>';
}
}
function displayLocalAircraftLookupResults(matches, searchTerm) {
const resultsDiv = document.getElementById('local-aircraft-lookup-results');
if (matches.length === 0) {
resultsDiv.innerHTML = '<div class="aircraft-no-match">No matches found</div>';
} else if (matches.length === 1) {
// Unique match found - auto-populate
const aircraft = matches[0];
resultsDiv.innerHTML = `
<div class="aircraft-match">
${aircraft.manufacturer_name} ${aircraft.model} (${aircraft.type_code})
</div>
`;
// Auto-populate the form fields
document.getElementById('local_registration').value = aircraft.registration;
document.getElementById('local_type').value = aircraft.type_code;
} else {
// Multiple matches - show list but don't auto-populate
resultsDiv.innerHTML = `
<div class="aircraft-no-match">
Multiple matches found (${matches.length}) - please be more specific
</div>
`;
}
}
function clearLocalAircraftLookup() {
document.getElementById('local-aircraft-lookup-results').innerHTML = '';
}
// Local Flight Edit Modal Functions
let currentLocalFlightId = null;
async function openLocalFlightEditModal(flightId) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/local-flights/${flightId}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) throw new Error('Failed to load flight');
const flight = await response.json();
currentLocalFlightId = flight.id;
// Populate form
document.getElementById('local-edit-flight-id').value = flight.id;
document.getElementById('local_edit_registration').value = flight.registration;
document.getElementById('local_edit_type').value = flight.type;
document.getElementById('local_edit_callsign').value = flight.callsign || '';
document.getElementById('local_edit_pob').value = flight.pob;
document.getElementById('local_edit_flight_type').value = flight.flight_type;
document.getElementById('local_edit_notes').value = flight.notes || '';
// Parse and populate departure time if exists
if (flight.departure_dt) {
const dept = new Date(flight.departure_dt);
document.getElementById('local_edit_departure_date').value = dept.toISOString().slice(0, 10);
document.getElementById('local_edit_departure_time').value = dept.toISOString().slice(11, 16);
}
// Show/hide action buttons based on status
const deptBtn = document.getElementById('local-btn-departed');
const landBtn = document.getElementById('local-btn-landed');
const cancelBtn = document.getElementById('local-btn-cancel');
deptBtn.style.display = flight.status === 'BOOKED_OUT' ? 'inline-block' : 'none';
landBtn.style.display = flight.status === 'DEPARTED' ? 'inline-block' : 'none';
cancelBtn.style.display = (flight.status === 'BOOKED_OUT' || flight.status === 'DEPARTED') ? 'inline-block' : 'none';
document.getElementById('local-flight-edit-title').textContent = `${flight.registration} - ${flight.flight_type}`;
document.getElementById('localFlightEditModal').style.display = 'block';
} catch (error) {
console.error('Error loading flight:', error);
showNotification('Error loading flight details', true);
}
}
function closeLocalFlightEditModal() {
document.getElementById('localFlightEditModal').style.display = 'none';
currentLocalFlightId = null;
}
// Update status from table buttons (with flight ID passed)
async function updateLocalFlightStatusFromTable(flightId, status) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/local-flights/${flightId}/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');
loadPPRs(); // Refresh display
showNotification(`Flight marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating flight status', true);
}
}
// Update status from modal (uses currentLocalFlightId)
async function updateLocalFlightStatus(status) {
if (!currentLocalFlightId || !accessToken) return;
try {
const response = await fetch(`/api/v1/local-flights/${currentLocalFlightId}/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');
closeLocalFlightEditModal();
loadPPRs(); // Refresh display
showNotification(`Flight marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification('Error updating flight status', true);
}
}
// Local flight edit form submission
document.getElementById('local-flight-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!currentLocalFlightId || !accessToken) return;
const formData = new FormData(this);
const updateData = {};
formData.forEach((value, key) => {
if (key === 'id') return;
// Handle date/time combination for departure
if (key === 'departure_date' || key === 'departure_time') {
if (!updateData.departure_dt && formData.get('departure_date') && formData.get('departure_time')) {
const dateStr = formData.get('departure_date');
const timeStr = formData.get('departure_time');
updateData.departure_dt = new Date(`${dateStr}T${timeStr}`).toISOString();
}
return;
}
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (key === 'pob') {
updateData[key] = parseInt(value);
} else if (value.trim) {
updateData[key] = value.trim();
} else {
updateData[key] = value;
}
}
});
try {
const response = await fetch(`/api/v1/local-flights/${currentLocalFlightId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(updateData)
});
if (!response.ok) throw new Error('Failed to update flight');
closeLocalFlightEditModal();
loadPPRs(); // Refresh display
showNotification('Flight updated successfully');
} catch (error) {
console.error('Error updating flight:', error);
showNotification('Error updating flight', true);
}
});
// Add event listener for local flight form submission
document.getElementById('local-flight-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!accessToken) return;
const formData = new FormData(this);
const flightData = {};
formData.forEach((value, key) => {
// Skip the hidden id field and empty values
if (key === 'id') return;
// Handle date/time combination for departure
if (key === 'departure_date' || key === 'departure_time') {
if (!flightData.departure_dt && formData.get('departure_date') && formData.get('departure_time')) {
const dateStr = formData.get('departure_date');
const timeStr = formData.get('departure_time');
flightData.departure_dt = new Date(`${dateStr}T${timeStr}`).toISOString();
}
return;
}
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (key === 'pob') {
flightData[key] = parseInt(value);
} else if (value.trim) {
flightData[key] = value.trim();
} else {
flightData[key] = value;
}
}
});
console.log('Submitting flight data:', flightData);
try {
const response = await fetch('/api/v1/local-flights/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(flightData)
});
if (!response.ok) {
let errorMessage = 'Failed to book out flight';
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorMessage;
} 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();
closeLocalFlightModal();
loadPPRs(); // Refresh tables
showNotification(`Aircraft ${result.registration} booked out successfully!`);
} catch (error) {
console.error('Error booking out flight:', error);
showNotification(`Error: ${error.message}`, true);
}
});
// Add hover listeners to all notes tooltips
function setupTooltips() {
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {