diff --git a/backend/alembic/versions/005_flight_states.py b/backend/alembic/versions/005_flight_states.py index 4dbcf98..fad5539 100644 --- a/backend/alembic/versions/005_flight_states.py +++ b/backend/alembic/versions/005_flight_states.py @@ -18,20 +18,20 @@ 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')") + op.execute("ALTER TABLE local_flights MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CIRCUIT','CIRCUIT_DOWNWIND','CIRCUIT_BASE','CIRCUIT_FINAL','LANDED','CANCELLED')") # Add timestamp columns to local_flights op.add_column('local_flights', sa.Column('contact_dt', sa.DateTime(), nullable=True)) op.add_column('local_flights', sa.Column('takeoff_dt', sa.DateTime(), nullable=True)) # Add GROUND and ARRIVED to arrivals status enum - op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','LANDED','GROUND','LOCAL','CIRCUIT','ARRIVED','CANCELLED')") + op.execute("ALTER TABLE arrivals MODIFY COLUMN status ENUM('BOOKED_IN','INBOUND','LANDED','GROUND','LOCAL','CIRCUIT','CIRCUIT_DOWNWIND','CIRCUIT_BASE','CIRCUIT_FINAL','ARRIVED','CANCELLED')") # Add timestamp column to arrivals 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')") + op.execute("ALTER TABLE departures MODIFY COLUMN status ENUM('BOOKED_OUT','GROUND','DEPARTED','LOCAL','CIRCUIT','CIRCUIT_DOWNWIND','CIRCUIT_BASE','CIRCUIT_FINAL','LANDED','CANCELLED')") # Add timestamp columns to departures op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True)) diff --git a/backend/app/api/endpoints/arrivals.py b/backend/app/api/endpoints/arrivals.py index 0937d4b..924de95 100644 --- a/backend/app/api/endpoints/arrivals.py +++ b/backend/app/api/endpoints/arrivals.py @@ -38,12 +38,12 @@ async def create_arrival( current_user: User = Depends(get_current_operator_user) ): """Create a new arrival record""" - arrival = crud_arrival.create(db, obj_in=arrival_in, created_by=current_user.username) + arrival = crud_arrival.create(db, obj_in=arrival_in, created_by=current_user.username, submitted_via=arrival_in.submitted_via) # Send real-time update via WebSocket if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ - "type": "arrival_booked_in", + "type": "arrival_inbound", "data": { "id": arrival.id, "registration": arrival.registration, diff --git a/backend/app/api/endpoints/local_flights.py b/backend/app/api/endpoints/local_flights.py index 78dcd78..c5d0607 100644 --- a/backend/app/api/endpoints/local_flights.py +++ b/backend/app/api/endpoints/local_flights.py @@ -39,7 +39,7 @@ async def create_local_flight( current_user: User = Depends(get_current_operator_user) ): """Create a new local flight record (book out)""" - flight = crud_local_flight.create(db, obj_in=flight_in, created_by=current_user.username) + flight = crud_local_flight.create(db, obj_in=flight_in, created_by=current_user.username, submitted_via="ADMIN") # Send real-time update via WebSocket if hasattr(request.app.state, 'connection_manager'): diff --git a/backend/app/api/endpoints/public.py b/backend/app/api/endpoints/public.py index 1a6e2c3..63847d2 100644 --- a/backend/app/api/endpoints/public.py +++ b/backend/app/api/endpoints/public.py @@ -97,11 +97,11 @@ async def get_public_arrivals(db: Session = Depends(get_db)): # Add booked-in arrivals booked_in_arrivals = crud_arrival.get_multi(db, limit=1000) for arrival in booked_in_arrivals: - # Only include BOOKED_IN and LANDED arrivals - if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.LANDED): + # Only include BOOKED_IN, INBOUND and LANDED arrivals + if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.INBOUND, ArrivalStatus.LANDED): continue - # For BOOKED_IN, only include those created today - if arrival.status == ArrivalStatus.BOOKED_IN: + # For BOOKED_IN and INBOUND, only include those created today + if arrival.status in (ArrivalStatus.BOOKED_IN, ArrivalStatus.INBOUND): if not (today_start <= arrival.created_dt < today_end): continue # For LANDED, only include those landed today diff --git a/backend/app/api/endpoints/public_book.py b/backend/app/api/endpoints/public_book.py index e4c2f03..22d9a91 100644 --- a/backend/app/api/endpoints/public_book.py +++ b/backend/app/api/endpoints/public_book.py @@ -56,7 +56,7 @@ async def public_book_local_flight( notes=flight_in.notes, ) - flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT") + flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC") # Update with submission source and pilot email db.query(type(flight)).filter(type(flight).id == flight.id).update({ @@ -181,11 +181,10 @@ async def public_book_arrival( notes=arrival_in.notes, ) - arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT") + arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC") - # Update with submission source and pilot email + # Update with pilot email db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({ - type(arrival).submitted_via: ArrivalSubmissionSource.PUBLIC, type(arrival).pilot_email: arrival_in.pilot_email, }) db.commit() diff --git a/backend/app/crud/crud_arrival.py b/backend/app/crud/crud_arrival.py index 1599770..e420004 100644 --- a/backend/app/crud/crud_arrival.py +++ b/backend/app/crud/crud_arrival.py @@ -24,7 +24,17 @@ class CRUDArrival: query = db.query(Arrival) if status: - query = query.filter(Arrival.status == status) + if status == ArrivalStatus.CIRCUIT: + # Special case: when requesting CIRCUIT status, return all circuit-related statuses + circuit_statuses = [ + ArrivalStatus.CIRCUIT, + ArrivalStatus.CIRCUIT_DOWNWIND, + ArrivalStatus.CIRCUIT_BASE, + ArrivalStatus.CIRCUIT_FINAL + ] + query = query.filter(Arrival.status.in_(circuit_statuses)) + else: + query = query.filter(Arrival.status == status) if date_from: query = query.filter(func.date(Arrival.created_dt) >= date_from) @@ -35,23 +45,33 @@ class CRUDArrival: return query.order_by(desc(Arrival.created_dt)).offset(skip).limit(limit).all() def get_arrivals_today(self, db: Session) -> List[Arrival]: - """Get today's arrivals (booked in or landed)""" + """Get today's arrivals (booked in, inbound or landed)""" today = date.today() return db.query(Arrival).filter( and_( func.date(Arrival.created_dt) == today, or_( Arrival.status == ArrivalStatus.BOOKED_IN, + Arrival.status == ArrivalStatus.INBOUND, Arrival.status == ArrivalStatus.LANDED ) ) ).order_by(Arrival.created_dt).all() - def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival: + def create(self, db: Session, obj_in: ArrivalCreate, created_by: str, submitted_via: str = "ADMIN") -> Arrival: + from app.models.arrival import SubmissionSource + + # Set initial status based on submission source + initial_status = ArrivalStatus.BOOKED_IN + + if submitted_via == SubmissionSource.ADMIN: + initial_status = ArrivalStatus.INBOUND + db_obj = Arrival( - **obj_in.dict(), + **obj_in.dict(exclude={'submitted_via'}), created_by=created_by, - status=ArrivalStatus.BOOKED_IN + status=initial_status, + submitted_via=submitted_via ) db.add(db_obj) db.commit() diff --git a/backend/app/crud/crud_local_flight.py b/backend/app/crud/crud_local_flight.py index 6a5decf..bd42771 100644 --- a/backend/app/crud/crud_local_flight.py +++ b/backend/app/crud/crud_local_flight.py @@ -26,7 +26,17 @@ class CRUDLocalFlight: query = db.query(LocalFlight) if status: - query = query.filter(LocalFlight.status == status) + if status == LocalFlightStatus.CIRCUIT: + # Special case: when requesting CIRCUIT status, return all circuit-related statuses + circuit_statuses = [ + LocalFlightStatus.CIRCUIT, + LocalFlightStatus.CIRCUIT_DOWNWIND, + LocalFlightStatus.CIRCUIT_BASE, + LocalFlightStatus.CIRCUIT_FINAL + ] + query = query.filter(LocalFlight.status.in_(circuit_statuses)) + else: + query = query.filter(LocalFlight.status == status) if flight_type: query = query.filter(LocalFlight.flight_type == flight_type) @@ -74,11 +84,20 @@ class CRUDLocalFlight: ) ).order_by(LocalFlight.created_dt).all() - def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str) -> LocalFlight: + def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str, submitted_via: str = "ADMIN") -> LocalFlight: + from app.models.local_flight import SubmissionSource + + # Set initial status based on submission source + initial_status = LocalFlightStatus.BOOKED_OUT + + if submitted_via == SubmissionSource.ADMIN: + initial_status = LocalFlightStatus.GROUND + db_obj = LocalFlight( **obj_in.dict(), created_by=created_by, - status=LocalFlightStatus.BOOKED_OUT + status=initial_status, + submitted_via=submitted_via ) db.add(db_obj) db.commit() diff --git a/backend/app/models/arrival.py b/backend/app/models/arrival.py index 97aa645..7658e5a 100644 --- a/backend/app/models/arrival.py +++ b/backend/app/models/arrival.py @@ -11,10 +11,14 @@ class SubmissionSource(str, Enum): class ArrivalStatus(str, Enum): BOOKED_IN = "BOOKED_IN" + INBOUND = "INBOUND" LANDED = "LANDED" GROUND = "GROUND" LOCAL = "LOCAL" CIRCUIT = "CIRCUIT" + CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND" + CIRCUIT_BASE = "CIRCUIT_BASE" + CIRCUIT_FINAL = "CIRCUIT_FINAL" ARRIVED = "ARRIVED" CANCELLED = "CANCELLED" @@ -28,7 +32,7 @@ class Arrival(Base): callsign = Column(String(16), nullable=True) pob = Column(Integer, nullable=False) in_from = Column(String(4), nullable=False, index=True) - status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True) + status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.INBOUND, nullable=False, index=True) notes = Column(Text, nullable=True) created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True) eta = Column(DateTime, nullable=True, index=True) diff --git a/backend/app/models/local_flight.py b/backend/app/models/local_flight.py index ec03ffc..7945eb4 100644 --- a/backend/app/models/local_flight.py +++ b/backend/app/models/local_flight.py @@ -21,6 +21,9 @@ class LocalFlightStatus(str, Enum): DEPARTED = "DEPARTED" LOCAL = "LOCAL" CIRCUIT = "CIRCUIT" + CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND" + CIRCUIT_BASE = "CIRCUIT_BASE" + CIRCUIT_FINAL = "CIRCUIT_FINAL" LANDED = "LANDED" CANCELLED = "CANCELLED" diff --git a/backend/app/schemas/arrival.py b/backend/app/schemas/arrival.py index f5ef6a7..73fff26 100644 --- a/backend/app/schemas/arrival.py +++ b/backend/app/schemas/arrival.py @@ -6,10 +6,14 @@ from enum import Enum class ArrivalStatus(str, Enum): BOOKED_IN = "BOOKED_IN" + INBOUND = "INBOUND" LANDED = "LANDED" GROUND = "GROUND" LOCAL = "LOCAL" CIRCUIT = "CIRCUIT" + CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND" + CIRCUIT_BASE = "CIRCUIT_BASE" + CIRCUIT_FINAL = "CIRCUIT_FINAL" ARRIVED = "ARRIVED" CANCELLED = "CANCELLED" @@ -48,6 +52,7 @@ class ArrivalBase(BaseModel): class ArrivalCreate(ArrivalBase): eta: Optional[datetime] = None + submitted_via: Optional[SubmissionSource] = SubmissionSource.ADMIN class ArrivalUpdate(BaseModel): diff --git a/backend/app/schemas/local_flight.py b/backend/app/schemas/local_flight.py index 38e93f1..086cae7 100644 --- a/backend/app/schemas/local_flight.py +++ b/backend/app/schemas/local_flight.py @@ -16,6 +16,9 @@ class LocalFlightStatus(str, Enum): DEPARTED = "DEPARTED" LOCAL = "LOCAL" CIRCUIT = "CIRCUIT" + CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND" + CIRCUIT_BASE = "CIRCUIT_BASE" + CIRCUIT_FINAL = "CIRCUIT_FINAL" LANDED = "LANDED" CANCELLED = "CANCELLED" diff --git a/web/admin.html b/web/admin.html index c711383..f129f88 100644 --- a/web/admin.html +++ b/web/admin.html @@ -1837,8 +1837,8 @@ const today = new Date().toISOString().split('T')[0]; const bookedInToday = bookedInArrivals .filter(arrival => { - // Only include arrivals booked in today (created_dt) with BOOKED_IN status - if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false; + // Only include arrivals booked in today (created_dt) with INBOUND, LOCAL, or CIRCUIT status + if (!arrival.created_dt || !['INBOUND', 'LOCAL', 'CIRCUIT'].includes(arrival.status)) return false; const bookedDate = arrival.created_dt.split('T')[0]; return bookedDate === today; }) @@ -1860,7 +1860,7 @@ document.getElementById('arrivals-loading').style.display = 'none'; } - // Load departures (LANDED status for PPR, BOOKED_OUT only for local flights) + // Load departures (LANDED status for PPR, GROUND/LOCAL for local flights) async function loadDepartures() { document.getElementById('departures-loading').style.display = 'block'; document.getElementById('departures-table-content').style.display = 'none'; @@ -1868,7 +1868,7 @@ try { // Load PPR departures, local flight departures, and airport departures simultaneously - const [pprResponse, localBookedOutResponse, localOutGroundResponse, localLocalResponse, localCircuitResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse, arrLocalResponse, arrCircuitResponse] = 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/local-flights/?status=GROUND&limit=1000'), @@ -1876,9 +1876,7 @@ 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'), - authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'), - authenticatedFetch('/api/v1/arrivals/?status=CIRCUIT&limit=1000') + authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000') ]); if (!pprResponse.ok) { @@ -1893,8 +1891,6 @@ const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : []; const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : []; const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : []; - const arrLocal = arrLocalResponse.ok ? await arrLocalResponse.json() : []; - const arrCircuit = arrCircuitResponse.ok ? await arrCircuitResponse.json() : []; // Combine local flights const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal, ...localCircuit]; @@ -1912,7 +1908,7 @@ return etdDate === today; }); - // Add local flights (BOOKED_OUT, GROUND, and LOCAL status - ready to go) - only those booked out today + // Add local flights (GROUND and LOCAL status - ready to go) - only those booked out today const localDepartures = allLocalFlights .filter(flight => { // Only include flights booked out today (created_dt) @@ -1933,20 +1929,6 @@ })); departures.push(...depDepartures); - // Add arrivals in LOCAL status - const arrDepartures = arrLocal.map(flight => ({ - ...flight, - isArrival: true // Flag to distinguish from PPR - })); - departures.push(...arrDepartures); - - // Add arrivals in CIRCUIT status - const arrCircuitDepartures = arrCircuit.map(flight => ({ - ...flight, - isArrival: true // Flag to distinguish from PPR - })); - departures.push(...arrCircuitDepartures); - displayDepartures(departures); } catch (error) { console.error('Error loading departures:', error); @@ -2500,14 +2482,58 @@ eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-'); pob = flight.pob || '-'; fuel = '-'; - actionButtons = ` - - - `; + + // Different action buttons based on status + if (flight.status === 'INBOUND') { + actionButtons = ` + + + + `; + } else if (flight.status === 'LOCAL') { + // Arrival in local area - show circuit and land buttons + let circuitButton = ``; + actionButtons = ` + + ${circuitButton} + + `; + } else if (flight.status === 'CIRCUIT') { + // Arrival in circuit - show local, T&G and land buttons + let circuitButton = ``; + actionButtons = ` + + ${circuitButton} + + `; + } else { + actionButtons = ` + + + `; + } } else { // PPR display if (flight.ac_call && flight.ac_call.trim()) { @@ -4581,8 +4607,8 @@ // Show/hide quick action buttons based on status const landedBtn = document.getElementById('arrival-btn-landed'); const cancelBtn = document.getElementById('arrival-btn-cancel'); - landedBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none'; - cancelBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none'; + landedBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none'; + cancelBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none'; // Show modal document.getElementById('arrivalEditModal').style.display = 'block'; diff --git a/web/atc.html b/web/atc.html index bbedee0..4043107 100644 --- a/web/atc.html +++ b/web/atc.html @@ -70,6 +70,23 @@ border-bottom: none; } + .aircraft-item.local-flight, + .aircraft-item.circuit { + background-color: #ffcccc; /* light red */ + } + + .aircraft-item.departure { + background-color: #ffffcc; /* light yellow */ + } + + .aircraft-item.inbound { + background-color: #ccccff; /* light blue */ + } + + .aircraft-item.overflight { + background-color: #ccffcc; /* light green */ + } + .aircraft-info { display: flex; flex-direction: row; @@ -158,8 +175,22 @@ transform: scale(1.05); } - .status-btn:active { - transform: scale(0.95); + .status-btn.small-btn { + padding: 0.2rem 0.4rem; + font-size: 0.7rem; + min-width: 24px; + height: 24px; + } + + .status-btn.active-position { + background-color: #28a745; + color: white; + border-color: #28a745; + } + + .status-btn.active-position:hover { + background-color: #218838; + border-color: #1e7e34; } /* Responsive adjustments */ @@ -213,9 +244,9 @@