Major WIP state machine
This commit is contained in:
@@ -1,186 +0,0 @@
|
|||||||
# Flight State Flow Documentation
|
|
||||||
|
|
||||||
This document describes the state flows for each flight type in the PPR system.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The system manages different types of aircraft operations, each with their own state machines and status transitions. The main flight types are:
|
|
||||||
|
|
||||||
1. **Circuits** - Touch-and-go circuit training flights
|
|
||||||
2. **Local Flights** - General local operations
|
|
||||||
3. **Inbound/Arrivals** - Aircraft arriving at the airport
|
|
||||||
4. **Departures** - Aircraft departing to other airports
|
|
||||||
5. **PPR Records** - Prior permission required advance bookings
|
|
||||||
6. **Overflights** - Aircraft passing through airspace
|
|
||||||
|
|
||||||
## 1. Circuits
|
|
||||||
|
|
||||||
Circuits are a special type of local flight where aircraft perform touch-and-go operations. The circuit system tracks individual circuit completions.
|
|
||||||
|
|
||||||
### State Flow
|
|
||||||
```
|
|
||||||
BOOKED_OUT → DEPARTED → LANDED
|
|
||||||
↓ ↓ ↓
|
|
||||||
CANCELLED CANCELLED CANCELLED
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Descriptions
|
|
||||||
- **BOOKED_OUT**: Flight is booked out and ready for departure
|
|
||||||
- **DEPARTED**: Aircraft has departed the airport
|
|
||||||
- **LANDED**: Aircraft has landed and flight is complete
|
|
||||||
- **CANCELLED**: Flight was cancelled before completion
|
|
||||||
|
|
||||||
### Additional Features
|
|
||||||
- Individual circuit timestamps are recorded in the `circuits` table
|
|
||||||
- The `circuits` field on the local_flight record stores the total number of completed circuits
|
|
||||||
- Circuits are counted automatically when the flight status changes to LANDED
|
|
||||||
|
|
||||||
## 2. Local Flights
|
|
||||||
|
|
||||||
Local flights represent general aviation operations within the local area.
|
|
||||||
|
|
||||||
### State Flow
|
|
||||||
```
|
|
||||||
BOOKED_OUT → DEPARTED → LANDED
|
|
||||||
↓ ↓ ↓
|
|
||||||
CANCELLED CANCELLED CANCELLED
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Descriptions
|
|
||||||
- **BOOKED_OUT**: Flight is booked out and ready for departure
|
|
||||||
- **DEPARTED**: Aircraft has departed the airport
|
|
||||||
- **LANDED**: Aircraft has landed and flight is complete
|
|
||||||
- **CANCELLED**: Flight was cancelled before completion
|
|
||||||
|
|
||||||
### Flight Types
|
|
||||||
Local flights can be categorized by their `flight_type`:
|
|
||||||
- **LOCAL**: Standard local flight
|
|
||||||
- **CIRCUITS**: Circuit training flight (see Circuits section above)
|
|
||||||
- **DEPARTURE**: Flight departing to another airport (may transition to Departure records)
|
|
||||||
|
|
||||||
## 3. Inbound/Arrivals
|
|
||||||
|
|
||||||
Inbound flights are aircraft arriving at the airport, either through the PPR system or direct bookings.
|
|
||||||
|
|
||||||
### State Flow
|
|
||||||
```
|
|
||||||
BOOKED_IN → LANDED
|
|
||||||
↓ ↓
|
|
||||||
CANCELLED CANCELLED
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Descriptions
|
|
||||||
- **BOOKED_IN**: Aircraft is expected to arrive (booked in the system)
|
|
||||||
- **LANDED**: Aircraft has landed at the airport
|
|
||||||
- **CANCELLED**: Arrival was cancelled
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- Arrivals can originate from PPR records that have landed
|
|
||||||
- Direct arrival bookings are also supported
|
|
||||||
- Landing timestamp is recorded when status changes to LANDED
|
|
||||||
|
|
||||||
## 4. Departures
|
|
||||||
|
|
||||||
Departures are aircraft leaving the airport to fly to other destinations.
|
|
||||||
|
|
||||||
### State Flow
|
|
||||||
```
|
|
||||||
BOOKED_OUT → DEPARTED
|
|
||||||
↓ ↓
|
|
||||||
CANCELLED CANCELLED
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Descriptions
|
|
||||||
- **BOOKED_OUT**: Flight is booked out and ready for departure
|
|
||||||
- **DEPARTED**: Aircraft has departed to its destination
|
|
||||||
- **CANCELLED**: Departure was cancelled
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- Departures can originate from PPR records that are departing
|
|
||||||
- Local flights with `flight_type = DEPARTURE` may transition to departure records
|
|
||||||
- Departure timestamp is recorded when status changes to DEPARTED
|
|
||||||
|
|
||||||
## 5. PPR (Prior Permission Required) Records
|
|
||||||
|
|
||||||
PPR records represent advance permission requests for aircraft operations and have a more complex lifecycle.
|
|
||||||
|
|
||||||
### State Flow
|
|
||||||
```
|
|
||||||
NEW → CONFIRMED → LANDED → DEPARTED
|
|
||||||
↓ ↓ ↓ ↓
|
|
||||||
CANCELED CANCELED CANCELED (terminal)
|
|
||||||
↓
|
|
||||||
DELETED
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Descriptions
|
|
||||||
- **NEW**: PPR has been submitted but not yet confirmed
|
|
||||||
- **CONFIRMED**: PPR has been confirmed by ATC
|
|
||||||
- **LANDED**: Aircraft has landed (for inbound operations)
|
|
||||||
- **DEPARTED**: Aircraft has departed (for outbound operations)
|
|
||||||
- **CANCELED**: PPR was cancelled
|
|
||||||
- **DELETED**: PPR was soft-deleted (marked as deleted)
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- PPRs can represent both arrivals and departures
|
|
||||||
- The system tracks both ETA (Estimated Time of Arrival) and ETD (Estimated Time of Departure)
|
|
||||||
- Timestamps are recorded for actual landing and departure times
|
|
||||||
- PPRs can transition between arrival and departure operations
|
|
||||||
|
|
||||||
## 6. Overflights
|
|
||||||
|
|
||||||
Overflights represent aircraft passing through the airspace without landing at the airport.
|
|
||||||
|
|
||||||
### State Flow
|
|
||||||
```
|
|
||||||
ACTIVE → INACTIVE
|
|
||||||
↓ ↓
|
|
||||||
CANCELLED CANCELLED
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Descriptions
|
|
||||||
- **ACTIVE**: Overflight is currently active/tracking
|
|
||||||
- **INACTIVE**: Overflight has completed or is no longer active
|
|
||||||
- **CANCELLED**: Overflight was cancelled
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- Overflights track aircraft that call in for frequency changes (QSY)
|
|
||||||
- Call time and QSY time are recorded
|
|
||||||
- Overflights are typically managed separately from landing operations
|
|
||||||
|
|
||||||
## Status Transition Rules
|
|
||||||
|
|
||||||
### Automatic Transitions
|
|
||||||
- Status changes automatically set appropriate timestamps:
|
|
||||||
- `DEPARTED`: Sets `departed_dt`
|
|
||||||
- `LANDED`: Sets `landed_dt`
|
|
||||||
- For circuits: Also counts completed circuits
|
|
||||||
|
|
||||||
### Manual Transitions
|
|
||||||
- Operators can manually update statuses through the admin interface
|
|
||||||
- Status changes are logged in the journal/audit trail
|
|
||||||
- WebSocket notifications are sent for real-time updates
|
|
||||||
|
|
||||||
### Validation Rules
|
|
||||||
- Cannot transition backwards in the flow (e.g., from LANDED to DEPARTED)
|
|
||||||
- CANCELLED/CANCELED are terminal states for most flight types
|
|
||||||
- DELETED is only used for PPR soft-deletion
|
|
||||||
- Status enum values: Most use "CANCELLED", PPR uses "CANCELED"
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### UI Integration
|
|
||||||
- Admin interface provides buttons for status updates
|
|
||||||
- Real-time updates via WebSocket
|
|
||||||
- Modal dialogs for status changes with timestamp confirmation
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
- REST endpoints for status updates: `PATCH /{id}/status`
|
|
||||||
- Status filtering available on list endpoints
|
|
||||||
- Journal logging for all status changes
|
|
||||||
|
|
||||||
### Database Integration
|
|
||||||
- Status enums ensure data integrity
|
|
||||||
- Foreign key relationships maintain consistency
|
|
||||||
- Audit trail tracks all changes</content>
|
|
||||||
<parameter name="filePath">/home/jamesp/docker/pprdev/FLIGHT_STATE_FLOWS.md
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Add granular flight states and timestamps
|
||||||
|
|
||||||
|
Revision ID: 8adefaee847c
|
||||||
|
Revises: 004_user_aircraft
|
||||||
|
Create Date: 2026-03-24 09:09:00.944815
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '005_flight_states'
|
||||||
|
down_revision = '004_user_aircraft'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add GROUND and LOCAL to local_flights status enum
|
||||||
|
op.execute("ALTER TABLE local_flights MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CIRCUIT','LANDED','CANCELLED')")
|
||||||
|
|
||||||
|
# Add timestamp columns to local_flights
|
||||||
|
op.add_column('local_flights', sa.Column('contact_dt', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('local_flights', sa.Column('takeoff_dt', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
# Add GROUND and ARRIVED to arrivals status enum
|
||||||
|
op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','GROUND','ARRIVED','CANCELLED')")
|
||||||
|
|
||||||
|
# Add timestamp column to arrivals
|
||||||
|
op.add_column('arrivals', sa.Column('arrived_dt', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
# Add GROUND and LOCAL to departures status enum
|
||||||
|
op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CANCELLED')")
|
||||||
|
|
||||||
|
# Add timestamp columns to departures
|
||||||
|
op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('departures', sa.Column('takeoff_dt', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove timestamp columns from departures
|
||||||
|
op.drop_column('departures', 'takeoff_dt')
|
||||||
|
op.drop_column('departures', 'contact_dt')
|
||||||
|
|
||||||
|
# Remove GROUND and LOCAL from departures status enum
|
||||||
|
op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','DEPARTED','CANCELLED')")
|
||||||
|
|
||||||
|
# Remove timestamp column from arrivals
|
||||||
|
op.drop_column('arrivals', 'arrived_dt')
|
||||||
|
|
||||||
|
# Remove GROUND and ARRIVED from arrivals status enum
|
||||||
|
op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','CANCELLED')")
|
||||||
|
|
||||||
|
# Remove timestamp columns from local_flights
|
||||||
|
op.drop_column('local_flights', 'takeoff_dt')
|
||||||
|
op.drop_column('local_flights', 'contact_dt')
|
||||||
|
|
||||||
|
# Remove GROUND and LOCAL from local_flights status enum
|
||||||
|
op.execute("ALTER TABLE local_flights MODIFY COLUMN status ENUM('BOOKED_OUT','DEPARTED','LANDED','CANCELLED')")
|
||||||
@@ -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'):
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from app.crud.crud_circuit import crud_circuit
|
|||||||
from app.crud.crud_departure import departure as crud_departure
|
from app.crud.crud_departure import departure as crud_departure
|
||||||
from app.crud.crud_arrival import arrival as crud_arrival
|
from app.crud.crud_arrival import arrival as crud_arrival
|
||||||
from app.models.local_flight import SubmissionSource
|
from app.models.local_flight import SubmissionSource
|
||||||
from app.models.departure import SubmissionSource as DepartureSubmissionSource
|
from app.models.departure import DepartureStatus
|
||||||
from app.models.arrival import SubmissionSource as ArrivalSubmissionSource
|
from app.models.arrival import SubmissionSource as ArrivalSubmissionSource
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -136,11 +136,10 @@ async def public_book_departure(
|
|||||||
notes=departure_in.notes,
|
notes=departure_in.notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
departure = crud_departure.create(db, obj_in=departure_create, created_by="PUBLIC_PILOT")
|
departure = crud_departure.create(db, obj_in=departure_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||||
|
|
||||||
# Update with submission source and pilot email
|
# Update with pilot email (submitted_via is already set in create method)
|
||||||
db.query(type(departure)).filter(type(departure).id == departure.id).update({
|
db.query(type(departure)).filter(type(departure).id == departure.id).update({
|
||||||
type(departure).submitted_via: DepartureSubmissionSource.PUBLIC,
|
|
||||||
type(departure).pilot_email: departure_in.pilot_email,
|
type(departure).pilot_email: departure_in.pilot_email,
|
||||||
})
|
})
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -113,8 +113,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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -144,10 +144,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,8 @@ class SubmissionSource(str, Enum):
|
|||||||
class ArrivalStatus(str, Enum):
|
class ArrivalStatus(str, Enum):
|
||||||
BOOKED_IN = "BOOKED_IN"
|
BOOKED_IN = "BOOKED_IN"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
|
GROUND = "GROUND"
|
||||||
|
ARRIVED = "ARRIVED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ class Arrival(Base):
|
|||||||
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)
|
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ class SubmissionSource(str, 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"
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +32,9 @@ 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)
|
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ 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"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
@@ -37,7 +40,9 @@ 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)
|
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from enum import Enum
|
|||||||
class ArrivalStatus(str, Enum):
|
class ArrivalStatus(str, Enum):
|
||||||
BOOKED_IN = "BOOKED_IN"
|
BOOKED_IN = "BOOKED_IN"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
|
GROUND = "GROUND"
|
||||||
|
ARRIVED = "ARRIVED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
|
||||||
@@ -52,6 +54,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
|
||||||
|
|
||||||
|
|
||||||
@@ -66,6 +72,7 @@ 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
|
submitted_via: Optional[SubmissionSource] = None
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ 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"
|
||||||
|
|
||||||
|
|
||||||
@@ -53,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
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +73,9 @@ 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
|
updated_at: datetime
|
||||||
submitted_via: Optional[SubmissionSource] = None
|
submitted_via: Optional[SubmissionSource] = None
|
||||||
pilot_email: Optional[str] = None
|
pilot_email: Optional[str] = None
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ 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"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
@@ -66,7 +69,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
|
||||||
|
|
||||||
@@ -81,7 +86,9 @@ 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
|
||||||
|
|||||||
+86
-29
@@ -29,6 +29,7 @@
|
|||||||
⚙️ 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="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>
|
||||||
@@ -1867,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) {
|
||||||
@@ -1878,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
|
||||||
@@ -1890,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 (BOOKED_OUT, 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) {
|
||||||
@@ -2584,7 +2595,15 @@
|
|||||||
// 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">
|
||||||
|
CONTACT
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} 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
|
TAKE OFF
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
@@ -2599,6 +2618,26 @@
|
|||||||
LAND
|
LAND
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
} else if (flight.status === 'LOCAL') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; updateLocalFlightStatusFromTable(${flight.id}, 'CIRCUIT')" title="Rejoin Circuit">
|
||||||
|
REJOIN
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'CIRCUIT') {
|
||||||
|
// Circuit traffic - show LOCAL, T&G and LAND buttons
|
||||||
|
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||||
|
T&G
|
||||||
|
</button>`;
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; updateLocalFlightStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
|
||||||
|
LOCAL
|
||||||
|
</button>
|
||||||
|
${circuitButton}
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
actionButtons = '<span style="color: #999;">-</span>';
|
actionButtons = '<span style="color: #999;">-</span>';
|
||||||
}
|
}
|
||||||
@@ -2622,10 +2661,22 @@
|
|||||||
// 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>
|
||||||
`;
|
`;
|
||||||
|
} else if (flight.status === 'LOCAL') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
|
||||||
|
QSY
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
} else if (flight.status === 'DEPARTED') {
|
} else if (flight.status === 'DEPARTED') {
|
||||||
actionButtons = '<span style="color: #999;">Departed</span>';
|
actionButtons = '<span style="color: #999;">Departed</span>';
|
||||||
} else {
|
} else {
|
||||||
@@ -2965,6 +3016,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
|
||||||
|
|||||||
+5543
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user