Major WIP state machine

This commit is contained in:
2026-03-24 11:22:20 -04:00
parent 423023d3d9
commit bb6597ff76
16 changed files with 5781 additions and 234 deletions
-186
View File
@@ -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')")
+1 -1
View File
@@ -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'):
+14 -6
View File
@@ -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
})
+3 -4
View File
@@ -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()
+6 -2
View File
@@ -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()
+22 -4
View File
@@ -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()
+11 -1
View File
@@ -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
+3
View File
@@ -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
+5 -1
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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
+7
View File
@@ -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
+86 -29
View File
@@ -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,33 +1908,26 @@
return etdDate === today;
});
// Add local flights (BOOKED_OUT status - ready to go) - only those booked out today
if (localResponse.ok) {
const localFlights = await localResponse.json();
const today = new Date().toISOString().split('T')[0];
const localDepartures = localFlights
.filter(flight => {
// Only include flights booked out today (created_dt)
if (!flight.created_dt) return false;
const createdDate = flight.created_dt.split('T')[0];
return createdDate === today;
})
.map(flight => ({
...flight,
isLocalFlight: true // Flag to distinguish from PPR
}));
departures.push(...localDepartures);
}
// Add departures to other airports (BOOKED_OUT status)
if (depResponse.ok) {
const depFlights = await depResponse.json();
const depDepartures = depFlights.map(flight => ({
// Add local flights (BOOKED_OUT, GROUND, and LOCAL status - ready to go) - only those booked out today
const localDepartures = allLocalFlights
.filter(flight => {
// Only include flights booked out today (created_dt)
if (!flight.created_dt) return false;
const createdDate = flight.created_dt.split('T')[0];
return createdDate === today;
})
.map(flight => ({
...flight,
isDeparture: true // Flag to distinguish from PPR
isLocalFlight: true // Flag to distinguish from PPR
}));
departures.push(...depDepartures);
}
departures.push(...localDepartures);
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
const depDepartures = allDepartures.map(flight => ({
...flight,
isDeparture: true // Flag to distinguish from PPR
}));
departures.push(...depDepartures);
displayDepartures(departures);
} catch (error) {
@@ -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
View File
File diff suppressed because it is too large Load Diff