From 91b734426c1e8b2e29d9a269a1c183755cdccd02 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Thu, 23 Oct 2025 16:59:37 +0000 Subject: [PATCH] Airport lookup --- backend/app/api/api.py | 5 +- backend/app/api/endpoints/airport.py | 71 +++++++ web/admin.html | 293 ++++++++++++++++++++++++++- 3 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 backend/app/api/endpoints/airport.py diff --git a/backend/app/api/api.py b/backend/app/api/api.py index 3d25739..935b4fe 100644 --- a/backend/app/api/api.py +++ b/backend/app/api/api.py @@ -1,9 +1,10 @@ from fastapi import APIRouter -from app.api.endpoints import auth, pprs, public, aircraft +from app.api.endpoints import auth, pprs, public, aircraft, airport api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["authentication"]) api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"]) api_router.include_router(public.router, prefix="/public", tags=["public"]) -api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"]) \ No newline at end of file +api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"]) +api_router.include_router(airport.router, prefix="/airport", tags=["airport"]) \ No newline at end of file diff --git a/backend/app/api/endpoints/airport.py b/backend/app/api/endpoints/airport.py new file mode 100644 index 0000000..0b42211 --- /dev/null +++ b/backend/app/api/endpoints/airport.py @@ -0,0 +1,71 @@ +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from app.api.deps import get_db, get_current_active_user +from app.models.ppr import Airport +from app.schemas.ppr import Airport as AirportSchema +from app.models.ppr import User + +router = APIRouter() + + +@router.get("/lookup/{code_or_name}", response_model=List[AirportSchema]) +async def lookup_airport_by_code_or_name( + code_or_name: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """ + Lookup airport by ICAO code or name. + If input is 4 characters and all uppercase letters, treat as ICAO code. + Otherwise, search by name. + """ + clean_input = code_or_name.strip().upper() + + if len(clean_input) < 2: + return [] + + # Check if input looks like an ICAO code (4 letters) + if len(clean_input) == 4 and clean_input.isalpha(): + # Exact ICAO match first + airport = db.query(Airport).filter(Airport.icao == clean_input).first() + if airport: + return [airport] + # Then search ICAO codes that start with input + airports = db.query(Airport).filter( + Airport.icao.like(clean_input + "%") + ).limit(5).all() + return airports + else: + # Search by name (case-insensitive partial match) + airports = db.query(Airport).filter( + Airport.name.ilike("%" + code_or_name + "%") + ).limit(10).all() + return airports + + +@router.get("/search", response_model=List[AirportSchema]) +async def search_airports( + q: Optional[str] = Query(None, description="Search query for ICAO code, IATA code, or airport name"), + limit: int = Query(10, ge=1, le=100, description="Maximum number of results"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """ + Search airports by ICAO code, IATA code, or name. + """ + if not q or len(q.strip()) < 2: + return [] + + search_term = q.strip() + clean_search = search_term.upper() + + # Search across ICAO, IATA, and name fields + airports = db.query(Airport).filter( + (Airport.icao.like("%" + clean_search + "%")) | + (Airport.iata.like("%" + clean_search + "%")) | + (Airport.name.ilike("%" + search_term + "%")) | + (Airport.city.ilike("%" + search_term + "%")) + ).limit(limit).all() + + return airports \ No newline at end of file diff --git a/web/admin.html b/web/admin.html index 765b8cf..49e9747 100644 --- a/web/admin.html +++ b/web/admin.html @@ -464,6 +464,78 @@ color: #007bff; } + /* Airport Lookup Styles */ + #arrival-airport-lookup-results, #departure-airport-lookup-results { + margin-top: 0.5rem; + padding: 0.5rem; + background-color: #f8f9fa; + border-radius: 4px; + font-size: 0.9rem; + min-height: 20px; + border: 1px solid #e9ecef; + } + + .airport-match { + padding: 0.3rem; + background-color: #e8f5e8; + border: 1px solid #c3e6c3; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-weight: bold; + } + + .airport-no-match { + color: #6c757d; + font-style: italic; + } + + .airport-searching { + color: #007bff; + } + + .airport-list { + max-height: 200px; + overflow-y: auto; + border: 1px solid #dee2e6; + border-radius: 4px; + background-color: white; + } + + .airport-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; + } + + .airport-option:hover { + background-color: #f8f9fa; + } + + .airport-option:last-child { + border-bottom: none; + } + + .airport-code { + font-family: 'Courier New', monospace; + font-weight: bold; + color: #495057; + } + + .airport-name { + color: #6c757d; + font-size: 0.85rem; + } + + .airport-location { + color: #868e96; + font-size: 0.8rem; + font-style: italic; + } + .notification { position: fixed; top: 20px; @@ -652,7 +724,7 @@
- +
@@ -660,7 +732,8 @@
- + +
@@ -681,7 +754,8 @@
- + +
@@ -1189,7 +1263,7 @@ // Ensure the datetime string is treated as UTC const utcDateStr = dateStr.includes('Z') ? dateStr : dateStr + 'Z'; const date = new Date(utcDateStr); - return date.toISOString().slice(11, 16) + 'Z'; + return date.toISOString().slice(11, 16); } function formatDateTime(dateStr) { @@ -1197,7 +1271,7 @@ // Ensure the datetime string is treated as UTC const utcDateStr = dateStr.includes('Z') ? dateStr : dateStr + 'Z'; const date = new Date(utcDateStr); - return date.toISOString().slice(0, 10) + ' ' + date.toISOString().slice(11, 16) + 'Z'; + return date.toISOString().slice(0, 10) + ' ' + date.toISOString().slice(11, 16); } // Modal functions @@ -1233,6 +1307,8 @@ // Clear aircraft lookup results clearAircraftLookup(); + clearArrivalAirportLookup(); + clearDepartureAirportLookup(); document.getElementById('pprModal').style.display = 'block'; @@ -1659,6 +1735,213 @@ function clearAircraftLookup() { document.getElementById('aircraft-lookup-results').innerHTML = ''; } + + function clearArrivalAirportLookup() { + document.getElementById('arrival-airport-lookup-results').innerHTML = ''; + } + + function clearDepartureAirportLookup() { + document.getElementById('departure-airport-lookup-results').innerHTML = ''; + } + + // Airport Lookup Functions + let arrivalAirportLookupTimeout; + let departureAirportLookupTimeout; + + function handleArrivalAirportLookup(codeOrName) { + // Clear previous timeout + if (arrivalAirportLookupTimeout) { + clearTimeout(arrivalAirportLookupTimeout); + } + + // 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 if (matches.length === 1) { + // Unique match found - auto-populate with ICAO code + const airport = matches[0]; + resultsDiv.innerHTML = ` +
+ ✓ ${airport.name} (${airport.icao}) +
+ `; + + // Auto-populate with ICAO code + document.getElementById('in_from').value = airport.icao; + + } else { + // Multiple matches - show clickable list + const listHtml = matches.map(airport => ` +
+
+
${airport.icao}
+
${airport.name}
+ ${airport.city ? `
${airport.city}, ${airport.country}
` : ''} +
+
+ `).join(''); + + resultsDiv.innerHTML = ` +
+ Multiple matches found - select one: +
+
+ ${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 if (matches.length === 1) { + // Unique match found - auto-populate with ICAO code + const airport = matches[0]; + resultsDiv.innerHTML = ` +
+ ✓ ${airport.name} (${airport.icao}) +
+ `; + + // Auto-populate with ICAO code + document.getElementById('out_to').value = airport.icao; + + } else { + // Multiple matches - show clickable list + const listHtml = matches.map(airport => ` +
+
+
${airport.icao}
+
${airport.name}
+ ${airport.city ? `
${airport.city}, ${airport.country}
` : ''} +
+
+ `).join(''); + + resultsDiv.innerHTML = ` +
+ Multiple matches found - select one: +
+
+ ${listHtml} +
+ `; + } + } + + function clearArrivalAirportLookup() { + document.getElementById('arrival-airport-lookup-results').innerHTML = ''; + } + + function clearDepartureAirportLookup() { + document.getElementById('departure-airport-lookup-results').innerHTML = ''; + } + + // Airport selection functions + function selectArrivalAirport(icaoCode) { + document.getElementById('in_from').value = icaoCode; + clearArrivalAirportLookup(); + } + + function selectDepartureAirport(icaoCode) { + document.getElementById('out_to').value = icaoCode; + clearDepartureAirportLookup(); + } \ No newline at end of file