From 2dce14507b85774a510b44569b57c211eecabf36 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Wed, 25 Mar 2026 17:25:26 -0400 Subject: [PATCH] Booking form fixes --- TODO | 11 ++ web/atc.html | 4 +- web/book.html | 308 ++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 237 insertions(+), 86 deletions(-) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..afe56e9 --- /dev/null +++ b/TODO @@ -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 diff --git a/web/atc.html b/web/atc.html index 4043107..ba3dd76 100644 --- a/web/atc.html +++ b/web/atc.html @@ -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 { diff --git a/web/book.html b/web/book.html index dbd448b..b30972f 100644 --- a/web/book.html +++ b/web/book.html @@ -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 @@
+ +
Booking out really helps our volunteers in the tower, thank you!
@@ -390,16 +455,22 @@
-
- - -
+
+
+ + +
+
+
+ + +
- - + +
@@ -407,18 +478,14 @@
-
+
- - + +
- -
-
- - +
@@ -439,16 +506,22 @@
-
- - -
+
+
+ + +
+
+
+ + +
- - + +
@@ -456,18 +529,14 @@
-
+
- - + +
- -
-
- - +
@@ -488,16 +557,22 @@
-
- - -
+
+
+ + +
+
+
+ + +
- - + +
@@ -505,18 +580,14 @@
-
-
- - -
+
- +
@@ -540,16 +611,22 @@
-
- - -
+
+
+ + +
+
+
+ + +
- - + +
@@ -557,18 +634,14 @@
-
-
- - -
+
- +
@@ -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; } }); }