Compare commits
2 Commits
63564b54dd
...
ac29b6e929
| Author | SHA1 | Date | |
|---|---|---|---|
| ac29b6e929 | |||
| 0149f45893 |
@@ -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=
|
||||||
|
|
||||||
|
|||||||
@@ -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)"""
|
||||||
@@ -200,4 +225,18 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
|||||||
'isDeparture': True
|
'isDeparture': True
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
197
web/admin.html
197
web/admin.html
@@ -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()">×</button>
|
<button class="close" onclick="closeModal('localFlightModal')">×</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()">×</button>
|
<button class="close" onclick="closeModal('bookInModal')">×</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()">×</button>
|
<button class="close" onclick="closeModal('overflightModal')">×</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()">×</button>
|
<button class="close" onclick="closeModal('overflightEditModal')">×</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()">×</button>
|
<button class="close" onclick="closeModal('tableHelpModal')">×</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()">×</button>
|
<button class="close" onclick="closeModal('userManagementModal')">×</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;">
|
||||||
@@ -1114,6 +1114,58 @@
|
|||||||
let sessionExpiryCheckInterval = null;
|
let sessionExpiryCheckInterval = null;
|
||||||
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() {
|
||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user