diff --git a/backend/alembic/versions/002_local_flights.py b/backend/alembic/versions/002_local_flights.py
index 45d477e..cdf20e5 100644
--- a/backend/alembic/versions/002_local_flights.py
+++ b/backend/alembic/versions/002_local_flights.py
@@ -5,7 +5,8 @@ Revises: 001_initial_schema
Create Date: 2025-12-12 12:00:00.000000
This migration adds a new table for tracking local flights (circuits, local, departure)
-that don't require PPR submissions.
+that don't require PPR submissions. Also adds etd and renames booked_out_dt to created_dt,
+and departure_dt to departed_dt for consistency.
"""
from alembic import op
@@ -32,8 +33,9 @@ def upgrade() -> None:
sa.Column('flight_type', sa.Enum('LOCAL', 'CIRCUITS', 'DEPARTURE', name='localflighttype'), nullable=False),
sa.Column('status', sa.Enum('BOOKED_OUT', 'DEPARTED', 'LANDED', 'CANCELLED', name='localflightstatus'), nullable=False, server_default='BOOKED_OUT'),
sa.Column('notes', sa.Text(), nullable=True),
- sa.Column('booked_out_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
- sa.Column('departure_dt', sa.DateTime(), nullable=True),
+ sa.Column('created_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
+ sa.Column('etd', sa.DateTime(), nullable=True),
+ sa.Column('departed_dt', sa.DateTime(), nullable=True),
sa.Column('landed_dt', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.String(length=16), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
@@ -47,7 +49,8 @@ def upgrade() -> None:
op.create_index('idx_registration', 'local_flights', ['registration'])
op.create_index('idx_flight_type', 'local_flights', ['flight_type'])
op.create_index('idx_status', 'local_flights', ['status'])
- op.create_index('idx_booked_out_dt', 'local_flights', ['booked_out_dt'])
+ op.create_index('idx_created_dt', 'local_flights', ['created_dt'])
+ op.create_index('idx_etd', 'local_flights', ['etd'])
op.create_index('idx_created_by', 'local_flights', ['created_by'])
# Create departures table for non-PPR departures to other airports
@@ -60,8 +63,9 @@ def upgrade() -> None:
sa.Column('out_to', sa.String(length=64), nullable=False),
sa.Column('status', sa.Enum('BOOKED_OUT', 'DEPARTED', 'CANCELLED', name='departuresstatus'), nullable=False, server_default='BOOKED_OUT'),
sa.Column('notes', sa.Text(), nullable=True),
- sa.Column('booked_out_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
- sa.Column('departure_dt', sa.DateTime(), nullable=True),
+ sa.Column('created_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
+ sa.Column('etd', sa.DateTime(), nullable=True),
+ sa.Column('departed_dt', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.String(length=16), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id'),
@@ -73,7 +77,8 @@ def upgrade() -> None:
op.create_index('idx_dep_registration', 'departures', ['registration'])
op.create_index('idx_dep_out_to', 'departures', ['out_to'])
op.create_index('idx_dep_status', 'departures', ['status'])
- op.create_index('idx_dep_booked_out_dt', 'departures', ['booked_out_dt'])
+ op.create_index('idx_dep_created_dt', 'departures', ['created_dt'])
+ op.create_index('idx_dep_etd', 'departures', ['etd'])
op.create_index('idx_dep_created_by', 'departures', ['created_by'])
# Create arrivals table for non-PPR arrivals from elsewhere
diff --git a/backend/app/api/endpoints/departures.py b/backend/app/api/endpoints/departures.py
index 8d46197..970bd94 100644
--- a/backend/app/api/endpoints/departures.py
+++ b/backend/app/api/endpoints/departures.py
@@ -132,7 +132,7 @@ async def update_departure_status(
"id": departure.id,
"registration": departure.registration,
"status": departure.status.value,
- "departure_dt": departure.departure_dt.isoformat() if departure.departure_dt else None
+ "departed_dt": departure.departed_dt.isoformat() if departure.departed_dt else None
}
})
diff --git a/backend/app/api/endpoints/public.py b/backend/app/api/endpoints/public.py
index e7b30f6..4cf9ece 100644
--- a/backend/app/api/endpoints/public.py
+++ b/backend/app/api/endpoints/public.py
@@ -48,7 +48,7 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
'ac_reg': flight.registration,
'ac_type': flight.type,
'in_from': None,
- 'eta': flight.departure_dt,
+ 'eta': flight.departed_dt,
'landed_dt': None,
'status': 'DEPARTED',
'isLocalFlight': True,
@@ -92,7 +92,7 @@ async def get_public_departures(db: Session = Depends(get_db)):
'ac_reg': flight.registration,
'ac_type': flight.type,
'out_to': None,
- 'etd': flight.booked_out_dt,
+ 'etd': flight.etd or flight.created_dt,
'departed_dt': None,
'status': 'BOOKED_OUT',
'isLocalFlight': True,
@@ -114,7 +114,7 @@ async def get_public_departures(db: Session = Depends(get_db)):
'ac_reg': dep.registration,
'ac_type': dep.type,
'out_to': dep.out_to,
- 'etd': dep.booked_out_dt,
+ 'etd': dep.etd or dep.created_dt,
'departed_dt': None,
'status': 'BOOKED_OUT',
'isLocalFlight': False,
diff --git a/backend/app/crud/crud_departure.py b/backend/app/crud/crud_departure.py
index 015a09f..7533ed8 100644
--- a/backend/app/crud/crud_departure.py
+++ b/backend/app/crud/crud_departure.py
@@ -25,25 +25,25 @@ class CRUDDeparture:
query = query.filter(Departure.status == status)
if date_from:
- query = query.filter(func.date(Departure.booked_out_dt) >= date_from)
+ query = query.filter(func.date(Departure.created_dt) >= date_from)
if date_to:
- query = query.filter(func.date(Departure.booked_out_dt) <= date_to)
+ query = query.filter(func.date(Departure.created_dt) <= date_to)
- return query.order_by(desc(Departure.booked_out_dt)).offset(skip).limit(limit).all()
+ return query.order_by(desc(Departure.created_dt)).offset(skip).limit(limit).all()
def get_departures_today(self, db: Session) -> List[Departure]:
"""Get today's departures (booked out or departed)"""
today = date.today()
return db.query(Departure).filter(
and_(
- func.date(Departure.booked_out_dt) == today,
+ func.date(Departure.created_dt) == today,
or_(
Departure.status == DepartureStatus.BOOKED_OUT,
Departure.status == DepartureStatus.DEPARTED
)
)
- ).order_by(Departure.booked_out_dt).all()
+ ).order_by(Departure.created_dt).all()
def create(self, db: Session, obj_in: DepartureCreate, created_by: str) -> Departure:
db_obj = Departure(
@@ -82,7 +82,7 @@ class CRUDDeparture:
db_obj.status = status
if status == DepartureStatus.DEPARTED and timestamp:
- db_obj.departure_dt = timestamp
+ db_obj.departed_dt = timestamp
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 259612d..25c2b12 100644
--- a/backend/app/crud/crud_local_flight.py
+++ b/backend/app/crud/crud_local_flight.py
@@ -29,12 +29,12 @@ class CRUDLocalFlight:
query = query.filter(LocalFlight.flight_type == flight_type)
if date_from:
- query = query.filter(func.date(LocalFlight.booked_out_dt) >= date_from)
+ query = query.filter(func.date(LocalFlight.created_dt) >= date_from)
if date_to:
- query = query.filter(func.date(LocalFlight.booked_out_dt) <= date_to)
+ query = query.filter(func.date(LocalFlight.created_dt) <= date_to)
- return query.order_by(desc(LocalFlight.booked_out_dt)).offset(skip).limit(limit).all()
+ return query.order_by(desc(LocalFlight.created_dt)).offset(skip).limit(limit).all()
def get_active_flights(self, db: Session) -> List[LocalFlight]:
"""Get currently active (booked out or departed) flights"""
@@ -43,33 +43,33 @@ class CRUDLocalFlight:
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.DEPARTED
)
- ).order_by(desc(LocalFlight.booked_out_dt)).all()
+ ).order_by(desc(LocalFlight.created_dt)).all()
def get_departures_today(self, db: Session) -> List[LocalFlight]:
"""Get today's departures (booked out or departed)"""
today = date.today()
return db.query(LocalFlight).filter(
and_(
- func.date(LocalFlight.booked_out_dt) == today,
+ func.date(LocalFlight.created_dt) == today,
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.DEPARTED
)
)
- ).order_by(LocalFlight.booked_out_dt).all()
+ ).order_by(LocalFlight.created_dt).all()
def get_booked_out_today(self, db: Session) -> List[LocalFlight]:
"""Get all flights booked out today"""
today = date.today()
return db.query(LocalFlight).filter(
and_(
- func.date(LocalFlight.booked_out_dt) == today,
+ func.date(LocalFlight.created_dt) == today,
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.LANDED
)
)
- ).order_by(LocalFlight.booked_out_dt).all()
+ ).order_by(LocalFlight.created_dt).all()
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str) -> LocalFlight:
db_obj = LocalFlight(
@@ -114,7 +114,7 @@ class CRUDLocalFlight:
# Set timestamps based on status
current_time = timestamp if timestamp is not None else datetime.utcnow()
if status == LocalFlightStatus.DEPARTED:
- db_obj.departure_dt = current_time
+ db_obj.departed_dt = current_time
elif status == LocalFlightStatus.LANDED:
db_obj.landed_dt = current_time
diff --git a/backend/app/models/departure.py b/backend/app/models/departure.py
index 9f4132b..8cb576b 100644
--- a/backend/app/models/departure.py
+++ b/backend/app/models/departure.py
@@ -20,10 +20,11 @@ class Departure(Base):
type = Column(String(32), nullable=True)
callsign = Column(String(16), nullable=True)
pob = Column(Integer, nullable=False)
- out_to = Column(String(4), nullable=False, index=True)
+ out_to = Column(String(64), nullable=False, index=True)
status = Column(SQLEnum(DepartureStatus), default=DepartureStatus.BOOKED_OUT, nullable=False, index=True)
notes = Column(Text, nullable=True)
- booked_out_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
- departure_dt = Column(DateTime, nullable=True)
+ created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
+ etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
+ departed_dt = Column(DateTime, nullable=True) # Actual departure time
created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
diff --git a/backend/app/models/local_flight.py b/backend/app/models/local_flight.py
index 593e155..9fb9ba3 100644
--- a/backend/app/models/local_flight.py
+++ b/backend/app/models/local_flight.py
@@ -28,8 +28,9 @@ class LocalFlight(Base):
flight_type = Column(SQLEnum(LocalFlightType), nullable=False, index=True)
status = Column(SQLEnum(LocalFlightStatus), nullable=False, default=LocalFlightStatus.BOOKED_OUT, index=True)
notes = Column(Text, nullable=True)
- booked_out_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
- departure_dt = Column(DateTime, nullable=True) # Actual takeoff time
+ created_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
+ etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
+ departed_dt = Column(DateTime, nullable=True) # Actual takeoff time
landed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
diff --git a/backend/app/schemas/departure.py b/backend/app/schemas/departure.py
index 710fb45..dbbfb25 100644
--- a/backend/app/schemas/departure.py
+++ b/backend/app/schemas/departure.py
@@ -16,6 +16,7 @@ class DepartureBase(BaseModel):
callsign: Optional[str] = None
pob: int
out_to: str
+ etd: Optional[datetime] = None # Estimated Time of Departure
notes: Optional[str] = None
@validator('registration')
@@ -46,6 +47,7 @@ class DepartureUpdate(BaseModel):
callsign: Optional[str] = None
pob: Optional[int] = None
out_to: Optional[str] = None
+ etd: Optional[datetime] = None
notes: Optional[str] = None
@@ -57,10 +59,6 @@ class DepartureStatusUpdate(BaseModel):
class Departure(DepartureBase):
id: int
status: DepartureStatus
- booked_out_dt: datetime
- departure_dt: Optional[datetime] = None
- created_by: Optional[str] = None
- updated_at: datetime
-
- class Config:
- from_attributes = True
+ created_dt: datetime
+ etd: Optional[datetime] = None
+ departed_dt: Optional[datetime] = None
diff --git a/backend/app/schemas/local_flight.py b/backend/app/schemas/local_flight.py
index e5e1e59..d6d2e45 100644
--- a/backend/app/schemas/local_flight.py
+++ b/backend/app/schemas/local_flight.py
@@ -23,6 +23,7 @@ class LocalFlightBase(BaseModel):
callsign: Optional[str] = None
pob: int
flight_type: LocalFlightType
+ etd: Optional[datetime] = None # Estimated Time of Departure
notes: Optional[str] = None
@validator('registration')
@@ -57,7 +58,8 @@ class LocalFlightUpdate(BaseModel):
pob: Optional[int] = None
flight_type: Optional[LocalFlightType] = None
status: Optional[LocalFlightStatus] = None
- departure_dt: Optional[datetime] = None
+ etd: Optional[datetime] = None
+ departed_dt: Optional[datetime] = None
notes: Optional[str] = None
@@ -69,8 +71,9 @@ class LocalFlightStatusUpdate(BaseModel):
class LocalFlightInDBBase(LocalFlightBase):
id: int
status: LocalFlightStatus
- booked_out_dt: datetime
- departure_dt: Optional[datetime] = None
+ created_dt: datetime
+ etd: Optional[datetime] = None
+ departed_dt: Optional[datetime] = None
landed_dt: Optional[datetime] = None
created_by: Optional[str] = None
updated_at: datetime
@@ -80,4 +83,4 @@ class LocalFlightInDBBase(LocalFlightBase):
class LocalFlight(LocalFlightInDBBase):
- pass
+ pass
\ No newline at end of file
diff --git a/web/admin.css b/web/admin.css
index 8141b58..141bd91 100644
--- a/web/admin.css
+++ b/web/admin.css
@@ -593,3 +593,56 @@ tbody tr:hover {
.notification.error {
background-color: #e74c3c;
}
+
+/* Unified Lookup Styles */
+.lookup-no-match {
+ color: #6c757d;
+ font-style: italic;
+}
+
+.lookup-searching {
+ color: #007bff;
+}
+
+.lookup-list {
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid #dee2e6;
+ border-radius: 4px;
+ background-color: white;
+}
+
+.lookup-option {
+ padding: 0.5rem;
+ border-bottom: 1px solid #f0f0f0;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.lookup-option:hover {
+ background-color: #f8f9fa;
+}
+
+.lookup-option:last-child {
+ border-bottom: none;
+}
+
+.lookup-code {
+ font-family: 'Courier New', monospace;
+ font-weight: bold;
+ color: #495057;
+}
+
+.lookup-name {
+ color: #6c757d;
+ font-size: 0.85rem;
+}
+
+.lookup-location {
+ color: #868e96;
+ font-size: 0.8rem;
+ font-style: italic;
+}
diff --git a/web/admin.html b/web/admin.html
index 40aa51b..951c1ff 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -5,11 +5,12 @@
PPR Admin Interface
+
-
✈️ Swansea PPR
+ ✈️ Swansea Tower
+
@@ -1632,8 +1642,8 @@
// Sort departures by ETD (ascending), nulls last
departures.sort((a, b) => {
- const aTime = a.etd || a.booked_out_dt;
- const bTime = b.etd || b.booked_out_dt;
+ const aTime = a.etd || a.created_dt;
+ const bTime = b.etd || b.created_dt;
if (!aTime) return 1;
if (!bTime) return -1;
return new Date(aTime) - new Date(bTime);
@@ -1673,7 +1683,7 @@
aircraftDisplay = `
${flight.registration}`;
}
toDisplay = `
${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}`;
- etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
+ etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
pob = flight.pob || '-';
fuel = '-';
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
@@ -1711,10 +1721,10 @@
if (flight.out_to && flight.out_to.length === 4 && /^[A-Z]{4}$/.test(flight.out_to)) {
toDisplay = await getAirportDisplay(flight.out_to);
}
- etd = flight.booked_out_dt ? formatTimeOnly(flight.booked_out_dt) : '-';
+ etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
pob = flight.pob || '-';
fuel = '-';
- landedDt = flight.departure_dt ? formatTimeOnly(flight.departure_dt) : '-';
+ landedDt = flight.departed_dt ? formatTimeOnly(flight.departed_dt) : '-';
// Action buttons for departure
if (flight.status === 'BOOKED_OUT') {
@@ -2608,90 +2618,6 @@
}
}
- // Aircraft Lookup Functions
- let aircraftLookupTimeout;
-
- function handleAircraftLookup(registration) {
- // Clear previous timeout
- if (aircraftLookupTimeout) {
- clearTimeout(aircraftLookupTimeout);
- }
-
- // Clear results if input is too short
- if (registration.length < 4) {
- clearAircraftLookup();
- return;
- }
-
- // Show searching indicator
- document.getElementById('aircraft-lookup-results').innerHTML =
- '
Searching...
';
-
- // Debounce the search - wait 300ms after user stops typing
- aircraftLookupTimeout = setTimeout(() => {
- performAircraftLookup(registration);
- }, 300);
- }
-
- async function performAircraftLookup(registration) {
- try {
- // Clean the input (remove non-alphanumeric characters and make uppercase)
- const cleanInput = registration.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
-
- if (cleanInput.length < 4) {
- clearAircraftLookup();
- return;
- }
-
- // Call the real API
- const response = await authenticatedFetch(`/api/v1/aircraft/lookup/${cleanInput}`);
-
- if (!response.ok) {
- throw new Error('Failed to fetch aircraft data');
- }
-
- const matches = await response.json();
- displayAircraftLookupResults(matches, cleanInput);
-
- } catch (error) {
- console.error('Aircraft lookup error:', error);
- document.getElementById('aircraft-lookup-results').innerHTML =
- '
Lookup failed - please enter manually
';
- }
- }
-
- function displayAircraftLookupResults(matches, searchTerm) {
- const resultsDiv = document.getElementById('aircraft-lookup-results');
-
- if (matches.length === 0) {
- resultsDiv.innerHTML = '
No matches found
';
- } else if (matches.length === 1) {
- // Unique match found - auto-populate
- const aircraft = matches[0];
- resultsDiv.innerHTML = `
-
- ✓ ${aircraft.manufacturer_name} ${aircraft.model} (${aircraft.type_code})
-
- `;
-
- // Auto-populate the form fields
- document.getElementById('ac_reg').value = aircraft.registration;
- document.getElementById('ac_type').value = aircraft.type_code;
-
- } else {
- // Multiple matches - show list but don't auto-populate
- resultsDiv.innerHTML = `
-
- Multiple matches found (${matches.length}) - please be more specific
-
- `;
- }
- }
-
- function clearAircraftLookup() {
- document.getElementById('aircraft-lookup-results').innerHTML = '';
- }
-
function clearArrivalAirportLookup() {
document.getElementById('arrival-airport-lookup-results').innerHTML = '';
}
@@ -2700,208 +2626,12 @@
document.getElementById('departure-airport-lookup-results').innerHTML = '';
}
- // Airport Lookup Functions
- let arrivalAirportLookupTimeout;
- let departureAirportLookupTimeout;
-
- function handleArrivalAirportLookup(codeOrName) {
- // Clear previous timeout
- if (arrivalAirportLookupTimeout) {
- clearTimeout(arrivalAirportLookupTimeout);
+ // Add listener for ETD date changes
+ document.addEventListener('change', function(e) {
+ if (e.target.id === 'local_etd_date') {
+ populateETDTimeSlots();
}
-
- // Clear results if input is too short
- if (codeOrName.length < 2) {
- clearArrivalAirportLookup();
- return;
- }
-
- // Show searching indicator
- document.getElementById('arrival-airport-lookup-results').innerHTML =
- '
Searching...
';
-
- // Debounce the search - wait 300ms after user stops typing
- arrivalAirportLookupTimeout = setTimeout(() => {
- performArrivalAirportLookup(codeOrName);
- }, 300);
- }
-
- function handleDepartureAirportLookup(codeOrName) {
- // Clear previous timeout
- if (departureAirportLookupTimeout) {
- clearTimeout(departureAirportLookupTimeout);
- }
-
- // Clear results if input is too short
- if (codeOrName.length < 2) {
- clearDepartureAirportLookup();
- return;
- }
-
- // Show searching indicator
- document.getElementById('departure-airport-lookup-results').innerHTML =
- '
Searching...
';
-
- // Debounce the search - wait 300ms after user stops typing
- departureAirportLookupTimeout = setTimeout(() => {
- performDepartureAirportLookup(codeOrName);
- }, 300);
- }
-
- async function performArrivalAirportLookup(codeOrName) {
- try {
- const cleanInput = codeOrName.trim();
-
- if (cleanInput.length < 2) {
- clearArrivalAirportLookup();
- return;
- }
-
- // Call the airport lookup API
- const response = await authenticatedFetch(`/api/v1/airport/lookup/${encodeURIComponent(cleanInput)}`);
-
- if (!response.ok) {
- throw new Error('Failed to fetch airport data');
- }
-
- const matches = await response.json();
- displayArrivalAirportLookupResults(matches, cleanInput);
-
- } catch (error) {
- console.error('Arrival airport lookup error:', error);
- document.getElementById('arrival-airport-lookup-results').innerHTML =
- '
Lookup failed - will use as entered
';
- }
- }
-
- async function performDepartureAirportLookup(codeOrName) {
- try {
- const cleanInput = codeOrName.trim();
-
- if (cleanInput.length < 2) {
- clearDepartureAirportLookup();
- return;
- }
-
- // Call the airport lookup API
- const response = await authenticatedFetch(`/api/v1/airport/lookup/${encodeURIComponent(cleanInput)}`);
-
- if (!response.ok) {
- throw new Error('Failed to fetch airport data');
- }
-
- const matches = await response.json();
- displayDepartureAirportLookupResults(matches, cleanInput);
-
- } catch (error) {
- console.error('Departure airport lookup error:', error);
- document.getElementById('departure-airport-lookup-results').innerHTML =
- '
Lookup failed - will use as entered
';
- }
- }
-
- function displayArrivalAirportLookupResults(matches, searchTerm) {
- const resultsDiv = document.getElementById('arrival-airport-lookup-results');
-
- if (matches.length === 0) {
- resultsDiv.innerHTML = '
No matches found - will use as entered
';
- } else {
- // Show matches as clickable options (single or multiple)
- const matchText = matches.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:';
- const listHtml = matches.map(airport => `
-
-
-
${airport.icao}
-
${airport.name}
- ${airport.city ? `
${airport.city}, ${airport.country}
` : ''}
-
-
- `).join('');
-
- resultsDiv.innerHTML = `
-
- ${matchText}
-
-
- ${listHtml}
-
- `;
- }
- }
-
- function displayDepartureAirportLookupResults(matches, searchTerm) {
- const resultsDiv = document.getElementById('departure-airport-lookup-results');
-
- if (matches.length === 0) {
- resultsDiv.innerHTML = '
No matches found - will use as entered
';
- } else {
- // Show matches as clickable options (single or multiple)
- const matchText = matches.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:';
- const listHtml = matches.map(airport => `
-
-
-
${airport.icao}
-
${airport.name}
- ${airport.city ? `
${airport.city}, ${airport.country}
` : ''}
-
-
- `).join('');
-
- resultsDiv.innerHTML = `
-
- ${matchText}
-
-
- ${listHtml}
-
- `;
- }
- }
-
- // Airport selection functions
- function selectArrivalAirport(icaoCode) {
- document.getElementById('in_from').value = icaoCode;
- clearArrivalAirportLookup();
- }
-
- function selectDepartureAirport(icaoCode) {
- document.getElementById('out_to').value = icaoCode;
- clearDepartureAirportLookup();
- }
-
- // Position tooltip dynamically to avoid being cut off
- function positionTooltip(event) {
- const indicator = event.currentTarget;
- const tooltip = indicator.querySelector('.tooltip-text');
- if (!tooltip) return;
-
- const rect = indicator.getBoundingClientRect();
- const tooltipWidth = 300;
- const tooltipHeight = tooltip.offsetHeight || 100;
-
- // Position to the right of the indicator by default
- let left = rect.right + 10;
- let top = rect.top + (rect.height / 2) - (tooltipHeight / 2);
-
- // Check if tooltip would go off the right edge
- if (left + tooltipWidth > window.innerWidth) {
- // Position to the left instead
- left = rect.left - tooltipWidth - 10;
- }
-
- // Check if tooltip would go off the bottom
- if (top + tooltipHeight > window.innerHeight) {
- top = window.innerHeight - tooltipHeight - 10;
- }
-
- // Check if tooltip would go off the top
- if (top < 10) {
- top = 10;
- }
-
- tooltip.style.left = left + 'px';
- tooltip.style.top = top + 'px';
- }
+ });
// Local Flight (Book Out) Modal Functions
function openLocalFlightModal(flightType = 'LOCAL') {
@@ -2932,95 +2662,83 @@
const destGroup = document.getElementById('departure-destination-group');
const destInput = document.getElementById('local_out_to');
const destLabel = document.getElementById('departure-destination-label');
+ const etdGroup = document.getElementById('local-etd-group');
if (flightType === 'DEPARTURE') {
destGroup.style.display = 'block';
destInput.required = true;
destLabel.textContent = 'Destination Airport *';
+ etdGroup.style.display = 'block';
} else {
destGroup.style.display = 'none';
destInput.required = false;
destInput.value = '';
destLabel.textContent = 'Destination Airport';
+ etdGroup.style.display = 'none';
+ document.getElementById('local_etd_date').value = '';
+ document.getElementById('local_etd_time').value = '';
}
- }
-
- // Handle aircraft lookup for local flights
- let localAircraftLookupTimeout;
- function handleLocalAircraftLookup(registration) {
- // Clear previous timeout
- if (localAircraftLookupTimeout) {
- clearTimeout(localAircraftLookupTimeout);
- }
-
- // Clear results if input is too short
- if (registration.length < 4) {
- clearLocalAircraftLookup();
- return;
- }
-
- // Show searching indicator
- document.getElementById('local-aircraft-lookup-results').innerHTML =
- '
Searching...
';
-
- // Debounce the search - wait 300ms after user stops typing
- localAircraftLookupTimeout = setTimeout(() => {
- performLocalAircraftLookup(registration);
- }, 300);
- }
-
- async function performLocalAircraftLookup(registration) {
- try {
- // Clean the input (remove non-alphanumeric characters and make uppercase)
- const cleanInput = registration.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
-
- if (cleanInput.length < 4) {
- clearLocalAircraftLookup();
- return;
- }
-
- // Call the API
- const response = await authenticatedFetch(`/api/v1/aircraft/lookup/${cleanInput}`);
-
- if (!response.ok) {
- throw new Error('Failed to fetch aircraft data');
- }
-
- const matches = await response.json();
- displayLocalAircraftLookupResults(matches, cleanInput);
-
- } catch (error) {
- console.error('Aircraft lookup error:', error);
- document.getElementById('local-aircraft-lookup-results').innerHTML =
- '
Lookup failed - please enter manually
';
- }
- }
-
- function displayLocalAircraftLookupResults(matches, searchTerm) {
- const resultsDiv = document.getElementById('local-aircraft-lookup-results');
- if (matches.length === 0) {
- resultsDiv.innerHTML = '
No matches found
';
- } else if (matches.length === 1) {
- // Unique match found - auto-populate
- const aircraft = matches[0];
- resultsDiv.innerHTML = `
-
- ✓ ${aircraft.manufacturer_name} ${aircraft.model} (${aircraft.type_code})
-
- `;
-
- // Auto-populate the form fields
- document.getElementById('local_registration').value = aircraft.registration;
- document.getElementById('local_type').value = aircraft.type_code;
-
- } else {
- // Multiple matches - show list but don't auto-populate
- resultsDiv.innerHTML = `
-
- Multiple matches found (${matches.length}) - please be more specific
-
- `;
+ // Populate ETD time slots
+ populateETDTimeSlots();
+ }
+
+ function getNearest15MinSlot() {
+ const now = new Date();
+ const minutes = now.getMinutes();
+ const remainder = minutes % 15;
+ const roundedMinutes = remainder >= 7.5 ? minutes + (15 - remainder) : minutes - remainder;
+
+ const future = new Date(now);
+ future.setMinutes(roundedMinutes === 60 ? 0 : roundedMinutes);
+ if (roundedMinutes === 60) {
+ future.setHours(future.getHours() + 1);
+ }
+
+ return future;
+ }
+
+ function populateETDTimeSlots() {
+ const timeSelect = document.getElementById('local_etd_time');
+ const dateInput = document.getElementById('local_etd_date');
+
+ // Get the selected date or use today
+ let selectedDate = dateInput.value;
+ if (!selectedDate) {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, '0');
+ const day = String(today.getDate()).padStart(2, '0');
+ selectedDate = `${year}-${month}-${day}`;
+ dateInput.value = selectedDate;
+ }
+
+ // Clear and repopulate
+ timeSelect.innerHTML = '
';
+
+ const future = getNearest15MinSlot();
+ const selectedDateTime = new Date(selectedDate + 'T00:00:00');
+
+ // If selected date is today, start from nearest 15-min slot; otherwise start from 06:00
+ let startHour = selectedDateTime.toDateString() === new Date().toDateString() ? future.getHours() : 6;
+ let startMinute = selectedDateTime.toDateString() === new Date().toDateString() ? future.getMinutes() : 0;
+
+ // Generate 15-minute slots from start time to 22:00
+ for (let hour = startHour; hour < 24; hour++) {
+ for (let minute = (hour === startHour ? startMinute : 0); minute < 60; minute += 15) {
+ const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
+ const option = document.createElement('option');
+ option.value = timeStr;
+ option.textContent = timeStr;
+
+ // Auto-select the nearest slot for today
+ if (selectedDateTime.toDateString() === new Date().toDateString() &&
+ hour === future.getHours() && minute === future.getMinutes()) {
+ option.selected = true;
+ }
+
+ timeSelect.appendChild(option);
+ }
}
}
@@ -3225,12 +2943,12 @@
// Skip the hidden id field and empty values
if (key === 'id') return;
- // Handle date/time combination for departure
- if (key === 'departure_date' || key === 'departure_time') {
- if (!flightData.departure_dt && formData.get('departure_date') && formData.get('departure_time')) {
- const dateStr = formData.get('departure_date');
- const timeStr = formData.get('departure_time');
- flightData.departure_dt = new Date(`${dateStr}T${timeStr}`).toISOString();
+ // Handle date/time combination for ETD (departures)
+ if (key === 'etd_date' || key === 'etd_time') {
+ if (!flightData.etd && formData.get('etd_date') && formData.get('etd_time')) {
+ const dateStr = formData.get('etd_date');
+ const timeStr = formData.get('etd_time');
+ flightData.etd = new Date(`${dateStr}T${timeStr}`).toISOString();
}
return;
}
diff --git a/web/lookups.js b/web/lookups.js
new file mode 100644
index 0000000..e566ba6
--- /dev/null
+++ b/web/lookups.js
@@ -0,0 +1,317 @@
+/**
+ * Lookup Utilities - Reusable functions for aircraft and airport lookups
+ */
+
+/**
+ * Creates a reusable lookup handler
+ * @param {string} fieldId - ID of the input field
+ * @param {string} resultsId - ID of the results container
+ * @param {function} selectCallback - Function to call when item is selected
+ * @param {object} options - Additional options (minLength, debounceMs, etc.)
+ */
+function createLookup(fieldId, resultsId, selectCallback, options = {}) {
+ const defaults = {
+ minLength: 2,
+ debounceMs: 300,
+ isAirport: false,
+ isAircraft: false,
+ maxResults: 10
+ };
+ const config = { ...defaults, ...options };
+ let debounceTimeout;
+
+ const lookup = {
+ // Main handler called by oninput
+ handle: (value) => {
+ clearTimeout(debounceTimeout);
+
+ if (!value || value.trim().length < config.minLength) {
+ lookup.clear();
+ return;
+ }
+
+ lookup.showSearching();
+ debounceTimeout = setTimeout(() => {
+ lookup.perform(value);
+ }, config.debounceMs);
+ },
+
+ // Perform the lookup
+ perform: async (searchTerm) => {
+ try {
+ const cleanInput = searchTerm.trim();
+ let endpoint;
+
+ if (config.isAircraft) {
+ const cleaned = cleanInput.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
+ if (cleaned.length < config.minLength) {
+ lookup.clear();
+ return;
+ }
+ endpoint = `/api/v1/aircraft/lookup/${cleaned}`;
+ } else if (config.isAirport) {
+ endpoint = `/api/v1/airport/lookup/${encodeURIComponent(cleanInput)}`;
+ }
+
+ if (!endpoint) throw new Error('Invalid lookup type');
+
+ const response = await authenticatedFetch(endpoint);
+ if (!response.ok) throw new Error('Lookup failed');
+
+ const results = await response.json();
+ lookup.display(results, cleanInput);
+ } catch (error) {
+ console.error('Lookup error:', error);
+ lookup.showError();
+ }
+ },
+
+ // Display results
+ display: (results, searchTerm) => {
+ const resultsDiv = document.getElementById(resultsId);
+
+ if (config.isAircraft) {
+ // Aircraft lookup: auto-populate on single match, show message on multiple
+ if (!results || results.length === 0) {
+ resultsDiv.innerHTML = '
No matches found
';
+ } else if (results.length === 1) {
+ // Single match - auto-populate
+ const aircraft = results[0];
+ resultsDiv.innerHTML = `
+
+ ✓ ${aircraft.manufacturer_name || ''} ${aircraft.model || aircraft.type_code || ''}
+
+ `;
+
+ // Auto-populate the form fields
+ const field = document.getElementById(fieldId);
+ if (field) field.value = aircraft.registration;
+
+ // Also populate type field
+ let typeFieldId;
+ if (fieldId === 'ac_reg') {
+ typeFieldId = 'ac_type';
+ } else if (fieldId === 'local_registration') {
+ typeFieldId = 'local_type';
+ }
+
+ if (typeFieldId) {
+ const typeField = document.getElementById(typeFieldId);
+ if (typeField) typeField.value = aircraft.type_code || '';
+ }
+ } else {
+ // Multiple matches
+ resultsDiv.innerHTML = `
+
+ Multiple matches found (${results.length}) - please be more specific
+
+ `;
+ }
+ } else {
+ // Airport lookup: show list of options
+ if (!results || results.length === 0) {
+ resultsDiv.innerHTML = '
No matches found - will use as entered
';
+ return;
+ }
+
+ const itemsToShow = results.slice(0, config.maxResults);
+ const matchText = itemsToShow.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:';
+
+ let html = `
${matchText}
`;
+
+ itemsToShow.forEach(item => {
+ html += `
+
+
${item.icao}
+
${item.name || '-'}
+ ${item.city ? `
${item.city}, ${item.country}
` : ''}
+
+ `;
+ });
+
+ html += '
';
+ resultsDiv.innerHTML = html;
+ }
+ },
+
+ // Show searching state
+ showSearching: () => {
+ const resultsDiv = document.getElementById(resultsId);
+ if (resultsDiv) {
+ resultsDiv.innerHTML = '
Searching...
';
+ }
+ },
+
+ // Show error state
+ showError: () => {
+ const resultsDiv = document.getElementById(resultsId);
+ if (resultsDiv) {
+ resultsDiv.innerHTML = '
Lookup failed - will use as entered
';
+ }
+ },
+
+ // Clear results
+ clear: () => {
+ const resultsDiv = document.getElementById(resultsId);
+ if (resultsDiv) {
+ resultsDiv.innerHTML = '';
+ }
+ },
+
+ // Set the selected value
+ setValue: (value) => {
+ const field = document.getElementById(fieldId);
+ if (field) {
+ field.value = value;
+ }
+ lookup.clear();
+ if (selectCallback) selectCallback(value);
+ }
+ };
+
+ return lookup;
+}
+
+/**
+ * Global lookup manager for all lookups on the page
+ */
+const lookupManager = {
+ lookups: {},
+
+ // Register a lookup instance
+ register: (name, lookup) => {
+ lookupManager.lookups[name] = lookup;
+ },
+
+ // Generic item selection handler
+ selectItem: (resultsId, fieldId, itemCode) => {
+ const field = document.getElementById(fieldId);
+ if (field) {
+ field.value = itemCode;
+ }
+ const resultsDiv = document.getElementById(resultsId);
+ if (resultsDiv) {
+ resultsDiv.innerHTML = '';
+ }
+ }
+};
+
+// Initialize all lookups when page loads
+function initializeLookups() {
+ // Create reusable lookup instances
+ const arrivalAirportLookup = createLookup(
+ 'in_from',
+ 'arrival-airport-lookup-results',
+ null,
+ { isAirport: true, minLength: 2 }
+ );
+ lookupManager.register('arrival-airport', arrivalAirportLookup);
+
+ const departureAirportLookup = createLookup(
+ 'out_to',
+ 'departure-airport-lookup-results',
+ null,
+ { isAirport: true, minLength: 2 }
+ );
+ lookupManager.register('departure-airport', departureAirportLookup);
+
+ const localOutToLookup = createLookup(
+ 'local_out_to',
+ 'local-out-to-lookup-results',
+ null,
+ { isAirport: true, minLength: 2 }
+ );
+ lookupManager.register('local-out-to', localOutToLookup);
+
+ const aircraftLookup = createLookup(
+ 'ac_reg',
+ 'aircraft-lookup-results',
+ null,
+ { isAircraft: true, minLength: 4, debounceMs: 300 }
+ );
+ lookupManager.register('aircraft', aircraftLookup);
+
+ const localAircraftLookup = createLookup(
+ 'local_registration',
+ 'local-aircraft-lookup-results',
+ null,
+ { isAircraft: true, minLength: 4, debounceMs: 300 }
+ );
+ lookupManager.register('local-aircraft', localAircraftLookup);
+}
+
+// Initialize on DOM ready or immediately if already loaded
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeLookups);
+} else {
+ initializeLookups();
+}
+
+/**
+ * Convenience wrapper functions for backward compatibility
+ */
+function handleArrivalAirportLookup(value) {
+ const lookup = lookupManager.lookups['arrival-airport'];
+ if (lookup) lookup.handle(value);
+}
+
+function handleDepartureAirportLookup(value) {
+ const lookup = lookupManager.lookups['departure-airport'];
+ if (lookup) lookup.handle(value);
+}
+
+function handleLocalOutToAirportLookup(value) {
+ const lookup = lookupManager.lookups['local-out-to'];
+ if (lookup) lookup.handle(value);
+}
+
+function handleAircraftLookup(value) {
+ const lookup = lookupManager.lookups['aircraft'];
+ if (lookup) lookup.handle(value);
+}
+
+function handleLocalAircraftLookup(value) {
+ const lookup = lookupManager.lookups['local-aircraft'];
+ if (lookup) lookup.handle(value);
+}
+
+function clearArrivalAirportLookup() {
+ const lookup = lookupManager.lookups['arrival-airport'];
+ if (lookup) lookup.clear();
+}
+
+function clearDepartureAirportLookup() {
+ const lookup = lookupManager.lookups['departure-airport'];
+ if (lookup) lookup.clear();
+}
+
+function clearLocalOutToAirportLookup() {
+ const lookup = lookupManager.lookups['local-out-to'];
+ if (lookup) lookup.clear();
+}
+
+function clearAircraftLookup() {
+ const lookup = lookupManager.lookups['aircraft'];
+ if (lookup) lookup.clear();
+}
+
+function clearLocalAircraftLookup() {
+ const lookup = lookupManager.lookups['local-aircraft'];
+ if (lookup) lookup.clear();
+}
+
+function selectArrivalAirport(icaoCode) {
+ lookupManager.selectItem('arrival-airport-lookup-results', 'in_from', icaoCode);
+}
+
+function selectDepartureAirport(icaoCode) {
+ lookupManager.selectItem('departure-airport-lookup-results', 'out_to', icaoCode);
+}
+
+function selectLocalOutToAirport(icaoCode) {
+ lookupManager.selectItem('local-out-to-lookup-results', 'local_out_to', icaoCode);
+}
+
+function selectLocalAircraft(registration) {
+ lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration);
+}