1078 lines
36 KiB
HTML
1078 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PPR Admin Interface</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: #f5f5f5;
|
||
color: #333;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #2c3e50, #3498db);
|
||
color: white;
|
||
padding: 1rem 2rem;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header h1 {
|
||
margin: 0;
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
.header .user-info {
|
||
float: right;
|
||
font-size: 0.9rem;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.controls {
|
||
background: white;
|
||
padding: 1.5rem;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
margin-bottom: 2rem;
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
padding: 0.7rem 1.5rem;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: #2980b9;
|
||
}
|
||
|
||
.btn-success {
|
||
background-color: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background-color: #229954;
|
||
}
|
||
|
||
.btn-warning {
|
||
background-color: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background-color: #e67e22;
|
||
}
|
||
|
||
.btn-danger {
|
||
background-color: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background-color: #c0392b;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 0.4rem 0.8rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-group label {
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
|
||
.filter-group select, .filter-group input {
|
||
padding: 0.5rem;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.ppr-table {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table-header {
|
||
background: #34495e;
|
||
color: white;
|
||
padding: 1rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: #666;
|
||
}
|
||
|
||
.spinner {
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #3498db;
|
||
border-radius: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th, td {
|
||
padding: 0.8rem;
|
||
text-align: left;
|
||
border-bottom: 1px solid #eee;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
th {
|
||
background-color: #f8f9fa;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
|
||
tbody tr {
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
tbody tr:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.status {
|
||
display: inline-block;
|
||
padding: 0.3rem 0.6rem;
|
||
border-radius: 12px;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status.new { background: #e3f2fd; color: #1565c0; }
|
||
.status.confirmed { background: #e8f5e8; color: #2e7d32; }
|
||
.status.landed { background: #fff3e0; color: #ef6c00; }
|
||
.status.departed { background: #fce4ec; color: #c2185b; }
|
||
.status.canceled { background: #ffebee; color: #d32f2f; }
|
||
.status.deleted { background: #f3e5f5; color: #7b1fa2; }
|
||
|
||
.no-data {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: #666;
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
margin: 5% auto;
|
||
padding: 0;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 800px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.modal-header {
|
||
background: #34495e;
|
||
color: white;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px 8px 0 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.close {
|
||
color: white;
|
||
font-size: 1.5rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
border: none;
|
||
background: none;
|
||
}
|
||
|
||
.close:hover {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.form-group.full-width {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.form-group label {
|
||
font-weight: 600;
|
||
margin-bottom: 0.3rem;
|
||
color: #555;
|
||
}
|
||
|
||
.form-group input, .form-group select, .form-group textarea {
|
||
padding: 0.6rem;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||
outline: none;
|
||
border-color: #3498db;
|
||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: flex-end;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.journal-section {
|
||
margin-top: 2rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.journal-entries {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
padding: 1rem;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.journal-entry {
|
||
margin-bottom: 0.8rem;
|
||
padding-bottom: 0.8rem;
|
||
border-bottom: 1px solid #ddd;
|
||
}
|
||
|
||
.journal-entry:last-child {
|
||
border-bottom: none;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.journal-meta {
|
||
font-size: 0.8rem;
|
||
color: #666;
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
.journal-text {
|
||
font-size: 0.9rem;
|
||
color: #333;
|
||
}
|
||
|
||
.quick-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.controls {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 95%;
|
||
margin: 2% auto;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>✈️ PPR Administration</h1>
|
||
<div class="user-info">
|
||
Logged in as: <span id="current-user">Loading...</span> |
|
||
<a href="#" onclick="logout()" style="color: white;">Logout</a>
|
||
</div>
|
||
<div style="clear: both;"></div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="controls">
|
||
<button class="btn btn-success" onclick="openNewPPRModal()">
|
||
➕ New PPR Entry
|
||
</button>
|
||
|
||
<div class="filter-group">
|
||
<label for="viewDate">Date:</label>
|
||
<input type="date" id="viewDate" onchange="loadPPRs()">
|
||
</div>
|
||
|
||
<button class="btn btn-primary" onclick="loadPPRs()">
|
||
🔄 Refresh
|
||
</button>
|
||
|
||
<div class="filter-group" style="margin-left: auto;">
|
||
<label>
|
||
<input type="checkbox" id="showAllStatuses" onchange="loadPPRs()">
|
||
Show all statuses (including departed/canceled)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Arrivals Table -->
|
||
<div class="ppr-table">
|
||
<div class="table-header">
|
||
🛬 Today's Arrivals - <span id="arrivals-count">0</span> entries (NEW & CONFIRMED)
|
||
</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>Type</th>
|
||
<th>Captain</th>
|
||
<th>From</th>
|
||
<th>ETA Time</th>
|
||
<th>POB</th>
|
||
<th>Fuel</th>
|
||
<th>Status</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 arrivals for today</h3>
|
||
<p>No NEW or CONFIRMED arrivals scheduled for today.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Departures Table -->
|
||
<div class="ppr-table" style="margin-top: 2rem;">
|
||
<div class="table-header">
|
||
🛫 Today's Departures - <span id="departures-count">0</span> entries (LANDED)
|
||
</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>Type</th>
|
||
<th>Captain</th>
|
||
<th>To</th>
|
||
<th>ETD Time</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 departures for today</h3>
|
||
<p>No aircraft currently landed and ready to depart.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PPR Detail/Edit Modal -->
|
||
<div id="pprModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2 id="modal-title">PPR Details</h2>
|
||
<button class="close" onclick="closePPRModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="quick-actions">
|
||
<button id="btn-confirm" class="btn btn-success btn-sm" onclick="updateStatus('CONFIRMED')">
|
||
✓ Confirm
|
||
</button>
|
||
<button id="btn-landed" class="btn btn-warning btn-sm" onclick="updateStatus('LANDED')">
|
||
🛬 Landed
|
||
</button>
|
||
<button id="btn-departed" class="btn btn-primary btn-sm" onclick="updateStatus('DEPARTED')">
|
||
🛫 Departed
|
||
</button>
|
||
<button id="btn-cancel" class="btn btn-danger btn-sm" onclick="updateStatus('CANCELED')">
|
||
❌ Cancel
|
||
</button>
|
||
</div>
|
||
|
||
<form id="ppr-form">
|
||
<input type="hidden" id="ppr-id" name="id">
|
||
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label for="ac_reg">Aircraft Registration *</label>
|
||
<input type="text" id="ac_reg" name="ac_reg" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="ac_type">Aircraft Type *</label>
|
||
<input type="text" id="ac_type" name="ac_type" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="ac_call">Callsign</label>
|
||
<input type="text" id="ac_call" name="ac_call">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="captain">Captain *</label>
|
||
<input type="text" id="captain" name="captain" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="in_from">Arriving From *</label>
|
||
<input type="text" id="in_from" name="in_from" required placeholder="ICAO Code">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="eta">ETA *</label>
|
||
<input type="datetime-local" id="eta" name="eta" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="pob_in">POB Inbound *</label>
|
||
<input type="number" id="pob_in" name="pob_in" required min="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="fuel">Fuel Required</label>
|
||
<select id="fuel" name="fuel">
|
||
<option value="">None</option>
|
||
<option value="100LL">100LL</option>
|
||
<option value="JET A1">JET A1</option>
|
||
<option value="FULL">Full Tanks</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="out_to">Departing To</label>
|
||
<input type="text" id="out_to" name="out_to" placeholder="ICAO Code">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="etd">ETD</label>
|
||
<input type="datetime-local" id="etd" name="etd">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="pob_out">POB Outbound</label>
|
||
<input type="number" id="pob_out" name="pob_out" min="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="email">Email</label>
|
||
<input type="email" id="email" name="email">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="phone">Phone</label>
|
||
<input type="tel" id="phone" name="phone">
|
||
</div>
|
||
<div class="form-group full-width">
|
||
<label for="notes">Notes</label>
|
||
<textarea id="notes" name="notes" rows="3"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-danger" id="delete-btn" onclick="deletePPR()" style="display: none;">
|
||
🗑️ Delete
|
||
</button>
|
||
<button type="button" class="btn btn-primary" onclick="closePPRModal()">
|
||
Cancel
|
||
</button>
|
||
<button type="submit" class="btn btn-success">
|
||
💾 Save Changes
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="journal-section" id="journal-section">
|
||
<h3>Activity Journal</h3>
|
||
<div id="journal-entries" class="journal-entries">
|
||
Loading journal...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentUser = null;
|
||
let accessToken = null;
|
||
let currentPPRId = null;
|
||
let isNewPPR = false;
|
||
|
||
// Initialize the application
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
authenticate();
|
||
});
|
||
|
||
// Authentication
|
||
function authenticate() {
|
||
const username = prompt('Username:');
|
||
const password = prompt('Password:');
|
||
|
||
if (!username || !password) {
|
||
alert('Authentication required');
|
||
return;
|
||
}
|
||
|
||
fetch('/api/v1/auth/login', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.access_token) {
|
||
accessToken = data.access_token;
|
||
currentUser = username;
|
||
document.getElementById('current-user').textContent = username;
|
||
loadPPRs();
|
||
setDefaultDates();
|
||
} else {
|
||
alert('Authentication failed');
|
||
authenticate();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Auth error:', error);
|
||
alert('Authentication failed');
|
||
authenticate();
|
||
});
|
||
}
|
||
|
||
function logout() {
|
||
accessToken = null;
|
||
currentUser = null;
|
||
location.reload();
|
||
}
|
||
|
||
function setDefaultDates() {
|
||
const today = new Date();
|
||
document.getElementById('viewDate').value = today.toISOString().split('T')[0];
|
||
}
|
||
|
||
// Load PPR records - now loads both arrivals and departures
|
||
async function loadPPRs() {
|
||
if (!accessToken) return;
|
||
|
||
// Load both arrivals and departures simultaneously
|
||
await Promise.all([loadArrivals(), loadDepartures()]);
|
||
}
|
||
|
||
// Load arrivals (NEW and CONFIRMED status)
|
||
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 {
|
||
const viewDate = document.getElementById('viewDate').value;
|
||
const showAll = document.getElementById('showAllStatuses').checked;
|
||
|
||
let url = '/api/v1/pprs/?limit=1000';
|
||
if (viewDate) {
|
||
url += `&date_from=${viewDate}&date_to=${viewDate}`;
|
||
}
|
||
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch arrivals');
|
||
}
|
||
|
||
const allPPRs = await response.json();
|
||
|
||
// Filter for arrivals (NEW, CONFIRMED, or all if showAll is checked)
|
||
let arrivals;
|
||
if (showAll) {
|
||
// Show all PPRs with ETA for the selected date
|
||
arrivals = allPPRs.filter(ppr => ppr.eta);
|
||
} else {
|
||
// Show only NEW and CONFIRMED with ETA
|
||
arrivals = allPPRs.filter(ppr =>
|
||
(ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && ppr.eta
|
||
);
|
||
}
|
||
|
||
displayArrivals(arrivals);
|
||
} catch (error) {
|
||
console.error('Error loading arrivals:', error);
|
||
alert('Error loading arrivals');
|
||
}
|
||
|
||
document.getElementById('arrivals-loading').style.display = 'none';
|
||
}
|
||
|
||
// Load departures (LANDED status)
|
||
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 {
|
||
const viewDate = document.getElementById('viewDate').value;
|
||
const showAll = document.getElementById('showAllStatuses').checked;
|
||
|
||
let url = '/api/v1/pprs/?limit=1000';
|
||
if (viewDate) {
|
||
url += `&date_from=${viewDate}&date_to=${viewDate}`;
|
||
}
|
||
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch departures');
|
||
}
|
||
|
||
const allPPRs = await response.json();
|
||
|
||
// Filter for departures (LANDED, or include DEPARTED if showAll is checked)
|
||
let departures;
|
||
if (showAll) {
|
||
departures = allPPRs.filter(ppr =>
|
||
ppr.status === 'LANDED' || ppr.status === 'DEPARTED'
|
||
);
|
||
} else {
|
||
departures = allPPRs.filter(ppr => ppr.status === 'LANDED');
|
||
}
|
||
|
||
displayDepartures(departures);
|
||
} catch (error) {
|
||
console.error('Error loading departures:', error);
|
||
alert('Error loading departures');
|
||
}
|
||
|
||
document.getElementById('departures-loading').style.display = 'none';
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
tbody.innerHTML = '';
|
||
document.getElementById('arrivals-table-content').style.display = 'block';
|
||
|
||
arrivals.forEach(ppr => {
|
||
const row = document.createElement('tr');
|
||
row.onclick = () => openPPRModal(ppr.id);
|
||
|
||
row.innerHTML = `
|
||
<td><strong>${ppr.ac_reg}</strong></td>
|
||
<td>${ppr.ac_type}</td>
|
||
<td>${ppr.captain}</td>
|
||
<td>${ppr.in_from}</td>
|
||
<td>${formatTimeOnly(ppr.eta)}</td>
|
||
<td>${ppr.pob_in}</td>
|
||
<td>${ppr.fuel || '-'}</td>
|
||
<td><span class="status ${ppr.status.toLowerCase()}">${ppr.status}</span></td>
|
||
<td>
|
||
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openPPRModal(${ppr.id})">
|
||
Edit
|
||
</button>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
tbody.innerHTML = '';
|
||
document.getElementById('departures-table-content').style.display = 'block';
|
||
|
||
departures.forEach(ppr => {
|
||
const row = document.createElement('tr');
|
||
row.onclick = () => openPPRModal(ppr.id);
|
||
|
||
row.innerHTML = `
|
||
<td><strong>${ppr.ac_reg}</strong></td>
|
||
<td>${ppr.ac_type}</td>
|
||
<td>${ppr.captain}</td>
|
||
<td>${ppr.out_to || '-'}</td>
|
||
<td>${ppr.etd ? formatTimeOnly(ppr.etd) : '-'}</td>
|
||
<td>${ppr.pob_out || ppr.pob_in}</td>
|
||
<td>${ppr.fuel || '-'}</td>
|
||
<td>${ppr.landed_dt ? formatTimeOnly(ppr.landed_dt) : '-'}</td>
|
||
<td>
|
||
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openPPRModal(${ppr.id})">
|
||
Edit
|
||
</button>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
function formatTimeOnly(dateStr) {
|
||
if (!dateStr) return '-';
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleTimeString('en-GB', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
function formatDateTime(dateStr) {
|
||
if (!dateStr) return '-';
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString('en-GB', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
// Modal functions
|
||
function openNewPPRModal() {
|
||
isNewPPR = true;
|
||
currentPPRId = null;
|
||
document.getElementById('modal-title').textContent = 'New PPR Entry';
|
||
document.getElementById('delete-btn').style.display = 'none';
|
||
document.getElementById('journal-section').style.display = 'none';
|
||
document.querySelector('.quick-actions').style.display = 'none';
|
||
|
||
// Clear form
|
||
document.getElementById('ppr-form').reset();
|
||
document.getElementById('ppr-id').value = '';
|
||
|
||
document.getElementById('pprModal').style.display = 'block';
|
||
}
|
||
|
||
async function openPPRModal(pprId) {
|
||
if (!accessToken) return;
|
||
|
||
isNewPPR = false;
|
||
currentPPRId = pprId;
|
||
document.getElementById('modal-title').textContent = 'Edit PPR Entry';
|
||
document.getElementById('delete-btn').style.display = 'inline-block';
|
||
document.querySelector('.quick-actions').style.display = 'flex';
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/pprs/${pprId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch PPR details');
|
||
}
|
||
|
||
const ppr = await response.json();
|
||
populateForm(ppr);
|
||
await loadJournal(pprId); // Always load journal when opening a PPR
|
||
|
||
document.getElementById('pprModal').style.display = 'block';
|
||
} catch (error) {
|
||
console.error('Error loading PPR details:', error);
|
||
alert('Error loading PPR details');
|
||
}
|
||
}
|
||
|
||
function populateForm(ppr) {
|
||
Object.keys(ppr).forEach(key => {
|
||
const field = document.getElementById(key);
|
||
if (field) {
|
||
if (key === 'eta' || key === 'etd') {
|
||
if (ppr[key]) {
|
||
field.value = new Date(ppr[key]).toISOString().slice(0, 16);
|
||
}
|
||
} else {
|
||
field.value = ppr[key] || '';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function loadJournal(pprId) {
|
||
try {
|
||
const response = await fetch(`/api/v1/pprs/${pprId}/journal`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch journal');
|
||
}
|
||
|
||
const entries = await response.json();
|
||
displayJournal(entries);
|
||
} catch (error) {
|
||
console.error('Error loading journal:', error);
|
||
document.getElementById('journal-entries').innerHTML = 'Error loading journal entries';
|
||
}
|
||
}
|
||
|
||
function displayJournal(entries) {
|
||
const container = document.getElementById('journal-entries');
|
||
|
||
if (entries.length === 0) {
|
||
container.innerHTML = '<p>No journal entries yet.</p>';
|
||
} else {
|
||
container.innerHTML = entries.map(entry => `
|
||
<div class="journal-entry">
|
||
<div class="journal-meta">
|
||
${formatDateTime(entry.entry_dt)} by ${entry.user}
|
||
</div>
|
||
<div class="journal-text">${entry.entry}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Always show journal section when displaying entries
|
||
document.getElementById('journal-section').style.display = 'block';
|
||
}
|
||
|
||
function closePPRModal() {
|
||
document.getElementById('pprModal').style.display = 'none';
|
||
currentPPRId = null;
|
||
isNewPPR = false;
|
||
}
|
||
|
||
// Form submission
|
||
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
if (!accessToken) return;
|
||
|
||
const formData = new FormData(this);
|
||
const pprData = {};
|
||
|
||
formData.forEach((value, key) => {
|
||
if (key !== 'id' && value.trim() !== '') {
|
||
if (key === 'pob_in' || key === 'pob_out') {
|
||
pprData[key] = parseInt(value);
|
||
} else {
|
||
pprData[key] = value;
|
||
}
|
||
}
|
||
});
|
||
|
||
try {
|
||
let response;
|
||
if (isNewPPR) {
|
||
response = await fetch('/api/v1/pprs/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${accessToken}`
|
||
},
|
||
body: JSON.stringify(pprData)
|
||
});
|
||
} else {
|
||
response = await fetch(`/api/v1/pprs/${currentPPRId}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${accessToken}`
|
||
},
|
||
body: JSON.stringify(pprData)
|
||
});
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to save PPR');
|
||
}
|
||
|
||
closePPRModal();
|
||
loadPPRs(); // Refresh both tables
|
||
alert(isNewPPR ? 'PPR created successfully!' : 'PPR updated successfully!');
|
||
} catch (error) {
|
||
console.error('Error saving PPR:', error);
|
||
alert('Error saving PPR');
|
||
}
|
||
});
|
||
|
||
// Status update functions
|
||
async function updateStatus(status) {
|
||
if (!currentPPRId || !accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/pprs/${currentPPRId}/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');
|
||
}
|
||
|
||
await loadJournal(currentPPRId); // Refresh journal
|
||
loadPPRs(); // Refresh both tables
|
||
alert(`Status updated to ${status}`);
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
alert('Error updating status');
|
||
}
|
||
}
|
||
|
||
async function deletePPR() {
|
||
if (!currentPPRId || !accessToken) return;
|
||
|
||
if (!confirm('Are you sure you want to delete this PPR? This action cannot be undone.')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/pprs/${currentPPRId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete PPR');
|
||
}
|
||
|
||
closePPRModal();
|
||
loadPPRs(); // Refresh both tables
|
||
alert('PPR deleted successfully!');
|
||
} catch (error) {
|
||
console.error('Error deleting PPR:', error);
|
||
alert('Error deleting PPR');
|
||
}
|
||
}
|
||
|
||
// Close modal when clicking outside
|
||
window.onclick = function(event) {
|
||
const modal = document.getElementById('pprModal');
|
||
if (event.target === modal) {
|
||
closePPRModal();
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |