local-flights #5

Merged
jamesp merged 37 commits from local-flights into main 2025-12-20 12:29:32 -05:00
3 changed files with 295 additions and 30 deletions
Showing only changes of commit f3eb83665f - Show all commits

View File

@@ -42,6 +42,7 @@ class ArrivalCreate(ArrivalBase):
class ArrivalUpdate(BaseModel): class ArrivalUpdate(BaseModel):
registration: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
callsign: Optional[str] = None callsign: Optional[str] = None
pob: Optional[int] = None pob: Optional[int] = None

View File

@@ -43,6 +43,7 @@ class DepartureCreate(DepartureBase):
class DepartureUpdate(BaseModel): class DepartureUpdate(BaseModel):
registration: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
callsign: Optional[str] = None callsign: Optional[str] = None
pob: Optional[int] = None pob: Optional[int] = None

View File

@@ -353,6 +353,9 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-info" onclick="closePPRModal()">
Close
</button>
<button type="button" class="btn btn-danger" id="btn-cancel" onclick="updateStatus('CANCELED')"> <button type="button" class="btn btn-danger" id="btn-cancel" onclick="updateStatus('CANCELED')">
❌ Cancel PPR ❌ Cancel PPR
</button> </button>
@@ -431,8 +434,8 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeLocalFlightModal()"> <button type="button" class="btn btn-info" onclick="closeLocalFlightModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
🛫 Book Out 🛫 Book Out
@@ -459,7 +462,7 @@
🛬 Land 🛬 Land
</button> </button>
<button id="local-btn-cancel" class="btn btn-danger btn-sm" onclick="updateLocalFlightStatus('CANCELLED')" style="display: none;"> <button id="local-btn-cancel" class="btn btn-danger btn-sm" onclick="updateLocalFlightStatus('CANCELLED')" style="display: none;">
❌ Cancel ❌ Cancel Flight
</button> </button>
</div> </div>
@@ -509,8 +512,8 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeLocalFlightEditModal()"> <button type="button" class="btn btn-info" onclick="closeLocalFlightEditModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
💾 Save Changes 💾 Save Changes
@@ -576,8 +579,8 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeBookInModal()"> <button type="button" class="btn btn-info" onclick="closeBookInModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
🛬 Book In 🛬 Book In
@@ -601,7 +604,7 @@
🛫 Mark Departed 🛫 Mark Departed
</button> </button>
<button id="departure-btn-cancel" class="btn btn-danger btn-sm" onclick="updateDepartureStatus('CANCELLED')" style="display: none;"> <button id="departure-btn-cancel" class="btn btn-danger btn-sm" onclick="updateDepartureStatus('CANCELLED')" style="display: none;">
❌ Cancel ❌ Cancel Departure
</button> </button>
</div> </div>
@@ -611,37 +614,100 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label for="departure_edit_registration">Aircraft Registration</label> <label for="departure_edit_registration">Aircraft Registration</label>
<input type="text" id="departure_edit_registration" name="registration" readonly style="background-color: #f5f5f5; cursor: not-allowed;"> <input type="text" id="departure_edit_registration" name="registration">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="departure_edit_type">Aircraft Type</label> <label for="departure_edit_type">Aircraft Type</label>
<input type="text" id="departure_edit_type" name="type" readonly style="background-color: #f5f5f5; cursor: not-allowed;"> <input type="text" id="departure_edit_type" name="type">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="departure_edit_callsign">Callsign</label> <label for="departure_edit_callsign">Callsign</label>
<input type="text" id="departure_edit_callsign" name="callsign" readonly style="background-color: #f5f5f5; cursor: not-allowed;"> <input type="text" id="departure_edit_callsign" name="callsign">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="departure_edit_out_to">Destination</label> <label for="departure_edit_out_to">Destination</label>
<input type="text" id="departure_edit_out_to" name="out_to" readonly style="background-color: #f5f5f5; cursor: not-allowed;"> <input type="text" id="departure_edit_out_to" name="out_to">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="departure_edit_etd">ETD (Estimated Time of Departure)</label> <label for="departure_edit_etd">ETD (Estimated Time of Departure)</label>
<div style="display: flex; gap: 0.5rem;"> <div style="display: flex; gap: 0.5rem;">
<input type="date" id="departure_edit_etd_date" name="etd_date" readonly style="flex: 1; background-color: #f5f5f5; cursor: not-allowed;"> <input type="date" id="departure_edit_etd_date" name="etd_date" style="flex: 1;">
<input type="time" id="departure_edit_etd_time" name="etd_time" readonly style="flex: 1; background-color: #f5f5f5; cursor: not-allowed;"> <input type="time" id="departure_edit_etd_time" name="etd_time" style="flex: 1;">
</div> </div>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label for="departure_edit_notes">Notes</label> <label for="departure_edit_notes">Notes</label>
<textarea id="departure_edit_notes" name="notes" rows="3" readonly style="background-color: #f5f5f5; cursor: not-allowed;"></textarea> <textarea id="departure_edit_notes" name="notes" rows="3"></textarea>
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeDepartureEditModal()"> <button type="button" class="btn btn-info" onclick="closeDepartureEditModal()">
Close Close
</button> </button>
<button type="submit" class="btn btn-success">
Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Arrival Edit Modal -->
<div id="arrivalEditModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="arrival-edit-title">Arrival Details</h2>
<button class="close" onclick="closeArrivalEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="quick-actions">
<button id="arrival-btn-landed" class="btn btn-primary btn-sm" onclick="updateArrivalStatus('LANDED')" style="display: none;">
🛬 Mark Landed
</button>
<button id="arrival-btn-cancel" class="btn btn-danger btn-sm" onclick="updateArrivalStatus('CANCELLED')" style="display: none;">
❌ Cancel Arrival
</button>
</div>
<form id="arrival-edit-form">
<input type="hidden" id="arrival-edit-id" name="id">
<div class="form-grid">
<div class="form-group">
<label for="arrival_edit_registration">Aircraft Registration</label>
<input type="text" id="arrival_edit_registration" name="registration">
</div>
<div class="form-group">
<label for="arrival_edit_type">Aircraft Type</label>
<input type="text" id="arrival_edit_type" name="type">
</div>
<div class="form-group">
<label for="arrival_edit_callsign">Callsign</label>
<input type="text" id="arrival_edit_callsign" name="callsign">
</div>
<div class="form-group">
<label for="arrival_edit_in_from">Origin Airport</label>
<input type="text" id="arrival_edit_in_from" name="in_from">
</div>
<div class="form-group">
<label for="arrival_edit_pob">POB (Persons on Board)</label>
<input type="number" id="arrival_edit_pob" name="pob" min="1">
</div>
<div class="form-group full-width">
<label for="arrival_edit_notes">Notes</label>
<textarea id="arrival_edit_notes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeArrivalEditModal()">
Close
</button>
<button type="submit" class="btn btn-success">
Save Changes
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -659,7 +725,7 @@
<!-- Content will be populated by JavaScript --> <!-- Content will be populated by JavaScript -->
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary" onclick="closeTableHelp()">Close</button> <button class="btn btn-info" onclick="closeTableHelp()">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -738,8 +804,8 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeUserModal()"> <button type="button" class="btn btn-info" onclick="closeUserModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
💾 Save User 💾 Save User
@@ -772,8 +838,8 @@
<input type="password" id="change-password-confirm" name="confirm_password" required> <input type="password" id="change-password-confirm" name="confirm_password" required>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeChangePasswordModal()"> <button type="button" class="btn btn-info" onclick="closeChangePasswordModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-warning"> <button type="submit" class="btn btn-warning">
🔐 Change Password 🔐 Change Password
@@ -801,8 +867,8 @@
<input type="datetime-local" id="event-timestamp" name="timestamp" required> <input type="datetime-local" id="event-timestamp" name="timestamp" required>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeTimestampModal()"> <button type="button" class="btn btn-info" onclick="closeTimestampModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-success" id="timestamp-submit-btn"> <button type="submit" class="btn btn-success" id="timestamp-submit-btn">
Confirm Confirm
@@ -827,8 +893,8 @@
<input type="datetime-local" id="circuit-timestamp" name="timestamp" required> <input type="datetime-local" id="circuit-timestamp" name="timestamp" required>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeCircuitModal()"> <button type="button" class="btn btn-info" onclick="closeCircuitModal()">
Cancel Close
</button> </button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
Record Circuit Record Circuit
@@ -1137,6 +1203,27 @@
return; return;
} }
// Press 'Escape' to close local flight edit modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('localFlightEditModal').style.display === 'block') {
e.preventDefault();
closeLocalFlightEditModal();
return;
}
// Press 'Escape' to close departure edit modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('departureEditModal').style.display === 'block') {
e.preventDefault();
closeDepartureEditModal();
return;
}
// Press 'Escape' to close arrival edit modal if it's open (allow even when typing in inputs)
if (e.key === 'Escape' && document.getElementById('arrivalEditModal').style.display === 'block') {
e.preventDefault();
closeArrivalEditModal();
return;
}
// Only trigger other shortcuts when not typing in input fields // Only trigger other shortcuts when not typing in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
return; return;
@@ -1871,9 +1958,7 @@
if (isLocal) { if (isLocal) {
openLocalFlightEditModal(flight.id); openLocalFlightEditModal(flight.id);
} else if (isBookedIn) { } else if (isBookedIn) {
// For booked-in flights, we might add a view modal later openArrivalEditModal(flight.id);
// For now, just show a message
showNotification(`Booked-in flight: ${flight.registration}`, false);
} else { } else {
openPPRModal(flight.id); openPPRModal(flight.id);
} }
@@ -3276,8 +3361,10 @@
document.getElementById('local_edit_notes').value = flight.notes || ''; document.getElementById('local_edit_notes').value = flight.notes || '';
// Parse and populate departure time if exists // Parse and populate departure time if exists
if (flight.departure_dt) { // Use departed_dt (actual departure) if available, otherwise etd (planned departure)
const dept = new Date(flight.departure_dt); const departureTime = flight.departed_dt || flight.etd;
if (departureTime) {
const dept = new Date(departureTime);
document.getElementById('local_edit_departure_date').value = dept.toISOString().slice(0, 10); document.getElementById('local_edit_departure_date').value = dept.toISOString().slice(0, 10);
document.getElementById('local_edit_departure_time').value = dept.toISOString().slice(11, 16); document.getElementById('local_edit_departure_time').value = dept.toISOString().slice(11, 16);
} }
@@ -3396,6 +3483,182 @@
currentDepartureId = null; currentDepartureId = null;
} }
// Departure edit form submission
document.getElementById('departure-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!currentDepartureId || !accessToken) return;
const formData = new FormData(this);
const updateData = {};
formData.forEach((value, key) => {
if (key === 'id') return;
// Handle date/time combination for ETD
if (key === 'etd_date' || key === 'etd_time') {
if (!updateData.etd && formData.get('etd_date') && formData.get('etd_time')) {
const dateStr = formData.get('etd_date');
const timeStr = formData.get('etd_time');
updateData.etd = new Date(`${dateStr}T${timeStr}`).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/departures/${currentDepartureId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(updateData)
});
if (!response.ok) throw new Error('Failed to update departure');
closeDepartureEditModal();
loadPPRs(); // Refresh departures display
showNotification('Departure updated successfully');
} catch (error) {
console.error('Error updating departure:', error);
showNotification('Error updating departure', true);
}
});
// Arrival Edit Modal Functions
let currentArrivalId = null;
async function openArrivalEditModal(arrivalId) {
if (!accessToken) return;
try {
const response = await fetch(`/api/v1/arrivals/${arrivalId}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) throw new Error('Failed to load arrival');
const arrival = await response.json();
currentArrivalId = arrival.id;
// Populate form
document.getElementById('arrival-edit-id').value = arrival.id;
document.getElementById('arrival_edit_registration').value = arrival.registration || '';
document.getElementById('arrival_edit_type').value = arrival.type || '';
document.getElementById('arrival_edit_callsign').value = arrival.callsign || '';
document.getElementById('arrival_edit_in_from').value = arrival.in_from || '';
document.getElementById('arrival_edit_pob').value = arrival.pob || '';
document.getElementById('arrival_edit_notes').value = arrival.notes || '';
// Update title
const regOrCallsign = arrival.callsign || arrival.registration;
document.getElementById('arrival-edit-title').textContent = `Arrival: ${regOrCallsign}`;
// Show/hide quick action buttons based on status
const landedBtn = document.getElementById('arrival-btn-landed');
const cancelBtn = document.getElementById('arrival-btn-cancel');
landedBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none';
cancelBtn.style.display = arrival.status === 'BOOKED_IN' ? 'block' : 'none';
// Show modal
document.getElementById('arrivalEditModal').style.display = 'block';
} catch (error) {
console.error('Error loading arrival:', error);
showNotification('Error loading arrival details', true);
}
}
function closeArrivalEditModal() {
document.getElementById('arrivalEditModal').style.display = 'none';
currentArrivalId = null;
}
// Arrival edit form submission
document.getElementById('arrival-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!currentArrivalId || !accessToken) return;
const formData = new FormData(this);
const updateData = {};
formData.forEach((value, key) => {
if (key === 'id') return;
// Only include non-empty values
if (typeof value === 'number' || (typeof value === 'string' && value.trim() !== '')) {
if (key === 'pob') {
updateData[key] = parseInt(value);
} else if (value.trim) {
updateData[key] = value.trim();
} else {
updateData[key] = value;
}
}
});
try {
const response = await fetch(`/api/v1/arrivals/${currentArrivalId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(updateData)
});
if (!response.ok) throw new Error('Failed to update arrival');
closeArrivalEditModal();
loadPPRs(); // Refresh arrivals display
showNotification('Arrival updated successfully');
} catch (error) {
console.error('Error updating arrival:', error);
showNotification('Error updating arrival', true);
}
});
async function updateArrivalStatus(status) {
if (!currentArrivalId || !accessToken) return;
if (status === 'CANCELLED') {
if (!confirm('Are you sure you want to cancel this arrival?')) {
return;
}
}
try {
const response = await fetch(`/api/v1/arrivals/${currentArrivalId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({ status: status })
});
if (!response.ok) throw new Error('Failed to update arrival status');
closeArrivalEditModal();
loadPPRs(); // Refresh arrivals display
showNotification(`Arrival marked as ${status.toLowerCase()}`);
} catch (error) {
console.error('Error updating arrival status:', error);
showNotification('Error updating arrival status', true);
}
}
// Update status from table buttons (with flight ID passed) // Update status from table buttons (with flight ID passed)
async function updateLocalFlightStatusFromTable(flightId, status) { async function updateLocalFlightStatusFromTable(flightId, status) {
if (!accessToken) return; if (!accessToken) return;