diff --git a/backend/app/api/endpoints/overflights.py b/backend/app/api/endpoints/overflights.py
index dad3946..fe20c5a 100644
--- a/backend/app/api/endpoints/overflights.py
+++ b/backend/app/api/endpoints/overflights.py
@@ -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
)
diff --git a/backend/app/schemas/overflight.py b/backend/app/schemas/overflight.py
index ac66580..0248473 100644
--- a/backend/app/schemas/overflight.py
+++ b/backend/app/schemas/overflight.py
@@ -93,6 +93,7 @@ class OverflightUpdate(BaseModel):
class OverflightStatusUpdate(BaseModel):
status: OverflightStatus
+ qsy_dt: Optional[datetime] = None
class Overflight(OverflightBase):
diff --git a/web/admin.html b/web/admin.html
index 34e9ece..c630463 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -698,6 +698,78 @@
+
+
@@ -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 @@
'ACTIVE' :
'QSY\'D';
+ // Action buttons for overflight
+ let actionButtons = '';
+ if (flight.status === 'ACTIVE') {
+ actionButtons = `
+
+
+ `;
+ } else {
+ actionButtons = '-';
+ }
+
row.innerHTML = `
${flight.registration || '-'} |
🔄 |
@@ -1775,10 +1869,7 @@
${formatTimeOnly(flight.call_dt)} |
${flight.pob || '-'} |
${statusBadge} |
-
-
-
- |
+ ${actionButtons} |
`;
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();