local-flights #5
58
backend/alembic/versions/002_local_flights.py
Normal file
58
backend/alembic/versions/002_local_flights.py
Normal 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')
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from fastapi import APIRouter
|
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 = APIRouter()
|
||||||
|
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||||
api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"])
|
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(public.router, prefix="/public", tags=["public"])
|
||||||
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
|
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
|
||||||
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])
|
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])
|
||||||
195
backend/app/api/endpoints/local_flights.py
Normal file
195
backend/app/api/endpoints/local_flights.py
Normal 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)
|
||||||
136
backend/app/crud/crud_local_flight.py
Normal file
136
backend/app/crud/crud_local_flight.py
Normal 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()
|
||||||
@@ -8,6 +8,10 @@ import redis.asyncio as redis
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.api.api import api_router
|
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
|
# Set up logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
35
backend/app/models/local_flight.py
Normal file
35
backend/app/models/local_flight.py
Normal 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())
|
||||||
81
backend/app/schemas/local_flight.py
Normal file
81
backend/app/schemas/local_flight.py
Normal 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
|
||||||
785
web/admin.html
785
web/admin.html
@@ -632,6 +632,9 @@
|
|||||||
<button class="btn btn-success" onclick="openNewPPRModal()">
|
<button class="btn btn-success" onclick="openNewPPRModal()">
|
||||||
➕ New PPR
|
➕ New PPR
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-info" onclick="openLocalFlightModal()">
|
||||||
|
🛫 Book Out
|
||||||
|
</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>
|
||||||
@@ -965,6 +968,137 @@
|
|||||||
</div>
|
</div>
|
||||||
</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()">×</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()">×</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 -->
|
<!-- User Management Modal -->
|
||||||
<div id="userManagementModal" class="modal">
|
<div id="userManagementModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -1508,22 +1642,24 @@
|
|||||||
await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked(), loadUpcoming()]);
|
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() {
|
async function loadArrivals() {
|
||||||
document.getElementById('arrivals-loading').style.display = 'block';
|
document.getElementById('arrivals-loading').style.display = 'block';
|
||||||
document.getElementById('arrivals-table-content').style.display = 'none';
|
document.getElementById('arrivals-table-content').style.display = 'none';
|
||||||
document.getElementById('arrivals-no-data').style.display = 'none';
|
document.getElementById('arrivals-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load all PPRs and filter client-side for today's arrivals
|
// Load PPRs and local flights that are in the air
|
||||||
// We filter by ETA date (not ETD) and NEW/CONFIRMED status
|
const [pprResponse, localResponse] = await Promise.all([
|
||||||
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
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');
|
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];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
// Filter for arrivals with ETA today and NEW or CONFIRMED status
|
// Filter for arrivals with ETA today and NEW or CONFIRMED status
|
||||||
@@ -1536,6 +1672,16 @@
|
|||||||
return etaDate === today;
|
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);
|
displayArrivals(arrivals);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading arrivals:', error);
|
console.error('Error loading arrivals:', error);
|
||||||
@@ -1547,25 +1693,27 @@
|
|||||||
document.getElementById('arrivals-loading').style.display = 'none';
|
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() {
|
async function loadDepartures() {
|
||||||
document.getElementById('departures-loading').style.display = 'block';
|
document.getElementById('departures-loading').style.display = 'block';
|
||||||
document.getElementById('departures-table-content').style.display = 'none';
|
document.getElementById('departures-table-content').style.display = 'none';
|
||||||
document.getElementById('departures-no-data').style.display = 'none';
|
document.getElementById('departures-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load all PPRs and filter client-side for today's departures
|
// Load PPR departures and local flight departures (BOOKED_OUT only) simultaneously
|
||||||
// We filter by ETD date and LANDED status only (exclude DEPARTED)
|
const [pprResponse, localResponse] = await Promise.all([
|
||||||
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000')
|
||||||
|
]);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!pprResponse.ok) {
|
||||||
throw new Error('Failed to fetch departures');
|
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];
|
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 => {
|
const departures = allPPRs.filter(ppr => {
|
||||||
if (!ppr.etd || ppr.status !== 'LANDED') {
|
if (!ppr.etd || ppr.status !== 'LANDED') {
|
||||||
return false;
|
return false;
|
||||||
@@ -1575,6 +1723,16 @@
|
|||||||
return etdDate === today;
|
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);
|
displayDepartures(departures);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading departures:', error);
|
console.error('Error loading departures:', error);
|
||||||
@@ -1593,16 +1751,19 @@
|
|||||||
document.getElementById('departed-no-data').style.display = 'none';
|
document.getElementById('departed-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
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');
|
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];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
// Filter for aircraft departed today
|
// Filter for PPRs departed today
|
||||||
const departed = allPPRs.filter(ppr => {
|
const departed = allPPRs.filter(ppr => {
|
||||||
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
|
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
|
||||||
return false;
|
return false;
|
||||||
@@ -1611,6 +1772,20 @@
|
|||||||
return departedDate === today;
|
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);
|
displayDeparted(departed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading departed aircraft:', error);
|
console.error('Error loading departed aircraft:', error);
|
||||||
@@ -1632,22 +1807,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort by departed time
|
// 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 = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('departed-table-content').style.display = 'block';
|
document.getElementById('departed-table-content').style.display = 'block';
|
||||||
|
|
||||||
for (const ppr of departed) {
|
for (const flight of departed) {
|
||||||
const row = document.createElement('tr');
|
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;';
|
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
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;">${flight.registration || '-'}</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;">${flight.callsign || '-'}</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;">-</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;">${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);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1885,47 +2081,89 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort arrivals by ETA (ascending)
|
// Sort arrivals by ETA/departure time (ascending)
|
||||||
arrivals.sort((a, b) => {
|
arrivals.sort((a, b) => {
|
||||||
if (!a.eta) return 1;
|
const aTime = a.eta || a.departure_dt;
|
||||||
if (!b.eta) return -1;
|
const bTime = b.eta || b.departure_dt;
|
||||||
return new Date(a.eta) - new Date(b.eta);
|
if (!aTime) return 1;
|
||||||
|
if (!bTime) return -1;
|
||||||
|
return new Date(aTime) - new Date(bTime);
|
||||||
});
|
});
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('arrivals-table-content').style.display = 'block';
|
document.getElementById('arrivals-table-content').style.display = 'block';
|
||||||
for (const ppr of arrivals) {
|
for (const flight of arrivals) {
|
||||||
const row = document.createElement('tr');
|
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
|
// 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-tooltip">
|
||||||
<span class="notes-indicator">📝</span>
|
<span class="notes-indicator">📝</span>
|
||||||
<span class="tooltip-text">${ppr.notes}</span>
|
<span class="tooltip-text">${flight.notes}</span>
|
||||||
</span>` : '';
|
</span>` : '';
|
||||||
// Display callsign as main item if present, registration below; otherwise show registration
|
|
||||||
const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
|
let aircraftDisplay, acType, fromDisplay, eta, pob, fuel, actionButtons;
|
||||||
`<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>`;
|
if (isLocal) {
|
||||||
// Lookup airport name for in_from
|
// Local flight display
|
||||||
let fromDisplay = ppr.in_from;
|
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration;
|
||||||
if (ppr.in_from && ppr.in_from.length === 4 && /^[A-Z]{4}$/.test(ppr.in_from)) {
|
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||||||
fromDisplay = await getAirportDisplay(ppr.in_from);
|
acType = flight.type;
|
||||||
}
|
fromDisplay = '-';
|
||||||
row.innerHTML = `
|
eta = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
|
||||||
<td>${aircraftDisplay}${notesIndicator}</td>
|
pob = flight.pob || '-';
|
||||||
<td>${ppr.ac_type}</td>
|
fuel = '-';
|
||||||
<td>${fromDisplay}</td>
|
actionButtons = `
|
||||||
<td>${formatTimeOnly(ppr.eta)}</td>
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
|
||||||
<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">
|
|
||||||
LAND
|
LAND
|
||||||
</button>
|
</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
|
CANCEL
|
||||||
</button>
|
</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);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
@@ -1944,46 +2182,100 @@
|
|||||||
|
|
||||||
// Sort departures by ETD (ascending), nulls last
|
// Sort departures by ETD (ascending), nulls last
|
||||||
departures.sort((a, b) => {
|
departures.sort((a, b) => {
|
||||||
if (!a.etd) return 1;
|
const aTime = a.etd || a.booked_out_dt;
|
||||||
if (!b.etd) return -1;
|
const bTime = b.etd || b.booked_out_dt;
|
||||||
return new Date(a.etd) - new Date(b.etd);
|
if (!aTime) return 1;
|
||||||
|
if (!bTime) return -1;
|
||||||
|
return new Date(aTime) - new Date(bTime);
|
||||||
});
|
});
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('departures-table-content').style.display = 'block';
|
document.getElementById('departures-table-content').style.display = 'block';
|
||||||
for (const ppr of departures) {
|
for (const flight of departures) {
|
||||||
const row = document.createElement('tr');
|
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
|
// 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-tooltip">
|
||||||
<span class="notes-indicator">📝</span>
|
<span class="notes-indicator">📝</span>
|
||||||
<span class="tooltip-text">${ppr.notes}</span>
|
<span class="tooltip-text">${flight.notes}</span>
|
||||||
</span>` : '';
|
</span>` : '';
|
||||||
// Display callsign as main item if present, registration below; otherwise show registration
|
|
||||||
const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
|
let aircraftDisplay, toDisplay, etd, pob, fuel, landedDt, actionButtons;
|
||||||
`<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>`;
|
if (isLocal) {
|
||||||
// Lookup airport name for out_to
|
// Local flight display
|
||||||
let toDisplay = ppr.out_to || '-';
|
const callsign = flight.callsign && flight.callsign.trim() ? flight.callsign : flight.registration;
|
||||||
if (ppr.out_to && ppr.out_to.length === 4 && /^[A-Z]{4}$/.test(ppr.out_to)) {
|
aircraftDisplay = `<strong>${callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||||||
toDisplay = await getAirportDisplay(ppr.out_to);
|
toDisplay = '-';
|
||||||
}
|
etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
|
||||||
row.innerHTML = `
|
pob = flight.pob || '-';
|
||||||
<td>${aircraftDisplay}${notesIndicator}</td>
|
fuel = '-';
|
||||||
<td>${ppr.ac_type}</td>
|
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
||||||
<td>${toDisplay}</td>
|
|
||||||
<td>${ppr.etd ? formatTimeOnly(ppr.etd) : '-'}</td>
|
// Action buttons for local flight
|
||||||
<td>${ppr.pob_out || ppr.pob_in}</td>
|
if (flight.status === 'BOOKED_OUT') {
|
||||||
<td>${ppr.fuel || '-'}</td>
|
actionButtons = `
|
||||||
<td>${ppr.landed_dt ? formatTimeOnly(ppr.landed_dt) : '-'}</td>
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'DEPARTED')" title="Mark as Departed">
|
||||||
<td>
|
|
||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ppr.id})" title="Mark as Departed">
|
|
||||||
TAKE OFF
|
TAKE OFF
|
||||||
</button>
|
</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
|
CANCEL
|
||||||
</button>
|
</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);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
@@ -3114,6 +3406,337 @@
|
|||||||
tooltip.style.top = top + 'px';
|
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
|
// Add hover listeners to all notes tooltips
|
||||||
function setupTooltips() {
|
function setupTooltips() {
|
||||||
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
||||||
|
|||||||
Reference in New Issue
Block a user