Compare commits

...

2 Commits

Author SHA1 Message Date
James Pattinson
bd1200f377 Session timeout issue and Extra tables 2025-12-10 14:02:31 +00:00
James Pattinson
f4b69aace0 Info display tweaks 2025-12-10 13:29:12 +00:00
7 changed files with 346 additions and 37 deletions

View File

@@ -33,7 +33,11 @@ async def login_for_access_token(
subject=user.username, expires_delta=access_token_expires subject=user.username, expires_delta=access_token_expires
) )
return {"access_token": access_token, "token_type": "bearer"} return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": settings.access_token_expire_minutes * 60 # seconds
}
@router.post("/test-token", response_model=User) @router.post("/test-token", response_model=User)

View File

@@ -3,19 +3,19 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_db from app.api.deps import get_db
from app.crud.crud_ppr import ppr as crud_ppr from app.crud.crud_ppr import ppr as crud_ppr
from app.schemas.ppr import PPR from app.schemas.ppr import PPRPublic
router = APIRouter() router = APIRouter()
@router.get("/arrivals", response_model=List[PPR]) @router.get("/arrivals", response_model=List[PPRPublic])
async def get_public_arrivals(db: Session = Depends(get_db)): async def get_public_arrivals(db: Session = Depends(get_db)):
"""Get today's arrivals for public display""" """Get today's arrivals for public display"""
arrivals = crud_ppr.get_arrivals_today(db) arrivals = crud_ppr.get_arrivals_today(db)
return arrivals return arrivals
@router.get("/departures", response_model=List[PPR]) @router.get("/departures", response_model=List[PPRPublic])
async def get_public_departures(db: Session = Depends(get_db)): async def get_public_departures(db: Session = Depends(get_db)):
"""Get today's departures for public display""" """Get today's departures for public display"""
departures = crud_ppr.get_departures_today(db) departures = crud_ppr.get_departures_today(db)

View File

@@ -48,7 +48,7 @@ class CRUDPPR:
return query.order_by(desc(PPRRecord.submitted_dt)).offset(skip).limit(limit).all() return query.order_by(desc(PPRRecord.submitted_dt)).offset(skip).limit(limit).all()
def get_arrivals_today(self, db: Session) -> List[PPRRecord]: def get_arrivals_today(self, db: Session) -> List[PPRRecord]:
"""Get today's arrivals""" """Get today's arrivals - includes aircraft that have arrived and may have departed"""
today = date.today() today = date.today()
return db.query(PPRRecord).filter( return db.query(PPRRecord).filter(
and_( and_(
@@ -56,7 +56,8 @@ class CRUDPPR:
or_( or_(
PPRRecord.status == PPRStatus.NEW, PPRRecord.status == PPRStatus.NEW,
PPRRecord.status == PPRStatus.CONFIRMED, PPRRecord.status == PPRStatus.CONFIRMED,
PPRRecord.status == PPRStatus.LANDED PPRRecord.status == PPRStatus.LANDED,
PPRRecord.status == PPRStatus.DEPARTED
) )
) )
).order_by(PPRRecord.eta).all() ).order_by(PPRRecord.eta).all()

View File

@@ -96,6 +96,25 @@ class PPR(PPRInDBBase):
pass pass
class PPRPublic(BaseModel):
"""Public schema for arrivals/departures board - excludes sensitive data"""
id: int
status: PPRStatus
ac_reg: str
ac_type: str
ac_call: Optional[str] = None
in_from: str
eta: datetime
out_to: Optional[str] = None
etd: Optional[datetime] = None
landed_dt: Optional[datetime] = None
departed_dt: Optional[datetime] = None
submitted_dt: datetime
class Config:
from_attributes = True
class PPRInDB(PPRInDBBase): class PPRInDB(PPRInDBBase):
pass pass
@@ -135,6 +154,7 @@ class UserInDB(UserInDBBase):
class Token(BaseModel): class Token(BaseModel):
access_token: str access_token: str
token_type: str token_type: str
expires_in: int # Token expiry in seconds
class TokenData(BaseModel): class TokenData(BaseModel):

View File

