Live ac search
This commit is contained in:
@@ -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"])
|
||||||
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):
|
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
|
||||||
223
web/admin.html
223
web/admin.html
@@ -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>
|
||||||
Reference in New Issue
Block a user