From ab3319af067d03cde1f2974bdc1c384bdc550cdd Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Fri, 12 Dec 2025 12:11:00 -0500 Subject: [PATCH] Feature enhancement --- backend/alembic/versions/002_local_flights.py | 19 +- backend/app/api/endpoints/departures.py | 2 +- backend/app/api/endpoints/public.py | 6 +- backend/app/crud/crud_departure.py | 12 +- backend/app/crud/crud_local_flight.py | 18 +- backend/app/models/departure.py | 7 +- backend/app/models/local_flight.py | 5 +- backend/app/schemas/departure.py | 12 +- backend/app/schemas/local_flight.py | 11 +- web/admin.css | 53 ++ web/admin.html | 466 ++++-------------- web/lookups.js | 317 ++++++++++++ 12 files changed, 512 insertions(+), 416 deletions(-) create mode 100644 web/lookups.js 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); +}