1580 lines
81 KiB
HTML
1580 lines
81 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PPR Admin Interface</title>
|
||
<link rel="stylesheet" href="admin.css">
|
||
<script src="lookups.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="top-bar">
|
||
<div class="title">
|
||
<h1 id="tower-title">✈️ Swansea Tower</h1>
|
||
</div>
|
||
<div class="menu-buttons">
|
||
<div class="dropdown">
|
||
<button class="btn btn-success dropdown-toggle" id="actionsDropdownBtn">
|
||
📋 Actions
|
||
</button>
|
||
<div class="dropdown-menu" id="actionsDropdownMenu">
|
||
<a href="#" onclick="openNewPPRModal(); closeActionsDropdown()">➕ New PPR <span class="shortcut">(N)</span></a>
|
||
<a href="#" onclick="openLocalFlightModal('LOCAL'); closeActionsDropdown()">🛫 Book Out <span class="shortcut">(L)</span></a>
|
||
<a href="#" onclick="openBookInModal(); closeActionsDropdown()">🛬 Book In <span class="shortcut">(I)</span></a>
|
||
<a href="#" onclick="openOverflightModal(); closeActionsDropdown()">🔄 Overflight <span class="shortcut">(O)</span></a>
|
||
</div>
|
||
</div>
|
||
<div class="dropdown">
|
||
<button class="btn btn-warning dropdown-toggle" id="adminDropdownBtn">
|
||
⚙️ Admin
|
||
</button>
|
||
<div class="dropdown-menu" id="adminDropdownMenu">
|
||
<a href="#" onclick="window.location.href = '/atc'">🎛️ ATC View</a>
|
||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
|
||
<a href="#" onclick="window.location.href = '/movements'">📈 Movements</a>
|
||
<a href="#" onclick="window.location.href = '/drone-requests'">Drone Requests</a>
|
||
<a href="#" onclick="window.location.href = '/bulk-log'">🧾 Bulk Flight Log</a>
|
||
<a href="#" onclick="window.location.href = '/journal'">📔 Journal Log</a>
|
||
<a href="#" onclick="openUserAircraftModal(); closeAdminDropdown()" id="user-aircraft-dropdown">✈️ User Aircraft</a>
|
||
<a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="user-info">
|
||
Logged in as: <span id="current-user">Loading...</span> |
|
||
<a href="#" onclick="logout()" style="color: white;">Logout</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
|
||
<!-- Local Flights Table -->
|
||
<div class="ppr-table">
|
||
<div class="table-header">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span>🛩️ Today's Local Flights - <span id="local-flights-count">0</span></span>
|
||
<span class="info-icon" onclick="showTableHelp('local-flights')" title="What is this?">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="local-flights-loading" class="loading">
|
||
<div class="spinner"></div>
|
||
Loading local flights...
|
||
</div>
|
||
|
||
<div id="local-flights-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Registration</th>
|
||
<th style="width: 30px; text-align: center;"></th>
|
||
<th>Type</th>
|
||
<th>Flight</th>
|
||
<th>ETD</th>
|
||
<th>POB</th>
|
||
<th>Status</th>
|
||
<th>Circuits</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="local-flights-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="local-flights-no-data" class="no-data" style="display: none;">
|
||
<h3>No Local Flights</h3>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Arrivals Table -->
|
||
<div class="ppr-table" style="margin-top: 2rem;">
|
||
<div class="table-header">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span>🛬 Today's Pending Arrivals - <span id="arrivals-count">0</span></span>
|
||
<span class="info-icon" onclick="showTableHelp('arrivals')" title="What is this?">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="arrivals-loading" class="loading">
|
||
<div class="spinner"></div>
|
||
Loading arrivals...
|
||
</div>
|
||
|
||
<div id="arrivals-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Registration</th>
|
||
<th style="width: 30px; text-align: center;"></th>
|
||
<th>Type</th>
|
||
<th>From</th>
|
||
<th>ETA</th>
|
||
<th>POB</th>
|
||
<th>Fuel</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="arrivals-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="arrivals-no-data" class="no-data" style="display: none;">
|
||
<h3>No Pending Arrivals</h3>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Departures Table -->
|
||
<div class="ppr-table" style="margin-top: 2rem;">
|
||
<div class="table-header">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span>🛫 Today's Pending Departures - <span id="departures-count">0</span></span>
|
||
<span class="info-icon" onclick="showTableHelp('departures')" title="What is this?">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="departures-loading" class="loading">
|
||
<div class="spinner"></div>
|
||
Loading departures...
|
||
</div>
|
||
|
||
<div id="departures-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Registration</th>
|
||
<th style="width: 30px; text-align: center;"></th>
|
||
<th>Type</th>
|
||
<th>To</th>
|
||
<th>ETD</th>
|
||
<th>POB</th>
|
||
<th>Fuel</th>
|
||
<th>Landed</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="departures-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="departures-no-data" class="no-data" style="display: none;">
|
||
<h3>No Pending Departures</h3>
|
||
<p>No aircraft currently landed and ready to depart.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Overflights Table -->
|
||
<div class="ppr-table" style="margin-top: 2rem;">
|
||
<div class="table-header">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span>🔄 Active Overflights - <span id="overflights-count">0</span></span>
|
||
<span class="info-icon" onclick="showTableHelp('overflights')" title="What is this?">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="overflights-loading" class="loading">
|
||
<div class="spinner"></div>
|
||
Loading overflights...
|
||
</div>
|
||
|
||
<div id="overflights-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Registration</th>
|
||
<th style="width: 30px; text-align: center;"></th>
|
||
<th>Callsign</th>
|
||
<th>Type</th>
|
||
<th>From</th>
|
||
<th>To</th>
|
||
<th>Called</th>
|
||
<th>POB</th>
|
||
<th>Status</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="overflights-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="overflights-no-data" class="no-data" style="display: none;">
|
||
<h3>No Active Overflights</h3>
|
||
</div>
|
||
</div>
|
||
<br>
|
||
<!-- Departed and Parked Tables -->
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem;">
|
||
<!-- Departed Today -->
|
||
<div class="ppr-table">
|
||
<div class="table-header" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span>✈️ Departed Today - <span id="departed-count">0</span></span>
|
||
<span class="info-icon" onclick="showTableHelp('departed')" title="What is this?" style="font-size: 1.1rem; cursor: pointer;">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="departed-loading" class="loading" style="display: none;">
|
||
<div class="spinner"></div>
|
||
Loading departed aircraft...
|
||
</div>
|
||
|
||
<div id="departed-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr style="font-size: 0.85rem !important;">
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Registration</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; width: 30px; text-align: center;"></th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Callsign</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Destination</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Departed</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="departed-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="departed-no-data" class="no-data" style="display: none; padding: 1rem; font-size: 0.9rem;">
|
||
<p>No departures today.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Parked Visitors -->
|
||
<div class="ppr-table">
|
||
<div class="table-header" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span>🅿️ Parked Visitors - <span id="parked-count">0</span></span>
|
||
<span class="info-icon" onclick="showTableHelp('parked')" title="What is this?" style="font-size: 1.1rem; cursor: pointer;">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="parked-loading" class="loading" style="display: none;">
|
||
<div class="spinner"></div>
|
||
Loading parked visitors...
|
||
</div>
|
||
|
||
<div id="parked-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr style="font-size: 0.85rem !important;">
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Registration</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; width: 30px; text-align: center;"></th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Type</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">From</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Arrived</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">ETD</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="parked-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="parked-no-data" class="no-data" style="display: none; padding: 1rem; font-size: 0.9rem;">
|
||
<p>No parked visitors.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Upcoming PPRs (Future Days) -->
|
||
<div class="ppr-table" style="margin-top: 1rem;">
|
||
<div class="table-header-collapsible" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;" onclick="toggleUpcomingTable()">
|
||
<span>📅 Future PPRs - <span id="upcoming-count">0</span></span>
|
||
<span class="collapse-icon collapsed" id="upcoming-collapse-icon">▼</span>
|
||
</div>
|
||
|
||
<div id="upcoming-loading" class="loading" style="display: none;">
|
||
<div class="spinner"></div>
|
||
Loading upcoming PPRs...
|
||
</div>
|
||
|
||
<div id="upcoming-table-content" style="display: none;">
|
||
<table>
|
||
<thead>
|
||
<tr style="font-size: 0.85rem !important;">
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Date</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Registration</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; width: 30px; text-align: center;"></th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Type</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">From</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">ETA</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Notes</th>
|
||
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">ACK</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="upcoming-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="upcoming-no-data" class="no-data" style="display: none; padding: 1rem; font-size: 0.9rem;">
|
||
<p>No upcoming PPRs.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!--#include virtual="/shared-modals.html" -->
|
||
|
||
<script src="shared.js"></script>
|
||
<script>
|
||
async function loadPPRs() {
|
||
if (!accessToken) return;
|
||
|
||
// Debounce: prevent duplicate refreshes if called multiple times in quick succession
|
||
// (e.g., from form submission + WebSocket update at the same time)
|
||
if (loadPPRsTimeout) {
|
||
clearTimeout(loadPPRsTimeout);
|
||
}
|
||
|
||
loadPPRsTimeout = setTimeout(async () => {
|
||
// Load all tables simultaneously
|
||
await Promise.all([loadArrivals(), loadDepartures(), loadLocalFlights(), loadOverflights(), loadDeparted(), loadParked(), loadUpcoming()]);
|
||
loadPPRsTimeout = null;
|
||
}, 100); // Wait 100ms before executing to batch multiple calls
|
||
}
|
||
|
||
// Load arrivals (NEW and CONFIRMED status for PPR only)
|
||
async function loadArrivals() {
|
||
document.getElementById('arrivals-loading').style.display = 'block';
|
||
document.getElementById('arrivals-table-content').style.display = 'none';
|
||
document.getElementById('arrivals-no-data').style.display = 'none';
|
||
|
||
try {
|
||
// Load PPRs and booked-in arrivals
|
||
const [pprResponse, bookInResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||
authenticatedFetch('/api/v1/arrivals/?limit=1000')
|
||
]);
|
||
|
||
if (!pprResponse.ok) {
|
||
throw new Error('Failed to fetch arrivals');
|
||
}
|
||
|
||
const allPPRs = await pprResponse.json();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// Filter for arrivals with ETA today and NEW or CONFIRMED status
|
||
const arrivals = allPPRs.filter(ppr => {
|
||
if (!ppr.eta || (ppr.status !== 'NEW' && ppr.status !== 'CONFIRMED')) {
|
||
return false;
|
||
}
|
||
// Extract date from ETA (UTC)
|
||
const etaDate = ppr.eta.split('T')[0];
|
||
return etaDate === today;
|
||
});
|
||
|
||
// Add booked-in arrivals from the arrivals table
|
||
if (bookInResponse.ok) {
|
||
const bookedInArrivals = await bookInResponse.json();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const bookedInToday = bookedInArrivals
|
||
.filter(arrival => {
|
||
// Only include arrivals booked in today (created_dt) with INBOUND, LOCAL, or CIRCUIT status
|
||
if (!arrival.created_dt || !['INBOUND', 'LOCAL', 'CIRCUIT'].includes(arrival.status)) return false;
|
||
const bookedDate = arrival.created_dt.split('T')[0];
|
||
return bookedDate === today;
|
||
})
|
||
.map(arrival => ({
|
||
...arrival,
|
||
isBookedIn: true // Flag to distinguish from PPR and local
|
||
}));
|
||
arrivals.push(...bookedInToday);
|
||
}
|
||
|
||
displayArrivals(arrivals);
|
||
} catch (error) {
|
||
console.error('Error loading arrivals:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading arrivals', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('arrivals-loading').style.display = 'none';
|
||
}
|
||
|
||
// Load departures (LANDED status for PPR plus non-local airport departures)
|
||
async function loadDepartures() {
|
||
document.getElementById('departures-loading').style.display = 'block';
|
||
document.getElementById('departures-table-content').style.display = 'none';
|
||
document.getElementById('departures-no-data').style.display = 'none';
|
||
|
||
try {
|
||
// Load PPR departures and airport departures simultaneously
|
||
const [pprResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
||
]);
|
||
|
||
if (!pprResponse.ok) {
|
||
throw new Error('Failed to fetch PPR departures');
|
||
}
|
||
|
||
const allPPRs = await pprResponse.json();
|
||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
||
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
|
||
|
||
// Combine departures
|
||
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// Filter for PPR departures with ETD today and LANDED status only
|
||
const departures = allPPRs.filter(ppr => {
|
||
if (!ppr.etd || ppr.status !== 'LANDED') {
|
||
return false;
|
||
}
|
||
// Extract date from ETD (UTC)
|
||
const etdDate = ppr.etd.split('T')[0];
|
||
return etdDate === today;
|
||
});
|
||
|
||
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
|
||
const depDepartures = allDepartures.map(flight => ({
|
||
...flight,
|
||
isDeparture: true // Flag to distinguish from PPR
|
||
}));
|
||
departures.push(...depDepartures);
|
||
|
||
displayDepartures(departures);
|
||
} catch (error) {
|
||
console.error('Error loading departures:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading departures', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('departures-loading').style.display = 'none';
|
||
}
|
||
|
||
async function loadLocalFlights() {
|
||
document.getElementById('local-flights-loading').style.display = 'block';
|
||
document.getElementById('local-flights-table-content').style.display = 'none';
|
||
document.getElementById('local-flights-no-data').style.display = 'none';
|
||
|
||
try {
|
||
const response = await authenticatedFetch('/api/v1/local-flights/?limit=1000');
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch local flights');
|
||
}
|
||
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const localFlights = (await response.json()).filter(flight => {
|
||
if (!flight.created_dt || ['CANCELLED', 'LANDED'].includes(flight.status)) return false;
|
||
return flight.created_dt.split('T')[0] === today;
|
||
});
|
||
|
||
displayLocalFlights(localFlights);
|
||
} catch (error) {
|
||
console.error('Error loading local flights:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading local flights', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('local-flights-loading').style.display = 'none';
|
||
}
|
||
|
||
// Load overflights (ACTIVE status only)
|
||
async function loadOverflights() {
|
||
document.getElementById('overflights-loading').style.display = 'block';
|
||
document.getElementById('overflights-table-content').style.display = 'none';
|
||
document.getElementById('overflights-no-data').style.display = 'none';
|
||
|
||
try {
|
||
const response = await authenticatedFetch('/api/v1/overflights/?status=ACTIVE&limit=100');
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch overflights');
|
||
}
|
||
|
||
const overflights = await response.json();
|
||
displayOverflights(overflights);
|
||
} catch (error) {
|
||
console.error('Error loading overflights:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading overflights', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('overflights-loading').style.display = 'none';
|
||
}
|
||
|
||
function displayOverflights(overflights) {
|
||
const tbody = document.getElementById('overflights-table-body');
|
||
|
||
document.getElementById('overflights-count').textContent = overflights.length;
|
||
|
||
if (overflights.length === 0) {
|
||
document.getElementById('overflights-no-data').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// Sort by call_dt most recent
|
||
overflights.sort((a, b) => new Date(b.call_dt) - new Date(a.call_dt));
|
||
|
||
tbody.innerHTML = '';
|
||
|
||
for (const flight of overflights) {
|
||
const row = document.createElement('tr');
|
||
row.style.cursor = 'pointer';
|
||
row.onclick = () => {
|
||
openOverflightEditModal(flight.id);
|
||
};
|
||
|
||
const statusBadge = flight.status === 'ACTIVE' ?
|
||
'<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8rem;">ACTIVE</span>' :
|
||
'<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8rem;">QSY\'D</span>';
|
||
|
||
// Action buttons for overflight
|
||
let actionButtons = '';
|
||
if (flight.status === 'ACTIVE') {
|
||
actionButtons = `
|
||
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentOverflightId = ${flight.id}; showOverflightQSYModal()" title="Mark QSY">
|
||
QSY
|
||
</button>
|
||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); cancelOverflightFromTable(${flight.id})" title="Cancel">
|
||
CANCEL
|
||
</button>
|
||
`;
|
||
} else {
|
||
actionButtons = '<span style="color: #999;">-</span>';
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td>${flight.registration || '-'}</td>
|
||
<td style="width: 30px; text-align: center;"><span style="color: #ff6b6b; font-weight: bold;" title="Overflight">🔄</span></td>
|
||
<td>${flight.callsign || '-'}</td>
|
||
<td>${flight.type || '-'}</td>
|
||
<td>${flight.departure_airfield || '-'}</td>
|
||
<td>${flight.destination_airfield || '-'}</td>
|
||
<td>${formatTimeOnly(flight.call_dt)}</td>
|
||
<td>${flight.pob || '-'}</td>
|
||
<td>${statusBadge}</td>
|
||
<td style="white-space: nowrap;">${actionButtons}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
|
||
document.getElementById('overflights-table-content').style.display = 'block';
|
||
}
|
||
|
||
|
||
// Load departed aircraft (DEPARTED status with departed_dt today)
|
||
async function loadDeparted() {
|
||
document.getElementById('departed-loading').style.display = 'block';
|
||
document.getElementById('departed-table-content').style.display = 'none';
|
||
document.getElementById('departed-no-data').style.display = 'none';
|
||
|
||
try {
|
||
const [pprResponse, depResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||
authenticatedFetch('/api/v1/departures/?status=DEPARTED&limit=1000')
|
||
]);
|
||
|
||
if (!pprResponse.ok) {
|
||
throw new Error('Failed to fetch departed aircraft');
|
||
}
|
||
|
||
const allPPRs = await pprResponse.json();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
|
||
const departed = allPPRs.filter(ppr => {
|
||
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
|
||
return false;
|
||
}
|
||
const departedDate = ppr.departed_dt.split('T')[0];
|
||
return departedDate === today;
|
||
});
|
||
|
||
// Add departures to other airports that departed today (booked out departures)
|
||
if (depResponse.ok) {
|
||
const depFlights = await depResponse.json();
|
||
const depDeparted = depFlights.filter(flight => {
|
||
if (!flight.departed_dt) return false;
|
||
const departedDate = flight.departed_dt.split('T')[0];
|
||
return departedDate === today;
|
||
}).map(flight => ({
|
||
...flight,
|
||
isDeparture: true
|
||
}));
|
||
departed.push(...depDeparted);
|
||
}
|
||
|
||
displayDeparted(departed);
|
||
} catch (error) {
|
||
console.error('Error loading departed aircraft:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading departed aircraft', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('departed-loading').style.display = 'none';
|
||
}
|
||
|
||
function displayDeparted(departed) {
|
||
const tbody = document.getElementById('departed-table-body');
|
||
|
||
document.getElementById('departed-count').textContent = departed.length;
|
||
|
||
if (departed.length === 0) {
|
||
document.getElementById('departed-no-data').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// Sort by departed time
|
||
departed.sort((a, b) => {
|
||
const aTime = a.departed_dt;
|
||
const bTime = b.departed_dt;
|
||
return new Date(aTime) - new Date(bTime);
|
||
});
|
||
|
||
tbody.innerHTML = '';
|
||
document.getElementById('departed-table-content').style.display = 'block';
|
||
|
||
for (const flight of departed) {
|
||
const row = document.createElement('tr');
|
||
const isLocal = flight.isLocalFlight;
|
||
const isDeparture = flight.isDeparture;
|
||
|
||
row.onclick = () => {
|
||
if (isLocal) {
|
||
openLocalFlightEditModal(flight.id);
|
||
} else if (isDeparture) {
|
||
openDepartureEditModal(flight.id);
|
||
} else {
|
||
openPPRModal(flight.id);
|
||
}
|
||
};
|
||
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
|
||
|
||
if (isLocal) {
|
||
row.innerHTML = `
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.registration || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;">${flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold;" title="Submitted by Pilot Online">O</span>' : ''}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.callsign || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">-</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
||
`;
|
||
} else if (isDeparture) {
|
||
row.innerHTML = `
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.registration || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;">${flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold;" title="Submitted by Pilot Online">O</span>' : ''}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.callsign || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
||
`;
|
||
} else {
|
||
row.innerHTML = `
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_reg || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_call || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
||
`;
|
||
}
|
||
tbody.appendChild(row);
|
||
}
|
||
}
|
||
|
||
// Load parked visitors (LANDED status with no ETD today or ETD not today)
|
||
async function loadParked() {
|
||
document.getElementById('parked-loading').style.display = 'block';
|
||
document.getElementById('parked-table-content').style.display = 'none';
|
||
document.getElementById('parked-no-data').style.display = 'none';
|
||
|
||
try {
|
||
const pprResponse = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
||
|
||
if (!pprResponse.ok) {
|
||
throw new Error('Failed to fetch parked visitors');
|
||
}
|
||
|
||
const allPPRs = await pprResponse.json();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// Filter for parked PPR visitors: LANDED status and ETD on a different day
|
||
const parked = allPPRs.filter(ppr => {
|
||
if (ppr.status !== 'LANDED') {
|
||
return false;
|
||
}
|
||
// Only show if ETD exists and is not today
|
||
if (!ppr.etd) {
|
||
return false;
|
||
}
|
||
const etdDate = ppr.etd.split('T')[0];
|
||
return etdDate !== today;
|
||
});
|
||
|
||
displayParked(parked);
|
||
} catch (error) {
|
||
console.error('Error loading parked visitors:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading parked visitors', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('parked-loading').style.display = 'none';
|
||
}
|
||
|
||
function displayParked(parked) {
|
||
const tbody = document.getElementById('parked-table-body');
|
||
|
||
document.getElementById('parked-count').textContent = parked.length;
|
||
|
||
if (parked.length === 0) {
|
||
document.getElementById('parked-no-data').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// Sort by landed time
|
||
parked.sort((a, b) => {
|
||
if (!a.landed_dt) return 1;
|
||
if (!b.landed_dt) return -1;
|
||
return new Date(a.landed_dt) - new Date(b.landed_dt);
|
||
});
|
||
|
||
tbody.innerHTML = '';
|
||
document.getElementById('parked-table-content').style.display = 'block';
|
||
|
||
for (const ppr of parked) {
|
||
const row = document.createElement('tr');
|
||
|
||
// All rows are PPR, so make them clickable
|
||
row.onclick = () => openPPRModal(ppr.id);
|
||
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
|
||
|
||
// Get registration
|
||
const registration = ppr.ac_reg || '-';
|
||
const typeIconParked = '<span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span>';
|
||
|
||
// Get aircraft type
|
||
const acType = ppr.ac_type || '-';
|
||
|
||
// Get from airport
|
||
const fromAirport = ppr.in_from || '-';
|
||
|
||
// Format arrival: time if today, date if not
|
||
const today = new Date().toISOString().split('T')[0];
|
||
let arrivedDisplay = '-';
|
||
if (ppr.landed_dt) {
|
||
const landedDate = ppr.landed_dt.split('T')[0];
|
||
if (landedDate === today) {
|
||
// Today - show time only
|
||
arrivedDisplay = formatTimeOnly(ppr.landed_dt);
|
||
} else {
|
||
// Not today - show date (DD/MM)
|
||
const date = new Date(ppr.landed_dt);
|
||
arrivedDisplay = date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
|
||
}
|
||
}
|
||
|
||
// Format ETD as just the date (DD/MM)
|
||
let etdDisplay = '-';
|
||
if (ppr.etd) {
|
||
const etdDate = new Date(ppr.etd);
|
||
etdDisplay = etdDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${registration}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;">${typeIconParked}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${acType}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${fromAirport}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${arrivedDisplay}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${etdDisplay}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
}
|
||
|
||
// Load upcoming PPRs (future days with NEW or CONFIRMED status)
|
||
async function loadUpcoming() {
|
||
document.getElementById('upcoming-loading').style.display = 'block';
|
||
document.getElementById('upcoming-table-content').style.display = 'none';
|
||
document.getElementById('upcoming-no-data').style.display = 'none';
|
||
|
||
try {
|
||
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch upcoming PPRs');
|
||
}
|
||
|
||
const allPPRs = await response.json();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// Filter for PPRs with ETA in the future (not today) and NEW or CONFIRMED status
|
||
const upcoming = allPPRs.filter(ppr => {
|
||
if (!ppr.eta || (ppr.status !== 'NEW' && ppr.status !== 'CONFIRMED')) {
|
||
return false;
|
||
}
|
||
const etaDate = ppr.eta.split('T')[0];
|
||
return etaDate > today;
|
||
});
|
||
|
||
displayUpcoming(upcoming);
|
||
} catch (error) {
|
||
console.error('Error loading upcoming PPRs:', error);
|
||
if (error.message !== 'Session expired. Please log in again.') {
|
||
showNotification('Error loading upcoming PPRs', true);
|
||
}
|
||
}
|
||
|
||
document.getElementById('upcoming-loading').style.display = 'none';
|
||
}
|
||
|
||
function displayUpcoming(upcoming) {
|
||
const tbody = document.getElementById('upcoming-table-body');
|
||
|
||
document.getElementById('upcoming-count').textContent = upcoming.length;
|
||
|
||
if (upcoming.length === 0) {
|
||
// Don't show anything if collapsed by default
|
||
return;
|
||
}
|
||
|
||
// Sort by ETA date and time
|
||
upcoming.sort((a, b) => new Date(a.eta) - new Date(b.eta));
|
||
|
||
tbody.innerHTML = '';
|
||
// Don't auto-expand, keep collapsed by default
|
||
|
||
for (const ppr of upcoming) {
|
||
const row = document.createElement('tr');
|
||
row.onclick = () => openPPRModal(ppr.id);
|
||
row.style.cssText = 'font-size: 0.85rem !important;';
|
||
if (pprNeedsStripAck(ppr)) {
|
||
row.classList.add('ppr-strip-unacknowledged');
|
||
}
|
||
|
||
// Format date as Day DD/MM (e.g., Wed 11/12)
|
||
const etaDate = new Date(ppr.eta);
|
||
const dayName = etaDate.toLocaleDateString('en-GB', { weekday: 'short' });
|
||
const dateStr = etaDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
|
||
const dateDisplay = `${dayName} ${dateStr}`;
|
||
|
||
// Create notes indicator if notes exist
|
||
const notesIndicator = ppr.notes && ppr.notes.trim() ?
|
||
`<span class="notes-tooltip">
|
||
<span class="notes-indicator">📝</span>
|
||
<span class="tooltip-text">${ppr.notes}</span>
|
||
</span>` : '';
|
||
const ackButton = pprNeedsStripAck(ppr)
|
||
? `<button class="btn btn-ack btn-icon" onclick='event.stopPropagation(); acknowledgePPRStrip(${ppr.id}, ${JSON.stringify(ppr.ac_reg || 'PPR')})' title="Acknowledge paper strip created">ACK</button>`
|
||
: '<span class="ack-complete" title="Paper strip acknowledged">ACK</span>';
|
||
|
||
row.innerHTML = `
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${dateDisplay}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_reg || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_type || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.in_from || '-'}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(ppr.eta)}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${notesIndicator}</td>
|
||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ackButton}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
// Setup tooltips after rendering
|
||
setupTooltips();
|
||
}
|
||
|
||
// Toggle upcoming table collapse/expand
|
||
function toggleUpcomingTable() {
|
||
const content = document.getElementById('upcoming-table-content');
|
||
const noData = document.getElementById('upcoming-no-data');
|
||
const icon = document.getElementById('upcoming-collapse-icon');
|
||
|
||
const isVisible = content.style.display === 'block' || noData.style.display === 'block';
|
||
|
||
if (isVisible) {
|
||
content.style.display = 'none';
|
||
noData.style.display = 'none';
|
||
icon.classList.add('collapsed');
|
||
} else {
|
||
const count = parseInt(document.getElementById('upcoming-count').textContent);
|
||
if (count > 0) {
|
||
content.style.display = 'block';
|
||
} else {
|
||
noData.style.display = 'block';
|
||
}
|
||
icon.classList.remove('collapsed');
|
||
}
|
||
}
|
||
|
||
// ICAO code to airport name cache
|
||
const airportNameCache = {};
|
||
|
||
async function getAirportDisplay(code) {
|
||
if (!code || code.length !== 4 || !/^[A-Z]{4}$/.test(code)) return code;
|
||
if (airportNameCache[code]) return `${code}<br><span style="font-size:0.8em;color:#666;font-style:italic;">${airportNameCache[code]}</span>`;
|
||
try {
|
||
const resp = await authenticatedFetch(`/api/v1/airport/lookup/${code}`);
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
if (data && data.length && data[0].name) {
|
||
airportNameCache[code] = data[0].name;
|
||
return `${code}<br><span style="font-size:0.8em;color:#666;font-style:italic;">${data[0].name}</span>`;
|
||
}
|
||
}
|
||
} catch {}
|
||
return code;
|
||
}
|
||
|
||
async function displayArrivals(arrivals) {
|
||
const tbody = document.getElementById('arrivals-table-body');
|
||
const recordCount = document.getElementById('arrivals-count');
|
||
|
||
recordCount.textContent = arrivals.length;
|
||
if (arrivals.length === 0) {
|
||
document.getElementById('arrivals-no-data').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// Sort arrivals by ETA/departure time (ascending)
|
||
arrivals.sort((a, b) => {
|
||
const aTime = a.eta || a.departure_dt;
|
||
const bTime = b.eta || b.departure_dt;
|
||
if (!aTime) return 1;
|
||
if (!bTime) return -1;
|
||
return new Date(aTime) - new Date(bTime);
|
||
});
|
||
tbody.innerHTML = '';
|
||
document.getElementById('arrivals-table-content').style.display = 'block';
|
||
for (const flight of arrivals) {
|
||
const row = document.createElement('tr');
|
||
const isLocal = flight.isLocalFlight;
|
||
const isBookedIn = flight.isBookedIn;
|
||
if (!isLocal && !isBookedIn && pprNeedsStripAck(flight)) {
|
||
row.classList.add('ppr-strip-unacknowledged');
|
||
}
|
||
|
||
// Click handler that routes to correct modal
|
||
row.onclick = () => {
|
||
if (isLocal) {
|
||
openLocalFlightEditModal(flight.id);
|
||
} else if (isBookedIn) {
|
||
openArrivalEditModal(flight.id);
|
||
} else {
|
||
openPPRModal(flight.id);
|
||
}
|
||
};
|
||
|
||
// Create notes indicator if notes exist
|
||
const notesIndicator = flight.notes && flight.notes.trim() ?
|
||
`<span class="notes-tooltip">
|
||
<span class="notes-indicator">📝</span>
|
||
<span class="tooltip-text">${flight.notes}</span>
|
||
</span>` : '';
|
||
|
||
let aircraftDisplay, acType, fromDisplay, eta, pob, fuel, actionButtons, typeIcon;
|
||
|
||
if (isLocal) {
|
||
// Local flight display
|
||
if (flight.callsign && flight.callsign.trim()) {
|
||
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||
}
|
||
acType = flight.type;
|
||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||
fromDisplay = `<i>${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}</i>`;
|
||
|
||
// Calculate ETA: use departed_dt (actual departure) if available, otherwise etd (planned departure)
|
||
// Then add duration to get ETA
|
||
let departureTime = flight.departed_dt || flight.etd;
|
||
let etaTime = departureTime;
|
||
if (departureTime && flight.duration) {
|
||
const departTime = new Date(departureTime);
|
||
etaTime = new Date(departTime.getTime() + flight.duration * 60000).toISOString(); // duration is in minutes
|
||
}
|
||
eta = etaTime ? formatTimeOnly(etaTime) : '-';
|
||
pob = flight.pob || '-';
|
||
fuel = '-';
|
||
|
||
// Allow touch and go for all local flight types
|
||
let circuitButton = '';
|
||
circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||
T&G
|
||
</button>`;
|
||
|
||
actionButtons = `
|
||
${circuitButton}
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else if (isBookedIn) {
|
||
// Booked-in arrival display
|
||
if (flight.callsign && flight.callsign.trim()) {
|
||
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||
}
|
||
acType = flight.type;
|
||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||
|
||
// Lookup airport name for in_from
|
||
let fromDisplay_temp = flight.in_from;
|
||
if (flight.in_from && flight.in_from.length === 4 && /^[A-Z]{4}$/.test(flight.in_from)) {
|
||
fromDisplay_temp = await getAirportDisplay(flight.in_from);
|
||
}
|
||
fromDisplay = fromDisplay_temp;
|
||
|
||
// Show ETA if available, otherwise show landed_dt
|
||
eta = flight.eta ? formatTimeOnly(flight.eta) : (flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-');
|
||
pob = flight.pob || '-';
|
||
fuel = '-';
|
||
|
||
// Different action buttons based on status
|
||
if (flight.status === 'INBOUND') {
|
||
actionButtons = `
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
|
||
CANCEL
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'LOCAL') {
|
||
// Arrival in local area - show circuit and land buttons
|
||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||
T&G
|
||
</button>`;
|
||
actionButtons = `
|
||
${circuitButton}
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'CIRCUIT') {
|
||
// Arrival in circuit - show local, T&G and land buttons
|
||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||
T&G
|
||
</button>`;
|
||
actionButtons = `
|
||
${circuitButton}
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else {
|
||
actionButtons = `
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentBookedInArrivalId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, false, false, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CANCELLED')" title="Cancel Arrival">
|
||
CANCEL
|
||
</button>
|
||
`;
|
||
}
|
||
} else {
|
||
// PPR display
|
||
if (flight.ac_call && flight.ac_call.trim()) {
|
||
aircraftDisplay = `<strong>${flight.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.ac_reg}</strong>`;
|
||
}
|
||
acType = flight.ac_type;
|
||
typeIcon = '<span style="color: #032cfc; font-weight: bold; font-size: 0.9em;" title="From PPR">P</span>';
|
||
|
||
// Lookup airport name for in_from
|
||
let fromDisplay_temp = flight.in_from;
|
||
if (flight.in_from && flight.in_from.length === 4 && /^[A-Z]{4}$/.test(flight.in_from)) {
|
||
fromDisplay_temp = await getAirportDisplay(flight.in_from);
|
||
}
|
||
fromDisplay = fromDisplay_temp;
|
||
|
||
eta = formatTimeOnly(flight.eta);
|
||
pob = flight.pob_in;
|
||
fuel = flight.fuel || '-';
|
||
const ackButton = pprNeedsStripAck(flight)
|
||
? `<button class="btn btn-ack btn-icon" onclick='event.stopPropagation(); acknowledgePPRStrip(${flight.id}, ${JSON.stringify(flight.ac_reg || 'PPR')})' title="Acknowledge paper strip created">
|
||
ACK
|
||
</button>`
|
||
: '';
|
||
actionButtons = `
|
||
${ackButton}
|
||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${flight.id})" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td>${aircraftDisplay}${notesIndicator}</td>
|
||
<td style="text-align: center; width: 30px;">${typeIcon}</td>
|
||
<td>${acType}</td>
|
||
<td>${fromDisplay}</td>
|
||
<td>${eta}</td>
|
||
<td>${pob}</td>
|
||
<td>${fuel}</td>
|
||
<td>${actionButtons}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
// Setup tooltips after rendering
|
||
setupTooltips();
|
||
}
|
||
|
||
function localFlightStatusBadge(status) {
|
||
const labels = {
|
||
BOOKED_OUT: 'BOOKED OUT',
|
||
GROUND: 'GROUND',
|
||
DEPARTED: 'AIRBORNE',
|
||
LOCAL: 'LOCAL',
|
||
CIRCUIT: 'CIRCUIT',
|
||
CIRCUIT_DOWNWIND: 'DOWNWIND',
|
||
CIRCUIT_BASE: 'BASE',
|
||
CIRCUIT_FINAL: 'FINAL',
|
||
LANDED: 'LANDED'
|
||
};
|
||
return `<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8rem;">${labels[status] || status || '-'}</span>`;
|
||
}
|
||
|
||
async function displayLocalFlights(localFlights) {
|
||
const tbody = document.getElementById('local-flights-table-body');
|
||
const recordCount = document.getElementById('local-flights-count');
|
||
|
||
recordCount.textContent = localFlights.length;
|
||
if (localFlights.length === 0) {
|
||
document.getElementById('local-flights-no-data').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
localFlights.sort((a, b) => {
|
||
const aTime = a.etd || a.created_dt;
|
||
const bTime = b.etd || b.created_dt;
|
||
if (!aTime) return 1;
|
||
if (!bTime) return -1;
|
||
return new Date(aTime) - new Date(bTime);
|
||
});
|
||
|
||
tbody.innerHTML = '';
|
||
document.getElementById('local-flights-table-content').style.display = 'block';
|
||
|
||
const circuitCounts = await loadLocalFlightCircuitCounts(localFlights);
|
||
|
||
for (const flight of localFlights) {
|
||
const row = document.createElement('tr');
|
||
row.onclick = () => openLocalFlightEditModal(flight.id);
|
||
|
||
const aircraftDisplay = flight.callsign && flight.callsign.trim()
|
||
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
|
||
: `<strong>${flight.registration}</strong>`;
|
||
const typeIcon = flight.submitted_via === 'PUBLIC'
|
||
? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>'
|
||
: '<span style="color: #228b22; font-weight: bold; font-size: 0.9em;" title="Local flight">L</span>';
|
||
const flightType = flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
|
||
const etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||
const circuits = circuitCounts[flight.id] ?? flight.circuits ?? 0;
|
||
|
||
let actionButtons = '';
|
||
if (flight.status === 'BOOKED_OUT') {
|
||
actionButtons = `
|
||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
|
||
CONTACT
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'GROUND') {
|
||
const takeoffStatus = flight.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
|
||
actionButtons = `
|
||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('${takeoffStatus}', ${flight.id}, true)" title="Mark as airborne">
|
||
TAKE OFF
|
||
</button>
|
||
`;
|
||
} else if (['DEPARTED', 'LOCAL', 'CIRCUIT', 'CIRCUIT_DOWNWIND', 'CIRCUIT_BASE', 'CIRCUIT_FINAL'].includes(flight.status)) {
|
||
actionButtons = `
|
||
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||
T&G
|
||
</button>
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else {
|
||
actionButtons = '<span style="color: #999;">-</span>';
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td>${aircraftDisplay}</td>
|
||
<td style="text-align: center; width: 30px;">${typeIcon}</td>
|
||
<td>${flight.type || '-'}</td>
|
||
<td>${flightType}</td>
|
||
<td>${etd}</td>
|
||
<td>${flight.pob || '-'}</td>
|
||
<td>${localFlightStatusBadge(flight.status)}</td>
|
||
<td>${circuits}</td>
|
||
<td style="white-space: nowrap;">${actionButtons}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
}
|
||
|
||
async function loadLocalFlightCircuitCounts(localFlights) {
|
||
const counts = {};
|
||
await Promise.all(localFlights.map(async (flight) => {
|
||
try {
|
||
const response = await authenticatedFetch(`/api/v1/circuits/flight/${flight.id}`);
|
||
if (!response.ok) return;
|
||
const circuits = await response.json();
|
||
counts[flight.id] = circuits.length;
|
||
} catch (error) {
|
||
console.warn(`Unable to load circuits for local flight ${flight.id}:`, error);
|
||
}
|
||
}));
|
||
return counts;
|
||
}
|
||
|
||
function pprNeedsStripAck(ppr) {
|
||
return (ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && !ppr.acknowledged_dt;
|
||
}
|
||
|
||
async function acknowledgePPRStrip(pprId, acReg) {
|
||
if (!confirm(`Confirm paper strip has been created for ${acReg}?`)) return;
|
||
|
||
try {
|
||
const response = await authenticatedFetch(`/api/v1/pprs/${pprId}/acknowledge`, { method: 'POST' });
|
||
if (!response.ok) {
|
||
const err = await response.json().catch(() => ({}));
|
||
showNotification(err.detail || 'Failed to acknowledge PPR strip', true);
|
||
return;
|
||
}
|
||
|
||
showNotification('PPR strip acknowledged');
|
||
await loadPPRs();
|
||
} catch (error) {
|
||
console.error('Error acknowledging PPR strip:', error);
|
||
showNotification('Error acknowledging PPR strip', true);
|
||
}
|
||
}
|
||
|
||
async function displayDepartures(departures) {
|
||
const tbody = document.getElementById('departures-table-body');
|
||
const recordCount = document.getElementById('departures-count');
|
||
|
||
recordCount.textContent = departures.length;
|
||
if (departures.length === 0) {
|
||
document.getElementById('departures-no-data').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// Sort departures by ETD (ascending), nulls last
|
||
departures.sort((a, b) => {
|
||
const aTime = a.etd || a.created_dt;
|
||
const bTime = b.etd || b.created_dt;
|
||
if (!aTime) return 1;
|
||
if (!bTime) return -1;
|
||
return new Date(aTime) - new Date(bTime);
|
||
});
|
||
tbody.innerHTML = '';
|
||
document.getElementById('departures-table-content').style.display = 'block';
|
||
for (const flight of departures) {
|
||
const row = document.createElement('tr');
|
||
const isLocal = flight.isLocalFlight;
|
||
const isDeparture = flight.isDeparture;
|
||
const isArrival = flight.isArrival;
|
||
|
||
// Click handler that routes to correct modal
|
||
row.onclick = () => {
|
||
if (isLocal) {
|
||
openLocalFlightEditModal(flight.id);
|
||
} else if (isDeparture) {
|
||
openDepartureEditModal(flight.id);
|
||
} else if (isArrival) {
|
||
openArrivalEditModal(flight.id);
|
||
} else {
|
||
openPPRModal(flight.id);
|
||
}
|
||
};
|
||
|
||
// Create notes indicator if notes exist
|
||
const notesIndicator = flight.notes && flight.notes.trim() ?
|
||
`<span class="notes-tooltip">
|
||
<span class="notes-indicator">📝</span>
|
||
<span class="tooltip-text">${flight.notes}</span>
|
||
</span>` : '';
|
||
|
||
let aircraftDisplay, toDisplay, etd, pob, fuel, landedDt, actionButtons, typeIcon;
|
||
|
||
if (isLocal) {
|
||
// Local flight display
|
||
if (flight.callsign && flight.callsign.trim()) {
|
||
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||
}
|
||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||
toDisplay = `<i>${flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local Flight' : 'Departure'}</i>`;
|
||
etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||
pob = flight.pob || '-';
|
||
fuel = '-';
|
||
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
||
|
||
// Action buttons for local flight
|
||
if (flight.status === 'BOOKED_OUT') {
|
||
actionButtons = `
|
||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
|
||
CONTACT
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'GROUND') {
|
||
const takeoffStatus = flight.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
|
||
const takeoffTitle = flight.flight_type === 'CIRCUITS' ? 'Mark as Circuit' : 'Mark as Local';
|
||
actionButtons = `
|
||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('${takeoffStatus}', ${flight.id}, true)" title="${takeoffTitle}">
|
||
TAKE OFF
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'DEPARTED') {
|
||
// Allow touch and go for all local flight types
|
||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||
T&G
|
||
</button>`;
|
||
actionButtons = `
|
||
${circuitButton}
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'LOCAL') {
|
||
actionButtons = `
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'CIRCUIT') {
|
||
// Circuit traffic - show LOCAL, T&G and LAND buttons
|
||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||
T&G
|
||
</button>`;
|
||
actionButtons = `
|
||
${circuitButton}
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('LANDED', ${flight.id}, true)" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else {
|
||
actionButtons = '<span style="color: #999;">-</span>';
|
||
}
|
||
} else if (isDeparture) {
|
||
// Departure to other airport display
|
||
if (flight.callsign && flight.callsign.trim()) {
|
||
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||
}
|
||
typeIcon = flight.submitted_via === 'PUBLIC' ? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>' : '';
|
||
toDisplay = flight.out_to || '-';
|
||
if (flight.out_to && flight.out_to.length === 4 && /^[A-Z]{4}$/.test(flight.out_to)) {
|
||
toDisplay = await getAirportDisplay(flight.out_to);
|
||
}
|
||
etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||
pob = flight.pob || '-';
|
||
fuel = '-';
|
||
landedDt = flight.departed_dt ? formatTimeOnly(flight.departed_dt) : '-';
|
||
|
||
// Action buttons for departure
|
||
if (flight.status === 'BOOKED_OUT') {
|
||
actionButtons = `
|
||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)" title="Contact Pilot">
|
||
CONTACT
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'GROUND') {
|
||
actionButtons = `
|
||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('LOCAL', ${flight.id}, false, true)" title="Mark as Local">
|
||
TAKE OFF
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'LOCAL') {
|
||
actionButtons = `
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
|
||
QSY
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'DEPARTED') {
|
||
actionButtons = '<span style="color: #999;">Departed</span>';
|
||
} else {
|
||
actionButtons = '<span style="color: #999;">-</span>';
|
||
}
|
||
} else if (isArrival) {
|
||
// Arrival display
|
||
if (flight.callsign && flight.callsign.trim()) {
|
||
aircraftDisplay = `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.registration}</strong>`;
|
||
}
|
||
typeIcon = '<span style="color: #228b22; font-weight: bold; font-size: 0.9em;" title="Arrival">A</span>';
|
||
toDisplay = `<i>Arrival from ${flight.in_from || '?'}</i>`;
|
||
etd = flight.eta ? formatTimeOnly(flight.eta) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||
pob = flight.pob || '-';
|
||
fuel = '-';
|
||
landedDt = flight.arrived_dt ? formatTimeOnly(flight.arrived_dt) : '-';
|
||
|
||
// Action buttons for arrival
|
||
if (flight.status === 'LOCAL') {
|
||
actionButtons = `
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; updateArrivalStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else if (flight.status === 'CIRCUIT') {
|
||
actionButtons = `
|
||
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; showCircuitModal(null, ${flight.id})" title="Record Touch & Go">
|
||
T&G
|
||
</button>
|
||
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentArrivalId = ${flight.id}; updateArrivalStatusFromTable(${flight.id}, 'LANDED')" title="Mark as Landed">
|
||
LAND
|
||
</button>
|
||
`;
|
||
} else {
|
||
actionButtons = '<span style="color: #999;">-</span>';
|
||
}
|
||
} else {
|
||
// PPR display
|
||
if (flight.ac_call && flight.ac_call.trim()) {
|
||
aircraftDisplay = `<strong>${flight.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`;
|
||
} else {
|
||
aircraftDisplay = `<strong>${flight.ac_reg}</strong>`;
|
||
}
|
||
typeIcon = '<span style="color: #032cfc; font-weight: bold; font-size: 0.9em;" title="From PPR">P</span>';
|
||
toDisplay = flight.out_to || '-';
|
||
if (flight.out_to && flight.out_to.length === 4 && /^[A-Z]{4}$/.test(flight.out_to)) {
|
||
toDisplay = await getAirportDisplay(flight.out_to);
|
||
}
|
||
etd = flight.etd ? formatTimeOnly(flight.etd) : '-';
|
||
pob = flight.pob_out || flight.pob_in;
|
||
fuel = flight.fuel || '-';
|
||
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
||
|
||
actionButtons = `
|
||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
||
TAKE OFF
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td>${aircraftDisplay}${notesIndicator}</td>
|
||
<td style="text-align: center; width: 30px;">${typeIcon}</td>
|
||
<td>${isLocal ? flight.type : isDeparture ? flight.type : flight.ac_type}</td>
|
||
<td>${toDisplay}</td>
|
||
<td>${etd}</td>
|
||
<td>${pob}</td>
|
||
<td>${fuel}</td>
|
||
<td>${landedDt}</td>
|
||
<td>${actionButtons}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
}
|
||
|
||
|
||
async function updateLocalFlightStatusFromTable(flightId, status) {
|
||
if (!accessToken) return;
|
||
|
||
// Show confirmation for cancel actions
|
||
if (status === 'CANCELLED') {
|
||
if (!confirm('Are you sure you want to cancel this flight? This action cannot be easily undone.')) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/local-flights/${flightId}/status`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${accessToken}`
|
||
},
|
||
body: JSON.stringify({ status: status })
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to update status');
|
||
|
||
loadPPRs(); // Refresh display
|
||
showNotification(`Flight marked as ${status.toLowerCase()}`);
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
showNotification('Error updating flight status', true);
|
||
}
|
||
}
|
||
|
||
// Update status from table for departures
|
||
async function updateDepartureStatusFromTable(departureId, status) {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/departures/${departureId}/status`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${accessToken}`
|
||
},
|
||
body: JSON.stringify({ status: status })
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to update status');
|
||
|
||
loadPPRs(); // Refresh display
|
||
showNotification(`Departure marked as ${status.toLowerCase()}`);
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
showNotification('Error updating departure status', true);
|
||
}
|
||
}
|
||
|
||
// Update status from table for booked-in arrivals
|
||
async function updateArrivalStatusFromTable(arrivalId, status) {
|
||
if (!accessToken) return;
|
||
|
||
// Show confirmation for cancel actions
|
||
if (status === 'CANCELLED') {
|
||
if (!confirm('Are you sure you want to cancel this arrival? This action cannot be easily undone.')) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/arrivals/${arrivalId}/status`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${accessToken}`
|
||
},
|
||
body: JSON.stringify({ status: status })
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to update status');
|
||
|
||
loadArrivals(); // Refresh arrivals table
|
||
showNotification(`Arrival marked as ${status.toLowerCase()}`);
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
showNotification('Error updating arrival status', true);
|
||
}
|
||
}
|
||
|
||
|
||
// Override: admin uses loadDepartures after circuit save
|
||
async function afterCircuitSaved() {
|
||
closeCircuitModal();
|
||
loadLocalFlights();
|
||
}
|
||
</script>
|
||
|
||
<!-- Footer Bar -->
|
||
<div class="footer-bar">
|
||
Please contact James Pattinson if you have any ideas about or problems with this system
|
||
</div>
|
||
</body>
|
||
</html>
|