@@ -564,7 +564,7 @@
</div> </div>
<div class="menu-buttons"> <div class="menu-buttons">
<button class="btn btn-success" onclick="openNewPPRModal()"> <button class="btn btn-success" onclick="openNewPPRModal()">
New PPR Entry New PPR
</button> </button>
<button class="btn btn-primary" onclick="window.open('reports.html', '_blank')"> <button class="btn btn-primary" onclick="window.open('reports.html', '_blank')">
📊 Reports 📊 Reports
@@ -587,7 +587,7 @@
<!-- Arrivals Table --> <!-- Arrivals Table -->
<div class="ppr-table"> <div class="ppr-table">
<div class="table-header"> <div class="table-header">
🛬 Today's Arrivals - <span id="arrivals-count">0</span> entries 🛬 Pending Arrivals - <span id="arrivals-count">0</span>
</div> </div>
<div id="arrivals-loading" class="loading"> <div id="arrivals-loading" class="loading">
@@ -614,15 +614,14 @@
</div> </div>
<div id="arrivals-no-data" class="no-data" style="display: none;"> <div id="arrivals-no-data" class="no-data" style="display: none;">
<h3>No arrivals for today</h3> <h3>No Pending Arrivals</h3>
<p>No NEW or CONFIRMED arrivals scheduled for today.</p>
</div> </div>
</div> </div>
<!-- Departures Table --> <!-- Departures Table -->
<div class="ppr-table" style="margin-top: 2rem;"> <div class="ppr-table" style="margin-top: 2rem;">
<div class="table-header"> <div class="table-header">
🛫 Today's Departures - <span id="departures-count">0</span> entries 🛫 Pending Departures - <span id="departures-count">0</span>
</div> </div>
<div id="departures-loading" class="loading"> <div id="departures-loading" class="loading">
@@ -650,10 +649,76 @@
</div> </div>
<div id="departures-no-data" class="no-data" style="display: none;"> <div id="departures-no-data" class="no-data" style="display: none;">
<h3>No departures for today</h3> <h3>No Pending Departures</h3>
<p>No aircraft currently landed and ready to depart.</p> <p>No aircraft currently landed and ready to depart.</p>
</div> </div>
</div> </div>
<br>
<!-- Departed and Parked Tables -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem;">
<!-- Departed Today -->
<div class="ppr-table">
<div class="table-header" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">
✈️ Departed Today - <span id="departed-count">0</span>
</div>
<div id="departed-loading" class="loading" style="display: none;">
<div class="spinner"></div>
Loading departed aircraft...
</div>
<div id="departed-table-content" style="display: none;">
<table>
<thead>
<tr style="font-size: 0.85rem !important;">
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Registration</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Callsign</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Destination</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Departed</th>
</tr>
</thead>
<tbody id="departed-table-body">
</tbody>
</table>
</div>
<div id="departed-no-data" class="no-data" style="display: none; padding: 1rem; font-size: 0.9rem;">
<p>No departures today.</p>
</div>
</div>
<!-- Parked Visitors -->
<div class="ppr-table">
<div class="table-header" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">
🅿️ Parked Visitors - <span id="parked-count">0</span>
</div>
<div id="parked-loading" class="loading" style="display: none;">
<div class="spinner"></div>
Loading parked visitors...
</div>
<div id="parked-table-content" style="display: none;">
<table>
<thead>
<tr style="font-size: 0.85rem !important;">
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Registration</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Type</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">From</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">Arrived</th>
<th style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">ETD</th>
</tr>
</thead>
<tbody id="parked-table-body">
</tbody>
</table>
</div>
<div id="parked-no-data" class="no-data" style="display: none; padding: 1rem; font-size: 0.9rem;">
<p>No parked visitors.</p>
</div>
</div>
</div>
</div> </div>
<!-- Login Modal --> <!-- Login Modal -->
@@ -1227,8 +1292,9 @@
const data = await response.json(); const data = await response.json();
if (response.ok && data.access_token) { if (response.ok && data.access_token) {
// Store token and user info with expiry (30 minutes from now) // Store token and user info with expiry from server response
const expiryTime = new Date().getTime() + (30 * 60 * 1000); // 30 minutes const expiresInMs = (data.expires_in || 1800) * 1000; // Use server value or default to 30 min
const expiryTime = new Date().getTime() + expiresInMs;
localStorage.setItem('ppr_access_token', data.access_token); localStorage.setItem('ppr_access_token', data.access_token);
localStorage.setItem('ppr_username', username); localStorage.setItem('ppr_username', username);
@@ -1309,12 +1375,12 @@
return response; return response;
} }
// Load PPR records - now loads both arrivals and departures // Load PPR records - now loads all tables
async function loadPPRs() { async function loadPPRs() {
if (!accessToken) return; if (!accessToken) return;
// Load both arrivals and departures simultaneously // Load all tables simultaneously
await Promise.all([loadArrivals(), loadDepartures()]); await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked()]);
} }
// Load arrivals (NEW and CONFIRMED status) // Load arrivals (NEW and CONFIRMED status)
@@ -1324,22 +1390,26 @@
document.getElementById('arrivals-no-data').style.display = 'none'; document.getElementById('arrivals-no-data').style.display = 'none';
try { try {
// Always load today's date // Load all PPRs and filter client-side for today's arrivals
const today = new Date().toISOString().split('T')[0]; // We filter by ETA date (not ETD) and NEW/CONFIRMED status
let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`; const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
const response = await authenticatedFetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch arrivals'); throw new Error('Failed to fetch arrivals');
} }
const allPPRs = await response.json(); const allPPRs = await response.json();
const today = new Date().toISOString().split('T')[0];
// Filter for arrivals (NEW and CONFIRMED with ETA only) // Filter for arrivals with ETA today and NEW or CONFIRMED status
const arrivals = allPPRs.filter(ppr => const arrivals = allPPRs.filter(ppr => {
(ppr.status === 'NEW' || ppr.status === 'CONFIRMED') && ppr.eta if (!ppr.eta || (ppr.status !== 'NEW' && ppr.status !== 'CONFIRMED')) {
); return false;
}
// Extract date from ETA (UTC)
const etaDate = ppr.eta.split('T')[0];
return etaDate === today;
});
displayArrivals(arrivals); displayArrivals(arrivals);
} catch (error) { } catch (error) {
@@ -1359,20 +1429,26 @@
document.getElementById('departures-no-data').style.display = 'none'; document.getElementById('departures-no-data').style.display = 'none';
try { try {
// Always load today's date // Load all PPRs and filter client-side for today's departures
const today = new Date().toISOString().split('T')[0]; // We filter by ETD date and LANDED status only (exclude DEPARTED)
let url = `/api/v1/pprs/?limit=1000&date_from=${today}&date_to=${today}`; const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
const response = await authenticatedFetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch departures'); throw new Error('Failed to fetch departures');
} }
const allPPRs = await response.json(); const allPPRs = await response.json();
const today = new Date().toISOString().split('T')[0];
// Filter for departures (LANDED status only) // Filter for departures with ETD today and LANDED status only
const departures = allPPRs.filter(ppr => ppr.status === 'LANDED'); const departures = allPPRs.filter(ppr => {
if (!ppr.etd || ppr.status !== 'LANDED') {
return false;
}
// Extract date from ETD (UTC)
const etdDate = ppr.etd.split('T')[0];
return etdDate === today;
});
displayDepartures(departures); displayDepartures(departures);
} catch (error) { } catch (error) {
@@ -1385,6 +1461,171 @@
document.getElementById('departures-loading').style.display = 'none'; document.getElementById('departures-loading').style.display = 'none';
} }
// Load departed aircraft (DEPARTED status with departed_dt today)
async function loadDeparted() {
document.getElementById('departed-loading').style.display = 'block';
document.getElementById('departed-table-content').style.display = 'none';
document.getElementById('departed-no-data').style.display = 'none';
try {
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
if (!response.ok) {
throw new Error('Failed to fetch departed aircraft');
}
const allPPRs = await response.json();
const today = new Date().toISOString().split('T')[0];
// Filter for aircraft departed today
const departed = allPPRs.filter(ppr => {
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
return false;
}
const departedDate = ppr.departed_dt.split('T')[0];
return departedDate === today;
});
displayDeparted(departed);
} catch (error) {
console.error('Error loading departed aircraft:', error);
if (error.message !== 'Session expired. Please log in again.') {
showNotification('Error loading departed aircraft', true);
}
}
document.getElementById('departed-loading').style.display = 'none';
}
function displayDeparted(departed) {
const tbody = document.getElementById('departed-table-body');
document.getElementById('departed-count').textContent = departed.length;
if (departed.length === 0) {
document.getElementById('departed-no-data').style.display = 'block';
return;
}
// Sort by departed time
departed.sort((a, b) => new Date(a.departed_dt) - new Date(b.departed_dt));
tbody.innerHTML = '';
document.getElementById('departed-table-content').style.display = 'block';
for (const ppr of departed) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
row.innerHTML = `
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_reg || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_call || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.out_to || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(ppr.departed_dt)}</td>
`;
tbody.appendChild(row);
}
}
// Load parked visitors (LANDED status with no ETD today or ETD not today)
async function loadParked() {
document.getElementById('parked-loading').style.display = 'block';
document.getElementById('parked-table-content').style.display = 'none';
document.getElementById('parked-no-data').style.display = 'none';
try {
const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
if (!response.ok) {
throw new Error('Failed to fetch parked visitors');
}
const allPPRs = await response.json();
const today = new Date().toISOString().split('T')[0];
// Filter for parked visitors: LANDED status and (no ETD or ETD not today)
// Show all parked aircraft regardless of when they arrived
const parked = allPPRs.filter(ppr => {
if (ppr.status !== 'LANDED') {
return false;
}
// No ETD means parked
if (!ppr.etd) {
return true;
}
// ETD exists but is not today
const etdDate = ppr.etd.split('T')[0];
return etdDate !== today;
});
displayParked(parked);
} catch (error) {
console.error('Error loading parked visitors:', error);
if (error.message !== 'Session expired. Please log in again.') {
showNotification('Error loading parked visitors', true);
}
}
document.getElementById('parked-loading').style.display = 'none';
}
function displayParked(parked) {
const tbody = document.getElementById('parked-table-body');
document.getElementById('parked-count').textContent = parked.length;
if (parked.length === 0) {
document.getElementById('parked-no-data').style.display = 'block';
return;
}
// Sort by landed time
parked.sort((a, b) => {
if (!a.landed_dt) return 1;
if (!b.landed_dt) return -1;
return new Date(a.landed_dt) - new Date(b.landed_dt);
});
tbody.innerHTML = '';
document.getElementById('parked-table-content').style.display = 'block';
for (const ppr of parked) {
const row = document.createElement('tr');
row.onclick = () => openPPRModal(ppr.id);
row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
// Format arrival: time if today, date if not
const today = new Date().toISOString().split('T')[0];
let arrivedDisplay = '-';
if (ppr.landed_dt) {
const landedDate = ppr.landed_dt.split('T')[0];
if (landedDate === today) {
// Today - show time only
arrivedDisplay = formatTimeOnly(ppr.landed_dt);
} else {
// Not today - show date (DD/MM)
const date = new Date(ppr.landed_dt);
arrivedDisplay = date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
}
}
// Format ETD as just the date (DD/MM)
let etdDisplay = '-';
if (ppr.etd) {
const etdDate = new Date(ppr.etd);
etdDisplay = etdDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
}
row.innerHTML = `
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_reg || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.ac_type || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${ppr.in_from || '-'}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${arrivedDisplay}</td>
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${etdDisplay}</td>
`;
tbody.appendChild(row);
}
}
// ICAO code to airport name cache // ICAO code to airport name cache
const airportNameCache = {}; const airportNameCache = {};
@@ -1548,7 +1789,7 @@
isNewPPR = true; isNewPPR = true;
currentPPRId = null; currentPPRId = null;
etdManuallyEdited = false; // Reset the manual edit flag for new PPR etdManuallyEdited = false; // Reset the manual edit flag for new PPR
document.getElementById('modal-title').textContent = 'New PPR Entry'; document.getElementById('modal-title').textContent = 'New PPR';
document.getElementById('delete-btn').style.display = 'none'; document.getElementById('delete-btn').style.display = 'none';
document.getElementById('journal-section').style.display = 'none'; document.getElementById('journal-section').style.display = 'none';
document.querySelector('.quick-actions').style.display = 'none'; document.querySelector('.quick-actions').style.display = 'none';

View File

@@ -309,7 +309,7 @@
// Show landed time if available, otherwise ETA // Show landed time if available, otherwise ETA
let timeDisplay; let timeDisplay;
if (arrival.status === 'LANDED' && arrival.landed_dt) { if ((arrival.status === 'LANDED' || arrival.status === 'DEPARTED') && arrival.landed_dt) {
const time = convertToLocalTime(arrival.landed_dt); const time = convertToLocalTime(arrival.landed_dt);
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`; timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #27ae60; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #27ae60; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">LANDED</span></div>`;
} else { } else {

View File

@@ -454,6 +454,41 @@
console.log('Source:', window.PPR_CONFIG?.apiBase ? 'config.js' : 'fallback'); console.log('Source:', window.PPR_CONFIG?.apiBase ? 'config.js' : 'fallback');
console.log('======================='); console.log('=======================');
// Track if user has manually edited ETD
let etdManuallyEdited = false;
// Function to update ETD based on ETA (2 hours later)
function updateETDFromETA() {
// Only auto-update if user hasn't manually edited ETD
if (etdManuallyEdited) {
return;
}
const etaDate = document.getElementById('eta-date').value;
const etaTime = document.getElementById('eta-time').value;
if (etaDate && etaTime) {
// Parse ETA
const eta = new Date(`${etaDate}T${etaTime}`);
// Calculate ETD (2 hours after ETA)
const etd = new Date(eta.getTime() + 2 * 60 * 60 * 1000);
// Format ETD
const etdDateStr = `${etd.getFullYear()}-${String(etd.getMonth() + 1).padStart(2, '0')}-${String(etd.getDate()).padStart(2, '0')}`;
const etdTimeStr = `${String(etd.getHours()).padStart(2, '0')}:${String(etd.getMinutes()).padStart(2, '0')}`;
// Update ETD fields
document.getElementById('etd-date').value = etdDateStr;
document.getElementById('etd-time').value = etdTimeStr;
}
}
// Function to mark ETD as manually edited
function markETDAsManuallyEdited() {
etdManuallyEdited = true;
}
// Iframe resizing functionality // Iframe resizing functionality
function sendHeightToParent() { function sendHeightToParent() {
const height = document.body.scrollHeight || document.documentElement.scrollHeight; const height = document.body.scrollHeight || document.documentElement.scrollHeight;
@@ -814,9 +849,9 @@
const nextHour = new Date(now); const nextHour = new Date(now);
nextHour.setHours(now.getHours() + 1, 0, 0, 0); nextHour.setHours(now.getHours() + 1, 0, 0, 0);
// ETD is 1 hour after ETA // ETD is 2 hours after ETA
const etd = new Date(nextHour); const etd = new Date(nextHour);
etd.setHours(nextHour.getHours() + 1); etd.setHours(nextHour.getHours() + 2);
// Format date and time for separate inputs // Format date and time for separate inputs
function formatDate(date) { function formatDate(date) {
@@ -845,6 +880,14 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initializeTimeDropdowns(); initializeTimeDropdowns();
setDefaultDateTime(); setDefaultDateTime();
// Add event listeners to ETA fields to auto-update ETD
document.getElementById('eta-date').addEventListener('change', updateETDFromETA);
document.getElementById('eta-time').addEventListener('change', updateETDFromETA);
// Add event listeners to ETD fields to mark as manually edited
document.getElementById('etd-date').addEventListener('change', markETDAsManuallyEdited);
document.getElementById('etd-time').addEventListener('change', markETDAsManuallyEdited);
}); });
</script> </script>
</body> </body>