From c1accd82c585eed8fc911277a5e4b879a7aa1512 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Tue, 21 Oct 2025 21:06:38 +0000 Subject: [PATCH] Live ac search --- backend/app/api/api.py | 5 +- backend/app/api/endpoints/aircraft.py | 60 +++++++ backend/app/schemas/ppr.py | 8 +- web/admin.html | 229 ++++++++++++++++++-------- 4 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 backend/app/api/endpoints/aircraft.py diff --git a/backend/app/api/api.py b/backend/app/api/api.py index 9a324e0..3d25739 100644 --- a/backend/app/api/api.py +++ b/backend/app/api/api.py @@ -1,8 +1,9 @@ from fastapi import APIRouter -from app.api.endpoints import auth, pprs, public +from app.api.endpoints import auth, pprs, public, aircraft 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"]) \ No newline at end of file +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 diff --git a/backend/app/api/endpoints/aircraft.py b/backend/app/api/endpoints/aircraft.py new file mode 100644 index 0000000..b6dba8b --- /dev/null +++ b/backend/app/api/endpoints/aircraft.py @@ -0,0 +1,60 @@ +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 Aircraft +from app.schemas.ppr import Aircraft as AircraftSchema +from app.models.ppr import User + +router = APIRouter() + + +@router.get("/lookup/{registration}", response_model=List[AircraftSchema]) +async def lookup_aircraft_by_registration( + registration: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """ + Lookup aircraft by registration (clean match). + Removes non-alphanumeric characters from input for matching. + """ + # Clean the input registration (remove non-alphanumeric characters) + clean_input = ''.join(c for c in registration if c.isalnum()).upper() + + if len(clean_input) < 4: + return [] + + # Query aircraft table using clean_reg column + aircraft_list = db.query(Aircraft).filter( + Aircraft.clean_reg.like(f"{clean_input}%") + ).limit(10).all() + + return aircraft_list + + +@router.get("/search", response_model=List[AircraftSchema]) +async def search_aircraft( + q: Optional[str] = Query(None, description="Search query for registration, type, or manufacturer"), + 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 aircraft by registration, type code, or manufacturer name. + """ + if not q or len(q) < 2: + return [] + + # Clean search term + clean_query = ''.join(c for c in q if c.isalnum()).upper() + + # Search across multiple fields + aircraft_list = db.query(Aircraft).filter( + (Aircraft.clean_reg.like(f"%{clean_query}%")) | + (Aircraft.type_code.like(f"%{q.upper()}%")) | + (Aircraft.manufacturer_name.like(f"%{q}%")) | + (Aircraft.model.like(f"%{q}%")) + ).limit(limit).all() + + return aircraft_list \ No newline at end of file diff --git a/backend/app/schemas/ppr.py b/backend/app/schemas/ppr.py index 9a5b559..ad963f7 100644 --- a/backend/app/schemas/ppr.py +++ b/backend/app/schemas/ppr.py @@ -164,13 +164,15 @@ class Airport(AirportBase): class AircraftBase(BaseModel): icao24: Optional[str] = None registration: Optional[str] = None - manufacturericao: Optional[str] = None - typecode: Optional[str] = None - manufacturername: Optional[str] = None + manufacturer_icao: Optional[str] = None + type_code: Optional[str] = None + manufacturer_name: Optional[str] = None model: Optional[str] = None clean_reg: Optional[str] = None class Aircraft(AircraftBase): + id: int + class Config: from_attributes = True \ No newline at end of file diff --git a/web/admin.html b/web/admin.html index 7e621ef..0f2c089 100644 --- a/web/admin.html +++ b/web/admin.html @@ -41,16 +41,25 @@ padding: 2rem; } - .controls { - background: white; - padding: 1.5rem; - border-radius: 8px; + .top-menu { + background: #2c3e50; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1); - margin-bottom: 2rem; + } + + .menu-left { + display: flex; + gap: 1rem; + align-items: center; + } + + .menu-right { display: flex; gap: 1rem; align-items: center; - flex-wrap: wrap; } .btn { @@ -421,14 +430,48 @@ margin-bottom: 1rem; } + /* Aircraft Lookup Styles */ + #aircraft-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; + } + + .aircraft-match { + padding: 0.3rem; + background-color: #e8f5e8; + border: 1px solid #c3e6c3; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-weight: bold; + } + + .aircraft-no-match { + color: #6c757d; + font-style: italic; + } + + .aircraft-searching { + color: #007bff; + } + @media (max-width: 768px) { .container { padding: 1rem; } - .controls { + .top-menu { flex-direction: column; - align-items: stretch; + gap: 1rem; + padding: 1rem; + } + + .menu-left, .menu-right { + justify-content: center; } .form-grid { @@ -452,28 +495,20 @@
-
-
+
+ + +
+ +
@@ -604,7 +639,8 @@
- + +
@@ -715,7 +751,6 @@ currentUser = cachedUser; document.getElementById('current-user').textContent = cachedUser; loadPPRs(); - setDefaultDates(); return; } } @@ -778,7 +813,6 @@ hideLogin(); loadPPRs(); - setDefaultDates(); } else { throw new Error(data.detail || 'Authentication failed'); } @@ -814,11 +848,6 @@ showLogin(); } - function setDefaultDates() { - const today = new Date(); - document.getElementById('viewDate').value = today.toISOString().split('T')[0]; - } - // Enhanced fetch wrapper with token expiry handling async function authenticatedFetch(url, options = {}) { if (!accessToken) { @@ -861,13 +890,9 @@ document.getElementById('arrivals-no-data').style.display = 'none'; try { - const viewDate = document.getElementById('viewDate').value; - const showAll = document.getElementById('showAllStatuses').checked; - - let url = '/api/v1/pprs/?limit=1000'; - if (viewDate) { - url += `&date_from=${viewDate}&date_to=${viewDate}`; - } + // Always load today's date + const today = new Date().toISOString().split('T')[0]; + let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`; const response = await authenticatedFetch(url); @@ -877,17 +902,10 @@ const allPPRs = await response.json(); - // Filter for arrivals (NEW, CONFIRMED, or all if showAll is checked) - let arrivals; - if (showAll) { - // Show all PPRs with ETA for the selected date - arrivals = allPPRs.filter(ppr => ppr.eta); - } else { - // Show only NEW and CONFIRMED with ETA - arrivals = allPPRs.filter(ppr => - (ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && ppr.eta - ); - } + // Filter for arrivals (NEW and CONFIRMED with ETA only) + const arrivals = allPPRs.filter(ppr => + (ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && ppr.eta + ); displayArrivals(arrivals); } catch (error) { @@ -907,13 +925,9 @@ document.getElementById('departures-no-data').style.display = 'none'; try { - const viewDate = document.getElementById('viewDate').value; - const showAll = document.getElementById('showAllStatuses').checked; - - let url = '/api/v1/pprs/?limit=1000'; - if (viewDate) { - url += `&date_from=${viewDate}&date_to=${viewDate}`; - } + // Always load today's date + const today = new Date().toISOString().split('T')[0]; + let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`; const response = await authenticatedFetch(url); @@ -923,15 +937,8 @@ const allPPRs = await response.json(); - // Filter for departures (LANDED, or include DEPARTED if showAll is checked) - let departures; - if (showAll) { - departures = allPPRs.filter(ppr => - ppr.status === 'LANDED' || ppr.status === 'DEPARTED' - ); - } else { - departures = allPPRs.filter(ppr => ppr.status === 'LANDED'); - } + // Filter for departures (LANDED status only) + const departures = allPPRs.filter(ppr => ppr.status === 'LANDED'); displayDepartures(departures); } catch (error) { @@ -1063,7 +1070,15 @@ document.getElementById('ppr-form').reset(); document.getElementById('ppr-id').value = ''; + // Clear aircraft lookup results + clearAircraftLookup(); + document.getElementById('pprModal').style.display = 'block'; + + // Auto-focus on aircraft registration field + setTimeout(() => { + document.getElementById('ac_reg').focus(); + }, 100); } async function openPPRModal(pprId) { @@ -1270,6 +1285,90 @@ closePPRModal(); } } + + // 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 = ''; + } \ No newline at end of file