Files
ppr-ng/web/admin.html
T
2026-06-28 07:37:41 -04:00

1576 lines
80 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<script src="topbar.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) => parseUtcDate(b.call_dt) - parseUtcDate(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 parseUtcDate(aTime) - parseUtcDate(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 parseUtcDate(a.landed_dt) - parseUtcDate(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)
arrivedDisplay = formatUtcDayMonth(ppr.landed_dt);
}
}
// Format ETD as just the date (DD/MM)
let etdDisplay = '-';
if (ppr.etd) {
etdDisplay = formatUtcDayMonth(ppr.etd);
}
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) => parseUtcDate(a.eta) - parseUtcDate(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 dateDisplay = formatUtcWeekdayDayMonth(ppr.eta);
// 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 parseUtcDate(aTime) - parseUtcDate(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 = parseUtcDate(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 parseUtcDate(aTime) - parseUtcDate(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 parseUtcDate(aTime) - parseUtcDate(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>