Increments
This commit is contained in:
307
web/admin.html
307
web/admin.html
@@ -210,6 +210,59 @@
|
|||||||
color: #666;
|
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 Styles */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -301,6 +354,22 @@
|
|||||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
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 {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -423,7 +492,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Registration</th>
|
<th>Registration</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Captain</th>
|
|
||||||
<th>From</th>
|
<th>From</th>
|
||||||
<th>ETA Time</th>
|
<th>ETA Time</th>
|
||||||
<th>POB</th>
|
<th>POB</th>
|
||||||
@@ -460,7 +528,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Registration</th>
|
<th>Registration</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Captain</th>
|
|
||||||
<th>To</th>
|
<th>To</th>
|
||||||
<th>ETD Time</th>
|
<th>ETD Time</th>
|
||||||
<th>POB</th>
|
<th>POB</th>
|
||||||
@@ -481,6 +548,33 @@
|
|||||||
</div>
|
</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 -->
|
<!-- PPR Detail/Edit Modal -->
|
||||||
<div id="pprModal" class="modal">
|
<div id="pprModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -602,50 +696,122 @@
|
|||||||
|
|
||||||
// Initialize the application
|
// Initialize the application
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
authenticate();
|
initializeAuth();
|
||||||
|
setupLoginForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Authentication
|
// Authentication management
|
||||||
function authenticate() {
|
function initializeAuth() {
|
||||||
const username = prompt('Username:');
|
// Try to get cached token
|
||||||
const password = prompt('Password:');
|
const cachedToken = localStorage.getItem('ppr_access_token');
|
||||||
|
const cachedUser = localStorage.getItem('ppr_username');
|
||||||
|
const tokenExpiry = localStorage.getItem('ppr_token_expiry');
|
||||||
|
|
||||||
if (!username || !password) {
|
if (cachedToken && cachedUser && tokenExpiry) {
|
||||||
alert('Authentication required');
|
const now = new Date().getTime();
|
||||||
return;
|
if (now < parseInt(tokenExpiry)) {
|
||||||
}
|
// Token is still valid
|
||||||
|
accessToken = cachedToken;
|
||||||
fetch('/api/v1/auth/login', {
|
currentUser = cachedUser;
|
||||||
method: 'POST',
|
document.getElementById('current-user').textContent = cachedUser;
|
||||||
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();
|
loadPPRs();
|
||||||
setDefaultDates();
|
setDefaultDates();
|
||||||
} else {
|
return;
|
||||||
alert('Authentication failed');
|
|
||||||
authenticate();
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.catch(error => {
|
|
||||||
console.error('Auth error:', error);
|
// No valid cached token, show login
|
||||||
alert('Authentication failed');
|
showLogin();
|
||||||
authenticate();
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
setDefaultDates();
|
||||||
|
} 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() {
|
function logout() {
|
||||||
|
// Clear stored token and user info
|
||||||
|
localStorage.removeItem('ppr_access_token');
|
||||||
|
localStorage.removeItem('ppr_username');
|
||||||
|
localStorage.removeItem('ppr_token_expiry');
|
||||||
|
|
||||||
accessToken = null;
|
accessToken = null;
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
location.reload();
|
|
||||||
|
// Close any open modals
|
||||||
|
closePPRModal();
|
||||||
|
|
||||||
|
// Show login again
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy authenticate function - now redirects to new login
|
||||||
|
function authenticate() {
|
||||||
|
showLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDefaultDates() {
|
function setDefaultDates() {
|
||||||
@@ -653,6 +819,33 @@
|
|||||||
document.getElementById('viewDate').value = today.toISOString().split('T')[0];
|
document.getElementById('viewDate').value = today.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Load PPR records - now loads both arrivals and departures
|
||||||
async function loadPPRs() {
|
async function loadPPRs() {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
@@ -676,11 +869,7 @@
|
|||||||
url += `&date_from=${viewDate}&date_to=${viewDate}`;
|
url += `&date_from=${viewDate}&date_to=${viewDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await authenticatedFetch(url);
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch arrivals');
|
throw new Error('Failed to fetch arrivals');
|
||||||
@@ -703,7 +892,9 @@
|
|||||||
displayArrivals(arrivals);
|
displayArrivals(arrivals);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading arrivals:', error);
|
console.error('Error loading arrivals:', error);
|
||||||
alert('Error loading arrivals');
|
if (error.message !== 'Session expired. Please log in again.') {
|
||||||
|
alert('Error loading arrivals');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('arrivals-loading').style.display = 'none';
|
document.getElementById('arrivals-loading').style.display = 'none';
|
||||||
@@ -724,11 +915,7 @@
|
|||||||
url += `&date_from=${viewDate}&date_to=${viewDate}`;
|
url += `&date_from=${viewDate}&date_to=${viewDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await authenticatedFetch(url);
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch departures');
|
throw new Error('Failed to fetch departures');
|
||||||
@@ -749,7 +936,9 @@
|
|||||||
displayDepartures(departures);
|
displayDepartures(departures);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading departures:', error);
|
console.error('Error loading departures:', error);
|
||||||
alert('Error loading departures');
|
if (error.message !== 'Session expired. Please log in again.') {
|
||||||
|
alert('Error loading departures');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('departures-loading').style.display = 'none';
|
document.getElementById('departures-loading').style.display = 'none';
|
||||||
@@ -773,10 +962,16 @@
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.onclick = () => openPPRModal(ppr.id);
|
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 = `
|
row.innerHTML = `
|
||||||
<td><strong>${ppr.ac_reg}</strong></td>
|
<td><strong>${ppr.ac_reg}</strong>${notesIndicator}</td>
|
||||||
<td>${ppr.ac_type}</td>
|
<td>${ppr.ac_type}</td>
|
||||||
<td>${ppr.captain}</td>
|
|
||||||
<td>${ppr.in_from}</td>
|
<td>${ppr.in_from}</td>
|
||||||
<td>${formatTimeOnly(ppr.eta)}</td>
|
<td>${formatTimeOnly(ppr.eta)}</td>
|
||||||
<td>${ppr.pob_in}</td>
|
<td>${ppr.pob_in}</td>
|
||||||
@@ -811,10 +1006,16 @@
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.onclick = () => openPPRModal(ppr.id);
|
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 = `
|
row.innerHTML = `
|
||||||
<td><strong>${ppr.ac_reg}</strong></td>
|
<td><strong>${ppr.ac_reg}</strong>${notesIndicator}</td>
|
||||||
<td>${ppr.ac_type}</td>
|
<td>${ppr.ac_type}</td>
|
||||||
<td>${ppr.captain}</td>
|
|
||||||
<td>${ppr.out_to || '-'}</td>
|
<td>${ppr.out_to || '-'}</td>
|
||||||
<td>${ppr.etd ? formatTimeOnly(ppr.etd) : '-'}</td>
|
<td>${ppr.etd ? formatTimeOnly(ppr.etd) : '-'}</td>
|
||||||
<td>${ppr.pob_out || ppr.pob_in}</td>
|
<td>${ppr.pob_out || ppr.pob_in}</td>
|
||||||
@@ -875,11 +1076,7 @@
|
|||||||
document.querySelector('.quick-actions').style.display = 'flex';
|
document.querySelector('.quick-actions').style.display = 'flex';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/pprs/${pprId}`, {
|
const response = await authenticatedFetch(`/api/v1/pprs/${pprId}`);
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch PPR details');
|
throw new Error('Failed to fetch PPR details');
|
||||||
|
|||||||
Reference in New Issue
Block a user