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 @@
- +
-

🛫 Awaiting Departure 0

+

🛫 Apron 0

No departing aircraft
@@ -247,15 +278,15 @@
-

🔄 Circuits 0

+

🔄 Circuit Traffic 0

No aircraft in circuit
- +
-

📝 Pending PPRs 0

+

📝 Today's PPRs 0

No pending PPRs
@@ -1827,8 +1858,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; }) @@ -1850,7 +1881,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'; @@ -1896,7 +1927,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) @@ -1971,6 +2002,7 @@ for (const flight of overflights) { const row = document.createElement('tr'); row.style.cursor = 'pointer'; + row.style.backgroundColor = '#ccffcc'; row.onclick = () => { openOverflightEditModal(flight.id); }; @@ -2131,7 +2163,7 @@ } } - // Load booked out aircraft (BOOKED_OUT status for departures and local flights) + // Load booked out aircraft (BOOKED_OUT status for departures only) async function loadParked() { document.getElementById('parked-loading').style.display = 'block'; document.getElementById('parked-table-content').style.display = 'none'; @@ -2470,14 +2502,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()) { @@ -3076,6 +3152,14 @@ const circuit = await response.json(); showNotification(`✈️ Circuit recorded at ${formatTimeOnly(circuit.circuit_timestamp)}`); + + // Update flight status to CIRCUIT after successful T&G recording + if (currentLocalFlightId) { + await updateLocalFlightStatusFromTable(currentLocalFlightId, 'CIRCUIT'); + } else if (currentArrivalId) { + await updateArrivalStatusFromTable(currentArrivalId, 'CIRCUIT'); + } + closeCircuitModal(); // Refresh ATC display @@ -4488,8 +4572,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'; @@ -5192,13 +5276,13 @@ buttonTitle = 'Mark as Local'; clickType = 'departure'; } + const itemClass = isLocal ? 'local-flight' : 'departure'; return ` -
+
${reg}
-
${type}${dest ? ` → ${dest}` : ` (Local)`}
-
${ac.etd ? formatTimeOnly(ac.etd) : ''}
+
${type}${dest ? ` → ${dest}` : ` Local Flight`}
@@ -5212,13 +5296,15 @@ const response = await Promise.all([ authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'), authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'), - authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000') + authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'), + authenticatedFetch('/api/v1/overflights/?status=ACTIVE&limit=1000') ]); let locals = []; if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true })); if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true }))); if (response[2].ok) locals = locals.concat((await response[2].json()).map(a => ({ ...a, isArrival: true }))); + if (response[3].ok) locals = locals.concat((await response[3].json()).map(o => ({ ...o, isOverflight: true }))); displayLocalAircraft(locals); } catch (error) { @@ -5238,26 +5324,34 @@ } container.innerHTML = aircraft.map(ac => { - const reg = ac.registration || ac.ac_reg; - const type = ac.type || ac.ac_type; + const reg = ac.registration || ac.ac_reg || ac.callsign || '-'; + const type = ac.type || ac.ac_type || ac.aircraft_type || ''; const dest = ac.out_to; const isDeparture = ac.isDeparture; let buttons; if (isDeparture) { - // Departure in LOCAL status - show QSY button - buttons = ``; + // Departure in LOCAL status - show QSY and REJOIN buttons + buttons = ` + + + `; } else if (ac.isLocalFlight || ac.isArrival) { // Local flight or arrival in LOCAL status - show REJOIN button buttons = ``; + } else if (ac.isOverflight) { + // Overflight in ACTIVE status - show QSY button + buttons = ``; } + const itemClass = isDeparture ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight')); + const detailsText = isDeparture ? `${type}${dest ? ` → ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} → ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` → ${dest}` : ` Local Flight`}`)); + const entityType = isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local')); return ` -
+
${reg}
-
${type}${dest ? ` → ${dest}` : ` (Local)`}
-
${formatTimeOnly(ac.created_dt)}
+
${detailsText}
${buttons} @@ -5272,7 +5366,7 @@ try { const response = await Promise.all([ authenticatedFetch('/api/v1/pprs/?limit=1000'), - authenticatedFetch('/api/v1/arrivals/?status=BOOKED_IN&limit=1000') + authenticatedFetch('/api/v1/arrivals/?status=INBOUND&limit=1000') ]); const pprs = response[0].ok ? await response[0].json() : []; @@ -5307,16 +5401,15 @@ let buttons = ''; if (ac.isArrival) { - // Arrival in BOOKED_IN status - show CONTACT button - buttons = ``; + // Arrival in INBOUND status - show LOCAL button + buttons = ``; } return ` -
+
${reg}
-
${type} from ${from || '?'}
-
${eta ? formatTimeOnly(eta) : ''}
+
${type} from ${from || 'Local Flight'}
${buttons ? `
${buttons}
` : '
IB
'}
@@ -5334,9 +5427,9 @@ ]); let circuits = []; - if (response[0].ok) circuits = await response[0].json(); - if (response[1].ok) circuits = circuits.concat(await response[1].json()); - if (response[2].ok) circuits = circuits.concat((await response[2].json()).map(a => ({ ...a, isArrival: true }))); + if (response[0].ok) circuits = circuits.concat(await response[0].json()); + if (response[1].ok) circuits = circuits.concat((await response[1].json()).map(l => ({ ...l, circuitStatus: getCircuitStatus(l.status) }))); + if (response[2].ok) circuits = circuits.concat((await response[2].json()).map(a => ({ ...a, isArrival: true, circuitStatus: getCircuitStatus(a.status) }))); displayCircuitAircraft(circuits); } catch (error) { @@ -5344,10 +5437,33 @@ } } + // Helper function to determine circuit status from API response + function getCircuitStatus(status) { + if (status === 'CIRCUIT_DOWNWIND') return 'DOWNWIND'; + if (status === 'CIRCUIT_BASE') return 'BASE'; + if (status === 'CIRCUIT_FINAL') return 'FINAL'; + return 'CIRCUIT'; + } + function displayCircuitAircraft(aircraft) { const container = document.getElementById('circuit-list'); const countEl = document.getElementById('circuit-count'); + // Define status order for sorting + const statusOrder = { + 'CIRCUIT': 1, + 'DOWNWIND': 2, + 'BASE': 3, + 'FINAL': 4 + }; + + // Sort aircraft by circuit status + aircraft.sort((a, b) => { + const aStatus = a.circuitStatus || 'CIRCUIT'; + const bStatus = b.circuitStatus || 'CIRCUIT'; + return statusOrder[aStatus] - statusOrder[bStatus]; + }); + countEl.textContent = aircraft.length; if (aircraft.length === 0) { @@ -5360,12 +5476,22 @@ const entityType = isArrival ? 'arrival' : 'local'; const updateFunction = isArrival ? 'updateArrivalStatusFromTable' : 'updateLocalFlightStatusFromTable'; const landFunction = isArrival ? `${updateFunction}('${ac.id}', 'LANDED')` : `showTimestampModal('LANDED', ${ac.id}, true)`; + const circuitStatus = ac.circuitStatus || 'CIRCUIT'; let buttons = ` `; - // Show T&G for both local flights and arrivals + // Circuit position buttons - show all, highlight current + const downwindClass = circuitStatus === 'DOWNWIND' ? 'active-position' : ''; + const baseClass = circuitStatus === 'BASE' ? 'active-position' : ''; + const finalClass = circuitStatus === 'FINAL' ? 'active-position' : ''; + + buttons += ``; + buttons += ``; + buttons += ``; + + // Show T&G for both local flights and arrivals - resets to CIRCUIT const tgFunction = isArrival ? `currentArrivalId = '${ac.id}'; showCircuitModal(null, '${ac.id}')` : `currentLocalFlightId = '${ac.id}'; showCircuitModal('${ac.id}')`; @@ -5373,14 +5499,18 @@ buttons += ``; + const itemClass = isArrival ? 'inbound' : 'circuit'; + + // Display text: for arrivals show origin, for local flights show type + const displayText = isArrival ? `${ac.type} from ${ac.in_from || '?'}` : `${ac.type}`; + return ` -
+
${ac.registration}
-
${ac.type}
-
${formatTimeOnly(ac.created_dt)}
+
${displayText}
-
+
${buttons}
@@ -5417,7 +5547,6 @@
${ppr.ac_reg}
${ppr.ac_type}
-
${ppr.eta ? formatTimeOnly(ppr.eta) : ''}
PPR
@@ -5427,26 +5556,32 @@ // Load parked visitors async function loadParkedVisitors() { try { - const [depBookedOutResponse, localBookedOutResponse] = await Promise.all([ - authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'), - authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000') + const [localBookedOutResponse, depBookedOutResponse] = await Promise.all([ + authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'), + authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000') ]); - const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : []; const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : []; + const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : []; - // Combine and filter for today's bookings + // Filter for today's bookings const today = new Date().toISOString().split('T')[0]; - const bookedOutAircraft = [...depBookedOut, ...localBookedOut] - .filter(flight => { + const bookedOutAircraft = [ + ...localBookedOut.filter(flight => { const createdDate = flight.created_dt.split('T')[0]; return createdDate === today; - }) - .map(flight => ({ + }).map(flight => ({ ...flight, - isDeparture: depBookedOut.includes(flight), - isLocal: localBookedOut.includes(flight) - })); + isLocalFlight: true + })), + ...depBookedOut.filter(flight => { + const createdDate = flight.created_dt.split('T')[0]; + return createdDate === today; + }).map(flight => ({ + ...flight, + isDeparture: true + })) + ]; displayParkedVisitors(bookedOutAircraft); } catch (error) { @@ -5471,21 +5606,27 @@ const dest = flight.out_to; const createdTime = flight.created_dt ? formatTimeOnly(flight.created_dt) : ''; - let typeIcon = ''; - if (flight.isDeparture) { - typeIcon = 'D'; - } else if (flight.isLocal) { - typeIcon = 'L'; + // Determine the entity type and display details + let entityType, displayDetails, clickHandler, cssClass; + if (flight.isLocalFlight) { + entityType = 'local'; + displayDetails = `${type} - ${flight.flight_type || 'Local Flight'}`; + clickHandler = `handleATCClick('${flight.id}', 'local')`; + cssClass = 'local-flight'; + } else { + entityType = 'departure'; + displayDetails = `${type} → ${dest || 'Other Airport'}`; + clickHandler = `handleATCClick('${flight.id}', 'departure')`; + cssClass = 'departure'; } return ` -
+
-
${reg} ${typeIcon}
-
${type} → ${dest || '?'}
-
${createdTime}
+
${reg}
+
${displayDetails}
- +
`; }).join(''); @@ -5506,6 +5647,9 @@ case 'arrival': openArrivalEditModal(entityId); break; + case 'overflight': + openOverflightEditModal(entityId); + break; } } diff --git a/web/index.html b/web/index.html index 66e21af..b32fca9 100644 --- a/web/index.html +++ b/web/index.html @@ -699,7 +699,7 @@ timeDisplay = `
${time}LANDED
`; sortTime = arrival.landed_dt; } else { - // Show ETA if BOOKED_IN + // Show ETA if INBOUND const time = convertToLocalTime(arrival.eta); timeDisplay = `
${time}IN AIR
`; sortTime = arrival.eta;