Compare commits

...

2 Commits

Author SHA1 Message Date
ac29b6e929 UI config 2025-12-19 08:33:42 -05:00
0149f45893 AI tweaking 2025-12-19 08:06:47 -05:00
7 changed files with 191 additions and 105 deletions

View File

@@ -24,6 +24,11 @@ MAIL_FROM_NAME=your_mail_from_name_here
# Application settings # Application settings
BASE_URL=your_base_url_here BASE_URL=your_base_url_here
# UI Configuration
TAG=
TOP_BAR_BASE_COLOR=#2c3e50
ENVIRONMENT=development
# Redis (optional) # Redis (optional)
REDIS_URL= REDIS_URL=

View File

@@ -11,10 +11,35 @@ from app.models.local_flight import LocalFlightStatus
from app.models.departure import DepartureStatus from app.models.departure import DepartureStatus
from app.models.arrival import ArrivalStatus from app.models.arrival import ArrivalStatus
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
import re
router = APIRouter() router = APIRouter()
def lighten_color(hex_color, factor=0.3):
"""Lighten a hex color by a factor (0-1)"""
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
return hex_color # Invalid, return as is
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
r = min(255, int(r + (255 - r) * factor))
g = min(255, int(g + (255 - g) * factor))
b = min(255, int(b + (255 - b) * factor))
return f"#{r:02x}{g:02x}{b:02x}"
def darken_color(hex_color, factor=0.3):
"""Darken a hex color by a factor (0-1)"""
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
return hex_color # Invalid, return as is
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
r = max(0, int(r * (1 - factor)))
g = max(0, int(g * (1 - factor)))
b = max(0, int(b * (1 - factor)))
return f"#{r:02x}{g:02x}{b:02x}"
@router.get("/arrivals") @router.get("/arrivals")
async def get_public_arrivals(db: Session = Depends(get_db)): async def get_public_arrivals(db: Session = Depends(get_db)):
"""Get today's arrivals for public display (PPR and local flights)""" """Get today's arrivals for public display (PPR and local flights)"""
@@ -201,3 +226,17 @@ async def get_public_departures(db: Session = Depends(get_db)):
}) })
return departures_list return departures_list
@router.get("/config")
async def get_ui_config():
"""Get UI configuration for client-side rendering"""
from app.core.config import settings
base_color = settings.top_bar_base_color
return {
"tag": settings.tag,
"top_bar_gradient_start": base_color,
"top_bar_gradient_end": lighten_color(base_color, 0.4), # Lighten for gradient end
"footer_color": darken_color(base_color, 0.2), # Darken for footer
"environment": settings.environment
}

View File

@@ -28,6 +28,11 @@ class Settings(BaseSettings):
project_name: str = "Airfield PPR API" project_name: str = "Airfield PPR API"
base_url: str base_url: str
# UI Configuration
tag: str = ""
top_bar_base_color: str = "#2c3e50"
environment: str = "production" # production, development, staging, etc.
# Redis settings (for future use) # Redis settings (for future use)
redis_url: Optional[str] = None redis_url: Optional[str] = None

View File

@@ -26,6 +26,8 @@ services:
MAIL_FROM_NAME: ${MAIL_FROM_NAME} MAIL_FROM_NAME: ${MAIL_FROM_NAME}
BASE_URL: ${BASE_URL} BASE_URL: ${BASE_URL}
REDIS_URL: ${REDIS_URL} REDIS_URL: ${REDIS_URL}
TAG: ${TAG}
TOP_BAR_BASE_COLOR: ${TOP_BAR_BASE_COLOR}
ENVIRONMENT: production ENVIRONMENT: production
WORKERS: "4" WORKERS: "4"
ports: ports:

View File

@@ -38,6 +38,9 @@ services:
MAIL_FROM_NAME: ${MAIL_FROM_NAME} MAIL_FROM_NAME: ${MAIL_FROM_NAME}
BASE_URL: ${BASE_URL} BASE_URL: ${BASE_URL}
REDIS_URL: ${REDIS_URL} REDIS_URL: ${REDIS_URL}
TOWER_NAME: ${TOWER_NAME}
TOP_BAR_BASE_COLOR: ${TOP_BAR_BASE_COLOR}
ENVIRONMENT: ${ENVIRONMENT}
ports: ports:
- "${API_PORT_EXTERNAL}:8000" # Use different port to avoid conflicts with existing system - "${API_PORT_EXTERNAL}:8000" # Use different port to avoid conflicts with existing system
depends_on: depends_on:

