Commit often...

This commit is contained in:
James Pattinson
2025-10-23 16:43:01 +00:00
parent 981bb39888
commit b2f60322a1
5 changed files with 256 additions and 14 deletions

View File

@@ -110,9 +110,14 @@
background-color: #c0392b;
}
.btn-sm {
padding: 0.4rem 0.8rem;
.btn-icon {
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
min-width: auto;
}
.btn-icon:hover {
transform: scale(1.05);
}
.filter-group {
@@ -621,10 +626,10 @@
<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')">
<button id="btn-landed" class="btn btn-warning btn-sm" onclick="showTimestampModal('LANDED')">
🛬 Landed
</button>
<button id="btn-departed" class="btn btn-primary btn-sm" onclick="updateStatus('DEPARTED')">
<button id="btn-departed" class="btn btn-primary btn-sm" onclick="showTimestampModal('DEPARTED')">
🛫 Departed
</button>
<button id="btn-cancel" class="btn btn-danger btn-sm" onclick="updateStatus('CANCELED')">
@@ -726,11 +731,92 @@
<!-- Success Notification -->
<div id="notification" class="notification"></div>
<!-- Timestamp Modal for Landing/Departure -->
<div id="timestampModal" class="modal">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2 id="timestamp-modal-title">Confirm Landing Time</h2>
<button class="close" onclick="closeTimestampModal()">&times;</button>
</div>
<div class="modal-body">
<form id="timestamp-form">
<div class="form-group">
<label for="event-timestamp">Event Time (Local Time) *</label>
<input type="datetime-local" id="event-timestamp" name="timestamp" required>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeTimestampModal()">
Cancel
</button>
<button type="submit" class="btn btn-success" id="timestamp-submit-btn">
Confirm
</button>
</div>
</form>
</div>
</div>
</div>
<script>
let currentUser = null;
let accessToken = null;
let currentPPRId = null;
let isNewPPR = false;
let wsConnection = null;
let pendingStatusUpdate = null; // Track pending status update for timestamp modal
// WebSocket connection for real-time updates
function connectWebSocket() {
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
return; // Already connected
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/tower-updates`;
wsConnection = new WebSocket(wsUrl);
wsConnection.onopen = function(event) {
console.log('WebSocket connected for real-time updates');
};
wsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
// Refresh PPRs when any PPR-related event occurs
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) {
console.log('PPR update detected, refreshing...');
loadPPRs();
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
wsConnection.onclose = function(event) {
console.log('WebSocket disconnected');
// Attempt to reconnect after 5 seconds
setTimeout(() => {
if (accessToken) { // Only reconnect if still logged in
console.log('Attempting to reconnect WebSocket...');
connectWebSocket();
}
}, 5000);
};
wsConnection.onerror = function(error) {
console.error('WebSocket error:', error);
};
}
function disconnectWebSocket() {
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
}
// Notification system
function showNotification(message, isError = false) {
@@ -770,6 +856,7 @@
accessToken = cachedToken;
currentUser = cachedUser;
document.getElementById('current-user').textContent = cachedUser;
connectWebSocket(); // Connect WebSocket for real-time updates
loadPPRs();
return;
}
@@ -795,6 +882,13 @@
return;
}
// Press 'Escape' to close timestamp modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('timestampModal').style.display === 'block') {
e.preventDefault();
closeTimestampModal();
return;
}
// Only trigger other shortcuts when not typing in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
return;
@@ -854,6 +948,7 @@
document.getElementById('current-user').textContent = username;
hideLogin();
connectWebSocket(); // Connect WebSocket for real-time updates
loadPPRs();
} else {
throw new Error(data.detail || 'Authentication failed');
@@ -878,8 +973,11 @@
accessToken = null;
currentUser = null;
disconnectWebSocket(); // Disconnect WebSocket
// Close any open modals
closePPRModal();
closeTimestampModal();
// Show login again
showLogin();
@@ -1026,8 +1124,11 @@
<td>${ppr.pob_in}</td>
<td>${ppr.fuel || '-'}</td>
<td>
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openPPRModal(${ppr.id})">
Edit
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${ppr.id})" title="Mark as Landed">
🛬
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Arrival">
</button>
</td>
`;
@@ -1070,8 +1171,11 @@
<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 class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ppr.id})" title="Mark as Departed">
🛫
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Departure">
</button>
</td>
`;
@@ -1235,6 +1339,84 @@
isNewPPR = false;
}
// Timestamp modal functions
function showTimestampModal(status, pprId = null) {
const targetPprId = pprId || currentPPRId;
if (!targetPprId) return;
pendingStatusUpdate = { status: status, pprId: targetPprId };
const modalTitle = document.getElementById('timestamp-modal-title');
const submitBtn = document.getElementById('timestamp-submit-btn');
if (status === 'LANDED') {
modalTitle.textContent = 'Confirm Landing Time';
submitBtn.textContent = '🛬 Confirm Landing';
} else if (status === 'DEPARTED') {
modalTitle.textContent = 'Confirm Departure Time';
submitBtn.textContent = '🛫 Confirm Departure';
}
// Set default timestamp to current time
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
document.getElementById('event-timestamp').value = `${year}-${month}-${day}T${hours}:${minutes}`;
document.getElementById('timestampModal').style.display = 'block';
}
function closeTimestampModal() {
document.getElementById('timestampModal').style.display = 'none';
pendingStatusUpdate = null;
document.getElementById('timestamp-form').reset();
}
// Timestamp form submission
document.getElementById('timestamp-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!pendingStatusUpdate || !accessToken) return;
const timestampInput = document.getElementById('event-timestamp').value;
let timestamp = null;
if (timestampInput.trim()) {
// Convert local datetime-local to UTC ISO string
timestamp = new Date(timestampInput).toISOString();
}
try {
const response = await fetch(`/api/v1/pprs/${pendingStatusUpdate.pprId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
status: pendingStatusUpdate.status,
timestamp: timestamp
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Failed to update status: ${response.status} ${response.statusText} - ${errorData.detail || 'Unknown error'}`);
}
const updatedStatus = pendingStatusUpdate.status;
closeTimestampModal();
loadPPRs(); // Refresh both tables
showNotification(`Status updated to ${updatedStatus}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification(`Error updating status: ${error.message}`, true);
}
});
// Form submission
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
e.preventDefault();
@@ -1320,6 +1502,39 @@
}
}
async function updateStatusFromTable(pprId, status) {
if (!accessToken) return;
// Show confirmation for cancel actions
if (status === 'CANCELED') {
if (!confirm('Are you sure you want to cancel this PPR? This action cannot be easily undone.')) {
return;
}
}
try {
const response = await fetch(`/api/v1/pprs/${pprId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({ status: status })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Failed to update status: ${response.status} ${response.statusText} - ${errorData.detail || 'Unknown error'}`);
}
loadPPRs(); // Refresh both tables
showNotification(`Status updated to ${status}`);
} catch (error) {
console.error('Error updating status:', error);
showNotification(`Error updating status: ${error.message}`, true);
}
}
async function deletePPR() {
if (!currentPPRId || !accessToken) return;
@@ -1350,10 +1565,15 @@
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('pprModal');
if (event.target === modal) {
const pprModal = document.getElementById('pprModal');
const timestampModal = document.getElementById('timestampModal');
if (event.target === pprModal) {
closePPRModal();
}
if (event.target === timestampModal) {
closeTimestampModal();
}
}
// Aircraft Lookup Functions