List and edit user aircraft

This commit is contained in:
2026-03-23 13:09:49 -04:00
parent d2c9bc0370
commit fd0e521186
3 changed files with 338 additions and 7 deletions
+64 -1
View File
@@ -1,9 +1,10 @@
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import func
from app.api.deps import get_db, get_current_active_user from app.api.deps import get_db, get_current_active_user
from app.models.ppr import Aircraft, UserAircraft from app.models.ppr import Aircraft, UserAircraft
from app.schemas.ppr import Aircraft as AircraftSchema, UserAircraftCreate from app.schemas.ppr import Aircraft as AircraftSchema, UserAircraftCreate, UserAircraft as UserAircraftSchema
from app.models.ppr import User from app.models.ppr import User
router = APIRouter() router = APIRouter()
@@ -162,3 +163,65 @@ async def save_user_aircraft(
db.refresh(user_aircraft) db.refresh(user_aircraft)
return {"message": "Aircraft saved successfully", "id": user_aircraft.id} return {"message": "Aircraft saved successfully", "id": user_aircraft.id}
@router.get("/user-aircraft", response_model=List[UserAircraftSchema])
async def get_user_aircraft(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Get all user-defined aircraft types.
"""
user_aircraft = db.query(UserAircraft).order_by(UserAircraft.created_at.desc()).all()
return user_aircraft
@router.put("/user-aircraft/{registration}", response_model=dict)
async def update_user_aircraft(
registration: str,
aircraft: UserAircraftCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Update a user-defined aircraft type.
"""
# Find the existing user aircraft
existing = db.query(UserAircraft).filter(
UserAircraft.registration == registration.upper()
).first()
if not existing:
raise HTTPException(status_code=404, detail="User aircraft not found")
# Update the type
existing.type_code = aircraft.type_code.upper()
existing.updated_at = func.current_timestamp()
db.commit()
return {"message": "Aircraft updated successfully"}
@router.delete("/user-aircraft/{registration}", response_model=dict)
async def delete_user_aircraft(
registration: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Delete a user-defined aircraft type.
"""
# Find the existing user aircraft
existing = db.query(UserAircraft).filter(
UserAircraft.registration == registration.upper()
).first()
if not existing:
raise HTTPException(status_code=404, detail="User aircraft not found")
db.delete(existing)
db.commit()
return {"message": "Aircraft deleted successfully"}
+1 -1
View File
@@ -50,7 +50,7 @@ services:
cpus: '1' cpus: '1'
memory: 1G memory: 1G
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"] test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
+272 -4
View File
@@ -30,6 +30,7 @@
</button> </button>
<div class="dropdown-menu" id="adminDropdownMenu"> <div class="dropdown-menu" id="adminDropdownMenu">
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a> <a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
<a href="#" onclick="openUserAircraftModal(); closeAdminDropdown()" id="user-aircraft-dropdown">✈️ User Aircraft</a>
<a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a> <a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a>
</div> </div>
</div> </div>
@@ -1048,6 +1049,86 @@
<!-- Success Notification --> <!-- Success Notification -->
<div id="notification" class="notification"></div> <div id="notification" class="notification"></div>
<!-- User Aircraft Management Modal -->
<div id="userAircraftModal" class="modal">
<div class="modal-content" style="max-width: 1000px;">
<div class="modal-header">
<h2>User Aircraft Management</h2>
<button class="close" onclick="closeModal('userAircraftModal')">&times;</button>
</div>
<div class="modal-body">
<div class="quick-actions" style="margin-bottom: 1rem;">
<div class="form-group" style="display: inline-block; margin-right: 1rem;">
<label for="user-aircraft-search" style="display: inline; margin-right: 0.5rem;">Search:</label>
<input type="text" id="user-aircraft-search" placeholder="Filter by registration or type..." style="width: 250px;">
</div>
<button class="btn btn-info" onclick="loadUserAircraft()">
🔄 Refresh
</button>
</div>
<div id="user-aircraft-loading" class="loading">
<div class="spinner"></div>
Loading user aircraft...
</div>
<div id="user-aircraft-table-content" style="display: none;">
<table>
<thead>
<tr>
<th>Registration</th>
<th>Type</th>
<th>Added By</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="user-aircraft-table-body">
</tbody>
</table>
</div>
<div id="user-aircraft-no-data" class="no-data" style="display: none;">
<h3>No user aircraft found</h3>
<p>No custom aircraft types have been saved yet.</p>
</div>
</div>
</div>
</div>
<!-- User Aircraft Edit Modal -->
<div id="userAircraftEditModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2 id="user-aircraft-edit-title">Edit User Aircraft</h2>
<button class="close" onclick="closeModal('userAircraftEditModal')">&times;</button>
</div>
<div class="modal-body">
<form id="user-aircraft-edit-form">
<div class="form-group full-width">
<label for="edit-aircraft-registration">Registration *</label>
<input type="text" id="edit-aircraft-registration" name="registration" required readonly style="background-color: #f5f5f5;">
</div>
<div class="form-group full-width">
<label for="edit-aircraft-type">Aircraft Type *</label>
<input type="text" id="edit-aircraft-type" name="type_code" required>
</div>
<div class="form-actions">
<button type="button" class="btn btn-info" onclick="closeModal('userAircraftEditModal')">
Cancel
</button>
<button type="submit" class="btn btn-warning">
💾 Save Changes
</button>
<button type="button" class="btn btn-danger" onclick="deleteUserAircraft()">
🗑️ Delete
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Timestamp Modal for Landing/Departure --> <!-- Timestamp Modal for Landing/Departure -->
<div id="timestampModal" class="modal"> <div id="timestampModal" class="modal">
<div class="modal-content" style="max-width: 400px;"> <div class="modal-content" style="max-width: 400px;">
@@ -3468,6 +3549,182 @@
} }
}); });
// ==================== USER AIRCRAFT MANAGEMENT ====================
async function openUserAircraftModal() {
if (!accessToken) return;
document.getElementById('userAircraftModal').style.display = 'block';
await loadUserAircraft();
// Set up search functionality
const searchInput = document.getElementById('user-aircraft-search');
searchInput.addEventListener('input', filterUserAircraft);
}
async function loadUserAircraft() {
if (!accessToken) return;
document.getElementById('user-aircraft-loading').style.display = 'block';
document.getElementById('user-aircraft-table-content').style.display = 'none';
document.getElementById('user-aircraft-no-data').style.display = 'none';
try {
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft');
if (!response.ok) {
throw new Error('Failed to fetch user aircraft');
}
const userAircraft = await response.json();
displayUserAircraft(userAircraft);
} catch (error) {
console.error('Error loading user aircraft:', error);
if (error.message !== 'Session expired. Please log in again.') {
showNotification('Error loading user aircraft', true);
}
}
document.getElementById('user-aircraft-loading').style.display = 'none';
}
function displayUserAircraft(userAircraft) {
const tbody = document.getElementById('user-aircraft-table-body');
const searchInput = document.getElementById('user-aircraft-search');
// Store original data for filtering
tbody._originalData = userAircraft;
if (userAircraft.length === 0) {
document.getElementById('user-aircraft-no-data').style.display = 'block';
return;
}
// Apply current filter if any
const filterText = searchInput.value.trim().toLowerCase();
const filteredAircraft = filterText ?
userAircraft.filter(ua =>
ua.registration.toLowerCase().includes(filterText) ||
ua.type_code.toLowerCase().includes(filterText) ||
ua.created_by.toLowerCase().includes(filterText)
) : userAircraft;
if (filteredAircraft.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No aircraft match the search criteria</td></tr>';
} else {
tbody.innerHTML = '';
filteredAircraft.forEach(aircraft => {
const row = document.createElement('tr');
// Format created date
const createdDate = aircraft.created_at ? formatDateTime(aircraft.created_at) : '-';
row.innerHTML = `
<td><strong>${aircraft.registration}</strong></td>
<td>${aircraft.type_code}</td>
<td>${aircraft.created_by}</td>
<td>${createdDate}</td>
<td>
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); openUserAircraftEditModal('${aircraft.registration}', '${aircraft.type_code}')" title="Edit Aircraft">
✏️
</button>
</td>
`;
tbody.appendChild(row);
});
}
document.getElementById('user-aircraft-table-content').style.display = 'block';
}
function filterUserAircraft() {
const tbody = document.getElementById('user-aircraft-table-body');
if (tbody._originalData) {
displayUserAircraft(tbody._originalData);
}
}
function openUserAircraftEditModal(registration, typeCode) {
document.getElementById('edit-aircraft-registration').value = registration;
document.getElementById('edit-aircraft-type').value = typeCode;
document.getElementById('user-aircraft-edit-title').textContent = `Edit ${registration}`;
document.getElementById('userAircraftEditModal').style.display = 'block';
// Auto-focus on type field
setTimeout(() => {
document.getElementById('edit-aircraft-type').focus();
}, 100);
}
// User aircraft edit form submission
document.getElementById('user-aircraft-edit-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!accessToken) return;
const registration = document.getElementById('edit-aircraft-registration').value.trim();
const typeCode = document.getElementById('edit-aircraft-type').value.trim();
if (!registration || !typeCode) {
showNotification('Registration and type are required', true);
return;
}
try {
const response = await authenticatedFetch(`/api/v1/aircraft/user-aircraft/${registration}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
registration: registration,
type_code: typeCode
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to update aircraft');
}
closeModal('userAircraftEditModal');
await loadUserAircraft(); // Refresh list
showNotification('Aircraft updated successfully!');
} catch (error) {
console.error('Error updating aircraft:', error);
showNotification(`Error updating aircraft: ${error.message}`, true);
}
});
async function deleteUserAircraft() {
if (!accessToken) return;
const registration = document.getElementById('edit-aircraft-registration').value.trim();
if (!confirm(`Are you sure you want to delete the aircraft entry for ${registration}? This action cannot be undone.`)) {
return;
}
try {
const response = await authenticatedFetch(`/api/v1/aircraft/user-aircraft/${registration}`, {
method: 'DELETE'
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to delete aircraft');
}
closeModal('userAircraftEditModal');
await loadUserAircraft(); // Refresh list
showNotification('Aircraft deleted successfully!');
} catch (error) {
console.error('Error deleting aircraft:', error);
showNotification(`Error deleting aircraft: ${error.message}`, true);
}
}
// Update user role detection and UI visibility // Update user role detection and UI visibility
async function updateUserRole() { async function updateUserRole() {
if (!accessToken) { if (!accessToken) {
@@ -3485,16 +3742,27 @@
// Show user management in dropdown only for administrators // Show user management in dropdown only for administrators
const userManagementDropdown = document.getElementById('user-management-dropdown'); const userManagementDropdown = document.getElementById('user-management-dropdown');
// Show user aircraft for operators and administrators
const userAircraftDropdown = document.getElementById('user-aircraft-dropdown');
if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') { if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') {
userManagementDropdown.style.display = 'block'; if (userManagementDropdown) userManagementDropdown.style.display = 'block';
if (userAircraftDropdown) userAircraftDropdown.style.display = 'block';
} else if (currentUserRole && currentUserRole.toUpperCase() === 'OPERATOR') {
if (userManagementDropdown) userManagementDropdown.style.display = 'none';
if (userAircraftDropdown) userAircraftDropdown.style.display = 'block';
} else { } else {
userManagementDropdown.style.display = 'none'; if (userManagementDropdown) userManagementDropdown.style.display = 'none';
if (userAircraftDropdown) userAircraftDropdown.style.display = 'none';
} }
} }
} catch (error) { } catch (error) {
console.error('Error updating user role:', error); console.error('Error updating user role:', error);
// Hide user management by default on error // Hide admin features by default on error
document.getElementById('user-management-dropdown').style.display = 'none'; const userManagementDropdown = document.getElementById('user-management-dropdown');
const userAircraftDropdown = document.getElementById('user-aircraft-dropdown');
if (userManagementDropdown) userManagementDropdown.style.display = 'none';
if (userAircraftDropdown) userAircraftDropdown.style.display = 'none';
} }
} }