Files
ppr-ng/web/shared.js
T
2026-06-29 07:15:01 -04:00

3086 lines
131 KiB
JavaScript

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');
}
if (data.type && data.type.startsWith('drone_request_')) {
if (typeof window.refreshDroneRequestBadge === 'function') {
window.refreshDroneRequestBadge();
}
showNotification('Drone request 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 = '<option value="">Select Time</option>';
// 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
if (typeof window.refreshDroneRequestBadge === 'function') {
window.refreshDroneRequestBadge();
}
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;
}
function normalizeUtcDateString(dateStr) {
if (!dateStr) return null;
let utcDateStr = String(dateStr).trim();
if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T');
}
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
utcDateStr += 'Z';
}
return utcDateStr;
}
function parseUtcDate(dateStr) {
const normalized = normalizeUtcDateString(dateStr);
return normalized ? new Date(normalized) : null;
}
function utcDateOnly(dateStr) {
const date = parseUtcDate(dateStr);
return date && !Number.isNaN(date.getTime()) ? date.toISOString().slice(0, 10) : '';
}
function formatUtcDateInput(date) {
return date.toISOString().slice(0, 10);
}
function formatUtcTimeInput(date) {
return date.toISOString().slice(11, 16);
}
function formatUtcDayMonth(dateStr) {
const isoDate = utcDateOnly(dateStr);
return isoDate ? `${isoDate.slice(8, 10)}/${isoDate.slice(5, 7)}` : '-';
}
function formatUtcWeekdayDayMonth(dateStr) {
const date = parseUtcDate(dateStr);
if (!date || Number.isNaN(date.getTime())) return '-';
const dayName = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getUTCDay()];
return `${dayName} ${formatUtcDayMonth(dateStr)}`;
}
function combineUtcDateTimeInput(dateStr, timeStr) {
return `${dateStr}T${timeStr}:00Z`;
}
async function autoSaveUnsavedAircraft(form) {
if (!form || !form.hasAttribute('data-unsaved-aircraft') || !accessToken) return;
const formData = new FormData(form);
const registration = (
formData.get('ac_reg') ||
formData.get('registration') ||
formData.get('local_registration') ||
formData.get('book_in_registration') ||
formData.get('overflight_registration') ||
''
).trim();
const typeCode = (
formData.get('ac_type') ||
formData.get('type') ||
formData.get('local_type') ||
formData.get('book_in_type') ||
formData.get('overflight_type') ||
''
).trim();
if (!registration || !typeCode) return;
try {
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
registration,
type_code: typeCode
})
});
if (response.ok) {
form.removeAttribute('data-unsaved-aircraft');
}
} catch (error) {
console.warn('Could not save user aircraft type:', error);
}
}
// Load PPR records - now loads all tables
function formatTimeOnly(dateStr) {
if (!dateStr) return '-';
const date = parseUtcDate(dateStr);
return date && !Number.isNaN(date.getTime()) ? formatUtcTimeInput(date) : '-';
}
function formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = parseUtcDate(dateStr);
return date && !Number.isNaN(date.getTime()) ? `${formatUtcDateInput(date)} ${formatUtcTimeInput(date)}` : '-';
}
// 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
document.getElementById('eta-date').value = formatUtcDateInput(eta);
document.getElementById('eta-time').value = formatUtcTimeInput(eta);
document.getElementById('etd-date').value = formatUtcDateInput(etd);
document.getElementById('etd-time').value = formatUtcTimeInput(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) {
const eta = parseUtcDate(combineUtcDateTimeInput(etaDate, etaTime));
// Calculate ETD (2 hours after ETA)
const etd = new Date(eta.getTime() + 2 * 60 * 60 * 1000);
// Format ETD
const etdDateStr = formatUtcDateInput(etd);
const etdTimeStr = formatUtcTimeInput(etd);
// 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);
const departedBtn = document.getElementById('btn-departed');
departedBtn.textContent = '🛫 Depart';
departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
// 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';
departedBtn.style.display = 'inline-block';
departedBtn.textContent = '🛫 Take Off';
departedBtn.setAttribute('onclick', "showTimestampModal('LOCAL')");
document.getElementById('btn-cancel').style.display = 'inline-block';
} else if (ppr.status === 'LOCAL') {
document.getElementById('btn-landed').style.display = 'none';
departedBtn.style.display = 'inline-block';
departedBtn.textContent = 'QSY';
departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
document.getElementById('btn-cancel').style.display = 'none';
} 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]) {
const date = parseUtcDate(ppr[key]);
// Split into date and time components for separate inputs
const dateField = document.getElementById(`${key}-date`);
const timeField = document.getElementById(`${key}-time`);
if (dateField && timeField) {
const dateValue = formatUtcDateInput(date);
dateField.value = dateValue;
// Format time (round to nearest 15-minute interval)
const hours = String(date.getUTCHours()).padStart(2, '0');
const rawMinutes = date.getUTCMinutes();
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 = '<p>No journal entries yet.</p>';
} else {
container.innerHTML = entries.map(entry => `
<div class="journal-entry">
<div class="journal-meta">
${formatDateTime(entry.entry_dt)} by ${entry.user}
</div>
<div class="journal-text">${entry.entry}</div>
</div>
`).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 = combineUtcDateTimeInput(dateStr, timeStr);
} 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 = combineUtcDateTimeInput(dateStr, timeStr);
} 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 = `
<td><strong>${user.username}</strong></td>
<td>${roleDisplay}</td>
<td>${createdDate}</td>
<td>
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); openUserEditModal(${user.id})" title="Edit User">
✏️
</button>
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); openChangePasswordModal(${user.id}, '${user.username}')" title="Change Password">
🔐
</button>
</td>
`;
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 = '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No aircraft match the search criteria</td></tr>';
} 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 = `
<td><strong>${aircraft.registration}</strong></td>
<td>${aircraft.type_code}</td>
<td>${aircraft.created_by}</td>
<td>${createdDate}</td>
<td>
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); openUserAircraftEditModal('${aircraft.registration}', '${aircraft.type_code}')" title="Edit Aircraft">
✏️
</button>
</td>
`;
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 = parseUtcDate(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 = parseUtcDate(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 = '<option value="">Select Time</option>';
// 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 = '<p style="color: #d32f2f;">Error loading circuits</p>';
}
}
function displayCircuitsList(circuits) {
const circuitsList = document.getElementById('circuits-list');
if (circuits.length === 0) {
circuitsList.innerHTML = '<p style="color: #666; font-style: italic;">No touch & go records yet</p>';
return;
}
let html = `<p style="color: #333; font-weight: bold;">Total circuits: ${circuits.length}</p>`;
html += '<div style="background-color: #f5f5f5; border-radius: 4px; padding: 1rem; margin-top: 0.5rem;">';
circuits.forEach((circuit, index) => {
const time = formatTimeOnly(circuit.circuit_timestamp);
html += `<div style="padding: 0.5rem 0; border-bottom: 1px solid #ddd;">Circuit ${index + 1}: <strong>${time}</strong></div>`;
});
html += '</div>';
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: "Local Traffic",
text: "Displays local traffic booked out today, including local flights, circuits, and PPR departures that are airborne locally before QSY."
},
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 = `<p>${helpData.text}</p>`;
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);
});