1423 lines
49 KiB
HTML
1423 lines
49 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;
|
||
}
|
||
|
||
.top-menu {
|
||
background: #2c3e50;
|
||
padding: 1rem 2rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.menu-left {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.menu-right {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.notes-indicator {
|
||
display: inline-block;
|
||
background-color: #ffc107;
|
||
color: #856404;
|
||
font-size: 0.8rem;
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
margin-left: 5px;
|
||
cursor: help;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.notes-tooltip {
|
||
position: relative;
|
||
}
|
||
|
||
.notes-tooltip .tooltip-text {
|
||
visibility: hidden;
|
||
width: 300px;
|
||
background-color: #333;
|
||
color: #fff;
|
||
text-align: left;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
position: absolute;
|
||
z-index: 1000;
|
||
bottom: 50%;
|
||
left: 100%;
|
||
margin-left: 10px;
|
||
margin-bottom: -20px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
font-size: 0.9rem;
|
||
line-height: 1.4;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.notes-tooltip .tooltip-text::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 50%;
|
||
left: -5px;
|
||
margin-top: -5px;
|
||
border-width: 5px;
|
||
border-style: solid;
|
||
border-color: transparent #333 transparent transparent;
|
||
}
|
||
|
||
.notes-tooltip:hover .tooltip-text {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
#login-form .form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
#login-form .form-group input {
|
||
width: 100%;
|
||
}
|
||
|
||
#login-error {
|
||
background-color: #ffebee;
|
||
border: 1px solid #ffcdd2;
|
||
border-radius: 4px;
|
||
padding: 0.8rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* Aircraft Lookup Styles */
|
||
#aircraft-lookup-results {
|
||
margin-top: 0.5rem;
|
||
padding: 0.5rem;
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
min-height: 20px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.aircraft-match {
|
||
padding: 0.3rem;
|
||
background-color: #e8f5e8;
|
||
border: 1px solid #c3e6c3;
|
||
border-radius: 4px;
|
||
font-family: 'Courier New', monospace;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.aircraft-no-match {
|
||
color: #6c757d;
|
||
font-style: italic;
|
||
}
|
||
|
||
.aircraft-searching {
|
||
color: #007bff;
|
||
}
|
||
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background-color: #27ae60;
|
||
color: white;
|
||
padding: 12px 20px;
|
||
border-radius: 5px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||
z-index: 10000;
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
transition: all 0.3s ease;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.notification.show {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.notification.error {
|
||
background-color: #e74c3c;
|
||
}
|
||
</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="top-menu">
|
||
<div class="menu-left">
|
||
<button class="btn btn-success" onclick="openNewPPRModal()">
|
||
➕ New PPR Entry
|
||
</button>
|
||
</div>
|
||
<div class="menu-right">
|
||
<button class="btn btn-primary" onclick="loadPPRs()">
|
||
🔄 Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
|
||
<!-- 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>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>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>
|
||
|
||
<!-- Login Modal -->
|
||
<div id="loginModal" class="modal" style="display: none;">
|
||
<div class="modal-content" style="max-width: 400px;">
|
||
<div class="modal-header">
|
||
<h2>PPR Admin Login</h2>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="login-form">
|
||
<div class="form-group">
|
||
<label for="login-username">Username:</label>
|
||
<input type="text" id="login-username" name="username" required autofocus>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="login-password">Password:</label>
|
||
<input type="password" id="login-password" name="password" required>
|
||
</div>
|
||
<div id="login-error" style="color: #dc3545; margin: 1rem 0; display: none;"></div>
|
||
<div class="form-actions" style="border-top: none; padding-top: 0;">
|
||
<button type="submit" class="btn btn-success" id="login-btn">
|
||
🔐 Login
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</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 oninput="handleAircraftLookup(this.value)">
|
||
<div id="aircraft-lookup-results"></div>
|
||
</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 (Local Time) *</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 (Local Time)</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>
|
||
|
||
<!-- Success Notification -->
|
||
<div id="notification" class="notification"></div>
|
||
|
||
<script>
|
||
let currentUser = null;
|
||
let accessToken = null;
|
||
let currentPPRId = null;
|
||
let isNewPPR = false;
|
||
|
||
// Notification system
|
||
function showNotification(message, isError = false) {
|
||
const notification = document.getElementById('notification');
|
||
notification.textContent = message;
|
||
notification.className = 'notification' + (isError ? ' error' : '');
|
||
|
||
// Show notification
|
||
setTimeout(() => {
|
||
notification.classList.add('show');
|
||
}, 10);
|
||
|
||
// Hide after 3 seconds
|
||
setTimeout(() => {
|
||
notification.classList.remove('show');
|
||
}, 3000);
|
||
}
|
||
|
||
// Initialize the application
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializeAuth();
|
||
setupLoginForm();
|
||
});
|
||
|
||
// Authentication management
|
||
function initializeAuth() {
|
||
// Try to get cached token
|
||
const cachedToken = localStorage.getItem('ppr_access_token');
|
||
const cachedUser = localStorage.getItem('ppr_username');
|
||
const tokenExpiry = localStorage.getItem('ppr_token_expiry');
|
||
|
||
if (cachedToken && cachedUser && tokenExpiry) {
|
||
const now = new Date().getTime();
|
||
if (now < parseInt(tokenExpiry)) {
|
||
// Token is still valid
|
||
accessToken = cachedToken;
|
||
currentUser = cachedUser;
|
||
document.getElementById('current-user').textContent = cachedUser;
|
||
loadPPRs();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// No valid cached token, show login
|
||
showLogin();
|
||
}
|
||
|
||
function setupLoginForm() {
|
||
document.getElementById('login-form').addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
await handleLogin();
|
||
});
|
||
}
|
||
|
||
function showLogin() {
|
||
document.getElementById('loginModal').style.display = 'block';
|
||
document.getElementById('login-username').focus();
|
||
}
|
||
|
||
function hideLogin() {
|
||
document.getElementById('loginModal').style.display = 'none';
|
||
document.getElementById('login-error').style.display = 'none';
|
||
document.getElementById('login-form').reset();
|
||
}
|
||
|
||
async function handleLogin() {
|
||
const username = document.getElementById('login-username').value;
|
||
const password = document.getElementById('login-password').value;
|
||
const loginBtn = document.getElementById('login-btn');
|
||
const errorDiv = document.getElementById('login-error');
|
||
|
||
// Show loading state
|
||
loginBtn.disabled = true;
|
||
loginBtn.textContent = '🔄 Logging in...';
|
||
errorDiv.style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/auth/login', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.access_token) {
|
||
// Store token and user info with expiry (30 minutes from now)
|
||
const expiryTime = new Date().getTime() + (30 * 60 * 1000); // 30 minutes
|
||
|
||
localStorage.setItem('ppr_access_token', data.access_token);
|
||
localStorage.setItem('ppr_username', username);
|
||
localStorage.setItem('ppr_token_expiry', expiryTime.toString());
|
||
|
||
accessToken = data.access_token;
|
||
currentUser = username;
|
||
document.getElementById('current-user').textContent = username;
|
||
|
||
hideLogin();
|
||
loadPPRs();
|
||
} else {
|
||
throw new Error(data.detail || 'Authentication failed');
|
||
}
|
||
} catch (error) {
|
||
console.error('Login error:', error);
|
||
errorDiv.textContent = error.message || 'Login failed. Please check your credentials.';
|
||
errorDiv.style.display = 'block';
|
||
} finally {
|
||
// Reset button state
|
||
loginBtn.disabled = false;
|
||
loginBtn.textContent = '🔐 Login';
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
// Clear stored token and user info
|
||
localStorage.removeItem('ppr_access_token');
|
||
localStorage.removeItem('ppr_username');
|
||
localStorage.removeItem('ppr_token_expiry');
|
||
|
||
accessToken = null;
|
||
currentUser = null;
|
||
|
||
// Close any open modals
|
||
closePPRModal();
|
||
|
||
// Show login again
|
||
showLogin();
|
||
}
|
||
|
||
// Legacy authenticate function - now redirects to new login
|
||
function authenticate() {
|
||
showLogin();
|
||
}
|
||
|
||
// Enhanced fetch wrapper with token expiry handling
|
||
async function authenticatedFetch(url, options = {}) {
|
||
if (!accessToken) {
|
||
showLogin();
|
||
throw new Error('No access token available');
|
||
}
|
||
|
||
// Add authorization header
|
||
const headers = {
|
||
...options.headers,
|
||
'Authorization': `Bearer ${accessToken}`
|
||
};
|
||
|
||
const response = await fetch(url, {
|
||
...options,
|
||
headers
|
||
});
|
||
|
||
// Handle 401 - token expired
|
||
if (response.status === 401) {
|
||
logout();
|
||
throw new Error('Session expired. Please log in again.');
|
||
}
|
||
|
||
return response;
|
||
}
|
||
|
||
// 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 {
|
||
// Always load today's date
|
||
const today = new Date().toISOString().split('T')[0];
|
||
let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`;
|
||
|
||
const response = await authenticatedFetch(url);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch arrivals');
|
||
}
|
||
|
||
const allPPRs = await response.json();
|
||
|
||
// Filter for arrivals (NEW and CONFIRMED with ETA only)
|
||
const arrivals = allPPRs.filter(ppr =>
|
||
(ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && ppr.eta
|
||
);
|
||
|
||
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)
|
||
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 {
|
||
// Always load today's date
|
||
const today = new Date().toISOString().split('T')[0];
|
||
let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`;
|
||
|
||
const response = await authenticatedFetch(url);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch departures');
|
||
}
|
||
|
||
const allPPRs = await response.json();
|
||
|
||
// Filter for departures (LANDED status only)
|
||
const departures = allPPRs.filter(ppr => ppr.status === 'LANDED');
|
||
|
||
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';
|
||
}
|
||
|
||
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);
|
||
|
||
// 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><strong>${ppr.ac_reg}</strong>${notesIndicator}</td>
|
||
<td>${ppr.ac_type}</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);
|
||
|
||
// 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><strong>${ppr.ac_reg}</strong>${notesIndicator}</td>
|
||
<td>${ppr.ac_type}</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 '-';
|
||
// Ensure the datetime string is treated as UTC
|
||
const utcDateStr = dateStr.includes('Z') ? dateStr : dateStr + 'Z';
|
||
const date = new Date(utcDateStr);
|
||
return date.toISOString().slice(11, 16) + 'Z';
|
||
}
|
||
|
||
function formatDateTime(dateStr) {
|
||
if (!dateStr) return '-';
|
||
// Ensure the datetime string is treated as UTC
|
||
const utcDateStr = dateStr.includes('Z') ? dateStr : dateStr + 'Z';
|
||
const date = new Date(utcDateStr);
|
||
return date.toISOString().slice(0, 10) + ' ' + date.toISOString().slice(11, 16) + 'Z';
|
||
}
|
||
|
||
// 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 = '';
|
||
|
||
// Set default ETA and ETD
|
||
const now = new Date();
|
||
const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
|
||
const etd = new Date(now.getTime() + 2 * 60 * 60 * 1000); // +2 hours
|
||
|
||
// Format as local datetime-local value
|
||
function formatLocalDateTime(date) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||
}
|
||
|
||
document.getElementById('eta').value = formatLocalDateTime(eta);
|
||
document.getElementById('etd').value = etd ? formatLocalDateTime(etd) : '';
|
||
|
||
// Clear aircraft lookup results
|
||
clearAircraftLookup();
|
||
|
||
document.getElementById('pprModal').style.display = 'block';
|
||
|
||
// Auto-focus on aircraft registration field
|
||
setTimeout(() => {
|
||
document.getElementById('ac_reg').focus();
|
||
}, 100);
|
||
}
|
||
|
||
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 authenticatedFetch(`/api/v1/pprs/${pprId}`);
|
||
|
||
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);
|
||
showNotification('Error loading PPR details', true);
|
||
}
|
||
}
|
||
|
||
function populateForm(ppr) {
|
||
Object.keys(ppr).forEach(key => {
|
||
const field = document.getElementById(key);
|
||
if (field) {
|
||
if (key === 'eta' || key === 'etd') {
|
||
if (ppr[key]) {
|
||
// ppr[key] is UTC datetime string from API (naive, assume UTC)
|
||
const utcDateStr = ppr[key].includes('Z') ? ppr[key] : ppr[key] + 'Z';
|
||
const date = new Date(utcDateStr); // Now correctly parsed as UTC
|
||
// Format as local time for datetime-local input
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
field.value = `${year}-${month}-${day}T${hours}:${minutes}`;
|
||
}
|
||
} 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 if (key === 'eta' || key === 'etd') {
|
||
// Convert local datetime-local to UTC ISO string
|
||
pprData[key] = new Date(value).toISOString();
|
||
} 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');
|
||
}
|
||
|
||
const wasNewPPR = isNewPPR;
|
||
closePPRModal();
|
||
loadPPRs(); // Refresh both tables
|
||
showNotification(wasNewPPR ? 'PPR created successfully!' : 'PPR updated successfully!');
|
||
} catch (error) {
|
||
console.error('Error saving PPR:', error);
|
||
showNotification('Error saving PPR', true);
|
||
}
|
||
});
|
||
|
||
// 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
|
||
showNotification(`Status updated to ${status}`);
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
showNotification('Error updating status', true);
|
||
}
|
||
}
|
||
|
||
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
|
||
showNotification('PPR deleted successfully!');
|
||
} catch (error) {
|
||
console.error('Error deleting PPR:', error);
|
||
showNotification('Error deleting PPR', true);
|
||
}
|
||
}
|
||
|
||
// Close modal when clicking outside
|
||
window.onclick = function(event) {
|
||
const modal = document.getElementById('pprModal');
|
||
if (event.target === modal) {
|
||
closePPRModal();
|
||
}
|
||
}
|
||
|
||
// Aircraft Lookup Functions
|
||
let aircraftLookupTimeout;
|
||
|
||
function handleAircraftLookup(registration) {
|
||
// Clear previous timeout
|
||
if (aircraftLookupTimeout) {
|
||
clearTimeout(aircraftLookupTimeout);
|
||
}
|
||
|
||
// Clear results if input is too short
|
||
if (registration.length < 4) {
|
||
clearAircraftLookup();
|
||
return;
|
||
}
|
||
|
||
// Show searching indicator
|
||
document.getElementById('aircraft-lookup-results').innerHTML =
|
||
'<div class="aircraft-searching">Searching...</div>';
|
||
|
||
// Debounce the search - wait 300ms after user stops typing
|
||
aircraftLookupTimeout = setTimeout(() => {
|
||
performAircraftLookup(registration);
|
||
}, 300);
|
||
}
|
||
|
||
async function performAircraftLookup(registration) {
|
||
try {
|
||
// Clean the input (remove non-alphanumeric characters and make uppercase)
|
||
const cleanInput = registration.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
||
|
||
if (cleanInput.length < 4) {
|
||
clearAircraftLookup();
|
||
return;
|
||
}
|
||
|
||
// Call the real API
|
||
const response = await authenticatedFetch(`/api/v1/aircraft/lookup/${cleanInput}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch aircraft data');
|
||
}
|
||
|
||
const matches = await response.json();
|
||
displayAircraftLookupResults(matches, cleanInput);
|
||
|
||
} catch (error) {
|
||
console.error('Aircraft lookup error:', error);
|
||
document.getElementById('aircraft-lookup-results').innerHTML =
|
||
'<div class="aircraft-no-match">Lookup failed - please enter manually</div>';
|
||
}
|
||
}
|
||
|
||
function displayAircraftLookupResults(matches, searchTerm) {
|
||
const resultsDiv = document.getElementById('aircraft-lookup-results');
|
||
|
||
if (matches.length === 0) {
|
||
resultsDiv.innerHTML = '<div class="aircraft-no-match">No matches found</div>';
|
||
} else if (matches.length === 1) {
|
||
// Unique match found - auto-populate
|
||
const aircraft = matches[0];
|
||
resultsDiv.innerHTML = `
|
||
<div class="aircraft-match">
|
||
✓ ${aircraft.manufacturer_name} ${aircraft.model} (${aircraft.type_code})
|
||
</div>
|
||
`;
|
||
|
||
// Auto-populate the form fields
|
||
document.getElementById('ac_reg').value = aircraft.registration;
|
||
document.getElementById('ac_type').value = aircraft.type_code;
|
||
|
||
} else {
|
||
// Multiple matches - show list but don't auto-populate
|
||
resultsDiv.innerHTML = `
|
||
<div class="aircraft-no-match">
|
||
Multiple matches found (${matches.length}) - please be more specific
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function clearAircraftLookup() {
|
||
document.getElementById('aircraft-lookup-results').innerHTML = '';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |