Compare commits
2 Commits
98d0e3cfd7
...
6209c7acce
| Author | SHA1 | Date | |
|---|---|---|---|
| 6209c7acce | |||
| d7eefdb652 |
@@ -91,7 +91,8 @@ def upgrade() -> None:
|
|||||||
sa.Column('in_from', sa.String(length=64), nullable=False),
|
sa.Column('in_from', sa.String(length=64), nullable=False),
|
||||||
sa.Column('status', sa.Enum('BOOKED_IN', 'LANDED', 'CANCELLED', name='arrivalsstatus'), nullable=False, server_default='BOOKED_IN'),
|
sa.Column('status', sa.Enum('BOOKED_IN', 'LANDED', 'CANCELLED', name='arrivalsstatus'), nullable=False, server_default='BOOKED_IN'),
|
||||||
sa.Column('notes', sa.Text(), nullable=True),
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
sa.Column('booked_in_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
sa.Column('created_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||||
|
sa.Column('eta', sa.DateTime(), nullable=True),
|
||||||
sa.Column('landed_dt', sa.DateTime(), nullable=True),
|
sa.Column('landed_dt', sa.DateTime(), nullable=True),
|
||||||
sa.Column('created_by', sa.String(length=16), nullable=True),
|
sa.Column('created_by', sa.String(length=16), nullable=True),
|
||||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
|
||||||
@@ -104,7 +105,8 @@ def upgrade() -> None:
|
|||||||
op.create_index('idx_arr_registration', 'arrivals', ['registration'])
|
op.create_index('idx_arr_registration', 'arrivals', ['registration'])
|
||||||
op.create_index('idx_arr_in_from', 'arrivals', ['in_from'])
|
op.create_index('idx_arr_in_from', 'arrivals', ['in_from'])
|
||||||
op.create_index('idx_arr_status', 'arrivals', ['status'])
|
op.create_index('idx_arr_status', 'arrivals', ['status'])
|
||||||
op.create_index('idx_arr_booked_in_dt', 'arrivals', ['booked_in_dt'])
|
op.create_index('idx_arr_created_dt', 'arrivals', ['created_dt'])
|
||||||
|
op.create_index('idx_arr_eta', 'arrivals', ['eta'])
|
||||||
op.create_index('idx_arr_created_by', 'arrivals', ['created_by'])
|
op.create_index('idx_arr_created_by', 'arrivals', ['created_by'])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,28 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
|
|||||||
'flight_type': flight.flight_type.value
|
'flight_type': flight.flight_type.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Add booked-in arrivals
|
||||||
|
booked_in_arrivals = crud_arrival.get_multi(db, limit=1000)
|
||||||
|
for arrival in booked_in_arrivals:
|
||||||
|
# Only include BOOKED_IN and LANDED arrivals
|
||||||
|
if arrival.status not in (ArrivalStatus.BOOKED_IN, ArrivalStatus.LANDED):
|
||||||
|
continue
|
||||||
|
# For BOOKED_IN, only include those from today; for LANDED, include all
|
||||||
|
if arrival.status == ArrivalStatus.BOOKED_IN:
|
||||||
|
if not (today_start <= arrival.created_dt < today_end):
|
||||||
|
continue
|
||||||
|
|
||||||
|
arrivals_list.append({
|
||||||
|
'registration': arrival.registration,
|
||||||
|
'callsign': arrival.callsign,
|
||||||
|
'type': arrival.type,
|
||||||
|
'in_from': arrival.in_from,
|
||||||
|
'eta': arrival.eta,
|
||||||
|
'landed_dt': arrival.landed_dt,
|
||||||
|
'status': arrival.status.value,
|
||||||
|
'isBookedIn': True
|
||||||
|
})
|
||||||
|
|
||||||
return arrivals_list
|
return arrivals_list
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,25 +25,25 @@ class CRUDArrival:
|
|||||||
query = query.filter(Arrival.status == status)
|
query = query.filter(Arrival.status == status)
|
||||||
|
|
||||||
if date_from:
|
if date_from:
|
||||||
query = query.filter(func.date(Arrival.booked_in_dt) >= date_from)
|
query = query.filter(func.date(Arrival.created_dt) >= date_from)
|
||||||
|
|
||||||
if date_to:
|
if date_to:
|
||||||
query = query.filter(func.date(Arrival.booked_in_dt) <= date_to)
|
query = query.filter(func.date(Arrival.created_dt) <= date_to)
|
||||||
|
|
||||||
return query.order_by(desc(Arrival.booked_in_dt)).offset(skip).limit(limit).all()
|
return query.order_by(desc(Arrival.created_dt)).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
def get_arrivals_today(self, db: Session) -> List[Arrival]:
|
def get_arrivals_today(self, db: Session) -> List[Arrival]:
|
||||||
"""Get today's arrivals (booked in or landed)"""
|
"""Get today's arrivals (booked in or landed)"""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return db.query(Arrival).filter(
|
return db.query(Arrival).filter(
|
||||||
and_(
|
and_(
|
||||||
func.date(Arrival.booked_in_dt) == today,
|
func.date(Arrival.created_dt) == today,
|
||||||
or_(
|
or_(
|
||||||
Arrival.status == ArrivalStatus.BOOKED_IN,
|
Arrival.status == ArrivalStatus.BOOKED_IN,
|
||||||
Arrival.status == ArrivalStatus.LANDED
|
Arrival.status == ArrivalStatus.LANDED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).order_by(Arrival.booked_in_dt).all()
|
).order_by(Arrival.created_dt).all()
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival:
|
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival:
|
||||||
db_obj = Arrival(
|
db_obj = Arrival(
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class Arrival(Base):
|
|||||||
in_from = Column(String(4), nullable=False, index=True)
|
in_from = Column(String(4), nullable=False, index=True)
|
||||||
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True)
|
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
booked_in_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
created_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
|
||||||
|
eta = Column(DateTime, nullable=True, index=True)
|
||||||
landed_dt = Column(DateTime, nullable=True)
|
landed_dt = Column(DateTime, nullable=True)
|
||||||
created_by = Column(String(16), nullable=True, index=True)
|
created_by = Column(String(16), nullable=True, index=True)
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ArrivalBase(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ArrivalCreate(ArrivalBase):
|
class ArrivalCreate(ArrivalBase):
|
||||||
pass
|
eta: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class ArrivalUpdate(BaseModel):
|
class ArrivalUpdate(BaseModel):
|
||||||
@@ -57,7 +57,8 @@ class ArrivalStatusUpdate(BaseModel):
|
|||||||
class Arrival(ArrivalBase):
|
class Arrival(ArrivalBase):
|
||||||
id: int
|
id: int
|
||||||
status: ArrivalStatus
|
status: ArrivalStatus
|
||||||
booked_in_dt: datetime
|
created_dt: datetime
|
||||||
|
eta: Optional[datetime] = None
|
||||||
landed_dt: Optional[datetime] = None
|
landed_dt: Optional[datetime] = None
|
||||||
created_by: Optional[str] = None
|
created_by: Optional[str] = None
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|||||||
@@ -648,6 +648,16 @@ tbody tr:hover {
|
|||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lookup-option-selected {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-left: 3px solid #2196f3;
|
||||||
|
padding-left: calc(0.5rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lookup-option-selected:hover {
|
||||||
|
background-color: #bbdefb;
|
||||||
|
}
|
||||||
|
|
||||||
.lookup-option:last-child {
|
.lookup-option:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|||||||
355
web/admin.html
355
web/admin.html
@@ -19,6 +19,9 @@
|
|||||||
<button class="btn btn-info" onclick="openLocalFlightModal()">
|
<button class="btn btn-info" onclick="openLocalFlightModal()">
|
||||||
🛫 Book Out
|
🛫 Book Out
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-info" onclick="openBookInModal()">
|
||||||
|
🛬 Book In
|
||||||
|
</button>
|
||||||
<button class="btn btn-primary" onclick="window.open('reports.html', '_blank')">
|
<button class="btn btn-primary" onclick="window.open('reports.html', '_blank')">
|
||||||
📊 Reports
|
📊 Reports
|
||||||
</button>
|
</button>
|
||||||
@@ -505,6 +508,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Book In Modal -->
|
||||||
|
<div id="bookInModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Book In</h2>
|
||||||
|
<button class="close" onclick="closeBookInModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="book-in-form">
|
||||||
|
<input type="hidden" id="book-in-id" name="id">
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="book_in_registration">Aircraft Registration *</label>
|
||||||
|
<input type="text" id="book_in_registration" name="registration" required oninput="handleBookInAircraftLookup(this.value)" tabindex="1">
|
||||||
|
<div id="book-in-aircraft-lookup-results"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="book_in_type">Aircraft Type</label>
|
||||||
|
<input type="text" id="book_in_type" name="type" tabindex="4" placeholder="e.g., C172, PA34, AA5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="book_in_callsign">Callsign (optional)</label>
|
||||||
|
<input type="text" id="book_in_callsign" name="callsign" placeholder="If different from registration" tabindex="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="book_in_pob">Persons on Board *</label>
|
||||||
|
<input type="number" id="book_in_pob" name="pob" required min="1" tabindex="2">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="book_in_from">Coming From (Airport) *</label>
|
||||||
|
<input type="text" id="book_in_from" name="in_from" placeholder="ICAO Code or Airport Name" required oninput="handleBookInArrivalAirportLookup(this.value)" tabindex="3">
|
||||||
|
<div id="book-in-arrival-airport-lookup-results"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="book_in_eta_time">ETA (Estimated Time of Arrival) *</label>
|
||||||
|
<select id="book_in_eta_time" name="eta_time" required>
|
||||||
|
<option value="">Select Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="book_in_notes">Notes</label>
|
||||||
|
<textarea id="book_in_notes" name="notes" rows="3" placeholder="e.g., any special requirements"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="closeBookInModal()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
🛬 Book In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Departure Edit Modal -->
|
<!-- Departure Edit Modal -->
|
||||||
<div id="departureEditModal" class="modal">
|
<div id="departureEditModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -1014,6 +1076,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Press 'Escape' to close Book In modal if it's open (allow even when typing in inputs)
|
||||||
|
if (e.key === 'Escape' && document.getElementById('bookInModal').style.display === 'block') {
|
||||||
|
e.preventDefault();
|
||||||
|
closeBookInModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only trigger other shortcuts when not typing in input fields
|
// Only trigger other shortcuts when not typing in input fields
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
|
||||||
return;
|
return;
|
||||||
@@ -1042,6 +1111,12 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openLocalFlightModal('DEPARTURE');
|
openLocalFlightModal('DEPARTURE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Press 'i' to book in arrival
|
||||||
|
if (e.key === 'i' || e.key === 'I') {
|
||||||
|
e.preventDefault();
|
||||||
|
openBookInModal();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1177,10 +1252,11 @@
|
|||||||
document.getElementById('arrivals-no-data').style.display = 'none';
|
document.getElementById('arrivals-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load PPRs and local flights that are in the air
|
// Load PPRs, local flights, and booked-in arrivals
|
||||||
const [pprResponse, localResponse] = await Promise.all([
|
const [pprResponse, localResponse, bookInResponse] = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000')
|
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/arrivals/?limit=1000')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!pprResponse.ok) {
|
if (!pprResponse.ok) {
|
||||||
@@ -1218,6 +1294,24 @@
|
|||||||
arrivals.push(...localInAir);
|
arrivals.push(...localInAir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add booked-in arrivals from the arrivals table
|
||||||
|
if (bookInResponse.ok) {
|
||||||
|
const bookedInArrivals = await bookInResponse.json();
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const bookedInToday = bookedInArrivals
|
||||||
|
.filter(arrival => {
|
||||||
|
// Only include arrivals booked in today (created_dt) with BOOKED_IN status
|
||||||
|
if (!arrival.created_dt || arrival.status !== 'BOOKED_IN') return false;
|
||||||
|
const bookedDate = arrival.created_dt.split('T')[0];
|
||||||
|
return bookedDate === today;
|
||||||
|
})
|
||||||
|
.map(arrival => ({
|
||||||
|
...arrival,
|
||||||
|
isBookedIn: true // Flag to distinguish from PPR and local
|
||||||
|
}));
|
||||||
|
arrivals.push(...bookedInToday);
|
||||||
|
}
|
||||||
|
|
||||||
displayArrivals(arrivals);
|
displayArrivals(arrivals);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading arrivals:', error);
|
console.error('Error loading arrivals:', error);
|
||||||
@@ -1435,13 +1529,17 @@
|
|||||||
document.getElementById('parked-no-data').style.display = 'none';
|
document.getElementById('parked-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
// Load both PPRs and booked-in arrivals
|
||||||
|
const [pprResponse, bookedInResponse] = await Promise.all([
|
||||||
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/arrivals/?limit=1000')
|
||||||
|
]);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!pprResponse.ok) {
|
||||||
throw new Error('Failed to fetch parked visitors');
|
throw new Error('Failed to fetch parked visitors');
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPPRs = await response.json();
|
const allPPRs = await pprResponse.json();
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
// Filter for parked visitors: LANDED status and (no ETD or ETD not today)
|
// Filter for parked visitors: LANDED status and (no ETD or ETD not today)
|
||||||
@@ -1459,6 +1557,18 @@
|
|||||||
return etdDate !== today;
|
return etdDate !== today;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add booked-in arrivals with LANDED status
|
||||||
|
if (bookedInResponse.ok) {
|
||||||
|
const bookedInArrivals = await bookedInResponse.json();
|
||||||
|
const bookedInParked = bookedInArrivals
|
||||||
|
.filter(arrival => arrival.status === 'LANDED')
|
||||||
|
.map(arrival => ({
|
||||||
|
...arrival,
|
||||||
|
isBookedIn: true // Flag to distinguish from PPR
|
||||||
|
}));
|
||||||
|
parked.push(...bookedInParked);
|
||||||
|
}
|
||||||
|
|
||||||
displayParked(parked);
|
displayParked(parked);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading parked visitors:', error);
|
console.error('Error loading parked visitors:', error);
|
||||||
@@ -1491,9 +1601,25 @@
|
|||||||
|
|
||||||
for (const ppr of parked) {
|
for (const ppr of parked) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
const isBookedIn = ppr.isBookedIn;
|
||||||
|
|
||||||
|
// Click handler that routes to correct modal/display
|
||||||
|
if (isBookedIn) {
|
||||||
|
row.style.cursor = 'default'; // Booked-in arrivals don't have a modal yet
|
||||||
|
} else {
|
||||||
row.onclick = () => openPPRModal(ppr.id);
|
row.onclick = () => openPPRModal(ppr.id);
|
||||||
|
}
|
||||||
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
|
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
|
||||||
|
|
||||||
|
// Get registration based on type (PPR vs booked-in)
|
||||||
|
const registration = ppr.ac_reg || ppr.registration || '-';
|
||||||
|
|
||||||
|
// Get aircraft type based on type (PPR vs booked-in)
|
||||||
|
const acType = ppr.ac_type || ppr.type || '-';
|
||||||
|
|
||||||
|
// Get from airport
|
||||||
|
const fromAirport = ppr.in_from || '-';
|
||||||
|
|
||||||
// Format arrival: time if today, date if not
|
// Format arrival: time if today, date if not
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
let arrivedDisplay = '-';
|
let arrivedDisplay = '-';
|
||||||
@@ -1517,9 +1643,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_reg || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${registration}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_type || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${acType}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.in_from || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${fromAirport}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${arrivedDisplay}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${arrivedDisplay}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${etdDisplay}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${etdDisplay}</td>
|
||||||
`;
|
`;
|
||||||
@@ -1674,11 +1800,16 @@
|
|||||||
for (const flight of arrivals) {
|
for (const flight of arrivals) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
const isLocal = flight.isLocalFlight;
|
const isLocal = flight.isLocalFlight;
|
||||||
|
const isBookedIn = flight.isBookedIn;
|
||||||
|
|
||||||
// Click handler that routes to correct modal
|
// Click handler that routes to correct modal
|
||||||
row.onclick = () => {
|
row.onclick = () => {
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
openLocalFlightEditModal(flight.id);
|
openLocalFlightEditModal(flight.id);
|
||||||
|
} else if (isBookedIn) {
|
||||||
|
// For booked-in flights, we might add a view modal later
|
||||||
|
// For now, just show a message
|
||||||
|
showNotification(`Booked-in flight: ${flight.registration}`, false);
|
||||||
} else {
|
} else {
|
||||||
openPPRModal(flight.id);
|
openPPRModal(flight.id);
|
||||||
}
|
}
|
||||||
@@ -1713,6 +1844,34 @@
|
|||||||
CANCEL
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
} else if (isBookedIn) {
|
||||||
|
// Booked-in arrival display
|
||||||
|
if (flight.callsign && flight.callsign.trim()) {
|
||||||
|
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||||||
|
} else {
|
||||||
|
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||||||
|
}
|
||||||
|
acType = flight.type;
|
||||||
|
|
||||||
|
// Lookup airport name for in_from
|
||||||
|
let fromDisplay_temp = flight.in_from;
|
||||||
|
if (flight.in_from && flight.in_from.length === 4 && /^[A-Z]{4}$/.test(flight.in_from)) {
|
||||||
|
fromDisplay_temp = await getAirportDisplay(flight.in_from);
|
||||||
|
}
|
||||||
|
fromDisplay = fromDisplay_temp;
|
||||||
|
|
||||||
|
// Show ETA if available, otherwise show landed_dt
|
||||||
|
eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
|
||||||
|
pob = flight.pob || '-';
|
||||||
|
fuel = '-';
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||||||
|
LAND
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
// PPR display
|
// PPR display
|
||||||
if (flight.ac_call && flight.ac_call.trim()) {
|
if (flight.ac_call && flight.ac_call.trim()) {
|
||||||
@@ -2165,11 +2324,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp modal functions
|
// Timestamp modal functions
|
||||||
function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false) {
|
function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false, isBookedIn = false) {
|
||||||
const targetId = pprId || (isLocalFlight ? currentLocalFlightId : currentPPRId);
|
const targetId = pprId || (isLocalFlight ? currentLocalFlightId : (isBookedIn ? currentBookedInArrivalId : currentPPRId));
|
||||||
if (!targetId) return;
|
if (!targetId) return;
|
||||||
|
|
||||||
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture };
|
pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture, isBookedIn: isBookedIn };
|
||||||
|
|
||||||
const modalTitle = document.getElementById('timestamp-modal-title');
|
const modalTitle = document.getElementById('timestamp-modal-title');
|
||||||
const submitBtn = document.getElementById('timestamp-submit-btn');
|
const submitBtn = document.getElementById('timestamp-submit-btn');
|
||||||
@@ -2218,12 +2377,15 @@
|
|||||||
// Determine the correct API endpoint based on flight type
|
// Determine the correct API endpoint based on flight type
|
||||||
const isLocal = pendingStatusUpdate.isLocalFlight;
|
const isLocal = pendingStatusUpdate.isLocalFlight;
|
||||||
const isDeparture = pendingStatusUpdate.isDeparture;
|
const isDeparture = pendingStatusUpdate.isDeparture;
|
||||||
|
const isBookedIn = pendingStatusUpdate.isBookedIn;
|
||||||
let endpoint;
|
let endpoint;
|
||||||
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`;
|
endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`;
|
||||||
} else if (isDeparture) {
|
} else if (isDeparture) {
|
||||||
endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`;
|
endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`;
|
||||||
|
} else if (isBookedIn) {
|
||||||
|
endpoint = `/api/v1/arrivals/${pendingStatusUpdate.pprId}/status`;
|
||||||
} else {
|
} else {
|
||||||
endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
|
endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`;
|
||||||
}
|
}
|
||||||
@@ -2247,9 +2409,14 @@
|
|||||||
|
|
||||||
const updatedStatus = pendingStatusUpdate.status;
|
const updatedStatus = pendingStatusUpdate.status;
|
||||||
closeTimestampModal();
|
closeTimestampModal();
|
||||||
loadPPRs(); // Refresh all tables
|
// Refresh appropriate table based on flight type
|
||||||
|
if (isBookedIn) {
|
||||||
|
loadArrivals(); // Refresh arrivals table
|
||||||
|
} else {
|
||||||
|
loadPPRs(); // Refresh all tables (PPR, local, departures)
|
||||||
|
}
|
||||||
showNotification(`Status updated to ${updatedStatus}`);
|
showNotification(`Status updated to ${updatedStatus}`);
|
||||||
if (!isLocal) {
|
if (!isLocal && !isBookedIn) {
|
||||||
closePPRModal(); // Close PPR modal after successful status update
|
closePPRModal(); // Close PPR modal after successful status update
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2730,6 +2897,7 @@
|
|||||||
const userManagementModal = document.getElementById('userManagementModal');
|
const userManagementModal = document.getElementById('userManagementModal');
|
||||||
const userModal = document.getElementById('userModal');
|
const userModal = document.getElementById('userModal');
|
||||||
const tableHelpModal = document.getElementById('tableHelpModal');
|
const tableHelpModal = document.getElementById('tableHelpModal');
|
||||||
|
const bookInModal = document.getElementById('bookInModal');
|
||||||
|
|
||||||
if (event.target === pprModal) {
|
if (event.target === pprModal) {
|
||||||
closePPRModal();
|
closePPRModal();
|
||||||
@@ -2746,6 +2914,9 @@
|
|||||||
if (event.target === tableHelpModal) {
|
if (event.target === tableHelpModal) {
|
||||||
closeTableHelp();
|
closeTableHelp();
|
||||||
}
|
}
|
||||||
|
if (event.target === bookInModal) {
|
||||||
|
closeBookInModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearArrivalAirportLookup() {
|
function clearArrivalAirportLookup() {
|
||||||
@@ -2787,6 +2958,50 @@
|
|||||||
document.getElementById('localFlightModal').style.display = 'none';
|
document.getElementById('localFlightModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openBookInModal() {
|
||||||
|
document.getElementById('book-in-form').reset();
|
||||||
|
document.getElementById('book-in-id').value = '';
|
||||||
|
document.getElementById('bookInModal').style.display = 'block';
|
||||||
|
|
||||||
|
// Clear aircraft lookup results
|
||||||
|
clearBookInAircraftLookup();
|
||||||
|
clearBookInArrivalAirportLookup();
|
||||||
|
|
||||||
|
// Populate ETA time slots
|
||||||
|
populateETATimeSlots();
|
||||||
|
|
||||||
|
// Auto-focus on registration field
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('book_in_registration').focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBookInModal() {
|
||||||
|
document.getElementById('bookInModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateETATimeSlots() {
|
||||||
|
const select = document.getElementById('book_in_eta_time');
|
||||||
|
const next15MinSlot = getNext10MinuteSlot();
|
||||||
|
|
||||||
|
select.innerHTML = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < 14; i++) {
|
||||||
|
const time = new Date(next15MinSlot.getTime() + i * 10 * 60 * 1000);
|
||||||
|
const hours = time.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||||
|
const timeStr = `${hours}:${minutes}`;
|
||||||
|
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = timeStr;
|
||||||
|
option.textContent = timeStr;
|
||||||
|
if (i === 0) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle flight type change to show/hide destination field
|
// Handle flight type change to show/hide destination field
|
||||||
function handleFlightTypeChange(flightType) {
|
function handleFlightTypeChange(flightType) {
|
||||||
const destGroup = document.getElementById('departure-destination-group');
|
const destGroup = document.getElementById('departure-destination-group');
|
||||||
@@ -2870,6 +3085,7 @@
|
|||||||
|
|
||||||
// Local Flight Edit Modal Functions
|
// Local Flight Edit Modal Functions
|
||||||
let currentLocalFlightId = null;
|
let currentLocalFlightId = null;
|
||||||
|
let currentBookedInArrivalId = null;
|
||||||
|
|
||||||
async function openLocalFlightEditModal(flightId) {
|
async function openLocalFlightEditModal(flightId) {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
@@ -3026,6 +3242,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update status from table for booked-in arrivals
|
||||||
|
async function updateArrivalStatusFromTable(arrivalId, status) {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
// Show confirmation for cancel actions
|
||||||
|
if (status === 'CANCELLED') {
|
||||||
|
if (!confirm('Are you sure you want to cancel this arrival? This action cannot be easily undone.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/arrivals/${arrivalId}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: status })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to update status');
|
||||||
|
|
||||||
|
loadArrivals(); // Refresh arrivals table
|
||||||
|
showNotification(`Arrival marked as ${status.toLowerCase()}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating status:', error);
|
||||||
|
showNotification('Error updating arrival status', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update status from modal (uses currentLocalFlightId)
|
// Update status from modal (uses currentLocalFlightId)
|
||||||
async function updateLocalFlightStatus(status) {
|
async function updateLocalFlightStatus(status) {
|
||||||
if (!currentLocalFlightId || !accessToken) return;
|
if (!currentLocalFlightId || !accessToken) return;
|
||||||
@@ -3267,6 +3514,86 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('book-in-form').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const arrivalData = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
// Skip the hidden id field and empty values
|
||||||
|
if (key === 'id') return;
|
||||||
|
|
||||||
|
// Handle time-only ETA (always today)
|
||||||
|
if (key === 'eta_time') {
|
||||||
|
if (value.trim()) {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(today.getDate()).padStart(2, '0');
|
||||||
|
const dateStr = `${year}-${month}-${day}`;
|
||||||
|
// Store ETA in the eta field
|
||||||
|
arrivalData.eta = new Date(`${dateStr}T${value}`).toISOString();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include non-empty values
|
||||||
|
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
|
||||||
|
if (key === 'pob') {
|
||||||
|
arrivalData[key] = parseInt(value);
|
||||||
|
} else if (value.trim) {
|
||||||
|
arrivalData[key] = value.trim();
|
||||||
|
} else {
|
||||||
|
arrivalData[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Book In uses LANDED status (they're arriving now)
|
||||||
|
arrivalData.status = 'LANDED';
|
||||||
|
|
||||||
|
console.log('Submitting arrivals data:', arrivalData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/arrivals/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(arrivalData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = 'Failed to book in arrival';
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
if (errorData.detail) {
|
||||||
|
errorMessage = typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail);
|
||||||
|
} else if (errorData.errors) {
|
||||||
|
errorMessage = errorData.errors.map(e => e.msg).join(', ');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const text = await response.text();
|
||||||
|
console.error('Server response:', text);
|
||||||
|
errorMessage = `Server error (${response.status})`;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
closeBookInModal();
|
||||||
|
loadPPRs(); // Refresh tables
|
||||||
|
showNotification(`Aircraft ${result.registration} booked in successfully!`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error booking in arrival:', error);
|
||||||
|
showNotification(`Error: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add hover listeners to all notes tooltips
|
// Add hover listeners to all notes tooltips
|
||||||
function setupTooltips() {
|
function setupTooltips() {
|
||||||
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
||||||
|
|||||||
@@ -234,8 +234,8 @@
|
|||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log('WebSocket message received:', data);
|
console.log('WebSocket message received:', data);
|
||||||
|
|
||||||
// Refresh display when any PPR-related or local flight event occurs
|
// Refresh display when any PPR-related, local flight, departure, or arrival event occurs
|
||||||
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_'))) {
|
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_') || data.type.includes('departure_') || data.type.includes('arrival_'))) {
|
||||||
console.log('Flight update detected, refreshing display...');
|
console.log('Flight update detected, refreshing display...');
|
||||||
loadArrivals();
|
loadArrivals();
|
||||||
loadDepartures();
|
loadDepartures();
|
||||||
@@ -305,6 +305,7 @@
|
|||||||
// Build rows asynchronously to lookup airport names
|
// Build rows asynchronously to lookup airport names
|
||||||
const rows = await Promise.all(arrivals.map(async (arrival) => {
|
const rows = await Promise.all(arrivals.map(async (arrival) => {
|
||||||
const isLocal = arrival.isLocalFlight;
|
const isLocal = arrival.isLocalFlight;
|
||||||
|
const isBookedIn = arrival.isBookedIn;
|
||||||
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
// Local flight
|
// Local flight
|
||||||
@@ -321,6 +322,30 @@
|
|||||||
<td>${timeDisplay}</td>
|
<td>${timeDisplay}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
} else if (isBookedIn) {
|
||||||
|
// Booked-in arrival
|
||||||
|
const aircraftId = arrival.callsign || arrival.registration || '';
|
||||||
|
const aircraftDisplay = `${escapeHtml(aircraftId)} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.type || '')})</span>`;
|
||||||
|
const fromDisplay = await getAirportName(arrival.in_from || '');
|
||||||
|
|
||||||
|
let timeDisplay;
|
||||||
|
if (arrival.status === 'LANDED' && arrival.landed_dt) {
|
||||||
|
// Show landed time if LANDED
|
||||||
|
const time = convertToLocalTime(arrival.landed_dt);
|
||||||
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`;
|
||||||
|
} else {
|
||||||
|
// Show ETA if BOOKED_IN
|
||||||
|
const time = convertToLocalTime(arrival.eta);
|
||||||
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">IN AIR</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${aircraftDisplay}</td>
|
||||||
|
<td>${escapeHtml(fromDisplay)}</td>
|
||||||
|
<td>${timeDisplay}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
// PPR
|
// PPR
|
||||||
const aircraftId = arrival.ac_call || arrival.ac_reg || '';
|
const aircraftId = arrival.ac_call || arrival.ac_reg || '';
|
||||||
|
|||||||
170
web/lookups.js
170
web/lookups.js
@@ -2,6 +2,25 @@
|
|||||||
* Lookup Utilities - Reusable functions for aircraft and airport lookups
|
* Lookup Utilities - Reusable functions for aircraft and airport lookups
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format aircraft registration based on UK rules
|
||||||
|
* - 5 alphabetic chars: add hyphen after first char (GIVYY -> G-IVYY)
|
||||||
|
* - Otherwise: just uppercase (N123AD -> N123AD)
|
||||||
|
*/
|
||||||
|
function formatAircraftRegistration(input) {
|
||||||
|
if (!input) return '';
|
||||||
|
|
||||||
|
const cleaned = input.trim().toUpperCase();
|
||||||
|
|
||||||
|
// If exactly 5 characters and all alphabetic, add hyphen
|
||||||
|
if (cleaned.length === 5 && /^[A-Z]{5}$/.test(cleaned)) {
|
||||||
|
return cleaned[0] + '-' + cleaned.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just return uppercase version
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a reusable lookup handler
|
* Creates a reusable lookup handler
|
||||||
* @param {string} fieldId - ID of the input field
|
* @param {string} fieldId - ID of the input field
|
||||||
@@ -19,11 +38,15 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
};
|
};
|
||||||
const config = { ...defaults, ...options };
|
const config = { ...defaults, ...options };
|
||||||
let debounceTimeout;
|
let debounceTimeout;
|
||||||
|
let currentResults = [];
|
||||||
|
let selectedIndex = -1;
|
||||||
|
let keydownHandlerAttached = false;
|
||||||
|
|
||||||
const lookup = {
|
const lookup = {
|
||||||
// Main handler called by oninput
|
// Main handler called by oninput
|
||||||
handle: (value) => {
|
handle: (value) => {
|
||||||
clearTimeout(debounceTimeout);
|
clearTimeout(debounceTimeout);
|
||||||
|
selectedIndex = -1; // Reset selection on new input
|
||||||
|
|
||||||
if (!value || value.trim().length < config.minLength) {
|
if (!value || value.trim().length < config.minLength) {
|
||||||
lookup.clear();
|
lookup.clear();
|
||||||
@@ -36,6 +59,75 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
}, config.debounceMs);
|
}, config.debounceMs);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Attach keyboard handler once (for airport lookups)
|
||||||
|
attachKeyboardHandler: () => {
|
||||||
|
if (config.isAirport && !keydownHandlerAttached) {
|
||||||
|
try {
|
||||||
|
const inputField = document.getElementById(fieldId);
|
||||||
|
if (inputField) {
|
||||||
|
inputField.addEventListener('keydown', (e) => lookup.handleKeydown(e));
|
||||||
|
keydownHandlerAttached = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error attaching keyboard handler:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle keyboard events
|
||||||
|
handleKeydown: (event) => {
|
||||||
|
if (!currentResults || currentResults.length === 0) return;
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, currentResults.length - 1);
|
||||||
|
lookup.updateSelection();
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, -1);
|
||||||
|
lookup.updateSelection();
|
||||||
|
} else if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (selectedIndex >= 0 && currentResults[selectedIndex]) {
|
||||||
|
lookup.selectResult(currentResults[selectedIndex]);
|
||||||
|
} else if (currentResults.length === 1) {
|
||||||
|
// Auto-select if only one result and Enter pressed
|
||||||
|
lookup.selectResult(currentResults[0]);
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
lookup.clear();
|
||||||
|
selectedIndex = -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update visual selection
|
||||||
|
updateSelection: () => {
|
||||||
|
const resultsDiv = document.getElementById(resultsId);
|
||||||
|
if (!resultsDiv) return;
|
||||||
|
|
||||||
|
const options = resultsDiv.querySelectorAll('.lookup-option');
|
||||||
|
options.forEach((opt, idx) => {
|
||||||
|
if (idx === selectedIndex) {
|
||||||
|
opt.classList.add('lookup-option-selected');
|
||||||
|
opt.scrollIntoView({ block: 'nearest' });
|
||||||
|
} else {
|
||||||
|
opt.classList.remove('lookup-option-selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Select a result item
|
||||||
|
selectResult: (item) => {
|
||||||
|
const field = document.getElementById(fieldId);
|
||||||
|
if (field) {
|
||||||
|
field.value = item.icao;
|
||||||
|
}
|
||||||
|
lookup.clear();
|
||||||
|
currentResults = [];
|
||||||
|
selectedIndex = -1;
|
||||||
|
if (selectCallback) selectCallback(item.icao);
|
||||||
|
},
|
||||||
|
|
||||||
// Perform the lookup
|
// Perform the lookup
|
||||||
perform: async (searchTerm) => {
|
perform: async (searchTerm) => {
|
||||||
try {
|
try {
|
||||||
@@ -71,9 +163,15 @@ 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, show message on multiple
|
// Aircraft lookup: auto-populate on single match, format input on no match
|
||||||
if (!results || results.length === 0) {
|
if (!results || results.length === 0) {
|
||||||
resultsDiv.innerHTML = '<div class="aircraft-no-match">No matches found</div>';
|
// Format the aircraft registration and auto-populate
|
||||||
|
const formatted = formatAircraftRegistration(searchTerm);
|
||||||
|
const field = document.getElementById(fieldId);
|
||||||
|
if (field) {
|
||||||
|
field.value = formatted;
|
||||||
|
}
|
||||||
|
resultsDiv.innerHTML = ''; // Clear results, field is auto-populated
|
||||||
} 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];
|
||||||
@@ -93,6 +191,8 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
typeFieldId = 'ac_type';
|
typeFieldId = 'ac_type';
|
||||||
} else if (fieldId === 'local_registration') {
|
} else if (fieldId === 'local_registration') {
|
||||||
typeFieldId = 'local_type';
|
typeFieldId = 'local_type';
|
||||||
|
} else if (fieldId === 'book_in_registration') {
|
||||||
|
typeFieldId = 'book_in_type';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeFieldId) {
|
if (typeFieldId) {
|
||||||
@@ -108,18 +208,21 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Airport lookup: show list of options
|
// Airport lookup: show list of options with keyboard navigation
|
||||||
if (!results || results.length === 0) {
|
if (!results || results.length === 0) {
|
||||||
resultsDiv.innerHTML = '<div class="lookup-no-match">No matches found - will use as entered</div>';
|
resultsDiv.innerHTML = '<div class="lookup-no-match">No matches found - will use as entered</div>';
|
||||||
|
currentResults = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsToShow = results.slice(0, config.maxResults);
|
currentResults = results.slice(0, config.maxResults);
|
||||||
const matchText = itemsToShow.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:';
|
selectedIndex = -1; // Reset selection when showing new results
|
||||||
|
|
||||||
|
const matchText = currentResults.length === 1 ? 'Match found - press ENTER or click to select:' : 'Multiple matches found - use arrow keys and ENTER to select:';
|
||||||
|
|
||||||
let html = `<div class="lookup-no-match" style="margin-bottom: 0.5rem;">${matchText}</div><div class="lookup-list">`;
|
let html = `<div class="lookup-no-match" style="margin-bottom: 0.5rem;">${matchText}</div><div class="lookup-list">`;
|
||||||
|
|
||||||
itemsToShow.forEach(item => {
|
currentResults.forEach((item, idx) => {
|
||||||
html += `
|
html += `
|
||||||
<div class="lookup-option" onclick="lookupManager.selectItem('${resultsId}', '${fieldId}', '${item.icao}')">
|
<div class="lookup-option" onclick="lookupManager.selectItem('${resultsId}', '${fieldId}', '${item.icao}')">
|
||||||
<div class="lookup-code">${item.icao}</div>
|
<div class="lookup-code">${item.icao}</div>
|
||||||
@@ -131,6 +234,9 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) {
|
|||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
resultsDiv.innerHTML = html;
|
resultsDiv.innerHTML = html;
|
||||||
|
|
||||||
|
// Attach keyboard handler (only once per lookup instance)
|
||||||
|
lookup.attachKeyboardHandler();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -238,6 +344,30 @@ function initializeLookups() {
|
|||||||
{ isAircraft: true, minLength: 4, debounceMs: 300 }
|
{ isAircraft: true, minLength: 4, debounceMs: 300 }
|
||||||
);
|
);
|
||||||
lookupManager.register('local-aircraft', localAircraftLookup);
|
lookupManager.register('local-aircraft', localAircraftLookup);
|
||||||
|
|
||||||
|
const bookInAircraftLookup = createLookup(
|
||||||
|
'book_in_registration',
|
||||||
|
'book-in-aircraft-lookup-results',
|
||||||
|
null,
|
||||||
|
{ isAircraft: true, minLength: 4, debounceMs: 300 }
|
||||||
|
);
|
||||||
|
lookupManager.register('book-in-aircraft', bookInAircraftLookup);
|
||||||
|
|
||||||
|
const bookInArrivalAirportLookup = createLookup(
|
||||||
|
'book_in_from',
|
||||||
|
'book-in-arrival-airport-lookup-results',
|
||||||
|
null,
|
||||||
|
{ isAirport: true, minLength: 2 }
|
||||||
|
);
|
||||||
|
lookupManager.register('book-in-arrival-airport', bookInArrivalAirportLookup);
|
||||||
|
|
||||||
|
// Attach keyboard handlers to airport input fields
|
||||||
|
setTimeout(() => {
|
||||||
|
if (arrivalAirportLookup.attachKeyboardHandler) arrivalAirportLookup.attachKeyboardHandler();
|
||||||
|
if (departureAirportLookup.attachKeyboardHandler) departureAirportLookup.attachKeyboardHandler();
|
||||||
|
if (localOutToLookup.attachKeyboardHandler) localOutToLookup.attachKeyboardHandler();
|
||||||
|
if (bookInArrivalAirportLookup.attachKeyboardHandler) bookInArrivalAirportLookup.attachKeyboardHandler();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on DOM ready or immediately if already loaded
|
// Initialize on DOM ready or immediately if already loaded
|
||||||
@@ -315,3 +445,31 @@ function selectLocalOutToAirport(icaoCode) {
|
|||||||
function selectLocalAircraft(registration) {
|
function selectLocalAircraft(registration) {
|
||||||
lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration);
|
lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBookInAircraftLookup(value) {
|
||||||
|
const lookup = lookupManager.lookups['book-in-aircraft'];
|
||||||
|
if (lookup) lookup.handle(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBookInArrivalAirportLookup(value) {
|
||||||
|
const lookup = lookupManager.lookups['book-in-arrival-airport'];
|
||||||
|
if (lookup) lookup.handle(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBookInAircraftLookup() {
|
||||||
|
const lookup = lookupManager.lookups['book-in-aircraft'];
|
||||||
|
if (lookup) lookup.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBookInArrivalAirportLookup() {
|
||||||
|
const lookup = lookupManager.lookups['book-in-arrival-airport'];
|
||||||
|
if (lookup) lookup.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBookInAircraft(registration) {
|
||||||
|
lookupManager.selectItem('book-in-aircraft-lookup-results', 'book_in_registration', registration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBookInArrivalAirport(icaoCode) {
|
||||||
|
lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user