let currentUser = null; let accessToken = null; let currentPPRId = null; let isNewPPR = false; let wsConnection = null; let pendingStatusUpdate = null; // Track pending status update for timestamp modal let wsHeartbeatInterval = null; let wsReconnectTimeout = null; let lastHeartbeatResponse = null; let sessionExpiryWarningShown = false; let sessionExpiryCheckInterval = null; let etdManuallyEdited = false; // Track if user has manually edited ETD let loadPPRsTimeout = null; // Debounce timer for loadPPRs to prevent duplicate refreshes // Modal state variables let currentLocalFlightId = null; let currentBookedInArrivalId = null; let currentDepartureId = null; let currentArrivalId = null; let currentOverflightId = null; let isOverflightQSYMode = false; // Track if we're in overflight QSY mode // User management variables let currentUserRole = null; let isNewUser = false; let currentUserId = null; let currentChangePasswordUserId = null; // ==================== GENERIC MODAL HELPER ==================== function closeModal(modalId, additionalCleanup = null) { document.getElementById(modalId).style.display = 'none'; if (additionalCleanup) { additionalCleanup(); } } // Load UI configuration from API async function loadUIConfig() { try { const response = await fetch('/api/v1/public/config'); if (response.ok) { const config = await response.json(); // Update tower title const titleElement = document.getElementById('tower-title'); if (titleElement && config.tag) { titleElement.innerHTML = `✈️ Tower Ops ${config.tag}`; } // Update top bar gradient const topBar = document.querySelector('.top-bar'); if (topBar && config.top_bar_gradient_start && config.top_bar_gradient_end) { topBar.style.background = `linear-gradient(135deg, ${config.top_bar_gradient_start}, ${config.top_bar_gradient_end})`; } // Update footer color const footerBar = document.querySelector('.footer-bar'); if (footerBar && config.footer_color) { footerBar.style.background = config.footer_color; } // Optionally indicate environment (e.g., add to title if not production) if (config.environment && config.environment !== 'production') { const envIndicator = ` (${config.environment.toUpperCase()})`; if (titleElement) { titleElement.innerHTML += envIndicator; } } } } catch (error) { console.warn('Failed to load UI config:', error); } } // WebSocket connection for real-time updates function connectWebSocket() { if (wsConnection && wsConnection.readyState === WebSocket.OPEN) { return; // Already connected } // Clear any existing reconnect timeout if (wsReconnectTimeout) { clearTimeout(wsReconnectTimeout); wsReconnectTimeout = null; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/tower-updates`; wsConnection = new WebSocket(wsUrl); wsConnection.onopen = function(event) { lastHeartbeatResponse = Date.now(); startHeartbeat(); showNotification('Real-time updates connected'); }; wsConnection.onmessage = function(event) { try { // Check if it's a heartbeat response if (event.data.startsWith('Heartbeat:')) { lastHeartbeatResponse = Date.now(); return; } const data = JSON.parse(event.data); // Refresh PPRs when any PPR-related event occurs if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) { loadPPRs(); showNotification('Data updated'); } // Refresh local flights when any local flight event occurs if (data.type && (data.type.includes('local_flight_'))) { loadPPRs(); showNotification('Local flight updated'); } // Refresh departures when any departure event occurs if (data.type && (data.type.includes('departure_'))) { loadDepartures(); showNotification('Departure updated'); } // Refresh arrivals when any arrival event occurs if (data.type && (data.type.includes('arrival_'))) { loadArrivals(); showNotification('Arrival updated'); } } catch (error) { console.error('Error parsing WebSocket message:', error); } }; wsConnection.onclose = function(event) { stopHeartbeat(); // Attempt to reconnect after 5 seconds if still logged in if (accessToken) { showNotification('Real-time updates disconnected, reconnecting...', true); wsReconnectTimeout = setTimeout(() => { connectWebSocket(); }, 5000); } }; wsConnection.onerror = function(error) { console.error('❌ WebSocket error:', error); }; } function startHeartbeat() { // Clear any existing heartbeat stopHeartbeat(); // Send ping every 30 seconds wsHeartbeatInterval = setInterval(() => { if (wsConnection && wsConnection.readyState === WebSocket.OPEN) { wsConnection.send('ping'); // Check if last heartbeat was more than 60 seconds ago if (lastHeartbeatResponse && (Date.now() - lastHeartbeatResponse > 60000)) { console.warn('⚠️ No heartbeat response for 60 seconds, reconnecting...'); wsConnection.close(); } } else { console.warn('⚠️ WebSocket not open, stopping heartbeat'); stopHeartbeat(); } }, 30000); } function stopHeartbeat() { if (wsHeartbeatInterval) { clearInterval(wsHeartbeatInterval); wsHeartbeatInterval = null; } } function disconnectWebSocket() { stopHeartbeat(); if (wsReconnectTimeout) { clearTimeout(wsReconnectTimeout); wsReconnectTimeout = null; } if (wsConnection) { wsConnection.close(); wsConnection = null; } } // Notification system function showNotification(message, isError = false) { const notification = document.getElementById('notification'); notification.textContent = message; notification.className = 'notification' + (isError ? ' error' : ''); // Show notification setTimeout(() => { notification.classList.add('show'); }, 10); // Hide after 3 seconds setTimeout(() => { notification.classList.remove('show'); }, 3000); } // Initialize time dropdowns function initializeTimeDropdowns() { const timeSelects = ['eta-time', 'etd-time']; timeSelects.forEach(selectId => { const select = document.getElementById(selectId); // Clear existing options except the first one select.innerHTML = ''; // Add time options in 15-minute intervals for (let hour = 0; hour < 24; hour++) { for (let minute = 0; minute < 60; minute += 15) { const timeString = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; const option = document.createElement('option'); option.value = timeString; option.textContent = timeString; select.appendChild(option); } } }); } // Session expiry monitoring function startSessionExpiryCheck() { // Clear any existing interval if (sessionExpiryCheckInterval) { clearInterval(sessionExpiryCheckInterval); } sessionExpiryWarningShown = false; // Check every 60 seconds sessionExpiryCheckInterval = setInterval(() => { const tokenExpiry = localStorage.getItem('ppr_token_expiry'); if (!tokenExpiry || !accessToken) { stopSessionExpiryCheck(); return; } const now = Date.now(); const expiryTime = parseInt(tokenExpiry); const timeUntilExpiry = expiryTime - now; // Warn if less than 5 minutes remaining if (timeUntilExpiry < 5 * 60 * 1000 && !sessionExpiryWarningShown) { sessionExpiryWarningShown = true; const minutesLeft = Math.ceil(timeUntilExpiry / 60000); showNotification(`⚠️ Session expires in ${minutesLeft} minute${minutesLeft !== 1 ? 's' : ''}. Please save your work.`, true); console.warn(`Session expires in ${minutesLeft} minutes`); } // Force logout if expired if (timeUntilExpiry <= 0) { console.error('Session has expired'); showNotification('Session expired. Please log in again.', true); logout(); } }, 60000); // Check every minute } function stopSessionExpiryCheck() { if (sessionExpiryCheckInterval) { clearInterval(sessionExpiryCheckInterval); sessionExpiryCheckInterval = null; } sessionExpiryWarningShown = false; } // Authentication management async function initializeAuth() { // Try to get cached token const cachedToken = localStorage.getItem('ppr_access_token'); const cachedUser = localStorage.getItem('ppr_username'); const tokenExpiry = localStorage.getItem('ppr_token_expiry'); if (cachedToken && cachedUser && tokenExpiry) { const now = new Date().getTime(); if (now < parseInt(tokenExpiry)) { // Token is still valid accessToken = cachedToken; currentUser = cachedUser; document.getElementById('current-user').textContent = cachedUser; await updateUserRole(); // Update role-based UI startSessionExpiryCheck(); // Start monitoring session expiry connectWebSocket(); // Connect WebSocket for real-time updates loadPPRs(); return; } } // No valid cached token, show login showLogin(); } function setupLoginForm() { document.getElementById('login-form').addEventListener('submit', async function(e) { e.preventDefault(); await handleLogin(); }); } function setupKeyboardShortcuts() { document.addEventListener('keydown', function(e) { // Press 'Escape' to close PPR modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('pprModal').style.display === 'block') { e.preventDefault(); closePPRModal(); return; } // Press 'Escape' to close timestamp modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('timestampModal').style.display === 'block') { e.preventDefault(); closeTimestampModal(); return; } // Press 'Escape' to close circuit modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('circuitModal').style.display === 'block') { e.preventDefault(); closeCircuitModal(); return; } // Press 'Escape' to close Book Out modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('localFlightModal').style.display === 'block') { e.preventDefault(); closeModal('localFlightModal'); 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(); closeModal('bookInModal'); return; } // Press 'Escape' to close Overflight modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('overflightModal').style.display === 'block') { e.preventDefault(); closeModal('overflightModal'); return; } // Press 'Escape' to close local flight edit modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('localFlightEditModal').style.display === 'block') { e.preventDefault(); closeLocalFlightEditModal(); return; } // Press 'Escape' to close departure edit modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('departureEditModal').style.display === 'block') { e.preventDefault(); closeDepartureEditModal(); return; } // Press 'Escape' to close arrival edit modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('arrivalEditModal').style.display === 'block') { e.preventDefault(); closeArrivalEditModal(); return; } // Press 'Escape' to close overflight edit modal if it's open (allow even when typing in inputs) if (e.key === 'Escape' && document.getElementById('overflightEditModal').style.display === 'block') { e.preventDefault(); closeModal('overflightEditModal'); return; } // Only trigger other shortcuts when not typing in input fields if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') { return; } // Press 'n' to open new PPR modal if (e.key === 'n' || e.key === 'N') { e.preventDefault(); openNewPPRModal(); } // Press 'g' to book out local flight starting with G if (e.key === 'g' || e.key === 'G') { e.preventDefault(); openLocalFlightModal('LOCAL', 'G'); } // Press 'l' to book out local flight (LOCAL type) if (e.key === 'l' || e.key === 'L') { e.preventDefault(); openLocalFlightModal('LOCAL'); } // Press 'o' to open overflight modal if (e.key === 'o' || e.key === 'O') { e.preventDefault(); openOverflightModal(); } // Press 'c' to book out circuits if (e.key === 'c' || e.key === 'C') { e.preventDefault(); openLocalFlightModal('CIRCUITS'); } // Press 'd' to book out departure if (e.key === 'd' || e.key === 'D') { e.preventDefault(); openLocalFlightModal('DEPARTURE'); } // Press 'i' to book in arrival if (e.key === 'i' || e.key === 'I') { e.preventDefault(); openBookInModal(); } }); } // Dropdown menu handlers document.addEventListener('click', function(e) { const actionsBtn = document.getElementById('actionsDropdownBtn'); const actionsMenu = document.getElementById('actionsDropdownMenu'); const adminBtn = document.getElementById('adminDropdownBtn'); const adminMenu = document.getElementById('adminDropdownMenu'); // Handle Actions dropdown if (e.target === actionsBtn) { e.preventDefault(); actionsMenu.classList.toggle('active'); adminMenu.classList.remove('active'); } else if (!actionsMenu.contains(e.target)) { actionsMenu.classList.remove('active'); } // Handle Admin dropdown if (e.target === adminBtn) { e.preventDefault(); adminMenu.classList.toggle('active'); actionsMenu.classList.remove('active'); } else if (!adminMenu.contains(e.target)) { adminMenu.classList.remove('active'); } }); function closeActionsDropdown() { document.getElementById('actionsDropdownMenu').classList.remove('active'); } function closeAdminDropdown() { document.getElementById('adminDropdownMenu').classList.remove('active'); } function showLogin() { document.getElementById('loginModal').style.display = 'block'; document.getElementById('login-username').focus(); } function hideLogin() { document.getElementById('loginModal').style.display = 'none'; document.getElementById('login-error').style.display = 'none'; document.getElementById('login-form').reset(); } async function handleLogin() { const username = document.getElementById('login-username').value; const password = document.getElementById('login-password').value; const loginBtn = document.getElementById('login-btn'); const errorDiv = document.getElementById('login-error'); // Show loading state loginBtn.disabled = true; loginBtn.textContent = '🔄 Logging in...'; errorDiv.style.display = 'none'; try { const response = await fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}` }); const data = await response.json(); if (response.ok && data.access_token) { // Store token and user info with expiry from server response const expiresInMs = (data.expires_in || 1800) * 1000; // Use server value or default to 30 min const expiryTime = new Date().getTime() + expiresInMs; localStorage.setItem('ppr_access_token', data.access_token); localStorage.setItem('ppr_username', username); localStorage.setItem('ppr_token_expiry', expiryTime.toString()); accessToken = data.access_token; currentUser = username; document.getElementById('current-user').textContent = username; hideLogin(); await updateUserRole(); // Update role-based UI startSessionExpiryCheck(); // Start monitoring session expiry connectWebSocket(); // Connect WebSocket for real-time updates loadPPRs(); } else { throw new Error(data.detail || 'Authentication failed'); } } catch (error) { console.error('Login error:', error); errorDiv.textContent = error.message || 'Login failed. Please check your credentials.'; errorDiv.style.display = 'block'; } finally { // Reset button state loginBtn.disabled = false; loginBtn.textContent = '🔐 Login'; } } function logout() { // Clear stored token and user info localStorage.removeItem('ppr_access_token'); localStorage.removeItem('ppr_username'); localStorage.removeItem('ppr_token_expiry'); accessToken = null; currentUser = null; stopSessionExpiryCheck(); // Stop monitoring session disconnectWebSocket(); // Disconnect WebSocket // Close any open modals closePPRModal(); closeTimestampModal(); // Show login again showLogin(); } // Legacy authenticate function - now redirects to new login function authenticate() { showLogin(); } // Enhanced fetch wrapper with token expiry handling async function authenticatedFetch(url, options = {}) { if (!accessToken) { showLogin(); throw new Error('No access token available'); } // Add authorization header const headers = { ...options.headers, 'Authorization': `Bearer ${accessToken}` }; const response = await fetch(url, { ...options, headers }); // Handle 401 - token expired if (response.status === 401) { logout(); throw new Error('Session expired. Please log in again.'); } return response; } // Load PPR records - now loads all tables function formatTimeOnly(dateStr) { if (!dateStr) return '-'; // Ensure the datetime string is treated as UTC let utcDateStr = dateStr; if (!utcDateStr.includes('T')) { utcDateStr = utcDateStr.replace(' ', 'T'); } if (!utcDateStr.includes('Z')) { utcDateStr += 'Z'; } const date = new Date(utcDateStr); return date.toISOString().slice(11, 16); } function formatDateTime(dateStr) { if (!dateStr) return '-'; // Ensure the datetime string is treated as UTC let utcDateStr = dateStr; if (!utcDateStr.includes('T')) { utcDateStr = utcDateStr.replace(' ', 'T'); } if (!utcDateStr.includes('Z')) { utcDateStr += 'Z'; } const date = new Date(utcDateStr); return date.toISOString().slice(0, 10) + ' ' + date.toISOString().slice(11, 16); } // Modal functions function openNewPPRModal() { isNewPPR = true; currentPPRId = null; etdManuallyEdited = false; // Reset the manual edit flag for new PPR document.getElementById('modal-title').textContent = 'New PPR'; document.getElementById('journal-section').style.display = 'none'; document.querySelector('.quick-actions').style.display = 'none'; // Clear form document.getElementById('ppr-form').reset(); document.getElementById('ppr-id').value = ''; // Clear the unsaved aircraft flag document.getElementById('ppr-form').removeAttribute('data-unsaved-aircraft'); // Set default ETA and ETD const now = new Date(); const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour const etd = new Date(now.getTime() + 2 * 60 * 60 * 1000); // +2 hours // Format date and time for separate inputs function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function formatTime(date) { const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(Math.ceil(date.getMinutes() / 15) * 15 % 60).padStart(2, '0'); // Round up to next 15-minute interval return `${hours}:${minutes}`; } document.getElementById('eta-date').value = formatDate(eta); document.getElementById('eta-time').value = formatTime(eta); document.getElementById('etd-date').value = formatDate(etd); document.getElementById('etd-time').value = formatTime(etd); // Clear aircraft lookup results clearAircraftLookup(); clearArrivalAirportLookup(); clearDepartureAirportLookup(); document.getElementById('pprModal').style.display = 'block'; // Auto-focus on aircraft registration field setTimeout(() => { document.getElementById('ac_reg').focus(); }, 100); } // Function to update ETD based on ETA (2 hours later) function updateETDFromETA() { // Only auto-update if user hasn't manually edited ETD if (etdManuallyEdited) { return; } const etaDate = document.getElementById('eta-date').value; const etaTime = document.getElementById('eta-time').value; if (etaDate && etaTime) { // Parse ETA const eta = new Date(`${etaDate}T${etaTime}`); // Calculate ETD (2 hours after ETA) const etd = new Date(eta.getTime() + 2 * 60 * 60 * 1000); // Format ETD const etdDateStr = `${etd.getFullYear()}-${String(etd.getMonth() + 1).padStart(2, '0')}-${String(etd.getDate()).padStart(2, '0')}`; const etdTimeStr = `${String(etd.getHours()).padStart(2, '0')}:${String(etd.getMinutes()).padStart(2, '0')}`; // Update ETD fields document.getElementById('etd-date').value = etdDateStr; document.getElementById('etd-time').value = etdTimeStr; } } // Function to mark ETD as manually edited function markETDAsManuallyEdited() { etdManuallyEdited = true; } async function openPPRModal(pprId) { if (!accessToken) return; isNewPPR = false; currentPPRId = pprId; document.getElementById('modal-title').textContent = 'Edit PPR Entry'; document.querySelector('.quick-actions').style.display = 'flex'; try { const response = await authenticatedFetch(`/api/v1/pprs/${pprId}`); if (!response.ok) { throw new Error('Failed to fetch PPR details'); } const ppr = await response.json(); populateForm(ppr); // Show/hide quick action buttons based on current status if (ppr.status === 'NEW') { document.getElementById('btn-landed').style.display = 'none'; document.getElementById('btn-departed').style.display = 'none'; document.getElementById('btn-cancel').style.display = 'inline-block'; } else if (ppr.status === 'CONFIRMED') { document.getElementById('btn-landed').style.display = 'inline-block'; document.getElementById('btn-departed').style.display = 'none'; document.getElementById('btn-cancel').style.display = 'inline-block'; } else if (ppr.status === 'LANDED') { document.getElementById('btn-landed').style.display = 'none'; document.getElementById('btn-departed').style.display = 'inline-block'; document.getElementById('btn-cancel').style.display = 'inline-block'; } else { // DEPARTED, CANCELED, DELETED - hide all quick actions and cancel button document.querySelector('.quick-actions').style.display = 'none'; document.getElementById('btn-cancel').style.display = 'none'; } await loadJournal(pprId); // Always load journal when opening a PPR document.getElementById('pprModal').style.display = 'block'; } catch (error) { console.error('Error loading PPR details:', error); showNotification('Error loading PPR details', true); } } function populateForm(ppr) { Object.keys(ppr).forEach(key => { if (key === 'eta' || key === 'etd') { if (ppr[key]) { // ppr[key] is UTC datetime string from API (naive, assume UTC) let utcDateStr = ppr[key]; if (!utcDateStr.includes('T')) { utcDateStr = utcDateStr.replace(' ', 'T'); } if (!utcDateStr.includes('Z')) { utcDateStr += 'Z'; } const date = new Date(utcDateStr); // Now correctly parsed as UTC // Split into date and time components for separate inputs const dateField = document.getElementById(`${key}-date`); const timeField = document.getElementById(`${key}-time`); if (dateField && timeField) { // Format date const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const dateValue = `${year}-${month}-${day}`; dateField.value = dateValue; // Format time (round to nearest 15-minute interval) const hours = String(date.getHours()).padStart(2, '0'); const rawMinutes = date.getMinutes(); const roundedMinutes = Math.round(rawMinutes / 15) * 15 % 60; const minutes = String(roundedMinutes).padStart(2, '0'); const timeValue = `${hours}:${minutes}`; timeField.value = timeValue; } } } else { const field = document.getElementById(key); if (field) { field.value = ppr[key] || ''; } } }); } // Generic function to load journal for any entity type async function loadJournalForEntity(entityType, entityId, containerElementId) { try { const response = await fetch(`/api/v1/journal/${entityType}/${entityId}`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) { throw new Error('Failed to fetch journal'); } const data = await response.json(); // The new API returns { entity_type, entity_id, entries, total_entries } displayJournalForContainer(data.entries || [], containerElementId); } catch (error) { console.error('Error loading journal:', error); const container = document.getElementById(containerElementId); if (container) container.innerHTML = 'Error loading journal entries'; } } // PPR-specific journal loader (backward compatible) async function loadJournal(pprId) { await loadJournalForEntity('PPR', pprId, 'journal-entries'); } // Local Flight specific journal loader async function loadLocalFlightJournal(flightId) { await loadJournalForEntity('LOCAL_FLIGHT', flightId, 'local-flight-journal-entries'); } // Departure specific journal loader async function loadDepartureJournal(departureId) { await loadJournalForEntity('DEPARTURE', departureId, 'departure-journal-entries'); } // Arrival specific journal loader async function loadArrivalJournal(arrivalId) { await loadJournalForEntity('ARRIVAL', arrivalId, 'arrival-journal-entries'); } // Display journal in a specific container function displayJournalForContainer(entries, containerElementId) { const container = document.getElementById(containerElementId); if (!container) return; if (entries.length === 0) { container.innerHTML = '

