Overflight improvements

This commit is contained in:
2025-12-19 05:51:55 -05:00
parent 3ab9a6e04c
commit 63564b54dd
3 changed files with 316 additions and 40 deletions

View File

@@ -127,7 +127,7 @@ async def update_overflight_status(
db,
overflight_id=overflight_id,
status=status_update.status,
timestamp=status_update.timestamp if hasattr(status_update, 'timestamp') else None,
timestamp=status_update.qsy_dt,
user=current_user.username,
user_ip=client_ip
)

View File

@@ -93,6 +93,7 @@ class OverflightUpdate(BaseModel):
class OverflightStatusUpdate(BaseModel):
status: OverflightStatus
qsy_dt: Optional[datetime] = None
class Overflight(OverflightBase):

View File

@@ -698,6 +698,78 @@
</div>
</div>
<!-- Overflight Edit Modal -->
<div id="overflightEditModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="overflight-edit-title">Overflight Details</h2>
<button class="close" onclick="closeOverflightEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="quick-actions">
<button id="overflight-btn-qsy" class="btn btn-primary btn-sm" onclick="showOverflightQSYModal()" style="display: none;">
📡 Mark QSY
</button>
<button id="overflight-btn-cancel" class="btn btn-danger btn-sm" onclick="confirmCancelOverflight()" style="display: none;">
❌ Cancel
</button>
</div>
<form id="overflight-edit-form">
<input type="hidden" id="overflight-edit-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="overflight_edit_registration">Callsign/Registration</label>
<input type="text" id="overflight_edit_registration" name="registration" readonly>
</div>
<div class="form-group">
<label for="overflight_edit_type">Aircraft Type</label>
<input type="text" id="overflight_edit_type" name="type">
</div>
<div class="form-group">
<label for="overflight_edit_pob">Persons on Board</label>
<input type="number" id="overflight_edit_pob" name="pob" min="1">
</div>
<div class="form-group">
<label for="overflight_edit_departure_airfield">Departure Airfield</label>
<input type="text" id="overflight_edit_departure_airfield" name="departure_airfield">
</div>
<div class="form-group">
<label for="overflight_edit_destination_airfield">Destination Airfield</label>
<input type="text" id="overflight_edit_destination_airfield" name="destination_airfield">
</div>
<div class="form-group">
<label for="overflight_edit_call_dt">Time of Call</label>
<input type="datetime-local" id="overflight_edit_call_dt" name="call_dt">
</div>
<div class="form-group">
<label for="overflight_edit_status">Status</label>
<input type="text" id="overflight_edit_status" name="status" readonly>
</div>
<div class="form-group">
<label for="overflight_edit_qsy_dt">QSY Time</label>
<input type="datetime-local" id="overflight_edit_qsy_dt" name="qsy_dt">
</div>
<div class="form-group full-width">
<label for="overflight_edit_notes">Notes</label>
<textarea id="overflight_edit_notes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeOverflightEditModal()">
Close
</button>
<button type="submit" class="btn btn-success">
Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Departure Edit Modal -->
<div id="departureEditModal" class="modal">
<div class="modal-content">
@@ -1354,6 +1426,13 @@
closeArrivalEditModal();
return;
}
// Press 'Escape' to close overflight edit modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('overflightEditModal').style.display === 'block') {
e.preventDefault();
closeOverflightEditModal();
return;
}
// Only trigger other shortcuts when not typing in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
@@ -1765,6 +1844,21 @@
'<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8rem;">ACTIVE</span>' :
'<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8rem;">QSY\'D</span>';
// Action buttons for overflight
let actionButtons = '';
if (flight.status === 'ACTIVE') {
actionButtons = `
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentOverflightId = ${flight.id}; showOverflightQSYModal()" title="Mark QSY">
QSY
</button>
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); cancelOverflightFromTable(${flight.id})" title="Cancel">
CANCEL
</button>
`;
} else {
actionButtons = '<span style="color: #999;">-</span>';
}
row.innerHTML = `
<td>${flight.registration || '-'}</td>
<td style="width: 30px; text-align: center;"><span style="color: #ff6b6b; font-weight: bold;" title="Overflight">🔄</span></td>
@@ -1775,10 +1869,7 @@
<td>${formatTimeOnly(flight.call_dt)}</td>
<td>${flight.pob || '-'}</td>
<td>${statusBadge}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="markQSY(event, ${flight.id})">QSY</button>
<button class="btn btn-sm btn-danger" onclick="cancelOverflight(event, ${flight.id})">Cancel</button>
</td>
<td style="white-space: nowrap;">${actionButtons}</td>
`;
tbody.appendChild(row);
}
@@ -2858,6 +2949,13 @@
document.getElementById('timestamp-form').addEventListener('submit', async function(e) {
e.preventDefault();
// Handle overflight QSY mode
if (isOverflightQSYMode) {
isOverflightQSYMode = false;
await handleOverflightQSYSubmit();
return;
}
if (!pendingStatusUpdate || !accessToken) return;
const timestampInput = document.getElementById('event-timestamp').value;
@@ -3504,64 +3602,241 @@
document.getElementById('overflightModal').style.display = 'none';
}
function openOverflightEditModal(id) {
// Open a simple modal or dialog for editing/managing overflight
// For now, show a confirmation for QSY
showNotification(`Overflight ${id} - Use QSY button to mark frequency change`, false);
}
let currentOverflightId = null;
let isOverflightQSYMode = false; // Track if we're in overflight QSY mode
async function markQSY(event, overflightId) {
event.stopPropagation();
async function openOverflightEditModal(overflightId) {
if (!accessToken) return;
try {
const response = await authenticatedFetch(`/api/v1/overflights/${overflightId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'INACTIVE'
})
const response = await fetch(`/api/v1/overflights/${overflightId}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) {
throw new Error('Failed to mark QSY');
if (!response.ok) throw new Error('Failed to load overflight');
const overflight = await response.json();
currentOverflightId = overflight.id;
// Populate form
document.getElementById('overflight-edit-id').value = overflight.id;
document.getElementById('overflight_edit_registration').value = overflight.registration;
document.getElementById('overflight_edit_type').value = overflight.type || '';
document.getElementById('overflight_edit_pob').value = overflight.pob || '';
document.getElementById('overflight_edit_departure_airfield').value = overflight.departure_airfield || '';
document.getElementById('overflight_edit_destination_airfield').value = overflight.destination_airfield || '';
document.getElementById('overflight_edit_status').value = overflight.status;
document.getElementById('overflight_edit_notes').value = overflight.notes || '';
// Parse and populate call_dt
if (overflight.call_dt) {
const callDt = new Date(overflight.call_dt);
document.getElementById('overflight_edit_call_dt').value = callDt.toISOString().slice(0, 16);
}
loadPPRs();
showNotification('Overflight marked as QSY (frequency changed)', false);
// Parse and populate qsy_dt if exists
if (overflight.qsy_dt) {
const qsyDt = new Date(overflight.qsy_dt);
document.getElementById('overflight_edit_qsy_dt').value = qsyDt.toISOString().slice(0, 16);
} else {
document.getElementById('overflight_edit_qsy_dt').value = '';
}
// Show/hide action buttons based on status
const qsyBtn = document.getElementById('overflight-btn-qsy');
const cancelBtn = document.getElementById('overflight-btn-cancel');
if (qsyBtn) qsyBtn.style.display = overflight.status === 'ACTIVE' ? 'inline-block' : 'none';
if (cancelBtn) cancelBtn.style.display = overflight.status === 'ACTIVE' ? 'inline-block' : 'none';
document.getElementById('overflight-edit-title').textContent = `${overflight.registration} - Overflight`;
document.getElementById('overflightEditModal').style.display = 'block';
} catch (error) {
console.error('Error marking QSY:', error);
showNotification(`Error: ${error.message}`, true);
console.error('Error loading overflight:', error);
showNotification('Error loading overflight details', true);
}
}
async function cancelOverflight(event, overflightId) {
event.stopPropagation();
if (!accessToken) return;
function closeOverflightEditModal() {
document.getElementById('overflightEditModal').style.display = 'none';
currentOverflightId = null;
}
async function updateOverflightStatus(newStatus, qsyTime = null) {
if (!currentOverflightId || !accessToken) return;
try {
const body = { status: newStatus };
if (qsyTime) {
body.qsy_dt = qsyTime;
}
const response = await fetch(`/api/v1/overflights/${currentOverflightId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(body)
});
if (!response.ok) throw new Error('Failed to update overflight status');
closeOverflightEditModal();
loadPPRs(); // Refresh overflights display
showNotification(`Overflight marked as ${newStatus}`);
} catch (error) {
console.error('Error updating overflight status:', error);
showNotification('Error updating overflight status', true);
}
}
function showOverflightQSYModal() {
if (!currentOverflightId) return;
if (!confirm('Are you sure you want to cancel this overflight?')) {
isOverflightQSYMode = true;
const modalTitle = document.getElementById('timestamp-modal-title');
const submitBtn = document.getElementById('timestamp-submit-btn');
modalTitle.textContent = 'Confirm QSY Time';
submitBtn.textContent = '📡 Confirm QSY';
// 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';
}
async function handleOverflightQSYSubmit() {
const timestamp = document.getElementById('event-timestamp').value;
if (!timestamp) {
showNotification('Please enter a QSY time', true);
return;
}
// Convert datetime-local value to ISO string
// datetime-local format is "YYYY-MM-DDTHH:mm" and we need to treat it as UTC
const isoString = timestamp + ':00Z'; // Add seconds and Z for UTC
closeTimestampModal();
await updateOverflightStatus('INACTIVE', isoString);
}
async function cancelOverflightFromTable(overflightId) {
if (!confirm('Are you sure you want to cancel this overflight? This action cannot be easily undone.')) {
return;
}
if (!accessToken) return;
try {
const response = await authenticatedFetch(`/api/v1/overflights/${overflightId}`, {
method: 'DELETE'
const response = await fetch(`/api/v1/overflights/${overflightId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error('Failed to cancel overflight');
}
if (!response.ok) throw new Error('Failed to cancel overflight');
loadPPRs();
showNotification('Overflight cancelled', false);
showNotification('Overflight cancelled');
} catch (error) {
console.error('Error cancelling overflight:', error);
showNotification(`Error: ${error.message}`, true);
showNotification('Error cancelling overflight', true);
}
}
function confirmCancelOverflight() {
if (!currentOverflightId) return;
if (!confirm('Are you sure you want to cancel this overflight? This action cannot be easily undone.')) {
return;
}
cancelOverflightFromModal();
}
async function cancelOverflightFromModal() {
if (!currentOverflightId || !accessToken) return;
try {
const response = await fetch(`/api/v1/overflights/${currentOverflightId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) throw new Error('Failed to cancel overflight');
closeOverflightEditModal();
loadPPRs();
showNotification('Overflight cancelled');
} catch (error) {
console.error('Error cancelling overflight:', error);
showNotification('Error cancelling overflight', true);
}
}
// Overflight edit form submission
document.getElementById('overflight-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!currentOverflightId || !accessToken) return;
const formData = new FormData(this);
const updateData = {};
formData.forEach((value, key) => {
if (key === 'id') return;
// Handle datetime-local fields
if (key === 'call_dt' || key === 'qsy_dt') {
if (value) {
updateData[key] = new Date(value).toISOString();
}
return;
}
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (value.trim) {
updateData[key] = value.trim();
} else {
updateData[key] = value;
}
}
});
try {
const response = await fetch(`/api/v1/overflights/${currentOverflightId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(updateData)
});
if (!response.ok) throw new Error('Failed to update overflight');
closeOverflightEditModal();
loadPPRs(); // Refresh overflights display
showNotification('Overflight updated successfully');
} catch (error) {
console.error('Error updating overflight:', error);
showNotification('Error updating overflight', true);
}
});
function populateETATimeSlots() {
const select = document.getElementById('book_in_eta_time');
const next15MinSlot = getNext10MinuteSlot();