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 { .aircraft-item.departure {
background-color: #ffffcc; /* light yellow */ background-color: #ccccff; /* light blue */
} }
.aircraft-item.inbound { .aircraft-item.inbound {
background-color: #ccccff; /* light blue */ background-color: #ffffcc; /* light yellow */
} }
.aircraft-item.overflight { .aircraft-item.overflight {
+224 -84
View File
@@ -102,11 +102,8 @@
font-family: inherit; font-family: inherit;
} }
input[type="time"],
input[type="number"] { input[type="number"] {
padding: 12px 8px;
text-align: center; text-align: center;
min-width: 80px;
} }
input:focus, select:focus, textarea:focus { input:focus, select:focus, textarea:focus {
@@ -198,6 +195,65 @@
padding: 20px; 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 { .spinner {
border: 4px solid #f3f3f3; border: 4px solid #f3f3f3;
border-top: 4px solid #3498db; border-top: 4px solid #3498db;
@@ -372,6 +428,15 @@
<div id="successMessage" class="success-message"></div> <div id="successMessage" class="success-message"></div>
<div id="errorMessage" class="error-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"> <div class="info-box">
Booking out really helps our volunteers in the tower, thank you! Booking out really helps our volunteers in the tower, thank you!
</div> </div>
@@ -390,16 +455,22 @@
</div> </div>
<form id="localFlightForm" onsubmit="handleSubmit(event, 'local')"> <form id="localFlightForm" onsubmit="handleSubmit(event, 'local')">
<div class="form-group"> <div class="form-row">
<label for="localReg">Aircraft Registration <span class="required">*</span></label> <div class="form-group">
<input type="text" id="localReg" name="registration" placeholder="e.g gbamc" required oninput="handleAircraftLookup(this.value, 'local')"> <label for="localReg">Aircraft Registration <span class="required">*</span></label>
<div id="localReg-lookup-results" class="aircraft-lookup-results"></div> <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>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="localType">Aircraft Type</label> <label for="localPOB">POB <span class="required">*</span></label>
<input type="text" id="localType" name="type" placeholder="e.g., Cessna 172"> <input type="number" id="localPOB" name="pob" value="1" min="1" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="localCallsign">Callsign</label> <label for="localCallsign">Callsign</label>
@@ -407,18 +478,14 @@
</div> </div>
</div> </div>
<div class="form-row-3col"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="localPOB">POB <span class="required">*</span></label> <label for="localETD">ETD</label>
<input type="number" id="localPOB" name="pob" value="1" min="1" required> <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>
<div class="form-group"> <div class="form-group">
<label for="localDuration">Duration</label> <label for="localDuration">Duration</label>
<input type="number" id="localDuration" name="duration" value="45" min="5"> <input type="number" id="localDuration" name="duration" value="45" min="5" inputmode="numeric">
</div>
<div class="form-group">
<label for="localETD">ETD</label>
<input type="time" id="localETD" name="etd">
</div> </div>
</div> </div>
@@ -439,16 +506,22 @@
</div> </div>
<form id="circuitForm" onsubmit="handleSubmit(event, 'circuits')"> <form id="circuitForm" onsubmit="handleSubmit(event, 'circuits')">
<div class="form-group"> <div class="form-row">
<label for="circuitReg">Aircraft Registration <span class="required">*</span></label> <div class="form-group">
<input type="text" id="circuitReg" name="registration" placeholder="e.g., G-BAMC" required oninput="handleAircraftLookup(this.value, 'circuits')"> <label for="circuitReg">Aircraft Registration <span class="required">*</span></label>
<div id="circuitReg-lookup-results" class="aircraft-lookup-results"></div> <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>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="circuitType">Aircraft Type</label> <label for="circuitPOB">POB <span class="required">*</span></label>
<input type="text" id="circuitType" name="type" placeholder="e.g., Cessna 172"> <input type="number" id="circuitPOB" name="pob" value="1" min="1" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="circuitCallsign">Callsign</label> <label for="circuitCallsign">Callsign</label>
@@ -456,18 +529,14 @@
</div> </div>
</div> </div>
<div class="form-row-3col"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="circuitPOB">POB <span class="required">*</span></label> <label for="circuitETD">ETD</label>
<input type="number" id="circuitPOB" name="pob" value="1" min="1" required> <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>
<div class="form-group"> <div class="form-group">
<label for="circuitDuration">Duration</label> <label for="circuitDuration">Duration</label>
<input type="number" id="circuitDuration" name="duration" value="30" min="5"> <input type="number" id="circuitDuration" name="duration" value="30" min="5" inputmode="numeric">
</div>
<div class="form-group">
<label for="circuitETD">ETD</label>
<input type="time" id="circuitETD" name="etd">
</div> </div>
</div> </div>
@@ -488,16 +557,22 @@
</div> </div>
<form id="departureForm" onsubmit="handleSubmit(event, 'departure')"> <form id="departureForm" onsubmit="handleSubmit(event, 'departure')">
<div class="form-group"> <div class="form-row">
<label for="depReg">Aircraft Registration <span class="required">*</span></label> <div class="form-group">
<input type="text" id="depReg" name="registration" placeholder="e.g., N12345" required oninput="handleAircraftLookup(this.value, 'departure')"> <label for="depReg">Aircraft Registration <span class="required">*</span></label>
<div id="depReg-lookup-results" class="aircraft-lookup-results"></div> <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>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="depType">Aircraft Type</label> <label for="depPOB">Persons on Board <span class="required">*</span></label>
<input type="text" id="depType" name="type" placeholder="e.g., Cessna 172"> <input type="number" id="depPOB" name="pob" value="1" min="1" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="depCallsign">Callsign</label> <label for="depCallsign">Callsign</label>
@@ -505,18 +580,14 @@
</div> </div>
</div> </div>
<div class="form-row-3col"> <div class="form-row">
<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-group"> <div class="form-group">
<label for="depTo">Destination (ICAO) <span class="required">*</span></label> <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')"> <input type="text" id="depTo" name="out_to" placeholder="e.g., KJFK" required oninput="handleAirportLookup(this.value, 'depTo')">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="depETD">Takeoff Time</label> <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> </div>
<div id="depTo-lookup-results" class="airport-lookup-results"></div> <div id="depTo-lookup-results" class="airport-lookup-results"></div>
@@ -540,16 +611,22 @@
</div> </div>
<form id="arrivalForm" onsubmit="handleSubmit(event, 'arrival')"> <form id="arrivalForm" onsubmit="handleSubmit(event, 'arrival')">
<div class="form-group"> <div class="form-row">
<label for="arrReg">Aircraft Registration <span class="required">*</span></label> <div class="form-group">
<input type="text" id="arrReg" name="registration" placeholder="e.g., N12345" required oninput="handleAircraftLookup(this.value, 'arrival')"> <label for="arrReg">Aircraft Registration <span class="required">*</span></label>
<div id="arrReg-lookup-results" class="aircraft-lookup-results"></div> <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>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="arrType">Aircraft Type</label> <label for="arrPOB">Persons on Board <span class="required">*</span></label>
<input type="text" id="arrType" name="type" placeholder="e.g., Cessna 172"> <input type="number" id="arrPOB" name="pob" value="1" min="1" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="arrCallsign">Callsign</label> <label for="arrCallsign">Callsign</label>
@@ -557,18 +634,14 @@
</div> </div>
</div> </div>
<div class="form-row-3col"> <div class="form-row">
<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-group"> <div class="form-group">
<label for="arrFrom">Origin (ICAO) <span class="required">*</span></label> <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')"> <input type="text" id="arrFrom" name="in_from" placeholder="e.g., KJFK" required oninput="handleAirportLookup(this.value, 'arrFrom')">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="arrETA">Arrival Time</label> <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> </div>
<div id="arrFrom-lookup-results" class="airport-lookup-results"></div> <div id="arrFrom-lookup-results" class="airport-lookup-results"></div>
@@ -608,6 +681,7 @@
const API_BASE = window.location.origin + '/api/v1'; const API_BASE = window.location.origin + '/api/v1';
const STORAGE_RECENT_REGS_KEY = 'bookingPage_recentRegs'; const STORAGE_RECENT_REGS_KEY = 'bookingPage_recentRegs';
const STORAGE_AIRCRAFT_TYPES_KEY = 'bookingPage_aircraftTypes'; const STORAGE_AIRCRAFT_TYPES_KEY = 'bookingPage_aircraftTypes';
const STORAGE_CALLSIGNS_KEY = 'bookingPage_callsigns';
const MAX_RECENT = 5; const MAX_RECENT = 5;
// ==================== localStorage Management ==================== // ==================== 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) { function applyRecentReg(registration, formType) {
const registerIdMap = { const registerIdMap = {
'local': 'localReg', 'local': 'localReg',
@@ -705,8 +816,16 @@
'arrival': 'arrType' 'arrival': 'arrType'
}; };
const callsignIdMap = {
'local': 'localCallsign',
'circuits': 'circuitCallsign',
'departure': 'depCallsign',
'arrival': 'arrCallsign'
};
const regId = registerIdMap[formType]; const regId = registerIdMap[formType];
const typeId = typeIdMap[formType]; const typeId = typeIdMap[formType];
const callsignId = callsignIdMap[formType];
console.log('📍 applyRecentReg called - reg:', registration, 'form:', formType); console.log('📍 applyRecentReg called - reg:', registration, 'form:', formType);
document.getElementById(regId).value = registration; document.getElementById(regId).value = registration;
@@ -720,6 +839,13 @@
console.log('️ No cached type found'); 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(); document.getElementById(regId).focus();
// Trigger the lookup // Trigger the lookup
handleAircraftLookup(registration, formType); handleAircraftLookup(registration, formType);
@@ -780,24 +906,24 @@
} }
function showMessage(message, isError = false) { function showMessage(message, isError = false) {
const successEl = document.getElementById('successMessage');
const errorEl = document.getElementById('errorMessage');
if (isError) { if (isError) {
const errorEl = document.getElementById('errorMessage');
errorEl.textContent = message; errorEl.textContent = message;
errorEl.style.display = 'block'; errorEl.style.display = 'block';
successEl.style.display = 'none'; // Auto-hide error after 5 seconds
setTimeout(() => {
errorEl.style.display = 'none';
}, 5000);
} else { } else {
successEl.textContent = message; // Show success modal instead
successEl.style.display = 'block'; const modalMessage = document.getElementById('modalMessage');
errorEl.style.display = 'none'; modalMessage.textContent = message;
document.getElementById('successModal').classList.add('active');
} }
}
// Auto-hide after 5 seconds function closeSuccessModal() {
setTimeout(() => { document.getElementById('successModal').classList.remove('active');
successEl.style.display = 'none';
errorEl.style.display = 'none';
}, 5000);
} }
async function handleSubmit(event, formType) { async function handleSubmit(event, formType) {
@@ -816,6 +942,11 @@
console.log('💾 Caching type on submit - reg:', data.registration, 'type:', data.type); console.log('💾 Caching type on submit - reg:', data.registration, 'type:', data.type);
cacheAircraftType(data.registration, 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 // Set flight_type based on form type
@@ -830,8 +961,12 @@
const timeFields = ['etd', 'circuit_timestamp', 'eta']; const timeFields = ['etd', 'circuit_timestamp', 'eta'];
timeFields.forEach(field => { timeFields.forEach(field => {
if (data[field]) { if (data[field]) {
// data[field] is in HH:MM format (time input) let timeValue = data[field];
const datetime = new Date(`${today}T${data[field]}:00`); // 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(); data[field] = datetime.toISOString();
} }
}); });
@@ -869,7 +1004,7 @@
} }
const result = await response.json(); 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(); form.reset();
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@@ -880,27 +1015,32 @@
} }
// Set current time as default for time inputs // 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() { function setDefaultTimes() {
const now = new Date(); const now = new Date();
const hours = String(now.getHours()).padStart(2, '0'); const futureTime = new Date(now.getTime() + 10 * 60000); // 10 minutes from now
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 futureHours = String(futureTime.getHours()).padStart(2, '0');
const futureMinutes = String(futureTime.getMinutes()).padStart(2, '0'); const futureMinutes = String(futureTime.getMinutes()).padStart(2, '0');
const futureTimeValue = `${futureHours}:${futureMinutes}`; const futureTimeValue = `${futureHours}:${futureMinutes}`;
const etdFieldIds = ['localETD', 'circuitETD', 'depETD']; const etdFieldIds = ['localETD', 'circuitETD', 'depETD', 'arrETA'];
document.querySelectorAll('input[type="time"]').forEach(input => { etdFieldIds.forEach(id => {
if (!input.value) { const input = document.getElementById(id);
if (etdFieldIds.includes(input.id)) { if (input && !input.value) {
input.value = futureTimeValue; input.value = futureTimeValue;
} else {
input.value = timeValue;
}
} }
}); });
} }