Live ac search
This commit is contained in:
@@ -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"])
|
||||
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
|
||||
60
backend/app/api/endpoints/aircraft.py
Normal file
60
backend/app/api/endpoints/aircraft.py
Normal 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
|
||||
@@ -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
|
||||
223
web/admin.html
223
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 @@
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="controls">
|
||||
<div class="top-menu">
|
||||
<div class="menu-left">
|
||||
<button class="btn btn-success" onclick="openNewPPRModal()">
|
||||
➕ New PPR Entry
|
||||
</button>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="viewDate">Date:</label>
|
||||
<input type="date" id="viewDate" onchange="loadPPRs()">
|
||||
</div>
|
||||
|
||||
<div class="menu-right">
|
||||
<button class="btn btn-primary" onclick="loadPPRs()">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group" style="margin-left: auto;">
|
||||
<label>
|
||||
<input type="checkbox" id="showAllStatuses" onchange="loadPPRs()">
|
||||
Show all statuses (including departed/canceled)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
|
||||
<!-- Arrivals Table -->
|
||||
<div class="ppr-table">
|
||||
@@ -604,7 +639,8 @@
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label for="ac_type">Aircraft Type *</label>
|
||||
@@ -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 =>
|
||||
// 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 =
|
||||
'<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>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user