Many more states WIP

This commit is contained in:
2026-03-25 13:16:36 -04:00
parent eb2321ef40
commit 9867156334
14 changed files with 351 additions and 128 deletions
@@ -18,20 +18,20 @@ depends_on = None
def upgrade() -> None: def upgrade() -> None:
# Add GROUND and LOCAL to local_flights status enum # 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 # 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('contact_dt', sa.DateTime(), nullable=True))
op.add_column('local_flights', sa.Column('takeoff_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 # 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 # Add timestamp column to arrivals
op.add_column('arrivals', sa.Column('arrived_dt', sa.DateTime(), nullable=True)) op.add_column('arrivals', sa.Column('arrived_dt', sa.DateTime(), nullable=True))
# Add GROUND and LOCAL to departures status enum # 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 # Add timestamp columns to departures
op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True)) op.add_column('departures', sa.Column('contact_dt', sa.DateTime(), nullable=True))
+2 -2
View File
@@ -38,12 +38,12 @@ async def create_arrival(
current_user: User = Depends(get_current_operator_user) current_user: User = Depends(get_current_operator_user)
): ):
"""Create a new arrival record""" """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 # Send real-time update via WebSocket
if hasattr(request.app.state, 'connection_manager'): if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({ await request.app.state.connection_manager.broadcast({
"type": "arrival_booked_in", "type": "arrival_inbound",
"data": { "data": {
"id": arrival.id, "id": arrival.id,
"registration": arrival.registration, "registration": arrival.registration,
+1 -1
View File
@@ -39,7 +39,7 @@ async def create_local_flight(
current_user: User = Depends(get_current_operator_user) current_user: User = Depends(get_current_operator_user)
): ):
"""Create a new local flight record (book out)""" """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 # Send real-time update via WebSocket
if hasattr(request.app.state, 'connection_manager'): if hasattr(request.app.state, 'connection_manager'):
+4 -4
View File
@@ -97,11 +97,11 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
# Add booked-in arrivals # Add booked-in arrivals
booked_in_arrivals = crud_arrival.get_multi(db, limit=1000) booked_in_arrivals = crud_arrival.get_multi(db, limit=1000)
for arrival in booked_in_arrivals: for arrival in booked_in_arrivals:
# Only include BOOKED_IN and LANDED arrivals # Only include BOOKED_IN, INBOUND and LANDED arrivals
if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.LANDED): if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.INBOUND, ArrivalStatus.LANDED):
continue continue
# For BOOKED_IN, only include those created today # For BOOKED_IN and INBOUND, only include those created today
if arrival.status == ArrivalStatus.BOOKED_IN: if arrival.status in (ArrivalStatus.BOOKED_IN, ArrivalStatus.INBOUND):
if not (today_start <= arrival.created_dt < today_end): if not (today_start <= arrival.created_dt < today_end):
continue continue
# For LANDED, only include those landed today # For LANDED, only include those landed today
+3 -4
View File
@@ -56,7 +56,7 @@ async def public_book_local_flight(
notes=flight_in.notes, 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 # Update with submission source and pilot email
db.query(type(flight)).filter(type(flight).id == flight.id).update({ db.query(type(flight)).filter(type(flight).id == flight.id).update({
@@ -181,11 +181,10 @@ async def public_book_arrival(
notes=arrival_in.notes, 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({ 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, type(arrival).pilot_email: arrival_in.pilot_email,
}) })
db.commit() db.commit()
+25 -5
View File
@@ -24,7 +24,17 @@ class CRUDArrival:
query = db.query(Arrival) query = db.query(Arrival)
if status: 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: if date_from:
query = query.filter(func.date(Arrival.created_dt) >= 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() return query.order_by(desc(Arrival.created_dt)).offset(skip).limit(limit).all()
def get_arrivals_today(self, db: Session) -> List[Arrival]: 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() today = date.today()
return db.query(Arrival).filter( return db.query(Arrival).filter(
and_( and_(
func.date(Arrival.created_dt) == today, func.date(Arrival.created_dt) == today,
or_( or_(
Arrival.status == ArrivalStatus.BOOKED_IN, Arrival.status == ArrivalStatus.BOOKED_IN,
Arrival.status == ArrivalStatus.INBOUND,
Arrival.status == ArrivalStatus.LANDED Arrival.status == ArrivalStatus.LANDED
) )
) )
).order_by(Arrival.created_dt).all() ).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( db_obj = Arrival(
**obj_in.dict(), **obj_in.dict(exclude={'submitted_via'}),
created_by=created_by, created_by=created_by,
status=ArrivalStatus.BOOKED_IN status=initial_status,
submitted_via=submitted_via
) )
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
+22 -3
View File
@@ -26,7 +26,17 @@ class CRUDLocalFlight:
query = db.query(LocalFlight) query = db.query(LocalFlight)
if status: 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: if flight_type:
query = query.filter(LocalFlight.flight_type == flight_type) query = query.filter(LocalFlight.flight_type == flight_type)
@@ -74,11 +84,20 @@ class CRUDLocalFlight:
) )
).order_by(LocalFlight.created_dt).all() ).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( db_obj = LocalFlight(
**obj_in.dict(), **obj_in.dict(),
created_by=created_by, created_by=created_by,
status=LocalFlightStatus.BOOKED_OUT status=initial_status,
submitted_via=submitted_via
) )
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
+5 -1
View File
@@ -11,10 +11,14 @@ class SubmissionSource(str, Enum):
class ArrivalStatus(str, Enum): class ArrivalStatus(str, Enum):
BOOKED_IN = "BOOKED_IN" BOOKED_IN = "BOOKED_IN"
INBOUND = "INBOUND"
LANDED = "LANDED" LANDED = "LANDED"
GROUND = "GROUND" GROUND = "GROUND"
LOCAL = "LOCAL" LOCAL = "LOCAL"
CIRCUIT = "CIRCUIT" CIRCUIT = "CIRCUIT"
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
CIRCUIT_BASE = "CIRCUIT_BASE"
CIRCUIT_FINAL = "CIRCUIT_FINAL"
ARRIVED = "ARRIVED" ARRIVED = "ARRIVED"
CANCELLED = "CANCELLED" CANCELLED = "CANCELLED"
@@ -28,7 +32,7 @@ class Arrival(Base):
callsign = Column(String(16), nullable=True) callsign = Column(String(16), nullable=True)
pob = Column(Integer, nullable=False) pob = Column(Integer, nullable=False)
in_from = Column(String(4), nullable=False, index=True) 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) notes = Column(Text, nullable=True)
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True) created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
eta = Column(DateTime, nullable=True, index=True) eta = Column(DateTime, nullable=True, index=True)
+3
View File
@@ -21,6 +21,9 @@ class LocalFlightStatus(str, Enum):
DEPARTED = "DEPARTED" DEPARTED = "DEPARTED"
LOCAL = "LOCAL" LOCAL = "LOCAL"
CIRCUIT = "CIRCUIT" CIRCUIT = "CIRCUIT"
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
CIRCUIT_BASE = "CIRCUIT_BASE"
CIRCUIT_FINAL = "CIRCUIT_FINAL"
LANDED = "LANDED" LANDED = "LANDED"
CANCELLED = "CANCELLED" CANCELLED = "CANCELLED"
+5
View File
@@ -6,10 +6,14 @@ from enum import Enum
class ArrivalStatus(str, Enum): class ArrivalStatus(str, Enum):
BOOKED_IN = "BOOKED_IN" BOOKED_IN = "BOOKED_IN"
INBOUND = "INBOUND"
LANDED = "LANDED" LANDED = "LANDED"
GROUND = "GROUND" GROUND = "GROUND"
LOCAL = "LOCAL" LOCAL = "LOCAL"
CIRCUIT = "CIRCUIT" CIRCUIT = "CIRCUIT"
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
CIRCUIT_BASE = "CIRCUIT_BASE"
CIRCUIT_FINAL = "CIRCUIT_FINAL"
ARRIVED = "ARRIVED" ARRIVED = "ARRIVED"
CANCELLED = "CANCELLED" CANCELLED = "CANCELLED"
@@ -48,6 +52,7 @@ class ArrivalBase(BaseModel):
class ArrivalCreate(ArrivalBase): class ArrivalCreate(ArrivalBase):
eta: Optional[datetime] = None eta: Optional[datetime] = None
submitted_via: Optional[SubmissionSource] = SubmissionSource.ADMIN
class ArrivalUpdate(BaseModel): class ArrivalUpdate(BaseModel):
+3
View File
@@ -16,6 +16,9 @@ class LocalFlightStatus(str, Enum):
DEPARTED = "DEPARTED" DEPARTED = "DEPARTED"
LOCAL = "LOCAL" LOCAL = "LOCAL"
CIRCUIT = "CIRCUIT" CIRCUIT = "CIRCUIT"
CIRCUIT_DOWNWIND = "CIRCUIT_DOWNWIND"
CIRCUIT_BASE = "CIRCUIT_BASE"
CIRCUIT_FINAL = "CIRCUIT_FINAL"
LANDED = "LANDED" LANDED = "LANDED"
CANCELLED = "CANCELLED" CANCELLED = "CANCELLED"
+60 -34
View File
@@ -1837,8 +1837,8 @@
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
const bookedInToday = bookedInArrivals const bookedInToday = bookedInArrivals
.filter(arrival => { .filter(arrival => {
// Only include arrivals booked in today (created_dt) with BOOKED_IN status // Only include arrivals booked in today (created_dt) with INBOUND, LOCAL, or CIRCUIT status
if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false; if (!arrival.created_dt || !['INBOUND', 'LOCAL', 'CIRCUIT'].includes(arrival.status)) return false;
const bookedDate = arrival.created_dt.split('T')[0]; const bookedDate = arrival.created_dt.split('T')[0];
return bookedDate === today; return bookedDate === today;
}) })
@@ -1860,7 +1860,7 @@
document.getElementById('arrivals-loading').style.display = 'none'; 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() { async function loadDepartures() {
document.getElementById('departures-loading').style.display = 'block'; document.getElementById('departures-loading').style.display = 'block';
document.getElementById('departures-table-content').style.display = 'none'; document.getElementById('departures-table-content').style.display = 'none';
@@ -1868,7 +1868,7 @@
try { try {
// Load PPR departures, local flight departures, and airport departures simultaneously // 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/pprs/?limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'), authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=GROUND&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/local-flights/?status=CIRCUIT&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'), authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'), authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
authenticatedFetch('/api/v1/departures/?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=CIRCUIT&limit=1000')
]); ]);
if (!pprResponse.ok) { if (!pprResponse.ok) {
@@ -1893,8 +1891,6 @@
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : []; const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : []; const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
const depLocal = depLocalResponse.ok ? await depLocalResponse.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 // Combine local flights
const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal, ...localCircuit]; const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal, ...localCircuit];
@@ -1912,7 +1908,7 @@
return etdDate === today; 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 const localDepartures = allLocalFlights
.filter(flight => { .filter(flight => {
// Only include flights booked out today (created_dt) // Only include flights booked out today (created_dt)
@@ -1933,20 +1929,6 @@
})); }));
departures.push(...depDepartures); 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); displayDepartures(departures);
} catch (error) { } catch (error) {
console.error('Error loading departures:', error); console.error('Error loading departures:', error);
@@ -2500,14 +2482,58 @@
eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-'); eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
pob = flight.pob || '-'; pob = flight.pob || '-';
fuel = '-'; fuel = '-';
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed"> // Different action buttons based on status
LAND if (flight.status === 'INBOUND') {
</button> actionButtons = `
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival"> <button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Mark as Local">
CANCEL LOCAL
</button> </button>
`; <button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
CANCEL
</button>
`;
} else if (flight.status === 'LOCAL') {
// Arrival in local area - show circuit and land buttons
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
T&G
</button>`;
actionButtons = `
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CIRCUIT')" title="Join Circuit">
CIRCUIT
</button>
${circuitButton}
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
`;
} else if (flight.status === 'CIRCUIT') {
// Arrival in circuit - show local, T&G and land buttons
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
T&G
</button>`;
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
LOCAL
</button>
${circuitButton}
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
`;
} else {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
CANCEL
</button>
`;
}
} else { } else {
// PPR display // PPR display
if (flight.ac_call && flight.ac_call.trim()) { if (flight.ac_call && flight.ac_call.trim()) {
@@ -4581,8 +4607,8 @@
// Show/hide quick action buttons based on status // Show/hide quick action buttons based on status
const landedBtn = document.getElementById('arrival-btn-landed'); const landedBtn = document.getElementById('arrival-btn-landed');
const cancelBtn = document.getElementById('arrival-btn-cancel'); const cancelBtn = document.getElementById('arrival-btn-cancel');
landedBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none'; landedBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none';
cancelBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none'; cancelBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none';
// Show modal // Show modal
document.getElementById('arrivalEditModal').style.display = 'block'; document.getElementById('arrivalEditModal').style.display = 'block';
+214 -70
View File
@@ -70,6 +70,23 @@
border-bottom: none; 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 { .aircraft-info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -158,8 +175,22 @@
transform: scale(1.05); transform: scale(1.05);
} }
.status-btn:active { .status-btn.small-btn {
transform: scale(0.95); 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 */ /* Responsive adjustments */
@@ -213,9 +244,9 @@
</div> </div>
<div class="atc-container"> <div class="atc-container">
<!-- Row 1: Awaiting Departure --> <!-- Row 1: Apron -->
<div class="atc-section"> <div class="atc-section">
<h2>🛫 Awaiting Departure <span class="count" id="departing-count">0</span></h2> <h2>🛫 Apron <span class="count" id="departing-count">0</span></h2>
<div class="aircraft-list" id="departing-list"> <div class="aircraft-list" id="departing-list">
<div class="no-aircraft">No departing aircraft</div> <div class="no-aircraft">No departing aircraft</div>
</div> </div>
@@ -247,15 +278,15 @@
<!-- Row 2: Circuit Traffic --> <!-- Row 2: Circuit Traffic -->
<div class="atc-section"> <div class="atc-section">
<h2>🔄 Circuits <span class="count" id="circuit-count">0</span></h2> <h2>🔄 Circuit Traffic <span class="count" id="circuit-count">0</span></h2>
<div class="aircraft-list" id="circuit-list"> <div class="aircraft-list" id="circuit-list">
<div class="no-aircraft">No aircraft in circuit</div> <div class="no-aircraft">No aircraft in circuit</div>
</div> </div>
</div> </div>
<!-- Row 2: Pending PPRs --> <!-- Row 2: Today's PPRs -->
<div class="atc-section"> <div class="atc-section">
<h2>📝 Pending PPRs <span class="count" id="pending-ppr-count">0</span></h2> <h2>📝 Today's PPRs <span class="count" id="pending-ppr-count">0</span></h2>
<div class="aircraft-list" id="pending-ppr-list"> <div class="aircraft-list" id="pending-ppr-list">
<div class="no-aircraft">No pending PPRs</div> <div class="no-aircraft">No pending PPRs</div>
</div> </div>
@@ -1827,8 +1858,8 @@
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
const bookedInToday = bookedInArrivals const bookedInToday = bookedInArrivals
.filter(arrival => { .filter(arrival => {
// Only include arrivals booked in today (created_dt) with BOOKED_IN status // Only include arrivals booked in today (created_dt) with INBOUND, LOCAL, or CIRCUIT status
if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false; if (!arrival.created_dt || !['INBOUND', 'LOCAL', 'CIRCUIT'].includes(arrival.status)) return false;
const bookedDate = arrival.created_dt.split('T')[0]; const bookedDate = arrival.created_dt.split('T')[0];
return bookedDate === today; return bookedDate === today;
}) })
@@ -1850,7 +1881,7 @@
document.getElementById('arrivals-loading').style.display = 'none'; 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() { async function loadDepartures() {
document.getElementById('departures-loading').style.display = 'block'; document.getElementById('departures-loading').style.display = 'block';
document.getElementById('departures-table-content').style.display = 'none'; document.getElementById('departures-table-content').style.display = 'none';
@@ -1896,7 +1927,7 @@
return etdDate === today; 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 const localDepartures = allLocalFlights
.filter(flight => { .filter(flight => {
// Only include flights booked out today (created_dt) // Only include flights booked out today (created_dt)
@@ -1971,6 +2002,7 @@
for (const flight of overflights) { for (const flight of overflights) {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.style.cursor = 'pointer'; row.style.cursor = 'pointer';
row.style.backgroundColor = '#ccffcc';
row.onclick = () => { row.onclick = () => {
openOverflightEditModal(flight.id); 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() { async function loadParked() {
document.getElementById('parked-loading').style.display = 'block'; document.getElementById('parked-loading').style.display = 'block';
document.getElementById('parked-table-content').style.display = 'none'; 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) : '-'); eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
pob = flight.pob || '-'; pob = flight.pob || '-';
fuel = '-'; fuel = '-';
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed"> // Different action buttons based on status
LAND if (flight.status === 'INBOUND') {
</button> actionButtons = `
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival"> <button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Mark as Local">
CANCEL LOCAL
</button> </button>
`; <button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
CANCEL
</button>
`;
} else if (flight.status === 'LOCAL') {
// Arrival in local area - show circuit and land buttons
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
T&G
</button>`;
actionButtons = `
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CIRCUIT')" title="Join Circuit">
CIRCUIT
</button>
${circuitButton}
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
`;
} else if (flight.status === 'CIRCUIT') {
// Arrival in circuit - show local, T&G and land buttons
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
T&G
</button>`;
actionButtons = `
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
LOCAL
</button>
${circuitButton}
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
`;
} else {
actionButtons = `
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
LAND
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
CANCEL
</button>
`;
}
} else { } else {
// PPR display // PPR display
if (flight.ac_call && flight.ac_call.trim()) { if (flight.ac_call && flight.ac_call.trim()) {
@@ -3076,6 +3152,14 @@
const circuit = await response.json(); const circuit = await response.json();
showNotification(`✈️ Circuit recorded at ${formatTimeOnly(circuit.circuit_timestamp)}`); 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(); closeCircuitModal();
// Refresh ATC display // Refresh ATC display
@@ -4488,8 +4572,8 @@
// Show/hide quick action buttons based on status // Show/hide quick action buttons based on status
const landedBtn = document.getElementById('arrival-btn-landed'); const landedBtn = document.getElementById('arrival-btn-landed');
const cancelBtn = document.getElementById('arrival-btn-cancel'); const cancelBtn = document.getElementById('arrival-btn-cancel');
landedBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none'; landedBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none';
cancelBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none'; cancelBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none';
// Show modal // Show modal
document.getElementById('arrivalEditModal').style.display = 'block'; document.getElementById('arrivalEditModal').style.display = 'block';
@@ -5192,13 +5276,13 @@
buttonTitle = 'Mark as Local'; buttonTitle = 'Mark as Local';
clickType = 'departure'; clickType = 'departure';
} }
const itemClass = isLocal ? 'local-flight' : 'departure';
return ` return `
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${clickType}')"> <div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${clickType}')">
<div class="aircraft-info"> <div class="aircraft-info">
<div class="aircraft-reg">${reg}</div> <div class="aircraft-reg">${reg}</div>
<div class="aircraft-details">${type}${dest ? `${dest}` : ` (Local)`}</div> <div class="aircraft-details">${type}${dest ? ` → ${dest}` : ` Local Flight`}</div>
<div class="aircraft-time">${ac.etd ? formatTimeOnly(ac.etd) : ''}</div>
</div> </div>
<button class="status-btn" onclick="${takeoffOnclick}" title="${buttonTitle}">${buttonText}</button> <button class="status-btn" onclick="${takeoffOnclick}" title="${buttonTitle}">${buttonText}</button>
</div> </div>
@@ -5212,13 +5296,15 @@
const response = await Promise.all([ const response = await Promise.all([
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'), authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/departures/?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 = []; let locals = [];
if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true })); 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[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[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); displayLocalAircraft(locals);
} catch (error) { } catch (error) {
@@ -5238,26 +5324,34 @@
} }
container.innerHTML = aircraft.map(ac => { container.innerHTML = aircraft.map(ac => {
const reg = ac.registration || ac.ac_reg; const reg = ac.registration || ac.ac_reg || ac.callsign || '-';
const type = ac.type || ac.ac_type; const type = ac.type || ac.ac_type || ac.aircraft_type || '';
const dest = ac.out_to; const dest = ac.out_to;
const isDeparture = ac.isDeparture; const isDeparture = ac.isDeparture;
let buttons; let buttons;
if (isDeparture) { if (isDeparture) {
// Departure in LOCAL status - show QSY button // Departure in LOCAL status - show QSY and REJOIN buttons
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>`; buttons = `
<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>
<button class="status-btn" onclick="event.stopPropagation(); updateDepartureStatusFromTable('${ac.id}', 'CIRCUIT')">REJOIN</button>
`;
} else if (ac.isLocalFlight || ac.isArrival) { } else if (ac.isLocalFlight || ac.isArrival) {
// Local flight or arrival in LOCAL status - show REJOIN button // Local flight or arrival in LOCAL status - show REJOIN button
buttons = `<button class="status-btn" onclick="event.stopPropagation(); ${ac.isArrival ? `updateArrivalStatusFromTable('${ac.id}', 'CIRCUIT')` : `updateLocalFlightStatusFromTable('${ac.id}', 'CIRCUIT')`}">REJOIN</button>`; buttons = `<button class="status-btn" onclick="event.stopPropagation(); ${ac.isArrival ? `updateArrivalStatusFromTable('${ac.id}', 'CIRCUIT')` : `updateLocalFlightStatusFromTable('${ac.id}', 'CIRCUIT')`}">REJOIN</button>`;
} else if (ac.isOverflight) {
// Overflight in ACTIVE status - show QSY button
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentOverflightId = '${ac.id}'; showOverflightQSYModal()">QSY</button>`;
} }
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 ` return `
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${isDeparture ? 'departure' : 'local'}')"> <div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
<div class="aircraft-info"> <div class="aircraft-info">
<div class="aircraft-reg">${reg}</div> <div class="aircraft-reg">${reg}</div>
<div class="aircraft-details">${type}${dest ? `${dest}` : ` (Local)`}</div> <div class="aircraft-details">${detailsText}</div>
<div class="aircraft-time">${formatTimeOnly(ac.created_dt)}</div>
</div> </div>
<div style="display: flex; gap: 0.5rem;"> <div style="display: flex; gap: 0.5rem;">
${buttons} ${buttons}
@@ -5272,7 +5366,7 @@
try { try {
const response = await Promise.all([ const response = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'), 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() : []; const pprs = response[0].ok ? await response[0].json() : [];
@@ -5307,16 +5401,15 @@
let buttons = ''; let buttons = '';
if (ac.isArrival) { if (ac.isArrival) {
// Arrival in BOOKED_IN status - show CONTACT button // Arrival in INBOUND status - show LOCAL button
buttons = `<button class="status-btn" onclick="event.stopPropagation(); updateArrivalStatusFromTable('${ac.id}', 'LOCAL')">CONTACT</button>`; buttons = `<button class="status-btn" onclick="event.stopPropagation(); updateArrivalStatusFromTable('${ac.id}', 'LOCAL')">→ LOCAL</button>`;
} }
return ` return `
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${ac.isArrival ? 'arrival' : 'ppr'}')"> <div class="aircraft-item inbound" onclick="handleATCClick('${ac.id}', '${ac.isArrival ? 'arrival' : 'ppr'}')">
<div class="aircraft-info"> <div class="aircraft-info">
<div class="aircraft-reg">${reg}</div> <div class="aircraft-reg">${reg}</div>
<div class="aircraft-details">${type} from ${from || '?'}</div> <div class="aircraft-details">${type} from ${from || 'Local Flight'}</div>
<div class="aircraft-time">${eta ? formatTimeOnly(eta) : ''}</div>
</div> </div>
${buttons ? `<div style="display: flex; gap: 0.5rem;">${buttons}</div>` : '<div class="aircraft-status status-inbound">IB</div>'} ${buttons ? `<div style="display: flex; gap: 0.5rem;">${buttons}</div>` : '<div class="aircraft-status status-inbound">IB</div>'}
</div> </div>
@@ -5334,9 +5427,9 @@
]); ]);
let circuits = []; let circuits = [];
if (response[0].ok) circuits = await response[0].json(); if (response[0].ok) circuits = circuits.concat(await response[0].json());
if (response[1].ok) circuits = circuits.concat(await response[1].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 }))); if (response[2].ok) circuits = circuits.concat((await response[2].json()).map(a => ({ ...a, isArrival: true, circuitStatus: getCircuitStatus(a.status) })));
displayCircuitAircraft(circuits); displayCircuitAircraft(circuits);
} catch (error) { } 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) { function displayCircuitAircraft(aircraft) {
const container = document.getElementById('circuit-list'); const container = document.getElementById('circuit-list');
const countEl = document.getElementById('circuit-count'); 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; countEl.textContent = aircraft.length;
if (aircraft.length === 0) { if (aircraft.length === 0) {
@@ -5360,12 +5476,22 @@
const entityType = isArrival ? 'arrival' : 'local'; const entityType = isArrival ? 'arrival' : 'local';
const updateFunction = isArrival ? 'updateArrivalStatusFromTable' : 'updateLocalFlightStatusFromTable'; const updateFunction = isArrival ? 'updateArrivalStatusFromTable' : 'updateLocalFlightStatusFromTable';
const landFunction = isArrival ? `${updateFunction}('${ac.id}', 'LANDED')` : `showTimestampModal('LANDED', ${ac.id}, true)`; const landFunction = isArrival ? `${updateFunction}('${ac.id}', 'LANDED')` : `showTimestampModal('LANDED', ${ac.id}, true)`;
const circuitStatus = ac.circuitStatus || 'CIRCUIT';
let buttons = ` let buttons = `
<button class="status-btn" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'LOCAL')">LOCAL</button> <button class="status-btn" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'LOCAL')">LOCAL</button>
`; `;
// 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 += `<button class="status-btn small-btn ${downwindClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_DOWNWIND')" title="Downwind">D</button>`;
buttons += `<button class="status-btn small-btn ${baseClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_BASE')" title="Base">B</button>`;
buttons += `<button class="status-btn small-btn ${finalClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_FINAL')" title="Final">F</button>`;
// Show T&G for both local flights and arrivals - resets to CIRCUIT
const tgFunction = isArrival const tgFunction = isArrival
? `currentArrivalId = '${ac.id}'; showCircuitModal(null, '${ac.id}')` ? `currentArrivalId = '${ac.id}'; showCircuitModal(null, '${ac.id}')`
: `currentLocalFlightId = '${ac.id}'; showCircuitModal('${ac.id}')`; : `currentLocalFlightId = '${ac.id}'; showCircuitModal('${ac.id}')`;
@@ -5373,14 +5499,18 @@
buttons += `<button class="status-btn" onclick="event.stopPropagation(); ${landFunction}">LAND</button>`; buttons += `<button class="status-btn" onclick="event.stopPropagation(); ${landFunction}">LAND</button>`;
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 ` return `
<div class="aircraft-item" onclick="handleATCClick('${ac.id}', '${entityType}')"> <div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
<div class="aircraft-info"> <div class="aircraft-info">
<div class="aircraft-reg">${ac.registration}</div> <div class="aircraft-reg">${ac.registration}</div>
<div class="aircraft-details">${ac.type}</div> <div class="aircraft-details">${displayText}</div>
<div class="aircraft-time">${formatTimeOnly(ac.created_dt)}</div>
</div> </div>
<div style="display: flex; gap: 0.5rem;"> <div style="display: flex; gap: 0.25rem;">
${buttons} ${buttons}
</div> </div>
</div> </div>
@@ -5417,7 +5547,6 @@
<div class="aircraft-info"> <div class="aircraft-info">
<div class="aircraft-reg">${ppr.ac_reg}</div> <div class="aircraft-reg">${ppr.ac_reg}</div>
<div class="aircraft-details">${ppr.ac_type}</div> <div class="aircraft-details">${ppr.ac_type}</div>
<div class="aircraft-time">${ppr.eta ? formatTimeOnly(ppr.eta) : ''}</div>
</div> </div>
<div class="aircraft-status status-pending">PPR</div> <div class="aircraft-status status-pending">PPR</div>
</div> </div>
@@ -5427,26 +5556,32 @@
// Load parked visitors // Load parked visitors
async function loadParkedVisitors() { async function loadParkedVisitors() {
try { try {
const [depBookedOutResponse, localBookedOutResponse] = await Promise.all([ const [localBookedOutResponse, depBookedOutResponse] = await Promise.all([
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'), authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000') authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000')
]); ]);
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.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 today = new Date().toISOString().split('T')[0];
const bookedOutAircraft = [...depBookedOut, ...localBookedOut] const bookedOutAircraft = [
.filter(flight => { ...localBookedOut.filter(flight => {
const createdDate = flight.created_dt.split('T')[0]; const createdDate = flight.created_dt.split('T')[0];
return createdDate === today; return createdDate === today;
}) }).map(flight => ({
.map(flight => ({
...flight, ...flight,
isDeparture: depBookedOut.includes(flight), isLocalFlight: true
isLocal: localBookedOut.includes(flight) })),
})); ...depBookedOut.filter(flight => {
const createdDate = flight.created_dt.split('T')[0];
return createdDate === today;
}).map(flight => ({
...flight,
isDeparture: true
}))
];
displayParkedVisitors(bookedOutAircraft); displayParkedVisitors(bookedOutAircraft);
} catch (error) { } catch (error) {
@@ -5471,21 +5606,27 @@
const dest = flight.out_to; const dest = flight.out_to;
const createdTime = flight.created_dt ? formatTimeOnly(flight.created_dt) : ''; const createdTime = flight.created_dt ? formatTimeOnly(flight.created_dt) : '';
let typeIcon = ''; // Determine the entity type and display details
if (flight.isDeparture) { let entityType, displayDetails, clickHandler, cssClass;
typeIcon = '<span style="color: #ff6b35; font-weight: bold;" title="Departure to Other Airport">D</span>'; if (flight.isLocalFlight) {
} else if (flight.isLocal) { entityType = 'local';
typeIcon = '<span style="color: #4CAF50; font-weight: bold;" title="Local Flight">L</span>'; 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 ` return `
<div class="aircraft-item" onclick="handleATCClick('${flight.id}', '${flight.isDeparture ? 'departure' : 'local'}')"> <div class="aircraft-item ${cssClass}" onclick="${clickHandler}">
<div class="aircraft-info"> <div class="aircraft-info">
<div class="aircraft-reg">${reg} ${typeIcon}</div> <div class="aircraft-reg">${reg}</div>
<div class="aircraft-details">${type}${dest || '?'}</div> <div class="aircraft-details">${displayDetails}</div>
<div class="aircraft-time">${createdTime}</div>
</div> </div>
<button class="status-btn" onclick="event.stopPropagation(); ${flight.isDeparture ? `currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)` : `currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)`}" title="Contact Pilot">CONTACT</button> <button class="status-btn" onclick="event.stopPropagation(); ${flight.isLocalFlight ? `currentLocalFlightId = '${flight.id}'; showTimestampModal('GROUND', '${flight.id}', true, false)` : `currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)`}" title="Contact Pilot">CONTACT</button>
</div> </div>
`; `;
}).join(''); }).join('');
@@ -5506,6 +5647,9 @@
case 'arrival': case 'arrival':
openArrivalEditModal(entityId); openArrivalEditModal(entityId);
break; break;
case 'overflight':
openOverflightEditModal(entityId);
break;
} }
} }
+1 -1
View File
@@ -699,7 +699,7 @@
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`; timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`;
sortTime = arrival.landed_dt; sortTime = arrival.landed_dt;
} else { } else {
// Show ETA if BOOKED_IN // Show ETA if INBOUND
const time = convertToLocalTime(arrival.eta); const time = convertToLocalTime(arrival.eta);
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">IN AIR</span></div>`; timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">IN AIR</span></div>`;
sortTime = arrival.eta; sortTime = arrival.eta;