Compare commits
11 Commits
stable
..
bb6597ff76
| Author | SHA1 | Date | |
|---|---|---|---|
| bb6597ff76 | |||
| 423023d3d9 | |||
| fd0e521186 | |||
| d2c9bc0370 | |||
| bddbe1451f | |||
| 785562407a | |||
| 5bb229ad78 | |||
| 8a2dd5544c | |||
| 3a4085afc6 | |||
| a43cf9b732 | |||
| 7f4e4a8459 |
@@ -1,3 +1,5 @@
|
||||
web/assets/booking-qr.png
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
+2
-1
@@ -3,11 +3,12 @@ FROM python:3.11-slim
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
# Install system dependencies including qrencode
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
qrencode \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Add public booking support with submitted_via and pilot_email columns
|
||||
|
||||
Revision ID: 003_public_booking
|
||||
Revises: 002_local_flights
|
||||
Create Date: 2026-02-20 12:00:00.000000
|
||||
|
||||
This migration adds support for public flight booking by adding:
|
||||
- submitted_via enum field to track ADMIN vs PUBLIC submissions
|
||||
- pilot_email field to store contact info for public submissions
|
||||
- Indexes on submitted_via for filtering queries
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '003_public_booking'
|
||||
down_revision = '002_local_flights'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""
|
||||
Add public booking support columns to local_flights, departures, and arrivals tables.
|
||||
"""
|
||||
|
||||
# Create the SubmissionSource enum type
|
||||
submission_source_enum = sa.Enum('ADMIN', 'PUBLIC', name='submissionsource')
|
||||
|
||||
# Add submitted_via and pilot_email to local_flights table
|
||||
op.add_column('local_flights', sa.Column('submitted_via', submission_source_enum, nullable=False, server_default='ADMIN'))
|
||||
op.add_column('local_flights', sa.Column('pilot_email', sa.String(length=128), nullable=True))
|
||||
|
||||
# Add indexes for submitted_via and pilot_email on local_flights
|
||||
op.create_index('idx_lf_submitted_via', 'local_flights', ['submitted_via'])
|
||||
op.create_index('idx_lf_pilot_email', 'local_flights', ['pilot_email'])
|
||||
|
||||
# Add submitted_via and pilot_email to departures table
|
||||
op.add_column('departures', sa.Column('submitted_via', submission_source_enum, nullable=False, server_default='ADMIN'))
|
||||
op.add_column('departures', sa.Column('pilot_email', sa.String(length=128), nullable=True))
|
||||
|
||||
# Add indexes for submitted_via and pilot_email on departures
|
||||
op.create_index('idx_dep_submitted_via', 'departures', ['submitted_via'])
|
||||
op.create_index('idx_dep_pilot_email', 'departures', ['pilot_email'])
|
||||
|
||||
# Add submitted_via and pilot_email to arrivals table
|
||||
op.add_column('arrivals', sa.Column('submitted_via', submission_source_enum, nullable=False, server_default='ADMIN'))
|
||||
op.add_column('arrivals', sa.Column('pilot_email', sa.String(length=128), nullable=True))
|
||||
|
||||
# Add indexes for submitted_via and pilot_email on arrivals
|
||||
op.create_index('idx_arr_submitted_via', 'arrivals', ['submitted_via'])
|
||||
op.create_index('idx_arr_pilot_email', 'arrivals', ['pilot_email'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Remove the submitted_via and pilot_email columns from local_flights, departures, and arrivals tables.
|
||||
"""
|
||||
|
||||
# Drop indexes first
|
||||
op.drop_index('idx_lf_submitted_via', table_name='local_flights')
|
||||
op.drop_index('idx_lf_pilot_email', table_name='local_flights')
|
||||
op.drop_index('idx_dep_submitted_via', table_name='departures')
|
||||
op.drop_index('idx_dep_pilot_email', table_name='departures')
|
||||
op.drop_index('idx_arr_submitted_via', table_name='arrivals')
|
||||
op.drop_index('idx_arr_pilot_email', table_name='arrivals')
|
||||
|
||||
# Drop columns from local_flights
|
||||
op.drop_column('local_flights', 'pilot_email')
|
||||
op.drop_column('local_flights', 'submitted_via')
|
||||
|
||||
# Drop columns from departures
|
||||
op.drop_column('departures', 'pilot_email')
|
||||
op.drop_column('departures', 'submitted_via')
|
||||
|
||||
# Drop columns from arrivals
|
||||
op.drop_column('arrivals', 'pilot_email')
|
||||
op.drop_column('arrivals', 'submitted_via')
|
||||
|
||||
# Drop the enum type
|
||||
op.execute('DROP TYPE IF EXISTS submissionsource')
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Add user_aircraft table for user-defined aircraft types
|
||||
|
||||
Revision ID: 004_user_aircraft
|
||||
Revises: 003_public_booking
|
||||
Create Date: 2026-03-23 12:00:00.000000
|
||||
|
||||
This migration adds a user_aircraft table to store aircraft types
|
||||
that are manually entered by users when not found in the main aircraft database.
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '004_user_aircraft'
|
||||
down_revision = '003_public_booking'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""
|
||||
Create user_aircraft table for storing user-defined aircraft types.
|
||||
"""
|
||||
op.create_table('user_aircraft',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('registration', sa.String(length=25), nullable=False),
|
||||
sa.Column('type_code', sa.String(length=30), nullable=False),
|
||||
sa.Column('clean_reg', sa.String(length=25), nullable=False),
|
||||
sa.Column('created_by', sa.String(length=16), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('registration')
|
||||
)
|
||||
|
||||
# Create indexes
|
||||
op.create_index('idx_user_aircraft_registration', 'user_aircraft', ['registration'])
|
||||
op.create_index('idx_user_aircraft_clean_reg', 'user_aircraft', ['clean_reg'])
|
||||
op.create_index('idx_user_aircraft_created_by', 'user_aircraft', ['created_by'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Drop user_aircraft table.
|
||||
"""
|
||||
op.drop_index('idx_user_aircraft_created_by', table_name='user_aircraft')
|
||||
op.drop_index('idx_user_aircraft_clean_reg', table_name='user_aircraft')
|
||||
op.drop_index('idx_user_aircraft_registration', table_name='user_aircraft')
|
||||
op.drop_table('user_aircraft')
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Add granular flight states and timestamps
|
||||
|
||||
Revision ID: 8adefaee847c
|
||||
Revises: 004_user_aircraft
|
||||
Create Date: 2026-03-24 09:09:00.944815
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '005_flight_states'
|
||||
down_revision = '004_user_aircraft'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add GROUND and LOCAL to local_flights status enum
|
||||
op.execute("ALTER TABLE local_flights MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CIRCUIT','LANDED','CANCELLED')")
|
||||
|
||||
# Add timestamp columns to local_flights
|
||||
op.add_column('local_flights', sa.Column('contact_dt', sa.DateTime(), nullable=True))
|
||||
op.add_column('local_flights', sa.Column('takeoff_dt', sa.DateTime(), nullable=True))
|
||||
|
||||
# Add GROUND and ARRIVED to arrivals status enum
|
||||
op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','GROUND','ARRIVED','CANCELLED')")
|
||||
|
||||
# Add timestamp column to arrivals
|
||||
op.add_column('arrivals', sa.Column('arrived_dt', sa.DateTime(), nullable=True))
|
||||
|
||||
# Add GROUND and LOCAL to departures status enum
|
||||
op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CANCELLED')")
|
||||
|
||||
# Add timestamp columns to departures
|
||||
op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True))
|
||||
op.add_column('departures', sa.Column('takeoff_dt', sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove timestamp columns from departures
|
||||
op.drop_column('departures', 'takeoff_dt')
|
||||
op.drop_column('departures', 'contact_dt')
|
||||
|
||||
# Remove GROUND and LOCAL from departures status enum
|
||||
op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','DEPARTED','CANCELLED')")
|
||||
|
||||
# Remove timestamp column from arrivals
|
||||
op.drop_column('arrivals', 'arrived_dt')
|
||||
|
||||
# Remove GROUND and ARRIVED from arrivals status enum
|
||||
op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','CANCELLED')")
|
||||
|
||||
# Remove timestamp columns from local_flights
|
||||
op.drop_column('local_flights', 'takeoff_dt')
|
||||
op.drop_column('local_flights', 'contact_dt')
|
||||
|
||||
# Remove GROUND and LOCAL from local_flights status enum
|
||||
op.execute("ALTER TABLE local_flights MODIFY COLUMN status ENUM('BOOKED_OUT','DEPARTED','LANDED','CANCELLED')")
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals, circuits, journal, overflights
|
||||
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals, circuits, journal, overflights, public_book
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -12,5 +12,6 @@ api_router.include_router(overflights.router, prefix="/overflights", tags=["over
|
||||
api_router.include_router(circuits.router, prefix="/circuits", tags=["circuits"])
|
||||
api_router.include_router(journal.router, prefix="/journal", tags=["journal"])
|
||||
api_router.include_router(public.router, prefix="/public", tags=["public"])
|
||||
api_router.include_router(public_book.router, prefix="/public-book", tags=["public_booking"])
|
||||
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
|
||||
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from app.api.deps import get_db, get_current_active_user
|
||||
from app.models.ppr import Aircraft
|
||||
from app.schemas.ppr import Aircraft as AircraftSchema
|
||||
from app.models.ppr import Aircraft, UserAircraft
|
||||
from app.schemas.ppr import Aircraft as AircraftSchema, UserAircraftCreate, UserAircraft as UserAircraftSchema
|
||||
from app.models.ppr import User
|
||||
|
||||
router = APIRouter()
|
||||
@@ -18,6 +19,7 @@ async def lookup_aircraft_by_registration(
|
||||
"""
|
||||
Lookup aircraft by registration (clean match).
|
||||
Removes non-alphanumeric characters from input for matching.
|
||||
Checks user_aircraft table first, then aircraft table.
|
||||
"""
|
||||
# Clean the input registration (remove non-alphanumeric characters)
|
||||
clean_input = ''.join(c for c in registration if c.isalnum()).upper()
|
||||
@@ -25,7 +27,29 @@ async def lookup_aircraft_by_registration(
|
||||
if len(clean_input) < 4:
|
||||
return []
|
||||
|
||||
# Query aircraft table using clean_reg column
|
||||
# First check user_aircraft table
|
||||
user_aircraft = db.query(UserAircraft).filter(
|
||||
UserAircraft.clean_reg.like(f"{clean_input}%")
|
||||
).limit(10).all()
|
||||
|
||||
if user_aircraft:
|
||||
# Convert UserAircraft to Aircraft-like objects
|
||||
result = []
|
||||
for ua in user_aircraft:
|
||||
# Create a mock Aircraft object with the user data
|
||||
result.append({
|
||||
'id': ua.id,
|
||||
'registration': ua.registration,
|
||||
'type_code': ua.type_code,
|
||||
'clean_reg': ua.clean_reg,
|
||||
'icao24': None,
|
||||
'manufacturer_icao': None,
|
||||
'manufacturer_name': None,
|
||||
'model': None
|
||||
})
|
||||
return result
|
||||
|
||||
# If no user aircraft found, check main aircraft table
|
||||
aircraft_list = db.query(Aircraft).filter(
|
||||
Aircraft.clean_reg.like(f"{clean_input}%")
|
||||
).limit(10).all()
|
||||
@@ -42,6 +66,7 @@ async def public_lookup_aircraft_by_registration(
|
||||
Public lookup aircraft by registration (clean match).
|
||||
Removes non-alphanumeric characters from input for matching.
|
||||
No authentication required.
|
||||
Checks user_aircraft table first, then aircraft table.
|
||||
"""
|
||||
# Clean the input registration (remove non-alphanumeric characters)
|
||||
clean_input = ''.join(c for c in registration if c.isalnum()).upper()
|
||||
@@ -49,7 +74,28 @@ async def public_lookup_aircraft_by_registration(
|
||||
if len(clean_input) < 4:
|
||||
return []
|
||||
|
||||
# Query aircraft table using clean_reg column
|
||||
# First check user_aircraft table
|
||||
user_aircraft = db.query(UserAircraft).filter(
|
||||
UserAircraft.clean_reg.like(f"{clean_input}%")
|
||||
).limit(10).all()
|
||||
|
||||
if user_aircraft:
|
||||
# Convert UserAircraft to Aircraft-like objects
|
||||
result = []
|
||||
for ua in user_aircraft:
|
||||
result.append({
|
||||
'id': ua.id,
|
||||
'registration': ua.registration,
|
||||
'type_code': ua.type_code,
|
||||
'clean_reg': ua.clean_reg,
|
||||
'icao24': None,
|
||||
'manufacturer_icao': None,
|
||||
'manufacturer_name': None,
|
||||
'model': None
|
||||
})
|
||||
return result
|
||||
|
||||
# If no user aircraft found, check main aircraft table
|
||||
aircraft_list = db.query(Aircraft).filter(
|
||||
Aircraft.clean_reg.like(f"{clean_input}%")
|
||||
).limit(10).all()
|
||||
@@ -81,4 +127,101 @@ async def search_aircraft(
|
||||
(Aircraft.model.like(f"%{q}%"))
|
||||
).limit(limit).all()
|
||||
|
||||
return aircraft_list
|
||||
return aircraft_list
|
||||
|
||||
|
||||
@router.post("/user-aircraft", response_model=dict)
|
||||
async def save_user_aircraft(
|
||||
aircraft: UserAircraftCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Save a user-defined aircraft type for a registration.
|
||||
"""
|
||||
# Clean the registration
|
||||
clean_reg = ''.join(c for c in aircraft.registration if c.isalnum()).upper()
|
||||
|
||||
# Check if already exists
|
||||
existing = db.query(UserAircraft).filter(
|
||||
UserAircraft.registration == aircraft.registration.upper()
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Aircraft registration already exists in user database")
|
||||
|
||||
# Create new user aircraft
|
||||
user_aircraft = UserAircraft(
|
||||
registration=aircraft.registration.upper(),
|
||||
type_code=aircraft.type_code.upper(),
|
||||
clean_reg=clean_reg,
|
||||
created_by=current_user.username
|
||||
)
|
||||
|
||||
db.add(user_aircraft)
|
||||
db.commit()
|
||||
db.refresh(user_aircraft)
|
||||
|
||||
return {"message": "Aircraft saved successfully", "id": user_aircraft.id}
|
||||
|
||||
|
||||
@router.get("/user-aircraft", response_model=List[UserAircraftSchema])
|
||||
async def get_user_aircraft(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Get all user-defined aircraft types.
|
||||
"""
|
||||
user_aircraft = db.query(UserAircraft).order_by(UserAircraft.created_at.desc()).all()
|
||||
return user_aircraft
|
||||
|
||||
|
||||
@router.put("/user-aircraft/{registration}", response_model=dict)
|
||||
async def update_user_aircraft(
|
||||
registration: str,
|
||||
aircraft: UserAircraftCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Update a user-defined aircraft type.
|
||||
"""
|
||||
# Find the existing user aircraft
|
||||
existing = db.query(UserAircraft).filter(
|
||||
UserAircraft.registration == registration.upper()
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="User aircraft not found")
|
||||
|
||||
# Update the type
|
||||
existing.type_code = aircraft.type_code.upper()
|
||||
existing.updated_at = func.current_timestamp()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"message": "Aircraft updated successfully"}
|
||||
|
||||
|
||||
@router.delete("/user-aircraft/{registration}", response_model=dict)
|
||||
async def delete_user_aircraft(
|
||||
registration: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Delete a user-defined aircraft type.
|
||||
"""
|
||||
# Find the existing user aircraft
|
||||
existing = db.query(UserAircraft).filter(
|
||||
UserAircraft.registration == registration.upper()
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="User aircraft not found")
|
||||
|
||||
db.delete(existing)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Aircraft deleted successfully"}
|
||||
@@ -38,7 +38,7 @@ async def create_departure(
|
||||
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)
|
||||
departure = crud_departure.create(db, obj_in=departure_in, created_by=current_user.username, submitted_via="ADMIN")
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
|
||||
@@ -173,10 +173,10 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
||||
'isDeparture': False
|
||||
})
|
||||
|
||||
# Add departures to other airports with BOOKED_OUT status
|
||||
# Add departures to other airports with BOOKED_OUT and GROUND status
|
||||
departures_to_airports = crud_departure.get_multi(
|
||||
db,
|
||||
status=DepartureStatus.BOOKED_OUT,
|
||||
status=None, # Get all statuses
|
||||
limit=1000
|
||||
)
|
||||
|
||||
@@ -187,17 +187,25 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
||||
|
||||
# Convert departures to match the format for display
|
||||
for dep in departures_to_airports:
|
||||
# Only include departures booked out today
|
||||
if not (today_start <= dep.created_dt < today_end):
|
||||
# Only include departures booked out today and not yet departed
|
||||
if not (today_start <= dep.created_dt < today_end) or dep.status == DepartureStatus.DEPARTED:
|
||||
continue
|
||||
|
||||
# Map status for display
|
||||
display_status = 'BOOKED_OUT'
|
||||
if dep.status == DepartureStatus.GROUND:
|
||||
display_status = 'CONTACT'
|
||||
elif dep.status == DepartureStatus.LOCAL:
|
||||
display_status = 'DEPARTED'
|
||||
|
||||
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.etd or dep.created_dt,
|
||||
'departed_dt': None,
|
||||
'status': 'BOOKED_OUT',
|
||||
'departed_dt': dep.departed_dt,
|
||||
'status': display_status,
|
||||
'isLocalFlight': False,
|
||||
'isDeparture': True
|
||||
})
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_db
|
||||
from app.core.config import settings
|
||||
from app.schemas.public_book import (
|
||||
PublicLocalFlightCreate,
|
||||
PublicCircuitCreate,
|
||||
PublicDepartureCreate,
|
||||
PublicArrivalCreate,
|
||||
)
|
||||
from app.schemas.local_flight import LocalFlight as LocalFlightSchema
|
||||
from app.schemas.circuit import Circuit as CircuitSchema
|
||||
from app.schemas.departure import Departure as DepartureSchema
|
||||
from app.schemas.arrival import Arrival as ArrivalSchema
|
||||
from app.crud.crud_local_flight import local_flight as crud_local_flight
|
||||
from app.crud.crud_circuit import crud_circuit
|
||||
from app.crud.crud_departure import departure as crud_departure
|
||||
from app.crud.crud_arrival import arrival as crud_arrival
|
||||
from app.models.local_flight import SubmissionSource
|
||||
from app.models.departure import DepartureStatus
|
||||
from app.models.arrival import SubmissionSource as ArrivalSubmissionSource
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def check_public_booking_enabled():
|
||||
"""Check if public booking is enabled"""
|
||||
if not settings.allow_public_booking:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Public booking is currently disabled"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/local-flights", response_model=LocalFlightSchema)
|
||||
async def public_book_local_flight(
|
||||
request: Request,
|
||||
flight_in: PublicLocalFlightCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Book a local flight via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
# Create the flight with public submission source
|
||||
from app.schemas.local_flight import LocalFlightCreate
|
||||
|
||||
flight_create = LocalFlightCreate(
|
||||
registration=flight_in.registration,
|
||||
type=flight_in.type,
|
||||
callsign=flight_in.callsign,
|
||||
pob=flight_in.pob,
|
||||
flight_type=flight_in.flight_type,
|
||||
duration=flight_in.duration,
|
||||
etd=flight_in.etd,
|
||||
notes=flight_in.notes,
|
||||
)
|
||||
|
||||
flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT")
|
||||
|
||||
# Update with submission source and pilot email
|
||||
db.query(type(flight)).filter(type(flight).id == flight.id).update({
|
||||
type(flight).submitted_via: SubmissionSource.PUBLIC,
|
||||
type(flight).pilot_email: flight_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
db.refresh(flight)
|
||||
|
||||
# Send real-time update via WebSocket if available
|
||||
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,
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return flight
|
||||
|
||||
|
||||
@router.post("/circuits", response_model=CircuitSchema)
|
||||
async def public_record_circuit(
|
||||
request: Request,
|
||||
circuit_in: PublicCircuitCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Record a circuit (touch and go) via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
from app.schemas.circuit import CircuitCreate
|
||||
|
||||
circuit_create = CircuitCreate(
|
||||
local_flight_id=circuit_in.local_flight_id,
|
||||
circuit_timestamp=circuit_in.circuit_timestamp,
|
||||
)
|
||||
|
||||
circuit = crud_circuit.create(db, obj_in=circuit_create)
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "circuit_recorded",
|
||||
"data": {
|
||||
"id": circuit.id,
|
||||
"local_flight_id": circuit.local_flight_id,
|
||||
"circuit_timestamp": circuit.circuit_timestamp.isoformat(),
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return circuit
|
||||
|
||||
|
||||
@router.post("/departures", response_model=DepartureSchema)
|
||||
async def public_book_departure(
|
||||
request: Request,
|
||||
departure_in: PublicDepartureCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Book a departure via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
from app.schemas.departure import DepartureCreate
|
||||
|
||||
departure_create = DepartureCreate(
|
||||
registration=departure_in.registration,
|
||||
type=departure_in.type,
|
||||
callsign=departure_in.callsign,
|
||||
pob=departure_in.pob,
|
||||
out_to=departure_in.out_to,
|
||||
etd=departure_in.etd,
|
||||
notes=departure_in.notes,
|
||||
)
|
||||
|
||||
departure = crud_departure.create(db, obj_in=departure_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||
|
||||
# Update with pilot email (submitted_via is already set in create method)
|
||||
db.query(type(departure)).filter(type(departure).id == departure.id).update({
|
||||
type(departure).pilot_email: departure_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
db.refresh(departure)
|
||||
|
||||
# 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,
|
||||
"status": departure.status.value,
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return departure
|
||||
|
||||
|
||||
@router.post("/arrivals", response_model=ArrivalSchema)
|
||||
async def public_book_arrival(
|
||||
request: Request,
|
||||
arrival_in: PublicArrivalCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Book an arrival via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
from app.schemas.arrival import ArrivalCreate
|
||||
|
||||
arrival_create = ArrivalCreate(
|
||||
registration=arrival_in.registration,
|
||||
type=arrival_in.type,
|
||||
callsign=arrival_in.callsign,
|
||||
pob=arrival_in.pob,
|
||||
in_from=arrival_in.in_from,
|
||||
eta=arrival_in.eta,
|
||||
notes=arrival_in.notes,
|
||||
)
|
||||
|
||||
arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT")
|
||||
|
||||
# Update with submission source and pilot email
|
||||
db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({
|
||||
type(arrival).submitted_via: ArrivalSubmissionSource.PUBLIC,
|
||||
type(arrival).pilot_email: arrival_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
db.refresh(arrival)
|
||||
|
||||
# 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,
|
||||
"status": arrival.status.value,
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return arrival
|
||||
@@ -33,6 +33,9 @@ class Settings(BaseSettings):
|
||||
top_bar_base_color: str = "#2c3e50"
|
||||
environment: str = "production" # production, development, staging, etc.
|
||||
|
||||
# Public booking settings
|
||||
allow_public_booking: bool = False # Enable/disable public flight booking
|
||||
|
||||
# Redis settings (for future use)
|
||||
redis_url: Optional[str] = None
|
||||
|
||||
|
||||
@@ -113,8 +113,12 @@ class CRUDArrival:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = status
|
||||
|
||||
if status == ArrivalStatus.LANDED and timestamp:
|
||||
db_obj.landed_dt = timestamp
|
||||
# Set timestamps based on status
|
||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||
if status == ArrivalStatus.LANDED:
|
||||
db_obj.landed_dt = current_time
|
||||
elif status == ArrivalStatus.ARRIVED:
|
||||
db_obj.arrived_dt = current_time
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
|
||||
@@ -47,11 +47,23 @@ class CRUDDeparture:
|
||||
)
|
||||
).order_by(Departure.created_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str) -> Departure:
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str, submitted_via: str = "ADMIN") -> Departure:
|
||||
from app.models.departure import SubmissionSource
|
||||
|
||||
# Set initial status based on submission source
|
||||
initial_status = DepartureStatus.BOOKED_OUT
|
||||
contact_dt = None
|
||||
|
||||
if submitted_via == SubmissionSource.ADMIN:
|
||||
initial_status = DepartureStatus.GROUND
|
||||
contact_dt = func.now() # Set contact_dt to creation time for admin submissions
|
||||
|
||||
db_obj = Departure(
|
||||
**obj_in.dict(),
|
||||
created_by=created_by,
|
||||
status=DepartureStatus.BOOKED_OUT
|
||||
status=initial_status,
|
||||
contact_dt=contact_dt,
|
||||
submitted_via=submitted_via
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
@@ -113,8 +125,14 @@ class CRUDDeparture:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = status
|
||||
|
||||
if status == DepartureStatus.DEPARTED and timestamp:
|
||||
db_obj.departed_dt = timestamp
|
||||
# Set timestamps based on status
|
||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||
if status == DepartureStatus.GROUND:
|
||||
db_obj.contact_dt = current_time
|
||||
elif status == DepartureStatus.DEPARTED:
|
||||
db_obj.departed_dt = current_time
|
||||
elif status == DepartureStatus.LOCAL:
|
||||
db_obj.takeoff_dt = current_time
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
|
||||
@@ -144,10 +144,20 @@ class CRUDLocalFlight:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = status
|
||||
|
||||
# Update flight_type based on status changes
|
||||
if status == LocalFlightStatus.LOCAL:
|
||||
db_obj.flight_type = LocalFlightType.LOCAL
|
||||
elif status == LocalFlightStatus.CIRCUIT:
|
||||
db_obj.flight_type = LocalFlightType.CIRCUITS
|
||||
|
||||
# Set timestamps based on status
|
||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||
if status == LocalFlightStatus.DEPARTED:
|
||||
if status == LocalFlightStatus.GROUND:
|
||||
db_obj.contact_dt = current_time
|
||||
elif status == LocalFlightStatus.DEPARTED:
|
||||
db_obj.departed_dt = current_time
|
||||
elif status == LocalFlightStatus.LOCAL:
|
||||
db_obj.takeoff_dt = current_time
|
||||
elif status == LocalFlightStatus.LANDED:
|
||||
db_obj.landed_dt = current_time
|
||||
# Count circuits from the circuits table and populate the circuits column
|
||||
|
||||
@@ -6,9 +6,16 @@ from datetime import datetime
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
GROUND = "GROUND"
|
||||
ARRIVED = "ARRIVED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@@ -26,5 +33,8 @@ class Arrival(Base):
|
||||
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||
eta = Column(DateTime, nullable=True, index=True)
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
arrived_dt = Column(DateTime, nullable=True) # Time when aircraft parks and shuts down
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
@@ -6,9 +6,16 @@ from datetime import datetime
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@@ -25,6 +32,10 @@ class Departure(Base):
|
||||
notes = Column(Text, nullable=True)
|
||||
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual departure time
|
||||
contact_dt = Column(DateTime, nullable=True) # Time when contact is established with pilot
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual departure time (QSY)
|
||||
takeoff_dt = Column(DateTime, nullable=True) # Time when aircraft becomes airborne
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
@@ -4,6 +4,11 @@ from enum import Enum
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class LocalFlightType(str, Enum):
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUITS = "CIRCUITS"
|
||||
@@ -12,7 +17,10 @@ class LocalFlightType(str, Enum):
|
||||
|
||||
class LocalFlightStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUIT = "CIRCUIT"
|
||||
LANDED = "LANDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
@@ -32,7 +40,11 @@ class LocalFlight(Base):
|
||||
notes = Column(Text, nullable=True)
|
||||
created_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
|
||||
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
|
||||
contact_dt = Column(DateTime, nullable=True) # Time when contact is established with pilot
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual takeoff time
|
||||
takeoff_dt = Column(DateTime, nullable=True) # Time when aircraft becomes airborne
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
|
||||
@@ -88,4 +88,16 @@ class Aircraft(Base):
|
||||
model = Column(String(255), nullable=True)
|
||||
clean_reg = Column(String(25), nullable=True, index=True)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
|
||||
|
||||
class UserAircraft(Base):
|
||||
__tablename__ = "user_aircraft"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
registration = Column(String(25), nullable=False, unique=True, index=True)
|
||||
type_code = Column(String(30), nullable=False)
|
||||
clean_reg = Column(String(25), nullable=False, index=True)
|
||||
created_by = Column(String(16), nullable=False, index=True)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
@@ -7,9 +7,16 @@ from enum import Enum
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
GROUND = "GROUND"
|
||||
ARRIVED = "ARRIVED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class ArrivalBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
@@ -47,6 +54,10 @@ class ArrivalUpdate(BaseModel):
|
||||
callsign: Optional[str] = None
|
||||
pob: Optional[int] = None
|
||||
in_from: Optional[str] = None
|
||||
status: Optional[ArrivalStatus] = None
|
||||
eta: Optional[datetime] = None
|
||||
landed_dt: Optional[datetime] = None
|
||||
arrived_dt: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@@ -61,8 +72,11 @@ class Arrival(ArrivalBase):
|
||||
created_dt: datetime
|
||||
eta: Optional[datetime] = None
|
||||
landed_dt: Optional[datetime] = None
|
||||
arrived_dt: Optional[datetime] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -6,10 +6,17 @@ from enum import Enum
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class DepartureBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
@@ -48,7 +55,11 @@ class DepartureUpdate(BaseModel):
|
||||
callsign: Optional[str] = None
|
||||
pob: Optional[int] = None
|
||||
out_to: Optional[str] = None
|
||||
status: Optional[DepartureStatus] = None
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@@ -62,4 +73,12 @@ class Departure(DepartureBase):
|
||||
status: DepartureStatus
|
||||
created_dt: datetime
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -12,11 +12,19 @@ class LocalFlightType(str, Enum):
|
||||
|
||||
class LocalFlightStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUIT = "CIRCUIT"
|
||||
LANDED = "LANDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class LocalFlightBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None # Aircraft type - optional, can be looked up later
|
||||
@@ -61,7 +69,9 @@ class LocalFlightUpdate(BaseModel):
|
||||
duration: Optional[int] = None
|
||||
status: Optional[LocalFlightStatus] = None
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
circuits: Optional[int] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
@@ -76,11 +86,15 @@ class LocalFlightInDBBase(LocalFlightBase):
|
||||
status: LocalFlightStatus
|
||||
created_dt: datetime
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
landed_dt: Optional[datetime] = None
|
||||
circuits: Optional[int] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -214,4 +214,24 @@ class Aircraft(AircraftBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# User Aircraft schemas
|
||||
class UserAircraftBase(BaseModel):
|
||||
registration: str
|
||||
type_code: str
|
||||
clean_reg: str
|
||||
created_by: str
|
||||
|
||||
|
||||
class UserAircraft(UserAircraftBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserAircraftCreate(BaseModel):
|
||||
registration: str
|
||||
type_code: str
|
||||
@@ -0,0 +1,129 @@
|
||||
from pydantic import BaseModel, validator, EmailStr
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class LocalFlightType(str, Enum):
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUITS = "CIRCUITS"
|
||||
DEPARTURE = "DEPARTURE"
|
||||
|
||||
|
||||
class PublicLocalFlightCreate(BaseModel):
|
||||
"""Schema for public local flight booking"""
|
||||
registration: str
|
||||
type: Optional[str] = None # Aircraft type - optional
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
flight_type: LocalFlightType
|
||||
duration: Optional[int] = 45 # Duration in minutes, default 45
|
||||
etd: Optional[datetime] = None # Estimated Time of Departure
|
||||
notes: Optional[str] = None
|
||||
pilot_email: Optional[str] = None # Pilot's email for contact (optional)
|
||||
pilot_name: Optional[str] = None # Pilot's name
|
||||
|
||||
@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('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
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class PublicCircuitCreate(BaseModel):
|
||||
"""Schema for public circuit (touch and go) recording"""
|
||||
local_flight_id: int
|
||||
circuit_timestamp: datetime
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class PublicDepartureCreate(BaseModel):
|
||||
"""Schema for public departure booking"""
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
out_to: str
|
||||
etd: Optional[datetime] = None # Estimated Time of Departure
|
||||
notes: Optional[str] = None
|
||||
pilot_email: Optional[str] = None
|
||||
pilot_name: 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
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class PublicArrivalCreate(BaseModel):
|
||||
"""Schema for public arrival booking"""
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
in_from: str
|
||||
eta: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
pilot_email: Optional[str] = None
|
||||
pilot_name: 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
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
@@ -174,6 +174,12 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Generating QR Code"
|
||||
echo "========================================="
|
||||
python3 /app/generate_qr.py
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Starting Application Server"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate booking QR code at container startup"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
def generate_booking_qr():
|
||||
"""Generate QR code for the booking page"""
|
||||
# Get base URL from environment, default to localhost
|
||||
base_url = os.environ.get('BASE_URL', 'http://localhost')
|
||||
booking_url = f"{base_url}/book"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_dir = '/web/assets'
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
output_file = f'{output_dir}/booking-qr.png'
|
||||
|
||||
try:
|
||||
# Generate QR code using qrencode
|
||||
subprocess.run(
|
||||
['qrencode', '-o', output_file, '-s', '5', booking_url],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
print(f"✓ Generated booking QR code: {output_file}")
|
||||
print(f" URL: {booking_url}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Failed to generate QR code: {e.stderr.decode()}", file=sys.stderr)
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("✗ qrencode not found. Install with: apt-get install qrencode", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = generate_booking_qr()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -28,6 +28,7 @@ services:
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
TAG: ${TAG}
|
||||
TOP_BAR_BASE_COLOR: ${TOP_BAR_BASE_COLOR}
|
||||
ALLOW_PUBLIC_BOOKING: ${ALLOW_PUBLIC_BOOKING}
|
||||
ENVIRONMENT: production
|
||||
WORKERS: "4"
|
||||
ports:
|
||||
@@ -35,6 +36,7 @@ services:
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./db-init:/db-init:ro # Mount CSV data for seeding
|
||||
- ./web/assets:/web/assets # Mount assets for QR code generation
|
||||
networks:
|
||||
- app_network
|
||||
extra_hosts:
|
||||
@@ -48,7 +50,7 @@ services:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -48,6 +48,7 @@ services:
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./db-init:/db-init:ro # Mount CSV data for seeding
|
||||
- ./web/assets:/web/assets # Mount assets for QR code generation
|
||||
networks:
|
||||
- private_network
|
||||
- public_network
|
||||
|
||||
+411
-79
@@ -29,7 +29,9 @@
|
||||
⚙️ Admin
|
||||
</button>
|
||||
<div class="dropdown-menu" id="adminDropdownMenu">
|
||||
<a href="#" onclick="window.location.href = '/atc'">🎛️ ATC View</a>
|
||||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
|
||||
<a href="#" onclick="openUserAircraftModal(); closeAdminDropdown()" id="user-aircraft-dropdown">✈️ User Aircraft</a>
|
||||
<a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -562,7 +564,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Circuits Section (for CIRCUITS flights only) -->
|
||||
<!-- Touch & Go Records Section (for all local flight types) -->
|
||||
<div id="circuits-section" style="display: none; margin-top: 2rem; border-top: 1px solid #ddd; padding-top: 1rem;">
|
||||
<h3>✈️ Touch & Go Records</h3>
|
||||
<div id="circuits-list" style="margin-top: 1rem;">
|
||||
@@ -1048,6 +1050,86 @@
|
||||
<!-- Success Notification -->
|
||||
<div id="notification" class="notification"></div>
|
||||
|
||||
<!-- User Aircraft Management Modal -->
|
||||
<div id="userAircraftModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 1000px;">
|
||||
<div class="modal-header">
|
||||
<h2>User Aircraft Management</h2>
|
||||
<button class="close" onclick="closeModal('userAircraftModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="quick-actions" style="margin-bottom: 1rem;">
|
||||
<div class="form-group" style="display: inline-block; margin-right: 1rem;">
|
||||
<label for="user-aircraft-search" style="display: inline; margin-right: 0.5rem;">Search:</label>
|
||||
<input type="text" id="user-aircraft-search" placeholder="Filter by registration or type..." style="width: 250px;">
|
||||
</div>
|
||||
<button class="btn btn-info" onclick="loadUserAircraft()">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="user-aircraft-loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
Loading user aircraft...
|
||||
</div>
|
||||
|
||||
<div id="user-aircraft-table-content" style="display: none;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Registration</th>
|
||||
<th>Type</th>
|
||||
<th>Added By</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="user-aircraft-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="user-aircraft-no-data" class="no-data" style="display: none;">
|
||||
<h3>No user aircraft found</h3>
|
||||
<p>No custom aircraft types have been saved yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Aircraft Edit Modal -->
|
||||
<div id="userAircraftEditModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="user-aircraft-edit-title">Edit User Aircraft</h2>
|
||||
<button class="close" onclick="closeModal('userAircraftEditModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="user-aircraft-edit-form">
|
||||
<div class="form-group full-width">
|
||||
<label for="edit-aircraft-registration">Registration *</label>
|
||||
<input type="text" id="edit-aircraft-registration" name="registration" required readonly style="background-color: #f5f5f5;">
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label for="edit-aircraft-type">Aircraft Type *</label>
|
||||
<input type="text" id="edit-aircraft-type" name="type_code" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-info" onclick="closeModal('userAircraftEditModal')">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
💾 Save Changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="deleteUserAircraft()">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timestamp Modal for Landing/Departure -->
|
||||
<div id="timestampModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
@@ -1494,6 +1576,12 @@
|
||||
openNewPPRModal();
|
||||
}
|
||||
|
||||
// Press 'g' to book out local flight starting with G
|
||||
if (e.key === 'g' || e.key === 'G') {
|
||||
e.preventDefault();
|
||||
openLocalFlightModal('LOCAL', 'G');
|
||||
}
|
||||
|
||||
// Press 'l' to book out local flight (LOCAL type)
|
||||
if (e.key === 'l' || e.key === 'L') {
|
||||
e.preventDefault();
|
||||
@@ -1780,10 +1868,15 @@
|
||||
|
||||
try {
|
||||
// Load PPR departures, local flight departures, and airport departures simultaneously
|
||||
const [pprResponse, localResponse, depResponse] = await Promise.all([
|
||||
const [pprResponse, localBookedOutResponse, localOutGroundResponse, localLocalResponse, localCircuitResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
|
||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000')
|
||||
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=CIRCUIT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
||||
]);
|
||||
|
||||
if (!pprResponse.ok) {
|
||||
@@ -1791,6 +1884,18 @@
|
||||
}
|
||||
|
||||
const allPPRs = await pprResponse.json();
|
||||
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
||||
const localOutGround = localOutGroundResponse.ok ? await localOutGroundResponse.json() : [];
|
||||
const localLocal = localLocalResponse.ok ? await localLocalResponse.json() : [];
|
||||
const localCircuit = localCircuitResponse.ok ? await localCircuitResponse.json() : [];
|
||||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||||
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
||||
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
|
||||
|
||||
// Combine local flights
|
||||
const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal, ...localCircuit];
|
||||
// Combine departures
|
||||
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Filter for PPR departures with ETD today and LANDED status only
|
||||
@@ -1803,33 +1908,26 @@
|
||||
return etdDate === today;
|
||||
});
|
||||
|
||||
// Add local flights (BOOKED_OUT status - ready to go) - only those booked out today
|
||||
if (localResponse.ok) {
|
||||
const localFlights = await localResponse.json();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const localDepartures = localFlights
|
||||
.filter(flight => {
|
||||
// Only include flights booked out today (created_dt)
|
||||
if (!flight.created_dt) return false;
|
||||
const createdDate = flight.created_dt.split('T')[0];
|
||||
return createdDate === today;
|
||||
})
|
||||
.map(flight => ({
|
||||
...flight,
|
||||
isLocalFlight: true // Flag to distinguish from PPR
|
||||
}));
|
||||
departures.push(...localDepartures);
|
||||
}
|
||||
|
||||
// Add departures to other airports (BOOKED_OUT status)
|
||||
if (depResponse.ok) {
|
||||
const depFlights = await depResponse.json();
|
||||
const depDepartures = depFlights.map(flight => ({
|
||||
// Add local flights (BOOKED_OUT, GROUND, and LOCAL status - ready to go) - only those booked out today
|
||||
const localDepartures = allLocalFlights
|
||||
.filter(flight => {
|
||||
// Only include flights booked out today (created_dt)
|
||||
if (!flight.created_dt) return false;
|
||||
const createdDate = flight.created_dt.split('T')[0];
|
||||
return createdDate === today;
|
||||
})
|
||||
.map(flight => ({
|
||||
...flight,
|
||||
isDeparture: true // Flag to distinguish from PPR
|
||||
isLocalFlight: true // Flag to distinguish from PPR
|
||||
}));
|
||||
departures.push(...depDepartures);
|
||||
}
|
||||
departures.push(...localDepartures);
|
||||
|
||||
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
|
||||
const depDepartures = allDepartures.map(flight => ({
|
||||
...flight,
|
||||
isDeparture: true // Flag to distinguish from PPR
|
||||
}));
|
||||
departures.push(...depDepartures);
|
||||
|
||||
displayDepartures(departures);
|
||||
} catch (error) {
|
||||
@@ -2019,7 +2117,7 @@
|
||||
if (isLocal) {
|
||||
row.innerHTML = `
|
||||
<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; text-align: center; width: 30px;"></td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;">${flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold;" title="Submitted by Pilot Online">O</span>' : ''}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.callsign || '-'}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">-</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
||||
@@ -2027,7 +2125,7 @@
|
||||
} else if (isDeparture) {
|
||||
row.innerHTML = `
|
||||
<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; text-align: center; width: 30px;"></td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;">${flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold;" title="Submitted by Pilot Online">O</span>' : ''}</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;">${flight.out_to || '-'}</td>
|
||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
||||
@@ -2336,7 +2434,7 @@
|
||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||
}
|
||||
acType = flight.type;
|
||||
typeIcon = '';
|
||||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||||
fromDisplay = `<i>${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}</i>`;
|
||||
|
||||
// Calculate ETA: use departed_dt (actual departure) if available, otherwise etd (planned departure)
|
||||
@@ -2351,22 +2449,17 @@
|
||||
pob = flight.pob || '-';
|
||||
fuel = '-';
|
||||
|
||||
// For circuits, add a circuit button
|
||||
// Allow touch and go for all local flight types
|
||||
let circuitButton = '';
|
||||
if (flight.flight_type === 'CIRCUITS') {
|
||||
circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||
T&G
|
||||
</button>`;
|
||||
}
|
||||
circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||
T&G
|
||||
</button>`;
|
||||
|
||||
actionButtons = `
|
||||
${circuitButton}
|
||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" 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 if (isBookedIn) {
|
||||
// Booked-in arrival display
|
||||
@@ -2376,7 +2469,7 @@
|
||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||
}
|
||||
acType = flight.type;
|
||||
typeIcon = '';
|
||||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||||
|
||||
// Lookup airport name for in_from
|
||||
let fromDisplay_temp = flight.in_from;
|
||||
@@ -2421,9 +2514,6 @@
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2495,7 +2585,7 @@
|
||||
} else {
|
||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||
}
|
||||
typeIcon = '';
|
||||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||||
toDisplay = `<i>${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}</i>`;
|
||||
etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||||
pob = flight.pob || '-';
|
||||
@@ -2505,28 +2595,47 @@
|
||||
// Action buttons for local flight
|
||||
if (flight.status === 'BOOKED_OUT') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
|
||||
TAKE OFF
|
||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
|
||||
CONTACT
|
||||
</button>
|
||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
|
||||
CANCEL
|
||||
`;
|
||||
} else if (flight.status === 'GROUND') {
|
||||
const takeoffStatus = flight.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
|
||||
const takeoffTitle = flight.flight_type === 'CIRCUITS' ? 'Mark as Circuit' : 'Mark as Local';
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('${takeoffStatus}', ${flight.id}, true)" title="${takeoffTitle}">
|
||||
TAKE OFF
|
||||
</button>
|
||||
`;
|
||||
} else if (flight.status === 'DEPARTED') {
|
||||
// For circuits, add a circuit button; for other flights, just show land button
|
||||
let circuitButton = '';
|
||||
if (flight.flight_type === 'CIRCUITS') {
|
||||
circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||
T&G
|
||||
</button>`;
|
||||
}
|
||||
// Allow touch and go for all local flight types
|
||||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||
T&G
|
||||
</button>`;
|
||||
actionButtons = `
|
||||
${circuitButton}
|
||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||||
LAND
|
||||
</button>
|
||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
|
||||
CANCEL
|
||||
`;
|
||||
} else if (flight.status === 'LOCAL') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; updateLocalFlightStatusFromTable(${flight.id}, 'CIRCUIT')" title="Rejoin Circuit">
|
||||
REJOIN
|
||||
</button>
|
||||
`;
|
||||
} else if (flight.status === 'CIRCUIT') {
|
||||
// Circuit traffic - show LOCAL, T&G and LAND buttons
|
||||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||
T&G
|
||||
</button>`;
|
||||
actionButtons = `
|
||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; updateLocalFlightStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
|
||||
LOCAL
|
||||
</button>
|
||||
${circuitButton}
|
||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||||
LAND
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
@@ -2539,7 +2648,7 @@
|
||||
} else {
|
||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||
}
|
||||
typeIcon = '';
|
||||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</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);
|
||||
@@ -2552,11 +2661,20 @@
|
||||
// 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">
|
||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)" title="Contact Pilot">
|
||||
CONTACT
|
||||
</button>
|
||||
`;
|
||||
} else if (flight.status === 'GROUND') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('LOCAL', ${flight.id}, false, true)" title="Mark as Local">
|
||||
TAKE OFF
|
||||
</button>
|
||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateDepartureStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel">
|
||||
CANCEL
|
||||
`;
|
||||
} else if (flight.status === 'LOCAL') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
|
||||
QSY
|
||||
</button>
|
||||
`;
|
||||
} else if (flight.status === 'DEPARTED') {
|
||||
@@ -2585,9 +2703,6 @@
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2647,6 +2762,9 @@
|
||||
document.getElementById('ppr-form').reset();
|
||||
document.getElementById('ppr-id').value = '';
|
||||
|
||||
// Clear the unsaved aircraft flag
|
||||
document.getElementById('ppr-form').removeAttribute('data-unsaved-aircraft');
|
||||
|
||||
// Set default ETA and ETD
|
||||
const now = new Date();
|
||||
const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
|
||||
@@ -2898,6 +3016,12 @@
|
||||
} else if (status === 'DEPARTED') {
|
||||
modalTitle.textContent = 'Confirm Departure Time';
|
||||
submitBtn.textContent = '🛫 Confirm Departure';
|
||||
} else if (status === 'GROUND') {
|
||||
modalTitle.textContent = 'Confirm Contact Time';
|
||||
submitBtn.textContent = '📞 Confirm Contact';
|
||||
} else if (status === 'LOCAL') {
|
||||
modalTitle.textContent = 'Confirm Takeoff Time';
|
||||
submitBtn.textContent = '🛫 Confirm Takeoff';
|
||||
}
|
||||
|
||||
// Set default timestamp to current time
|
||||
@@ -3063,6 +3187,9 @@
|
||||
|
||||
if (!accessToken) return;
|
||||
|
||||
// Auto-save any unsaved aircraft types
|
||||
await autoSaveUnsavedAircraft(this);
|
||||
|
||||
const formData = new FormData(this);
|
||||
const pprData = {};
|
||||
|
||||
@@ -3479,6 +3606,182 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== USER AIRCRAFT MANAGEMENT ====================
|
||||
|
||||
async function openUserAircraftModal() {
|
||||
if (!accessToken) return;
|
||||
|
||||
document.getElementById('userAircraftModal').style.display = 'block';
|
||||
await loadUserAircraft();
|
||||
|
||||
// Set up search functionality
|
||||
const searchInput = document.getElementById('user-aircraft-search');
|
||||
searchInput.addEventListener('input', filterUserAircraft);
|
||||
}
|
||||
|
||||
async function loadUserAircraft() {
|
||||
if (!accessToken) return;
|
||||
|
||||
document.getElementById('user-aircraft-loading').style.display = 'block';
|
||||
document.getElementById('user-aircraft-table-content').style.display = 'none';
|
||||
document.getElementById('user-aircraft-no-data').style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user aircraft');
|
||||
}
|
||||
|
||||
const userAircraft = await response.json();
|
||||
displayUserAircraft(userAircraft);
|
||||
} catch (error) {
|
||||
console.error('Error loading user aircraft:', error);
|
||||
if (error.message !== 'Session expired. Please log in again.') {
|
||||
showNotification('Error loading user aircraft', true);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('user-aircraft-loading').style.display = 'none';
|
||||
}
|
||||
|
||||
function displayUserAircraft(userAircraft) {
|
||||
const tbody = document.getElementById('user-aircraft-table-body');
|
||||
const searchInput = document.getElementById('user-aircraft-search');
|
||||
|
||||
// Store original data for filtering
|
||||
tbody._originalData = userAircraft;
|
||||
|
||||
if (userAircraft.length === 0) {
|
||||
document.getElementById('user-aircraft-no-data').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply current filter if any
|
||||
const filterText = searchInput.value.trim().toLowerCase();
|
||||
const filteredAircraft = filterText ?
|
||||
userAircraft.filter(ua =>
|
||||
ua.registration.toLowerCase().includes(filterText) ||
|
||||
ua.type_code.toLowerCase().includes(filterText) ||
|
||||
ua.created_by.toLowerCase().includes(filterText)
|
||||
) : userAircraft;
|
||||
|
||||
if (filteredAircraft.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No aircraft match the search criteria</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = '';
|
||||
filteredAircraft.forEach(aircraft => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Format created date
|
||||
const createdDate = aircraft.created_at ? formatDateTime(aircraft.created_at) : '-';
|
||||
|
||||
row.innerHTML = `
|
||||
<td><strong>${aircraft.registration}</strong></td>
|
||||
<td>${aircraft.type_code}</td>
|
||||
<td>${aircraft.created_by}</td>
|
||||
<td>${createdDate}</td>
|
||||
<td>
|
||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); openUserAircraftEditModal('${aircraft.registration}', '${aircraft.type_code}')" title="Edit Aircraft">
|
||||
✏️
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('user-aircraft-table-content').style.display = 'block';
|
||||
}
|
||||
|
||||
function filterUserAircraft() {
|
||||
const tbody = document.getElementById('user-aircraft-table-body');
|
||||
if (tbody._originalData) {
|
||||
displayUserAircraft(tbody._originalData);
|
||||
}
|
||||
}
|
||||
|
||||
function openUserAircraftEditModal(registration, typeCode) {
|
||||
document.getElementById('edit-aircraft-registration').value = registration;
|
||||
document.getElementById('edit-aircraft-type').value = typeCode;
|
||||
document.getElementById('user-aircraft-edit-title').textContent = `Edit ${registration}`;
|
||||
document.getElementById('userAircraftEditModal').style.display = 'block';
|
||||
|
||||
// Auto-focus on type field
|
||||
setTimeout(() => {
|
||||
document.getElementById('edit-aircraft-type').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// User aircraft edit form submission
|
||||
document.getElementById('user-aircraft-edit-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!accessToken) return;
|
||||
|
||||
const registration = document.getElementById('edit-aircraft-registration').value.trim();
|
||||
const typeCode = document.getElementById('edit-aircraft-type').value.trim();
|
||||
|
||||
if (!registration || !typeCode) {
|
||||
showNotification('Registration and type are required', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/v1/aircraft/user-aircraft/${registration}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
registration: registration,
|
||||
type_code: typeCode
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || 'Failed to update aircraft');
|
||||
}
|
||||
|
||||
closeModal('userAircraftEditModal');
|
||||
await loadUserAircraft(); // Refresh list
|
||||
showNotification('Aircraft updated successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error updating aircraft:', error);
|
||||
showNotification(`Error updating aircraft: ${error.message}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteUserAircraft() {
|
||||
if (!accessToken) return;
|
||||
|
||||
const registration = document.getElementById('edit-aircraft-registration').value.trim();
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the aircraft entry for ${registration}? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/v1/aircraft/user-aircraft/${registration}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || 'Failed to delete aircraft');
|
||||
}
|
||||
|
||||
closeModal('userAircraftEditModal');
|
||||
await loadUserAircraft(); // Refresh list
|
||||
showNotification('Aircraft deleted successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error deleting aircraft:', error);
|
||||
showNotification(`Error deleting aircraft: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Update user role detection and UI visibility
|
||||
async function updateUserRole() {
|
||||
if (!accessToken) {
|
||||
@@ -3496,16 +3799,27 @@
|
||||
|
||||
// Show user management in dropdown only for administrators
|
||||
const userManagementDropdown = document.getElementById('user-management-dropdown');
|
||||
// Show user aircraft for operators and administrators
|
||||
const userAircraftDropdown = document.getElementById('user-aircraft-dropdown');
|
||||
|
||||
if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') {
|
||||
userManagementDropdown.style.display = 'block';
|
||||
if (userManagementDropdown) userManagementDropdown.style.display = 'block';
|
||||
if (userAircraftDropdown) userAircraftDropdown.style.display = 'block';
|
||||
} else if (currentUserRole && currentUserRole.toUpperCase() === 'OPERATOR') {
|
||||
if (userManagementDropdown) userManagementDropdown.style.display = 'none';
|
||||
if (userAircraftDropdown) userAircraftDropdown.style.display = 'block';
|
||||
} else {
|
||||
userManagementDropdown.style.display = 'none';
|
||||
if (userManagementDropdown) userManagementDropdown.style.display = 'none';
|
||||
if (userAircraftDropdown) userAircraftDropdown.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user role:', error);
|
||||
// Hide user management by default on error
|
||||
document.getElementById('user-management-dropdown').style.display = 'none';
|
||||
// Hide admin features by default on error
|
||||
const userManagementDropdown = document.getElementById('user-management-dropdown');
|
||||
const userAircraftDropdown = document.getElementById('user-aircraft-dropdown');
|
||||
if (userManagementDropdown) userManagementDropdown.style.display = 'none';
|
||||
if (userAircraftDropdown) userAircraftDropdown.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3554,22 +3868,29 @@
|
||||
});
|
||||
|
||||
// Local Flight (Book Out) Modal Functions
|
||||
function openLocalFlightModal(flightType = 'LOCAL') {
|
||||
function openLocalFlightModal(flightType = 'LOCAL', prefillReg = '') {
|
||||
document.getElementById('local-flight-form').reset();
|
||||
document.getElementById('local-flight-id').value = '';
|
||||
document.getElementById('local-flight-modal-title').textContent = 'Book Out';
|
||||
document.getElementById('local_flight_type').value = flightType;
|
||||
document.getElementById('localFlightModal').style.display = 'block';
|
||||
|
||||
// Clear the unsaved aircraft flag
|
||||
document.getElementById('local-flight-form').removeAttribute('data-unsaved-aircraft');
|
||||
|
||||
// Clear aircraft lookup results
|
||||
clearLocalAircraftLookup();
|
||||
|
||||
// Update destination field visibility based on flight type
|
||||
handleFlightTypeChange(flightType);
|
||||
|
||||
// Auto-focus on registration field
|
||||
// Auto-focus on registration field and prefill if provided
|
||||
setTimeout(() => {
|
||||
document.getElementById('local_registration').focus();
|
||||
const regField = document.getElementById('local_registration');
|
||||
regField.focus();
|
||||
if (prefillReg) {
|
||||
regField.value = prefillReg;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -3578,6 +3899,9 @@
|
||||
document.getElementById('book-in-id').value = '';
|
||||
document.getElementById('bookInModal').style.display = 'block';
|
||||
|
||||
// Clear the unsaved aircraft flag
|
||||
document.getElementById('book-in-form').removeAttribute('data-unsaved-aircraft');
|
||||
|
||||
// Clear aircraft lookup results
|
||||
clearBookInAircraftLookup();
|
||||
clearBookInArrivalAirportLookup();
|
||||
@@ -3596,6 +3920,9 @@
|
||||
document.getElementById('overflight-id').value = '';
|
||||
document.getElementById('overflightModal').style.display = 'block';
|
||||
|
||||
// Clear the unsaved aircraft flag
|
||||
document.getElementById('overflight-form').removeAttribute('data-unsaved-aircraft');
|
||||
|
||||
// Clear aircraft lookup results
|
||||
clearOverflightAircraftLookup();
|
||||
clearOverflightDepartureAirportLookup();
|
||||
@@ -3994,14 +4321,10 @@
|
||||
|
||||
document.getElementById('local-flight-edit-title').textContent = `${flight.registration} - ${flight.flight_type}`;
|
||||
|
||||
// Load and display circuits if this is a CIRCUITS flight
|
||||
// Load and display circuits for all local flight types
|
||||
const circuitsSection = document.getElementById('circuits-section');
|
||||
if (flight.flight_type === 'CIRCUITS') {
|
||||
circuitsSection.style.display = 'block';
|
||||
loadCircuitsDisplay(flight.id);
|
||||
} else {
|
||||
circuitsSection.style.display = 'none';
|
||||
}
|
||||
circuitsSection.style.display = 'block';
|
||||
loadCircuitsDisplay(flight.id);
|
||||
|
||||
document.getElementById('localFlightEditModal').style.display = 'block';
|
||||
|
||||
@@ -4529,6 +4852,9 @@
|
||||
|
||||
if (!accessToken) return;
|
||||
|
||||
// Auto-save any unsaved aircraft types
|
||||
await autoSaveUnsavedAircraft(this);
|
||||
|
||||
const formData = new FormData(this);
|
||||
const flightType = formData.get('flight_type');
|
||||
const flightData = {};
|
||||
@@ -4612,6 +4938,9 @@
|
||||
|
||||
if (!accessToken) return;
|
||||
|
||||
// Auto-save any unsaved aircraft types
|
||||
await autoSaveUnsavedAircraft(this);
|
||||
|
||||
const formData = new FormData(this);
|
||||
const arrivalData = {};
|
||||
|
||||
@@ -4691,6 +5020,9 @@
|
||||
|
||||
if (!accessToken) return;
|
||||
|
||||
// Auto-save any unsaved aircraft types
|
||||
await autoSaveUnsavedAircraft(this);
|
||||
|
||||
const formData = new FormData(this);
|
||||
const overflightData = {};
|
||||
|
||||
|
||||
+5543
File diff suppressed because it is too large
Load Diff
+1147
File diff suppressed because it is too large
Load Diff
@@ -692,6 +692,9 @@
|
||||
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Auto-save any unsaved aircraft types
|
||||
await autoSaveUnsavedAircraft(this);
|
||||
|
||||
const formData = new FormData(this);
|
||||
const pprData = {};
|
||||
|
||||
|
||||
@@ -178,6 +178,35 @@
|
||||
left: 28px;
|
||||
}
|
||||
|
||||
/* QR code for booking */
|
||||
.qr-code-container {
|
||||
position: absolute;
|
||||
left: 300px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: white;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.qr-code-container .qr-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qr-code-container img {
|
||||
display: block;
|
||||
max-width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Santa hat styles */
|
||||
.santa-hat {
|
||||
position: absolute;
|
||||
@@ -357,6 +386,10 @@
|
||||
<body>
|
||||
<header>
|
||||
<img src="assets/logo.png" alt="EGFH Logo" class="left-image">
|
||||
<div class="qr-code-container">
|
||||
<img id="bookingQR" alt="Scan to book a flight" title="Scan to book a flight">
|
||||
<div class="qr-label">Book Out</div>
|
||||
</div>
|
||||
<h1>Flight Information</h1>
|
||||
<img src="assets/flightImg.png" alt="EGFH Logo" class="right-image">
|
||||
</header>
|
||||
@@ -847,11 +880,22 @@
|
||||
return typeMap[flightType] || flightType;
|
||||
}
|
||||
|
||||
// Generate QR code for booking page
|
||||
function generateBookingQR() {
|
||||
const qrImg = document.getElementById('bookingQR');
|
||||
if (qrImg) {
|
||||
qrImg.src = '/assets/booking-qr.png';
|
||||
}
|
||||
}
|
||||
|
||||
// Load data on page load
|
||||
window.addEventListener('load', function() {
|
||||
// Initialize Christmas mode
|
||||
initChristmasMode();
|
||||
|
||||
// Load booking QR code
|
||||
generateBookingQR();
|
||||
|
||||
loadArrivals();
|
||||
loadDepartures();
|
||||
|
||||
|
||||
+137
-4
@@ -163,15 +163,26 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
||||
const resultsDiv = document.getElementById(resultsId);
|
||||
|
||||
if (config.isAircraft) {
|
||||
// Aircraft lookup: auto-populate on single match, format input on no match
|
||||
// Aircraft lookup: auto-populate on single match, mark form for auto-save on no match
|
||||
if (!results || results.length === 0) {
|
||||
// Format the aircraft registration and auto-populate
|
||||
// Format the aircraft registration
|
||||
const formatted = formatAircraftRegistration(searchTerm);
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) {
|
||||
field.value = formatted;
|
||||
// Mark the form for auto-saving this aircraft
|
||||
const form = field.closest('form');
|
||||
if (form) {
|
||||
form.setAttribute('data-unsaved-aircraft', fieldId);
|
||||
}
|
||||
}
|
||||
resultsDiv.innerHTML = ''; // Clear results, field is auto-populated
|
||||
|
||||
// Show message that type will be saved
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="aircraft-no-match">
|
||||
No match found - aircraft type will be saved automatically when you submit
|
||||
</div>
|
||||
`;
|
||||
} else if (results.length === 1) {
|
||||
// Single match - auto-populate
|
||||
const aircraft = results[0];
|
||||
@@ -183,7 +194,14 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
||||
|
||||
// Auto-populate the form fields
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) field.value = aircraft.registration;
|
||||
if (field) {
|
||||
field.value = aircraft.registration;
|
||||
// Clear the unsaved aircraft flag since we found a match
|
||||
const form = field.closest('form');
|
||||
if (form) {
|
||||
form.removeAttribute('data-unsaved-aircraft');
|
||||
}
|
||||
}
|
||||
|
||||
// Also populate type field
|
||||
let typeFieldId;
|
||||
@@ -208,6 +226,14 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
||||
Multiple matches found (${results.length}) - please be more specific
|
||||
</div>
|
||||
`;
|
||||
// Clear the unsaved aircraft flag since multiple matches found
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) {
|
||||
const form = field.closest('form');
|
||||
if (form) {
|
||||
form.removeAttribute('data-unsaved-aircraft');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Airport lookup: show list of options with keyboard navigation
|
||||
@@ -501,3 +527,110 @@ function selectBookInAircraft(registration) {
|
||||
function selectBookInArrivalAirport(icaoCode) {
|
||||
lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode);
|
||||
}
|
||||
|
||||
// Save user aircraft type for future lookups
|
||||
async function saveUserAircraft(registrationFieldId, resultsDivId) {
|
||||
const regField = document.getElementById(registrationFieldId);
|
||||
if (!regField || !regField.value.trim()) {
|
||||
showNotification('Please enter a registration first', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the type field ID based on registration field
|
||||
let typeFieldId;
|
||||
if (registrationFieldId === 'ac_reg') {
|
||||
typeFieldId = 'ac_type';
|
||||
} else if (registrationFieldId === 'local_registration') {
|
||||
typeFieldId = 'local_type';
|
||||
} else if (registrationFieldId === 'book_in_registration') {
|
||||
typeFieldId = 'book_in_type';
|
||||
} else if (registrationFieldId === 'overflight_registration') {
|
||||
typeFieldId = 'overflight_type';
|
||||
}
|
||||
|
||||
const typeField = document.getElementById(typeFieldId);
|
||||
if (!typeField || !typeField.value.trim()) {
|
||||
showNotification('Please enter an aircraft type first', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
registration: regField.value.trim(),
|
||||
type_code: typeField.value.trim()
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
showNotification('Aircraft type saved for future use');
|
||||
|
||||
// Clear the results div
|
||||
const resultsDiv = document.getElementById(resultsDivId);
|
||||
if (resultsDiv) {
|
||||
resultsDiv.innerHTML = '';
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showNotification(error.detail || 'Failed to save aircraft', true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving aircraft:', error);
|
||||
showNotification('Error saving aircraft', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-save unsaved aircraft before form submission
|
||||
async function autoSaveUnsavedAircraft(form) {
|
||||
const unsavedFieldId = form.getAttribute('data-unsaved-aircraft');
|
||||
if (!unsavedFieldId) return; // No unsaved aircraft to save
|
||||
|
||||
const regField = document.getElementById(unsavedFieldId);
|
||||
if (!regField || !regField.value.trim()) return;
|
||||
|
||||
// Determine the type field ID
|
||||
let typeFieldId;
|
||||
if (unsavedFieldId === 'ac_reg') {
|
||||
typeFieldId = 'ac_type';
|
||||
} else if (unsavedFieldId === 'local_registration') {
|
||||
typeFieldId = 'local_type';
|
||||
} else if (unsavedFieldId === 'book_in_registration') {
|
||||
typeFieldId = 'book_in_type';
|
||||
} else if (unsavedFieldId === 'overflight_registration') {
|
||||
typeFieldId = 'overflight_type';
|
||||
}
|
||||
|
||||
const typeField = document.getElementById(typeFieldId);
|
||||
if (!typeField || !typeField.value.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
registration: regField.value.trim(),
|
||||
type_code: typeField.value.trim()
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Successfully saved, remove the flag
|
||||
form.removeAttribute('data-unsaved-aircraft');
|
||||
console.log('Auto-saved aircraft type for', regField.value.trim());
|
||||
} else if (response.status === 400) {
|
||||
// Already exists, just remove the flag
|
||||
form.removeAttribute('data-unsaved-aircraft');
|
||||
} else {
|
||||
console.error('Failed to auto-save aircraft');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error auto-saving aircraft:', error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user