Commit often...
This commit is contained in:
@@ -152,6 +152,7 @@ async def update_ppr_status(
|
|||||||
db,
|
db,
|
||||||
ppr_id=ppr_id,
|
ppr_id=ppr_id,
|
||||||
status=status_update.status,
|
status=status_update.status,
|
||||||
|
timestamp=status_update.timestamp,
|
||||||
user=current_user.username,
|
user=current_user.username,
|
||||||
user_ip=client_ip
|
user_ip=client_ip
|
||||||
)
|
)
|
||||||
@@ -169,7 +170,7 @@ async def update_ppr_status(
|
|||||||
"id": ppr.id,
|
"id": ppr.id,
|
||||||
"ac_reg": ppr.ac_reg,
|
"ac_reg": ppr.ac_reg,
|
||||||
"status": ppr.status.value,
|
"status": ppr.status.value,
|
||||||
"timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else None
|
"timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else (ppr.departed_dt.isoformat() if ppr.departed_dt else None)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class CRUDPPR:
|
|||||||
db: Session,
|
db: Session,
|
||||||
ppr_id: int,
|
ppr_id: int,
|
||||||
status: PPRStatus,
|
status: PPRStatus,
|
||||||
|
timestamp: Optional[datetime] = None,
|
||||||
user: str = "system",
|
user: str = "system",
|
||||||
user_ip: str = "127.0.0.1"
|
user_ip: str = "127.0.0.1"
|
||||||
) -> Optional[PPRRecord]:
|
) -> Optional[PPRRecord]:
|
||||||
@@ -120,11 +121,12 @@ class CRUDPPR:
|
|||||||
old_status = db_obj.status
|
old_status = db_obj.status
|
||||||
db_obj.status = status
|
db_obj.status = status
|
||||||
|
|
||||||
# Set timestamps based on status
|
# Set timestamps based on status - use provided timestamp or current time
|
||||||
|
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||||
if status == PPRStatus.LANDED:
|
if status == PPRStatus.LANDED:
|
||||||
db_obj.landed_dt = datetime.utcnow()
|
db_obj.landed_dt = current_time
|
||||||
elif status == PPRStatus.DEPARTED:
|
elif status == PPRStatus.DEPARTED:
|
||||||
db_obj.departed_dt = datetime.utcnow()
|
db_obj.departed_dt = current_time
|
||||||
|
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class PPRUpdate(BaseModel):
|
|||||||
|
|
||||||
class PPRStatusUpdate(BaseModel):
|
class PPRStatusUpdate(BaseModel):
|
||||||
status: PPRStatus
|
status: PPRStatus
|
||||||
|
timestamp: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class PPRInDBBase(PPRBase):
|
class PPRInDBBase(PPRBase):
|
||||||
|
|||||||
@@ -72,6 +72,24 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- ppr_network
|
- ppr_network
|
||||||
|
|
||||||
|
# phpMyAdmin for database management
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin/phpmyadmin
|
||||||
|
container_name: ppr_nextgen_phpmyadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PMA_HOST: db
|
||||||
|
PMA_PORT: 3306
|
||||||
|
PMA_USER: ppr_user
|
||||||
|
PMA_PASSWORD: ppr_password123
|
||||||
|
UPLOAD_LIMIT: 50M
|
||||||
|
ports:
|
||||||
|
- "8083:80" # phpMyAdmin web interface
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- ppr_network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
|
|
||||||
|
|||||||
240
web/admin.html
240
web/admin.html
@@ -110,9 +110,14 @@
|
|||||||
background-color: #c0392b;
|
background-color: #c0392b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-sm {
|
.btn-icon {
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.3rem 0.6rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
@@ -621,10 +626,10 @@
|
|||||||
<button id="btn-confirm" class="btn btn-success btn-sm" onclick="updateStatus('CONFIRMED')">
|
<button id="btn-confirm" class="btn btn-success btn-sm" onclick="updateStatus('CONFIRMED')">
|
||||||
✓ Confirm
|
✓ Confirm
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-landed" class="btn btn-warning btn-sm" onclick="updateStatus('LANDED')">
|
<button id="btn-landed" class="btn btn-warning btn-sm" onclick="showTimestampModal('LANDED')">
|
||||||
🛬 Landed
|
🛬 Landed
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-departed" class="btn btn-primary btn-sm" onclick="updateStatus('DEPARTED')">
|
<button id="btn-departed" class="btn btn-primary btn-sm" onclick="showTimestampModal('DEPARTED')">
|
||||||
🛫 Departed
|
🛫 Departed
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-cancel" class="btn btn-danger btn-sm" onclick="updateStatus('CANCELED')">
|
<button id="btn-cancel" class="btn btn-danger btn-sm" onclick="updateStatus('CANCELED')">
|
||||||
@@ -726,11 +731,92 @@
|
|||||||
<!-- Success Notification -->
|
<!-- Success Notification -->
|
||||||
<div id="notification" class="notification"></div>
|
<div id="notification" class="notification"></div>
|
||||||
|
|
||||||
|
<!-- Timestamp Modal for Landing/Departure -->
|
||||||
|
<div id="timestampModal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="timestamp-modal-title">Confirm Landing Time</h2>
|
||||||
|
<button class="close" onclick="closeTimestampModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="timestamp-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-timestamp">Event Time (Local Time) *</label>
|
||||||
|
<input type="datetime-local" id="event-timestamp" name="timestamp" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="closeTimestampModal()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-success" id="timestamp-submit-btn">
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let accessToken = null;
|
let accessToken = null;
|
||||||
let currentPPRId = null;
|
let currentPPRId = null;
|
||||||
let isNewPPR = false;
|
let isNewPPR = false;
|
||||||
|
let wsConnection = null;
|
||||||
|
let pendingStatusUpdate = null; // Track pending status update for timestamp modal
|
||||||
|
|
||||||
|
// WebSocket connection for real-time updates
|
||||||
|
function connectWebSocket() {
|
||||||
|
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
|
||||||
|
return; // Already connected
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/ws/tower-updates`;
|
||||||
|
|
||||||
|
wsConnection = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
wsConnection.onopen = function(event) {
|
||||||
|
console.log('WebSocket connected for real-time updates');
|
||||||
|
};
|
||||||
|
|
||||||
|
wsConnection.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('WebSocket message received:', data);
|
||||||
|
|
||||||
|
// Refresh PPRs when any PPR-related event occurs
|
||||||
|
if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) {
|
||||||
|
console.log('PPR update detected, refreshing...');
|
||||||
|
loadPPRs();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wsConnection.onclose = function(event) {
|
||||||
|
console.log('WebSocket disconnected');
|
||||||
|
// Attempt to reconnect after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (accessToken) { // Only reconnect if still logged in
|
||||||
|
console.log('Attempting to reconnect WebSocket...');
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
wsConnection.onerror = function(error) {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectWebSocket() {
|
||||||
|
if (wsConnection) {
|
||||||
|
wsConnection.close();
|
||||||
|
wsConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notification system
|
// Notification system
|
||||||
function showNotification(message, isError = false) {
|
function showNotification(message, isError = false) {
|
||||||
@@ -770,6 +856,7 @@
|
|||||||
accessToken = cachedToken;
|
accessToken = cachedToken;
|
||||||
currentUser = cachedUser;
|
currentUser = cachedUser;
|
||||||
document.getElementById('current-user').textContent = cachedUser;
|
document.getElementById('current-user').textContent = cachedUser;
|
||||||
|
connectWebSocket(); // Connect WebSocket for real-time updates
|
||||||
loadPPRs();
|
loadPPRs();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -795,6 +882,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Press 'Escape' to close timestamp modal if it's open (allow even when typing in inputs)
|
||||||
|
if (e.key === 'Escape' && document.getElementById('timestampModal').style.display === 'block') {
|
||||||
|
e.preventDefault();
|
||||||
|
closeTimestampModal();
|
||||||
|
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;
|
||||||
@@ -854,6 +948,7 @@
|
|||||||
document.getElementById('current-user').textContent = username;
|
document.getElementById('current-user').textContent = username;
|
||||||
|
|
||||||
hideLogin();
|
hideLogin();
|
||||||
|
connectWebSocket(); // Connect WebSocket for real-time updates
|
||||||
loadPPRs();
|
loadPPRs();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.detail || 'Authentication failed');
|
throw new Error(data.detail || 'Authentication failed');
|
||||||
@@ -878,8 +973,11 @@
|
|||||||
accessToken = null;
|
accessToken = null;
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
|
|
||||||
|
disconnectWebSocket(); // Disconnect WebSocket
|
||||||
|
|
||||||
// Close any open modals
|
// Close any open modals
|
||||||
closePPRModal();
|
closePPRModal();
|
||||||
|
closeTimestampModal();
|
||||||
|
|
||||||
// Show login again
|
// Show login again
|
||||||
showLogin();
|
showLogin();
|
||||||
@@ -1026,8 +1124,11 @@
|
|||||||
<td>${ppr.pob_in}</td>
|
<td>${ppr.pob_in}</td>
|
||||||
<td>${ppr.fuel || '-'}</td>
|
<td>${ppr.fuel || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openPPRModal(${ppr.id})">
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); showTimestampModal('LANDED', ${ppr.id})" title="Mark as Landed">
|
||||||
Edit
|
🛬
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Arrival">
|
||||||
|
❌
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@@ -1070,8 +1171,11 @@
|
|||||||
<td>${ppr.fuel || '-'}</td>
|
<td>${ppr.fuel || '-'}</td>
|
||||||
<td>${ppr.landed_dt ? formatTimeOnly(ppr.landed_dt) : '-'}</td>
|
<td>${ppr.landed_dt ? formatTimeOnly(ppr.landed_dt) : '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openPPRModal(${ppr.id})">
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ppr.id})" title="Mark as Departed">
|
||||||
Edit
|
🛫
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-icon" onclick="event.stopPropagation(); updateStatusFromTable(${ppr.id}, 'CANCELED')" title="Cancel Departure">
|
||||||
|
❌
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@@ -1235,6 +1339,84 @@
|
|||||||
isNewPPR = false;
|
isNewPPR = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timestamp modal functions
|
||||||
|
function showTimestampModal(status, pprId = null) {
|
||||||
|
const targetPprId = pprId || currentPPRId;
|
||||||
|
if (!targetPprId) return;
|
||||||
|
|
||||||
|
pendingStatusUpdate = { status: status, pprId: targetPprId };
|
||||||
|
|
||||||
|
const modalTitle = document.getElementById('timestamp-modal-title');
|
||||||
|
const submitBtn = document.getElementById('timestamp-submit-btn');
|
||||||
|
|
||||||
|
if (status === 'LANDED') {
|
||||||
|
modalTitle.textContent = 'Confirm Landing Time';
|
||||||
|
submitBtn.textContent = '🛬 Confirm Landing';
|
||||||
|
} else if (status === 'DEPARTED') {
|
||||||
|
modalTitle.textContent = 'Confirm Departure Time';
|
||||||
|
submitBtn.textContent = '🛫 Confirm Departure';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTimestampModal() {
|
||||||
|
document.getElementById('timestampModal').style.display = 'none';
|
||||||
|
pendingStatusUpdate = null;
|
||||||
|
document.getElementById('timestamp-form').reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp form submission
|
||||||
|
document.getElementById('timestamp-form').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!pendingStatusUpdate || !accessToken) return;
|
||||||
|
|
||||||
|
const timestampInput = document.getElementById('event-timestamp').value;
|
||||||
|
let timestamp = null;
|
||||||
|
|
||||||
|
if (timestampInput.trim()) {
|
||||||
|
// Convert local datetime-local to UTC ISO string
|
||||||
|
timestamp = new Date(timestampInput).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/pprs/${pendingStatusUpdate.pprId}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: pendingStatusUpdate.status,
|
||||||
|
timestamp: timestamp
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(`Failed to update status: ${response.status} ${response.statusText} - ${errorData.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedStatus = pendingStatusUpdate.status;
|
||||||
|
closeTimestampModal();
|
||||||
|
loadPPRs(); // Refresh both tables
|
||||||
|
showNotification(`Status updated to ${updatedStatus}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating status:', error);
|
||||||
|
showNotification(`Error updating status: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Form submission
|
// Form submission
|
||||||
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
|
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1320,6 +1502,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateStatusFromTable(pprId, status) {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
// Show confirmation for cancel actions
|
||||||
|
if (status === 'CANCELED') {
|
||||||
|
if (!confirm('Are you sure you want to cancel this PPR? This action cannot be easily undone.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/pprs/${pprId}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: status })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(`Failed to update status: ${response.status} ${response.statusText} - ${errorData.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPPRs(); // Refresh both tables
|
||||||
|
showNotification(`Status updated to ${status}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating status:', error);
|
||||||
|
showNotification(`Error updating status: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deletePPR() {
|
async function deletePPR() {
|
||||||
if (!currentPPRId || !accessToken) return;
|
if (!currentPPRId || !accessToken) return;
|
||||||
|
|
||||||
@@ -1350,10 +1565,15 @@
|
|||||||
|
|
||||||
// Close modal when clicking outside
|
// Close modal when clicking outside
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modal = document.getElementById('pprModal');
|
const pprModal = document.getElementById('pprModal');
|
||||||
if (event.target === modal) {
|
const timestampModal = document.getElementById('timestampModal');
|
||||||
|
|
||||||
|
if (event.target === pprModal) {
|
||||||
closePPRModal();
|
closePPRModal();
|
||||||
}
|
}
|
||||||
|
if (event.target === timestampModal) {
|
||||||
|
closeTimestampModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aircraft Lookup Functions
|
// Aircraft Lookup Functions
|
||||||
|
|||||||
Reference in New Issue
Block a user