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
+
+
+
PPR Total
+
0
+
+
+
+ + +
+
Non-PPR Movements
+
+
+
Local Flights
+
0
+
+
+
Circuits
+
0
+
+
+
Arrivals
+
0
+
+
+
Departures
+
0
+
+
+
Non-PPR Total
+
0
+
+
+
+ + +
+
Grand Total Movements
+
0
+
+
+
+
@@ -422,6 +519,54 @@

No records match your current filters.

+ + +
+
+
+ Other Flights +
Loading...
+
+
+ +
+
+ +
+
+ Loading flights... +
+ + + + +
@@ -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(','),