Compare commits
13 Commits
stable
...
9867156334
| Author | SHA1 | Date | |
|---|---|---|---|
| 9867156334 | |||
| eb2321ef40 | |||
| bb6597ff76 | |||
| 423023d3d9 | |||
| fd0e521186 | |||
| d2c9bc0370 | |||
| bddbe1451f | |||
| 785562407a | |||
| 5bb229ad78 | |||
| 8a2dd5544c | |||
| 3a4085afc6 | |||
| a43cf9b732 | |||
| 7f4e4a8459 |
@@ -1,3 +1,5 @@
|
|||||||
|
web/assets/booking-qr.png
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
+2
-1
@@ -3,11 +3,12 @@ FROM python:3.11-slim
|
|||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies including qrencode
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gcc \
|
gcc \
|
||||||
default-libmysqlclient-dev \
|
default-libmysqlclient-dev \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
qrencode \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements first for better caching
|
# 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,79 @@
|
|||||||
|
"""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','CIRCUIT_DOWNWIND','CIRCUIT_BASE','CIRCUIT_FINAL','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','INBOUND','LANDED','GROUND','LOCAL','CIRCUIT','CIRCUIT_DOWNWIND','CIRCUIT_BASE','CIRCUIT_FINAL','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','CIRCUIT','CIRCUIT_DOWNWIND','CIRCUIT_BASE','CIRCUIT_FINAL','LANDED','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))
|
||||||
|
|
||||||
|
# Add arrival_id column to circuits table to support circuit logging for arrivals
|
||||||
|
op.add_column('circuits', sa.Column('arrival_id', sa.BigInteger(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_circuits_arrival_id', 'circuits', 'arrivals', ['arrival_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_index('idx_circuit_arrival_id', 'circuits', ['arrival_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove arrival_id column from circuits table
|
||||||
|
op.drop_constraint('fk_circuits_arrival_id', 'circuits', type_='foreignkey')
|
||||||
|
op.drop_index('idx_circuit_arrival_id', table_name='circuits')
|
||||||
|
op.drop_column('circuits', 'arrival_id')
|
||||||
|
|
||||||
|
# Update departures with new status values to valid old values before modifying enum
|
||||||
|
op.execute("UPDATE departures SET status = 'DEPARTED' WHERE status IN ('GROUND', 'LOCAL')")
|
||||||
|
|
||||||
|
# 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')")
|
||||||
|
|
||||||
|
# Update arrivals with new status values to valid old values before modifying enum
|
||||||
|
op.execute("UPDATE arrivals SET status = 'LANDED' WHERE status IN ('GROUND', 'LOCAL', 'CIRCUIT', 'ARRIVED')")
|
||||||
|
|
||||||
|
# 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')")
|
||||||
|
|
||||||
|
# Update local_flights with new status values to valid old values before modifying enum
|
||||||
|
op.execute("UPDATE local_flights SET status = 'DEPARTED' WHERE status IN ('GROUND', 'LOCAL', 'CIRCUIT')")
|
||||||
|
|
||||||
|
# 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 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()
|
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(circuits.router, prefix="/circuits", tags=["circuits"])
|
||||||
api_router.include_router(journal.router, prefix="/journal", tags=["journal"])
|
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.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(aircraft.router, prefix="/aircraft", tags=["aircraft"])
|
||||||
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])
|
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func
|
||||||
from app.api.deps import get_db, get_current_active_user
|
from app.api.deps import get_db, get_current_active_user
|
||||||
from app.models.ppr import Aircraft
|
from app.models.ppr import Aircraft, UserAircraft
|
||||||
from app.schemas.ppr import Aircraft as AircraftSchema
|
from app.schemas.ppr import Aircraft as AircraftSchema, UserAircraftCreate, UserAircraft as UserAircraftSchema
|
||||||
from app.models.ppr import User
|
from app.models.ppr import User
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -18,6 +19,7 @@ async def lookup_aircraft_by_registration(
|
|||||||
"""
|
"""
|
||||||
Lookup aircraft by registration (clean match).
|
Lookup aircraft by registration (clean match).
|
||||||
Removes non-alphanumeric characters from input for matching.
|
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 the input registration (remove non-alphanumeric characters)
|
||||||
clean_input = ''.join(c for c in registration if c.isalnum()).upper()
|
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:
|
if len(clean_input) < 4:
|
||||||
return []
|
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_list = db.query(Aircraft).filter(
|
||||||
Aircraft.clean_reg.like(f"{clean_input}%")
|
Aircraft.clean_reg.like(f"{clean_input}%")
|
||||||
).limit(10).all()
|
).limit(10).all()
|
||||||
@@ -42,6 +66,7 @@ async def public_lookup_aircraft_by_registration(
|
|||||||
Public lookup aircraft by registration (clean match).
|
Public lookup aircraft by registration (clean match).
|
||||||
Removes non-alphanumeric characters from input for matching.
|
Removes non-alphanumeric characters from input for matching.
|
||||||
No authentication required.
|
No authentication required.
|
||||||
|
Checks user_aircraft table first, then aircraft table.
|
||||||
"""
|
"""
|
||||||
# Clean the input registration (remove non-alphanumeric characters)
|
# Clean the input registration (remove non-alphanumeric characters)
|
||||||
clean_input = ''.join(c for c in registration if c.isalnum()).upper()
|
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:
|
if len(clean_input) < 4:
|
||||||
return []
|
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_list = db.query(Aircraft).filter(
|
||||||
Aircraft.clean_reg.like(f"{clean_input}%")
|
Aircraft.clean_reg.like(f"{clean_input}%")
|
||||||
).limit(10).all()
|
).limit(10).all()
|
||||||
@@ -82,3 +128,100 @@ async def search_aircraft(
|
|||||||
).limit(limit).all()
|
).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,12 +38,12 @@ async def create_arrival(
|
|||||||
current_user: User = Depends(get_current_operator_user)
|
current_user: User = Depends(get_current_operator_user)
|
||||||
):
|
):
|
||||||
"""Create a new arrival record"""
|
"""Create a new arrival record"""
|
||||||
arrival = crud_arrival.create(db, obj_in=arrival_in, created_by=current_user.username)
|
arrival = crud_arrival.create(db, obj_in=arrival_in, created_by=current_user.username, submitted_via=arrival_in.submitted_via)
|
||||||
|
|
||||||
# Send real-time update via WebSocket
|
# Send real-time update via WebSocket
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
await request.app.state.connection_manager.broadcast({
|
await request.app.state.connection_manager.broadcast({
|
||||||
"type": "arrival_booked_in",
|
"type": "arrival_inbound",
|
||||||
"data": {
|
"data": {
|
||||||
"id": arrival.id,
|
"id": arrival.id,
|
||||||
"registration": arrival.registration,
|
"registration": arrival.registration,
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ async def get_circuits_by_flight(
|
|||||||
return circuits
|
return circuits
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/arrival/{arrival_id}", response_model=List[Circuit])
|
||||||
|
async def get_circuits_by_arrival(
|
||||||
|
arrival_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_read_user)
|
||||||
|
):
|
||||||
|
"""Get all circuits for a specific arrival"""
|
||||||
|
circuits = crud_circuit.get_by_arrival(db, arrival_id=arrival_id)
|
||||||
|
return circuits
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Circuit)
|
@router.post("/", response_model=Circuit)
|
||||||
async def create_circuit(
|
async def create_circuit(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -40,7 +51,19 @@ async def create_circuit(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_operator_user)
|
current_user: User = Depends(get_current_operator_user)
|
||||||
):
|
):
|
||||||
"""Record a new circuit (touch and go) for a local flight"""
|
"""Record a new circuit (touch and go) for a local flight or arrival"""
|
||||||
|
# Validate that exactly one of local_flight_id or arrival_id is provided
|
||||||
|
if not circuit_in.local_flight_id and not circuit_in.arrival_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Either local_flight_id or arrival_id must be provided"
|
||||||
|
)
|
||||||
|
if circuit_in.local_flight_id and circuit_in.arrival_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot provide both local_flight_id and arrival_id"
|
||||||
|
)
|
||||||
|
|
||||||
circuit = crud_circuit.create(db, obj_in=circuit_in)
|
circuit = crud_circuit.create(db, obj_in=circuit_in)
|
||||||
|
|
||||||
# Send real-time update via WebSocket
|
# Send real-time update via WebSocket
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ async def create_departure(
|
|||||||
current_user: User = Depends(get_current_operator_user)
|
current_user: User = Depends(get_current_operator_user)
|
||||||
):
|
):
|
||||||
"""Create a new departure record"""
|
"""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
|
# Send real-time update via WebSocket
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async def create_local_flight(
|
|||||||
current_user: User = Depends(get_current_operator_user)
|
current_user: User = Depends(get_current_operator_user)
|
||||||
):
|
):
|
||||||
"""Create a new local flight record (book out)"""
|
"""Create a new local flight record (book out)"""
|
||||||
flight = crud_local_flight.create(db, obj_in=flight_in, created_by=current_user.username)
|
flight = crud_local_flight.create(db, obj_in=flight_in, created_by=current_user.username, submitted_via="ADMIN")
|
||||||
|
|
||||||
# Send real-time update via WebSocket
|
# Send real-time update via WebSocket
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
|
|||||||
# Add booked-in arrivals
|
# Add booked-in arrivals
|
||||||
booked_in_arrivals = crud_arrival.get_multi(db, limit=1000)
|
booked_in_arrivals = crud_arrival.get_multi(db, limit=1000)
|
||||||
for arrival in booked_in_arrivals:
|
for arrival in booked_in_arrivals:
|
||||||
# Only include BOOKED_IN and LANDED arrivals
|
# Only include BOOKED_IN, INBOUND and LANDED arrivals
|
||||||
if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.LANDED):
|
if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.INBOUND, ArrivalStatus.LANDED):
|
||||||
continue
|
continue
|
||||||
# For BOOKED_IN, only include those created today
|
# For BOOKED_IN and INBOUND, only include those created today
|
||||||
if arrival.status == ArrivalStatus.BOOKED_IN:
|
if arrival.status in (ArrivalStatus.BOOKED_IN, ArrivalStatus.INBOUND):
|
||||||
if not (today_start <= arrival.created_dt < today_end):
|
if not (today_start <= arrival.created_dt < today_end):
|
||||||
continue
|
continue
|
||||||
# For LANDED, only include those landed today
|
# For LANDED, only include those landed today
|
||||||
@@ -173,10 +173,10 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
|||||||
'isDeparture': False
|
'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(
|
departures_to_airports = crud_departure.get_multi(
|
||||||
db,
|
db,
|
||||||
status=DepartureStatus.BOOKED_OUT,
|
status=None, # Get all statuses
|
||||||
limit=1000
|
limit=1000
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -187,17 +187,25 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
# Convert departures to match the format for display
|
# Convert departures to match the format for display
|
||||||
for dep in departures_to_airports:
|
for dep in departures_to_airports:
|
||||||
# Only include departures booked out today
|
# Only include departures booked out today and not yet departed
|
||||||
if not (today_start <= dep.created_dt < today_end):
|
if not (today_start <= dep.created_dt < today_end) or dep.status == DepartureStatus.DEPARTED:
|
||||||
continue
|
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({
|
departures_list.append({
|
||||||
'ac_call': dep.callsign or dep.registration,
|
'ac_call': dep.callsign or dep.registration,
|
||||||
'ac_reg': dep.registration,
|
'ac_reg': dep.registration,
|
||||||
'ac_type': dep.type,
|
'ac_type': dep.type,
|
||||||
'out_to': dep.out_to,
|
'out_to': dep.out_to,
|
||||||
'etd': dep.etd or dep.created_dt,
|
'etd': dep.etd or dep.created_dt,
|
||||||
'departed_dt': None,
|
'departed_dt': dep.departed_dt,
|
||||||
'status': 'BOOKED_OUT',
|
'status': display_status,
|
||||||
'isLocalFlight': False,
|
'isLocalFlight': False,
|
||||||
'isDeparture': True
|
'isDeparture': True
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
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", submitted_via="PUBLIC")
|
||||||
|
|
||||||
|
# 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", submitted_via="PUBLIC")
|
||||||
|
|
||||||
|
# Update with pilot email
|
||||||
|
db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({
|
||||||
|
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"
|
top_bar_base_color: str = "#2c3e50"
|
||||||
environment: str = "production" # production, development, staging, etc.
|
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 settings (for future use)
|
||||||
redis_url: Optional[str] = None
|
redis_url: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,17 @@ class CRUDArrival:
|
|||||||
query = db.query(Arrival)
|
query = db.query(Arrival)
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
query = query.filter(Arrival.status == status)
|
if status == ArrivalStatus.CIRCUIT:
|
||||||
|
# Special case: when requesting CIRCUIT status, return all circuit-related statuses
|
||||||
|
circuit_statuses = [
|
||||||
|
ArrivalStatus.CIRCUIT,
|
||||||
|
ArrivalStatus.CIRCUIT_DOWNWIND,
|
||||||
|
ArrivalStatus.CIRCUIT_BASE,
|
||||||
|
ArrivalStatus.CIRCUIT_FINAL
|
||||||
|
]
|
||||||
|
query = query.filter(Arrival.status.in_(circuit_statuses))
|
||||||
|
else:
|
||||||
|
query = query.filter(Arrival.status == status)
|
||||||
|
|
||||||
if date_from:
|
if date_from:
|
||||||
query = query.filter(func.date(Arrival.created_dt) >= date_from)
|
query = query.filter(func.date(Arrival.created_dt) >= date_from)
|
||||||
@@ -35,23 +45,33 @@ class CRUDArrival:
|
|||||||
return query.order_by(desc(Arrival.created_dt)).offset(skip).limit(limit).all()
|
return query.order_by(desc(Arrival.created_dt)).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
def get_arrivals_today(self, db: Session) -> List[Arrival]:
|
def get_arrivals_today(self, db: Session) -> List[Arrival]:
|
||||||
"""Get today's arrivals (booked in or landed)"""
|
"""Get today's arrivals (booked in, inbound or landed)"""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return db.query(Arrival).filter(
|
return db.query(Arrival).filter(
|
||||||
and_(
|
and_(
|
||||||
func.date(Arrival.created_dt) == today,
|
func.date(Arrival.created_dt) == today,
|
||||||
or_(
|
or_(
|
||||||
Arrival.status == ArrivalStatus.BOOKED_IN,
|
Arrival.status == ArrivalStatus.BOOKED_IN,
|
||||||
|
Arrival.status == ArrivalStatus.INBOUND,
|
||||||
Arrival.status == ArrivalStatus.LANDED
|
Arrival.status == ArrivalStatus.LANDED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).order_by(Arrival.created_dt).all()
|
).order_by(Arrival.created_dt).all()
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival:
|
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str, submitted_via: str = "ADMIN") -> Arrival:
|
||||||
|
from app.models.arrival import SubmissionSource
|
||||||
|
|
||||||
|
# Set initial status based on submission source
|
||||||
|
initial_status = ArrivalStatus.BOOKED_IN
|
||||||
|
|
||||||
|
if submitted_via == SubmissionSource.ADMIN:
|
||||||
|
initial_status = ArrivalStatus.INBOUND
|
||||||
|
|
||||||
db_obj = Arrival(
|
db_obj = Arrival(
|
||||||
**obj_in.dict(),
|
**obj_in.dict(exclude={'submitted_via'}),
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
status=ArrivalStatus.BOOKED_IN
|
status=initial_status,
|
||||||
|
submitted_via=submitted_via
|
||||||
)
|
)
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -113,8 +133,12 @@ class CRUDArrival:
|
|||||||
old_status = db_obj.status
|
old_status = db_obj.status
|
||||||
db_obj.status = status
|
db_obj.status = status
|
||||||
|
|
||||||
if status == ArrivalStatus.LANDED and timestamp:
|
# Set timestamps based on status
|
||||||
db_obj.landed_dt = timestamp
|
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.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ class CRUDCircuit:
|
|||||||
Circuit.local_flight_id == local_flight_id
|
Circuit.local_flight_id == local_flight_id
|
||||||
).order_by(Circuit.circuit_timestamp).all()
|
).order_by(Circuit.circuit_timestamp).all()
|
||||||
|
|
||||||
|
def get_by_arrival(self, db: Session, arrival_id: int) -> List[Circuit]:
|
||||||
|
"""Get all circuits for a specific arrival"""
|
||||||
|
return db.query(Circuit).filter(
|
||||||
|
Circuit.arrival_id == arrival_id
|
||||||
|
).order_by(Circuit.circuit_timestamp).all()
|
||||||
|
|
||||||
def get_multi(
|
def get_multi(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
@@ -27,6 +33,7 @@ class CRUDCircuit:
|
|||||||
def create(self, db: Session, obj_in: CircuitCreate) -> Circuit:
|
def create(self, db: Session, obj_in: CircuitCreate) -> Circuit:
|
||||||
db_obj = Circuit(
|
db_obj = Circuit(
|
||||||
local_flight_id=obj_in.local_flight_id,
|
local_flight_id=obj_in.local_flight_id,
|
||||||
|
arrival_id=obj_in.arrival_id,
|
||||||
circuit_timestamp=obj_in.circuit_timestamp
|
circuit_timestamp=obj_in.circuit_timestamp
|
||||||
)
|
)
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
|
|||||||
@@ -47,11 +47,23 @@ class CRUDDeparture:
|
|||||||
)
|
)
|
||||||
).order_by(Departure.created_dt).all()
|
).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(
|
db_obj = Departure(
|
||||||
**obj_in.dict(),
|
**obj_in.dict(),
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
status=DepartureStatus.BOOKED_OUT
|
status=initial_status,
|
||||||
|
contact_dt=contact_dt,
|
||||||
|
submitted_via=submitted_via
|
||||||
)
|
)
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -113,8 +125,14 @@ class CRUDDeparture:
|
|||||||
old_status = db_obj.status
|
old_status = db_obj.status
|
||||||
db_obj.status = status
|
db_obj.status = status
|
||||||
|
|
||||||
if status == DepartureStatus.DEPARTED and timestamp:
|
# Set timestamps based on status
|
||||||
db_obj.departed_dt = timestamp
|
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.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -26,7 +26,17 @@ class CRUDLocalFlight:
|
|||||||
query = db.query(LocalFlight)
|
query = db.query(LocalFlight)
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
query = query.filter(LocalFlight.status == status)
|
if status == LocalFlightStatus.CIRCUIT:
|
||||||
|
# Special case: when requesting CIRCUIT status, return all circuit-related statuses
|
||||||
|
circuit_statuses = [
|
||||||
|
LocalFlightStatus.CIRCUIT,
|
||||||
|
LocalFlightStatus.CIRCUIT_DOWNWIND,
|
||||||
|
LocalFlightStatus.CIRCUIT_BASE,
|
||||||
|
LocalFlightStatus.CIRCUIT_FINAL
|
||||||
|
]
|
||||||
|
query = query.filter(LocalFlight.status.in_(circuit_statuses))
|
||||||
|
else:
|
||||||
|
query = query.filter(LocalFlight.status == status)
|
||||||
|
|
||||||
if flight_type:
|
if flight_type:
|
||||||
query = query.filter(LocalFlight.flight_type == flight_type)
|
query = query.filter(LocalFlight.flight_type == flight_type)
|
||||||
@@ -74,11 +84,20 @@ class CRUDLocalFlight:
|
|||||||
)
|
)
|
||||||
).order_by(LocalFlight.created_dt).all()
|
).order_by(LocalFlight.created_dt).all()
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str) -> LocalFlight:
|
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str, submitted_via: str = "ADMIN") -> LocalFlight:
|
||||||
|
from app.models.local_flight import SubmissionSource
|
||||||
|
|
||||||
|
# Set initial status based on submission source
|
||||||
|
initial_status = LocalFlightStatus.BOOKED_OUT
|
||||||
|
|
||||||
|
if submitted_via == SubmissionSource.ADMIN:
|
||||||
|
initial_status = LocalFlightStatus.GROUND
|
||||||
|
|
||||||
db_obj = LocalFlight(
|
db_obj = LocalFlight(
|
||||||
**obj_in.dict(),
|
**obj_in.dict(),
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
status=LocalFlightStatus.BOOKED_OUT
|
status=initial_status,
|
||||||
|
submitted_via=submitted_via
|
||||||
)
|
)
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -144,10 +163,20 @@ class CRUDLocalFlight:
|
|||||||
old_status = db_obj.status
|
old_status = db_obj.status
|
||||||
db_obj.status = 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
|
# Set timestamps based on status
|
||||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
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
|
db_obj.departed_dt = current_time
|
||||||
|
elif status == LocalFlightStatus.LOCAL:
|
||||||
|
db_obj.takeoff_dt = current_time
|
||||||
elif status == LocalFlightStatus.LANDED:
|
elif status == LocalFlightStatus.LANDED:
|
||||||
db_obj.landed_dt = current_time
|
db_obj.landed_dt = current_time
|
||||||
# Count circuits from the circuits table and populate the circuits column
|
# Count circuits from the circuits table and populate the circuits column
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from app.models.journal import JournalEntry
|
|||||||
from app.models.local_flight import LocalFlight
|
from app.models.local_flight import LocalFlight
|
||||||
from app.models.departure import Departure
|
from app.models.departure import Departure
|
||||||
from app.models.arrival import Arrival
|
from app.models.arrival import Arrival
|
||||||
|
from app.models.circuit import Circuit
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, DateTime, Enum as SQLEnum, func
|
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 enum import Enum
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from app.db.session import Base
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
|
class SubmissionSource(str, Enum):
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
PUBLIC = "PUBLIC"
|
||||||
|
|
||||||
|
|
||||||
class ArrivalStatus(str, Enum):
|
class ArrivalStatus(str, Enum):
|
||||||
BOOKED_IN = "BOOKED_IN"
|
BOOKED_IN = "BOOKED_IN"
|
||||||
|
INBOUND = "INBOUND"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
|
GROUND = "GROUND"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
|
ARRIVED = "ARRIVED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
@@ -21,10 +32,13 @@ class Arrival(Base):
|
|||||||
callsign = Column(String(16), nullable=True)
|
callsign = Column(String(16), nullable=True)
|
||||||
pob = Column(Integer, nullable=False)
|
pob = Column(Integer, nullable=False)
|
||||||
in_from = Column(String(4), nullable=False, index=True)
|
in_from = Column(String(4), nullable=False, index=True)
|
||||||
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True)
|
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.INBOUND, nullable=False, index=True)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||||
eta = Column(DateTime, nullable=True, index=True)
|
eta = Column(DateTime, nullable=True, index=True)
|
||||||
landed_dt = Column(DateTime, nullable=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)
|
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)
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class Circuit(Base):
|
|||||||
__tablename__ = "circuits"
|
__tablename__ = "circuits"
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
local_flight_id = Column(BigInteger, ForeignKey("local_flights.id", ondelete="CASCADE"), nullable=False, index=True)
|
local_flight_id = Column(BigInteger, ForeignKey("local_flights.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||||
|
arrival_id = Column(BigInteger, ForeignKey("arrivals.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||||
circuit_timestamp = Column(DateTime, nullable=False, index=True)
|
circuit_timestamp = Column(DateTime, nullable=False, index=True)
|
||||||
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||||
|
|||||||
@@ -6,9 +6,16 @@ from datetime import datetime
|
|||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionSource(str, Enum):
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
PUBLIC = "PUBLIC"
|
||||||
|
|
||||||
|
|
||||||
class DepartureStatus(str, Enum):
|
class DepartureStatus(str, Enum):
|
||||||
BOOKED_OUT = "BOOKED_OUT"
|
BOOKED_OUT = "BOOKED_OUT"
|
||||||
|
GROUND = "GROUND"
|
||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +32,10 @@ class Departure(Base):
|
|||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||||
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
|
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)
|
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)
|
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
|
from app.db.session import Base
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionSource(str, Enum):
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
PUBLIC = "PUBLIC"
|
||||||
|
|
||||||
|
|
||||||
class LocalFlightType(str, Enum):
|
class LocalFlightType(str, Enum):
|
||||||
LOCAL = "LOCAL"
|
LOCAL = "LOCAL"
|
||||||
CIRCUITS = "CIRCUITS"
|
CIRCUITS = "CIRCUITS"
|
||||||
@@ -12,7 +17,13 @@ class LocalFlightType(str, Enum):
|
|||||||
|
|
||||||
class LocalFlightStatus(str, Enum):
|
class LocalFlightStatus(str, Enum):
|
||||||
BOOKED_OUT = "BOOKED_OUT"
|
BOOKED_OUT = "BOOKED_OUT"
|
||||||
|
GROUND = "GROUND"
|
||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
@@ -32,7 +43,11 @@ class LocalFlight(Base):
|
|||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=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
|
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
|
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)
|
landed_dt = Column(DateTime, nullable=True)
|
||||||
created_by = Column(String(16), nullable=True, index=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())
|
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||||
|
|||||||
@@ -89,3 +89,15 @@ class Aircraft(Base):
|
|||||||
clean_reg = Column(String(25), nullable=True, index=True)
|
clean_reg = Column(String(25), nullable=True, index=True)
|
||||||
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
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())
|
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())
|
||||||
@@ -6,10 +6,23 @@ from enum import Enum
|
|||||||
|
|
||||||
class ArrivalStatus(str, Enum):
|
class ArrivalStatus(str, Enum):
|
||||||
BOOKED_IN = "BOOKED_IN"
|
BOOKED_IN = "BOOKED_IN"
|
||||||
|
INBOUND = "INBOUND"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
|
GROUND = "GROUND"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
|
ARRIVED = "ARRIVED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionSource(str, Enum):
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
PUBLIC = "PUBLIC"
|
||||||
|
|
||||||
|
|
||||||
class ArrivalBase(BaseModel):
|
class ArrivalBase(BaseModel):
|
||||||
registration: str
|
registration: str
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
@@ -39,6 +52,7 @@ class ArrivalBase(BaseModel):
|
|||||||
|
|
||||||
class ArrivalCreate(ArrivalBase):
|
class ArrivalCreate(ArrivalBase):
|
||||||
eta: Optional[datetime] = None
|
eta: Optional[datetime] = None
|
||||||
|
submitted_via: Optional[SubmissionSource] = SubmissionSource.ADMIN
|
||||||
|
|
||||||
|
|
||||||
class ArrivalUpdate(BaseModel):
|
class ArrivalUpdate(BaseModel):
|
||||||
@@ -47,6 +61,10 @@ class ArrivalUpdate(BaseModel):
|
|||||||
callsign: Optional[str] = None
|
callsign: Optional[str] = None
|
||||||
pob: Optional[int] = None
|
pob: Optional[int] = None
|
||||||
in_from: Optional[str] = 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
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -61,8 +79,11 @@ class Arrival(ArrivalBase):
|
|||||||
created_dt: datetime
|
created_dt: datetime
|
||||||
eta: Optional[datetime] = None
|
eta: Optional[datetime] = None
|
||||||
landed_dt: Optional[datetime] = None
|
landed_dt: Optional[datetime] = None
|
||||||
|
arrived_dt: Optional[datetime] = None
|
||||||
created_by: Optional[str] = None
|
created_by: Optional[str] = None
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
submitted_via: Optional[SubmissionSource] = None
|
||||||
|
pilot_email: Optional[str] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from typing import Optional
|
|||||||
|
|
||||||
|
|
||||||
class CircuitBase(BaseModel):
|
class CircuitBase(BaseModel):
|
||||||
local_flight_id: int
|
local_flight_id: Optional[int] = None
|
||||||
|
arrival_id: Optional[int] = None
|
||||||
circuit_timestamp: datetime
|
circuit_timestamp: datetime
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ from enum import Enum
|
|||||||
|
|
||||||
class DepartureStatus(str, Enum):
|
class DepartureStatus(str, Enum):
|
||||||
BOOKED_OUT = "BOOKED_OUT"
|
BOOKED_OUT = "BOOKED_OUT"
|
||||||
|
GROUND = "GROUND"
|
||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionSource(str, Enum):
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
PUBLIC = "PUBLIC"
|
||||||
|
|
||||||
|
|
||||||
class DepartureBase(BaseModel):
|
class DepartureBase(BaseModel):
|
||||||
registration: str
|
registration: str
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
@@ -48,7 +55,11 @@ class DepartureUpdate(BaseModel):
|
|||||||
callsign: Optional[str] = None
|
callsign: Optional[str] = None
|
||||||
pob: Optional[int] = None
|
pob: Optional[int] = None
|
||||||
out_to: Optional[str] = None
|
out_to: Optional[str] = None
|
||||||
|
status: Optional[DepartureStatus] = None
|
||||||
etd: Optional[datetime] = None
|
etd: Optional[datetime] = None
|
||||||
|
contact_dt: Optional[datetime] = None
|
||||||
|
departed_dt: Optional[datetime] = None
|
||||||
|
takeoff_dt: Optional[datetime] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -62,4 +73,12 @@ class Departure(DepartureBase):
|
|||||||
status: DepartureStatus
|
status: DepartureStatus
|
||||||
created_dt: datetime
|
created_dt: datetime
|
||||||
etd: Optional[datetime] = None
|
etd: Optional[datetime] = None
|
||||||
|
contact_dt: Optional[datetime] = None
|
||||||
departed_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,22 @@ class LocalFlightType(str, Enum):
|
|||||||
|
|
||||||
class LocalFlightStatus(str, Enum):
|
class LocalFlightStatus(str, Enum):
|
||||||
BOOKED_OUT = "BOOKED_OUT"
|
BOOKED_OUT = "BOOKED_OUT"
|
||||||
|
GROUND = "GROUND"
|
||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionSource(str, Enum):
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
PUBLIC = "PUBLIC"
|
||||||
|
|
||||||
|
|
||||||
class LocalFlightBase(BaseModel):
|
class LocalFlightBase(BaseModel):
|
||||||
registration: str
|
registration: str
|
||||||
type: Optional[str] = None # Aircraft type - optional, can be looked up later
|
type: Optional[str] = None # Aircraft type - optional, can be looked up later
|
||||||
@@ -61,7 +72,9 @@ class LocalFlightUpdate(BaseModel):
|
|||||||
duration: Optional[int] = None
|
duration: Optional[int] = None
|
||||||
status: Optional[LocalFlightStatus] = None
|
status: Optional[LocalFlightStatus] = None
|
||||||
etd: Optional[datetime] = None
|
etd: Optional[datetime] = None
|
||||||
|
contact_dt: Optional[datetime] = None
|
||||||
departed_dt: Optional[datetime] = None
|
departed_dt: Optional[datetime] = None
|
||||||
|
takeoff_dt: Optional[datetime] = None
|
||||||
circuits: Optional[int] = None
|
circuits: Optional[int] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
@@ -76,11 +89,15 @@ class LocalFlightInDBBase(LocalFlightBase):
|
|||||||
status: LocalFlightStatus
|
status: LocalFlightStatus
|
||||||
created_dt: datetime
|
created_dt: datetime
|
||||||
etd: Optional[datetime] = None
|
etd: Optional[datetime] = None
|
||||||
|
contact_dt: Optional[datetime] = None
|
||||||
departed_dt: Optional[datetime] = None
|
departed_dt: Optional[datetime] = None
|
||||||
|
takeoff_dt: Optional[datetime] = None
|
||||||
landed_dt: Optional[datetime] = None
|
landed_dt: Optional[datetime] = None
|
||||||
circuits: Optional[int] = None
|
circuits: Optional[int] = None
|
||||||
created_by: Optional[str] = None
|
created_by: Optional[str] = None
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
submitted_via: Optional[SubmissionSource] = None
|
||||||
|
pilot_email: Optional[str] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -215,3 +215,23 @@ class Aircraft(AircraftBase):
|
|||||||
|
|
||||||
class Config:
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================="
|
||||||
|
echo "Generating QR Code"
|
||||||
|
echo "========================================="
|
||||||
|
python3 /app/generate_qr.py
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
echo "Starting Application Server"
|
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}
|
REDIS_URL: ${REDIS_URL}
|
||||||
TAG: ${TAG}
|
TAG: ${TAG}
|
||||||
TOP_BAR_BASE_COLOR: ${TOP_BAR_BASE_COLOR}
|
TOP_BAR_BASE_COLOR: ${TOP_BAR_BASE_COLOR}
|
||||||
|
ALLOW_PUBLIC_BOOKING: ${ALLOW_PUBLIC_BOOKING}
|
||||||
ENVIRONMENT: production
|
ENVIRONMENT: production
|
||||||
WORKERS: "4"
|
WORKERS: "4"
|
||||||
ports:
|
ports:
|
||||||
@@ -35,6 +36,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./db-init:/db-init:ro # Mount CSV data for seeding
|
- ./db-init:/db-init:ro # Mount CSV data for seeding
|
||||||
|
- ./web/assets:/web/assets # Mount assets for QR code generation
|
||||||
networks:
|
networks:
|
||||||
- app_network
|
- app_network
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
@@ -48,7 +50,7 @@ services:
|
|||||||
cpus: '1'
|
cpus: '1'
|
||||||
memory: 1G
|
memory: 1G
|
||||||
healthcheck:
|
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
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./db-init:/db-init:ro # Mount CSV data for seeding
|
- ./db-init:/db-init:ro # Mount CSV data for seeding
|
||||||
|
- ./web/assets:/web/assets # Mount assets for QR code generation
|
||||||
networks:
|
networks:
|
||||||
- private_network
|
- private_network
|
||||||
- public_network
|
- public_network
|
||||||
|
|||||||
+528
-99
@@ -29,7 +29,9 @@
|
|||||||
⚙️ Admin
|
⚙️ Admin
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu" id="adminDropdownMenu">
|
<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="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>
|
<a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,7 +564,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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;">
|
<div id="circuits-section" style="display: none; margin-top: 2rem; border-top: 1px solid #ddd; padding-top: 1rem;">
|
||||||
<h3>✈️ Touch & Go Records</h3>
|
<h3>✈️ Touch & Go Records</h3>
|
||||||
<div id="circuits-list" style="margin-top: 1rem;">
|
<div id="circuits-list" style="margin-top: 1rem;">
|
||||||
@@ -1048,6 +1050,86 @@
|
|||||||
<!-- Success Notification -->
|
<!-- Success Notification -->
|
||||||
<div id="notification" class="notification"></div>
|
<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 -->
|
<!-- Timestamp Modal for Landing/Departure -->
|
||||||
<div id="timestampModal" class="modal">
|
<div id="timestampModal" class="modal">
|
||||||
<div class="modal-content" style="max-width: 400px;">
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
@@ -1494,6 +1576,12 @@
|
|||||||
openNewPPRModal();
|
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)
|
// Press 'l' to book out local flight (LOCAL type)
|
||||||
if (e.key === 'l' || e.key === 'L') {
|
if (e.key === 'l' || e.key === 'L') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1749,8 +1837,8 @@
|
|||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const bookedInToday = bookedInArrivals
|
const bookedInToday = bookedInArrivals
|
||||||
.filter(arrival => {
|
.filter(arrival => {
|
||||||
// Only include arrivals booked in today (created_dt) with BOOKED_IN status
|
// Only include arrivals booked in today (created_dt) with INBOUND, LOCAL, or CIRCUIT status
|
||||||
if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false;
|
if (!arrival.created_dt || !['INBOUND', 'LOCAL', 'CIRCUIT'].includes(arrival.status)) return false;
|
||||||
const bookedDate = arrival.created_dt.split('T')[0];
|
const bookedDate = arrival.created_dt.split('T')[0];
|
||||||
return bookedDate === today;
|
return bookedDate === today;
|
||||||
})
|
})
|
||||||
@@ -1772,7 +1860,7 @@
|
|||||||
document.getElementById('arrivals-loading').style.display = 'none';
|
document.getElementById('arrivals-loading').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load departures (LANDED status for PPR, BOOKED_OUT only for local flights)
|
// Load departures (LANDED status for PPR, GROUND/LOCAL for local flights)
|
||||||
async function loadDepartures() {
|
async function loadDepartures() {
|
||||||
document.getElementById('departures-loading').style.display = 'block';
|
document.getElementById('departures-loading').style.display = 'block';
|
||||||
document.getElementById('departures-table-content').style.display = 'none';
|
document.getElementById('departures-table-content').style.display = 'none';
|
||||||
@@ -1780,10 +1868,15 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Load PPR departures, local flight departures, and airport departures simultaneously
|
// 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/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')
|
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) {
|
if (!pprResponse.ok) {
|
||||||
@@ -1791,6 +1884,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allPPRs = await pprResponse.json();
|
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];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
// Filter for PPR departures with ETD today and LANDED status only
|
// Filter for PPR departures with ETD today and LANDED status only
|
||||||
@@ -1803,33 +1908,26 @@
|
|||||||
return etdDate === today;
|
return etdDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add local flights (BOOKED_OUT status - ready to go) - only those booked out today
|
// Add local flights (GROUND and LOCAL status - ready to go) - only those booked out today
|
||||||
if (localResponse.ok) {
|
const localDepartures = allLocalFlights
|
||||||
const localFlights = await localResponse.json();
|
.filter(flight => {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
// Only include flights booked out today (created_dt)
|
||||||
const localDepartures = localFlights
|
if (!flight.created_dt) return false;
|
||||||
.filter(flight => {
|
const createdDate = flight.created_dt.split('T')[0];
|
||||||
// Only include flights booked out today (created_dt)
|
return createdDate === today;
|
||||||
if (!flight.created_dt) return false;
|
})
|
||||||
const createdDate = flight.created_dt.split('T')[0];
|
.map(flight => ({
|
||||||
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 => ({
|
|
||||||
...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);
|
displayDepartures(departures);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2019,7 +2117,7 @@
|
|||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
row.innerHTML = `
|
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;">${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.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;">-</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</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) {
|
} else if (isDeparture) {
|
||||||
row.innerHTML = `
|
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;">${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.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;">${flight.out_to || '-'}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</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>`;
|
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||||
}
|
}
|
||||||
acType = flight.type;
|
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>`;
|
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)
|
// Calculate ETA: use departed_dt (actual departure) if available, otherwise etd (planned departure)
|
||||||
@@ -2351,22 +2449,17 @@
|
|||||||
pob = flight.pob || '-';
|
pob = flight.pob || '-';
|
||||||
fuel = '-';
|
fuel = '-';
|
||||||
|
|
||||||
// For circuits, add a circuit button
|
// Allow touch and go for all local flight types
|
||||||
let circuitButton = '';
|
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">
|
||||||
circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
T&G
|
||||||
T&G
|
</button>`;
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
${circuitButton}
|
${circuitButton}
|
||||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||||||
LAND
|
LAND
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Flight">
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
} else if (isBookedIn) {
|
} else if (isBookedIn) {
|
||||||
// Booked-in arrival display
|
// Booked-in arrival display
|
||||||
@@ -2376,7 +2469,7 @@
|
|||||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||||
}
|
}
|
||||||
acType = flight.type;
|
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
|
// Lookup airport name for in_from
|
||||||
let fromDisplay_temp = flight.in_from;
|
let fromDisplay_temp = flight.in_from;
|
||||||
@@ -2389,14 +2482,58 @@
|
|||||||
eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
|
eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
|
||||||
pob = flight.pob || '-';
|
pob = flight.pob || '-';
|
||||||
fuel = '-';
|
fuel = '-';
|
||||||
actionButtons = `
|
|
||||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
// Different action buttons based on status
|
||||||
LAND
|
if (flight.status === 'INBOUND') {
|
||||||
</button>
|
actionButtons = `
|
||||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Mark as Local">
|
||||||
CANCEL
|
LOCAL
|
||||||
</button>
|
</button>
|
||||||
`;
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'LOCAL') {
|
||||||
|
// Arrival in local area - show circuit and land buttons
|
||||||
|
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||||
|
T&G
|
||||||
|
</button>`;
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CIRCUIT')" title="Join Circuit">
|
||||||
|
CIRCUIT
|
||||||
|
</button>
|
||||||
|
${circuitButton}
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'CIRCUIT') {
|
||||||
|
// Arrival in circuit - show local, T&G and land buttons
|
||||||
|
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||||
|
T&G
|
||||||
|
</button>`;
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
|
||||||
|
LOCAL
|
||||||
|
</button>
|
||||||
|
${circuitButton}
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// PPR display
|
// PPR display
|
||||||
if (flight.ac_call && flight.ac_call.trim()) {
|
if (flight.ac_call && flight.ac_call.trim()) {
|
||||||
@@ -2421,9 +2558,6 @@
|
|||||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${flight.id})" title="Mark as Landed">
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${flight.id})" title="Mark as Landed">
|
||||||
LAND
|
LAND
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${flight.id}, 'CANCELED')" title="Cancel Arrival">
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2467,6 +2601,7 @@
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
const isLocal = flight.isLocalFlight;
|
const isLocal = flight.isLocalFlight;
|
||||||
const isDeparture = flight.isDeparture;
|
const isDeparture = flight.isDeparture;
|
||||||
|
const isArrival = flight.isArrival;
|
||||||
|
|
||||||
// Click handler that routes to correct modal
|
// Click handler that routes to correct modal
|
||||||
row.onclick = () => {
|
row.onclick = () => {
|
||||||
@@ -2474,6 +2609,8 @@
|
|||||||
openLocalFlightEditModal(flight.id);
|
openLocalFlightEditModal(flight.id);
|
||||||
} else if (isDeparture) {
|
} else if (isDeparture) {
|
||||||
openDepartureEditModal(flight.id);
|
openDepartureEditModal(flight.id);
|
||||||
|
} else if (isArrival) {
|
||||||
|
openArrivalEditModal(flight.id);
|
||||||
} else {
|
} else {
|
||||||
openPPRModal(flight.id);
|
openPPRModal(flight.id);
|
||||||
}
|
}
|
||||||
@@ -2495,7 +2632,7 @@
|
|||||||
} else {
|
} else {
|
||||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
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>`;
|
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) : '-');
|
etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||||||
pob = flight.pob || '-';
|
pob = flight.pob || '-';
|
||||||
@@ -2505,28 +2642,47 @@
|
|||||||
// Action buttons for local flight
|
// Action buttons for local flight
|
||||||
if (flight.status === 'BOOKED_OUT') {
|
if (flight.status === 'BOOKED_OUT') {
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
|
||||||
TAKE OFF
|
CONTACT
|
||||||
</button>
|
</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>
|
</button>
|
||||||
`;
|
`;
|
||||||
} else if (flight.status === 'DEPARTED') {
|
} else if (flight.status === 'DEPARTED') {
|
||||||
// For circuits, add a circuit button; for other flights, just show land button
|
// Allow touch and go for all local flight types
|
||||||
let circuitButton = '';
|
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||||
if (flight.flight_type === 'CIRCUITS') {
|
T&G
|
||||||
circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
</button>`;
|
||||||
T&G
|
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
${circuitButton}
|
${circuitButton}
|
||||||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||||||
LAND
|
LAND
|
||||||
</button>
|
</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>
|
</button>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@@ -2539,7 +2695,7 @@
|
|||||||
} else {
|
} else {
|
||||||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
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 || '-';
|
toDisplay = flight.out_to || '-';
|
||||||
if (flight.out_to && flight.out_to.length === 4 && /^[A-Z]{4}$/.test(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);
|
toDisplay = await getAirportDisplay(flight.out_to);
|
||||||
@@ -2552,11 +2708,20 @@
|
|||||||
// Action buttons for departure
|
// Action buttons for departure
|
||||||
if (flight.status === 'BOOKED_OUT') {
|
if (flight.status === 'BOOKED_OUT') {
|
||||||
actionButtons = `
|
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
|
TAKE OFF
|
||||||
</button>
|
</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>
|
</button>
|
||||||
`;
|
`;
|
||||||
} else if (flight.status === 'DEPARTED') {
|
} else if (flight.status === 'DEPARTED') {
|
||||||
@@ -2564,6 +2729,42 @@
|
|||||||
} else {
|
} else {
|
||||||
actionButtons = '<span style="color: #999;">-</span>';
|
actionButtons = '<span style="color: #999;">-</span>';
|
||||||
}
|
}
|
||||||
|
} else if (isArrival) {
|
||||||
|
// Arrival display
|
||||||
|
if (flight.callsign && flight.callsign.trim()) {
|
||||||
|
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||||||
|
} else {
|
||||||
|
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||||
|
}
|
||||||
|
typeIcon = '<span style="color: #228b22; font-weight: bold; font-size: 0.9em;" title="Arrival">A</span>';
|
||||||
|
toDisplay = `<i>Arrival from ${flight.in_from || '?'}</i>`;
|
||||||
|
etd = flight.eta ? formatTimeOnly(flight.eta) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||||||
|
pob = flight.pob || '-';
|
||||||
|
fuel = '-';
|
||||||
|
landedDt = flight.arrived_dt ? formatTimeOnly(flight.arrived_dt) : '-';
|
||||||
|
|
||||||
|
// Action buttons for arrival
|
||||||
|
if (flight.status === 'LOCAL') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; updateArrivalStatusFromTable(${flight.id}, 'CIRCUIT')" title="Rejoin Circuit">
|
||||||
|
REJOIN
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'CIRCUIT') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
|
||||||
|
LOCAL
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal(null, ${flight.id})" title="Record Touch & Go">
|
||||||
|
T&G
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; updateArrivalStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
actionButtons = '<span style="color: #999;">-</span>';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// PPR display
|
// PPR display
|
||||||
if (flight.ac_call && flight.ac_call.trim()) {
|
if (flight.ac_call && flight.ac_call.trim()) {
|
||||||
@@ -2585,9 +2786,6 @@
|
|||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
||||||
TAKE OFF
|
TAKE OFF
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${flight.id}, 'CANCELED')" title="Cancel Departure">
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2647,6 +2845,9 @@
|
|||||||
document.getElementById('ppr-form').reset();
|
document.getElementById('ppr-form').reset();
|
||||||
document.getElementById('ppr-id').value = '';
|
document.getElementById('ppr-id').value = '';
|
||||||
|
|
||||||
|
// Clear the unsaved aircraft flag
|
||||||
|
document.getElementById('ppr-form').removeAttribute('data-unsaved-aircraft');
|
||||||
|
|
||||||
// Set default ETA and ETD
|
// Set default ETA and ETD
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
|
const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
|
||||||
@@ -2898,6 +3099,12 @@
|
|||||||
} else if (status === 'DEPARTED') {
|
} else if (status === 'DEPARTED') {
|
||||||
modalTitle.textContent = 'Confirm Departure Time';
|
modalTitle.textContent = 'Confirm Departure Time';
|
||||||
submitBtn.textContent = '🛫 Confirm Departure';
|
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
|
// Set default timestamp to current time
|
||||||
@@ -2919,8 +3126,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Circuit modal functions
|
// Circuit modal functions
|
||||||
function showCircuitModal() {
|
function showCircuitModal(localFlightId = null, arrivalId = null) {
|
||||||
if (!currentLocalFlightId) return;
|
if (!localFlightId && !arrivalId) return;
|
||||||
|
|
||||||
|
// Set the current IDs
|
||||||
|
currentLocalFlightId = localFlightId;
|
||||||
|
currentArrivalId = arrivalId;
|
||||||
|
|
||||||
// Set default timestamp to current time
|
// Set default timestamp to current time
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -2937,13 +3148,15 @@
|
|||||||
function closeCircuitModal() {
|
function closeCircuitModal() {
|
||||||
document.getElementById('circuitModal').style.display = 'none';
|
document.getElementById('circuitModal').style.display = 'none';
|
||||||
document.getElementById('circuit-form').reset();
|
document.getElementById('circuit-form').reset();
|
||||||
|
currentLocalFlightId = null;
|
||||||
|
currentArrivalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Circuit form submission
|
// Circuit form submission
|
||||||
document.getElementById('circuit-form').addEventListener('submit', async function(e) {
|
document.getElementById('circuit-form').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!currentLocalFlightId || !accessToken) return;
|
if ((!currentLocalFlightId && !currentArrivalId) || !accessToken) return;
|
||||||
|
|
||||||
const circuitTimestampInput = document.getElementById('circuit-timestamp').value;
|
const circuitTimestampInput = document.getElementById('circuit-timestamp').value;
|
||||||
if (!circuitTimestampInput) {
|
if (!circuitTimestampInput) {
|
||||||
@@ -2956,15 +3169,23 @@
|
|||||||
const localDate = new Date(circuitTimestampInput);
|
const localDate = new Date(circuitTimestampInput);
|
||||||
const circuitTimestamp = localDate.toISOString();
|
const circuitTimestamp = localDate.toISOString();
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
circuit_timestamp: circuitTimestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the appropriate ID based on what we're tracking
|
||||||
|
if (currentLocalFlightId) {
|
||||||
|
requestBody.local_flight_id = currentLocalFlightId;
|
||||||
|
} else if (currentArrivalId) {
|
||||||
|
requestBody.arrival_id = currentArrivalId;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await authenticatedFetch('/api/v1/circuits/', {
|
const response = await authenticatedFetch('/api/v1/circuits/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(requestBody)
|
||||||
local_flight_id: currentLocalFlightId,
|
|
||||||
circuit_timestamp: circuitTimestamp
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -3063,6 +3284,9 @@
|
|||||||
|
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
// Auto-save any unsaved aircraft types
|
||||||
|
await autoSaveUnsavedAircraft(this);
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const pprData = {};
|
const pprData = {};
|
||||||
|
|
||||||
@@ -3479,6 +3703,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
|
// Update user role detection and UI visibility
|
||||||
async function updateUserRole() {
|
async function updateUserRole() {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@@ -3496,16 +3896,27 @@
|
|||||||
|
|
||||||
// Show user management in dropdown only for administrators
|
// Show user management in dropdown only for administrators
|
||||||
const userManagementDropdown = document.getElementById('user-management-dropdown');
|
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') {
|
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 {
|
} else {
|
||||||
userManagementDropdown.style.display = 'none';
|
if (userManagementDropdown) userManagementDropdown.style.display = 'none';
|
||||||
|
if (userAircraftDropdown) userAircraftDropdown.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user role:', error);
|
console.error('Error updating user role:', error);
|
||||||
// Hide user management by default on error
|
// Hide admin features by default on error
|
||||||
document.getElementById('user-management-dropdown').style.display = 'none';
|
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 +3965,29 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Local Flight (Book Out) Modal Functions
|
// 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-form').reset();
|
||||||
document.getElementById('local-flight-id').value = '';
|
document.getElementById('local-flight-id').value = '';
|
||||||
document.getElementById('local-flight-modal-title').textContent = 'Book Out';
|
document.getElementById('local-flight-modal-title').textContent = 'Book Out';
|
||||||
document.getElementById('local_flight_type').value = flightType;
|
document.getElementById('local_flight_type').value = flightType;
|
||||||
document.getElementById('localFlightModal').style.display = 'block';
|
document.getElementById('localFlightModal').style.display = 'block';
|
||||||
|
|
||||||
|
// Clear the unsaved aircraft flag
|
||||||
|
document.getElementById('local-flight-form').removeAttribute('data-unsaved-aircraft');
|
||||||
|
|
||||||
// Clear aircraft lookup results
|
// Clear aircraft lookup results
|
||||||
clearLocalAircraftLookup();
|
clearLocalAircraftLookup();
|
||||||
|
|
||||||
// Update destination field visibility based on flight type
|
// Update destination field visibility based on flight type
|
||||||
handleFlightTypeChange(flightType);
|
handleFlightTypeChange(flightType);
|
||||||
|
|
||||||
// Auto-focus on registration field
|
// Auto-focus on registration field and prefill if provided
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('local_registration').focus();
|
const regField = document.getElementById('local_registration');
|
||||||
|
regField.focus();
|
||||||
|
if (prefillReg) {
|
||||||
|
regField.value = prefillReg;
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3578,6 +3996,9 @@
|
|||||||
document.getElementById('book-in-id').value = '';
|
document.getElementById('book-in-id').value = '';
|
||||||
document.getElementById('bookInModal').style.display = 'block';
|
document.getElementById('bookInModal').style.display = 'block';
|
||||||
|
|
||||||
|
// Clear the unsaved aircraft flag
|
||||||
|
document.getElementById('book-in-form').removeAttribute('data-unsaved-aircraft');
|
||||||
|
|
||||||
// Clear aircraft lookup results
|
// Clear aircraft lookup results
|
||||||
clearBookInAircraftLookup();
|
clearBookInAircraftLookup();
|
||||||
clearBookInArrivalAirportLookup();
|
clearBookInArrivalAirportLookup();
|
||||||
@@ -3596,6 +4017,9 @@
|
|||||||
document.getElementById('overflight-id').value = '';
|
document.getElementById('overflight-id').value = '';
|
||||||
document.getElementById('overflightModal').style.display = 'block';
|
document.getElementById('overflightModal').style.display = 'block';
|
||||||
|
|
||||||
|
// Clear the unsaved aircraft flag
|
||||||
|
document.getElementById('overflight-form').removeAttribute('data-unsaved-aircraft');
|
||||||
|
|
||||||
// Clear aircraft lookup results
|
// Clear aircraft lookup results
|
||||||
clearOverflightAircraftLookup();
|
clearOverflightAircraftLookup();
|
||||||
clearOverflightDepartureAirportLookup();
|
clearOverflightDepartureAirportLookup();
|
||||||
@@ -3994,14 +4418,10 @@
|
|||||||
|
|
||||||
document.getElementById('local-flight-edit-title').textContent = `${flight.registration} - ${flight.flight_type}`;
|
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');
|
const circuitsSection = document.getElementById('circuits-section');
|
||||||
if (flight.flight_type === 'CIRCUITS') {
|
circuitsSection.style.display = 'block';
|
||||||
circuitsSection.style.display = 'block';
|
loadCircuitsDisplay(flight.id);
|
||||||
loadCircuitsDisplay(flight.id);
|
|
||||||
} else {
|
|
||||||
circuitsSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('localFlightEditModal').style.display = 'block';
|
document.getElementById('localFlightEditModal').style.display = 'block';
|
||||||
|
|
||||||
@@ -4187,8 +4607,8 @@
|
|||||||
// Show/hide quick action buttons based on status
|
// Show/hide quick action buttons based on status
|
||||||
const landedBtn = document.getElementById('arrival-btn-landed');
|
const landedBtn = document.getElementById('arrival-btn-landed');
|
||||||
const cancelBtn = document.getElementById('arrival-btn-cancel');
|
const cancelBtn = document.getElementById('arrival-btn-cancel');
|
||||||
landedBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none';
|
landedBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none';
|
||||||
cancelBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none';
|
cancelBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none';
|
||||||
|
|
||||||
// Show modal
|
// Show modal
|
||||||
document.getElementById('arrivalEditModal').style.display = 'block';
|
document.getElementById('arrivalEditModal').style.display = 'block';
|
||||||
@@ -4529,6 +4949,9 @@
|
|||||||
|
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
// Auto-save any unsaved aircraft types
|
||||||
|
await autoSaveUnsavedAircraft(this);
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const flightType = formData.get('flight_type');
|
const flightType = formData.get('flight_type');
|
||||||
const flightData = {};
|
const flightData = {};
|
||||||
@@ -4612,6 +5035,9 @@
|
|||||||
|
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
// Auto-save any unsaved aircraft types
|
||||||
|
await autoSaveUnsavedAircraft(this);
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const arrivalData = {};
|
const arrivalData = {};
|
||||||
|
|
||||||
@@ -4691,6 +5117,9 @@
|
|||||||
|
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
// Auto-save any unsaved aircraft types
|
||||||
|
await autoSaveUnsavedAircraft(this);
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const overflightData = {};
|
const overflightData = {};
|
||||||
|
|
||||||
|
|||||||
+5695
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) {
|
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Auto-save any unsaved aircraft types
|
||||||
|
await autoSaveUnsavedAircraft(this);
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const pprData = {};
|
const pprData = {};
|
||||||
|
|
||||||
|
|||||||
+45
-1
@@ -178,6 +178,35 @@
|
|||||||
left: 28px;
|
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 styles */
|
||||||
.santa-hat {
|
.santa-hat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -357,6 +386,10 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<img src="assets/logo.png" alt="EGFH Logo" class="left-image">
|
<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>
|
<h1>Flight Information</h1>
|
||||||
<img src="assets/flightImg.png" alt="EGFH Logo" class="right-image">
|
<img src="assets/flightImg.png" alt="EGFH Logo" class="right-image">
|
||||||
</header>
|
</header>
|
||||||
@@ -666,7 +699,7 @@
|
|||||||
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`;
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`;
|
||||||
sortTime = arrival.landed_dt;
|
sortTime = arrival.landed_dt;
|
||||||
} else {
|
} else {
|
||||||
// Show ETA if BOOKED_IN
|
// Show ETA if INBOUND
|
||||||
const time = convertToLocalTime(arrival.eta);
|
const time = convertToLocalTime(arrival.eta);
|
||||||
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">IN AIR</span></div>`;
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">IN AIR</span></div>`;
|
||||||
sortTime = arrival.eta;
|
sortTime = arrival.eta;
|
||||||
@@ -847,11 +880,22 @@
|
|||||||
return typeMap[flightType] || flightType;
|
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
|
// Load data on page load
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
// Initialize Christmas mode
|
// Initialize Christmas mode
|
||||||
initChristmasMode();
|
initChristmasMode();
|
||||||
|
|
||||||
|
// Load booking QR code
|
||||||
|
generateBookingQR();
|
||||||
|
|
||||||
loadArrivals();
|
loadArrivals();
|
||||||
loadDepartures();
|
loadDepartures();
|
||||||
|
|
||||||
|
|||||||
+137
-4
@@ -163,15 +163,26 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
const resultsDiv = document.getElementById(resultsId);
|
const resultsDiv = document.getElementById(resultsId);
|
||||||
|
|
||||||
if (config.isAircraft) {
|
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) {
|
if (!results || results.length === 0) {
|
||||||
// Format the aircraft registration and auto-populate
|
// Format the aircraft registration
|
||||||
const formatted = formatAircraftRegistration(searchTerm);
|
const formatted = formatAircraftRegistration(searchTerm);
|
||||||
const field = document.getElementById(fieldId);
|
const field = document.getElementById(fieldId);
|
||||||
if (field) {
|
if (field) {
|
||||||
field.value = formatted;
|
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) {
|
} else if (results.length === 1) {
|
||||||
// Single match - auto-populate
|
// Single match - auto-populate
|
||||||
const aircraft = results[0];
|
const aircraft = results[0];
|
||||||
@@ -183,7 +194,14 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
|
|
||||||
// Auto-populate the form fields
|
// Auto-populate the form fields
|
||||||
const field = document.getElementById(fieldId);
|
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
|
// Also populate type field
|
||||||
let typeFieldId;
|
let typeFieldId;
|
||||||
@@ -208,6 +226,14 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
Multiple matches found (${results.length}) - please be more specific
|
Multiple matches found (${results.length}) - please be more specific
|
||||||
</div>
|
</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 {
|
} else {
|
||||||
// Airport lookup: show list of options with keyboard navigation
|
// Airport lookup: show list of options with keyboard navigation
|
||||||
@@ -501,3 +527,110 @@ function selectBookInAircraft(registration) {
|
|||||||
function selectBookInArrivalAirport(icaoCode) {
|
function selectBookInArrivalAirport(icaoCode) {
|
||||||
lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', 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