Live ac search

This commit is contained in:
James Pattinson
2025-10-21 21:06:38 +00:00
parent 4f952a5a1b
commit c1accd82c5
4 changed files with 232 additions and 70 deletions

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter 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 = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"]) api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"]) api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"])
api_router.include_router(public.router, prefix="/public", tags=["public"]) api_router.include_router(public.router, prefix="/public", tags=["public"])
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])

View File

@@ -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

View File

@@ -164,13 +164,15 @@ class Airport(AirportBase):
class AircraftBase(BaseModel): class AircraftBase(BaseModel):
icao24: Optional[str] = None icao24: Optional[str] = None
registration: Optional[str] = None registration: Optional[str] = None
manufacturericao: Optional[str] = None manufacturer_icao: Optional[str] = None
typecode: Optional[str] = None type_code: Optional[str] = None
manufacturername: Optional[str] = None manufacturer_name: Optional[str] = None
model: Optional[str] = None model: Optional[str] = None
clean_reg: Optional[str] = None clean_reg: Optional[str] = None
class Aircraft(AircraftBase): class Aircraft(AircraftBase):
id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -41,16 +41,25 @@
padding: 2rem; padding: 2rem;
} }
.controls { .top-menu {
background: white; background: #2c3e50;
padding: 1.5rem; padding: 1rem 2rem;
border-radius: 8px; display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); 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; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.btn { .btn {
@@ -421,14 +430,48 @@
margin-bottom: 1rem; 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) { @media (max-width: 768px) {
.container { .container {
padding: 1rem; padding: 1rem;
} }
.controls { .top-menu {
flex-direction: column; flex-direction: column;
align-items: stretch; gap: 1rem;
padding: 1rem;
}
.menu-left, .menu-right {
justify-content: center;
} }
.form-grid { .form-grid {
@@ -452,28 +495,20 @@
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
<div class="container"> <div class="top-menu">
<div class="controls"> <div class="menu-left">
<button class="btn btn-success" onclick="openNewPPRModal()"> <button class="btn btn-success" onclick="openNewPPRModal()">
New PPR Entry New PPR Entry
</button> </button>
<div class="filter-group">
<label for="viewDate">Date:</label>
<input type="date" id="viewDate" onchange="loadPPRs()">
</div> </div>
<div class="menu-right">
<button class="btn btn-primary" onclick="loadPPRs()"> <button class="btn btn-primary" onclick="loadPPRs()">
🔄 Refresh 🔄 Refresh
</button> </button>
</div>
</div>
<div class="filter-group" style="margin-left: auto;"> <div class="container">
<label>
<input type="checkbox" id="showAllStatuses" onchange="loadPPRs()">
Show all statuses (including departed/canceled)
</label>
</div>
</div>
<!-- Arrivals Table --> <!-- Arrivals Table -->
<div class="ppr-table"> <div class="ppr-table">
@@ -604,7 +639,8 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label for="ac_reg">Aircraft Registration *</label> <label for="ac_reg">Aircraft Registration *</label>
<input type="text" id="ac_reg" name="ac_reg" required> <input type="text" id="ac_reg" name="ac_reg" required oninput="handleAircraftLookup(this.value)">
<div id="aircraft-lookup-results"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="ac_type">Aircraft Type *</label> <label for="ac_type">Aircraft Type *</label>
@@ -715,7 +751,6 @@
currentUser = cachedUser; currentUser = cachedUser;
document.getElementById('current-user').textContent = cachedUser; document.getElementById('current-user').textContent = cachedUser;
loadPPRs(); loadPPRs();
setDefaultDates();
return; return;
} }
} }
@@ -778,7 +813,6 @@
hideLogin(); hideLogin();
loadPPRs(); loadPPRs();
setDefaultDates();
} else { } else {
throw new Error(data.detail || 'Authentication failed'); throw new Error(data.detail || 'Authentication failed');
} }
@@ -814,11 +848,6 @@
showLogin(); showLogin();
} }
function setDefaultDates() {
const today = new Date();
document.getElementById('viewDate').value = today.toISOString().split('T')[0];
}
// Enhanced fetch wrapper with token expiry handling // Enhanced fetch wrapper with token expiry handling
async function authenticatedFetch(url, options = {}) { async function authenticatedFetch(url, options = {}) {
if (!accessToken) { if (!accessToken) {
@@ -861,13 +890,9 @@
document.getElementById('arrivals-no-data').style.display = 'none'; document.getElementById('arrivals-no-data').style.display = 'none';
try { try {
const viewDate = document.getElementById('viewDate').value; // Always load today's date
const showAll = document.getElementById('showAllStatuses').checked; const today = new Date().toISOString().split('T')[0];
let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`;
let url = '/api/v1/pprs/?limit=1000';
if (viewDate) {
url += `&date_from=${viewDate}&date_to=${viewDate}`;
}
const response = await authenticatedFetch(url); const response = await authenticatedFetch(url);
@@ -877,17 +902,10 @@
const allPPRs = await response.json(); const allPPRs = await response.json();
// Filter for arrivals (NEW, CONFIRMED, or all if showAll is checked) // Filter for arrivals (NEW and CONFIRMED with ETA only)
let arrivals; const arrivals = allPPRs.filter(ppr =>
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 (ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && ppr.eta
); );
}
displayArrivals(arrivals); displayArrivals(arrivals);
} catch (error) { } catch (error) {
@@ -907,13 +925,9 @@
document.getElementById('departures-no-data').style.display = 'none'; document.getElementById('departures-no-data').style.display = 'none';
try { try {
const viewDate = document.getElementById('viewDate').value; // Always load today's date
const showAll = document.getElementById('showAllStatuses').checked; const today = new Date().toISOString().split('T')[0];
let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`;
let url = '/api/v1/pprs/?limit=1000';
if (viewDate) {
url += `&date_from=${viewDate}&date_to=${viewDate}`;
}
const response = await authenticatedFetch(url); const response = await authenticatedFetch(url);
@@ -923,15 +937,8 @@
const allPPRs = await response.json(); const allPPRs = await response.json();
// Filter for departures (LANDED, or include DEPARTED if showAll is checked) // Filter for departures (LANDED status only)
let departures; const departures = allPPRs.filter(ppr => ppr.status === 'LANDED');
if (showAll) {
departures = allPPRs.filter(ppr =>
ppr.status === 'LANDED' || ppr.status === 'DEPARTED'
);
} else {
departures = allPPRs.filter(ppr => ppr.status === 'LANDED');
}
displayDepartures(departures); displayDepartures(departures);
} catch (error) { } catch (error) {
@@ -1063,7 +1070,15 @@
document.getElementById('ppr-form').reset(); document.getElementById('ppr-form').reset();
document.getElementById('ppr-id').value = ''; document.getElementById('ppr-id').value = '';
// Clear aircraft lookup results
clearAircraftLookup();
document.getElementById('pprModal').style.display = 'block'; document.getElementById('pprModal').style.display = 'block';
// Auto-focus on aircraft registration field
setTimeout(() => {
document.getElementById('ac_reg').focus();
}, 100);
} }
async function openPPRModal(pprId) { async function openPPRModal(pprId) {
@@ -1270,6 +1285,90 @@
closePPRModal(); 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 =
'<div class="aircraft-searching">Searching...</div>';
// 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 =
'<div class="aircraft-no-match">Lookup failed - please enter manually</div>';
}
}
function displayAircraftLookupResults(matches, searchTerm) {
const resultsDiv = document.getElementById('aircraft-lookup-results');
if (matches.length === 0) {
resultsDiv.innerHTML = '<div class="aircraft-no-match">No matches found</div>';
} else if (matches.length === 1) {
// Unique match found - auto-populate
const aircraft = matches[0];
resultsDiv.innerHTML = `
<div class="aircraft-match">
${aircraft.manufacturer_name} ${aircraft.model} (${aircraft.type_code})
</div>
`;
// 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 = `
<div class="aircraft-no-match">
Multiple matches found (${matches.length}) - please be more specific
</div>
`;
}
}
function clearAircraftLookup() {
document.getElementById('aircraft-lookup-results').innerHTML = '';
}
</script> </script>
</body> </body>
</html> </html>