diff --git a/backend/alembic/versions/011_ppr_local_status.py b/backend/alembic/versions/011_ppr_local_status.py
new file mode 100644
index 0000000..7b85b3b
--- /dev/null
+++ b/backend/alembic/versions/011_ppr_local_status.py
@@ -0,0 +1,34 @@
+"""Add LOCAL status to PPR records
+
+Revision ID: 011_ppr_local_status
+Revises: 010_drone_request_agl_altitude
+Create Date: 2026-06-29 00:00:00.000000
+
+"""
+from alembic import op
+
+revision = '011_ppr_local_status'
+down_revision = '010_drone_request_agl_altitude'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.execute("ALTER TABLE submitted CHANGE COLUMN departed_dt takeoff_dt DATETIME NULL")
+ op.execute("ALTER TABLE submitted ADD COLUMN qsy_dt DATETIME NULL AFTER takeoff_dt")
+ op.execute(
+ "ALTER TABLE submitted MODIFY COLUMN status "
+ "ENUM('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') "
+ "NOT NULL DEFAULT 'NEW'"
+ )
+
+
+def downgrade() -> None:
+ op.execute("UPDATE submitted SET status = 'LANDED' WHERE status = 'LOCAL'")
+ op.execute("ALTER TABLE submitted DROP COLUMN qsy_dt")
+ op.execute("ALTER TABLE submitted CHANGE COLUMN takeoff_dt departed_dt DATETIME NULL")
+ op.execute(
+ "ALTER TABLE submitted MODIFY COLUMN status "
+ "ENUM('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED','ACTIVATED') "
+ "NOT NULL DEFAULT 'NEW'"
+ )
diff --git a/backend/app/api/endpoints/movements.py b/backend/app/api/endpoints/movements.py
index b98f193..b5dfd8c 100644
--- a/backend/app/api/endpoints/movements.py
+++ b/backend/app/api/endpoints/movements.py
@@ -564,7 +564,7 @@ async def bulk_log_movement(
else:
ppr.out_to = entry.to_location or ppr.out_to
ppr.pob_out = entry.pob or ppr.pob_out
- ppr.departed_dt = timestamp
+ ppr.takeoff_dt = timestamp
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED):
ppr.status = PPRStatus.DEPARTED
if entry.notes:
diff --git a/backend/app/api/endpoints/pprs.py b/backend/app/api/endpoints/pprs.py
index 1f10587..3c2bf87 100644
--- a/backend/app/api/endpoints/pprs.py
+++ b/backend/app/api/endpoints/pprs.py
@@ -222,13 +222,21 @@ async def update_ppr_status(
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
+ event_timestamp = None
+ if ppr.status == PPRStatus.LANDED and ppr.landed_dt:
+ event_timestamp = ppr.landed_dt.isoformat()
+ elif ppr.status == PPRStatus.LOCAL and ppr.takeoff_dt:
+ event_timestamp = ppr.takeoff_dt.isoformat()
+ elif ppr.status == PPRStatus.DEPARTED and ppr.qsy_dt:
+ event_timestamp = ppr.qsy_dt.isoformat()
+
await request.app.state.connection_manager.broadcast({
"type": "status_update",
"data": {
"id": ppr.id,
"ac_reg": ppr.ac_reg,
"status": ppr.status.value,
- "timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else (ppr.departed_dt.isoformat() if ppr.departed_dt else None)
+ "timestamp": event_timestamp
}
})
@@ -348,7 +356,7 @@ async def update_ppr_public(
detail="Invalid or expired token"
)
# Only allow editing if not already processed
- if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
+ if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.LOCAL, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be edited at this stage"
@@ -373,7 +381,7 @@ async def cancel_ppr_public(
detail="Invalid or expired token"
)
# Only allow canceling if not already processed
- if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
+ if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.LOCAL, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be cancelled at this stage"
diff --git a/backend/app/api/endpoints/public.py b/backend/app/api/endpoints/public.py
index 04fe39e..adc8ab9 100644
--- a/backend/app/api/endpoints/public.py
+++ b/backend/app/api/endpoints/public.py
@@ -145,7 +145,8 @@ async def get_public_departures(db: Session = Depends(get_db)):
'ac_type': departure.ac_type,
'out_to': departure.out_to,
'etd': departure.etd,
- 'departed_dt': departure.departed_dt,
+ 'takeoff_dt': departure.takeoff_dt,
+ 'qsy_dt': departure.qsy_dt,
'status': departure.status.value,
'isLocalFlight': False,
'isDeparture': False
diff --git a/backend/app/crud/crud_ppr.py b/backend/app/crud/crud_ppr.py
index c046848..d74e79c 100644
--- a/backend/app/crud/crud_ppr.py
+++ b/backend/app/crud/crud_ppr.py
@@ -58,6 +58,7 @@ class CRUDPPR:
PPRRecord.status == PPRStatus.NEW,
PPRRecord.status == PPRStatus.CONFIRMED,
PPRRecord.status == PPRStatus.LANDED,
+ PPRRecord.status == PPRStatus.LOCAL,
PPRRecord.status == PPRStatus.DEPARTED
)
)
@@ -71,6 +72,7 @@ class CRUDPPR:
func.date(PPRRecord.etd) == today,
or_(
PPRRecord.status == PPRStatus.LANDED,
+ PPRRecord.status == PPRStatus.LOCAL,
PPRRecord.status == PPRStatus.DEPARTED
)
)
@@ -151,8 +153,10 @@ class CRUDPPR:
current_time = timestamp if timestamp is not None else datetime.utcnow()
if status == PPRStatus.LANDED:
db_obj.landed_dt = current_time
+ elif status == PPRStatus.LOCAL:
+ db_obj.takeoff_dt = current_time
elif status == PPRStatus.DEPARTED:
- db_obj.departed_dt = current_time
+ db_obj.qsy_dt = current_time
db.add(db_obj)
db.commit()
diff --git a/backend/app/models/ppr.py b/backend/app/models/ppr.py
index a8ac46c..754378b 100644
--- a/backend/app/models/ppr.py
+++ b/backend/app/models/ppr.py
@@ -9,6 +9,7 @@ class PPRStatus(str, Enum):
CONFIRMED = "CONFIRMED"
CANCELED = "CANCELED"
LANDED = "LANDED"
+ LOCAL = "LOCAL"
DELETED = "DELETED"
DEPARTED = "DEPARTED"
ACTIVATED = "ACTIVATED"
@@ -40,7 +41,8 @@ class PPRRecord(Base):
phone = Column(String(16), nullable=True)
notes = Column(Text, nullable=True)
landed_dt = Column(DateTime, nullable=True)
- departed_dt = Column(DateTime, nullable=True)
+ takeoff_dt = Column(DateTime, nullable=True)
+ qsy_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
acknowledged_dt = Column(DateTime, nullable=True)
diff --git a/backend/app/schemas/ppr.py b/backend/app/schemas/ppr.py
index ec2bebf..6a327d3 100644
--- a/backend/app/schemas/ppr.py
+++ b/backend/app/schemas/ppr.py
@@ -9,6 +9,7 @@ class PPRStatus(str, Enum):
CONFIRMED = "CONFIRMED"
CANCELED = "CANCELED"
LANDED = "LANDED"
+ LOCAL = "LOCAL"
DELETED = "DELETED"
DEPARTED = "DEPARTED"
ACTIVATED = "ACTIVATED"
@@ -85,7 +86,8 @@ class PPRInDBBase(PPRBase):
id: int
status: PPRStatus
landed_dt: Optional[datetime] = None
- departed_dt: Optional[datetime] = None
+ takeoff_dt: Optional[datetime] = None
+ qsy_dt: Optional[datetime] = None
created_by: Optional[str] = None
submitted_dt: datetime
acknowledged_dt: Optional[datetime] = None
@@ -111,7 +113,8 @@ class PPRPublic(BaseModel):
out_to: Optional[str] = None
etd: Optional[datetime] = None
landed_dt: Optional[datetime] = None
- departed_dt: Optional[datetime] = None
+ takeoff_dt: Optional[datetime] = None
+ qsy_dt: Optional[datetime] = None
submitted_dt: datetime
class Config:
diff --git a/backend/schema_dump.sql b/backend/schema_dump.sql
index 1731595..e24251c 100644
--- a/backend/schema_dump.sql
+++ b/backend/schema_dump.sql
@@ -81,7 +81,7 @@ DROP TABLE IF EXISTS `submitted`;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `submitted` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
- `status` enum('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
+ `status` enum('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
`ac_reg` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`ac_type` varchar(32) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`ac_call` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
@@ -97,7 +97,8 @@ CREATE TABLE `submitted` (
`phone` varchar(16) DEFAULT NULL,
`notes` varchar(2000) DEFAULT NULL,
`landed_dt` datetime DEFAULT NULL,
- `departed_dt` datetime DEFAULT NULL,
+ `takeoff_dt` datetime DEFAULT NULL,
+ `qsy_dt` datetime DEFAULT NULL,
`created_by` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
`submitted_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `id` (`id`)
diff --git a/backend/tests/test_pprs_api.py b/backend/tests/test_pprs_api.py
index df8f313..a4f87d0 100644
--- a/backend/tests/test_pprs_api.py
+++ b/backend/tests/test_pprs_api.py
@@ -43,6 +43,34 @@ def test_authenticated_user_can_create_read_update_and_audit_ppr(auth_client, pp
assert any("Status changed from NEW to LANDED" in entry for entry in entries)
+def test_ppr_departure_lifecycle_goes_landed_local_departed(auth_client, ppr_payload):
+ created = auth_client.post("/api/v1/pprs/", json=ppr_payload).json()
+
+ landed_response = auth_client.patch(
+ f"/api/v1/pprs/{created['id']}/status",
+ json={"status": "LANDED", "timestamp": "2026-06-20T10:30:00"},
+ )
+ local_response = auth_client.patch(
+ f"/api/v1/pprs/{created['id']}/status",
+ json={"status": "LOCAL", "timestamp": "2026-06-20T12:55:00"},
+ )
+ departed_response = auth_client.patch(
+ f"/api/v1/pprs/{created['id']}/status",
+ json={"status": "DEPARTED", "timestamp": "2026-06-20T13:05:00"},
+ )
+
+ assert landed_response.status_code == 200
+ assert local_response.status_code == 200
+ assert local_response.json()["status"] == "LOCAL"
+ assert local_response.json()["landed_dt"] == "2026-06-20T10:30:00"
+ assert local_response.json()["takeoff_dt"] == "2026-06-20T12:55:00"
+ assert local_response.json()["qsy_dt"] is None
+ assert departed_response.status_code == 200
+ assert departed_response.json()["status"] == "DEPARTED"
+ assert departed_response.json()["takeoff_dt"] == "2026-06-20T12:55:00"
+ assert departed_response.json()["qsy_dt"] == "2026-06-20T13:05:00"
+
+
def test_ppr_list_supports_status_date_and_pagination_filters(auth_client, ppr_factory):
ppr_factory(
ac_reg="G-NEW1",
diff --git a/db-init/init_db.sql b/db-init/init_db.sql
index 45e8dc1..c25d2ad 100644
--- a/db-init/init_db.sql
+++ b/db-init/init_db.sql
@@ -22,7 +22,7 @@ CREATE TABLE users (
-- Main PPR submissions table with improvements
CREATE TABLE submitted (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
- status ENUM('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') NOT NULL DEFAULT 'NEW',
+ status ENUM('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') NOT NULL DEFAULT 'NEW',
ac_reg VARCHAR(16) NOT NULL,
ac_type VARCHAR(32) NOT NULL,
ac_call VARCHAR(16) DEFAULT NULL,
@@ -38,7 +38,8 @@ CREATE TABLE submitted (
phone VARCHAR(16) DEFAULT NULL,
notes TEXT DEFAULT NULL,
landed_dt DATETIME DEFAULT NULL,
- departed_dt DATETIME DEFAULT NULL,
+ takeoff_dt DATETIME DEFAULT NULL,
+ qsy_dt DATETIME DEFAULT NULL,
created_by VARCHAR(16) DEFAULT NULL,
submitted_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
acknowledged_dt DATETIME DEFAULT NULL,
diff --git a/web/admin.html b/web/admin.html
index f8257ad..9ea9134 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -49,11 +49,11 @@
-
+
@@ -84,7 +84,7 @@
-
No Local Flights
+ No Local Traffic
@@ -447,12 +447,11 @@
document.getElementById('departures-no-data').style.display = 'none';
try {
- // Load PPR departures and airport departures simultaneously
- const [pprResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
+ // Load PPR departures and airport departures that are still pending departure
+ const [pprResponse, depBookedOutResponse, depOutGroundResponse] = await Promise.all([
authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
- authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
- authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
+ authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000')
]);
if (!pprResponse.ok) {
@@ -462,10 +461,9 @@
const allPPRs = await pprResponse.json();
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
- const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
// Combine departures
- const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
+ const allDepartures = [...depBookedOut, ...depOutGround];
const today = new Date().toISOString().split('T')[0];
// Filter for PPR departures with ETD today and LANDED status only
@@ -478,7 +476,7 @@
return etdDate === today;
});
- // Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
+ // Add departures to other airports that are not yet airborne locally
const depDepartures = allDepartures.map(flight => ({
...flight,
isDeparture: true // Flag to distinguish from PPR
@@ -502,19 +500,41 @@
document.getElementById('local-flights-no-data').style.display = 'none';
try {
- const response = await authenticatedFetch('/api/v1/local-flights/?limit=1000');
+ const [localResponse, pprResponse, depResponse] = await Promise.all([
+ authenticatedFetch('/api/v1/local-flights/?limit=1000'),
+ authenticatedFetch('/api/v1/pprs/?status=LOCAL&limit=1000'),
+ authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
+ ]);
- if (!response.ok) {
+ if (!localResponse.ok || !pprResponse.ok || !depResponse.ok) {
throw new Error('Failed to fetch local flights');
}
const today = new Date().toISOString().split('T')[0];
- const localFlights = (await response.json()).filter(flight => {
+ const localFlights = (await localResponse.json()).filter(flight => {
if (!flight.created_dt || ['CANCELLED', 'LANDED'].includes(flight.status)) return false;
return flight.created_dt.split('T')[0] === today;
});
+ const pprLocalTraffic = (await pprResponse.json())
+ .filter(ppr => {
+ const dateFields = [ppr.etd, ppr.landed_dt, ppr.submitted_dt];
+ return dateFields.some(value => value && value.split('T')[0] === today);
+ })
+ .map(ppr => ({
+ ...ppr,
+ isPPRLocalTraffic: true
+ }));
+ const departureLocalTraffic = (await depResponse.json())
+ .filter(departure => {
+ const dateFields = [departure.created_dt, departure.etd, departure.takeoff_dt, departure.departed_dt];
+ return dateFields.some(value => value && value.split('T')[0] === today);
+ })
+ .map(departure => ({
+ ...departure,
+ isDepartureLocalTraffic: true
+ }));
- displayLocalFlights(localFlights);
+ displayLocalFlights([...localFlights, ...pprLocalTraffic, ...departureLocalTraffic]);
} catch (error) {
console.error('Error loading local flights:', error);
if (error.message !== 'Session expired. Please log in again.') {
@@ -610,7 +630,7 @@
}
- // Load departed aircraft (DEPARTED status with departed_dt today)
+ // Load departed aircraft (DEPARTED status with QSY/departed time today)
async function loadDeparted() {
document.getElementById('departed-loading').style.display = 'block';
document.getElementById('departed-table-content').style.display = 'none';
@@ -631,10 +651,10 @@
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
const departed = allPPRs.filter(ppr => {
- if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
+ if (!ppr.qsy_dt || ppr.status !== 'DEPARTED') {
return false;
}
- const departedDate = ppr.departed_dt.split('T')[0];
+ const departedDate = ppr.qsy_dt.split('T')[0];
return departedDate === today;
});
@@ -675,8 +695,8 @@
// Sort by departed time
departed.sort((a, b) => {
- const aTime = a.departed_dt;
- const bTime = b.departed_dt;
+ const aTime = a.isDeparture ? a.departed_dt : a.qsy_dt;
+ const bTime = b.isDeparture ? b.departed_dt : b.qsy_dt;
return parseUtcDate(aTime) - parseUtcDate(bTime);
});
@@ -721,7 +741,7 @@
P |
${flight.ac_call || '-'} |
${flight.out_to || '-'} |
- ${formatTimeOnly(flight.departed_dt)} |
+ ${formatTimeOnly(flight.qsy_dt)} |
`;
}
tbody.appendChild(row);
@@ -1200,24 +1220,52 @@
tbody.innerHTML = '';
document.getElementById('local-flights-table-content').style.display = 'block';
- const circuitCounts = await loadLocalFlightCircuitCounts(localFlights);
+ const circuitCounts = await loadLocalFlightCircuitCounts(localFlights.filter(flight => !flight.isPPRLocalTraffic && !flight.isDepartureLocalTraffic));
for (const flight of localFlights) {
const row = document.createElement('tr');
- row.onclick = () => openLocalFlightEditModal(flight.id);
+ const isPPR = flight.isPPRLocalTraffic;
+ const isDeparture = flight.isDepartureLocalTraffic;
+ row.onclick = () => isPPR ? openPPRModal(flight.id) : (isDeparture ? openDepartureEditModal(flight.id) : openLocalFlightEditModal(flight.id));
- const aircraftDisplay = flight.callsign && flight.callsign.trim()
- ? `${flight.callsign}
${flight.registration}`
- : `${flight.registration}`;
- const typeIcon = flight.submitted_via === 'PUBLIC'
+ const aircraftDisplay = isPPR
+ ? (flight.ac_call && flight.ac_call.trim()
+ ? `${flight.ac_call}
${flight.ac_reg}`
+ : `${flight.ac_reg}`)
+ : isDeparture
+ ? (flight.callsign && flight.callsign.trim()
+ ? `${flight.callsign}
${flight.registration}`
+ : `${flight.registration}`)
+ : (flight.callsign && flight.callsign.trim()
+ ? `${flight.callsign}
${flight.registration}`
+ : `${flight.registration}`);
+ const typeIcon = isPPR
+ ? 'P'
+ : isDeparture
+ ? (flight.submitted_via === 'PUBLIC'
+ ? 'O'
+ : 'D')
+ : flight.submitted_via === 'PUBLIC'
? 'O'
: 'L';
- const flightType = flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
+ const flightType = isPPR ? (flight.out_to ? `To ${flight.out_to}` : 'PPR Departure') : isDeparture ? (flight.out_to ? `To ${flight.out_to}` : 'Departure') : flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
const etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
- const circuits = circuitCounts[flight.id] ?? flight.circuits ?? 0;
+ const circuits = (isPPR || isDeparture) ? '-' : circuitCounts[flight.id] ?? flight.circuits ?? 0;
let actionButtons = '';
- if (flight.status === 'BOOKED_OUT') {
+ if (isPPR) {
+ actionButtons = `
+
+ `;
+ } else if (isDeparture) {
+ actionButtons = `
+
+ `;
+ } else if (flight.status === 'BOOKED_OUT') {
actionButtons = `
`;
actionButtons = `
-
+
LOCAL
${circuitButton}
@@ -1216,12 +1212,13 @@
`;
} else if (flight.status === 'GROUND') {
+ const takeoffStatus = flight.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
actionButtons = `
-
+
TAKE OFF
`;
- } else if (flight.status === 'DEPARTED') {
+ } else if (['DEPARTED', 'LOCAL', 'CIRCUIT', 'CIRCUIT_DOWNWIND', 'CIRCUIT_BASE', 'CIRCUIT_FINAL'].includes(flight.status)) {
// Allow touch and go for all local flight types
let circuitButton = `
T&G
@@ -1293,11 +1290,21 @@
fuel = flight.fuel || '-';
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
- actionButtons = `
-
- TAKE OFF
-
- `;
+ if (flight.status === 'LANDED') {
+ actionButtons = `
+
+ TAKE OFF
+
+ `;
+ } else if (flight.status === 'LOCAL') {
+ actionButtons = `
+
+ QSY
+
+ `;
+ } else {
+ actionButtons = '-';
+ }
}
row.innerHTML = `
@@ -1412,19 +1419,26 @@
// Load departing aircraft (ready to take off)
async function loadDepartingAircraft() {
try {
- const [groundDeparturesResponse, groundLocalResponse] = await Promise.all([
+ const [pprResponse, groundDeparturesResponse, groundLocalResponse] = await Promise.all([
+ authenticatedFetch('/api/v1/pprs/?limit=1000'),
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000')
]);
let groundAircraft = [];
- if (groundDeparturesResponse.ok) groundAircraft = await groundDeparturesResponse.json();
+ if (pprResponse.ok) {
+ const today = getLocalDateString();
+ groundAircraft = (await pprResponse.json())
+ .filter(ppr => ppr.status === 'LANDED' && ppr.etd && ppr.etd.split('T')[0] === today)
+ .map(ppr => ({ ...ppr, isPPR: true }));
+ }
+ if (groundDeparturesResponse.ok) groundAircraft = groundAircraft.concat(await groundDeparturesResponse.json());
if (groundLocalResponse.ok) groundAircraft = groundAircraft.concat((await groundLocalResponse.json()).map(l => ({ ...l, isLocalFlight: true })));
groundAircraft = groundAircraft.filter(ac => isTodayRecord(ac, ['created_dt', 'etd']));
displayDepartingAircraft(groundAircraft.map(ac => ({
...ac,
- isDeparture: !ac.isLocalFlight
+ isDeparture: !ac.isLocalFlight && !ac.isPPR
})));
} catch (error) {
console.error('Error loading departing aircraft:', error);
@@ -1447,6 +1461,7 @@
const type = ac.ac_type || ac.type;
const dest = ac.out_to;
const isLocal = ac.isLocalFlight;
+ const isPPR = ac.isPPR;
// All aircraft in awaiting departure are in GROUND status
let takeoffOnclick, buttonText, buttonTitle, clickType;
@@ -1457,13 +1472,18 @@
buttonText = 'TAKE OFF';
buttonTitle = takeoffTitle;
clickType = 'local';
+ } else if (isPPR) {
+ takeoffOnclick = `event.stopPropagation(); showTimestampModal('LOCAL', ${ac.id})`;
+ buttonText = 'TAKE OFF';
+ buttonTitle = 'Mark as Local';
+ clickType = 'ppr';
} else {
takeoffOnclick = `event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('LOCAL', ${ac.id}, false, true)`;
buttonText = 'TAKE OFF';
buttonTitle = 'Mark as Local';
clickType = 'departure';
}
- const itemClass = isLocal ? 'local-flight' : 'departure';
+ const itemClass = isLocal ? 'local-flight' : (isPPR ? 'departure' : 'departure');
return `
@@ -1481,6 +1501,7 @@
async function loadLocalAircraft() {
try {
const response = await Promise.all([
+ authenticatedFetch('/api/v1/pprs/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'),
authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'),
@@ -1488,12 +1509,14 @@
]);
let locals = [];
- if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true }));
- if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true })));
- if (response[2].ok) locals = locals.concat((await response[2].json()).map(a => ({ ...a, isArrival: true })));
- if (response[3].ok) locals = locals.concat((await response[3].json()).map(o => ({ ...o, isOverflight: true })));
+ if (response[0].ok) locals = (await response[0].json()).map(p => ({ ...p, isPPR: true }));
+ if (response[1].ok) locals = locals.concat((await response[1].json()).map(l => ({ ...l, isLocalFlight: true })));
+ if (response[2].ok) locals = locals.concat((await response[2].json()).map(d => ({ ...d, isDeparture: true })));
+ if (response[3].ok) locals = locals.concat((await response[3].json()).map(a => ({ ...a, isArrival: true })));
+ if (response[4].ok) locals = locals.concat((await response[4].json()).map(o => ({ ...o, isOverflight: true })));
locals = locals.filter(ac => {
if (ac.isOverflight) return true;
+ if (ac.isPPR) return isTodayRecord(ac, ['etd', 'landed_dt', 'submitted_dt']);
if (ac.isLocalFlight) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
if (ac.isArrival) return isTodayRecord(ac, ['created_dt', 'eta']);
if (ac.isDeparture) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
@@ -1513,7 +1536,7 @@
countEl.textContent = aircraft.length;
if (aircraft.length === 0) {
- container.innerHTML = '
No aircraft in local area
';
+ container.innerHTML = '
No local traffic
';
return;
}
@@ -1522,9 +1545,12 @@
const type = ac.type || ac.ac_type || ac.aircraft_type || '';
const dest = ac.out_to;
const isDeparture = ac.isDeparture;
+ const isPPR = ac.isPPR;
let buttons;
- if (isDeparture) {
+ if (isPPR) {
+ buttons = `
QSY`;
+ } else if (isDeparture) {
// Departure in LOCAL status - show QSY and REJOIN buttons
buttons = `
QSY
@@ -1537,9 +1563,9 @@
// Overflight in ACTIVE status - show QSY button
buttons = `
QSY`;
}
- const itemClass = isDeparture ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
- const detailsText = isDeparture ? `${type}${dest ? ` â ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} â ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` â ${dest}` : ` Local Flight`}`));
- const entityType = isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local'));
+ const itemClass = isDeparture || isPPR ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
+ const detailsText = isDeparture || isPPR ? `${type}${dest ? ` â ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} â ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` â ${dest}` : ` Local Flight`}`));
+ const entityType = isPPR ? 'ppr' : (isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local')));
return `
diff --git a/web/edit.html b/web/edit.html
index 40445fe..306c378 100644
--- a/web/edit.html
+++ b/web/edit.html
@@ -472,7 +472,7 @@
document.getElementById('phone').value = ppr.phone || '';
document.getElementById('notes').value = ppr.notes || '';
- if (['CANCELED', 'DELETED', 'LANDED', 'DEPARTED'].includes(ppr.status)) {
+ if (['CANCELED', 'DELETED', 'LANDED', 'LOCAL', 'DEPARTED'].includes(ppr.status)) {
document.getElementById('update-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
showNotification('This PPR can no longer be edited or cancelled online.', true);
diff --git a/web/index.html b/web/index.html
index 25063d9..b4f5f2a 100644
--- a/web/index.html
+++ b/web/index.html
@@ -829,10 +829,10 @@
const toDisplay = await getAirportName(departure.out_to || '');
let timeDisplay, sortTime;
- if (departure.status === 'DEPARTED' && departure.departed_dt) {
- const time = convertToLocalTime(departure.departed_dt);
+ if (departure.status === 'DEPARTED' && departure.qsy_dt) {
+ const time = convertToLocalTime(departure.qsy_dt);
timeDisplay = `
${time}DEPARTED
`;
- sortTime = departure.departed_dt;
+ sortTime = departure.qsy_dt;
} else {
timeDisplay = convertToLocalTime(departure.etd);
sortTime = departure.etd;
diff --git a/web/reports.html b/web/reports.html
index 73795c2..cce0f53 100644
--- a/web/reports.html
+++ b/web/reports.html
@@ -1181,7 +1181,7 @@
row.className = 'clickable-row';
row.onclick = () => openReportDetail('PPR', ppr.id);
- const takeoff = ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '-';
+ const takeoff = ppr.takeoff_dt ? formatDateTime(ppr.takeoff_dt) : '-';
const landing = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-';
@@ -1381,7 +1381,7 @@
}
function getPPRSortTime(ppr) {
- return ppr.landed_dt || ppr.departed_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
+ return ppr.landed_dt || ppr.qsy_dt || ppr.takeoff_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
}
const detailConfig = {
@@ -1397,7 +1397,8 @@
['Captain', r => r.captain],
['From', r => r.in_from],
['To', r => r.out_to],
- ['Takeoff', r => formatOptionalDateTime(r.departed_dt)],
+ ['Takeoff', r => formatOptionalDateTime(r.takeoff_dt)],
+ ['QSY', r => formatOptionalDateTime(r.qsy_dt)],
['Landing', r => formatOptionalDateTime(r.landed_dt)],
['ETA', r => formatOptionalDateTime(r.eta)],
['ETD', r => formatOptionalDateTime(r.etd)],
@@ -1571,7 +1572,7 @@
const headers = [
'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain',
- 'From', 'To', 'Takeoff', 'Landing', 'POB In', 'POB Out', 'Fuel',
+ 'From', 'To', 'Takeoff', 'QSY', 'Landing', 'POB In', 'POB Out', 'Fuel',
'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
];
@@ -1584,7 +1585,8 @@
ppr.captain,
ppr.in_from,
ppr.out_to || '',
- ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '',
+ ppr.takeoff_dt ? formatDateTime(ppr.takeoff_dt) : '',
+ ppr.qsy_dt ? formatDateTime(ppr.qsy_dt) : '',
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
ppr.pob_in,
ppr.pob_out || '',
diff --git a/web/shared.js b/web/shared.js
index fce83eb..eddce87 100644
--- a/web/shared.js
+++ b/web/shared.js
@@ -777,6 +777,9 @@
const ppr = await response.json();
populateForm(ppr);
+ const departedBtn = document.getElementById('btn-departed');
+ departedBtn.textContent = 'đĢ Depart';
+ departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
// Show/hide quick action buttons based on current status
if (ppr.status === 'NEW') {
@@ -789,8 +792,16 @@
document.getElementById('btn-cancel').style.display = 'inline-block';
} else if (ppr.status === 'LANDED') {
document.getElementById('btn-landed').style.display = 'none';
- document.getElementById('btn-departed').style.display = 'inline-block';
+ departedBtn.style.display = 'inline-block';
+ departedBtn.textContent = 'đĢ Take Off';
+ departedBtn.setAttribute('onclick', "showTimestampModal('LOCAL')");
document.getElementById('btn-cancel').style.display = 'inline-block';
+ } else if (ppr.status === 'LOCAL') {
+ document.getElementById('btn-landed').style.display = 'none';
+ departedBtn.style.display = 'inline-block';
+ departedBtn.textContent = 'QSY';
+ departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
+ document.getElementById('btn-cancel').style.display = 'none';
} else {
// DEPARTED, CANCELED, DELETED - hide all quick actions and cancel button
document.querySelector('.quick-actions').style.display = 'none';
@@ -2657,8 +2668,8 @@
text: "Displays visiting aircraft and airport departures that are ready to leave Swansea today. Local flights are shown in their own table."
},
"local-flights": {
- title: "Today's Local Flights",
- text: "Displays local and circuit flights booked out today, with shortcuts for contact, takeoff, circuit work, touch-and-go, and landing."
+ title: "Local Traffic",
+ text: "Displays local traffic booked out today, including local flights, circuits, and PPR departures that are airborne locally before QSY."
},
overflights: {
title: "Active Overflights",