Compare commits
2 Commits
11f7390694
...
86f1dc65f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86f1dc65f4 | ||
|
|
169c3af29b |
@@ -55,7 +55,8 @@ class CRUDPPR:
|
|||||||
func.date(PPRRecord.eta) == today,
|
func.date(PPRRecord.eta) == today,
|
||||||
or_(
|
or_(
|
||||||
PPRRecord.status == PPRStatus.NEW,
|
PPRRecord.status == PPRStatus.NEW,
|
||||||
PPRRecord.status == PPRStatus.CONFIRMED
|
PPRRecord.status == PPRStatus.CONFIRMED,
|
||||||
|
PPRRecord.status == PPRStatus.LANDED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).order_by(PPRRecord.eta).all()
|
).order_by(PPRRecord.eta).all()
|
||||||
@@ -66,7 +67,10 @@ class CRUDPPR:
|
|||||||
return db.query(PPRRecord).filter(
|
return db.query(PPRRecord).filter(
|
||||||
and_(
|
and_(
|
||||||
func.date(PPRRecord.etd) == today,
|
func.date(PPRRecord.etd) == today,
|
||||||
PPRRecord.status == PPRStatus.LANDED
|
or_(
|
||||||
|
PPRRecord.status == PPRStatus.LANDED,
|
||||||
|
PPRRecord.status == PPRStatus.DEPARTED
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).order_by(PPRRecord.etd).all()
|
).order_by(PPRRecord.etd).all()
|
||||||
|
|
||||||
|
|||||||
185
web/admin.html
185
web/admin.html
@@ -25,6 +25,8 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title h1 {
|
.title h1 {
|
||||||
@@ -541,11 +543,13 @@
|
|||||||
transform: translateY(-20px);
|
transform: translateY(-20px);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification.show {
|
.notification.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification.error {
|
.notification.error {
|
||||||
@@ -927,6 +931,12 @@
|
|||||||
let isNewPPR = false;
|
let isNewPPR = false;
|
||||||
let wsConnection = null;
|
let wsConnection = null;
|
||||||
let pendingStatusUpdate = null; // Track pending status update for timestamp modal
|
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
|
||||||
|
|
||||||
// WebSocket connection for real-time updates
|
// WebSocket connection for real-time updates
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
@@ -934,17 +944,34 @@
|
|||||||
return; // Already connected
|
return; // Already connected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any existing reconnect timeout
|
||||||
|
if (wsReconnectTimeout) {
|
||||||
|
clearTimeout(wsReconnectTimeout);
|
||||||
|
wsReconnectTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
console.log('✅ WebSocket connected for real-time updates');
|
||||||
|
lastHeartbeatResponse = Date.now();
|
||||||
|
startHeartbeat();
|
||||||
|
showNotification('Real-time updates connected');
|
||||||
};
|
};
|
||||||
|
|
||||||
wsConnection.onmessage = function(event) {
|
wsConnection.onmessage = function(event) {
|
||||||
try {
|
try {
|
||||||
|
// Check if it's a heartbeat response
|
||||||
|
if (event.data.startsWith('Heartbeat:')) {
|
||||||
|
lastHeartbeatResponse = Date.now();
|
||||||
|
console.log('💓 WebSocket heartbeat received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log('WebSocket message received:', data);
|
console.log('WebSocket message received:', data);
|
||||||
|
|
||||||
@@ -952,6 +979,7 @@
|
|||||||
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...');
|
console.log('PPR update detected, refreshing...');
|
||||||
loadPPRs();
|
loadPPRs();
|
||||||
|
showNotification('Data updated');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing WebSocket message:', error);
|
console.error('Error parsing WebSocket message:', error);
|
||||||
@@ -959,22 +987,59 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
wsConnection.onclose = function(event) {
|
wsConnection.onclose = function(event) {
|
||||||
console.log('WebSocket disconnected');
|
console.log('⚠️ WebSocket disconnected', event.code, event.reason);
|
||||||
// Attempt to reconnect after 5 seconds
|
stopHeartbeat();
|
||||||
setTimeout(() => {
|
|
||||||
if (accessToken) { // Only reconnect if still logged in
|
// Attempt to reconnect after 5 seconds if still logged in
|
||||||
|
if (accessToken) {
|
||||||
|
showNotification('Real-time updates disconnected, reconnecting...', true);
|
||||||
|
wsReconnectTimeout = setTimeout(() => {
|
||||||
console.log('Attempting to reconnect WebSocket...');
|
console.log('Attempting to reconnect WebSocket...');
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}, 5000);
|
||||||
}, 5000);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
wsConnection.onerror = function(error) {
|
wsConnection.onerror = function(error) {
|
||||||
console.error('WebSocket error:', 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');
|
||||||
|
console.log('💓 Sending WebSocket heartbeat');
|
||||||
|
|
||||||
|
// 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() {
|
function disconnectWebSocket() {
|
||||||
|
stopHeartbeat();
|
||||||
|
if (wsReconnectTimeout) {
|
||||||
|
clearTimeout(wsReconnectTimeout);
|
||||||
|
wsReconnectTimeout = null;
|
||||||
|
}
|
||||||
if (wsConnection) {
|
if (wsConnection) {
|
||||||
wsConnection.close();
|
wsConnection.close();
|
||||||
wsConnection = null;
|
wsConnection = null;
|
||||||
@@ -1020,6 +1085,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Authentication management
|
||||||
async function initializeAuth() {
|
async function initializeAuth() {
|
||||||
// Try to get cached token
|
// Try to get cached token
|
||||||
@@ -1035,6 +1146,7 @@
|
|||||||
currentUser = cachedUser;
|
currentUser = cachedUser;
|
||||||
document.getElementById('current-user').textContent = cachedUser;
|
document.getElementById('current-user').textContent = cachedUser;
|
||||||
await updateUserRole(); // Update role-based UI
|
await updateUserRole(); // Update role-based UI
|
||||||
|
startSessionExpiryCheck(); // Start monitoring session expiry
|
||||||
connectWebSocket(); // Connect WebSocket for real-time updates
|
connectWebSocket(); // Connect WebSocket for real-time updates
|
||||||
loadPPRs();
|
loadPPRs();
|
||||||
return;
|
return;
|
||||||
@@ -1128,6 +1240,7 @@
|
|||||||
|
|
||||||
hideLogin();
|
hideLogin();
|
||||||
await updateUserRole(); // Update role-based UI
|
await updateUserRole(); // Update role-based UI
|
||||||
|
startSessionExpiryCheck(); // Start monitoring session expiry
|
||||||
connectWebSocket(); // Connect WebSocket for real-time updates
|
connectWebSocket(); // Connect WebSocket for real-time updates
|
||||||
loadPPRs();
|
loadPPRs();
|
||||||
} else {
|
} else {
|
||||||
@@ -1153,6 +1266,7 @@
|
|||||||
accessToken = null;
|
accessToken = null;
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
|
|
||||||
|
stopSessionExpiryCheck(); // Stop monitoring session
|
||||||
disconnectWebSocket(); // Disconnect WebSocket
|
disconnectWebSocket(); // Disconnect WebSocket
|
||||||
|
|
||||||
// Close any open modals
|
// Close any open modals
|
||||||
@@ -1298,6 +1412,13 @@
|
|||||||
document.getElementById('arrivals-no-data').style.display = 'block';
|
document.getElementById('arrivals-no-data').style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort arrivals by ETA (ascending)
|
||||||
|
arrivals.sort((a, b) => {
|
||||||
|
if (!a.eta) return 1;
|
||||||
|
if (!b.eta) return -1;
|
||||||
|
return new Date(a.eta) - new Date(b.eta);
|
||||||
|
});
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('arrivals-table-content').style.display = 'block';
|
document.getElementById('arrivals-table-content').style.display = 'block';
|
||||||
for (const ppr of arrivals) {
|
for (const ppr of arrivals) {
|
||||||
@@ -1346,6 +1467,13 @@
|
|||||||
document.getElementById('departures-no-data').style.display = 'block';
|
document.getElementById('departures-no-data').style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort departures by ETD (ascending), nulls last
|
||||||
|
departures.sort((a, b) => {
|
||||||
|
if (!a.etd) return 1;
|
||||||
|
if (!b.etd) return -1;
|
||||||
|
return new Date(a.etd) - new Date(b.etd);
|
||||||
|
});
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('departures-table-content').style.display = 'block';
|
document.getElementById('departures-table-content').style.display = 'block';
|
||||||
for (const ppr of departures) {
|
for (const ppr of departures) {
|
||||||
@@ -1419,6 +1547,7 @@
|
|||||||
function openNewPPRModal() {
|
function openNewPPRModal() {
|
||||||
isNewPPR = true;
|
isNewPPR = true;
|
||||||
currentPPRId = null;
|
currentPPRId = null;
|
||||||
|
etdManuallyEdited = false; // Reset the manual edit flag for new PPR
|
||||||
document.getElementById('modal-title').textContent = 'New PPR Entry';
|
document.getElementById('modal-title').textContent = 'New PPR Entry';
|
||||||
document.getElementById('delete-btn').style.display = 'none';
|
document.getElementById('delete-btn').style.display = 'none';
|
||||||
document.getElementById('journal-section').style.display = 'none';
|
document.getElementById('journal-section').style.display = 'none';
|
||||||
@@ -1465,6 +1594,38 @@
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to update ETD based on ETA (2 hours later)
|
||||||
|
function updateETDFromETA() {
|
||||||
|
// Only auto-update if user hasn't manually edited ETD
|
||||||
|
if (etdManuallyEdited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const etaDate = document.getElementById('eta-date').value;
|
||||||
|
const etaTime = document.getElementById('eta-time').value;
|
||||||
|
|
||||||
|
if (etaDate && etaTime) {
|
||||||
|
// Parse ETA
|
||||||
|
const eta = new Date(`${etaDate}T${etaTime}`);
|
||||||
|
|
||||||
|
// Calculate ETD (2 hours after ETA)
|
||||||
|
const etd = new Date(eta.getTime() + 2 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Format ETD
|
||||||
|
const etdDateStr = `${etd.getFullYear()}-${String(etd.getMonth() + 1).padStart(2, '0')}-${String(etd.getDate()).padStart(2, '0')}`;
|
||||||
|
const etdTimeStr = `${String(etd.getHours()).padStart(2, '0')}:${String(etd.getMinutes()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
// Update ETD fields
|
||||||
|
document.getElementById('etd-date').value = etdDateStr;
|
||||||
|
document.getElementById('etd-time').value = etdTimeStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to mark ETD as manually edited
|
||||||
|
function markETDAsManuallyEdited() {
|
||||||
|
etdManuallyEdited = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function openPPRModal(pprId) {
|
async function openPPRModal(pprId) {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
@@ -2386,6 +2547,14 @@
|
|||||||
setupKeyboardShortcuts();
|
setupKeyboardShortcuts();
|
||||||
initializeTimeDropdowns(); // Initialize time dropdowns
|
initializeTimeDropdowns(); // Initialize time dropdowns
|
||||||
initializeAuth(); // Start authentication process
|
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);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
161
web/index.html
161
web/index.html
@@ -62,6 +62,9 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@@ -134,7 +137,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<img src="assets/logo.png" alt="EGFH Logo" class="left-image">
|
<img src="assets/logo.png" alt="EGFH Logo" class="left-image">
|
||||||
<h1>Arrivals/Departures Information</h1>
|
<h1>Flight Information</h1>
|
||||||
<img src="assets/flightImg.png" alt="EGFH Logo" class="right-image">
|
<img src="assets/flightImg.png" alt="EGFH Logo" class="right-image">
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -145,15 +148,14 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Registration</th>
|
<th style="width: 25%;">Aircraft</th>
|
||||||
<th>Aircraft Type</th>
|
<th style="width: 60%;">From</th>
|
||||||
<th>From</th>
|
<th style="width: 15%;">Due</th>
|
||||||
<th>Due</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="arrivals-tbody">
|
<tbody id="arrivals-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="loading">Loading arrivals...</td>
|
<td colspan="3" class="loading">Loading arrivals...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -165,15 +167,14 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Registration</th>
|
<th style="width: 25%;">Aircraft</th>
|
||||||
<th>Aircraft Type</th>
|
<th style="width: 60%;">To</th>
|
||||||
<th>To</th>
|
<th style="width: 15%;">Due</th>
|
||||||
<th>Due</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="departures-tbody">
|
<tbody id="departures-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="loading">Loading departures...</td>
|
<td colspan="3" class="loading">Loading departures...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -191,6 +192,27 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let wsConnection = null;
|
let wsConnection = null;
|
||||||
|
|
||||||
|
// ICAO code to airport name cache
|
||||||
|
const airportNameCache = {};
|
||||||
|
|
||||||
|
async function getAirportName(code) {
|
||||||
|
if (!code || code.length !== 4 || !/^[A-Z]{4}$/.test(code)) return code;
|
||||||
|
if (airportNameCache[code]) return airportNameCache[code];
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/v1/airport/public/lookup/${code}`);
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data && data.length && data[0].name) {
|
||||||
|
airportNameCache[code] = data[0].name;
|
||||||
|
return data[0].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error looking up airport:', error);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket connection for real-time updates
|
// WebSocket connection for real-time updates
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
@@ -237,30 +259,29 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert UTC time to local time (Europe/London)
|
// Convert UTC datetime to local time display
|
||||||
function convertToLocalTime(utcTimeString) {
|
function convertToLocalTime(utcDateTimeString) {
|
||||||
if (!utcTimeString) return '';
|
if (!utcDateTimeString) return '';
|
||||||
|
|
||||||
// Parse the time string (format: HH:MM or HH:MM:SS)
|
try {
|
||||||
const timeParts = utcTimeString.split(':');
|
// Parse the ISO datetime string
|
||||||
if (timeParts.length < 2) return utcTimeString;
|
const date = new Date(utcDateTimeString);
|
||||||
|
|
||||||
// Create a date object with today's date and the UTC time
|
// Check if valid date
|
||||||
const now = new Date();
|
if (isNaN(date.getTime())) {
|
||||||
const utcDate = new Date(Date.UTC(
|
console.error('Invalid date:', utcDateTimeString);
|
||||||
now.getUTCFullYear(),
|
return utcDateTimeString;
|
||||||
now.getUTCMonth(),
|
}
|
||||||
now.getUTCDate(),
|
|
||||||
parseInt(timeParts[0]),
|
// Format as HH:MM in local time
|
||||||
parseInt(timeParts[1]),
|
const localHours = date.getHours().toString().padStart(2, '0');
|
||||||
timeParts.length > 2 ? parseInt(timeParts[2]) : 0
|
const localMinutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
));
|
|
||||||
|
return `${localHours}:${localMinutes}`;
|
||||||
// Convert to local time
|
} catch (error) {
|
||||||
const localHours = utcDate.getHours().toString().padStart(2, '0');
|
console.error('Error converting time:', error, utcDateTimeString);
|
||||||
const localMinutes = utcDate.getMinutes().toString().padStart(2, '0');
|
return utcDateTimeString;
|
||||||
|
}
|
||||||
return `${localHours}:${localMinutes}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch and display arrivals
|
// Fetch and display arrivals
|
||||||
@@ -277,22 +298,38 @@
|
|||||||
const arrivals = await response.json();
|
const arrivals = await response.json();
|
||||||
|
|
||||||
if (arrivals.length === 0) {
|
if (arrivals.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4">No arrivals found.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="3">No arrivals found.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = arrivals.map(arrival => `
|
// Build rows asynchronously to lookup airport names
|
||||||
<tr>
|
const rows = await Promise.all(arrivals.map(async (arrival) => {
|
||||||
<td>${escapeHtml(arrival.ac_reg || '')}</td>
|
const aircraftDisplay = `${escapeHtml(arrival.ac_reg || '')} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(arrival.ac_type || '')})</span>`;
|
||||||
<td>${escapeHtml(arrival.ac_type || '')}</td>
|
const fromDisplay = await getAirportName(arrival.in_from || '');
|
||||||
<td>${escapeHtml(arrival.in_from || '')}</td>
|
|
||||||
<td>${convertToLocalTime(arrival.eta)}</td>
|
// Show landed time if available, otherwise ETA
|
||||||
</tr>
|
let timeDisplay;
|
||||||
`).join('');
|
if (arrival.status === 'LANDED' && arrival.landed_dt) {
|
||||||
|
const time = convertToLocalTime(arrival.landed_dt);
|
||||||
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`;
|
||||||
|
} else {
|
||||||
|
timeDisplay = convertToLocalTime(arrival.eta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${aircraftDisplay}</td>
|
||||||
|
<td>${escapeHtml(fromDisplay)}</td>
|
||||||
|
<td>${timeDisplay}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
tbody.innerHTML = rows.join('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading arrivals:', error);
|
console.error('Error loading arrivals:', error);
|
||||||
tbody.innerHTML = '<tr><td colspan="4" class="error">Error loading arrivals</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="3" class="error">Error loading arrivals</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,22 +347,38 @@
|
|||||||
const departures = await response.json();
|
const departures = await response.json();
|
||||||
|
|
||||||
if (departures.length === 0) {
|
if (departures.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4">No departures found.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="3">No departures found.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = departures.map(departure => `
|
// Build rows asynchronously to lookup airport names
|
||||||
<tr>
|
const rows = await Promise.all(departures.map(async (departure) => {
|
||||||
<td>${escapeHtml(departure.ac_reg || '')}</td>
|
const aircraftDisplay = `${escapeHtml(departure.ac_reg || '')} <span style="font-size: 0.8em; color: #666;">(${escapeHtml(departure.ac_type || '')})</span>`;
|
||||||
<td>${escapeHtml(departure.ac_type || '')}</td>
|
const toDisplay = await getAirportName(departure.out_to || '');
|
||||||
<td>${escapeHtml(departure.out_to || '')}</td>
|
|
||||||
<td>${convertToLocalTime(departure.etd)}</td>
|
// Show departed time if available, otherwise ETD
|
||||||
</tr>
|
let timeDisplay;
|
||||||
`).join('');
|
if (departure.status === 'DEPARTED' && departure.departed_dt) {
|
||||||
|
const time = convertToLocalTime(departure.departed_dt);
|
||||||
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">DEPARTED</span></div>`;
|
||||||
|
} else {
|
||||||
|
timeDisplay = convertToLocalTime(departure.etd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${aircraftDisplay}</td>
|
||||||
|
<td>${escapeHtml(toDisplay)}</td>
|
||||||
|
<td>${timeDisplay}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
tbody.innerHTML = rows.join('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading departures:', error);
|
console.error('Error loading departures:', error);
|
||||||
tbody.innerHTML = '<tr><td colspan="4" class="error">Error loading departures</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="3" class="error">Error loading departures</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user