3086 lines
131 KiB
JavaScript
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);
|
|
});
|