Getting there
This commit is contained in:
@@ -20,13 +20,13 @@ depends_on = None
|
||||
|
||||
def upgrade() -> None:
|
||||
"""
|
||||
Create local_flights table for tracking aircraft that book out locally.
|
||||
Create local_flights, departures, and arrivals tables.
|
||||
"""
|
||||
|
||||
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('type', sa.String(length=32), nullable=True),
|
||||
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),
|
||||
@@ -43,16 +43,70 @@ def upgrade() -> None:
|
||||
mysql_collate='utf8mb4_unicode_ci'
|
||||
)
|
||||
|
||||
# Create indexes for frequently queried columns
|
||||
# Create indexes for local_flights
|
||||
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'])
|
||||
|
||||
# Create departures table for non-PPR departures to other airports
|
||||
op.create_table('departures',
|
||||
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=True),
|
||||
sa.Column('callsign', sa.String(length=16), nullable=True),
|
||||
sa.Column('pob', sa.Integer(), nullable=False),
|
||||
sa.Column('out_to', sa.String(length=64), nullable=False),
|
||||
sa.Column('status', sa.Enum('BOOKED_OUT', 'DEPARTED', 'CANCELLED', name='departuresstatus'), 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('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'
|
||||
)
|
||||
|
||||
op.create_index('idx_dep_registration', 'departures', ['registration'])
|
||||
op.create_index('idx_dep_out_to', 'departures', ['out_to'])
|
||||
op.create_index('idx_dep_status', 'departures', ['status'])
|
||||
op.create_index('idx_dep_booked_out_dt', 'departures', ['booked_out_dt'])
|
||||
op.create_index('idx_dep_created_by', 'departures', ['created_by'])
|
||||
|
||||
# Create arrivals table for non-PPR arrivals from elsewhere
|
||||
op.create_table('arrivals',
|
||||
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=True),
|
||||
sa.Column('callsign', sa.String(length=16), nullable=True),
|
||||
sa.Column('pob', sa.Integer(), nullable=False),
|
||||
sa.Column('in_from', sa.String(length=64), nullable=False),
|
||||
sa.Column('status', sa.Enum('BOOKED_IN', 'LANDED', 'CANCELLED', name='arrivalsstatus'), nullable=False, server_default='BOOKED_IN'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('booked_in_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
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'
|
||||
)
|
||||
|
||||
op.create_index('idx_arr_registration', 'arrivals', ['registration'])
|
||||
op.create_index('idx_arr_in_from', 'arrivals', ['in_from'])
|
||||
op.create_index('idx_arr_status', 'arrivals', ['status'])
|
||||
op.create_index('idx_arr_booked_in_dt', 'arrivals', ['booked_in_dt'])
|
||||
op.create_index('idx_arr_created_by', 'arrivals', ['created_by'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Drop the local_flights table.
|
||||
Drop the local_flights, departures, and arrivals tables.
|
||||
"""
|
||||
op.drop_table('arrivals')
|
||||
op.drop_table('departures')
|
||||
op.drop_table('local_flights')
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights
|
||||
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals
|
||||
|
||||
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(departures.router, prefix="/departures", tags=["departures"])
|
||||
api_router.include_router(arrivals.router, prefix="/arrivals", tags=["arrivals"])
|
||||
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"])
|
||||
167
backend/app/api/endpoints/arrivals.py
Normal file
167
backend/app/api/endpoints/arrivals.py
Normal file
@@ -0,0 +1,167 @@
|
||||
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_arrival import arrival as crud_arrival
|
||||
from app.schemas.arrival import Arrival, ArrivalCreate, ArrivalUpdate, ArrivalStatus, ArrivalStatusUpdate
|
||||
from app.models.ppr import User
|
||||
from app.core.utils import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Arrival])
|
||||
async def get_arrivals(
|
||||
request: Request,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[ArrivalStatus] = 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 arrival records with optional filtering"""
|
||||
arrivals = crud_arrival.get_multi(
|
||||
db, skip=skip, limit=limit, status=status,
|
||||
date_from=date_from, date_to=date_to
|
||||
)
|
||||
return arrivals
|
||||
|
||||
|
||||
@router.post("/", response_model=Arrival)
|
||||
async def create_arrival(
|
||||
request: Request,
|
||||
arrival_in: ArrivalCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Create a new arrival record"""
|
||||
arrival = crud_arrival.create(db, obj_in=arrival_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": "arrival_booked_in",
|
||||
"data": {
|
||||
"id": arrival.id,
|
||||
"registration": arrival.registration,
|
||||
"in_from": arrival.in_from,
|
||||
"status": arrival.status.value
|
||||
}
|
||||
})
|
||||
|
||||
return arrival
|
||||
|
||||
|
||||
@router.get("/{arrival_id}", response_model=Arrival)
|
||||
async def get_arrival(
|
||||
arrival_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_read_user)
|
||||
):
|
||||
"""Get a specific arrival record"""
|
||||
arrival = crud_arrival.get(db, arrival_id=arrival_id)
|
||||
if not arrival:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Arrival record not found"
|
||||
)
|
||||
return arrival
|
||||
|
||||
|
||||
@router.put("/{arrival_id}", response_model=Arrival)
|
||||
async def update_arrival(
|
||||
request: Request,
|
||||
arrival_id: int,
|
||||
arrival_in: ArrivalUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Update an arrival record"""
|
||||
db_arrival = crud_arrival.get(db, arrival_id=arrival_id)
|
||||
if not db_arrival:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Arrival record not found"
|
||||
)
|
||||
|
||||
arrival = crud_arrival.update(db, db_obj=db_arrival, obj_in=arrival_in)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "arrival_updated",
|
||||
"data": {
|
||||
"id": arrival.id,
|
||||
"registration": arrival.registration,
|
||||
"status": arrival.status.value
|
||||
}
|
||||
})
|
||||
|
||||
return arrival
|
||||
|
||||
|
||||
@router.patch("/{arrival_id}/status", response_model=Arrival)
|
||||
async def update_arrival_status(
|
||||
request: Request,
|
||||
arrival_id: int,
|
||||
status_update: ArrivalStatusUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Update arrival status"""
|
||||
arrival = crud_arrival.update_status(
|
||||
db,
|
||||
arrival_id=arrival_id,
|
||||
status=status_update.status,
|
||||
timestamp=status_update.timestamp
|
||||
)
|
||||
if not arrival:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Arrival record not found"
|
||||
)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "arrival_status_update",
|
||||
"data": {
|
||||
"id": arrival.id,
|
||||
"registration": arrival.registration,
|
||||
"status": arrival.status.value,
|
||||
"landed_dt": arrival.landed_dt.isoformat() if arrival.landed_dt else None
|
||||
}
|
||||
})
|
||||
|
||||
return arrival
|
||||
|
||||
|
||||
@router.delete("/{arrival_id}", response_model=Arrival)
|
||||
async def cancel_arrival(
|
||||
request: Request,
|
||||
arrival_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Cancel an arrival record"""
|
||||
arrival = crud_arrival.cancel(db, arrival_id=arrival_id)
|
||||
if not arrival:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Arrival record not found"
|
||||
)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "arrival_cancelled",
|
||||
"data": {
|
||||
"id": arrival.id,
|
||||
"registration": arrival.registration
|
||||
}
|
||||
})
|
||||
|
||||
return arrival
|
||||
167
backend/app/api/endpoints/departures.py
Normal file
167
backend/app/api/endpoints/departures.py
Normal file
@@ -0,0 +1,167 @@
|
||||
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_departure import departure as crud_departure
|
||||
from app.schemas.departure import Departure, DepartureCreate, DepartureUpdate, DepartureStatus, DepartureStatusUpdate
|
||||
from app.models.ppr import User
|
||||
from app.core.utils import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Departure])
|
||||
async def get_departures(
|
||||
request: Request,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[DepartureStatus] = 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 departure records with optional filtering"""
|
||||
departures = crud_departure.get_multi(
|
||||
db, skip=skip, limit=limit, status=status,
|
||||
date_from=date_from, date_to=date_to
|
||||
)
|
||||
return departures
|
||||
|
||||
|
||||
@router.post("/", response_model=Departure)
|
||||
async def create_departure(
|
||||
request: Request,
|
||||
departure_in: DepartureCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Create a new departure record"""
|
||||
departure = crud_departure.create(db, obj_in=departure_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": "departure_booked_out",
|
||||
"data": {
|
||||
"id": departure.id,
|
||||
"registration": departure.registration,
|
||||
"out_to": departure.out_to,
|
||||
"status": departure.status.value
|
||||
}
|
||||
})
|
||||
|
||||
return departure
|
||||
|
||||
|
||||
@router.get("/{departure_id}", response_model=Departure)
|
||||
async def get_departure(
|
||||
departure_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_read_user)
|
||||
):
|
||||
"""Get a specific departure record"""
|
||||
departure = crud_departure.get(db, departure_id=departure_id)
|
||||
if not departure:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Departure record not found"
|
||||
)
|
||||
return departure
|
||||
|
||||
|
||||
@router.put("/{departure_id}", response_model=Departure)
|
||||
async def update_departure(
|
||||
request: Request,
|
||||
departure_id: int,
|
||||
departure_in: DepartureUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Update a departure record"""
|
||||
db_departure = crud_departure.get(db, departure_id=departure_id)
|
||||
if not db_departure:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Departure record not found"
|
||||
)
|
||||
|
||||
departure = crud_departure.update(db, db_obj=db_departure, obj_in=departure_in)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "departure_updated",
|
||||
"data": {
|
||||
"id": departure.id,
|
||||
"registration": departure.registration,
|
||||
"status": departure.status.value
|
||||
}
|
||||
})
|
||||
|
||||
return departure
|
||||
|
||||
|
||||
@router.patch("/{departure_id}/status", response_model=Departure)
|
||||
async def update_departure_status(
|
||||
request: Request,
|
||||
departure_id: int,
|
||||
status_update: DepartureStatusUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Update departure status"""
|
||||
departure = crud_departure.update_status(
|
||||
db,
|
||||
departure_id=departure_id,
|
||||
status=status_update.status,
|
||||
timestamp=status_update.timestamp
|
||||
)
|
||||
if not departure:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Departure record not found"
|
||||
)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "departure_status_update",
|
||||
"data": {
|
||||
"id": departure.id,
|
||||
"registration": departure.registration,
|
||||
"status": departure.status.value,
|
||||
"departure_dt": departure.departure_dt.isoformat() if departure.departure_dt else None
|
||||
}
|
||||
})
|
||||
|
||||
return departure
|
||||
|
||||
|
||||
@router.delete("/{departure_id}", response_model=Departure)
|
||||
async def cancel_departure(
|
||||
request: Request,
|
||||
departure_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Cancel a departure record"""
|
||||
departure = crud_departure.cancel(db, departure_id=departure_id)
|
||||
if not departure:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Departure record not found"
|
||||
)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "departure_cancelled",
|
||||
"data": {
|
||||
"id": departure.id,
|
||||
"registration": departure.registration
|
||||
}
|
||||
})
|
||||
|
||||
return departure
|
||||
@@ -4,8 +4,12 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_db
|
||||
from app.crud.crud_ppr import ppr as crud_ppr
|
||||
from app.crud.crud_local_flight import local_flight as crud_local_flight
|
||||
from app.crud.crud_departure import departure as crud_departure
|
||||
from app.crud.crud_arrival import arrival as crud_arrival
|
||||
from app.schemas.ppr import PPRPublic
|
||||
from app.models.local_flight import LocalFlightStatus
|
||||
from app.models.departure import DepartureStatus
|
||||
from app.models.arrival import ArrivalStatus
|
||||
from datetime import date
|
||||
|
||||
router = APIRouter()
|
||||
@@ -56,7 +60,7 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
|
||||
|
||||
@router.get("/departures")
|
||||
async def get_public_departures(db: Session = Depends(get_db)):
|
||||
"""Get today's departures for public display (PPR and local flights)"""
|
||||
"""Get today's departures for public display (PPR, local flights, and departures to other airports)"""
|
||||
departures = crud_ppr.get_departures_today(db)
|
||||
|
||||
# Convert PPR departures to dictionaries
|
||||
@@ -70,7 +74,8 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
||||
'etd': departure.etd,
|
||||
'departed_dt': departure.departed_dt,
|
||||
'status': departure.status.value,
|
||||
'isLocalFlight': False
|
||||
'isLocalFlight': False,
|
||||
'isDeparture': False
|
||||
})
|
||||
|
||||
# Add local flights with BOOKED_OUT status
|
||||
@@ -91,7 +96,29 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
||||
'departed_dt': None,
|
||||
'status': 'BOOKED_OUT',
|
||||
'isLocalFlight': True,
|
||||
'flight_type': flight.flight_type.value
|
||||
'flight_type': flight.flight_type.value,
|
||||
'isDeparture': False
|
||||
})
|
||||
|
||||
# Add departures to other airports with BOOKED_OUT status
|
||||
departures_to_airports = crud_departure.get_multi(
|
||||
db,
|
||||
status=DepartureStatus.BOOKED_OUT,
|
||||
limit=1000
|
||||
)
|
||||
|
||||
# Convert departures to match the format for display
|
||||
for dep in departures_to_airports:
|
||||
departures_list.append({
|
||||
'ac_call': dep.callsign or dep.registration,
|
||||
'ac_reg': dep.registration,
|
||||
'ac_type': dep.type,
|
||||
'out_to': dep.out_to,
|
||||
'etd': dep.booked_out_dt,
|
||||
'departed_dt': None,
|
||||
'status': 'BOOKED_OUT',
|
||||
'isLocalFlight': False,
|
||||
'isDeparture': True
|
||||
})
|
||||
|
||||
return departures_list
|
||||
104
backend/app/crud/crud_arrival.py
Normal file
104
backend/app/crud/crud_arrival.py
Normal file
@@ -0,0 +1,104 @@
|
||||
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.arrival import Arrival, ArrivalStatus
|
||||
from app.schemas.arrival import ArrivalCreate, ArrivalUpdate, ArrivalStatusUpdate
|
||||
|
||||
|
||||
class CRUDArrival:
|
||||
def get(self, db: Session, arrival_id: int) -> Optional[Arrival]:
|
||||
return db.query(Arrival).filter(Arrival.id == arrival_id).first()
|
||||
|
||||
def get_multi(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[ArrivalStatus] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None
|
||||
) -> List[Arrival]:
|
||||
query = db.query(Arrival)
|
||||
|
||||
if status:
|
||||
query = query.filter(Arrival.status == status)
|
||||
|
||||
if date_from:
|
||||
query = query.filter(func.date(Arrival.booked_in_dt) >= date_from)
|
||||
|
||||
if date_to:
|
||||
query = query.filter(func.date(Arrival.booked_in_dt) <= date_to)
|
||||
|
||||
return query.order_by(desc(Arrival.booked_in_dt)).offset(skip).limit(limit).all()
|
||||
|
||||
def get_arrivals_today(self, db: Session) -> List[Arrival]:
|
||||
"""Get today's arrivals (booked in or landed)"""
|
||||
today = date.today()
|
||||
return db.query(Arrival).filter(
|
||||
and_(
|
||||
func.date(Arrival.booked_in_dt) == today,
|
||||
or_(
|
||||
Arrival.status == ArrivalStatus.BOOKED_IN,
|
||||
Arrival.status == ArrivalStatus.LANDED
|
||||
)
|
||||
)
|
||||
).order_by(Arrival.booked_in_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival:
|
||||
db_obj = Arrival(
|
||||
**obj_in.dict(),
|
||||
created_by=created_by,
|
||||
status=ArrivalStatus.BOOKED_IN
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: Arrival, obj_in: ArrivalUpdate) -> Arrival:
|
||||
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,
|
||||
arrival_id: int,
|
||||
status: ArrivalStatus,
|
||||
timestamp: Optional[datetime] = None
|
||||
) -> Optional[Arrival]:
|
||||
db_obj = self.get(db, arrival_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = status
|
||||
|
||||
if status == ArrivalStatus.LANDED and timestamp:
|
||||
db_obj.landed_dt = timestamp
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def cancel(self, db: Session, arrival_id: int) -> Optional[Arrival]:
|
||||
db_obj = self.get(db, arrival_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = ArrivalStatus.CANCELLED
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
|
||||
arrival = CRUDArrival()
|
||||
104
backend/app/crud/crud_departure.py
Normal file
104
backend/app/crud/crud_departure.py
Normal file
@@ -0,0 +1,104 @@
|
||||
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.departure import Departure, DepartureStatus
|
||||
from app.schemas.departure import DepartureCreate, DepartureUpdate, DepartureStatusUpdate
|
||||
|
||||
|
||||
class CRUDDeparture:
|
||||
def get(self, db: Session, departure_id: int) -> Optional[Departure]:
|
||||
return db.query(Departure).filter(Departure.id == departure_id).first()
|
||||
|
||||
def get_multi(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[DepartureStatus] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None
|
||||
) -> List[Departure]:
|
||||
query = db.query(Departure)
|
||||
|
||||
if status:
|
||||
query = query.filter(Departure.status == status)
|
||||
|
||||
if date_from:
|
||||
query = query.filter(func.date(Departure.booked_out_dt) >= date_from)
|
||||
|
||||
if date_to:
|
||||
query = query.filter(func.date(Departure.booked_out_dt) <= date_to)
|
||||
|
||||
return query.order_by(desc(Departure.booked_out_dt)).offset(skip).limit(limit).all()
|
||||
|
||||
def get_departures_today(self, db: Session) -> List[Departure]:
|
||||
"""Get today's departures (booked out or departed)"""
|
||||
today = date.today()
|
||||
return db.query(Departure).filter(
|
||||
and_(
|
||||
func.date(Departure.booked_out_dt) == today,
|
||||
or_(
|
||||
Departure.status == DepartureStatus.BOOKED_OUT,
|
||||
Departure.status == DepartureStatus.DEPARTED
|
||||
)
|
||||
)
|
||||
).order_by(Departure.booked_out_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str) -> Departure:
|
||||
db_obj = Departure(
|
||||
**obj_in.dict(),
|
||||
created_by=created_by,
|
||||
status=DepartureStatus.BOOKED_OUT
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: Departure, obj_in: DepartureUpdate) -> Departure:
|
||||
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,
|
||||
departure_id: int,
|
||||
status: DepartureStatus,
|
||||
timestamp: Optional[datetime] = None
|
||||
) -> Optional[Departure]:
|
||||
db_obj = self.get(db, departure_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = status
|
||||
|
||||
if status == DepartureStatus.DEPARTED and timestamp:
|
||||
db_obj.departure_dt = timestamp
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def cancel(self, db: Session, departure_id: int) -> Optional[Departure]:
|
||||
db_obj = self.get(db, departure_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = DepartureStatus.CANCELLED
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
|
||||
departure = CRUDDeparture()
|
||||
@@ -11,6 +11,8 @@ 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
|
||||
from app.models.departure import Departure
|
||||
from app.models.arrival import Arrival
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
29
backend/app/models/arrival.py
Normal file
29
backend/app/models/arrival.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, DateTime, Enum as SQLEnum, func
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class Arrival(Base):
|
||||
__tablename__ = "arrivals"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
registration = Column(String(16), nullable=False, index=True)
|
||||
type = Column(String(32), nullable=True)
|
||||
callsign = Column(String(16), nullable=True)
|
||||
pob = Column(Integer, nullable=False)
|
||||
in_from = Column(String(4), nullable=False, index=True)
|
||||
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
booked_in_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
29
backend/app/models/departure.py
Normal file
29
backend/app/models/departure.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, DateTime, Enum as SQLEnum, func
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
DEPARTED = "DEPARTED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class Departure(Base):
|
||||
__tablename__ = "departures"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
registration = Column(String(16), nullable=False, index=True)
|
||||
type = Column(String(32), nullable=True)
|
||||
callsign = Column(String(16), nullable=True)
|
||||
pob = Column(Integer, nullable=False)
|
||||
out_to = Column(String(4), nullable=False, index=True)
|
||||
status = Column(SQLEnum(DepartureStatus), default=DepartureStatus.BOOKED_OUT, nullable=False, index=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
booked_out_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||
departure_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
66
backend/app/schemas/arrival.py
Normal file
66
backend/app/schemas/arrival.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from pydantic import BaseModel, validator
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class ArrivalBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
in_from: str
|
||||
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('in_from')
|
||||
def validate_in_from(cls, v):
|
||||
if not v or len(v.strip()) == 0:
|
||||
raise ValueError('Origin airport 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 ArrivalCreate(ArrivalBase):
|
||||
pass
|
||||
|
||||
|
||||
class ArrivalUpdate(BaseModel):
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: Optional[int] = None
|
||||
in_from: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class ArrivalStatusUpdate(BaseModel):
|
||||
status: ArrivalStatus
|
||||
timestamp: Optional[datetime] = None
|
||||
|
||||
|
||||
class Arrival(ArrivalBase):
|
||||
id: int
|
||||
status: ArrivalStatus
|
||||
booked_in_dt: datetime
|
||||
landed_dt: Optional[datetime] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
66
backend/app/schemas/departure.py
Normal file
66
backend/app/schemas/departure.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from pydantic import BaseModel, validator
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
DEPARTED = "DEPARTED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class DepartureBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
out_to: str
|
||||
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('out_to')
|
||||
def validate_out_to(cls, v):
|
||||
if not v or len(v.strip()) == 0:
|
||||
raise ValueError('Destination airport 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 DepartureCreate(DepartureBase):
|
||||
pass
|
||||
|
||||
|
||||
class DepartureUpdate(BaseModel):
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: Optional[int] = None
|
||||
out_to: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class DepartureStatusUpdate(BaseModel):
|
||||
status: DepartureStatus
|
||||
timestamp: Optional[datetime] = None
|
||||
|
||||
|
||||
class Departure(DepartureBase):
|
||||
id: int
|
||||
status: DepartureStatus
|
||||
booked_out_dt: datetime
|
||||
departure_dt: Optional[datetime] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -19,7 +19,7 @@ class LocalFlightStatus(str, Enum):
|
||||
|
||||
class LocalFlightBase(BaseModel):
|
||||
registration: str
|
||||
type: str # Aircraft type
|
||||
type: Optional[str] = None # Aircraft type - optional, can be looked up later
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
flight_type: LocalFlightType
|
||||
@@ -31,11 +31,13 @@ class LocalFlightBase(BaseModel):
|
||||
raise ValueError('Aircraft registration is required')
|
||||
return v.strip().upper()
|
||||
|
||||
@validator('type')
|
||||
@validator('type', pre=True, always=False)
|
||||
def validate_type(cls, v):
|
||||
if not v or len(v.strip()) == 0:
|
||||
raise ValueError('Aircraft type is required')
|
||||
return v.strip()
|
||||
if v is None or (isinstance(v, str) and len(v.strip()) == 0):
|
||||
return None
|
||||
if isinstance(v, str):
|
||||
return v.strip()
|
||||
return v
|
||||
|
||||
@validator('pob')
|
||||
def validate_pob(cls, v):
|
||||
|
||||
249
web/admin.html
249
web/admin.html
@@ -524,7 +524,7 @@
|
||||
}
|
||||
|
||||
/* Airport Lookup Styles */
|
||||
#arrival-airport-lookup-results, #departure-airport-lookup-results {
|
||||
#arrival-airport-lookup-results, #departure-airport-lookup-results, #local-out-to-lookup-results {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: #f8f9fa;
|
||||
@@ -986,12 +986,12 @@
|
||||
<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" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<label for="local_type">Aircraft Type</label>
|
||||
<input type="text" id="local_type" name="type" tabindex="4" placeholder="e.g., C172, PA34, AA5">
|
||||
</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" tabindex="4">
|
||||
<input type="text" id="local_callsign" name="callsign" placeholder="If different from registration" tabindex="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="local_pob">Persons on Board *</label>
|
||||
@@ -999,12 +999,17 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="local_flight_type">Flight Type *</label>
|
||||
<select id="local_flight_type" name="flight_type" required tabindex="3">
|
||||
<select id="local_flight_type" name="flight_type" required tabindex="5" onchange="handleFlightTypeChange(this.value)">
|
||||
<option value="LOCAL">Local Flight</option>
|
||||
<option value="CIRCUITS">Circuits</option>
|
||||
<option value="DEPARTURE">Departure</option>
|
||||
<option value="DEPARTURE">Departure to Other Airport</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="departure-destination-group" style="display: none;">
|
||||
<label for="local_out_to" id="departure-destination-label">Destination Airport</label>
|
||||
<input type="text" id="local_out_to" name="out_to" placeholder="ICAO Code or Airport Name" oninput="handleLocalOutToAirportLookup(this.value)" tabindex="3">
|
||||
<div id="local-out-to-lookup-results"></div>
|
||||
</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>
|
||||
@@ -1304,6 +1309,27 @@
|
||||
loadPPRs();
|
||||
showNotification('Data updated');
|
||||
}
|
||||
|
||||
// Refresh local flights when any local flight event occurs
|
||||
if (data.type && (data.type.includes('local_flight_'))) {
|
||||
console.log('Local flight update detected, refreshing...');
|
||||
loadLocalFlights();
|
||||
showNotification('Local flight updated');
|
||||
}
|
||||
|
||||
// Refresh departures when any departure event occurs
|
||||
if (data.type && (data.type.includes('departure_'))) {
|
||||
console.log('Departure update detected, refreshing...');
|
||||
loadDepartures();
|
||||
showNotification('Departure updated');
|
||||
}
|
||||
|
||||
// Refresh arrivals when any arrival event occurs
|
||||
if (data.type && (data.type.includes('arrival_'))) {
|
||||
console.log('Arrival update detected, refreshing...');
|
||||
loadArrivals();
|
||||
showNotification('Arrival updated');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
@@ -1521,8 +1547,8 @@
|
||||
openNewPPRModal();
|
||||
}
|
||||
|
||||
// Press 'b' to book out local flight (LOCAL type)
|
||||
if (e.key === 'b' || e.key === 'B') {
|
||||
// Press 'o' to book out local flight (LOCAL type)
|
||||
if (e.key === 'o' || e.key === 'O') {
|
||||
e.preventDefault();
|
||||
openLocalFlightModal('LOCAL');
|
||||
}
|
||||
@@ -1724,10 +1750,11 @@
|
||||
document.getElementById('departures-no-data').style.display = 'none';
|
||||
|
||||
try {
|
||||
// Load PPR departures and local flight departures (BOOKED_OUT only) simultaneously
|
||||
const [pprResponse, localResponse] = await Promise.all([
|
||||
// Load PPR departures, local flight departures, and airport departures simultaneously
|
||||
const [pprResponse, localResponse, depResponse] = await Promise.all([
|
||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000')
|
||||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000')
|
||||
]);
|
||||
|
||||
if (!pprResponse.ok) {
|
||||
@@ -1757,6 +1784,16 @@
|
||||
departures.push(...localDepartures);
|
||||
}
|
||||
|
||||
// Add departures to other airports (BOOKED_OUT status)
|
||||
if (depResponse.ok) {
|
||||
const depFlights = await depResponse.json();
|
||||
const depDepartures = depFlights.map(flight => ({
|
||||
...flight,
|
||||
isDeparture: true // Flag to distinguish from PPR
|
||||
}));
|
||||
departures.push(...depDepartures);
|
||||
}
|
||||
|
||||
displayDepartures(departures);
|
||||
} catch (error) {
|
||||
console.error('Error loading departures:', error);
|
||||
@@ -2223,11 +2260,14 @@
|
||||
for (const flight of departures) {
|
||||
const row = document.createElement('tr');
|
||||
const isLocal = flight.isLocalFlight;
|
||||
const isDeparture = flight.isDeparture;
|
||||
|
||||
// Click handler that routes to correct modal
|
||||
row.onclick = () => {
|
||||
if (isLocal) {
|
||||
openLocalFlightEditModal(flight.id);
|
||||
} else if (isDeparture) {
|
||||
// TODO: Open departure edit modal
|
||||
} else {
|
||||
openPPRModal(flight.id);
|
||||
}
|
||||
@@ -2277,6 +2317,37 @@
|
||||
} else {
|
||||
actionButtons = '<span style="color: #999;">-</span>';
|
||||
}
|
||||
} else if (isDeparture) {
|
||||
// Departure to other airport display
|
||||
if (flight.callsign && flight.callsign.trim()) {
|
||||
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||||
} else {
|
||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||
}
|
||||
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.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
|
||||
pob = flight.pob || '-';
|
||||
fuel = '-';
|
||||
landedDt = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
|
||||
|
||||
// Action buttons for departure
|
||||
if (flight.status === 'BOOKED_OUT') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
|
||||
TAKE OFF
|
||||
</button>
|
||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateDepartureStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel">
|
||||
CANCEL
|
||||
</button>
|
||||
`;
|
||||
} else if (flight.status === 'DEPARTED') {
|
||||
actionButtons = '<span style="color: #999;">Departed</span>';
|
||||
} else {
|
||||
actionButtons = '<span style="color: #999;">-</span>';
|
||||
}
|
||||
} else {
|
||||
// PPR display
|
||||
if (flight.ac_call && flight.ac_call.trim()) {
|
||||
@@ -2305,7 +2376,7 @@
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${aircraftDisplay}${notesIndicator}</td>
|
||||
<td>${isLocal ? flight.type : flight.ac_type}</td>
|
||||
<td>${isLocal ? flight.type : isDeparture ? flight.type : flight.ac_type}</td>
|
||||
<td>${toDisplay}</td>
|
||||
<td>${etd}</td>
|
||||
<td>${pob}</td>
|
||||
@@ -2575,11 +2646,11 @@
|
||||
}
|
||||
|
||||
// Timestamp modal functions
|
||||
function showTimestampModal(status, pprId = null, isLocalFlight = false) {
|
||||
function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false) {
|
||||
const targetId = pprId || (isLocalFlight ? currentLocalFlightId : currentPPRId);
|
||||
if (!targetId) return;
|
||||
|
||||
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight };
|
||||
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture };
|
||||
|
||||
const modalTitle = document.getElementById('timestamp-modal-title');
|
||||
const submitBtn = document.getElementById('timestamp-submit-btn');
|
||||
@@ -2627,9 +2698,16 @@
|
||||
try {
|
||||
// Determine the correct API endpoint based on flight type
|
||||
const isLocal = pendingStatusUpdate.isLocalFlight;
|
||||
const endpoint = isLocal ?
|
||||
`/api/v1/local-flights/${pendingStatusUpdate.pprId}/status` :
|
||||
`/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
|
||||
const isDeparture = pendingStatusUpdate.isDeparture;
|
||||
let endpoint;
|
||||
|
||||
if (isLocal) {
|
||||
endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`;
|
||||
} else if (isDeparture) {
|
||||
endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`;
|
||||
} else {
|
||||
endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'PATCH',
|
||||
@@ -3405,6 +3483,79 @@
|
||||
document.getElementById('departure-airport-lookup-results').innerHTML = '';
|
||||
}
|
||||
|
||||
function clearLocalOutToAirportLookup() {
|
||||
document.getElementById('local-out-to-lookup-results').innerHTML = '';
|
||||
}
|
||||
|
||||
// Airport lookup for Book Out modal departure field
|
||||
function handleLocalOutToAirportLookup(value) {
|
||||
if (!value.trim()) {
|
||||
clearLocalOutToAirportLookup();
|
||||
return;
|
||||
}
|
||||
performLocalOutToAirportLookup(value);
|
||||
}
|
||||
|
||||
async function performLocalOutToAirportLookup(codeOrName) {
|
||||
try {
|
||||
const cleanInput = codeOrName.trim();
|
||||
|
||||
if (cleanInput.length < 2) {
|
||||
clearLocalOutToAirportLookup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the airport lookup API using same endpoint as PPR modal
|
||||
const response = await authenticatedFetch(`/api/v1/airport/lookup/${encodeURIComponent(cleanInput)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch airport data');
|
||||
}
|
||||
|
||||
const matches = await response.json();
|
||||
displayLocalOutToAirportLookupResults(matches, cleanInput);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Local out-to airport lookup error:', error);
|
||||
document.getElementById('local-out-to-lookup-results').innerHTML =
|
||||
'<div class="airport-no-match">Lookup failed - will use as entered</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayLocalOutToAirportLookupResults(matches, searchTerm) {
|
||||
const resultsDiv = document.getElementById('local-out-to-lookup-results');
|
||||
|
||||
if (!matches || matches.length === 0) {
|
||||
resultsDiv.innerHTML = '<div class="airport-no-match">No matches found - will use as entered</div>';
|
||||
} else {
|
||||
// Show matches as clickable options (single or multiple)
|
||||
const matchText = matches.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:';
|
||||
const listHtml = matches.map(airport => `
|
||||
<div class="airport-option" onclick="selectLocalOutToAirport('${airport.icao}')">
|
||||
<div>
|
||||
<div class="airport-code">${airport.icao}</div>
|
||||
<div class="airport-name">${airport.name}</div>
|
||||
${airport.city ? `<div class="airport-location">${airport.city}, ${airport.country}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="airport-no-match" style="margin-bottom: 0.5rem;">
|
||||
${matchText}
|
||||
</div>
|
||||
<div class="airport-list">
|
||||
${listHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function selectLocalOutToAirport(icaoCode) {
|
||||
document.getElementById('local_out_to').value = icaoCode;
|
||||
clearLocalOutToAirportLookup();
|
||||
}
|
||||
|
||||
// Airport selection functions
|
||||
function selectArrivalAirport(icaoCode) {
|
||||
document.getElementById('in_from').value = icaoCode;
|
||||
@@ -3461,6 +3612,9 @@
|
||||
// Clear aircraft lookup results
|
||||
clearLocalAircraftLookup();
|
||||
|
||||
// Update destination field visibility based on flight type
|
||||
handleFlightTypeChange(flightType);
|
||||
|
||||
// Auto-focus on registration field
|
||||
setTimeout(() => {
|
||||
document.getElementById('local_registration').focus();
|
||||
@@ -3471,6 +3625,24 @@
|
||||
document.getElementById('localFlightModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Handle flight type change to show/hide destination field
|
||||
function handleFlightTypeChange(flightType) {
|
||||
const destGroup = document.getElementById('departure-destination-group');
|
||||
const destInput = document.getElementById('local_out_to');
|
||||
const destLabel = document.getElementById('departure-destination-label');
|
||||
|
||||
if (flightType === 'DEPARTURE') {
|
||||
destGroup.style.display = 'block';
|
||||
destInput.required = true;
|
||||
destLabel.textContent = 'Destination Airport *';
|
||||
} else {
|
||||
destGroup.style.display = 'none';
|
||||
destInput.required = false;
|
||||
destInput.value = '';
|
||||
destLabel.textContent = 'Destination Airport';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle aircraft lookup for local flights
|
||||
let localAircraftLookupTimeout;
|
||||
function handleLocalAircraftLookup(registration) {
|
||||
@@ -3632,6 +3804,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Update status from table for departures
|
||||
async function updateDepartureStatusFromTable(departureId, status) {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/departures/${departureId}/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(`Departure marked as ${status.toLowerCase()}`);
|
||||
} catch (error) {
|
||||
console.error('Error updating status:', error);
|
||||
showNotification('Error updating departure status', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Update status from modal (uses currentLocalFlightId)
|
||||
async function updateLocalFlightStatus(status) {
|
||||
if (!currentLocalFlightId || !accessToken) return;
|
||||
@@ -3719,7 +3915,9 @@
|
||||
if (!accessToken) return;
|
||||
|
||||
const formData = new FormData(this);
|
||||
const flightType = formData.get('flight_type');
|
||||
const flightData = {};
|
||||
let endpoint = '/api/v1/local-flights/';
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
// Skip the hidden id field and empty values
|
||||
@@ -3747,10 +3945,17 @@
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Submitting flight data:', flightData);
|
||||
// If DEPARTURE flight type, use departures endpoint instead
|
||||
if (flightType === 'DEPARTURE') {
|
||||
endpoint = '/api/v1/departures/';
|
||||
// Remove flight_type from data and use out_to instead
|
||||
delete flightData.flight_type;
|
||||
}
|
||||
|
||||
console.log(`Submitting ${endpoint} data:`, flightData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/local-flights/', {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -3763,7 +3968,11 @@
|
||||
let errorMessage = 'Failed to book out flight';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
if (errorData.detail) {
|
||||
errorMessage = typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail);
|
||||
} else if (errorData.errors) {
|
||||
errorMessage = errorData.errors.map(e => e.msg).join(', ');
|
||||
}
|
||||
} catch (e) {
|
||||
const text = await response.text();
|
||||
console.error('Server response:', text);
|
||||
|
||||
@@ -234,9 +234,9 @@
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket message received:', data);
|
||||
|
||||
// Refresh display when any PPR-related event occurs
|
||||
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) {
|
||||
console.log('PPR update detected, refreshing display...');
|
||||
// Refresh display when any PPR-related or local flight event occurs
|
||||
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_'))) {
|
||||
console.log('Flight update detected, refreshing display...');
|
||||
loadArrivals();
|
||||
loadDepartures();
|
||||
}
|
||||
@@ -375,6 +375,7 @@
|
||||
// Build rows asynchronously to lookup airport names
|
||||
const rows = await Promise.all(departures.map(async (departure) => {
|
||||
const isLocal = departure.isLocalFlight;
|
||||
const isDeparture = departure.isDeparture;
|
||||
|
||||
if (isLocal) {
|
||||
// Local flight
|
||||
@@ -384,6 +385,21 @@
|
||||
const time = convertToLocalTime(departure.etd);
|
||||
const timeDisplay = `<div>${escapeHtml(time)}</div>`;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${aircraftDisplay}</td>
|
||||
<td>${toDisplay}</td>
|
||||
<td>${timeDisplay}</td>
|
||||
</tr>
|
||||
`;
|
||||
} else if (isDeparture) {
|
||||
// Departure to other airport
|
||||
const aircraftId = departure.ac_call || departure.ac_reg || '';
|
||||
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`;
|
||||
const toDisplay = await getAirportName(departure.out_to || '');
|
||||
const time = convertToLocalTime(departure.etd);
|
||||
const timeDisplay = `<div>${escapeHtml(time)}</div>`;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${aircraftDisplay}</td>
|
||||
|
||||
Reference in New Issue
Block a user