1854 lines
89 KiB
HTML
1854 lines
89 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ATC Management Interface</title>
|
||
<link rel="stylesheet" href="admin.css">
|
||
<script src="lookups.js"></script>
|
||
<script src="topbar.js"></script>
|
||
<style>
|
||
/* ATC-specific styles */
|
||
.atc-container {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
grid-template-rows: auto auto;
|
||
gap: 1rem;
|
||
padding: 1rem;
|
||
height: calc(100vh - 80px);
|
||
}
|
||
|
||
.atc-section {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
padding: 1rem;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.atc-section h2 {
|
||
margin: 0 0 1rem 0;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 2px solid #3498db;
|
||
font-size: 1.2rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.atc-section .count {
|
||
background: #3498db;
|
||
color: white;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 12px;
|
||
font-size: 0.8rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.aircraft-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
max-height: calc(50vh - 100px);
|
||
}
|
||
|
||
.aircraft-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem 0.75rem;
|
||
border-bottom: 1px solid #eee;
|
||
transition: background-color 0.2s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.aircraft-item:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.aircraft-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.aircraft-item.local-flight,
|
||
.aircraft-item.circuit {
|
||
background-color: #ffcccc; /* light red */
|
||
}
|
||
|
||
.aircraft-item.departure {
|
||
background-color: #ccccff; /* light blue */
|
||
}
|
||
|
||
.aircraft-item.inbound {
|
||
background-color: #ffffcc; /* light yellow */
|
||
}
|
||
|
||
.aircraft-item.overflight {
|
||
background-color: #ccffcc; /* light green */
|
||
}
|
||
|
||
.aircraft-info {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 0.75rem;
|
||
flex: 1;
|
||
align-items: center;
|
||
}
|
||
|
||
.aircraft-reg {
|
||
font-weight: bold;
|
||
font-size: 0.95rem;
|
||
color: #2c3e50;
|
||
min-width: 70px;
|
||
}
|
||
|
||
.aircraft-details {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
flex: 1;
|
||
}
|
||
|
||
.aircraft-time {
|
||
font-size: 0.8rem;
|
||
color: #999;
|
||
min-width: 50px;
|
||
text-align: right;
|
||
}
|
||
|
||
.aircraft-status {
|
||
padding: 0.2rem 0.4rem;
|
||
border-radius: 3px;
|
||
font-size: 0.75rem;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
min-width: 65px;
|
||
margin-left: 0.5rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.status-active {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.status-inbound {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.status-departing {
|
||
background: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.status-circuit {
|
||
background: #9b59b6;
|
||
color: white;
|
||
}
|
||
|
||
.status-pending {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.no-aircraft {
|
||
text-align: center;
|
||
color: #999;
|
||
font-style: italic;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.status-btn {
|
||
padding: 0.3rem 0.6rem;
|
||
font-size: 0.75rem;
|
||
border: none;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: all 0.2s ease;
|
||
background: #2c3e50;
|
||
color: white;
|
||
}
|
||
|
||
.status-btn:hover {
|
||
background: #34495e;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.status-btn.small-btn {
|
||
padding: 0.2rem 0.4rem;
|
||
font-size: 0.7rem;
|
||
min-width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
.status-btn.active-position {
|
||
background-color: #28a745;
|
||
color: white;
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.status-btn.active-position:hover {
|
||
background-color: #218838;
|
||
border-color: #1e7e34;
|
||
}
|
||
|
||
/* Responsive adjustments */
|
||
@media (max-width: 1400px) {
|
||
.atc-container {
|
||
grid-template-columns: 1fr 1fr;
|
||
grid-template-rows: auto auto auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.atc-container {
|
||
grid-template-columns: 1fr;
|
||
grid-template-rows: auto auto auto auto auto auto;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="top-bar">
|
||
<div class="title">
|
||
<h1 id="tower-title">✈️ Swansea Tower - ATC View</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 = '/admin'">🏠 Admin View</a>
|
||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</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>
|
||
</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="atc-container">
|
||
<!-- Row 1: Apron -->
|
||
<div class="atc-section">
|
||
<h2>🛫 Apron <span class="count" id="departing-count">0</span></h2>
|
||
<div class="aircraft-list" id="departing-list">
|
||
<div class="no-aircraft">No departing aircraft</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 1: Local Area -->
|
||
<div class="atc-section">
|
||
<h2>📍 Local Area <span class="count" id="local-count">0</span></h2>
|
||
<div class="aircraft-list" id="local-list">
|
||
<div class="no-aircraft">No aircraft in local area</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 1: Inbound -->
|
||
<div class="atc-section">
|
||
<h2>🛬 Inbound <span class="count" id="inbound-count">0</span></h2>
|
||
<div class="aircraft-list" id="inbound-list">
|
||
<div class="no-aircraft">No inbound aircraft</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: Booked Out -->
|
||
<div class="atc-section">
|
||
<h2>📋 Booked Out <span class="count" id="parked-count">0</span></h2>
|
||
<div class="aircraft-list" id="parked-list">
|
||
<div class="no-aircraft">No booked out aircraft</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: Circuit Traffic -->
|
||
<div class="atc-section">
|
||
<h2>🔄 Circuit Traffic <span class="count" id="circuit-count">0</span></h2>
|
||
<div class="aircraft-list" id="circuit-list">
|
||
<div class="no-aircraft">No aircraft in circuit</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: Today's PPRs -->
|
||
<div class="atc-section">
|
||
<h2>📝 Today's PPRs <span class="count" id="pending-ppr-count">0</span></h2>
|
||
<div class="aircraft-list" id="pending-ppr-list">
|
||
<div class="no-aircraft">No pending PPRs</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(), loadOverflights(), loadDeparted(), loadParked(), loadUpcoming()]);
|
||
loadPPRsTimeout = null;
|
||
}, 100); // Wait 100ms before executing to batch multiple calls
|
||
}
|
||
|
||
// Load arrivals (NEW and CONFIRMED status for PPR, DEPARTED for local flights)
|
||
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, local flights, and booked-in arrivals
|
||
const [pprResponse, localResponse, bookInResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&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 local flights in DEPARTED status (in the air, heading back) - only those booked out today
|
||
if (localResponse.ok) {
|
||
const localFlights = await localResponse.json();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const localInAir = localFlights
|
||
.filter(flight => {
|
||
// Only include flights booked out today (created_dt)
|
||
if (!flight.created_dt) return false;
|
||
const createdDate = flight.created_dt.split('T')[0];
|
||
return createdDate === today;
|
||
})
|
||
.map(flight => ({
|
||
...flight,
|
||
isLocalFlight: true // Flag to distinguish from PPR
|
||
}));
|
||
arrivals.push(...localInAir);
|
||
}
|
||
|
||
// 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, GROUND/LOCAL for local flights)
|
||
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, local flight departures, and airport departures simultaneously
|
||
const [pprResponse, localBookedOutResponse, localOutGroundResponse, localLocalResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
||
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000'),
|
||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&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 localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
||
const localOutGround = localOutGroundResponse.ok ? await localOutGroundResponse.json() : [];
|
||
const localLocal = localLocalResponse.ok ? await localLocalResponse.json() : [];
|
||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
||
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
|
||
|
||
// Combine local flights
|
||
const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal];
|
||
// 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 local flights (GROUND and LOCAL status - ready to go) - only those booked out today
|
||
const localDepartures = allLocalFlights
|
||
.filter(flight => {
|
||
// Only include flights booked out today (created_dt)
|
||
if (!flight.created_dt) return false;
|
||
const createdDate = flight.created_dt.split('T')[0];
|
||
return createdDate === today;
|
||
})
|
||
.map(flight => ({
|
||
...flight,
|
||
isLocalFlight: true // Flag to distinguish from PPR
|
||
}));
|
||
departures.push(...localDepartures);
|
||
|
||
// 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';
|
||
}
|
||
|
||
// 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.style.backgroundColor = '#ccffcc';
|
||
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 booked out aircraft (BOOKED_OUT status for departures only)
|
||
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;';
|
||
|
||
// 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>` : '';
|
||
|
||
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>
|
||
`;
|
||
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;
|
||
|
||
// 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-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Mark as Local">
|
||
LOCAL
|
||
</button>
|
||
<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 = `
|
||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'CIRCUIT')" title="Join Circuit">
|
||
CIRCUIT
|
||
</button>
|
||
${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 = `
|
||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
|
||
LOCAL
|
||
</button>
|
||
${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 || '-';
|
||
actionButtons = `
|
||
<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();
|
||
}
|
||
|
||
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;
|
||
|
||
// Click handler that routes to correct modal
|
||
row.onclick = () => {
|
||
if (isLocal) {
|
||
openLocalFlightEditModal(flight.id);
|
||
} else if (isDeparture) {
|
||
openDepartureEditModal(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') {
|
||
actionButtons = `
|
||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
|
||
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 {
|
||
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 {
|
||
// 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');
|
||
|
||
loadATCAircraft(); // 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 arrivals
|
||
async function updateArrivalStatusFromTable(arrivalId, status) {
|
||
if (!accessToken) 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');
|
||
|
||
loadATCAircraft(); // Refresh display
|
||
showNotification(`Arrival marked as ${status.toLowerCase()}`);
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
showNotification('Error updating arrival status', true);
|
||
}
|
||
}
|
||
|
||
|
||
// Override: ATC updates flight status to CIRCUIT then refreshes ATC display
|
||
async function afterCircuitSaved() {
|
||
if (currentLocalFlightId) {
|
||
await updateLocalFlightStatusFromTable(currentLocalFlightId, 'CIRCUIT');
|
||
} else if (currentArrivalId) {
|
||
await updateArrivalStatusFromTable(currentArrivalId, 'CIRCUIT');
|
||
}
|
||
closeCircuitModal();
|
||
loadATCAircraft();
|
||
}
|
||
|
||
async function loadATCAircraft() {
|
||
try {
|
||
await Promise.all([
|
||
loadDepartingAircraft(),
|
||
loadLocalAircraft(),
|
||
loadInboundAircraft(),
|
||
loadCircuitAircraft(),
|
||
loadPendingPPRs(),
|
||
loadParkedVisitors()
|
||
]);
|
||
} catch (error) {
|
||
console.error('Error loading ATC aircraft:', error);
|
||
}
|
||
}
|
||
|
||
// Load departing aircraft (ready to take off)
|
||
async function loadDepartingAircraft() {
|
||
try {
|
||
const [groundDeparturesResponse, groundLocalResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
||
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000')
|
||
]);
|
||
|
||
let groundAircraft = [];
|
||
if (groundDeparturesResponse.ok) groundAircraft = await groundDeparturesResponse.json();
|
||
if (groundLocalResponse.ok) groundAircraft = groundAircraft.concat((await groundLocalResponse.json()).map(l => ({ ...l, isLocalFlight: true })));
|
||
|
||
displayDepartingAircraft(groundAircraft.map(ac => ({
|
||
...ac,
|
||
isDeparture: !ac.isLocalFlight
|
||
})));
|
||
} catch (error) {
|
||
console.error('Error loading departing aircraft:', error);
|
||
}
|
||
}
|
||
|
||
function displayDepartingAircraft(aircraft) {
|
||
const container = document.getElementById('departing-list');
|
||
const countEl = document.getElementById('departing-count');
|
||
|
||
countEl.textContent = aircraft.length;
|
||
|
||
if (aircraft.length === 0) {
|
||
container.innerHTML = '<div class="no-aircraft">No departing aircraft</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = aircraft.map(ac => {
|
||
const reg = ac.ac_reg || ac.registration;
|
||
const type = ac.ac_type || ac.type;
|
||
const dest = ac.out_to;
|
||
const isLocal = ac.isLocalFlight;
|
||
|
||
// All aircraft in awaiting departure are in GROUND status
|
||
let takeoffOnclick, buttonText, buttonTitle, clickType;
|
||
if (isLocal) {
|
||
const takeoffStatus = ac.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
|
||
const takeoffTitle = ac.flight_type === 'CIRCUITS' ? 'Mark as Circuit' : 'Mark as Local';
|
||
takeoffOnclick = `event.stopPropagation(); currentLocalFlightId = '${ac.id}'; showTimestampModal('${takeoffStatus}', ${ac.id}, true)`;
|
||
buttonText = 'TAKE OFF';
|
||
buttonTitle = takeoffTitle;
|
||
clickType = 'local';
|
||
} else {
|
||
takeoffOnclick = `event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('LOCAL', ${ac.id}, false, true)`;
|
||
buttonText = 'TAKE OFF';
|
||
buttonTitle = 'Mark as Local';
|
||
clickType = 'departure';
|
||
}
|
||
const itemClass = isLocal ? 'local-flight' : 'departure';
|
||
|
||
return `
|
||
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${clickType}')">
|
||
<div class="aircraft-info">
|
||
<div class="aircraft-reg">${reg}</div>
|
||
<div class="aircraft-details">${type}${dest ? ` → ${dest}` : ` Local Flight`}</div>
|
||
</div>
|
||
<button class="status-btn" onclick="${takeoffOnclick}" title="${buttonTitle}">${buttonText}</button>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Load local area aircraft
|
||
async function loadLocalAircraft() {
|
||
try {
|
||
const response = await Promise.all([
|
||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'),
|
||
authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'),
|
||
authenticatedFetch('/api/v1/overflights/?status=ACTIVE&limit=1000')
|
||
]);
|
||
|
||
let locals = [];
|
||
if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true }));
|
||
if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true })));
|
||
if (response[2].ok) locals = locals.concat((await response[2].json()).map(a => ({ ...a, isArrival: true })));
|
||
if (response[3].ok) locals = locals.concat((await response[3].json()).map(o => ({ ...o, isOverflight: true })));
|
||
|
||
displayLocalAircraft(locals);
|
||
} catch (error) {
|
||
console.error('Error loading local aircraft:', error);
|
||
}
|
||
}
|
||
|
||
function displayLocalAircraft(aircraft) {
|
||
const container = document.getElementById('local-list');
|
||
const countEl = document.getElementById('local-count');
|
||
|
||
countEl.textContent = aircraft.length;
|
||
|
||
if (aircraft.length === 0) {
|
||
container.innerHTML = '<div class="no-aircraft">No aircraft in local area</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = aircraft.map(ac => {
|
||
const reg = ac.registration || ac.ac_reg || ac.callsign || '-';
|
||
const type = ac.type || ac.ac_type || ac.aircraft_type || '';
|
||
const dest = ac.out_to;
|
||
const isDeparture = ac.isDeparture;
|
||
|
||
let buttons;
|
||
if (isDeparture) {
|
||
// Departure in LOCAL status - show QSY and REJOIN buttons
|
||
buttons = `
|
||
<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>
|
||
<button class="status-btn" onclick="event.stopPropagation(); updateDepartureStatusFromTable('${ac.id}', 'CIRCUIT')">REJOIN</button>
|
||
`;
|
||
} else if (ac.isLocalFlight || ac.isArrival) {
|
||
// Local flight or arrival in LOCAL status - show REJOIN button
|
||
buttons = `<button class="status-btn" onclick="event.stopPropagation(); ${ac.isArrival ? `updateArrivalStatusFromTable('${ac.id}', 'CIRCUIT')` : `updateLocalFlightStatusFromTable('${ac.id}', 'CIRCUIT')`}">REJOIN</button>`;
|
||
} else if (ac.isOverflight) {
|
||
// Overflight in ACTIVE status - show QSY button
|
||
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentOverflightId = '${ac.id}'; showOverflightQSYModal()">QSY</button>`;
|
||
}
|
||
const itemClass = isDeparture ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
|
||
const detailsText = isDeparture ? `${type}${dest ? ` → ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} → ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` → ${dest}` : ` Local Flight`}`));
|
||
const entityType = isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local'));
|
||
|
||
return `
|
||
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
|
||
<div class="aircraft-info">
|
||
<div class="aircraft-reg">${reg}</div>
|
||
<div class="aircraft-details">${detailsText}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem;">
|
||
${buttons}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Load inbound aircraft
|
||
async function loadInboundAircraft() {
|
||
try {
|
||
const response = await Promise.all([
|
||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||
authenticatedFetch('/api/v1/arrivals/?status=INBOUND&limit=1000')
|
||
]);
|
||
|
||
const pprs = response[0].ok ? await response[0].json() : [];
|
||
const arrivals = response[1].ok ? await response[1].json() : [];
|
||
|
||
const today = new Date().toISOString().split('T')[0];
|
||
let inbound = pprs.filter(p => p.status === 'CONFIRMED' && p.eta && p.eta.split('T')[0] === today);
|
||
inbound = inbound.concat(arrivals.map(a => ({ ...a, isArrival: true })));
|
||
|
||
displayInboundAircraft(inbound);
|
||
} catch (error) {
|
||
console.error('Error loading inbound aircraft:', error);
|
||
}
|
||
}
|
||
|
||
function displayInboundAircraft(aircraft) {
|
||
const container = document.getElementById('inbound-list');
|
||
const countEl = document.getElementById('inbound-count');
|
||
|
||
countEl.textContent = aircraft.length;
|
||
|
||
if (aircraft.length === 0) {
|
||
container.innerHTML = '<div class="no-aircraft">No inbound aircraft</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = aircraft.map(ac => {
|
||
const reg = ac.ac_reg || ac.registration;
|
||
const type = ac.ac_type || ac.type;
|
||
const from = ac.in_from;
|
||
const eta = ac.eta;
|
||
|
||
let buttons = '';
|
||
if (ac.isArrival) {
|
||
// Arrival in INBOUND status - show LOCAL button
|
||
buttons = `<button class="status-btn" onclick="event.stopPropagation(); updateArrivalStatusFromTable('${ac.id}', 'LOCAL')">→ LOCAL</button>`;
|
||
}
|
||
|
||
return `
|
||
<div class="aircraft-item inbound" onclick="handleATCClick('${ac.id}', '${ac.isArrival ? 'arrival' : 'ppr'}')">
|
||
<div class="aircraft-info">
|
||
<div class="aircraft-reg">${reg}</div>
|
||
<div class="aircraft-details">${type} from ${from || 'Local Flight'}</div>
|
||
</div>
|
||
${buttons ? `<div style="display: flex; gap: 0.5rem;">${buttons}</div>` : '<div class="aircraft-status status-inbound">IB</div>'}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Load circuit traffic
|
||
async function loadCircuitAircraft() {
|
||
try {
|
||
const response = await Promise.all([
|
||
authenticatedFetch('/api/v1/local-flights/?status=DEPARTED&limit=1000&flight_type=CIRCUITS'),
|
||
authenticatedFetch('/api/v1/local-flights/?status=CIRCUIT&limit=1000'),
|
||
authenticatedFetch('/api/v1/arrivals/?status=CIRCUIT&limit=1000')
|
||
]);
|
||
|
||
let circuits = [];
|
||
if (response[0].ok) circuits = circuits.concat(await response[0].json());
|
||
if (response[1].ok) circuits = circuits.concat((await response[1].json()).map(l => ({ ...l, circuitStatus: getCircuitStatus(l.status) })));
|
||
if (response[2].ok) circuits = circuits.concat((await response[2].json()).map(a => ({ ...a, isArrival: true, circuitStatus: getCircuitStatus(a.status) })));
|
||
|
||
displayCircuitAircraft(circuits);
|
||
} catch (error) {
|
||
console.error('Error loading circuit aircraft:', error);
|
||
}
|
||
}
|
||
|
||
// Helper function to determine circuit status from API response
|
||
function getCircuitStatus(status) {
|
||
if (status === 'CIRCUIT_DOWNWIND') return 'DOWNWIND';
|
||
if (status === 'CIRCUIT_BASE') return 'BASE';
|
||
if (status === 'CIRCUIT_FINAL') return 'FINAL';
|
||
return 'CIRCUIT';
|
||
}
|
||
|
||
function displayCircuitAircraft(aircraft) {
|
||
const container = document.getElementById('circuit-list');
|
||
const countEl = document.getElementById('circuit-count');
|
||
|
||
// Define status order for sorting
|
||
const statusOrder = {
|
||
'CIRCUIT': 1,
|
||
'DOWNWIND': 2,
|
||
'BASE': 3,
|
||
'FINAL': 4
|
||
};
|
||
|
||
// Sort aircraft by circuit status
|
||
aircraft.sort((a, b) => {
|
||
const aStatus = a.circuitStatus || 'CIRCUIT';
|
||
const bStatus = b.circuitStatus || 'CIRCUIT';
|
||
return statusOrder[aStatus] - statusOrder[bStatus];
|
||
});
|
||
|
||
countEl.textContent = aircraft.length;
|
||
|
||
if (aircraft.length === 0) {
|
||
container.innerHTML = '<div class="no-aircraft">No aircraft in circuit</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = aircraft.map(ac => {
|
||
const isArrival = ac.isArrival;
|
||
const entityType = isArrival ? 'arrival' : 'local';
|
||
const updateFunction = isArrival ? 'updateArrivalStatusFromTable' : 'updateLocalFlightStatusFromTable';
|
||
const landFunction = isArrival ? `${updateFunction}('${ac.id}', 'LANDED')` : `showTimestampModal('LANDED', ${ac.id}, true)`;
|
||
const circuitStatus = ac.circuitStatus || 'CIRCUIT';
|
||
|
||
let buttons = `
|
||
<button class="status-btn" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'LOCAL')">LOCAL</button>
|
||
`;
|
||
|
||
// Circuit position buttons - show all, highlight current
|
||
const downwindClass = circuitStatus === 'DOWNWIND' ? 'active-position' : '';
|
||
const baseClass = circuitStatus === 'BASE' ? 'active-position' : '';
|
||
const finalClass = circuitStatus === 'FINAL' ? 'active-position' : '';
|
||
|
||
buttons += `<button class="status-btn small-btn ${downwindClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_DOWNWIND')" title="Downwind">D</button>`;
|
||
buttons += `<button class="status-btn small-btn ${baseClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_BASE')" title="Base">B</button>`;
|
||
buttons += `<button class="status-btn small-btn ${finalClass}" onclick="event.stopPropagation(); ${updateFunction}('${ac.id}', 'CIRCUIT_FINAL')" title="Final">F</button>`;
|
||
|
||
// Show T&G for both local flights and arrivals - resets to CIRCUIT
|
||
const tgFunction = isArrival
|
||
? `currentArrivalId = '${ac.id}'; showCircuitModal(null, '${ac.id}')`
|
||
: `currentLocalFlightId = '${ac.id}'; showCircuitModal('${ac.id}')`;
|
||
buttons += `<button class="status-btn" onclick="event.stopPropagation(); ${tgFunction}">T&G</button>`;
|
||
|
||
buttons += `<button class="status-btn" onclick="event.stopPropagation(); ${landFunction}">LAND</button>`;
|
||
|
||
const itemClass = isArrival ? 'inbound' : 'circuit';
|
||
|
||
// Display text: for arrivals show origin, for local flights show type
|
||
const displayText = isArrival ? `${ac.type} from ${ac.in_from || '?'}` : `${ac.type}`;
|
||
|
||
return `
|
||
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
|
||
<div class="aircraft-info">
|
||
<div class="aircraft-reg">${ac.registration}</div>
|
||
<div class="aircraft-details">${displayText}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.25rem;">
|
||
${buttons}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Load pending PPRs
|
||
async function loadPendingPPRs() {
|
||
try {
|
||
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
|
||
const pprs = response.ok ? await response.json() : [];
|
||
|
||
const pending = pprs.filter(p => p.status === 'NEW' || p.status === 'CONFIRMED');
|
||
displayPendingPPRs(pending);
|
||
} catch (error) {
|
||
console.error('Error loading pending PPRs:', error);
|
||
}
|
||
}
|
||
|
||
function displayPendingPPRs(pprs) {
|
||
const container = document.getElementById('pending-ppr-list');
|
||
const countEl = document.getElementById('pending-ppr-count');
|
||
|
||
countEl.textContent = pprs.length;
|
||
|
||
if (pprs.length === 0) {
|
||
container.innerHTML = '<div class="no-aircraft">No pending PPRs</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = pprs.map(ppr => `
|
||
<div class="aircraft-item" onclick="handleATCClick('${ppr.id}', 'ppr')">
|
||
<div class="aircraft-info">
|
||
<div class="aircraft-reg">${ppr.ac_reg}</div>
|
||
<div class="aircraft-details">${ppr.ac_type} from ${ppr.in_from}${ppr.eta ? ' ETA ' + formatTimeOnly(ppr.eta) : ''}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.25rem; align-items: center;">
|
||
<div class="aircraft-status status-pending">PPR</div>
|
||
<button class="status-btn" onclick="event.stopPropagation(); activatePPR(${ppr.id}, '${ppr.ac_reg}', ${ppr.out_to ? 'true' : 'false'})">ACTIVATE</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
async function activatePPR(pprId, acReg, hasDeparture) {
|
||
const msg = `Activate PPR for ${acReg}?\nThis will create an INBOUND arrival.`
|
||
+ (hasDeparture ? '\nThe outbound departure will appear automatically when the aircraft lands.' : '');
|
||
if (!confirm(msg)) return;
|
||
|
||
try {
|
||
const response = await authenticatedFetch(`/api/v1/pprs/${pprId}/activate`, { method: 'POST' });
|
||
if (!response.ok) {
|
||
const err = await response.json().catch(() => ({}));
|
||
showNotification(err.detail || 'Failed to activate PPR', true);
|
||
return;
|
||
}
|
||
const result = await response.json();
|
||
showNotification(result.message || 'PPR activated');
|
||
await loadPPRs();
|
||
} catch (error) {
|
||
console.error('Error activating PPR:', error);
|
||
showNotification('Error activating PPR', true);
|
||
}
|
||
}
|
||
|
||
// Load parked visitors
|
||
async function loadParkedVisitors() {
|
||
try {
|
||
const [localBookedOutResponse, depBookedOutResponse] = await Promise.all([
|
||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000')
|
||
]);
|
||
|
||
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||
|
||
// Filter for today's bookings
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const bookedOutAircraft = [
|
||
...localBookedOut.filter(flight => {
|
||
const createdDate = flight.created_dt.split('T')[0];
|
||
return createdDate === today;
|
||
}).map(flight => ({
|
||
...flight,
|
||
isLocalFlight: true
|
||
})),
|
||
...depBookedOut.filter(flight => {
|
||
const createdDate = flight.created_dt.split('T')[0];
|
||
return createdDate === today;
|
||
}).map(flight => ({
|
||
...flight,
|
||
isDeparture: true
|
||
}))
|
||
];
|
||
|
||
displayParkedVisitors(bookedOutAircraft);
|
||
} catch (error) {
|
||
console.error('Error loading booked out aircraft:', error);
|
||
}
|
||
}
|
||
|
||
function displayParkedVisitors(bookedOutAircraft) {
|
||
const container = document.getElementById('parked-list');
|
||
const countEl = document.getElementById('parked-count');
|
||
|
||
countEl.textContent = bookedOutAircraft.length;
|
||
|
||
if (bookedOutAircraft.length === 0) {
|
||
container.innerHTML = '<div class="no-aircraft">No booked out aircraft</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = bookedOutAircraft.map(flight => {
|
||
const reg = flight.registration || flight.ac_reg;
|
||
const type = flight.type || flight.ac_type;
|
||
const dest = flight.out_to;
|
||
const createdTime = flight.created_dt ? formatTimeOnly(flight.created_dt) : '';
|
||
|
||
// Determine the entity type and display details
|
||
let entityType, displayDetails, clickHandler, cssClass;
|
||
if (flight.isLocalFlight) {
|
||
entityType = 'local';
|
||
displayDetails = `${type} - ${flight.flight_type || 'Local Flight'}`;
|
||
clickHandler = `handleATCClick('${flight.id}', 'local')`;
|
||
cssClass = 'local-flight';
|
||
} else {
|
||
entityType = 'departure';
|
||
displayDetails = `${type} → ${dest || 'Other Airport'}`;
|
||
clickHandler = `handleATCClick('${flight.id}', 'departure')`;
|
||
cssClass = 'departure';
|
||
}
|
||
|
||
return `
|
||
<div class="aircraft-item ${cssClass}" onclick="${clickHandler}">
|
||
<div class="aircraft-info">
|
||
<div class="aircraft-reg">${reg}</div>
|
||
<div class="aircraft-details">${displayDetails}</div>
|
||
</div>
|
||
<button class="status-btn" onclick="event.stopPropagation(); ${flight.isLocalFlight ? `currentLocalFlightId = '${flight.id}'; showTimestampModal('GROUND', '${flight.id}', true, false)` : `currentDepartureId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, false, true)`}" title="Contact Pilot">CONTACT</button>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Handle ATC aircraft item click - open appropriate modal
|
||
function handleATCClick(entityId, entityType) {
|
||
switch(entityType) {
|
||
case 'ppr':
|
||
openPPRModal(entityId);
|
||
break;
|
||
case 'local':
|
||
openLocalFlightEditModal(entityId);
|
||
break;
|
||
case 'departure':
|
||
openDepartureEditModal(entityId);
|
||
break;
|
||
case 'arrival':
|
||
openArrivalEditModal(entityId);
|
||
break;
|
||
case 'overflight':
|
||
openOverflightEditModal(entityId);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Override authentication to also load ATC data
|
||
const originalInitializeAuth = initializeAuth;
|
||
initializeAuth = async function() {
|
||
await originalInitializeAuth();
|
||
if (accessToken) {
|
||
loadATCAircraft();
|
||
// Refresh every 30 seconds
|
||
setInterval(loadATCAircraft, 30000);
|
||
}
|
||
};
|
||
|
||
// Override loadPPRs to use ATC functions instead (fixes WebSocket and other event triggers)
|
||
const originalLoadPPRs = loadPPRs;
|
||
loadPPRs = async function() {
|
||
try {
|
||
await loadATCAircraft();
|
||
} catch (error) {
|
||
console.error('Error loading ATC aircraft:', error);
|
||
}
|
||
};
|
||
|
||
// Override loadDepartures to use ATC functions instead (fixes circuit recording refresh)
|
||
const originalLoadDepartures = loadDepartures;
|
||
loadDepartures = async function() {
|
||
try {
|
||
await loadATCAircraft();
|
||
} catch (error) {
|
||
console.error('Error loading ATC aircraft:', error);
|
||
}
|
||
};
|
||
|
||
</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>
|