Unknown type supprt

This commit is contained in:
2026-03-23 12:47:08 -04:00
parent bddbe1451f
commit d2c9bc0370
7 changed files with 345 additions and 13 deletions
@@ -0,0 +1,50 @@
"""Add user_aircraft table for user-defined aircraft types
Revision ID: 004_user_aircraft
Revises: 003_public_booking
Create Date: 2026-03-23 12:00:00.000000
This migration adds a user_aircraft table to store aircraft types
that are manually entered by users when not found in the main aircraft database.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '004_user_aircraft'
down_revision = '003_public_booking'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""
Create user_aircraft table for storing user-defined aircraft types.
"""
op.create_table('user_aircraft',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('registration', sa.String(length=25), nullable=False),
sa.Column('type_code', sa.String(length=30), nullable=False),
sa.Column('clean_reg', sa.String(length=25), nullable=False),
sa.Column('created_by', sa.String(length=16), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('registration')
)
# Create indexes
op.create_index('idx_user_aircraft_registration', 'user_aircraft', ['registration'])
op.create_index('idx_user_aircraft_clean_reg', 'user_aircraft', ['clean_reg'])
op.create_index('idx_user_aircraft_created_by', 'user_aircraft', ['created_by'])
def downgrade() -> None:
"""
Drop user_aircraft table.
"""
op.drop_index('idx_user_aircraft_created_by', table_name='user_aircraft')
op.drop_index('idx_user_aircraft_clean_reg', table_name='user_aircraft')
op.drop_index('idx_user_aircraft_registration', table_name='user_aircraft')
op.drop_table('user_aircraft')
+84 -4
View File
@@ -2,8 +2,8 @@ from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_db, get_current_active_user from app.api.deps import get_db, get_current_active_user
from app.models.ppr import Aircraft from app.models.ppr import Aircraft, UserAircraft
from app.schemas.ppr import Aircraft as AircraftSchema from app.schemas.ppr import Aircraft as AircraftSchema, UserAircraftCreate
from app.models.ppr import User from app.models.ppr import User
router = APIRouter() router = APIRouter()
@@ -18,6 +18,7 @@ async def lookup_aircraft_by_registration(
""" """
Lookup aircraft by registration (clean match). Lookup aircraft by registration (clean match).
Removes non-alphanumeric characters from input for matching. Removes non-alphanumeric characters from input for matching.
Checks user_aircraft table first, then aircraft table.
""" """
# Clean the input registration (remove non-alphanumeric characters) # Clean the input registration (remove non-alphanumeric characters)
clean_input = ''.join(c for c in registration if c.isalnum()).upper() clean_input = ''.join(c for c in registration if c.isalnum()).upper()
@@ -25,7 +26,29 @@ async def lookup_aircraft_by_registration(
if len(clean_input) < 4: if len(clean_input) < 4:
return [] return []
# Query aircraft table using clean_reg column # First check user_aircraft table
user_aircraft = db.query(UserAircraft).filter(
UserAircraft.clean_reg.like(f"{clean_input}%")
).limit(10).all()
if user_aircraft:
# Convert UserAircraft to Aircraft-like objects
result = []
for ua in user_aircraft:
# Create a mock Aircraft object with the user data
result.append({
'id': ua.id,
'registration': ua.registration,
'type_code': ua.type_code,
'clean_reg': ua.clean_reg,
'icao24': None,
'manufacturer_icao': None,
'manufacturer_name': None,
'model': None
})
return result
# If no user aircraft found, check main aircraft table
aircraft_list = db.query(Aircraft).filter( aircraft_list = db.query(Aircraft).filter(
Aircraft.clean_reg.like(f"{clean_input}%") Aircraft.clean_reg.like(f"{clean_input}%")
).limit(10).all() ).limit(10).all()
@@ -42,6 +65,7 @@ async def public_lookup_aircraft_by_registration(
Public lookup aircraft by registration (clean match). Public lookup aircraft by registration (clean match).
Removes non-alphanumeric characters from input for matching. Removes non-alphanumeric characters from input for matching.
No authentication required. No authentication required.
Checks user_aircraft table first, then aircraft table.
""" """
# Clean the input registration (remove non-alphanumeric characters) # Clean the input registration (remove non-alphanumeric characters)
clean_input = ''.join(c for c in registration if c.isalnum()).upper() clean_input = ''.join(c for c in registration if c.isalnum()).upper()
@@ -49,7 +73,28 @@ async def public_lookup_aircraft_by_registration(
if len(clean_input) < 4: if len(clean_input) < 4:
return [] return []
# Query aircraft table using clean_reg column # First check user_aircraft table
user_aircraft = db.query(UserAircraft).filter(
UserAircraft.clean_reg.like(f"{clean_input}%")
).limit(10).all()
if user_aircraft:
# Convert UserAircraft to Aircraft-like objects
result = []
for ua in user_aircraft:
result.append({
'id': ua.id,
'registration': ua.registration,
'type_code': ua.type_code,
'clean_reg': ua.clean_reg,
'icao24': None,
'manufacturer_icao': None,
'manufacturer_name': None,
'model': None
})
return result
# If no user aircraft found, check main aircraft table
aircraft_list = db.query(Aircraft).filter( aircraft_list = db.query(Aircraft).filter(
Aircraft.clean_reg.like(f"{clean_input}%") Aircraft.clean_reg.like(f"{clean_input}%")
).limit(10).all() ).limit(10).all()
@@ -82,3 +127,38 @@ async def search_aircraft(
).limit(limit).all() ).limit(limit).all()
return aircraft_list return aircraft_list
@router.post("/user-aircraft", response_model=dict)
async def save_user_aircraft(
aircraft: UserAircraftCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Save a user-defined aircraft type for a registration.
"""
# Clean the registration
clean_reg = ''.join(c for c in aircraft.registration if c.isalnum()).upper()
# Check if already exists
existing = db.query(UserAircraft).filter(
UserAircraft.registration == aircraft.registration.upper()
).first()
if existing:
raise HTTPException(status_code=400, detail="Aircraft registration already exists in user database")
# Create new user aircraft
user_aircraft = UserAircraft(
registration=aircraft.registration.upper(),
type_code=aircraft.type_code.upper(),
clean_reg=clean_reg,
created_by=current_user.username
)
db.add(user_aircraft)
db.commit()
db.refresh(user_aircraft)
return {"message": "Aircraft saved successfully", "id": user_aircraft.id}
+12
View File
@@ -89,3 +89,15 @@ class Aircraft(Base):
clean_reg = Column(String(25), nullable=True, index=True) clean_reg = Column(String(25), nullable=True, index=True)
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp()) created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()) updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
class UserAircraft(Base):
__tablename__ = "user_aircraft"
id = Column(Integer, primary_key=True, autoincrement=True)
registration = Column(String(25), nullable=False, unique=True, index=True)
type_code = Column(String(30), nullable=False)
clean_reg = Column(String(25), nullable=False, index=True)
created_by = Column(String(16), nullable=False, index=True)
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
+20
View File
@@ -215,3 +215,23 @@ class Aircraft(AircraftBase):
class Config: class Config:
from_attributes = True from_attributes = True
# User Aircraft schemas
class UserAircraftBase(BaseModel):
registration: str
type_code: str
clean_reg: str
created_by: str
class UserAircraft(UserAircraftBase):
id: int
class Config:
from_attributes = True
class UserAircraftCreate(BaseModel):
registration: str
type_code: str
+37 -3
View File
@@ -1494,6 +1494,12 @@
openNewPPRModal(); openNewPPRModal();
} }
// Press 'g' to book out local flight starting with G
if (e.key === 'g' || e.key === 'G') {
e.preventDefault();
openLocalFlightModal('LOCAL', 'G');
}
// Press 'l' to book out local flight (LOCAL type) // Press 'l' to book out local flight (LOCAL type)
if (e.key === 'l' || e.key === 'L') { if (e.key === 'l' || e.key === 'L') {
e.preventDefault(); e.preventDefault();
@@ -2624,6 +2630,9 @@
document.getElementById('ppr-form').reset(); document.getElementById('ppr-form').reset();
document.getElementById('ppr-id').value = ''; document.getElementById('ppr-id').value = '';
// Clear the unsaved aircraft flag
document.getElementById('ppr-form').removeAttribute('data-unsaved-aircraft');
// Set default ETA and ETD // Set default ETA and ETD
const now = new Date(); const now = new Date();
const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
@@ -3040,6 +3049,9 @@
if (!accessToken) return; if (!accessToken) return;
// Auto-save any unsaved aircraft types
await autoSaveUnsavedAircraft(this);
const formData = new FormData(this); const formData = new FormData(this);
const pprData = {}; const pprData = {};
@@ -3531,22 +3543,29 @@
}); });
// Local Flight (Book Out) Modal Functions // Local Flight (Book Out) Modal Functions
function openLocalFlightModal(flightType = 'LOCAL') { function openLocalFlightModal(flightType = 'LOCAL', prefillReg = '') {
document.getElementById('local-flight-form').reset(); document.getElementById('local-flight-form').reset();
document.getElementById('local-flight-id').value = ''; document.getElementById('local-flight-id').value = '';
document.getElementById('local-flight-modal-title').textContent = 'Book Out'; document.getElementById('local-flight-modal-title').textContent = 'Book Out';
document.getElementById('local_flight_type').value = flightType; document.getElementById('local_flight_type').value = flightType;
document.getElementById('localFlightModal').style.display = 'block'; document.getElementById('localFlightModal').style.display = 'block';
// Clear the unsaved aircraft flag
document.getElementById('local-flight-form').removeAttribute('data-unsaved-aircraft');
// Clear aircraft lookup results // Clear aircraft lookup results
clearLocalAircraftLookup(); clearLocalAircraftLookup();
// Update destination field visibility based on flight type // Update destination field visibility based on flight type
handleFlightTypeChange(flightType); handleFlightTypeChange(flightType);
// Auto-focus on registration field // Auto-focus on registration field and prefill if provided
setTimeout(() => { setTimeout(() => {
document.getElementById('local_registration').focus(); const regField = document.getElementById('local_registration');
regField.focus();
if (prefillReg) {
regField.value = prefillReg;
}
}, 100); }, 100);
} }
@@ -3555,6 +3574,9 @@
document.getElementById('book-in-id').value = ''; document.getElementById('book-in-id').value = '';
document.getElementById('bookInModal').style.display = 'block'; document.getElementById('bookInModal').style.display = 'block';
// Clear the unsaved aircraft flag
document.getElementById('book-in-form').removeAttribute('data-unsaved-aircraft');
// Clear aircraft lookup results // Clear aircraft lookup results
clearBookInAircraftLookup(); clearBookInAircraftLookup();
clearBookInArrivalAirportLookup(); clearBookInArrivalAirportLookup();
@@ -3573,6 +3595,9 @@
document.getElementById('overflight-id').value = ''; document.getElementById('overflight-id').value = '';
document.getElementById('overflightModal').style.display = 'block'; document.getElementById('overflightModal').style.display = 'block';
// Clear the unsaved aircraft flag
document.getElementById('overflight-form').removeAttribute('data-unsaved-aircraft');
// Clear aircraft lookup results // Clear aircraft lookup results
clearOverflightAircraftLookup(); clearOverflightAircraftLookup();
clearOverflightDepartureAirportLookup(); clearOverflightDepartureAirportLookup();
@@ -4502,6 +4527,9 @@
if (!accessToken) return; if (!accessToken) return;
// Auto-save any unsaved aircraft types
await autoSaveUnsavedAircraft(this);
const formData = new FormData(this); const formData = new FormData(this);
const flightType = formData.get('flight_type'); const flightType = formData.get('flight_type');
const flightData = {}; const flightData = {};
@@ -4585,6 +4613,9 @@
if (!accessToken) return; if (!accessToken) return;
// Auto-save any unsaved aircraft types
await autoSaveUnsavedAircraft(this);
const formData = new FormData(this); const formData = new FormData(this);
const arrivalData = {}; const arrivalData = {};
@@ -4664,6 +4695,9 @@
if (!accessToken) return; if (!accessToken) return;
// Auto-save any unsaved aircraft types
await autoSaveUnsavedAircraft(this);
const formData = new FormData(this); const formData = new FormData(this);
const overflightData = {}; const overflightData = {};
+3
View File
@@ -692,6 +692,9 @@
document.getElementById('ppr-form').addEventListener('submit', async function(e) { document.getElementById('ppr-form').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
// Auto-save any unsaved aircraft types
await autoSaveUnsavedAircraft(this);
const formData = new FormData(this); const formData = new FormData(this);
const pprData = {}; const pprData = {};
+137 -4
View File
@@ -163,15 +163,26 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
const resultsDiv = document.getElementById(resultsId); const resultsDiv = document.getElementById(resultsId);
if (config.isAircraft) { if (config.isAircraft) {
// Aircraft lookup: auto-populate on single match, format input on no match // Aircraft lookup: auto-populate on single match, mark form for auto-save on no match
if (!results || results.length === 0) { if (!results || results.length === 0) {
// Format the aircraft registration and auto-populate // Format the aircraft registration
const formatted = formatAircraftRegistration(searchTerm); const formatted = formatAircraftRegistration(searchTerm);
const field = document.getElementById(fieldId); const field = document.getElementById(fieldId);
if (field) { if (field) {
field.value = formatted; field.value = formatted;
// Mark the form for auto-saving this aircraft
const form = field.closest('form');
if (form) {
form.setAttribute('data-unsaved-aircraft', fieldId);
}
} }
resultsDiv.innerHTML = ''; // Clear results, field is auto-populated
// Show message that type will be saved
resultsDiv.innerHTML = `
<div class="aircraft-no-match">
No match found - aircraft type will be saved automatically when you submit
</div>
`;
} else if (results.length === 1) { } else if (results.length === 1) {
// Single match - auto-populate // Single match - auto-populate
const aircraft = results[0]; const aircraft = results[0];
@@ -183,7 +194,14 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
// Auto-populate the form fields // Auto-populate the form fields
const field = document.getElementById(fieldId); const field = document.getElementById(fieldId);
if (field) field.value = aircraft.registration; if (field) {
field.value = aircraft.registration;
// Clear the unsaved aircraft flag since we found a match
const form = field.closest('form');
if (form) {
form.removeAttribute('data-unsaved-aircraft');
}
}
// Also populate type field // Also populate type field
let typeFieldId; let typeFieldId;
@@ -208,6 +226,14 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
Multiple matches found (${results.length}) - please be more specific Multiple matches found (${results.length}) - please be more specific
</div> </div>
`; `;
// Clear the unsaved aircraft flag since multiple matches found
const field = document.getElementById(fieldId);
if (field) {
const form = field.closest('form');
if (form) {
form.removeAttribute('data-unsaved-aircraft');
}
}
} }
} else { } else {
// Airport lookup: show list of options with keyboard navigation // Airport lookup: show list of options with keyboard navigation
@@ -501,3 +527,110 @@ function selectBookInAircraft(registration) {
function selectBookInArrivalAirport(icaoCode) { function selectBookInArrivalAirport(icaoCode) {
lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode); lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode);
} }
// Save user aircraft type for future lookups
async function saveUserAircraft(registrationFieldId, resultsDivId) {
const regField = document.getElementById(registrationFieldId);
if (!regField || !regField.value.trim()) {
showNotification('Please enter a registration first', true);
return;
}
// Determine the type field ID based on registration field
let typeFieldId;
if (registrationFieldId === 'ac_reg') {
typeFieldId = 'ac_type';
} else if (registrationFieldId === 'local_registration') {
typeFieldId = 'local_type';
} else if (registrationFieldId === 'book_in_registration') {
typeFieldId = 'book_in_type';
} else if (registrationFieldId === 'overflight_registration') {
typeFieldId = 'overflight_type';
}
const typeField = document.getElementById(typeFieldId);
if (!typeField || !typeField.value.trim()) {
showNotification('Please enter an aircraft type first', true);
return;
}
try {
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
registration: regField.value.trim(),
type_code: typeField.value.trim()
})
});
if (response.ok) {
const data = await response.json();
showNotification('Aircraft type saved for future use');
// Clear the results div
const resultsDiv = document.getElementById(resultsDivId);
if (resultsDiv) {
resultsDiv.innerHTML = '';
}
} else {
const error = await response.json();
showNotification(error.detail || 'Failed to save aircraft', true);
}
} catch (error) {
console.error('Error saving aircraft:', error);
showNotification('Error saving aircraft', true);
}
}
// Auto-save unsaved aircraft before form submission
async function autoSaveUnsavedAircraft(form) {
const unsavedFieldId = form.getAttribute('data-unsaved-aircraft');
if (!unsavedFieldId) return; // No unsaved aircraft to save
const regField = document.getElementById(unsavedFieldId);
if (!regField || !regField.value.trim()) return;
// Determine the type field ID
let typeFieldId;
if (unsavedFieldId === 'ac_reg') {
typeFieldId = 'ac_type';
} else if (unsavedFieldId === 'local_registration') {
typeFieldId = 'local_type';
} else if (unsavedFieldId === 'book_in_registration') {
typeFieldId = 'book_in_type';
} else if (unsavedFieldId === 'overflight_registration') {
typeFieldId = 'overflight_type';
}
const typeField = document.getElementById(typeFieldId);
if (!typeField || !typeField.value.trim()) return;
try {
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
registration: regField.value.trim(),
type_code: typeField.value.trim()
})
});
if (response.ok) {
// Successfully saved, remove the flag
form.removeAttribute('data-unsaved-aircraft');
console.log('Auto-saved aircraft type for', regField.value.trim());
} else if (response.status === 400) {
// Already exists, just remove the flag
form.removeAttribute('data-unsaved-aircraft');
} else {
console.error('Failed to auto-save aircraft');
}
} catch (error) {
console.error('Error auto-saving aircraft:', error);
}
}