local-flights #5
@@ -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
|
||||
)
|
||||
|
||||
@@ -93,6 +93,7 @@ class OverflightUpdate(BaseModel):
|
||||
|
||||
class OverflightStatusUpdate(BaseModel):
|
||||
status: OverflightStatus
|
||||
qsy_dt: Optional[datetime] = None
|
||||
|
||||
|
||||
class Overflight(OverflightBase):
|
||||
|
||||
347
web/admin.html
347
web/admin.html
@@ -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()">×</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">
|
||||
@@ -1355,6 +1427,13 @@
|
||||
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') {
|
||||
return;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
if (!confirm('Are you sure you want to cancel this overflight?')) {
|
||||
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;
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user