No journal entries yet.

'; } else { container.innerHTML = entries.map(entry => `
${formatDateTime(entry.entry_dt)} by ${entry.user}
${entry.entry}
`).join(''); } } function displayJournal(entries) { displayJournalForContainer(entries, 'journal-entries'); // Always show journal section when displaying entries document.getElementById('journal-section').style.display = 'block'; } function closePPRModal() { closeModal('pprModal', () => { currentPPRId = null; isNewPPR = false; }); } // Timestamp modal functions function showTimestampModal(status, pprId = null, isLocalFlight = false, isDeparture = false, isBookedIn = false) { const targetId = pprId || (isLocalFlight ? currentLocalFlightId : (isBookedIn ? currentBookedInArrivalId : currentPPRId)); if (!targetId) return; pendingStatusUpdate = { status: status, pprId: targetId, isLocalFlight: isLocalFlight, isDeparture: isDeparture, isBookedIn: isBookedIn }; const modalTitle = document.getElementById('timestamp-modal-title'); const submitBtn = document.getElementById('timestamp-submit-btn'); if (status === 'LANDED') { modalTitle.textContent = 'Confirm Landing Time'; submitBtn.textContent = '🛬 Confirm Landing'; } else if (status === 'DEPARTED') { modalTitle.textContent = 'Confirm Departure Time'; submitBtn.textContent = '🛫 Confirm Departure'; } else if (status === 'GROUND') { modalTitle.textContent = 'Confirm Contact Time'; submitBtn.textContent = '📞 Confirm Contact'; } else if (status === 'LOCAL') { modalTitle.textContent = 'Confirm Takeoff Time'; submitBtn.textContent = '🛫 Confirm Takeoff'; } // Set default timestamp to current UTC time const now = new Date(); const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, '0'); const day = String(now.getUTCDate()).padStart(2, '0'); const hours = String(now.getUTCHours()).padStart(2, '0'); const minutes = String(now.getUTCMinutes()).padStart(2, '0'); document.getElementById('event-timestamp').value = `${year}-${month}-${day}T${hours}:${minutes}`; document.getElementById('timestampModal').style.display = 'block'; } function closeTimestampModal() { document.getElementById('timestampModal').style.display = 'none'; pendingStatusUpdate = null; document.getElementById('timestamp-form').reset(); } // Circuit modal functions function showCircuitModal(localFlightId = null, arrivalId = null) { localFlightId = localFlightId || currentLocalFlightId; arrivalId = arrivalId || currentArrivalId; if (!localFlightId && !arrivalId) return; // Set the current IDs currentLocalFlightId = localFlightId; currentArrivalId = arrivalId; // Set default timestamp to current UTC time const now = new Date(); const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, '0'); const day = String(now.getUTCDate()).padStart(2, '0'); const hours = String(now.getUTCHours()).padStart(2, '0'); const minutes = String(now.getUTCMinutes()).padStart(2, '0'); document.getElementById('circuit-timestamp').value = `${year}-${month}-${day}T${hours}:${minutes}`; document.getElementById('circuitModal').style.display = 'block'; } function closeCircuitModal() { document.getElementById('circuitModal').style.display = 'none'; document.getElementById('circuit-form').reset(); currentLocalFlightId = null; currentArrivalId = null; } // Circuit form submission document.getElementById('circuit-form').addEventListener('submit', async function(e) { e.preventDefault(); if ((!currentLocalFlightId && !currentArrivalId) || !accessToken) return; const circuitTimestampInput = document.getElementById('circuit-timestamp').value; if (!circuitTimestampInput) { showNotification('Please select a circuit time', true); return; } try { // Input is treated as UTC - append Z to force UTC interpretation const circuitTimestamp = circuitTimestampInput + ':00Z'; const requestBody = { circuit_timestamp: circuitTimestamp }; // Add the appropriate ID based on what we're tracking if (currentLocalFlightId) { requestBody.local_flight_id = currentLocalFlightId; } else if (currentArrivalId) { requestBody.arrival_id = currentArrivalId; } const response = await authenticatedFetch('/api/v1/circuits/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to record circuit'); } const circuit = await response.json(); showNotification(`✈️ Circuit recorded at ${formatTimeOnly(circuit.circuit_timestamp)}`); await afterCircuitSaved(); } catch (error) { console.error('Error recording circuit:', error); showNotification('Error recording circuit: ' + error.message, true); } }); // Default hook: override per page to add post-save behaviour async function afterCircuitSaved() { closeCircuitModal(); loadPPRs(); } document.getElementById('timestamp-form').addEventListener('submit', async function(e) { e.preventDefault(); // Handle overflight QSY mode if (isOverflightQSYMode) { isOverflightQSYMode = false; await handleOverflightQSYSubmit(); return; } if (!pendingStatusUpdate || !accessToken) return; const timestampInput = document.getElementById('event-timestamp').value; let timestamp = null; if (timestampInput.trim()) { // Input is UTC - append Z to force UTC interpretation timestamp = timestampInput + ':00Z'; } try { // Determine the correct API endpoint based on flight type const isLocal = pendingStatusUpdate.isLocalFlight; const isDeparture = pendingStatusUpdate.isDeparture; const isBookedIn = pendingStatusUpdate.isBookedIn; let endpoint; if (isLocal) { endpoint = `/api/v1/local-flights/${pendingStatusUpdate.pprId}/status`; } else if (isDeparture) { endpoint = `/api/v1/departures/${pendingStatusUpdate.pprId}/status`; } else if (isBookedIn) { endpoint = `/api/v1/arrivals/${pendingStatusUpdate.pprId}/status`; } else { endpoint = `/api/v1/pprs/${pendingStatusUpdate.pprId}/status`; } const response = await fetch(endpoint, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ status: pendingStatusUpdate.status, timestamp: timestamp }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Failed to update status: ${response.status} ${response.statusText} - ${errorData.detail || 'Unknown error'}`); } const updatedStatus = pendingStatusUpdate.status; closeTimestampModal(); // 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}`); if (!isLocal && !isBookedIn) { closePPRModal(); // Close PPR modal after successful status update } } catch (error) { console.error('Error updating status:', error); showNotification(`Error updating status: ${error.message}`, true); } }); // Form submission document.getElementById('ppr-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken) return; // Auto-save any unsaved aircraft types await autoSaveUnsavedAircraft(this); const formData = new FormData(this); const pprData = {}; formData.forEach((value, key) => { if (key !== 'id' && value.trim() !== '') { if (key === 'pob_in' || key === 'pob_out') { pprData[key] = parseInt(value); } else if (key === 'eta-date' && formData.get('eta-time')) { // Combine date and time for ETA const dateStr = formData.get('eta-date'); const timeStr = formData.get('eta-time'); pprData.eta = new Date(`${dateStr}T${timeStr}`).toISOString(); } else if (key === 'etd-date' && formData.get('etd-time')) { // Combine date and time for ETD const dateStr = formData.get('etd-date'); const timeStr = formData.get('etd-time'); pprData.etd = new Date(`${dateStr}T${timeStr}`).toISOString(); } else if (key !== 'eta-time' && key !== 'etd-time') { // Skip the time fields as they're handled above pprData[key] = value; } } }); try { let response; if (isNewPPR) { response = await fetch('/api/v1/pprs/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(pprData) }); } else { response = await fetch(`/api/v1/pprs/${currentPPRId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(pprData) }); } if (!response.ok) { throw new Error('Failed to save PPR'); } const wasNewPPR = isNewPPR; closePPRModal(); loadPPRs(); // Refresh both tables showNotification(wasNewPPR ? 'PPR created successfully!' : 'PPR updated successfully!'); } catch (error) { console.error('Error saving PPR:', error); showNotification('Error saving PPR', true); } }); // Status update functions async function updateStatus(status) { if (!currentPPRId || !accessToken) return; // Show confirmation for cancel actions if (status === 'CANCELED') { if (!confirm('Are you sure you want to cancel this PPR? This action cannot be easily undone.')) { return; } } try { const response = await fetch(`/api/v1/pprs/${currentPPRId}/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'); } await loadJournal(currentPPRId); // Refresh journal loadPPRs(); // Refresh both tables showNotification(`Status updated to ${status}`); closePPRModal(); // Close modal after successful status update } catch (error) { console.error('Error updating status:', error); showNotification('Error updating status', true); } } async function updateStatusFromTable(pprId, status) { if (!accessToken) return; // Show confirmation for cancel actions if (status === 'CANCELED') { if (!confirm('Are you sure you want to cancel this PPR? This action cannot be easily undone.')) { return; } } try { const response = await fetch(`/api/v1/pprs/${pprId}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ status: status }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Failed to update status: ${response.status} ${response.statusText} - ${errorData.detail || 'Unknown error'}`); } loadPPRs(); // Refresh both tables showNotification(`Status updated to ${status}`); } catch (error) { console.error('Error updating status:', error); showNotification(`Error updating status: ${error.message}`, true); } } async function deletePPR() { if (!currentPPRId || !accessToken) return; if (!confirm('Are you sure you want to delete this PPR? This action cannot be undone.')) { return; } try { const response = await fetch(`/api/v1/pprs/${currentPPRId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) { throw new Error('Failed to delete PPR'); } closePPRModal(); loadPPRs(); // Refresh both tables showNotification('PPR deleted successfully!'); } catch (error) { console.error('Error deleting PPR:', error); showNotification('Error deleting PPR', true); } } // User Management Functions async function openUserManagementModal() { if (!accessToken) return; document.getElementById('userManagementModal').style.display = 'block'; await loadUsers(); } async function loadUsers() { if (!accessToken) return; document.getElementById('users-loading').style.display = 'block'; document.getElementById('users-table-content').style.display = 'none'; document.getElementById('users-no-data').style.display = 'none'; try { const response = await authenticatedFetch('/api/v1/auth/users'); if (!response.ok) { throw new Error('Failed to fetch users'); } const users = await response.json(); displayUsers(users); } catch (error) { console.error('Error loading users:', error); if (error.message !== 'Session expired. Please log in again.') { showNotification('Error loading users', true); } } document.getElementById('users-loading').style.display = 'none'; } function displayUsers(users) { const tbody = document.getElementById('users-table-body'); if (users.length === 0) { document.getElementById('users-no-data').style.display = 'block'; return; } tbody.innerHTML = ''; document.getElementById('users-table-content').style.display = 'block'; users.forEach(user => { const row = document.createElement('tr'); // Format role for display const roleDisplay = { 'ADMINISTRATOR': 'Administrator', 'OPERATOR': 'Operator', 'READ_ONLY': 'Read Only' }[user.role] || user.role; // Format created date const createdDate = user.created_at ? formatDateTime(user.created_at) : '-'; row.innerHTML = ` ${user.username} ${roleDisplay} ${createdDate} `; tbody.appendChild(row); }); } function openUserCreateModal() { isNewUser = true; currentUserId = null; document.getElementById('user-modal-title').textContent = 'Create New User'; // Clear form document.getElementById('user-form').reset(); document.getElementById('user-id').value = ''; document.getElementById('user-password').required = true; // Show password help text const passwordHelp = document.querySelector('#user-password + small'); if (passwordHelp) passwordHelp.style.display = 'none'; document.getElementById('userModal').style.display = 'block'; // Auto-focus on username field setTimeout(() => { document.getElementById('user-username').focus(); }, 100); } async function openUserEditModal(userId) { if (!accessToken) return; isNewUser = false; currentUserId = userId; document.getElementById('user-modal-title').textContent = 'Edit User'; try { const response = await authenticatedFetch(`/api/v1/auth/users/${userId}`); if (!response.ok) { throw new Error('Failed to fetch user details'); } const user = await response.json(); populateUserForm(user); document.getElementById('userModal').style.display = 'block'; } catch (error) { console.error('Error loading user details:', error); showNotification('Error loading user details', true); } } function populateUserForm(user) { document.getElementById('user-id').value = user.id; document.getElementById('user-username').value = user.username; document.getElementById('user-password').value = ''; // Don't populate password document.getElementById('user-role').value = user.role; // Make password optional for editing document.getElementById('user-password').required = false; // Show password help text const passwordHelp = document.querySelector('#user-password + small'); if (passwordHelp) passwordHelp.style.display = 'block'; } function closeUserModal() { closeModal('userModal', () => { currentUserId = null; isNewUser = false; }); } function openChangePasswordModal(userId, username) { if (!accessToken) return; currentChangePasswordUserId = userId; document.getElementById('change-password-username').value = username; document.getElementById('change-password-new').value = ''; document.getElementById('change-password-confirm').value = ''; document.getElementById('changePasswordModal').style.display = 'block'; // Auto-focus on new password field setTimeout(() => { document.getElementById('change-password-new').focus(); }, 100); } function closeChangePasswordModal() { closeModal('changePasswordModal', () => { currentChangePasswordUserId = null; }); } // Change password form submission document.getElementById('change-password-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken || !currentChangePasswordUserId) return; const newPassword = document.getElementById('change-password-new').value.trim(); const confirmPassword = document.getElementById('change-password-confirm').value.trim(); // Validate passwords match if (newPassword !== confirmPassword) { showNotification('Passwords do not match!', true); return; } // Validate password length if (newPassword.length < 6) { showNotification('Password must be at least 6 characters long!', true); return; } try { const response = await fetch(`/api/v1/auth/users/${currentChangePasswordUserId}/change-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ password: newPassword }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || 'Failed to change password'); } closeChangePasswordModal(); showNotification('Password changed successfully!'); } catch (error) { console.error('Error changing password:', error); showNotification(`Error changing password: ${error.message}`, true); } }); // User form submission document.getElementById('user-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken) return; const formData = new FormData(this); const userData = {}; formData.forEach((value, key) => { if (key !== 'id' && value.trim() !== '') { userData[key] = value; } }); try { let response; if (isNewUser) { response = await fetch('/api/v1/auth/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(userData) }); } else { response = await fetch(`/api/v1/auth/users/${currentUserId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(userData) }); } if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || 'Failed to save user'); } const wasNewUser = isNewUser; closeUserModal(); await loadUsers(); // Refresh user list showNotification(wasNewUser ? 'User created successfully!' : 'User updated successfully!'); } catch (error) { console.error('Error saving user:', error); showNotification(`Error saving user: ${error.message}`, true); } }); // ==================== USER AIRCRAFT MANAGEMENT ==================== async function openUserAircraftModal() { if (!accessToken) return; document.getElementById('userAircraftModal').style.display = 'block'; await loadUserAircraft(); // Set up search functionality const searchInput = document.getElementById('user-aircraft-search'); searchInput.addEventListener('input', filterUserAircraft); } async function loadUserAircraft() { if (!accessToken) return; document.getElementById('user-aircraft-loading').style.display = 'block'; document.getElementById('user-aircraft-table-content').style.display = 'none'; document.getElementById('user-aircraft-no-data').style.display = 'none'; try { const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft'); if (!response.ok) { throw new Error('Failed to fetch user aircraft'); } const userAircraft = await response.json(); displayUserAircraft(userAircraft); } catch (error) { console.error('Error loading user aircraft:', error); if (error.message !== 'Session expired. Please log in again.') { showNotification('Error loading user aircraft', true); } } document.getElementById('user-aircraft-loading').style.display = 'none'; } function displayUserAircraft(userAircraft) { const tbody = document.getElementById('user-aircraft-table-body'); const searchInput = document.getElementById('user-aircraft-search'); // Store original data for filtering tbody._originalData = userAircraft; if (userAircraft.length === 0) { document.getElementById('user-aircraft-no-data').style.display = 'block'; return; } // Apply current filter if any const filterText = searchInput.value.trim().toLowerCase(); const filteredAircraft = filterText ? userAircraft.filter(ua => ua.registration.toLowerCase().includes(filterText) || ua.type_code.toLowerCase().includes(filterText) || ua.created_by.toLowerCase().includes(filterText) ) : userAircraft; if (filteredAircraft.length === 0) { tbody.innerHTML = 'No aircraft match the search criteria'; } else { tbody.innerHTML = ''; filteredAircraft.forEach(aircraft => { const row = document.createElement('tr'); // Format created date const createdDate = aircraft.created_at ? formatDateTime(aircraft.created_at) : '-'; row.innerHTML = ` ${aircraft.registration} ${aircraft.type_code} ${aircraft.created_by} ${createdDate} `; tbody.appendChild(row); }); } document.getElementById('user-aircraft-table-content').style.display = 'block'; } function filterUserAircraft() { const tbody = document.getElementById('user-aircraft-table-body'); if (tbody._originalData) { displayUserAircraft(tbody._originalData); } } function openUserAircraftEditModal(registration, typeCode) { document.getElementById('edit-aircraft-registration').value = registration; document.getElementById('edit-aircraft-type').value = typeCode; document.getElementById('user-aircraft-edit-title').textContent = `Edit ${registration}`; document.getElementById('userAircraftEditModal').style.display = 'block'; // Auto-focus on type field setTimeout(() => { document.getElementById('edit-aircraft-type').focus(); }, 100); } // User aircraft edit form submission document.getElementById('user-aircraft-edit-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken) return; const registration = document.getElementById('edit-aircraft-registration').value.trim(); const typeCode = document.getElementById('edit-aircraft-type').value.trim(); if (!registration || !typeCode) { showNotification('Registration and type are required', true); return; } try { const response = await authenticatedFetch(`/api/v1/aircraft/user-aircraft/${registration}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ registration: registration, type_code: typeCode }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || 'Failed to update aircraft'); } closeModal('userAircraftEditModal'); await loadUserAircraft(); // Refresh list showNotification('Aircraft updated successfully!'); } catch (error) { console.error('Error updating aircraft:', error); showNotification(`Error updating aircraft: ${error.message}`, true); } }); async function deleteUserAircraft() { if (!accessToken) return; const registration = document.getElementById('edit-aircraft-registration').value.trim(); if (!confirm(`Are you sure you want to delete the aircraft entry for ${registration}? This action cannot be undone.`)) { return; } try { const response = await authenticatedFetch(`/api/v1/aircraft/user-aircraft/${registration}`, { method: 'DELETE' }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || 'Failed to delete aircraft'); } closeModal('userAircraftEditModal'); await loadUserAircraft(); // Refresh list showNotification('Aircraft deleted successfully!'); } catch (error) { console.error('Error deleting aircraft:', error); showNotification(`Error deleting aircraft: ${error.message}`, true); } } // Update user role detection and UI visibility async function updateUserRole() { if (!accessToken) { return; } try { const response = await authenticatedFetch('/api/v1/auth/test-token', { method: 'POST' }); if (response.ok) { const userData = await response.json(); currentUserRole = userData.role; // Show user management in dropdown only for administrators const userManagementDropdown = document.getElementById('user-management-dropdown'); // Show user aircraft for operators and administrators const userAircraftDropdown = document.getElementById('user-aircraft-dropdown'); if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') { if (userManagementDropdown) userManagementDropdown.style.display = 'block'; if (userAircraftDropdown) userAircraftDropdown.style.display = 'block'; } else if (currentUserRole && currentUserRole.toUpperCase() === 'OPERATOR') { if (userManagementDropdown) userManagementDropdown.style.display = 'none'; if (userAircraftDropdown) userAircraftDropdown.style.display = 'block'; } else { if (userManagementDropdown) userManagementDropdown.style.display = 'none'; if (userAircraftDropdown) userAircraftDropdown.style.display = 'none'; } } } catch (error) { console.error('Error updating user role:', error); // Hide admin features by default on error const userManagementDropdown = document.getElementById('user-management-dropdown'); const userAircraftDropdown = document.getElementById('user-aircraft-dropdown'); if (userManagementDropdown) userManagementDropdown.style.display = 'none'; if (userAircraftDropdown) userAircraftDropdown.style.display = 'none'; } } // Close modal when clicking outside window.onclick = function(event) { const pprModal = document.getElementById('pprModal'); const timestampModal = document.getElementById('timestampModal'); const userManagementModal = document.getElementById('userManagementModal'); const userModal = document.getElementById('userModal'); const tableHelpModal = document.getElementById('tableHelpModal'); const bookInModal = document.getElementById('bookInModal'); if (event.target === pprModal) { closePPRModal(); } if (event.target === timestampModal) { closeTimestampModal(); } if (event.target === userManagementModal) { closeModal('userManagementModal'); } if (event.target === userModal) { closeUserModal(); } if (event.target === tableHelpModal) { closeModal('tableHelpModal'); } if (event.target === bookInModal) { closeModal('bookInModal'); } } function clearArrivalAirportLookup() { document.getElementById('arrival-airport-lookup-results').innerHTML = ''; } function clearDepartureAirportLookup() { document.getElementById('departure-airport-lookup-results').innerHTML = ''; } // Add listener for ETD date changes document.addEventListener('change', function(e) { if (e.target.id === 'local_etd_date') { populateETDTimeSlots(); } }); // Local Flight (Book Out) Modal Functions function openLocalFlightModal(flightType = 'LOCAL', prefillReg = '') { document.getElementById('local-flight-form').reset(); document.getElementById('local-flight-id').value = ''; document.getElementById('local-flight-modal-title').textContent = 'Book Out'; document.getElementById('local_flight_type').value = flightType; document.getElementById('localFlightModal').style.display = 'block'; // Clear the unsaved aircraft flag document.getElementById('local-flight-form').removeAttribute('data-unsaved-aircraft'); // Clear aircraft lookup results clearLocalAircraftLookup(); // Update destination field visibility based on flight type handleFlightTypeChange(flightType); // Auto-focus on registration field and prefill if provided setTimeout(() => { const regField = document.getElementById('local_registration'); regField.focus(); if (prefillReg) { regField.value = prefillReg; } }, 100); } function openBookInModal() { document.getElementById('book-in-form').reset(); document.getElementById('book-in-id').value = ''; document.getElementById('bookInModal').style.display = 'block'; // Clear the unsaved aircraft flag document.getElementById('book-in-form').removeAttribute('data-unsaved-aircraft'); // 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 openOverflightModal() { document.getElementById('overflight-form').reset(); document.getElementById('overflight-id').value = ''; document.getElementById('overflightModal').style.display = 'block'; // Clear the unsaved aircraft flag document.getElementById('overflight-form').removeAttribute('data-unsaved-aircraft'); // Clear aircraft lookup results clearOverflightAircraftLookup(); clearOverflightDepartureAirportLookup(); clearOverflightDestinationAirportLookup(); // Set current UTC time as call time const now = new Date(); const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, '0'); const day = String(now.getUTCDate()).padStart(2, '0'); const hours = now.getUTCHours().toString().padStart(2, '0'); const minutes = now.getUTCMinutes().toString().padStart(2, '0'); document.getElementById('overflight_call_dt').value = `${year}-${month}-${day}T${hours}:${minutes}`; // Auto-focus on registration field setTimeout(() => { document.getElementById('overflight_registration').focus(); }, 100); } async function openOverflightEditModal(overflightId) { if (!accessToken) return; try { const response = await fetch(`/api/v1/overflights/${overflightId}`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error('Failed to load overflight'); const overflight = await response.json(); currentOverflightId = overflight.id; // Populate form document.getElementById('overflight-edit-id').value = overflight.id; document.getElementById('overflight_edit_registration').value = overflight.registration; document.getElementById('overflight_edit_type').value = overflight.type || ''; document.getElementById('overflight_edit_pob').value = overflight.pob || ''; document.getElementById('overflight_edit_departure_airfield').value = overflight.departure_airfield || ''; document.getElementById('overflight_edit_destination_airfield').value = overflight.destination_airfield || ''; document.getElementById('overflight_edit_status').value = overflight.status; document.getElementById('overflight_edit_notes').value = overflight.notes || ''; // Parse and populate call_dt if (overflight.call_dt) { const callDt = new Date(overflight.call_dt); document.getElementById('overflight_edit_call_dt').value = callDt.toISOString().slice(0, 16); } // Parse and populate qsy_dt if exists if (overflight.qsy_dt) { const qsyDt = new Date(overflight.qsy_dt); document.getElementById('overflight_edit_qsy_dt').value = qsyDt.toISOString().slice(0, 16); } else { document.getElementById('overflight_edit_qsy_dt').value = ''; } // Show/hide action buttons based on status const qsyBtn = document.getElementById('overflight-btn-qsy'); const cancelBtn = document.getElementById('overflight-btn-cancel'); if (qsyBtn) qsyBtn.style.display = overflight.status === 'ACTIVE' ? 'inline-block' : 'none'; if (cancelBtn) cancelBtn.style.display = overflight.status === 'ACTIVE' ? 'inline-block' : 'none'; document.getElementById('overflight-edit-title').textContent = `${overflight.registration} - Overflight`; document.getElementById('overflightEditModal').style.display = 'block'; } catch (error) { console.error('Error loading overflight:', error); showNotification('Error loading overflight details', true); } } function closeOverflightEditModal() { closeModal('overflightEditModal'); } async function updateOverflightStatus(newStatus, qsyTime = null) { if (!currentOverflightId || !accessToken) return; try { const body = { status: newStatus }; if (qsyTime) { body.qsy_dt = qsyTime; } const response = await fetch(`/api/v1/overflights/${currentOverflightId}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(body) }); if (!response.ok) throw new Error('Failed to update overflight status'); closeOverflightEditModal(); loadPPRs(); // Refresh overflights display showNotification(`Overflight marked as ${newStatus}`); } catch (error) { console.error('Error updating overflight status:', error); showNotification('Error updating overflight status', true); } } function showOverflightQSYModal() { if (!currentOverflightId) return; isOverflightQSYMode = true; const modalTitle = document.getElementById('timestamp-modal-title'); const submitBtn = document.getElementById('timestamp-submit-btn'); modalTitle.textContent = 'Confirm QSY Time'; submitBtn.textContent = '📡 Confirm QSY'; // Set default timestamp to current UTC time const now = new Date(); const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, '0'); const day = String(now.getUTCDate()).padStart(2, '0'); const hours = String(now.getUTCHours()).padStart(2, '0'); const minutes = String(now.getUTCMinutes()).padStart(2, '0'); document.getElementById('event-timestamp').value = `${year}-${month}-${day}T${hours}:${minutes}`; document.getElementById('timestampModal').style.display = 'block'; } async function handleOverflightQSYSubmit() { const timestamp = document.getElementById('event-timestamp').value; if (!timestamp) { showNotification('Please enter a QSY time', true); return; } // Convert datetime-local value to ISO string // datetime-local format is "YYYY-MM-DDTHH:mm" and we need to treat it as UTC const isoString = timestamp + ':00Z'; // Add seconds and Z for UTC closeTimestampModal(); await updateOverflightStatus('INACTIVE', isoString); } async function cancelOverflightFromTable(overflightId) { if (!confirm('Are you sure you want to cancel this overflight? This action cannot be easily undone.')) { return; } if (!accessToken) return; try { const response = await fetch(`/api/v1/overflights/${overflightId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error('Failed to cancel overflight'); loadPPRs(); showNotification('Overflight cancelled'); } catch (error) { console.error('Error cancelling overflight:', error); showNotification('Error cancelling overflight', true); } } function confirmCancelOverflight() { if (!currentOverflightId) return; if (!confirm('Are you sure you want to cancel this overflight? This action cannot be easily undone.')) { return; } cancelOverflightFromModal(); } async function cancelOverflightFromModal() { if (!currentOverflightId || !accessToken) return; try { const response = await fetch(`/api/v1/overflights/${currentOverflightId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error('Failed to cancel overflight'); closeOverflightEditModal(); loadPPRs(); showNotification('Overflight cancelled'); } catch (error) { console.error('Error cancelling overflight:', error); showNotification('Error cancelling overflight', true); } } // Overflight edit form submission document.getElementById('overflight-edit-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!currentOverflightId || !accessToken) return; const formData = new FormData(this); const updateData = {}; formData.forEach((value, key) => { if (key === 'id') return; // Handle datetime-local fields - treat as UTC if (key === 'call_dt' || key === 'qsy_dt') { if (value) { updateData[key] = value + ':00Z'; } return; } // Only include non-empty values if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) { if (value.trim) { updateData[key] = value.trim(); } else { updateData[key] = value; } } }); try { const response = await fetch(`/api/v1/overflights/${currentOverflightId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(updateData) }); if (!response.ok) throw new Error('Failed to update overflight'); closeOverflightEditModal(); loadPPRs(); // Refresh overflights display showNotification('Overflight updated successfully'); } catch (error) { console.error('Error updating overflight:', error); showNotification('Error updating overflight', true); } }); 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.getUTCHours().toString().padStart(2, '0'); const minutes = time.getUTCMinutes().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 function handleFlightTypeChange(flightType) { const destGroup = document.getElementById('departure-destination-group'); const destInput = document.getElementById('local_out_to'); const destLabel = document.getElementById('departure-destination-label'); if (flightType === 'DEPARTURE') { destGroup.style.display = 'block'; destInput.required = true; destLabel.textContent = 'Destination Airport *'; } else { destGroup.style.display = 'none'; destInput.required = false; destInput.value = ''; destLabel.textContent = 'Destination Airport'; } // Populate ETD time slots for all flight types populateETDTimeSlots(); } function getNext10MinuteSlot() { const now = new Date(); const utcMinutes = now.getUTCMinutes(); const remainder = utcMinutes % 10; const next = new Date(now); if (remainder !== 0) { next.setTime(now.getTime() + (10 - remainder) * 60 * 1000); } next.setUTCSeconds(0, 0); return next; } function populateETDTimeSlots() { const timeSelect = document.getElementById('local_etd_time'); // Clear and repopulate timeSelect.innerHTML = ''; // Get the next 10-minute slot const defaultTime = getNext10MinuteSlot(); const defaultHour = defaultTime.getUTCHours(); const defaultMinute = defaultTime.getUTCMinutes(); // Generate 10-minute slots from next slot to 22:00 let currentHour = defaultHour; let currentMinute = defaultMinute; // Generate 10-minute slots from start time to 22:00 while (currentHour < 24) { const timeStr = `${String(currentHour).padStart(2, '0')}:${String(currentMinute).padStart(2, '0')}`; const option = document.createElement('option'); option.value = timeStr; option.textContent = timeStr; // Auto-select the next 10-minute slot if (currentHour === defaultHour && currentMinute === defaultMinute) { option.selected = true; } timeSelect.appendChild(option); // Move to next 10-minute interval currentMinute += 10; if (currentMinute >= 60) { currentMinute -= 60; currentHour += 1; } } } function clearLocalAircraftLookup() { document.getElementById('local-aircraft-lookup-results').innerHTML = ''; } // Local Flight Edit Modal Functions async function openLocalFlightEditModal(flightId) { if (!accessToken) return; try { const response = await fetch(`/api/v1/local-flights/${flightId}`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error('Failed to load flight'); const flight = await response.json(); currentLocalFlightId = flight.id; // Populate form document.getElementById('local-edit-flight-id').value = flight.id; document.getElementById('local_edit_registration').value = flight.registration; document.getElementById('local_edit_type').value = flight.type; document.getElementById('local_edit_callsign').value = flight.callsign || ''; document.getElementById('local_edit_pob').value = flight.pob; document.getElementById('local_edit_flight_type').value = flight.flight_type; document.getElementById('local_edit_duration').value = flight.duration || 45; document.getElementById('local_edit_notes').value = flight.notes || ''; // Parse and populate takeoff time if exists // Use takeoff_dt (actual takeoff) if available, otherwise departed_dt, otherwise etd const takeoffTime = flight.takeoff_dt || flight.departed_dt || flight.etd; if (takeoffTime) { const tStr = takeoffTime.includes('Z') ? takeoffTime : takeoffTime.replace(' ', 'T') + 'Z'; const dept = new Date(tStr); document.getElementById('local_edit_departure_date').value = dept.toISOString().slice(0, 10); document.getElementById('local_edit_departure_time').value = dept.toISOString().slice(11, 16); } else { document.getElementById('local_edit_departure_date').value = ''; document.getElementById('local_edit_departure_time').value = ''; } // Parse and populate landing time if exists if (flight.landed_dt) { const lStr = flight.landed_dt.includes('Z') ? flight.landed_dt : flight.landed_dt.replace(' ', 'T') + 'Z'; const land = new Date(lStr); document.getElementById('local_edit_landed_date').value = land.toISOString().slice(0, 10); document.getElementById('local_edit_landed_time').value = land.toISOString().slice(11, 16); } else { document.getElementById('local_edit_landed_date').value = ''; document.getElementById('local_edit_landed_time').value = ''; } // Show/hide action buttons based on status const deptBtn = document.getElementById('local-btn-departed'); const landBtn = document.getElementById('local-btn-landed'); const cancelBtn = document.getElementById('local-btn-cancel'); deptBtn.style.display = flight.status === 'BOOKED_OUT' ? 'inline-block' : 'none'; landBtn.style.display = flight.status === 'DEPARTED' ? 'inline-block' : 'none'; cancelBtn.style.display = (flight.status === 'BOOKED_OUT' || flight.status === 'DEPARTED') ? 'inline-block' : 'none'; document.getElementById('local-flight-edit-title').textContent = `${flight.registration} - ${flight.flight_type}`; // Load and display circuits for all local flight types const circuitsSection = document.getElementById('circuits-section'); circuitsSection.style.display = 'block'; loadCircuitsDisplay(flight.id); document.getElementById('localFlightEditModal').style.display = 'block'; // Load journal for this local flight await loadLocalFlightJournal(flightId); } catch (error) { console.error('Error loading flight:', error); showNotification('Error loading flight details', true); } } async function loadCircuitsDisplay(localFlightId) { try { const response = await authenticatedFetch(`/api/v1/circuits/flight/${localFlightId}`); if (!response.ok) throw new Error('Failed to load circuits'); const circuits = await response.json(); displayCircuitsList(circuits); } catch (error) { console.error('Error loading circuits:', error); document.getElementById('circuits-list').innerHTML = '

