Booking form fixes

This commit is contained in:
2026-03-25 17:25:26 -04:00
parent 9867156334
commit 2dce14507b
3 changed files with 237 additions and 86 deletions
+11
View File
@@ -0,0 +1,11 @@
TODO
Implement mark's 'tick off the PPRs' in the old admin screen
Define schema for 'movements' table. We generate movement records as they happen so as not to reply on maths
Flow to create an arrival and maybe departure from a PPR. Perhaps we need a correlation column somewhere
Ability to add a position report to a strip
Improve journaling
+2 -2
View File
@@ -76,11 +76,11 @@
}
.aircraft-item.departure {
background-color: #ffffcc; /* light yellow */
background-color: #ccccff; /* light blue */
}
.aircraft-item.inbound {
background-color: #ccccff; /* light blue */
background-color: #ffffcc; /* light yellow */
}
.aircraft-item.overflight {
+224 -84
View File
@@ -102,11 +102,8 @@
font-family: inherit;
}
input[type="time"],
input[type="number"] {
padding: 12px 8px;
text-align: center;
min-width: 80px;
}
input:focus, select:focus, textarea:focus {
@@ -198,6 +195,65 @@
padding: 20px;
}
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: white;
border-radius: 8px;
padding: 30px;
max-width: 400px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.modal-icon {
font-size: 48px;
margin-bottom: 20px;
}
.modal h2 {
color: #27ae60;
margin-bottom: 15px;
font-size: 20px;
}
.modal p {
color: #333;
margin-bottom: 20px;
line-height: 1.5;
}
.modal-button {
background: #27ae60;
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s ease;
}
.modal-button:hover {
background: #229954;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
@@ -372,6 +428,15 @@
<div id="successMessage" class="success-message"></div>
<div id="errorMessage" class="error-message"></div>
<div id="successModal" class="modal-overlay">
<div class="modal">
<div class="modal-icon"></div>
<h2>Booking Submitted!</h2>
<p id="modalMessage"></p>
<button class="modal-button" onclick="closeSuccessModal()">Done</button>
</div>
</div>
<div class="info-box">
Booking out really helps our volunteers in the tower, thank you!
</div>
@@ -390,16 +455,22 @@
</div>
<form id="localFlightForm" onsubmit="handleSubmit(event, 'local')">
<div class="form-group">
<label for="localReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="localReg" name="registration" placeholder="e.g gbamc" required oninput="handleAircraftLookup(this.value, 'local')">
<div id="localReg-lookup-results" class="aircraft-lookup-results"></div>
<div class="form-row">
<div class="form-group">
<label for="localReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="localReg" name="registration" placeholder="e.g gbamc" required oninput="handleAircraftLookup(this.value, 'local')">
<div id="localReg-lookup-results" class="aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="localType">Aircraft Type</label>
<input type="text" id="localType" name="type" placeholder="e.g., Cessna 172">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="localType">Aircraft Type</label>
<input type="text" id="localType" name="type" placeholder="e.g., Cessna 172">
<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="localCallsign">Callsign</label>
@@ -407,18 +478,14 @@
</div>
</div>
<div class="form-row-3col">
<div class="form-row">
<div class="form-group">
<label for="localPOB">POB <span class="required">*</span></label>
<input type="number" id="localPOB" name="pob" value="1" min="1" required>
<label for="localETD">ETD</label>
<input type="text" id="localETD" name="etd" placeholder="HH:MM" pattern="([0-9]{2}:[0-9]{2}|[0-9]{4})" inputmode="decimal" oninput="formatTimeInput(this)">
</div>
<div class="form-group">
<label for="localDuration">Duration</label>
<input type="number" id="localDuration" name="duration" value="45" min="5">
</div>
<div class="form-group">
<label for="localETD">ETD</label>
<input type="time" id="localETD" name="etd">
<input type="number" id="localDuration" name="duration" value="45" min="5" inputmode="numeric">
</div>
</div>
@@ -439,16 +506,22 @@
</div>
<form id="circuitForm" onsubmit="handleSubmit(event, 'circuits')">
<div class="form-group">
<label for="circuitReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="circuitReg" name="registration" placeholder="e.g., G-BAMC" required oninput="handleAircraftLookup(this.value, 'circuits')">
<div id="circuitReg-lookup-results" class="aircraft-lookup-results"></div>
<div class="form-row">
<div class="form-group">
<label for="circuitReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="circuitReg" name="registration" placeholder="e.g., G-BAMC" required oninput="handleAircraftLookup(this.value, 'circuits')">
<div id="circuitReg-lookup-results" class="aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="circuitType">Aircraft Type</label>
<input type="text" id="circuitType" name="type" placeholder="e.g., Cessna 172">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="circuitType">Aircraft Type</label>
<input type="text" id="circuitType" name="type" placeholder="e.g., Cessna 172">
<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="circuitCallsign">Callsign</label>
@@ -456,18 +529,14 @@
</div>
</div>
<div class="form-row-3col">
<div class="form-row">
<div class="form-group">
<label for="circuitPOB">POB <span class="required">*</span></label>
<input type="number" id="circuitPOB" name="pob" value="1" min="1" required>
<label for="circuitETD">ETD</label>
<input type="text" id="circuitETD" name="etd" placeholder="HH:MM" pattern="([0-9]{2}:[0-9]{2}|[0-9]{4})" inputmode="decimal" oninput="formatTimeInput(this)">
</div>
<div class="form-group">
<label for="circuitDuration">Duration</label>
<input type="number" id="circuitDuration" name="duration" value="30" min="5">
</div>
<div class="form-group">
<label for="circuitETD">ETD</label>
<input type="time" id="circuitETD" name="etd">
<input type="number" id="circuitDuration" name="duration" value="30" min="5" inputmode="numeric">
</div>
</div>
@@ -488,16 +557,22 @@
</div>
<form id="departureForm" onsubmit="handleSubmit(event, 'departure')">
<div class="form-group">
<label for="depReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="depReg" name="registration" placeholder="e.g., N12345" required oninput="handleAircraftLookup(this.value, 'departure')">
<div id="depReg-lookup-results" class="aircraft-lookup-results"></div>
<div class="form-row">
<div class="form-group">
<label for="depReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="depReg" name="registration" placeholder="e.g., N12345" required oninput="handleAircraftLookup(this.value, 'departure')">
<div id="depReg-lookup-results" class="aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="depType">Aircraft Type</label>
<input type="text" id="depType" name="type" placeholder="e.g., Cessna 172">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="depType">Aircraft Type</label>
<input type="text" id="depType" name="type" placeholder="e.g., Cessna 172">
<label for="depPOB">Persons on Board <span class="required">*</span></label>
<input type="number" id="depPOB" name="pob" value="1" min="1" required>
</div>
<div class="form-group">
<label for="depCallsign">Callsign</label>
@@ -505,18 +580,14 @@
</div>
</div>
<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>
</div>
<div class="form-row">
<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>
<div class="form-group">
<label for="depETD">Takeoff Time</label>
<input type="time" id="depETD" name="etd">
<input type="text" id="depETD" name="etd" placeholder="HH:MM" pattern="([0-9]{2}:[0-9]{2}|[0-9]{4})" inputmode="decimal" oninput="formatTimeInput(this)">
</div>
</div>
<div id="depTo-lookup-results" class="airport-lookup-results"></div>
@@ -540,16 +611,22 @@
</div>
<form id="arrivalForm" onsubmit="handleSubmit(event, 'arrival')">
<div class="form-group">
<label for="arrReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="arrReg" name="registration" placeholder="e.g., N12345" required oninput="handleAircraftLookup(this.value, 'arrival')">
<div id="arrReg-lookup-results" class="aircraft-lookup-results"></div>
<div class="form-row">
<div class="form-group">
<label for="arrReg">Aircraft Registration <span class="required">*</span></label>
<input type="text" id="arrReg" name="registration" placeholder="e.g., N12345" required oninput="handleAircraftLookup(this.value, 'arrival')">
<div id="arrReg-lookup-results" class="aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="arrType">Aircraft Type</label>
<input type="text" id="arrType" name="type" placeholder="e.g., Cessna 172">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="arrType">Aircraft Type</label>
<input type="text" id="arrType" name="type" placeholder="e.g., Cessna 172">
<label for="arrPOB">Persons on Board <span class="required">*</span></label>
<input type="number" id="arrPOB" name="pob" value="1" min="1" required>
</div>
<div class="form-group">
<label for="arrCallsign">Callsign</label>
@@ -557,18 +634,14 @@
</div>
</div>
<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>
</div>
<div class="form-row">
<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>
<div class="form-group">
<label for="arrETA">Arrival Time</label>
<input type="time" id="arrETA" name="eta">
<input type="text" id="arrETA" name="eta" placeholder="HH:MM" pattern="([0-9]{2}:[0-9]{2}|[0-9]{4})" inputmode="decimal" oninput="formatTimeInput(this)">
</div>
</div>
<div id="arrFrom-lookup-results" class="airport-lookup-results"></div>
@@ -608,6 +681,7 @@
const API_BASE = window.location.origin + '/api/v1';
const STORAGE_RECENT_REGS_KEY = 'bookingPage_recentRegs';
const STORAGE_AIRCRAFT_TYPES_KEY = 'bookingPage_aircraftTypes';
const STORAGE_CALLSIGNS_KEY = 'bookingPage_callsigns';
const MAX_RECENT = 5;
// ==================== localStorage Management ====================
@@ -690,6 +764,43 @@
}
}
function cacheCallsign(registration, callsign) {
try {
if (!registration || !callsign || callsign.trim() === '') {
return;
}
const reg = registration.toUpperCase().trim();
const cs = callsign.toUpperCase().trim();
const stored = localStorage.getItem(STORAGE_CALLSIGNS_KEY);
let callsigns = stored ? JSON.parse(stored) : {};
callsigns[reg] = cs;
localStorage.setItem(STORAGE_CALLSIGNS_KEY, JSON.stringify(callsigns));
console.log('✅ Cached callsign:', reg, '=', cs);
} catch (e) {
console.error('Error caching callsign:', e);
}
}
function getCachedCallsign(registration) {
try {
const reg = registration.toUpperCase().trim();
const stored = localStorage.getItem(STORAGE_CALLSIGNS_KEY);
if (!stored) {
return null;
}
const callsigns = JSON.parse(stored);
const callsign = callsigns[reg] || null;
console.log('🔍 Retrieved cached callsign for', reg, '=', callsign);
return callsign;
} catch (e) {
console.error('Error retrieving cached callsign:', e);
return null;
}
}
function applyRecentReg(registration, formType) {
const registerIdMap = {
'local': 'localReg',
@@ -705,8 +816,16 @@
'arrival': 'arrType'
};
const callsignIdMap = {
'local': 'localCallsign',
'circuits': 'circuitCallsign',
'departure': 'depCallsign',
'arrival': 'arrCallsign'
};
const regId = registerIdMap[formType];
const typeId = typeIdMap[formType];
const callsignId = callsignIdMap[formType];
console.log('📍 applyRecentReg called - reg:', registration, 'form:', formType);
document.getElementById(regId).value = registration;
@@ -720,6 +839,13 @@
console.log('️ No cached type found');
}
// Restore cached callsign if available
const cachedCallsign = getCachedCallsign(registration);
if (cachedCallsign) {
console.log('✔️ Applying cached callsign:', cachedCallsign);
document.getElementById(callsignId).value = cachedCallsign;
}
document.getElementById(regId).focus();
// Trigger the lookup
handleAircraftLookup(registration, formType);
@@ -780,24 +906,24 @@
}
function showMessage(message, isError = false) {
const successEl = document.getElementById('successMessage');
const errorEl = document.getElementById('errorMessage');
if (isError) {
const errorEl = document.getElementById('errorMessage');
errorEl.textContent = message;
errorEl.style.display = 'block';
successEl.style.display = 'none';
// Auto-hide error after 5 seconds
setTimeout(() => {
errorEl.style.display = 'none';
}, 5000);
} else {
successEl.textContent = message;
successEl.style.display = 'block';
errorEl.style.display = 'none';
// Show success modal instead
const modalMessage = document.getElementById('modalMessage');
modalMessage.textContent = message;
document.getElementById('successModal').classList.add('active');
}
}
// Auto-hide after 5 seconds
setTimeout(() => {
successEl.style.display = 'none';
errorEl.style.display = 'none';
}, 5000);
function closeSuccessModal() {
document.getElementById('successModal').classList.remove('active');
}
async function handleSubmit(event, formType) {
@@ -816,6 +942,11 @@
console.log('💾 Caching type on submit - reg:', data.registration, 'type:', data.type);
cacheAircraftType(data.registration, data.type);
}
// Cache the callsign if provided
if (data.callsign) {
cacheCallsign(data.registration, data.callsign);
}
}
// Set flight_type based on form type
@@ -830,8 +961,12 @@
const timeFields = ['etd', 'circuit_timestamp', 'eta'];
timeFields.forEach(field => {
if (data[field]) {
// data[field] is in HH:MM format (time input)
const datetime = new Date(`${today}T${data[field]}:00`);
let timeValue = data[field];
// Normalize 4-digit format (HHMM) to HH:MM
if (/^[0-9]{4}$/.test(timeValue)) {
timeValue = timeValue.slice(0, 2) + ':' + timeValue.slice(2);
}
const datetime = new Date(`${today}T${timeValue}:00`);
data[field] = datetime.toISOString();
}
});
@@ -869,7 +1004,7 @@
}
const result = await response.json();
showMessage(`✅ Success! Your booking has been submitted. Booking ID: ${result.id}`);
showMessage(`Thanks - Your booking has been submitted. Have a great flight!`);
form.reset();
} catch (error) {
console.error('Error:', error);
@@ -880,27 +1015,32 @@
}
// Set current time as default for time inputs
function formatTimeInput(input) {
let value = input.value.replace(/[^0-9]/g, '');
// Auto-format: if user types 1430, it becomes 14:30
if (value.length === 4) {
input.value = value.slice(0, 2) + ':' + value.slice(2);
} else if (value.length <= 2) {
input.value = value;
} else if (value.length === 3) {
input.value = value.slice(0, 2) + ':' + value.slice(2);
}
}
function setDefaultTimes() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
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 futureTime = new Date(now.getTime() + 10 * 60000); // 10 minutes from now
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'];
const etdFieldIds = ['localETD', 'circuitETD', 'depETD', 'arrETA'];
document.querySelectorAll('input[type="time"]').forEach(input => {
if (!input.value) {
if (etdFieldIds.includes(input.id)) {
input.value = futureTimeValue;
} else {
input.value = timeValue;
}
etdFieldIds.forEach(id => {
const input = document.getElementById(id);
if (input && !input.value) {
input.value = futureTimeValue;
}
});
}