Compare commits

..

5 Commits

Author SHA1 Message Date
bddbe1451f Little tidy 2026-02-20 16:50:03 -05:00
785562407a localStorage for booking out 2026-02-20 16:42:06 -05:00
5bb229ad78 Oops 2026-02-20 12:23:09 -05:00
8a2dd5544c ignore QR 2026-02-20 12:21:12 -05:00
3a4085afc6 Booking out QR code 2026-02-20 12:19:21 -05:00
8 changed files with 430 additions and 56 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
web/assets/booking-qr.png
# Python
__pycache__/
*.py[cod]

View File

@@ -3,11 +3,12 @@ FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
# Install system dependencies including qrencode
RUN apt-get update && apt-get install -y \
gcc \
default-libmysqlclient-dev \
pkg-config \
qrencode \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching

View File

@@ -174,6 +174,12 @@ else
exit 1
fi
echo ""
echo "========================================="
echo "Generating QR Code"
echo "========================================="
python3 /app/generate_qr.py
echo ""
echo "========================================="
echo "Starting Application Server"

38
backend/generate_qr.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Generate booking QR code at container startup"""
import os
import sys
import subprocess
def generate_booking_qr():
"""Generate QR code for the booking page"""
# Get base URL from environment, default to localhost
base_url = os.environ.get('BASE_URL', 'http://localhost')
booking_url = f"{base_url}/book"
# Create output directory if it doesn't exist
output_dir = '/web/assets'
os.makedirs(output_dir, exist_ok=True)
output_file = f'{output_dir}/booking-qr.png'
try:
# Generate QR code using qrencode
subprocess.run(
['qrencode', '-o', output_file, '-s', '5', booking_url],
check=True,
capture_output=True
)
print(f"✓ Generated booking QR code: {output_file}")
print(f" URL: {booking_url}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to generate QR code: {e.stderr.decode()}", file=sys.stderr)
return False
except FileNotFoundError:
print("✗ qrencode not found. Install with: apt-get install qrencode", file=sys.stderr)
return False
if __name__ == '__main__':
success = generate_booking_qr()
sys.exit(0 if success else 1)

View File

@@ -36,6 +36,7 @@ services:
volumes:
- ./backend:/app
- ./db-init:/db-init:ro # Mount CSV data for seeding
- ./web/assets:/web/assets # Mount assets for QR code generation
networks:
- app_network
extra_hosts:

View File

@@ -48,6 +48,7 @@ services:
volumes:
- ./backend:/app
- ./db-init:/db-init:ro # Mount CSV data for seeding
- ./web/assets:/web/assets # Mount assets for QR code generation
networks:
- private_network
- public_network

View File

@@ -98,10 +98,17 @@
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-size: 16px;
font-family: inherit;
}
input[type="time"],
input[type="number"] {
padding: 12px 8px;
text-align: center;
min-width: 80px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #3498db;
@@ -119,6 +126,12 @@
gap: 10px;
}
.form-row-3col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.btn-submit {
width: 100%;
padding: 14px;
@@ -284,6 +297,50 @@
font-size: 12px;
}
.recent-regs-section {
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 10px;
margin-bottom: 15px;
}
.recent-regs-title {
font-weight: 600;
font-size: 12px;
color: #666;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.recent-regs-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.recent-reg-btn {
background: white;
border: 1px solid #3498db;
color: #3498db;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
.recent-reg-btn:hover {
background: #3498db;
color: white;
}
.recent-regs-section.empty {
display: none;
}
@media (max-width: 480px) {
.container {
padding: 15px;
@@ -294,10 +351,6 @@
padding: 15px;
}
.form-row {
grid-template-columns: 1fr;
}
.tabs {
gap: 5px;
}
@@ -327,11 +380,14 @@
<button class="tab-btn active" onclick="switchTab(this, 'local')">Local</button>
<button class="tab-btn" onclick="switchTab(this, 'circuits')">Circuits</button>
<button class="tab-btn" onclick="switchTab(this, 'departure')">Departure</button>
<button class="tab-btn" onclick="switchTab(this, 'arrival')">Arrival</button>
</div>
<!-- Local Flight Form -->
<div id="local" class="tab-content active">
<div id="localRecentSection" class="recent-regs-section">
<div class="recent-regs-title">⏱️ Recently Used</div>
<div id="localRecent" class="recent-regs-container"></div>
</div>
<form id="localFlightForm" onsubmit="handleSubmit(event, 'local')">
<div class="form-group">
@@ -351,21 +407,20 @@
</div>
</div>
<div class="form-row">
<div class="form-row-3col">
<div class="form-group">
<label for="localPOB">Persons on Board <span class="required">*</span></label>
<label for="localPOB">POB <span class="required">*</span></label>
<input type="number" id="localPOB" name="pob" value="1" min="1" required>
</div>
<div class="form-group">
<label for="localDuration">Est. Duration (minutes)</label>
<label for="localDuration">Duration</label>
<input type="number" id="localDuration" name="duration" value="45" min="5">
</div>
</div>
<div class="form-group">
<label for="localETD">Est. Takeoff Time</label>
<label for="localETD">ETD</label>
<input type="time" id="localETD" name="etd">
</div>
</div>
<div class="form-group">
<label for="localNotes">Notes</label>
@@ -378,6 +433,11 @@
<!-- Circuit Form -->
<div id="circuits" class="tab-content">
<div id="circuitsRecentSection" class="recent-regs-section">
<div class="recent-regs-title">⏱️ Recently Used</div>
<div id="circuitsRecent" class="recent-regs-container"></div>
</div>
<form id="circuitForm" onsubmit="handleSubmit(event, 'circuits')">
<div class="form-group">
<label for="circuitReg">Aircraft Registration <span class="required">*</span></label>
@@ -396,21 +456,20 @@
</div>
</div>
<div class="form-row">
<div class="form-row-3col">
<div class="form-group">
<label for="circuitPOB">Persons on Board <span class="required">*</span></label>
<label for="circuitPOB">POB <span class="required">*</span></label>
<input type="number" id="circuitPOB" name="pob" value="1" min="1" required>
</div>
<div class="form-group">
<label for="circuitDuration">Est. Duration (minutes)</label>
<label for="circuitDuration">Duration</label>
<input type="number" id="circuitDuration" name="duration" value="30" min="5">
</div>
</div>
<div class="form-group">
<label for="circuitETD">Est. Takeoff Time</label>
<label for="circuitETD">ETD</label>
<input type="time" id="circuitETD" name="etd">
</div>
</div>
<div class="form-group">
<label for="circuitNotes">Notes</label>
@@ -423,9 +482,11 @@
<!-- Departure Form -->
<div id="departure" class="tab-content">
<div class="info-box">
➡️ Book a flight departing to another airport (email optional)
<div id="departureRecentSection" class="recent-regs-section">
<div class="recent-regs-title">⏱️ Recently Used</div>
<div id="departureRecent" class="recent-regs-container"></div>
</div>
<form id="departureForm" onsubmit="handleSubmit(event, 'departure')">
<div class="form-group">
<label for="depReg">Aircraft Registration <span class="required">*</span></label>
@@ -444,7 +505,7 @@
</div>
</div>
<div class="form-row">
<div class="form-row-3col">
<div class="form-group">
<label for="depPOB">Persons on Board <span class="required">*</span></label>
<input type="number" id="depPOB" name="pob" value="1" min="1" required>
@@ -452,30 +513,19 @@
<div class="form-group">
<label for="depTo">Destination (ICAO) <span class="required">*</span></label>
<input type="text" id="depTo" name="out_to" placeholder="e.g., KJFK" required oninput="handleAirportLookup(this.value, 'depTo')">
<div id="depTo-lookup-results" class="airport-lookup-results"></div>
</div>
</div>
<div class="form-group">
<label for="depETD">Takeoff Time</label>
<input type="time" id="depETD" name="etd">
</div>
</div>
<div id="depTo-lookup-results" class="airport-lookup-results"></div>
<div class="form-group">
<label for="depNotes">Notes</label>
<textarea id="depNotes" name="notes" placeholder="Any additional information..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="depEmail">Pilot Email</label>
<input type="email" id="depEmail" name="pilot_email">
</div>
<div class="form-group">
<label for="depName">Pilot Name</label>
<input type="text" id="depName" name="pilot_name" placeholder="Optional">
</div>
</div>
<button type="submit" class="btn-submit">✈️ Book Departure</button>
</form>
@@ -483,9 +533,12 @@
<!-- Arrival Form -->
<div id="arrival" class="tab-content">
<div class="info-box">
⬅️ Book an arrival from another airport (email optional)
<div id="arrivalRecentSection" class="recent-regs-section">
<div class="recent-regs-title">⏱️ Recently Used</div>
<div id="arrivalRecent" class="recent-regs-container"></div>
</div>
<form id="arrivalForm" onsubmit="handleSubmit(event, 'arrival')">
<div class="form-group">
<label for="arrReg">Aircraft Registration <span class="required">*</span></label>
@@ -504,7 +557,7 @@
</div>
</div>
<div class="form-row">
<div class="form-row-3col">
<div class="form-group">
<label for="arrPOB">Persons on Board <span class="required">*</span></label>
<input type="number" id="arrPOB" name="pob" value="1" min="1" required>
@@ -512,14 +565,13 @@
<div class="form-group">
<label for="arrFrom">Origin (ICAO) <span class="required">*</span></label>
<input type="text" id="arrFrom" name="in_from" placeholder="e.g., KJFK" required oninput="handleAirportLookup(this.value, 'arrFrom')">
<div id="arrFrom-lookup-results" class="airport-lookup-results"></div>
</div>
</div>
<div class="form-group">
<label for="arrETA">Arrival Time</label>
<input type="time" id="arrETA" name="eta">
</div>
</div>
<div id="arrFrom-lookup-results" class="airport-lookup-results"></div>
<div class="form-group">
<label for="arrNotes">Notes</label>
@@ -547,13 +599,171 @@
</div>
<div class="footer">
<p>📞 Questions? Contact the airfield operations team</p>
<p>📞 Questions? Tower 01792 687042</p>
<p style="margin-top: 10px; font-size: 12px; color: #999;">Version 1.0</p>
</div>
</div>
<script>
const API_BASE = window.location.origin + '/api/v1';
const STORAGE_RECENT_REGS_KEY = 'bookingPage_recentRegs';
const STORAGE_AIRCRAFT_TYPES_KEY = 'bookingPage_aircraftTypes';
const MAX_RECENT = 5;
// ==================== localStorage Management ====================
function getRecentRegistrations() {
try {
const stored = localStorage.getItem(STORAGE_RECENT_REGS_KEY);
return stored ? JSON.parse(stored) : [];
} catch (e) {
console.error('Error getting recent registrations:', e);
return [];
}
}
function saveRecentRegistration(registration) {
try {
if (!registration || registration.trim() === '') {
console.log('⚠️ saveRecentRegistration: empty registration');
return;
}
const reg = registration.toUpperCase().trim();
let recentRegs = getRecentRegistrations();
console.log('📝 Saving recent reg:', reg);
// Remove if already exists
recentRegs = recentRegs.filter(r => r !== reg);
// Add to front
recentRegs.unshift(reg);
// Keep only last MAX_RECENT
recentRegs = recentRegs.slice(0, MAX_RECENT);
localStorage.setItem(STORAGE_RECENT_REGS_KEY, JSON.stringify(recentRegs));
console.log('✅ Recent regs updated:', recentRegs);
updateAllRecentRegsUI();
} catch (e) {
console.error('Error saving recent registration:', e);
}
}
function cacheAircraftType(registration, typeCode) {
try {
if (!registration || !typeCode) {
console.log('⚠️ cache skipped - registration:', registration, 'typeCode:', typeCode);
return;
}
const reg = registration.toUpperCase().trim();
const stored = localStorage.getItem(STORAGE_AIRCRAFT_TYPES_KEY);
let aircraftTypes = stored ? JSON.parse(stored) : {};
aircraftTypes[reg] = typeCode;
localStorage.setItem(STORAGE_AIRCRAFT_TYPES_KEY, JSON.stringify(aircraftTypes));
console.log('✅ Cached aircraft type:', reg, '=', typeCode);
console.log('All cached types:', aircraftTypes);
} catch (e) {
console.error('Error caching aircraft type:', e);
}
}
function getCachedAircraftType(registration) {
try {
const reg = registration.toUpperCase().trim();
const stored = localStorage.getItem(STORAGE_AIRCRAFT_TYPES_KEY);
if (!stored) {
console.log('⚠️ No cached types found for:', reg);
return null;
}
const aircraftTypes = JSON.parse(stored);
const typeCode = aircraftTypes[reg] || null;
console.log('🔍 Retrieved cached type for', reg, '=', typeCode);
return typeCode;
} catch (e) {
console.error('Error getting cached aircraft type:', e);
return null;
}
}
function applyRecentReg(registration, formType) {
const registerIdMap = {
'local': 'localReg',
'circuits': 'circuitReg',
'departure': 'depReg',
'arrival': 'arrReg'
};
const typeIdMap = {
'local': 'localType',
'circuits': 'circuitType',
'departure': 'depType',
'arrival': 'arrType'
};
const regId = registerIdMap[formType];
const typeId = typeIdMap[formType];
console.log('📍 applyRecentReg called - reg:', registration, 'form:', formType);
document.getElementById(regId).value = registration;
// Restore cached type if available (always overwrite)
const cachedType = getCachedAircraftType(registration);
if (cachedType) {
console.log('✔️ Applying cached type:', cachedType);
document.getElementById(typeId).value = cachedType;
} else {
console.log(' No cached type found');
}
document.getElementById(regId).focus();
// Trigger the lookup
handleAircraftLookup(registration, formType);
}
function updateRecentRegsUI(formType) {
const sectionIdMap = {
'local': 'localRecentSection',
'circuits': 'circuitsRecentSection',
'departure': 'departureRecentSection',
'arrival': 'arrivalRecentSection'
};
const containerIdMap = {
'local': 'localRecent',
'circuits': 'circuitsRecent',
'departure': 'departureRecent',
'arrival': 'arrivalRecent'
};
const recentRegs = getRecentRegistrations();
const section = document.getElementById(sectionIdMap[formType]);
const container = document.getElementById(containerIdMap[formType]);
if (recentRegs.length === 0) {
section.classList.add('empty');
return;
}
section.classList.remove('empty');
container.innerHTML = recentRegs.map(reg => `
<button type="button" class="recent-reg-btn" onclick="applyRecentReg('${reg}', '${formType}')" title="Use ${reg}">
${reg}
</button>
`).join('');
}
function updateAllRecentRegsUI() {
['local', 'circuits', 'departure', 'arrival'].forEach(formType => {
updateRecentRegsUI(formType);
});
}
// ==================== End localStorage Management ====================
function switchTab(button, tabName) {
// Hide all tabs
@@ -597,6 +807,17 @@
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// Save the registration to localStorage
if (data.registration) {
saveRecentRegistration(data.registration);
// Also cache the aircraft type if provided
if (data.type) {
console.log('💾 Caching type on submit - reg:', data.registration, 'type:', data.type);
cacheAircraftType(data.registration, data.type);
}
}
// Set flight_type based on form type
if (formType === 'local' && !data.flight_type) {
data.flight_type = 'LOCAL';
@@ -665,16 +886,40 @@
const minutes = String(now.getMinutes()).padStart(2, '0');
const timeValue = `${hours}:${minutes}`;
// ETD fields should default to 15 minutes from now
const futureTime = new Date(now.getTime() + 15 * 60000);
const futureHours = String(futureTime.getHours()).padStart(2, '0');
const futureMinutes = String(futureTime.getMinutes()).padStart(2, '0');
const futureTimeValue = `${futureHours}:${futureMinutes}`;
const etdFieldIds = ['localETD', 'circuitETD', 'depETD'];
document.querySelectorAll('input[type="time"]').forEach(input => {
if (!input.value) {
if (etdFieldIds.includes(input.id)) {
input.value = futureTimeValue;
} else {
input.value = timeValue;
}
}
});
}
// Aircraft lookup functionality
let aircraftLookupTimeouts = {};
function formatAircraftRegistration(input) {
const reg = input.trim().toUpperCase();
// If 5 alpha characters, add hyphen: GIVYY -> G-IVYY
if (/^[A-Z]{5}$/.test(reg)) {
return reg[0] + '-' + reg.slice(1);
}
// Otherwise just capitalize
return reg;
}
async function handleAircraftLookup(registration, formType) {
if (aircraftLookupTimeouts[formType]) {
clearTimeout(aircraftLookupTimeouts[formType]);
@@ -703,6 +948,7 @@
return;
}
console.log('🔎 Aircraft lookup:', registration, 'formType:', formType);
resultsDiv.innerHTML = '<span class="lookup-searching">Searching...</span>';
aircraftLookupTimeouts[formType] = setTimeout(async () => {
@@ -710,19 +956,32 @@
const response = await fetch(`${API_BASE}/aircraft/public/lookup/${registration.toUpperCase()}`);
if (response.ok) {
const data = await response.json();
console.log('📡 API Response:', data, 'Length:', data ? data.length : 'null');
if (data && data.length > 0) {
if (data.length === 1) {
const aircraft = data[0];
document.getElementById(regId).value = aircraft.registration || registration.toUpperCase();
const regValue = aircraft.registration || registration.toUpperCase();
console.log('✨ Single match found:', regValue);
console.log('📋 Full aircraft object:', aircraft);
console.log('🔤 aircraft.type_code:', aircraft.type_code);
document.getElementById(regId).value = regValue;
// Cache the aircraft type
if (aircraft.type_code) {
console.log('💾 About to cache - reg:', regValue, 'type:', aircraft.type_code);
cacheAircraftType(regValue, aircraft.type_code);
} else {
console.log('⚠️ type_code is empty or null, not caching');
}
resultsDiv.innerHTML = `
<div class="lookup-match">
${aircraft.registration || registration.toUpperCase()} - ${aircraft.type_code || 'Unknown'} ${aircraft.model ? `(${aircraft.model})` : ''}
${regValue} - ${aircraft.type_code || 'Unknown'} ${aircraft.model ? `(${aircraft.model})` : ''}
</div>
`;
if (!document.getElementById(typeId).value) {
document.getElementById(typeId).value = aircraft.type_code || '';
}
} else if (data.length <= 10) {
console.log('🎲 Multiple matches found:', data.length);
resultsDiv.innerHTML = `
<div class="aircraft-list">
${data.map(aircraft => `
@@ -737,9 +996,14 @@
resultsDiv.innerHTML = '<span class="lookup-no-match">Too many matches, please be more specific</span>';
}
} else {
resultsDiv.innerHTML = '<span class="lookup-no-match">No aircraft found</span>';
// No aircraft found - auto-format and apply the registration
const formattedReg = formatAircraftRegistration(registration);
document.getElementById(regId).value = formattedReg;
console.log('❌ No match - formatted:', registration, '→', formattedReg);
resultsDiv.innerHTML = `<span class="lookup-no-match">No match found - formatted as ${formattedReg}</span>`;
}
} else {
console.log('⚠️ Lookup response not ok:', response.status);
resultsDiv.innerHTML = '';
}
} catch (error) {
@@ -767,8 +1031,15 @@
const regId = registerIdMap[formType];
const typeId = typeIdMap[formType];
console.log('🎯 selectAircraft called:', registration, typeCode);
document.getElementById(regId).value = registration;
document.getElementById(typeId).value = typeCode;
// Cache the aircraft type
if (typeCode) {
cacheAircraftType(registration, typeCode);
}
document.getElementById(`${regId}-lookup-results`).innerHTML = '';
document.getElementById(regId).blur();
}
@@ -847,6 +1118,8 @@
// Clear lookup results on blur
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 Page loaded - initializing...');
['localReg', 'depReg', 'arrReg', 'depTo', 'arrFrom'].forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
@@ -857,6 +1130,14 @@
});
}
});
// Initialize recent registrations UI
updateAllRecentRegsUI();
console.log('✅ Recent regs UI initialized');
console.log('🗄️ Cached aircraft types:', localStorage.getItem(STORAGE_AIRCRAFT_TYPES_KEY));
// Set default times
setDefaultTimes();
});
// Initialize on page load

View File

@@ -178,6 +178,35 @@
left: 28px;
}
/* QR code for booking */
.qr-code-container {
position: absolute;
left: 300px;
top: 50%;
transform: translateY(-50%);
background: white;
padding: 5px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.qr-code-container .qr-label {
font-size: 12px;
font-weight: bold;
color: #333;
white-space: nowrap;
}
.qr-code-container img {
display: block;
max-width: 120px;
height: auto;
}
/* Santa hat styles */
.santa-hat {
position: absolute;
@@ -357,6 +386,10 @@
<body>
<header>
<img src="assets/logo.png" alt="EGFH Logo" class="left-image">
<div class="qr-code-container">
<img id="bookingQR" alt="Scan to book a flight" title="Scan to book a flight">
<div class="qr-label">Book Out</div>
</div>
<h1>Flight Information</h1>
<img src="assets/flightImg.png" alt="EGFH Logo" class="right-image">
</header>
@@ -847,11 +880,22 @@
return typeMap[flightType] || flightType;
}
// Generate QR code for booking page
function generateBookingQR() {
const qrImg = document.getElementById('bookingQR');
if (qrImg) {
qrImg.src = '/assets/booking-qr.png';
}
}
// Load data on page load
window.addEventListener('load', function() {
// Initialize Christmas mode
initChristmasMode();
// Load booking QR code
generateBookingQR();
loadArrivals();
loadDepartures();