diff --git a/backend/alembic/versions/002_local_flights.py b/backend/alembic/versions/002_local_flights.py
index d8fe175..d0162f7 100644
--- a/backend/alembic/versions/002_local_flights.py
+++ b/backend/alembic/versions/002_local_flights.py
@@ -79,6 +79,7 @@ def upgrade() -> None:
sa.Column('flight_type', sa.Enum('LOCAL', 'CIRCUITS', 'DEPARTURE', name='localflighttype'), nullable=False),
sa.Column('status', sa.Enum('BOOKED_OUT', 'DEPARTED', 'LANDED', 'CANCELLED', name='localflightstatus'), nullable=False, server_default='BOOKED_OUT'),
sa.Column('duration', sa.Integer(), nullable=True, comment='Duration in minutes'),
+ sa.Column('circuits', sa.Integer(), nullable=True, default=0, comment='Actual number of circuits completed'),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('created_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('etd', sa.DateTime(), nullable=True),
diff --git a/backend/app/crud/crud_local_flight.py b/backend/app/crud/crud_local_flight.py
index 7d432cf..621923c 100644
--- a/backend/app/crud/crud_local_flight.py
+++ b/backend/app/crud/crud_local_flight.py
@@ -5,6 +5,7 @@ from datetime import date, datetime
from app.models.local_flight import LocalFlight, LocalFlightStatus, LocalFlightType
from app.schemas.local_flight import LocalFlightCreate, LocalFlightUpdate, LocalFlightStatusUpdate
from app.models.journal import EntityType
+from app.models.circuit import Circuit
from app.crud.crud_journal import journal
@@ -149,6 +150,11 @@ class CRUDLocalFlight:
db_obj.departed_dt = current_time
elif status == LocalFlightStatus.LANDED:
db_obj.landed_dt = current_time
+ # Count circuits from the circuits table and populate the circuits column
+ circuit_count = db.query(func.count(Circuit.id)).filter(
+ Circuit.local_flight_id == flight_id
+ ).scalar()
+ db_obj.circuits = circuit_count
db.add(db_obj)
db.commit()
diff --git a/backend/app/models/local_flight.py b/backend/app/models/local_flight.py
index 3ea5e7f..0b716e6 100644
--- a/backend/app/models/local_flight.py
+++ b/backend/app/models/local_flight.py
@@ -28,6 +28,7 @@ class LocalFlight(Base):
flight_type = Column(SQLEnum(LocalFlightType), nullable=False, index=True)
status = Column(SQLEnum(LocalFlightStatus), nullable=False, default=LocalFlightStatus.BOOKED_OUT, index=True)
duration = Column(Integer, nullable=True) # Duration in minutes
+ circuits = Column(Integer, nullable=True, default=0) # Actual number of circuits completed
notes = Column(Text, nullable=True)
created_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
diff --git a/backend/app/schemas/local_flight.py b/backend/app/schemas/local_flight.py
index 7b9fa64..dfbc408 100644
--- a/backend/app/schemas/local_flight.py
+++ b/backend/app/schemas/local_flight.py
@@ -62,6 +62,7 @@ class LocalFlightUpdate(BaseModel):
status: Optional[LocalFlightStatus] = None
etd: Optional[datetime] = None
departed_dt: Optional[datetime] = None
+ circuits: Optional[int] = None
notes: Optional[str] = None
@@ -77,6 +78,7 @@ class LocalFlightInDBBase(LocalFlightBase):
etd: Optional[datetime] = None
departed_dt: Optional[datetime] = None
landed_dt: Optional[datetime] = None
+ circuits: Optional[int] = None
created_by: Optional[str] = None
updated_at: datetime
diff --git a/web/admin.html b/web/admin.html
index 8c684c8..259dceb 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -2626,7 +2626,7 @@
showNotification(`✈️ Circuit recorded at ${formatTimeOnly(circuit.circuit_timestamp)}`);
closeCircuitModal();
- // Refresh departures to show updated circuit count
+ // Refresh departures to show updated flight info
loadDepartures();
} catch (error) {
console.error('Error recording circuit:', error);
diff --git a/web/reports.html b/web/reports.html
index 80c3690..7540499 100644
--- a/web/reports.html
+++ b/web/reports.html
@@ -227,6 +227,45 @@
.status.canceled { background: #ffebee; color: #d32f2f; }
.status.deleted { background: #f3e5f5; color: #7b1fa2; }
+ .summary-box {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border-radius: 8px;
+ padding: 2rem;
+ margin-bottom: 2rem;
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
+ }
+
+ .summary-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ }
+
+ .summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+ }
+
+ .summary-item {
+ background: rgba(255,255,255,0.1);
+ border-radius: 6px;
+ padding: 1rem;
+ border-left: 4px solid rgba(255,255,255,0.3);
+ }
+
+ .summary-item-label {
+ font-size: 0.85rem;
+ opacity: 0.9;
+ margin-bottom: 0.5rem;
+ }
+
+ .summary-item-value {
+ font-size: 2rem;
+ font-weight: 700;
+ }
+
.no-data {
text-align: center;
padding: 3rem;
@@ -366,6 +405,64 @@
+
+
+
📊 Movements Summary
+
+
+
+
PPR Movements
+
+
+
Arrivals (Landings)
+
0
+
+
+
Departures (Takeoffs)
+
0
+
+
+
+
+
+
+
+
+
+
+
Grand Total Movements
+
0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Status |
+ Type |
+ Aircraft |
+ Aircraft Type |
+ Callsign |
+ From |
+ To |
+ ETA / ETD |
+ Landed / Departed |
+ Circuits |
+
+
+
+
+
+
+
+
+
+
No other flights found
+
No flights match your current filters.
+
+
@@ -431,6 +576,7 @@
let currentUser = null;
let accessToken = null;
let currentPPRs = []; // Store current results for export
+ let currentOtherFlights = []; // Store other flights for export
// Initialize the page
async function initializePage() {
@@ -508,6 +654,9 @@
document.getElementById('reports-loading').style.display = 'block';
document.getElementById('reports-table-content').style.display = 'none';
document.getElementById('reports-no-data').style.display = 'none';
+ document.getElementById('other-flights-loading').style.display = 'block';
+ document.getElementById('other-flights-table-content').style.display = 'none';
+ document.getElementById('other-flights-no-data').style.display = 'none';
try {
const dateFrom = document.getElementById('date-from').value;
@@ -521,13 +670,19 @@
if (dateTo) url += `&date_to=${dateTo}`;
if (status) url += `&status=${status}`;
- const response = await authenticatedFetch(url);
+ // Fetch all data in parallel
+ const [pprResponse, arrivalsResponse, departuresResponse, localFlightsResponse] = await Promise.all([
+ authenticatedFetch(url),
+ authenticatedFetch(`/api/v1/arrivals/?limit=10000${dateFrom ? `&date_from=${dateFrom}` : ''}${dateTo ? `&date_to=${dateTo}` : ''}`),
+ authenticatedFetch(`/api/v1/departures/?limit=10000${dateFrom ? `&date_from=${dateFrom}` : ''}${dateTo ? `&date_to=${dateTo}` : ''}`),
+ authenticatedFetch(`/api/v1/local-flights/?limit=10000${dateFrom ? `&date_from=${dateFrom}` : ''}${dateTo ? `&date_to=${dateTo}` : ''}`)
+ ]);
- if (!response.ok) {
+ if (!pprResponse.ok) {
throw new Error('Failed to fetch PPR records');
}
- let pprs = await response.json();
+ let pprs = await pprResponse.json();
// Apply client-side search filtering
if (search) {
@@ -543,6 +698,64 @@
currentPPRs = pprs; // Store for export
displayReports(pprs);
+
+ // Process other flights
+ let otherFlights = [];
+
+ if (arrivalsResponse.ok) {
+ const arrivals = await arrivalsResponse.json();
+ otherFlights.push(...arrivals.map(f => ({
+ ...f,
+ flightType: 'ARRIVAL',
+ aircraft_type: f.type,
+ timeField: f.eta || f.landed_dt,
+ fromField: f.in_from,
+ toField: 'EGFH'
+ })));
+ }
+
+ if (departuresResponse.ok) {
+ const departures = await departuresResponse.json();
+ otherFlights.push(...departures.map(f => ({
+ ...f,
+ flightType: 'DEPARTURE',
+ aircraft_type: f.type,
+ timeField: f.etd || f.departed_dt,
+ fromField: 'EGFH',
+ toField: f.out_to
+ })));
+ }
+
+ if (localFlightsResponse.ok) {
+ const localFlights = await localFlightsResponse.json();
+ otherFlights.push(...localFlights.map(f => ({
+ ...f,
+ flightType: f.flight_type === 'CIRCUITS' ? 'CIRCUIT' : f.flight_type,
+ aircraft_type: f.type,
+ circuits: f.circuits,
+ timeField: f.departed_dt,
+ fromField: 'EGFH',
+ toField: 'EGFH'
+ })));
+ }
+
+ // Apply search filtering to other flights
+ if (search) {
+ const searchLower = search.toLowerCase();
+ otherFlights = otherFlights.filter(f =>
+ (f.registration && f.registration.toLowerCase().includes(searchLower)) ||
+ (f.callsign && f.callsign.toLowerCase().includes(searchLower)) ||
+ (f.fromField && f.fromField.toLowerCase().includes(searchLower)) ||
+ (f.toField && f.toField.toLowerCase().includes(searchLower))
+ );
+ }
+
+ currentOtherFlights = otherFlights;
+ displayOtherFlights(otherFlights);
+
+ // Calculate and display movements summary
+ calculateMovementsSummary(pprs, otherFlights);
+
} catch (error) {
console.error('Error loading reports:', error);
if (error.message !== 'Session expired. Please log in again.') {
@@ -551,6 +764,63 @@
}
document.getElementById('reports-loading').style.display = 'none';
+ document.getElementById('other-flights-loading').style.display = 'none';
+ }
+
+ // Calculate and display movements summary
+ function calculateMovementsSummary(pprs, otherFlights) {
+ let pprArrivals = 0; // PPR landings
+ let pprDepartures = 0; // PPR takeoffs
+ let localFlightsMovements = 0;
+ let circuitsMovements = 0;
+ let nonPprArrivals = 0;
+ let nonPprDepartures = 0;
+
+ // PPR movements:
+ // - LANDED = 1 arrival (landing)
+ // - DEPARTED = 1 departure + 1 arrival (because departure implies a prior landing)
+ pprs.forEach(ppr => {
+ if (ppr.status === 'LANDED') {
+ pprArrivals += 1;
+ } else if (ppr.status === 'DEPARTED') {
+ pprDepartures += 1;
+ pprArrivals += 1; // Each departure implies a landing happened
+ }
+ // CANCELED = 0 movements, not counted
+ });
+
+ // Other flights movements
+ otherFlights.forEach(flight => {
+ if (flight.flightType === 'ARRIVAL') {
+ nonPprArrivals += 1;
+ } else if (flight.flightType === 'DEPARTURE') {
+ nonPprDepartures += 1;
+ } else if (flight.flightType === 'LOCAL') {
+ // 2 movements (takeoff + landing) for the flight itself
+ localFlightsMovements += 2;
+ } else if (flight.flightType === 'CIRCUIT') {
+ // 2 movements (takeoff + landing) plus the circuit count
+ const circuits = flight.circuits || 0;
+ circuitsMovements += 2 + circuits;
+ }
+ });
+
+ const pprTotal = pprArrivals + pprDepartures;
+ const nonPprTotal = localFlightsMovements + circuitsMovements + nonPprArrivals + nonPprDepartures;
+ const grandTotal = pprTotal + nonPprTotal;
+
+ // Update the summary display
+ document.getElementById('ppr-arrivals').textContent = pprArrivals;
+ document.getElementById('ppr-departures').textContent = pprDepartures;
+ document.getElementById('ppr-total').textContent = pprTotal;
+
+ document.getElementById('local-flights-movements').textContent = localFlightsMovements;
+ document.getElementById('circuits-movements').textContent = circuitsMovements;
+ document.getElementById('non-ppr-arrivals').textContent = nonPprArrivals;
+ document.getElementById('non-ppr-departures').textContent = nonPprDepartures;
+ document.getElementById('non-ppr-total').textContent = nonPprTotal;
+
+ document.getElementById('grand-total-movements').textContent = grandTotal;
}
// Display reports in table
@@ -615,6 +885,63 @@
}
}
+ // Display other flights in table
+ function displayOtherFlights(flights) {
+ const tbody = document.getElementById('other-flights-table-body');
+ const tableInfo = document.getElementById('other-flights-info');
+
+ tableInfo.textContent = `${flights.length} flights found`;
+
+ if (flights.length === 0) {
+ document.getElementById('other-flights-no-data').style.display = 'block';
+ return;
+ }
+
+ // Sort by time field (ascending)
+ flights.sort((a, b) => {
+ const aTime = a.timeField;
+ const bTime = b.timeField;
+ if (!aTime) return 1;
+ if (!bTime) return -1;
+ return new Date(aTime) - new Date(bTime);
+ });
+
+ tbody.innerHTML = '';
+ document.getElementById('other-flights-table-content').style.display = 'block';
+
+ for (const flight of flights) {
+ const row = document.createElement('tr');
+
+ const typeLabel = flight.flightType;
+ const registration = flight.registration || '-';
+ const aircraftType = flight.aircraft_type || '-';
+ const callsign = flight.callsign || '-';
+ const from = flight.fromField || '-';
+ const to = flight.toField || '-';
+ const timeDisplay = flight.timeField ? formatDateTime(flight.timeField) : '-';
+ const actualDisplay = flight.flightType === 'ARRIVAL'
+ ? (flight.landed_dt ? formatDateTime(flight.landed_dt) : '-')
+ : (flight.departed_dt ? formatDateTime(flight.departed_dt) : '-');
+ const status = flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING');
+ const circuits = (flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits > 0 ? flight.circuits : '-') : '-';
+
+ row.innerHTML = `
+ ${status} |
+ ${typeLabel} |
+ ${registration} |
+ ${aircraftType} |
+ ${callsign} |
+ ${from} |
+ ${to} |
+ ${timeDisplay} |
+ ${actualDisplay} |
+ ${circuits} |
+ `;
+
+ tbody.appendChild(row);
+ }
+ }
+
function formatDateTime(dateStr) {
if (!dateStr) return '-';
let utcDateStr = dateStr;
@@ -683,6 +1010,35 @@
downloadCSV(headers, csvData, 'ppr_reports.csv');
}
+ function exportOtherFlightsToCSV() {
+ if (currentOtherFlights.length === 0) {
+ showNotification('No data to export', true);
+ return;
+ }
+
+ const headers = [
+ 'Flight Type', 'Aircraft Registration', 'Aircraft Type', 'Callsign', 'From', 'To',
+ 'ETA/ETD', 'Landed/Departed', 'Status', 'Circuits'
+ ];
+
+ const csvData = currentOtherFlights.map(flight => [
+ flight.flightType,
+ flight.registration || '',
+ flight.aircraft_type || '',
+ flight.callsign || '',
+ flight.fromField || '',
+ flight.toField || '',
+ flight.timeField ? formatDateTime(flight.timeField) : '',
+ flight.flightType === 'ARRIVAL'
+ ? (flight.landed_dt ? formatDateTime(flight.landed_dt) : '')
+ : (flight.departed_dt ? formatDateTime(flight.departed_dt) : ''),
+ flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING'),
+ (flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits || '') : ''
+ ]);
+
+ downloadCSV(headers, csvData, 'other_flights_reports.csv');
+ }
+
function downloadCSV(headers, data, filename) {
const csvContent = [
headers.join(','),