Error loading circuits

'; } } function displayCircuitsList(circuits) { const circuitsList = document.getElementById('circuits-list'); if (circuits.length === 0) { circuitsList.innerHTML = '

No touch & go records yet

'; return; } let html = `

Total circuits: ${circuits.length}

`; html += '
'; circuits.forEach((circuit, index) => { const time = formatTimeOnly(circuit.circuit_timestamp); html += `
Circuit ${index + 1}: ${time}
`; }); html += '
'; circuitsList.innerHTML = html; } function closeLocalFlightEditModal() { document.getElementById('localFlightEditModal').style.display = 'none'; currentLocalFlightId = null; } // Open departure edit modal async function openDepartureEditModal(departureId) { if (!accessToken) return; try { const response = await fetch(`/api/v1/departures/${departureId}`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error('Failed to load departure'); const departure = await response.json(); currentDepartureId = departure.id; // Populate form document.getElementById('departure-edit-id').value = departure.id; document.getElementById('departure_edit_registration').value = departure.registration; document.getElementById('departure_edit_type').value = departure.type || ''; document.getElementById('departure_edit_callsign').value = departure.callsign || ''; document.getElementById('departure_edit_pob').value = departure.pob || ''; document.getElementById('departure_edit_out_to').value = departure.out_to; document.getElementById('departure_edit_notes').value = departure.notes || ''; // Parse and populate ETD if exists (UTC) if (departure.etd) { const eStr = departure.etd.includes('Z') ? departure.etd : departure.etd.replace(' ', 'T') + 'Z'; const etd = new Date(eStr); document.getElementById('departure_edit_etd_date').value = etd.toISOString().slice(0, 10); document.getElementById('departure_edit_etd_time').value = etd.toISOString().slice(11, 16); } else { document.getElementById('departure_edit_etd_date').value = ''; document.getElementById('departure_edit_etd_time').value = ''; } // Parse and populate takeoff time if exists (takeoff_dt or departed_dt) const takeoffTime = departure.takeoff_dt || departure.departed_dt; if (takeoffTime) { const tStr = takeoffTime.includes('Z') ? takeoffTime : takeoffTime.replace(' ', 'T') + 'Z'; const takeoff = new Date(tStr); document.getElementById('departure_edit_takeoff_date').value = takeoff.toISOString().slice(0, 10); document.getElementById('departure_edit_takeoff_time').value = takeoff.toISOString().slice(11, 16); } else { document.getElementById('departure_edit_takeoff_date').value = ''; document.getElementById('departure_edit_takeoff_time').value = ''; } // Show/hide action buttons based on status const deptBtn = document.getElementById('departure-btn-departed'); const cancelBtn = document.getElementById('departure-btn-cancel'); if (deptBtn) deptBtn.style.display = departure.status === 'BOOKED_OUT' ? 'inline-block' : 'none'; if (cancelBtn) cancelBtn.style.display = departure.status === 'BOOKED_OUT' ? 'inline-block' : 'none'; document.getElementById('departure-edit-title').textContent = `${departure.registration} to ${departure.out_to}`; document.getElementById('departureEditModal').style.display = 'block'; // Load journal for this departure await loadDepartureJournal(departureId); } catch (error) { console.error('Error loading departure:', error); showNotification('Error loading departure details', true); } } function closeDepartureEditModal() { closeModal('departureEditModal', () => { currentDepartureId = null; }); } // Departure edit form submission document.getElementById('departure-edit-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!currentDepartureId || !accessToken) return; const formData = new FormData(this); const updateData = {}; formData.forEach((value, key) => { if (key === 'id') return; // Handle date/time combination for ETD if (key === 'etd_date' || key === 'etd_time') { if (!updateData.etd && formData.get('etd_date') && formData.get('etd_time')) { const dateStr = formData.get('etd_date'); const timeStr = formData.get('etd_time'); updateData.etd = `${dateStr}T${timeStr}:00Z`; } return; } // Handle date/time combination for takeoff_dt if (key === 'takeoff_date' || key === 'takeoff_time') { if (!updateData.takeoff_dt && formData.get('takeoff_date') && formData.get('takeoff_time')) { const dateStr = formData.get('takeoff_date'); const timeStr = formData.get('takeoff_time'); updateData.takeoff_dt = `${dateStr}T${timeStr}:00Z`; } return; } // Cast pob to integer if (key === 'pob') { const num = parseInt(value, 10); if (!isNaN(num)) updateData.pob = num; return; } // Only include non-empty values if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) { if (value.trim) { updateData[key] = value.trim(); } else { updateData[key] = value; } } }); try { const response = await fetch(`/api/v1/departures/${currentDepartureId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(updateData) }); if (!response.ok) throw new Error('Failed to update departure'); closeDepartureEditModal(); loadPPRs(); // Refresh departures display showNotification('Departure updated successfully'); } catch (error) { console.error('Error updating departure:', error); showNotification('Error updating departure', true); } }); // Arrival Edit Modal Functions async function openArrivalEditModal(arrivalId) { if (!accessToken) return; try { const response = await fetch(`/api/v1/arrivals/${arrivalId}`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error('Failed to load arrival'); const arrival = await response.json(); currentArrivalId = arrival.id; // Populate form document.getElementById('arrival-edit-id').value = arrival.id; document.getElementById('arrival_edit_registration').value = arrival.registration || ''; document.getElementById('arrival_edit_type').value = arrival.type || ''; document.getElementById('arrival_edit_callsign').value = arrival.callsign || ''; document.getElementById('arrival_edit_in_from').value = arrival.in_from || ''; document.getElementById('arrival_edit_pob').value = arrival.pob || ''; document.getElementById('arrival_edit_notes').value = arrival.notes || ''; // Update title const regOrCallsign = arrival.callsign || arrival.registration; document.getElementById('arrival-edit-title').textContent = `Arrival: ${regOrCallsign}`; // Show/hide quick action buttons based on status const landedBtn = document.getElementById('arrival-btn-landed'); const cancelBtn = document.getElementById('arrival-btn-cancel'); landedBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none'; cancelBtn.style.display = arrival.status === 'INBOUND' ? 'block' : 'none'; // Show modal document.getElementById('arrivalEditModal').style.display = 'block'; // Load journal for this arrival await loadArrivalJournal(arrivalId); } catch (error) { console.error('Error loading arrival:', error); showNotification('Error loading arrival details', true); } } function closeArrivalEditModal() { closeModal('arrivalEditModal', () => { currentArrivalId = null; }); } // Arrival edit form submission document.getElementById('arrival-edit-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!currentArrivalId || !accessToken) return; const formData = new FormData(this); const updateData = {}; formData.forEach((value, key) => { if (key === 'id') return; // Only include non-empty values if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) { if (key === 'pob') { updateData[key] = parseInt(value); } else if (value.trim) { updateData[key] = value.trim(); } else { updateData[key] = value; } } }); try { const response = await fetch(`/api/v1/arrivals/${currentArrivalId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(updateData) }); if (!response.ok) throw new Error('Failed to update arrival'); closeArrivalEditModal(); loadPPRs(); // Refresh arrivals display showNotification('Arrival updated successfully'); } catch (error) { console.error('Error updating arrival:', error); showNotification('Error updating arrival', true); } }); async function updateArrivalStatus(status) { if (!currentArrivalId || !accessToken) return; if (status === 'CANCELLED') { if (!confirm('Are you sure you want to cancel this arrival?')) { return; } } try { const response = await fetch(`/api/v1/arrivals/${currentArrivalId}/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 arrival status'); closeArrivalEditModal(); loadPPRs(); // Refresh arrivals display showNotification(`Arrival marked as ${status.toLowerCase()}`); } catch (error) { console.error('Error updating arrival status:', error); showNotification('Error updating arrival status', true); } } // Update status from table buttons (with flight ID passed) // Update status from modal (uses currentLocalFlightId) async function updateLocalFlightStatus(status) { if (!currentLocalFlightId || !accessToken) return; // Show confirmation for cancel actions if (status === 'CANCELLED') { if (!confirm('Are you sure you want to cancel this flight? This action cannot be easily undone.')) { return; } } try { const response = await fetch(`/api/v1/local-flights/${currentLocalFlightId}/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'); closeLocalFlightEditModal(); loadPPRs(); // Refresh display showNotification(`Flight marked as ${status.toLowerCase()}`); } catch (error) { console.error('Error updating status:', error); showNotification('Error updating flight status', true); } } async function updateDepartureStatus(status) { if (!currentDepartureId || !accessToken) return; // Show confirmation for cancel actions if (status === 'CANCELLED') { if (!confirm('Are you sure you want to cancel this departure? This action cannot be easily undone.')) { return; } } try { const response = await fetch(`/api/v1/departures/${currentDepartureId}/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'); closeDepartureEditModal(); loadPPRs(); // Refresh display showNotification(`Departure marked as ${status.toLowerCase()}`); } catch (error) { console.error('Error updating status:', error); showNotification('Error updating departure status', true); } } // Table help modal functions const tableHelpTexts = { arrivals: { title: "Today's Pending Arrivals", text: "Displays aircraft that are expected to arrive at Swansea today. These are PPR arrivals and aircraft booked in as arrivals." }, departures: { title: "Today's Pending Departures", text: "Displays visiting aircraft and airport departures that are ready to leave Swansea today. Local flights are shown in their own table." }, "local-flights": { title: "Today's Local Flights", text: "Displays local and circuit flights booked out today, with shortcuts for contact, takeoff, circuit work, touch-and-go, and landing." }, overflights: { title: "Active Overflights", text: "Displays aircraft that are currently in contact with Air / Ground. Once marked a QSY (changed frequency), they are no longer considered active overflights." }, departed: { title: "Departed Today", text: "Displays the flights which have departed today to other airfields, either by way of a PPR or by booking out." }, parked: { title: "Parked Visitors", text: "Displays visiting aircraft that are currently parked at Swansea airport and are NOT expected to depart today. The ETD column shows the day the aircraft intends to depart." } }; function showTableHelp(tableType) { const helpData = tableHelpTexts[tableType]; if (!helpData) return; const modal = document.getElementById('tableHelpModal'); const content = document.getElementById('tableHelpContent'); const header = modal.querySelector('.modal-header h2'); header.textContent = helpData.title; content.innerHTML = `