View File

@@ -10,7 +10,7 @@
<body> <body>
<div class="top-bar"> <div class="top-bar">
<div class="title"> <div class="title">
<h1>✈️ Swansea Tower</h1> <h1 id="tower-title">✈️ Swansea Tower</h1>
</div> </div>
<div class="menu-buttons"> <div class="menu-buttons">
<div class="dropdown"> <div class="dropdown">
@@ -421,7 +421,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 id="local-flight-modal-title">Book Out</h2> <h2 id="local-flight-modal-title">Book Out</h2>
<button class="close" onclick="closeLocalFlightModal()">&times;</button> <button class="close" onclick="closeModal('localFlightModal')">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="local-flight-form"> <form id="local-flight-form">
@@ -475,7 +475,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeLocalFlightModal()"> <button type="button" class="btn btn-info" onclick="closeModal('localFlightModal')">
Close Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
@@ -586,7 +586,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>Book In</h2> <h2>Book In</h2>
<button class="close" onclick="closeBookInModal()">&times;</button> <button class="close" onclick="closeModal('bookInModal')">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="book-in-form"> <form id="book-in-form">
@@ -628,7 +628,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeBookInModal()"> <button type="button" class="btn btn-info" onclick="closeModal('bookInModal')">
Close Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
@@ -645,7 +645,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>Register Overflight</h2> <h2>Register Overflight</h2>
<button class="close" onclick="closeOverflightModal()">&times;</button> <button class="close" onclick="closeModal('overflightModal')">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="overflight-form"> <form id="overflight-form">
@@ -686,7 +686,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeOverflightModal()"> <button type="button" class="btn btn-info" onclick="closeModal('overflightModal')">
Close Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
@@ -703,7 +703,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 id="overflight-edit-title">Overflight Details</h2> <h2 id="overflight-edit-title">Overflight Details</h2>
<button class="close" onclick="closeOverflightEditModal()">&times;</button> <button class="close" onclick="closeModal('overflightEditModal')">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="quick-actions"> <div class="quick-actions">
@@ -758,7 +758,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeOverflightEditModal()"> <button type="button" class="btn btn-info" onclick="closeModal('overflightEditModal')">
Close Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
@@ -914,13 +914,13 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>Table Information</h2> <h2>Table Information</h2>
<button class="close" onclick="closeTableHelp()">&times;</button> <button class="close" onclick="closeModal('tableHelpModal')">&times;</button>
</div> </div>
<div class="modal-body" id="tableHelpContent"> <div class="modal-body" id="tableHelpContent">
<!-- Content will be populated by JavaScript --> <!-- Content will be populated by JavaScript -->
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-info" onclick="closeTableHelp()">Close</button> <button class="btn btn-info" onclick="closeModal('tableHelpModal')">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -930,7 +930,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>User Management</h2> <h2>User Management</h2>
<button class="close" onclick="closeUserManagementModal()">&times;</button> <button class="close" onclick="closeModal('userManagementModal')">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="quick-actions" style="margin-bottom: 1rem;"> <div class="quick-actions" style="margin-bottom: 1rem;">
@@ -1115,6 +1115,58 @@
let etdManuallyEdited = false; // Track if user has manually edited ETD let etdManuallyEdited = false; // Track if user has manually edited ETD
let loadPPRsTimeout = null; // Debounce timer for loadPPRs to prevent duplicate refreshes 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;
// 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 // WebSocket connection for real-time updates
function connectWebSocket() { function connectWebSocket() {
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) { if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
@@ -1130,11 +1182,9 @@
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/tower-updates`; const wsUrl = `${protocol}//${window.location.host}/ws/tower-updates`;
console.log('Connecting to WebSocket:', wsUrl);
wsConnection = new WebSocket(wsUrl); wsConnection = new WebSocket(wsUrl);
wsConnection.onopen = function(event) { wsConnection.onopen = function(event) {
console.log('✅ WebSocket connected for real-time updates');
lastHeartbeatResponse = Date.now(); lastHeartbeatResponse = Date.now();
startHeartbeat(); startHeartbeat();
showNotification('Real-time updates connected'); showNotification('Real-time updates connected');
@@ -1145,37 +1195,31 @@
// Check if it's a heartbeat response // Check if it's a heartbeat response
if (event.data.startsWith('Heartbeat:')) { if (event.data.startsWith('Heartbeat:')) {
lastHeartbeatResponse = Date.now(); lastHeartbeatResponse = Date.now();
console.log('💓 WebSocket heartbeat received');
return; return;
} }
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
// Refresh PPRs when any PPR-related event occurs // Refresh PPRs when any PPR-related event occurs
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) { if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) {
console.log('PPR update detected, refreshing...');
loadPPRs(); loadPPRs();
showNotification('Data updated'); showNotification('Data updated');
} }
// Refresh local flights when any local flight event occurs // Refresh local flights when any local flight event occurs
if (data.type && (data.type.includes('local_flight_'))) { if (data.type && (data.type.includes('local_flight_'))) {
console.log('Local flight update detected, refreshing...');
loadPPRs(); loadPPRs();
showNotification('Local flight updated'); showNotification('Local flight updated');
} }
// Refresh departures when any departure event occurs // Refresh departures when any departure event occurs
if (data.type && (data.type.includes('departure_'))) { if (data.type && (data.type.includes('departure_'))) {
console.log('Departure update detected, refreshing...');
loadDepartures(); loadDepartures();
showNotification('Departure updated'); showNotification('Departure updated');
} }
// Refresh arrivals when any arrival event occurs // Refresh arrivals when any arrival event occurs
if (data.type && (data.type.includes('arrival_'))) { if (data.type && (data.type.includes('arrival_'))) {
console.log('Arrival update detected, refreshing...');
loadArrivals(); loadArrivals();
showNotification('Arrival updated'); showNotification('Arrival updated');
} }
@@ -1185,14 +1229,12 @@
}; };
wsConnection.onclose = function(event) { wsConnection.onclose = function(event) {
console.log('⚠️ WebSocket disconnected', event.code, event.reason);
stopHeartbeat(); stopHeartbeat();
// Attempt to reconnect after 5 seconds if still logged in // Attempt to reconnect after 5 seconds if still logged in
if (accessToken) { if (accessToken) {
showNotification('Real-time updates disconnected, reconnecting...', true); showNotification('Real-time updates disconnected, reconnecting...', true);
wsReconnectTimeout = setTimeout(() => { wsReconnectTimeout = setTimeout(() => {
console.log('Attempting to reconnect WebSocket...');
connectWebSocket(); connectWebSocket();
}, 5000); }, 5000);
} }
@@ -1211,7 +1253,6 @@
wsHeartbeatInterval = setInterval(() => { wsHeartbeatInterval = setInterval(() => {
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) { if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
wsConnection.send('ping'); wsConnection.send('ping');
console.log('💓 Sending WebSocket heartbeat');
// Check if last heartbeat was more than 60 seconds ago // Check if last heartbeat was more than 60 seconds ago
if (lastHeartbeatResponse && (Date.now() - lastHeartbeatResponse > 60000)) { if (lastHeartbeatResponse && (Date.now() - lastHeartbeatResponse > 60000)) {
@@ -1388,21 +1429,21 @@
// Press 'Escape' to close Book Out modal if it's open (allow even when typing in inputs) // 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') { if (e.key === 'Escape' && document.getElementById('localFlightModal').style.display === 'block') {
e.preventDefault(); e.preventDefault();
closeLocalFlightModal(); closeModal('localFlightModal');
return; return;
} }
// Press 'Escape' to close Book In modal if it's open (allow even when typing in inputs) // 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') { if (e.key === 'Escape' && document.getElementById('bookInModal').style.display === 'block') {
e.preventDefault(); e.preventDefault();
closeBookInModal(); closeModal('bookInModal');
return; return;
} }
// Press 'Escape' to close Overflight modal if it's open (allow even when typing in inputs) // 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') { if (e.key === 'Escape' && document.getElementById('overflightModal').style.display === 'block') {
e.preventDefault(); e.preventDefault();
closeOverflightModal(); closeModal('overflightModal');
return; return;
} }
@@ -1430,7 +1471,7 @@
// Press 'Escape' to close overflight edit modal if it's open (allow even when typing in inputs) // 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') { if (e.key === 'Escape' && document.getElementById('overflightEditModal').style.display === 'block') {
e.preventDefault(); e.preventDefault();
closeOverflightEditModal(); closeModal('overflightEditModal');
return; return;
} }
@@ -2714,11 +2755,9 @@
} }
function populateForm(ppr) { function populateForm(ppr) {
console.log('populateForm called with:', ppr);
Object.keys(ppr).forEach(key => { Object.keys(ppr).forEach(key => {
if (key === 'eta' || key === 'etd') { if (key === 'eta' || key === 'etd') {
if (ppr[key]) { if (ppr[key]) {
console.log(`Processing ${key}:`, ppr[key]);
// ppr[key] is UTC datetime string from API (naive, assume UTC) // ppr[key] is UTC datetime string from API (naive, assume UTC)
let utcDateStr = ppr[key]; let utcDateStr = ppr[key];
if (!utcDateStr.includes('T')) { if (!utcDateStr.includes('T')) {
@@ -2728,7 +2767,6 @@
utcDateStr += 'Z'; utcDateStr += 'Z';
} }
const date = new Date(utcDateStr); // Now correctly parsed as UTC const date = new Date(utcDateStr); // Now correctly parsed as UTC
console.log(`Parsed date for ${key}:`, date);
// Split into date and time components for separate inputs // Split into date and time components for separate inputs
const dateField = document.getElementById(`${key}-date`); const dateField = document.getElementById(`${key}-date`);
@@ -2741,7 +2779,6 @@
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');
const dateValue = `${year}-${month}-${day}`; const dateValue = `${year}-${month}-${day}`;
dateField.value = dateValue; dateField.value = dateValue;
console.log(`Set ${key}-date to:`, dateValue);
// Format time (round to nearest 15-minute interval) // Format time (round to nearest 15-minute interval)
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0');
@@ -2750,19 +2787,12 @@
const minutes = String(roundedMinutes).padStart(2, '0'); const minutes = String(roundedMinutes).padStart(2, '0');
const timeValue = `${hours}:${minutes}`; const timeValue = `${hours}:${minutes}`;
timeField.value = timeValue; timeField.value = timeValue;
console.log(`Set ${key}-time to:`, timeValue, `(from ${rawMinutes} minutes)`);
} else {
console.log(`Date/time fields not found for ${key}: dateField=${dateField}, timeField=${timeField}`);
} }
} else {
console.log(`${key} is empty`);
} }
} else { } else {
const field = document.getElementById(key); const field = document.getElementById(key);
if (field) { if (field) {
field.value = ppr[key] || ''; field.value = ppr[key] || '';
} else {
console.log(`Field not found for key: ${key}`);
} }
} }
}); });
@@ -2838,9 +2868,10 @@
} }
function closePPRModal() { function closePPRModal() {
document.getElementById('pprModal').style.display = 'none'; closeModal('pprModal', () => {
currentPPRId = null; currentPPRId = null;
isNewPPR = false; isNewPPR = false;
});
} }
// Timestamp modal functions // Timestamp modal functions
@@ -3181,10 +3212,6 @@
} }
// User Management Functions // User Management Functions
let currentUserRole = null;
let isNewUser = false;
let currentUserId = null;
async function openUserManagementModal() { async function openUserManagementModal() {
if (!accessToken) return; if (!accessToken) return;
@@ -3192,10 +3219,6 @@
await loadUsers(); await loadUsers();
} }
function closeUserManagementModal() {
document.getElementById('userManagementModal').style.display = 'none';
}
async function loadUsers() { async function loadUsers() {
if (!accessToken) return; if (!accessToken) return;
@@ -3325,13 +3348,12 @@
} }
function closeUserModal() { function closeUserModal() {
document.getElementById('userModal').style.display = 'none'; closeModal('userModal', () => {
currentUserId = null; currentUserId = null;
isNewUser = false; isNewUser = false;
});
} }
let currentChangePasswordUserId = null;
function openChangePasswordModal(userId, username) { function openChangePasswordModal(userId, username) {
if (!accessToken) return; if (!accessToken) return;
@@ -3348,8 +3370,9 @@
} }
function closeChangePasswordModal() { function closeChangePasswordModal() {
document.getElementById('changePasswordModal').style.display = 'none'; closeModal('changePasswordModal', () => {
currentChangePasswordUserId = null; currentChangePasswordUserId = null;
});
} }
// Change password form submission // Change password form submission
@@ -3450,9 +3473,7 @@
// Update user role detection and UI visibility // Update user role detection and UI visibility
async function updateUserRole() { async function updateUserRole() {
console.log('updateUserRole called'); // Debug log
if (!accessToken) { if (!accessToken) {
console.log('No access token, skipping role update'); // Debug log
return; return;
} }
@@ -3464,16 +3485,13 @@
if (response.ok) { if (response.ok) {
const userData = await response.json(); const userData = await response.json();
currentUserRole = userData.role; currentUserRole = userData.role;
console.log('User role from API:', currentUserRole); // Debug log
// Show user management in dropdown only for administrators // Show user management in dropdown only for administrators
const userManagementDropdown = document.getElementById('user-management-dropdown'); const userManagementDropdown = document.getElementById('user-management-dropdown');
if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') { if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') {
userManagementDropdown.style.display = 'block'; userManagementDropdown.style.display = 'block';
console.log('Showing user management in dropdown'); // Debug log
} else { } else {
userManagementDropdown.style.display = 'none'; userManagementDropdown.style.display = 'none';
console.log('Hiding user management, current role:', currentUserRole); // Debug log
} }
} }
} catch (error) { } catch (error) {
@@ -3499,16 +3517,16 @@
closeTimestampModal(); closeTimestampModal();
} }
if (event.target === userManagementModal) { if (event.target === userManagementModal) {
closeUserManagementModal(); closeModal('userManagementModal');
} }
if (event.target === userModal) { if (event.target === userModal) {
closeUserModal(); closeUserModal();
} }
if (event.target === tableHelpModal) { if (event.target === tableHelpModal) {
closeTableHelp(); closeModal('tableHelpModal');
} }
if (event.target === bookInModal) { if (event.target === bookInModal) {
closeBookInModal(); closeModal('bookInModal');
} }
} }
@@ -3547,10 +3565,6 @@
}, 100); }, 100);
} }
function closeLocalFlightModal() {
document.getElementById('localFlightModal').style.display = 'none';
}
function openBookInModal() { function openBookInModal() {
document.getElementById('book-in-form').reset(); document.getElementById('book-in-form').reset();
document.getElementById('book-in-id').value = ''; document.getElementById('book-in-id').value = '';
@@ -3569,10 +3583,6 @@
}, 100); }, 100);
} }
function closeBookInModal() {
document.getElementById('bookInModal').style.display = 'none';
}
function openOverflightModal() { function openOverflightModal() {
document.getElementById('overflight-form').reset(); document.getElementById('overflight-form').reset();
document.getElementById('overflight-id').value = ''; document.getElementById('overflight-id').value = '';
@@ -3598,13 +3608,6 @@
}, 100); }, 100);
} }
function closeOverflightModal() {
document.getElementById('overflightModal').style.display = 'none';
}
let currentOverflightId = null;
let isOverflightQSYMode = false; // Track if we're in overflight QSY mode
async function openOverflightEditModal(overflightId) { async function openOverflightEditModal(overflightId) {
if (!accessToken) return; if (!accessToken) return;
@@ -3658,8 +3661,7 @@
} }
function closeOverflightEditModal() { function closeOverflightEditModal() {
document.getElementById('overflightEditModal').style.display = 'none'; closeModal('overflightEditModal');
currentOverflightId = null;
} }
async function updateOverflightStatus(newStatus, qsyTime = null) { async function updateOverflightStatus(newStatus, qsyTime = null) {
@@ -3941,9 +3943,6 @@
} }
// Local Flight Edit Modal Functions // Local Flight Edit Modal Functions
let currentLocalFlightId = null;
let currentBookedInArrivalId = null;
async function openLocalFlightEditModal(flightId) { async function openLocalFlightEditModal(flightId) {
if (!accessToken) return; if (!accessToken) return;
@@ -4092,8 +4091,9 @@
} }
function closeDepartureEditModal() { function closeDepartureEditModal() {
document.getElementById('departureEditModal').style.display = 'none'; closeModal('departureEditModal', () => {
currentDepartureId = null; currentDepartureId = null;
});
} }
// Departure edit form submission // Departure edit form submission
@@ -4150,8 +4150,6 @@
}); });
// Arrival Edit Modal Functions // Arrival Edit Modal Functions
let currentArrivalId = null;
async function openArrivalEditModal(arrivalId) { async function openArrivalEditModal(arrivalId) {
if (!accessToken) return; if (!accessToken) return;
@@ -4196,8 +4194,9 @@
} }
function closeArrivalEditModal() { function closeArrivalEditModal() {
document.getElementById('arrivalEditModal').style.display = 'none'; closeModal('arrivalEditModal', () => {
currentArrivalId = null; currentArrivalId = null;
});
} }
// Arrival edit form submission // Arrival edit form submission
@@ -4457,10 +4456,6 @@
modal.style.display = 'block'; modal.style.display = 'block';
} }
function closeTableHelp() {
document.getElementById('tableHelpModal').style.display = 'none';
}
// Local flight edit form submission // Local flight edit form submission
document.getElementById('local-flight-edit-form').addEventListener('submit', async function(e) { document.getElementById('local-flight-edit-form').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
@@ -4563,8 +4558,6 @@
delete flightData.flight_type; delete flightData.flight_type;
} }
console.log(`Submitting ${endpoint} data:`, flightData);
try { try {
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
@@ -4593,7 +4586,7 @@
} }
const result = await response.json(); const result = await response.json();
closeLocalFlightModal(); closeModal('localFlightModal');
loadPPRs(); // Refresh tables loadPPRs(); // Refresh tables
showNotification(`Aircraft ${result.registration} booked out successfully!`); showNotification(`Aircraft ${result.registration} booked out successfully!`);
} catch (error) { } catch (error) {
@@ -4643,8 +4636,6 @@
// Book In uses LANDED status (they're arriving now) // Book In uses LANDED status (they're arriving now)
arrivalData.status = 'LANDED'; arrivalData.status = 'LANDED';
console.log('Submitting arrivals data:', arrivalData);
try { try {
const response = await fetch('/api/v1/arrivals/', { const response = await fetch('/api/v1/arrivals/', {
method: 'POST', method: 'POST',
@@ -4673,7 +4664,7 @@
} }
const result = await response.json(); const result = await response.json();
closeBookInModal(); closeModal('bookInModal');
loadPPRs(); // Refresh tables loadPPRs(); // Refresh tables
showNotification(`Aircraft ${result.registration} booked in successfully!`); showNotification(`Aircraft ${result.registration} booked in successfully!`);
} catch (error) { } catch (error) {
@@ -4710,8 +4701,6 @@
} }
}); });
console.log('Submitting overflight data:', overflightData);
try { try {
const response = await fetch('/api/v1/overflights/', { const response = await fetch('/api/v1/overflights/', {
method: 'POST', method: 'POST',
@@ -4740,7 +4729,7 @@
} }
const result = await response.json(); const result = await response.json();
closeOverflightModal(); closeModal('overflightModal');
loadPPRs(); loadPPRs();
showNotification(`Overflight ${result.registration} registered successfully!`); showNotification(`Overflight ${result.registration} registered successfully!`);
} catch (error) { } catch (error) {
@@ -4789,6 +4778,7 @@
// Initialize the page when DOM is loaded // Initialize the page when DOM is loaded
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadUIConfig(); // Load UI configuration first
setupLoginForm(); setupLoginForm();
setupKeyboardShortcuts(); setupKeyboardShortcuts();
initializeTimeDropdowns(); // Initialize time dropdowns initializeTimeDropdowns(); // Initialize time dropdowns
@@ -4802,6 +4792,7 @@
document.getElementById('etd-date').addEventListener('change', markETDAsManuallyEdited); document.getElementById('etd-date').addEventListener('change', markETDAsManuallyEdited);
document.getElementById('etd-time').addEventListener('change', markETDAsManuallyEdited); document.getElementById('etd-time').addEventListener('change', markETDAsManuallyEdited);
}); });
</script> </script>
<!-- Footer Bar --> <!-- Footer Bar -->

View File

@@ -367,7 +367,7 @@
</button> </button>
</div> </div>
<div class="title"> <div class="title">
<h1>📊 PPR Reports</h1> <h1 id="tower-title">📊 PPR Reports</h1>
</div> </div>
<div class="user-info"> <div class="user-info">
Logged in as: <span id="current-user">Loading...</span> | Logged in as: <span id="current-user">Loading...</span> |
@@ -591,8 +591,49 @@
let currentPPRs = []; // Store current results for export let currentPPRs = []; // Store current results for export
let currentOtherFlights = []; // Store other flights for export let currentOtherFlights = []; // Store other flights for export
// 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 = `📊 Reports ${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 page title
if (config.tag) {
document.title = `PPR Reports - ${config.tag}`;
}
// 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;
}
if (document.title) {
document.title += envIndicator;
}
}
}
} catch (error) {
console.warn('Failed to load UI config:', error);
}
}
// Initialize the page // Initialize the page
async function initializePage() { async function initializePage() {
loadUIConfig(); // Load UI configuration first
await initializeAuth(); await initializeAuth();
setupDefaultDateRange(); setupDefaultDateRange();
await loadReports(); await loadReports();