Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9867156334 | |||
| eb2321ef40 |
@@ -18,27 +18,40 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add GROUND and LOCAL to local_flights status enum
|
# Add GROUND and LOCAL to local_flights status enum
|
||||||
op.execute("ALTER TABLE local_flights MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CIRCUIT','LANDED','CANCELLED')")
|
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
|
# 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('contact_dt', sa.DateTime(), nullable=True))
|
||||||
op.add_column('local_flights', sa.Column('takeoff_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
|
# Add GROUND and ARRIVED to arrivals status enum
|
||||||
op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','GROUND','ARRIVED','CANCELLED')")
|
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
|
# Add timestamp column to arrivals
|
||||||
op.add_column('arrivals', sa.Column('arrived_dt', sa.DateTime(), nullable=True))
|
op.add_column('arrivals', sa.Column('arrived_dt', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
# Add GROUND and LOCAL to departures status enum
|
# Add GROUND and LOCAL to departures status enum
|
||||||
op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CANCELLED')")
|
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
|
# Add timestamp columns to departures
|
||||||
op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True))
|
op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True))
|
||||||
op.add_column('departures', sa.Column('takeoff_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:
|
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
|
# Remove timestamp columns from departures
|
||||||
op.drop_column('departures', 'takeoff_dt')
|
op.drop_column('departures', 'takeoff_dt')
|
||||||
op.drop_column('departures', 'contact_dt')
|
op.drop_column('departures', 'contact_dt')
|
||||||
@@ -46,12 +59,18 @@ def downgrade() -> None:
|
|||||||
# Remove GROUND and LOCAL from departures status enum
|
# Remove GROUND and LOCAL from departures status enum
|
||||||
op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','DEPARTED','CANCELLED')")
|
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
|
# Remove timestamp column from arrivals
|
||||||
op.drop_column('arrivals', 'arrived_dt')
|
op.drop_column('arrivals', 'arrived_dt')
|
||||||
|
|
||||||
# Remove GROUND and ARRIVED from arrivals status enum
|
# Remove GROUND and ARRIVED from arrivals status enum
|
||||||
op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','CANCELLED')")
|
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
|
# Remove timestamp columns from local_flights
|
||||||
op.drop_column('local_flights', 'takeoff_dt')
|
op.drop_column('local_flights', 'takeoff_dt')
|
||||||
op.drop_column('local_flights', 'contact_dt')
|
op.drop_column('local_flights', 'contact_dt')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async def public_book_local_flight(
|
|||||||
notes=flight_in.notes,
|
notes=flight_in.notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT")
|
flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||||
|
|
||||||
# Update with submission source and pilot email
|
# Update with submission source and pilot email
|
||||||
db.query(type(flight)).filter(type(flight).id == flight.id).update({
|
db.query(type(flight)).filter(type(flight).id == flight.id).update({
|
||||||
@@ -181,11 +181,10 @@ async def public_book_arrival(
|
|||||||
notes=arrival_in.notes,
|
notes=arrival_in.notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT")
|
arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||||
|
|
||||||
# Update with submission source and pilot email
|
# Update with pilot email
|
||||||
db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({
|
db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({
|
||||||
type(arrival).submitted_via: ArrivalSubmissionSource.PUBLIC,
|
|
||||||
type(arrival).pilot_email: arrival_in.pilot_email,
|
type(arrival).pilot_email: arrival_in.pilot_email,
|
||||||
})
|
})
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ class CRUDArrival:
|
|||||||
query = db.query(Arrival)
|
query = db.query(Arrival)
|
||||||
|
|
||||||
if status:
|
if 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)
|
query = query.filter(Arrival.status == status)
|
||||||
|
|
||||||
if date_from:
|
if 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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ class CRUDLocalFlight:
|
|||||||
query = db.query(LocalFlight)
|
query = db.query(LocalFlight)
|
||||||
|
|
||||||
if status:
|
if 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)
|
query = query.filter(LocalFlight.status == status)
|
||||||
|
|
||||||
if flight_type:
|
if 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()
|
||||||
|
|||||||
@@ -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,9 +1,7 @@
|
|||||||
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):
|
class SubmissionSource(str, Enum):
|
||||||
@@ -13,8 +11,14 @@ class SubmissionSource(str, 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"
|
GROUND = "GROUND"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
ARRIVED = "ARRIVED"
|
ARRIVED = "ARRIVED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
@@ -28,7 +32,7 @@ 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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class LocalFlightStatus(str, Enum):
|
|||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
LOCAL = "LOCAL"
|
LOCAL = "LOCAL"
|
||||||
CIRCUIT = "CIRCUIT"
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ 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"
|
GROUND = "GROUND"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
ARRIVED = "ARRIVED"
|
ARRIVED = "ARRIVED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
@@ -46,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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class LocalFlightStatus(str, Enum):
|
|||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
LOCAL = "LOCAL"
|
LOCAL = "LOCAL"
|
||||||
CIRCUIT = "CIRCUIT"
|
CIRCUIT = "CIRCUIT"
|
||||||
|
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
|
||||||
|
CIRCUIT_BASE = "CIRCUIT_BASE"
|
||||||
|
CIRCUIT_FINAL = "CIRCUIT_FINAL"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|||||||
+110
-13
@@ -1837,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;
|
||||||
})
|
})
|
||||||
@@ -1860,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';
|
||||||
@@ -1908,7 +1908,7 @@
|
|||||||
return etdDate === today;
|
return etdDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add local flights (BOOKED_OUT, GROUND, and LOCAL status - ready to go) - only those booked out today
|
// Add local flights (GROUND and LOCAL status - ready to go) - only those booked out today
|
||||||
const localDepartures = allLocalFlights
|
const localDepartures = allLocalFlights
|
||||||
.filter(flight => {
|
.filter(flight => {
|
||||||
// Only include flights booked out today (created_dt)
|
// Only include flights booked out today (created_dt)
|
||||||
@@ -2482,6 +2482,49 @@
|
|||||||
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 = '-';
|
||||||
|
|
||||||
|
// Different action buttons based on status
|
||||||
|
if (flight.status === 'INBOUND') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Mark as Local">
|
||||||
|
LOCAL
|
||||||
|
</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 = `
|
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">
|
<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
|
LAND
|
||||||
@@ -2490,6 +2533,7 @@
|
|||||||
CANCEL
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// PPR display
|
// PPR display
|
||||||
if (flight.ac_call && flight.ac_call.trim()) {
|
if (flight.ac_call && flight.ac_call.trim()) {
|
||||||
@@ -2557,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 = () => {
|
||||||
@@ -2564,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);
|
||||||
}
|
}
|
||||||
@@ -2682,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()) {
|
||||||
@@ -3043,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();
|
||||||
@@ -3061,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) {
|
||||||
@@ -3080,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) {
|
||||||
@@ -4510,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';
|
||||||
|
|||||||
+260
-108
@@ -70,6 +70,23 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aircraft-item.local-flight,
|
||||||
|
.aircraft-item.circuit {
|
||||||
|
background-color: #ffcccc; /* light red */
|
||||||
|
}
|
||||||
|
|
||||||
|
.aircraft-item.departure {
|
||||||
|
background-color: #ffffcc; /* light yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.aircraft-item.inbound {
|
||||||
|
background-color: #ccccff; /* light blue */
|
||||||
|
}
|
||||||
|
|
||||||
|
.aircraft-item.overflight {
|
||||||
|
background-color: #ccffcc; /* light green */
|
||||||
|
}
|
||||||
|
|
||||||
.aircraft-info {
|
.aircraft-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -158,8 +175,22 @@
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-btn:active {
|
.status-btn.small-btn {
|
||||||
transform: scale(0.95);
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-btn.active-position {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-btn.active-position:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
border-color: #1e7e34;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@@ -213,9 +244,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="atc-container">
|
<div class="atc-container">
|
||||||
<!-- Row 1: Awaiting Departure -->
|
<!-- Row 1: Apron -->
|
||||||
<div class="atc-section">
|
<div class="atc-section">
|
||||||
<h2>🛫 Awaiting Departure <span class="count" id="departing-count">0</span></h2>
|
<h2>🛫 Apron <span class="count" id="departing-count">0</span></h2>
|
||||||
<div class="aircraft-list" id="departing-list">
|
<div class="aircraft-list" id="departing-list">
|
||||||
<div class="no-aircraft">No departing aircraft</div>
|
<div class="no-aircraft">No departing aircraft</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,15 +278,15 @@
|
|||||||
|
|
||||||
<!-- Row 2: Circuit Traffic -->
|
<!-- Row 2: Circuit Traffic -->
|
||||||
<div class="atc-section">
|
<div class="atc-section">
|
||||||
<h2>🔄 Circuits <span class="count" id="circuit-count">0</span></h2>
|
<h2>🔄 Circuit Traffic <span class="count" id="circuit-count">0</span></h2>
|
||||||
<div class="aircraft-list" id="circuit-list">
|
<div class="aircraft-list" id="circuit-list">
|
||||||
<div class="no-aircraft">No aircraft in circuit</div>
|
<div class="no-aircraft">No aircraft in circuit</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 2: Pending PPRs -->
|
<!-- Row 2: Today's PPRs -->
|
||||||
<div class="atc-section">
|
<div class="atc-section">
|
||||||
<h2>📝 Pending PPRs <span class="count" id="pending-ppr-count">0</span></h2>
|
<h2>📝 Today's PPRs <span class="count" id="pending-ppr-count">0</span></h2>
|
||||||
<div class="aircraft-list" id="pending-ppr-list">
|
<div class="aircraft-list" id="pending-ppr-list">
|
||||||
<div class="no-aircraft">No pending PPRs</div>
|
<div class="no-aircraft">No pending PPRs</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1827,8 +1858,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;
|
||||||
})
|
})
|
||||||
@@ -1850,7 +1881,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';
|
||||||
@@ -1896,7 +1927,7 @@
|
|||||||
return etdDate === today;
|
return etdDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add local flights (BOOKED_OUT, GROUND, and LOCAL status - ready to go) - only those booked out today
|
// Add local flights (GROUND and LOCAL status - ready to go) - only those booked out today
|
||||||
const localDepartures = allLocalFlights
|
const localDepartures = allLocalFlights
|
||||||
.filter(flight => {
|
.filter(flight => {
|
||||||
// Only include flights booked out today (created_dt)
|
// Only include flights booked out today (created_dt)
|
||||||
@@ -1971,6 +2002,7 @@
|
|||||||
for (const flight of overflights) {
|
for (const flight of overflights) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.style.cursor = 'pointer';
|
row.style.cursor = 'pointer';
|
||||||
|
row.style.backgroundColor = '#ccffcc';
|
||||||
row.onclick = () => {
|
row.onclick = () => {
|
||||||
openOverflightEditModal(flight.id);
|
openOverflightEditModal(flight.id);
|
||||||
};
|
};
|
||||||
@@ -2131,7 +2163,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load booked out aircraft (BOOKED_OUT status for departures and local flights)
|
// Load booked out aircraft (BOOKED_OUT status for departures only)
|
||||||
async function loadParked() {
|
async function loadParked() {
|
||||||
document.getElementById('parked-loading').style.display = 'block';
|
document.getElementById('parked-loading').style.display = 'block';
|
||||||
document.getElementById('parked-table-content').style.display = 'none';
|
document.getElementById('parked-table-content').style.display = 'none';
|
||||||
@@ -2470,6 +2502,49 @@
|
|||||||
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 = '-';
|
||||||
|
|
||||||
|
// Different action buttons based on status
|
||||||
|
if (flight.status === 'INBOUND') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Mark as Local">
|
||||||
|
LOCAL
|
||||||
|
</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 = `
|
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">
|
<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
|
LAND
|
||||||
@@ -2478,6 +2553,7 @@
|
|||||||
CANCEL
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// PPR display
|
// PPR display
|
||||||
if (flight.ac_call && flight.ac_call.trim()) {
|
if (flight.ac_call && flight.ac_call.trim()) {
|
||||||
@@ -3009,8 +3085,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();
|
||||||
@@ -3027,13 +3107,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) {
|
||||||
@@ -3046,15 +3128,21 @@
|
|||||||
const localDate = new Date(circuitTimestampInput);
|
const localDate = new Date(circuitTimestampInput);
|
||||||
const circuitTimestamp = localDate.toISOString();
|
const circuitTimestamp = localDate.toISOString();
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
circuit_timestamp: circuitTimestamp
|
||||||
|
};
|
||||||
|
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) {
|
||||||
@@ -3064,6 +3152,14 @@
|
|||||||
|
|
||||||
const circuit = await response.json();
|
const circuit = await response.json();
|
||||||
showNotification(`✈️ Circuit recorded at ${formatTimeOnly(circuit.circuit_timestamp)}`);
|
showNotification(`✈️ Circuit recorded at ${formatTimeOnly(circuit.circuit_timestamp)}`);
|
||||||
|
|
||||||
|
// Update flight status to CIRCUIT after successful T&G recording
|
||||||
|
if (currentLocalFlightId) {
|
||||||
|
await updateLocalFlightStatusFromTable(currentLocalFlightId, 'CIRCUIT');
|
||||||
|
} else if (currentArrivalId) {
|
||||||
|
await updateArrivalStatusFromTable(currentArrivalId, 'CIRCUIT');
|
||||||
|
}
|
||||||
|
|
||||||
closeCircuitModal();
|
closeCircuitModal();
|
||||||
|
|
||||||
// Refresh ATC display
|
// Refresh ATC display
|
||||||
@@ -4476,8 +4572,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';
|
||||||
@@ -4602,41 +4698,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status from table for departures
|
// Update status from table for arrivals
|
||||||
async function updateDepartureStatusFromTable(departureId, status) {
|
|
||||||
if (!accessToken) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/v1/departures/${departureId}/status`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ status: status })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to update status');
|
|
||||||
|
|
||||||
loadPPRs(); // Refresh display
|
|
||||||
showNotification(`Departure marked as ${status.toLowerCase()}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating status:', error);
|
|
||||||
showNotification('Error updating departure status', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status from table for booked-in arrivals
|
|
||||||
async function updateArrivalStatusFromTable(arrivalId, status) {
|
async function updateArrivalStatusFromTable(arrivalId, status) {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
// Show confirmation for cancel actions
|
|
||||||
if (status === 'CANCELLED') {
|
|
||||||
if (!confirm('Are you sure you want to cancel this arrival? This action cannot be easily undone.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/arrivals/${arrivalId}/status`, {
|
const response = await fetch(`/api/v1/arrivals/${arrivalId}/status`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -4649,7 +4714,7 @@
|
|||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to update status');
|
if (!response.ok) throw new Error('Failed to update status');
|
||||||
|
|
||||||
loadArrivals(); // Refresh arrivals table
|
loadATCAircraft(); // Refresh display
|
||||||
showNotification(`Arrival marked as ${status.toLowerCase()}`);
|
showNotification(`Arrival marked as ${status.toLowerCase()}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating status:', error);
|
console.error('Error updating status:', error);
|
||||||
@@ -5211,13 +5276,13 @@
|
|||||||
buttonTitle = 'Mark as Local';
|
buttonTitle = 'Mark as Local';
|
||||||
clickType = 'departure';
|
clickType = 'departure';
|
||||||
}
|
}
|
||||||
|
const itemClass = isLocal ? 'local-flight' : 'departure';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${clickType}')">
|
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${clickType}')">
|
||||||
<div class="aircraft-info">
|
<div class="aircraft-info">
|
||||||
<div class="aircraft-reg">${reg}</div>
|
<div class="aircraft-reg">${reg}</div>
|
||||||
<div class="aircraft-details">${type}${dest ? ` → ${dest}` : ` (Local)`}</div>
|
<div class="aircraft-details">${type}${dest ? ` → ${dest}` : ` Local Flight`}</div>
|
||||||
<div class="aircraft-time">${ac.etd ? formatTimeOnly(ac.etd) : ''}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="status-btn" onclick="${takeoffOnclick}" title="${buttonTitle}">${buttonText}</button>
|
<button class="status-btn" onclick="${takeoffOnclick}" title="${buttonTitle}">${buttonText}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -5230,12 +5295,16 @@
|
|||||||
try {
|
try {
|
||||||
const response = await Promise.all([
|
const response = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/overflights/?status=ACTIVE&limit=1000')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let locals = [];
|
let locals = [];
|
||||||
if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true }));
|
if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true }));
|
||||||
if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true })));
|
if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true })));
|
||||||
|
if (response[2].ok) locals = locals.concat((await response[2].json()).map(a => ({ ...a, isArrival: true })));
|
||||||
|
if (response[3].ok) locals = locals.concat((await response[3].json()).map(o => ({ ...o, isOverflight: true })));
|
||||||
|
|
||||||
displayLocalAircraft(locals);
|
displayLocalAircraft(locals);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -5255,26 +5324,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = aircraft.map(ac => {
|
container.innerHTML = aircraft.map(ac => {
|
||||||
const reg = ac.registration || ac.ac_reg;
|
const reg = ac.registration || ac.ac_reg || ac.callsign || '-';
|
||||||
const type = ac.type || ac.ac_type;
|
const type = ac.type || ac.ac_type || ac.aircraft_type || '';
|
||||||
const dest = ac.out_to;
|
const dest = ac.out_to;
|
||||||
const isDeparture = ac.isDeparture;
|
const isDeparture = ac.isDeparture;
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
if (isDeparture) {
|
if (isDeparture) {
|
||||||
// Departure in LOCAL status - show QSY button
|
// Departure in LOCAL status - show QSY and REJOIN buttons
|
||||||
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>`;
|
buttons = `
|
||||||
} else if (ac.isLocalFlight) {
|
<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>
|
||||||
// Local flight in LOCAL status - show REJOIN button
|
<button class="status-btn" onclick="event.stopPropagation(); updateDepartureStatusFromTable('${ac.id}', 'CIRCUIT')">REJOIN</button>
|
||||||
buttons = `<button class="status-btn" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable('${ac.id}', 'CIRCUIT')">REJOIN</button>`;
|
`;
|
||||||
|
} else if (ac.isLocalFlight || ac.isArrival) {
|
||||||
|
// Local flight or arrival in LOCAL status - show REJOIN button
|
||||||
|
buttons = `<button class="status-btn" onclick="event.stopPropagation(); ${ac.isArrival ? `updateArrivalStatusFromTable('${ac.id}', 'CIRCUIT')` : `updateLocalFlightStatusFromTable('${ac.id}', 'CIRCUIT')`}">REJOIN</button>`;
|
||||||
|
} else if (ac.isOverflight) {
|
||||||
|
// Overflight in ACTIVE status - show QSY button
|
||||||
|
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentOverflightId = '${ac.id}'; showOverflightQSYModal()">QSY</button>`;
|
||||||
}
|
}
|
||||||
|
const itemClass = isDeparture ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
|
||||||
|
const detailsText = isDeparture ? `${type}${dest ? ` → ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} → ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` → ${dest}` : ` Local Flight`}`));
|
||||||
|
const entityType = isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local'));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${isDeparture ? 'departure' : 'local'}')">
|
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
|
||||||
<div class="aircraft-info">
|
<div class="aircraft-info">
|
||||||
<div class="aircraft-reg">${reg}</div>
|
<div class="aircraft-reg">${reg}</div>
|
||||||
<div class="aircraft-details">${type}${dest ? ` → ${dest}` : ` (Local)`}</div>
|
<div class="aircraft-details">${detailsText}</div>
|
||||||
<div class="aircraft-time">${formatTimeOnly(ac.created_dt)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 0.5rem;">
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
${buttons}
|
${buttons}
|
||||||
@@ -5289,7 +5366,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await Promise.all([
|
const response = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
authenticatedFetch('/api/v1/arrivals/?status=BOOKED_IN&limit=1000')
|
authenticatedFetch('/api/v1/arrivals/?status=INBOUND&limit=1000')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const pprs = response[0].ok ? await response[0].json() : [];
|
const pprs = response[0].ok ? await response[0].json() : [];
|
||||||
@@ -5322,14 +5399,19 @@
|
|||||||
const from = ac.in_from;
|
const from = ac.in_from;
|
||||||
const eta = ac.eta;
|
const eta = ac.eta;
|
||||||
|
|
||||||
|
let buttons = '';
|
||||||
|
if (ac.isArrival) {
|
||||||
|
// Arrival in INBOUND status - show LOCAL button
|
||||||
|
buttons = `<button class="status-btn" onclick="event.stopPropagation(); updateArrivalStatusFromTable('${ac.id}', 'LOCAL')">→ LOCAL</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${ac.isArrival ? 'arrival' : 'ppr'}')">
|
<div class="aircraft-item inbound" onclick="handleATCClick('${ac.id}', '${ac.isArrival ? 'arrival' : 'ppr'}')">
|
||||||
<div class="aircraft-info">
|
<div class="aircraft-info">
|
||||||
<div class="aircraft-reg">${reg}</div>
|
<div class="aircraft-reg">${reg}</div>
|
||||||
<div class="aircraft-details">${type} from ${from || '?'}</div>
|
<div class="aircraft-details">${type} from ${from || 'Local Flight'}</div>
|
||||||
<div class="aircraft-time">${eta ? formatTimeOnly(eta) : ''}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="aircraft-status status-inbound">IB</div>
|
${buttons ? `<div style="display: flex; gap: 0.5rem;">${buttons}</div>` : '<div class="aircraft-status status-inbound">IB</div>'}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -5340,12 +5422,14 @@
|
|||||||
try {
|
try {
|
||||||
const response = await Promise.all([
|
const response = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000&flight_type=CIRCUITS'),
|
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000&flight_type=CIRCUITS'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=CIRCUIT&limit=1000')
|
authenticatedFetch('/api/v1/local-flights/?status=CIRCUIT&limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/arrivals/?status=CIRCUIT&limit=1000')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let circuits = [];
|
let circuits = [];
|
||||||
if (response[0].ok) circuits = await response[0].json();
|
if (response[0].ok) circuits = circuits.concat(await response[0].json());
|
||||||
if (response[1].ok) circuits = circuits.concat(await response[1].json());
|
if (response[1].ok) circuits = circuits.concat((await response[1].json()).map(l => ({ ...l, circuitStatus: getCircuitStatus(l.status) })));
|
||||||
|
if (response[2].ok) circuits = circuits.concat((await response[2].json()).map(a => ({ ...a, isArrival: true, circuitStatus: getCircuitStatus(a.status) })));
|
||||||
|
|
||||||
displayCircuitAircraft(circuits);
|
displayCircuitAircraft(circuits);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -5353,10 +5437,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to determine circuit status from API response
|
||||||
|
function getCircuitStatus(status) {
|
||||||
|
if (status === 'CIRCUIT_DOWNWIND') return 'DOWNWIND';
|
||||||
|
if (status === 'CIRCUIT_BASE') return 'BASE';
|
||||||
|
if (status === 'CIRCUIT_FINAL') return 'FINAL';
|
||||||
|
return 'CIRCUIT';
|
||||||
|
}
|
||||||
|
|
||||||
function displayCircuitAircraft(aircraft) {
|
function displayCircuitAircraft(aircraft) {
|
||||||
const container = document.getElementById('circuit-list');
|
const container = document.getElementById('circuit-list');
|
||||||
const countEl = document.getElementById('circuit-count');
|
const countEl = document.getElementById('circuit-count');
|
||||||
|
|
||||||
|
// Define status order for sorting
|
||||||
|
const statusOrder = {
|
||||||
|
'CIRCUIT': 1,
|
||||||
|
'DOWNWIND': 2,
|
||||||
|
'BASE': 3,
|
||||||
|
'FINAL': 4
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort aircraft by circuit status
|
||||||
|
aircraft.sort((a, b) => {
|
||||||
|
const aStatus = a.circuitStatus || 'CIRCUIT';
|
||||||
|
const bStatus = b.circuitStatus || 'CIRCUIT';
|
||||||
|
return statusOrder[aStatus] - statusOrder[bStatus];
|
||||||
|
});
|
||||||
|
|
||||||
countEl.textContent = aircraft.length;
|
countEl.textContent = aircraft.length;
|
||||||
|
|
||||||
if (aircraft.length === 0) {
|
if (aircraft.length === 0) {
|
||||||
@@ -5364,20 +5471,51 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = aircraft.map(ac => `
|
container.innerHTML = aircraft.map(ac => {
|
||||||
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', 'local')">
|
const isArrival = ac.isArrival;
|
||||||
|
const entityType = isArrival ? 'arrival' : 'local';
|
||||||
|
const updateFunction = isArrival ? 'updateArrivalStatusFromTable' : 'updateLocalFlightStatusFromTable';
|
||||||
|
const landFunction = isArrival ? `${updateFunction}('${ac.id}', 'LANDED')` : `showTimestampModal('LANDED', ${ac.id}, true)`;
|
||||||
|
const circuitStatus = ac.circuitStatus || 'CIRCUIT';
|
||||||
|
|
||||||
|
let buttons = `
|
||||||
|
<button class="status-btn" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'LOCAL')">LOCAL</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Circuit position buttons - show all, highlight current
|
||||||
|
const downwindClass = circuitStatus === 'DOWNWIND' ? 'active-position' : '';
|
||||||
|
const baseClass = circuitStatus === 'BASE' ? 'active-position' : '';
|
||||||
|
const finalClass = circuitStatus === 'FINAL' ? 'active-position' : '';
|
||||||
|
|
||||||
|
buttons += `<button class="status-btn small-btn ${downwindClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_DOWNWIND')" title="Downwind">D</button>`;
|
||||||
|
buttons += `<button class="status-btn small-btn ${baseClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_BASE')" title="Base">B</button>`;
|
||||||
|
buttons += `<button class="status-btn small-btn ${finalClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_FINAL')" title="Final">F</button>`;
|
||||||
|
|
||||||
|
// Show T&G for both local flights and arrivals - resets to CIRCUIT
|
||||||
|
const tgFunction = isArrival
|
||||||
|
? `currentArrivalId = '${ac.id}'; showCircuitModal(null, '${ac.id}')`
|
||||||
|
: `currentLocalFlightId = '${ac.id}'; showCircuitModal('${ac.id}')`;
|
||||||
|
buttons += `<button class="status-btn" onclick="event.stopPropagation(); ${tgFunction}">T&G</button>`;
|
||||||
|
|
||||||
|
buttons += `<button class="status-btn" onclick="event.stopPropagation(); ${landFunction}">LAND</button>`;
|
||||||
|
|
||||||
|
const itemClass = isArrival ? 'inbound' : 'circuit';
|
||||||
|
|
||||||
|
// Display text: for arrivals show origin, for local flights show type
|
||||||
|
const displayText = isArrival ? `${ac.type} from ${ac.in_from || '?'}` : `${ac.type}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
|
||||||
<div class="aircraft-info">
|
<div class="aircraft-info">
|
||||||
<div class="aircraft-reg">${ac.registration}</div>
|
<div class="aircraft-reg">${ac.registration}</div>
|
||||||
<div class="aircraft-details">${ac.type}</div>
|
<div class="aircraft-details">${displayText}</div>
|
||||||
<div class="aircraft-time">${formatTimeOnly(ac.created_dt)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 0.5rem;">
|
<div style="display: flex; gap: 0.25rem;">
|
||||||
<button class="status-btn" onclick="event.stopPropagation(); updateLocalFlightStatusFromTable('${ac.id}', 'LOCAL')">LOCAL</button>
|
${buttons}
|
||||||
<button class="status-btn" onclick="event.stopPropagation(); currentLocalFlightId = '${ac.id}'; showCircuitModal()">T&G</button>
|
|
||||||
<button class="status-btn" onclick="event.stopPropagation(); currentLocalFlightId = '${ac.id}'; showTimestampModal('LANDED', ${ac.id}, true)">LAND</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load pending PPRs
|
// Load pending PPRs
|
||||||
@@ -5409,7 +5547,6 @@
|
|||||||
<div class="aircraft-info">
|
<div class="aircraft-info">
|
||||||
<div class="aircraft-reg">${ppr.ac_reg}</div>
|
<div class="aircraft-reg">${ppr.ac_reg}</div>
|
||||||
<div class="aircraft-details">${ppr.ac_type}</div>
|
<div class="aircraft-details">${ppr.ac_type}</div>
|
||||||
<div class="aircraft-time">${ppr.eta ? formatTimeOnly(ppr.eta) : ''}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="aircraft-status status-pending">PPR</div>
|
<div class="aircraft-status status-pending">PPR</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5419,26 +5556,32 @@
|
|||||||
// Load parked visitors
|
// Load parked visitors
|
||||||
async function loadParkedVisitors() {
|
async function loadParkedVisitors() {
|
||||||
try {
|
try {
|
||||||
const [depBookedOutResponse, localBookedOutResponse] = await Promise.all([
|
const [localBookedOutResponse, depBookedOutResponse] = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&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')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
|
||||||
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
||||||
|
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||||||
|
|
||||||
// Combine and filter for today's bookings
|
// Filter for today's bookings
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const bookedOutAircraft = [...depBookedOut, ...localBookedOut]
|
const bookedOutAircraft = [
|
||||||
.filter(flight => {
|
...localBookedOut.filter(flight => {
|
||||||
const createdDate = flight.created_dt.split('T')[0];
|
const createdDate = flight.created_dt.split('T')[0];
|
||||||
return createdDate === today;
|
return createdDate === today;
|
||||||
})
|
}).map(flight => ({
|
||||||
.map(flight => ({
|
|
||||||
...flight,
|
...flight,
|
||||||
isDeparture: depBookedOut.includes(flight),
|
isLocalFlight: true
|
||||||
isLocal: localBookedOut.includes(flight)
|
})),
|
||||||
}));
|
...depBookedOut.filter(flight => {
|
||||||
|
const createdDate = flight.created_dt.split('T')[0];
|
||||||
|
return createdDate === today;
|
||||||
|
}).map(flight => ({
|
||||||
|
...flight,
|
||||||
|
isDeparture: true
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
displayParkedVisitors(bookedOutAircraft);
|
displayParkedVisitors(bookedOutAircraft);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -5463,21 +5606,27 @@
|
|||||||
const dest = flight.out_to;
|
const dest = flight.out_to;
|
||||||
const createdTime = flight.created_dt ? formatTimeOnly(flight.created_dt) : '';
|
const createdTime = flight.created_dt ? formatTimeOnly(flight.created_dt) : '';
|
||||||
|
|
||||||
let typeIcon = '';
|
// Determine the entity type and display details
|
||||||
if (flight.isDeparture) {
|
let entityType, displayDetails, clickHandler, cssClass;
|
||||||
typeIcon = '<span style="color: #ff6b35; font-weight: bold;" title="Departure to Other Airport">D</span>';
|
if (flight.isLocalFlight) {
|
||||||
} else if (flight.isLocal) {
|
entityType = 'local';
|
||||||
typeIcon = '<span style="color: #4CAF50; font-weight: bold;" title="Local Flight">L</span>';
|
displayDetails = `${type} - ${flight.flight_type || 'Local Flight'}`;
|
||||||
|
clickHandler = `handleATCClick('${flight.id}', 'local')`;
|
||||||
|
cssClass = 'local-flight';
|
||||||
|
} else {
|
||||||
|
entityType = 'departure';
|
||||||
|
displayDetails = `${type} → ${dest || 'Other Airport'}`;
|
||||||
|
clickHandler = `handleATCClick('${flight.id}', 'departure')`;
|
||||||
|
cssClass = 'departure';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aircraft-item" onclick="handleATCClick('${flight.id}', '${flight.isDeparture ? 'departure' : 'local'}')">
|
<div class="aircraft-item ${cssClass}" onclick="${clickHandler}">
|
||||||
<div class="aircraft-info">
|
<div class="aircraft-info">
|
||||||
<div class="aircraft-reg">${reg} ${typeIcon}</div>
|
<div class="aircraft-reg">${reg}</div>
|
||||||
<div class="aircraft-details">${type} → ${dest || '?'}</div>
|
<div class="aircraft-details">${displayDetails}</div>
|
||||||
<div class="aircraft-time">${createdTime}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="status-btn" onclick="event.stopPropagation(); ${flight.isDeparture ? `currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)` : `currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)`}" title="Contact Pilot">CONTACT</button>
|
<button class="status-btn" onclick="event.stopPropagation(); ${flight.isLocalFlight ? `currentLocalFlightId = '${flight.id}'; showTimestampModal('GROUND', '${flight.id}', true, false)` : `currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)`}" title="Contact Pilot">CONTACT</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -5498,6 +5647,9 @@
|
|||||||
case 'arrival':
|
case 'arrival':
|
||||||
openArrivalEditModal(entityId);
|
openArrivalEditModal(entityId);
|
||||||
break;
|
break;
|
||||||
|
case 'overflight':
|
||||||
|
openOverflightEditModal(entityId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -699,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;
|
||||||
|
|||||||
Reference in New Issue
Block a user