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();
+ }