Increments

This commit is contained in:
James Pattinson
2025-10-21 20:40:37 +00:00
parent 28af669993
commit 4f952a5a1b

View File

@@ -210,6 +210,59 @@
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;
@@ -301,6 +354,22 @@
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;
@@ -423,7 +492,6 @@
<tr>
<th>Registration</th>
<th>Type</th>
<th>Captain</th>
<th>From</th>
<th>ETA Time</th>
<th>POB</th>
@@ -460,7 +528,6 @@
<tr>
<th>Registration</th>
<th>Type</th>
<th>Captain</th>
<th>To</th>
<th>ETD Time</th>
<th>POB</th>
@@ -481,6 +548,33 @@
</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">
@@ -602,50 +696,122 @@
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
authenticate();
initializeAuth();
setupLoginForm();
});
// Authentication
function authenticate() {
const username = prompt('Username:');
const password = prompt('Password:');
// 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 (!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;
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();
setDefaultDates();
} else {
alert('Authentication failed');
authenticate();
return;
}
})
.catch(error => {
console.error('Auth error:', error);
alert('Authentication failed');
authenticate();
}
// 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();
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() {
// Clear stored token and user info
localStorage.removeItem('ppr_access_token');
localStorage.removeItem('ppr_username');
localStorage.removeItem('ppr_token_expiry');
accessToken = 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() {
@@ -653,6 +819,33 @@
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
async function loadPPRs() {
if (!accessToken) return;
@@ -676,11 +869,7 @@
url += `&date_from=${viewDate}&date_to=${viewDate}`;
}
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const response = await authenticatedFetch(url);
if (!response.ok) {
throw new Error('Failed to fetch arrivals');
@@ -703,7 +892,9 @@
displayArrivals(arrivals);
} catch (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';
@@ -724,11 +915,7 @@
url += `&date_from=${viewDate}&date_to=${viewDate}`;
}
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const response = await authenticatedFetch(url);
if (!response.ok) {
throw new Error('Failed to fetch departures');
@@ -749,7 +936,9 @@
displayDepartures(departures);
} catch (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';
@@ -773,10 +962,16 @@
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></td>
<td><strong>${ppr.ac_reg}</strong>${notesIndicator}</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>
@@ -811,10 +1006,16 @@
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></td>
<td><strong>${ppr.ac_reg}</strong>${notesIndicator}</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>
@@ -875,11 +1076,7 @@
document.querySelector('.quick-actions').style.display = 'flex';
try {
const response = await fetch(`/api/v1/pprs/${pprId}`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const response = await authenticatedFetch(`/api/v1/pprs/${pprId}`);
if (!response.ok) {
throw new Error('Failed to fetch PPR details');