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)
|
||||
):
|
||||
"""Create a new departure record"""
|
||||
departure = crud_departure.create(db, obj_in=departure_in, created_by=current_user.username)
|
||||
departure = crud_departure.create(db, obj_in=departure_in, created_by=current_user.username, submitted_via="ADMIN")
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
|
||||
@@ -173,10 +173,10 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
||||
'isDeparture': False
|
||||
})
|
||||
|
||||
# Add departures to other airports with BOOKED_OUT status
|
||||
# Add departures to other airports with BOOKED_OUT and GROUND status
|
||||
departures_to_airports = crud_departure.get_multi(
|
||||
db,
|
||||
status=DepartureStatus.BOOKED_OUT,
|
||||
status=None, # Get all statuses
|
||||
limit=1000
|
||||
)
|
||||
|
||||
@@ -187,17 +187,25 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
||||
|
||||
# Convert departures to match the format for display
|
||||
for dep in departures_to_airports:
|
||||
# Only include departures booked out today
|
||||
if not (today_start <= dep.created_dt < today_end):
|
||||
# Only include departures booked out today and not yet departed
|
||||
if not (today_start <= dep.created_dt < today_end) or dep.status == DepartureStatus.DEPARTED:
|
||||
continue
|
||||
|
||||
# Map status for display
|
||||
display_status = 'BOOKED_OUT'
|
||||
if dep.status == DepartureStatus.GROUND:
|
||||
display_status = 'CONTACT'
|
||||
elif dep.status == DepartureStatus.LOCAL:
|
||||
display_status = 'DEPARTED'
|
||||
|
||||
departures_list.append({
|
||||
'ac_call': dep.callsign or dep.registration,
|
||||
'ac_reg': dep.registration,
|
||||
'ac_type': dep.type,
|
||||
'out_to': dep.out_to,
|
||||
'etd': dep.etd or dep.created_dt,
|
||||
'departed_dt': None,
|
||||
'status': 'BOOKED_OUT',
|
||||
'departed_dt': dep.departed_dt,
|
||||
'status': display_status,
|
||||
'isLocalFlight': False,
|
||||
'isDeparture': True
|
||||
})
|
||||
|
||||
@@ -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_arrival import arrival as crud_arrival
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
@@ -136,11 +136,10 @@ async def public_book_departure(
|
||||
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({
|
||||
type(departure).submitted_via: DepartureSubmissionSource.PUBLIC,
|
||||
type(departure).pilot_email: departure_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
|
||||
@@ -113,8 +113,12 @@ class CRUDArrival:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = status
|
||||
|
||||
if status == ArrivalStatus.LANDED and timestamp:
|
||||
db_obj.landed_dt = timestamp
|
||||
# Set timestamps based on status
|
||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||
if status == ArrivalStatus.LANDED:
|
||||
db_obj.landed_dt = current_time
|
||||
elif status == ArrivalStatus.ARRIVED:
|
||||
db_obj.arrived_dt = current_time
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
|
||||
@@ -47,11 +47,23 @@ class CRUDDeparture:
|
||||
)
|
||||
).order_by(Departure.created_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str) -> Departure:
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str, submitted_via: str = "ADMIN") -> Departure:
|
||||
from app.models.departure import SubmissionSource
|
||||
|
||||
# Set initial status based on submission source
|
||||
initial_status = DepartureStatus.BOOKED_OUT
|
||||
contact_dt = None
|
||||
|
||||
if submitted_via == SubmissionSource.ADMIN:
|
||||
initial_status = DepartureStatus.GROUND
|
||||
contact_dt = func.now() # Set contact_dt to creation time for admin submissions
|
||||
|
||||
db_obj = Departure(
|
||||
**obj_in.dict(),
|
||||
created_by=created_by,
|
||||
status=DepartureStatus.BOOKED_OUT
|
||||
status=initial_status,
|
||||
contact_dt=contact_dt,
|
||||
submitted_via=submitted_via
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
@@ -113,8 +125,14 @@ class CRUDDeparture:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = status
|
||||
|
||||
if status == DepartureStatus.DEPARTED and timestamp:
|
||||
db_obj.departed_dt = timestamp
|
||||
# Set timestamps based on status
|
||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||
if status == DepartureStatus.GROUND:
|
||||
db_obj.contact_dt = current_time
|
||||
elif status == DepartureStatus.DEPARTED:
|
||||
db_obj.departed_dt = current_time
|
||||
elif status == DepartureStatus.LOCAL:
|
||||
db_obj.takeoff_dt = current_time
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
|
||||
@@ -144,10 +144,20 @@ class CRUDLocalFlight:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = status
|
||||
|
||||
# Update flight_type based on status changes
|
||||
if status == LocalFlightStatus.LOCAL:
|
||||
db_obj.flight_type = LocalFlightType.LOCAL
|
||||
elif status == LocalFlightStatus.CIRCUIT:
|
||||
db_obj.flight_type = LocalFlightType.CIRCUITS
|
||||
|
||||
# Set timestamps based on status
|
||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||
if status == LocalFlightStatus.DEPARTED:
|
||||
if status == LocalFlightStatus.GROUND:
|
||||
db_obj.contact_dt = current_time
|
||||
elif status == LocalFlightStatus.DEPARTED:
|
||||
db_obj.departed_dt = current_time
|
||||
elif status == LocalFlightStatus.LOCAL:
|
||||
db_obj.takeoff_dt = current_time
|
||||
elif status == LocalFlightStatus.LANDED:
|
||||
db_obj.landed_dt = current_time
|
||||
# Count circuits from the circuits table and populate the circuits column
|
||||
|
||||
@@ -14,6 +14,8 @@ class SubmissionSource(str, Enum):
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
GROUND = "GROUND"
|
||||
ARRIVED = "ARRIVED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@@ -31,6 +33,7 @@ class Arrival(Base):
|
||||
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||
eta = Column(DateTime, nullable=True, index=True)
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
arrived_dt = Column(DateTime, nullable=True) # Time when aircraft parks and shuts down
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
|
||||
@@ -13,7 +13,9 @@ class SubmissionSource(str, Enum):
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@@ -30,7 +32,9 @@ class Departure(Base):
|
||||
notes = Column(Text, nullable=True)
|
||||
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual departure time
|
||||
contact_dt = Column(DateTime, nullable=True) # Time when contact is established with pilot
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual departure time (QSY)
|
||||
takeoff_dt = Column(DateTime, nullable=True) # Time when aircraft becomes airborne
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
|
||||
@@ -17,7 +17,10 @@ class LocalFlightType(str, Enum):
|
||||
|
||||
class LocalFlightStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUIT = "CIRCUIT"
|
||||
LANDED = "LANDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
@@ -37,7 +40,9 @@ class LocalFlight(Base):
|
||||
notes = Column(Text, nullable=True)
|
||||
created_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
|
||||
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
|
||||
contact_dt = Column(DateTime, nullable=True) # Time when contact is established with pilot
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual takeoff time
|
||||
takeoff_dt = Column(DateTime, nullable=True) # Time when aircraft becomes airborne
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
|
||||
@@ -7,6 +7,8 @@ from enum import Enum
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
GROUND = "GROUND"
|
||||
ARRIVED = "ARRIVED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@@ -52,6 +54,10 @@ class ArrivalUpdate(BaseModel):
|
||||
callsign: Optional[str] = None
|
||||
pob: Optional[int] = None
|
||||
in_from: Optional[str] = None
|
||||
status: Optional[ArrivalStatus] = None
|
||||
eta: Optional[datetime] = None
|
||||
landed_dt: Optional[datetime] = None
|
||||
arrived_dt: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@@ -66,6 +72,7 @@ class Arrival(ArrivalBase):
|
||||
created_dt: datetime
|
||||
eta: Optional[datetime] = None
|
||||
landed_dt: Optional[datetime] = None
|
||||
arrived_dt: Optional[datetime] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
|
||||
@@ -6,7 +6,9 @@ from enum import Enum
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@@ -53,7 +55,11 @@ class DepartureUpdate(BaseModel):
|
||||
callsign: Optional[str] = None
|
||||
pob: Optional[int] = None
|
||||
out_to: Optional[str] = None
|
||||
status: Optional[DepartureStatus] = None
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@@ -67,7 +73,9 @@ class Departure(DepartureBase):
|
||||
status: DepartureStatus
|
||||
created_dt: datetime
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
@@ -12,7 +12,10 @@ class LocalFlightType(str, Enum):
|
||||
|
||||
class LocalFlightStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
GROUND = "GROUND"
|
||||
DEPARTED = "DEPARTED"
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUIT = "CIRCUIT"
|
||||
LANDED = "LANDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
@@ -66,7 +69,9 @@ class LocalFlightUpdate(BaseModel):
|
||||
duration: Optional[int] = None
|
||||
status: Optional[LocalFlightStatus] = None
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
circuits: Optional[int] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
@@ -81,7 +86,9 @@ class LocalFlightInDBBase(LocalFlightBase):
|
||||
status: LocalFlightStatus
|
||||
created_dt: datetime
|
||||
etd: Optional[datetime] = None
|
||||
contact_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
takeoff_dt: Optional[datetime] = None
|
||||
landed_dt: Optional[datetime] = None
|
||||
circuits: Optional[int] = None
|
||||
created_by: Optional[str] = None
|
||||
|
||||
+72
-15
@@ -29,6 +29,7 @@
|
||||
⚙️ Admin
|
||||
</button>
|
||||
<div class="dropdown-menu" id="adminDropdownMenu">
|
||||
<a href="#" onclick="window.location.href = '/atc'">🎛️ ATC View</a>
|
||||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
|
||||
<a href="#" onclick="openUserAircraftModal(); closeAdminDropdown()" id="user-aircraft-dropdown">✈️ User Aircraft</a>
|
||||
<a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a>
|
||||
@@ -1867,10 +1868,15 @@
|
||||
|
||||
try {
|
||||
// Load PPR departures, local flight departures, and airport departures simultaneously
|
||||
const [pprResponse, localResponse, depResponse] = await Promise.all([
|
||||
const [pprResponse, localBookedOutResponse, localOutGroundResponse, localLocalResponse, localCircuitResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
|
||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000')
|
||||
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
||||
authenticatedFetch('/api/v1/local-flights/?status=CIRCUIT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
||||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
||||
]);
|
||||
|
||||
if (!pprResponse.ok) {
|
||||
@@ -1878,6 +1884,18 @@
|
||||
}
|
||||
|
||||
const allPPRs = await pprResponse.json();
|
||||
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
||||
const localOutGround = localOutGroundResponse.ok ? await localOutGroundResponse.json() : [];
|
||||
const localLocal = localLocalResponse.ok ? await localLocalResponse.json() : [];
|
||||
const localCircuit = localCircuitResponse.ok ? await localCircuitResponse.json() : [];
|
||||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||||
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
||||
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
|
||||
|
||||
// Combine local flights
|
||||
const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal, ...localCircuit];
|
||||
// Combine departures
|
||||
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Filter for PPR departures with ETD today and LANDED status only
|
||||
@@ -1890,11 +1908,8 @@
|
||||
return etdDate === today;
|
||||
});
|
||||
|
||||
// Add local flights (BOOKED_OUT status - ready to go) - only those booked out today
|
||||
if (localResponse.ok) {
|
||||
const localFlights = await localResponse.json();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const localDepartures = localFlights
|
||||
// Add local flights (BOOKED_OUT, GROUND, and LOCAL status - ready to go) - only those booked out today
|
||||
const localDepartures = allLocalFlights
|
||||
.filter(flight => {
|
||||
// Only include flights booked out today (created_dt)
|
||||
if (!flight.created_dt) return false;
|
||||
@@ -1906,17 +1921,13 @@
|
||||
isLocalFlight: true // Flag to distinguish from PPR
|
||||
}));
|
||||
departures.push(...localDepartures);
|
||||
}
|
||||
|
||||
// Add departures to other airports (BOOKED_OUT status)
|
||||
if (depResponse.ok) {
|
||||
const depFlights = await depResponse.json();
|
||||
const depDepartures = depFlights.map(flight => ({
|
||||
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
|
||||
const depDepartures = allDepartures.map(flight => ({
|
||||
...flight,
|
||||
isDeparture: true // Flag to distinguish from PPR
|
||||
}));
|
||||
departures.push(...depDepartures);
|
||||
}
|
||||
|
||||
displayDepartures(departures);
|
||||
} catch (error) {
|
||||
@@ -2584,7 +2595,15 @@
|
||||
// Action buttons for local flight
|
||||
if (flight.status === 'BOOKED_OUT') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
|
||||
<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
|
||||
</button>
|
||||
`;
|
||||
@@ -2599,6 +2618,26 @@
|
||||
LAND
|
||||
</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 {
|
||||
actionButtons = '<span style="color: #999;">-</span>';
|
||||
}
|
||||
@@ -2622,10 +2661,22 @@
|
||||
// Action buttons for departure
|
||||
if (flight.status === 'BOOKED_OUT') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
|
||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)" title="Contact Pilot">
|
||||
CONTACT
|
||||
</button>
|
||||
`;
|
||||
} else if (flight.status === 'GROUND') {
|
||||
actionButtons = `
|
||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('LOCAL', ${flight.id}, false, true)" title="Mark as Local">
|
||||
TAKE OFF
|
||||
</button>
|
||||
`;
|
||||
} 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') {
|
||||
actionButtons = '<span style="color: #999;">Departed</span>';
|
||||
} else {
|
||||
@@ -2965,6 +3016,12 @@
|
||||
} else if (status === 'DEPARTED') {
|
||||
modalTitle.textContent = 'Confirm Departure Time';
|
||||
submitBtn.textContent = '🛫 Confirm Departure';
|
||||
} else if (status === 'GROUND') {
|
||||
modalTitle.textContent = 'Confirm Contact Time';
|
||||
submitBtn.textContent = '📞 Confirm Contact';
|
||||
} else if (status === 'LOCAL') {
|
||||
modalTitle.textContent = 'Confirm Takeoff Time';
|
||||
submitBtn.textContent = '🛫 Confirm Takeoff';
|
||||
}
|
||||
|
||||
// Set default timestamp to current time
|
||||
|
||||
+5543
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user