${helpData.text}

`; modal.style.display = 'block'; } // Local flight edit form submission document.getElementById('local-flight-edit-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!currentLocalFlightId || !accessToken) return; const formData = new FormData(this); const updateData = {}; formData.forEach((value, key) => { if (key === 'id') return; // Handle date/time combination for takeoff if (key === 'departure_date' || key === 'departure_time') { if (!updateData.takeoff_dt && formData.get('departure_date') && formData.get('departure_time')) { const dateStr = formData.get('departure_date'); const timeStr = formData.get('departure_time'); updateData.takeoff_dt = `${dateStr}T${timeStr}:00Z`; } return; } // Handle date/time combination for landing if (key === 'landed_date' || key === 'landed_time') { if (!updateData.landed_dt && formData.get('landed_date') && formData.get('landed_time')) { const dateStr = formData.get('landed_date'); const timeStr = formData.get('landed_time'); updateData.landed_dt = `${dateStr}T${timeStr}:00Z`; } return; } // Only include non-empty values if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) { if (key === 'pob' || key === 'duration') { updateData[key] = parseInt(value); } else if (value.trim) { updateData[key] = value.trim(); } else { updateData[key] = value; } } }); try { const response = await fetch(`/api/v1/local-flights/${currentLocalFlightId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(updateData) }); if (!response.ok) throw new Error('Failed to update flight'); closeLocalFlightEditModal(); loadPPRs(); // Refresh display showNotification('Flight updated successfully'); } catch (error) { console.error('Error updating flight:', error); showNotification('Error updating flight', true); } }); // Add event listener for local flight form submission document.getElementById('local-flight-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken) return; // Auto-save any unsaved aircraft types await autoSaveUnsavedAircraft(this); const formData = new FormData(this); const flightType = formData.get('flight_type'); const flightData = {}; let endpoint = '/api/v1/local-flights/'; formData.forEach((value, key) => { // Skip the hidden id field and empty values if (key === 'id') return; // Handle time-only ETD (always today UTC) if (key === 'etd_time') { if (value.trim()) { const today = new Date(); const dateStr = today.toISOString().split('T')[0]; // UTC date flightData.etd = `${dateStr}T${value}:00Z`; } return; } // Only include non-empty values if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) { if (key === 'pob' || key === 'duration') { flightData[key] = parseInt(value); } else if (value.trim) { flightData[key] = value.trim(); } else { flightData[key] = value; } } }); // If DEPARTURE flight type, use departures endpoint instead if (flightType === 'DEPARTURE') { endpoint = '/api/v1/departures/'; // Remove flight_type from data and use out_to instead delete flightData.flight_type; } try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(flightData) }); if (!response.ok) { let errorMessage = 'Failed to book out flight'; 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(); closeModal('localFlightModal'); loadPPRs(); // Refresh tables showNotification(`Aircraft ${result.registration} booked out successfully!`); } catch (error) { console.error('Error booking out flight:', error); showNotification(`Error: ${error.message}`, true); } }); document.getElementById('book-in-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken) return; // Auto-save any unsaved aircraft types await autoSaveUnsavedAircraft(this); 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 UTC) if (key === 'eta_time') { if (value.trim()) { const today = new Date(); const dateStr = today.toISOString().split('T')[0]; // UTC date arrivalData.eta = `${dateStr}T${value}:00Z`; } 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'; 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(); closeModal('bookInModal'); loadPPRs(); // Refresh tables showNotification(`Aircraft ${result.registration} booked in successfully!`); } catch (error) { console.error('Error booking in arrival:', error); showNotification(`Error: ${error.message}`, true); } }); // Overflight form submission document.getElementById('overflight-form').addEventListener('submit', async function(e) { e.preventDefault(); if (!accessToken) return; // Auto-save any unsaved aircraft types await autoSaveUnsavedAircraft(this); const formData = new FormData(this); const overflightData = {}; formData.forEach((value, key) => { if (key === 'id') return; if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) { if (key === 'pob') { overflightData[key] = parseInt(value); } else if (key === 'call_dt') { // Treat as UTC - append Z if (value.trim()) { overflightData[key] = value + ':00Z'; } } else if (value.trim) { overflightData[key] = value.trim(); } else { overflightData[key] = value; } } }); try { const response = await fetch('/api/v1/overflights/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify(overflightData) }); if (!response.ok) { let errorMessage = 'Failed to register overflight'; 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(); closeModal('overflightModal'); loadPPRs(); showNotification(`Overflight ${result.registration} registered successfully!`); } catch (error) { console.error('Error registering overflight:', error); showNotification(`Error: ${error.message}`, true); } }); // Overflight lookup handlers - use lookupManager function handleOverflightAircraftLookup(input) { const lookup = lookupManager.lookups['overflight-aircraft']; if (lookup) lookup.handle(input); } function clearOverflightAircraftLookup() { const lookup = lookupManager.lookups['overflight-aircraft']; if (lookup) lookup.clear(); } function handleOverflightDepartureAirportLookup(input) { const lookup = lookupManager.lookups['overflight-departure']; if (lookup) lookup.handle(input); } function clearOverflightDepartureAirportLookup() { const lookup = lookupManager.lookups['overflight-departure']; if (lookup) lookup.clear(); } function handleOverflightDestinationAirportLookup(input) { const lookup = lookupManager.lookups['overflight-destination']; if (lookup) lookup.handle(input); } function clearOverflightDestinationAirportLookup() { const lookup = lookupManager.lookups['overflight-destination']; if (lookup) lookup.clear(); } // Position tooltip near mouse cursor function positionTooltip(event) { const tooltip = event.currentTarget.querySelector('.tooltip-text'); if (tooltip) { const rect = tooltip.getBoundingClientRect(); const tooltipWidth = 300; // matches CSS width const tooltipHeight = rect.height || 100; // estimate if not yet rendered let left = event.pageX + 10; let top = event.pageY + 10; // Adjust if tooltip would go off screen if (left + tooltipWidth > window.innerWidth) { left = event.pageX - tooltipWidth - 10; } if (top + tooltipHeight > window.innerHeight) { top = event.pageY - tooltipHeight - 10; } tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; } } // Add hover listeners to all notes tooltips function setupTooltips() { document.querySelectorAll('.notes-tooltip').forEach(tooltip => { tooltip.addEventListener('mouseenter', positionTooltip); tooltip.addEventListener('mouseleave', hideTooltip); }); } // Hide tooltip when mouse leaves function hideTooltip(event) { const tooltip = event.currentTarget.querySelector('.tooltip-text'); if (tooltip) { tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; } } // Initialize the page when DOM is loaded document.addEventListener('DOMContentLoaded', function() { loadUIConfig(); // Load UI configuration first setupLoginForm(); setupKeyboardShortcuts(); initializeTimeDropdowns(); // Initialize time dropdowns initializeAuth(); // Start authentication process // Add event listeners to ETA fields to auto-update ETD document.getElementById('eta-date').addEventListener('change', updateETDFromETA); document.getElementById('eta-time').addEventListener('change', updateETDFromETA); // Add event listeners to ETD fields to mark as manually edited document.getElementById('etd-date').addEventListener('change', markETDAsManuallyEdited); document.getElementById('etd-time').addEventListener('change', markETDAsManuallyEdited); });