Reporting and TZ updates

This commit is contained in:
2026-06-28 07:37:41 -04:00
parent 5e12561fb2
commit c2e4d2adeb
18 changed files with 719 additions and 268 deletions
+18 -10
View File
@@ -1,7 +1,8 @@
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from datetime import date from datetime import date, timezone
from zoneinfo import ZoneInfo
from app.api.deps import get_db, get_current_read_user, get_current_operator_user from app.api.deps import get_db, get_current_read_user, get_current_operator_user
from app.crud.crud_ppr import ppr as crud_ppr from app.crud.crud_ppr import ppr as crud_ppr
from app.crud.crud_journal import journal as crud_journal from app.crud.crud_journal import journal as crud_journal
@@ -19,6 +20,14 @@ from app.core.config import settings
router = APIRouter() router = APIRouter()
def format_local_datetime(dt):
if not dt:
return "N/A"
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(ZoneInfo(settings.local_timezone)).strftime("%Y-%m-%d %H:%M")
@router.get("/", response_model=List[PPR]) @router.get("/", response_model=List[PPR])
async def get_pprs( async def get_pprs(
request: Request, request: Request,
@@ -94,8 +103,8 @@ async def create_public_ppr(
template_vars={ template_vars={
"name": ppr_in.captain, "name": ppr_in.captain,
"aircraft": ppr_in.ac_reg, "aircraft": ppr_in.ac_reg,
"arrival_time": ppr_in.eta.strftime("%Y-%m-%d %H:%M"), "arrival_time": format_local_datetime(ppr_in.eta),
"departure_time": ppr_in.etd.strftime("%Y-%m-%d %H:%M") if ppr_in.etd else "N/A", "departure_time": format_local_datetime(ppr_in.etd),
"purpose": ppr_in.notes or "N/A", "purpose": ppr_in.notes or "N/A",
"public_token": ppr.public_token, "public_token": ppr.public_token,
"base_url": settings.base_url "base_url": settings.base_url
@@ -232,8 +241,8 @@ async def update_ppr_status(
template_vars={ template_vars={
"name": ppr.captain, "name": ppr.captain,
"aircraft": ppr.ac_reg, "aircraft": ppr.ac_reg,
"arrival_time": ppr.eta.strftime("%Y-%m-%d %H:%M"), "arrival_time": format_local_datetime(ppr.eta),
"departure_time": ppr.etd.strftime("%Y-%m-%d %H:%M") if ppr.etd else "N/A" "departure_time": format_local_datetime(ppr.etd)
} }
) )
@@ -316,11 +325,10 @@ async def get_ppr_for_edit(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid or expired token" detail="Invalid or expired token"
) )
# Only allow editing if not already processed if ppr.status == PPRStatus.DELETED:
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be edited at this stage" detail="PPR is no longer available"
) )
return ppr return ppr
@@ -390,8 +398,8 @@ async def cancel_ppr_public(
template_vars={ template_vars={
"name": cancelled_ppr.captain, "name": cancelled_ppr.captain,
"aircraft": cancelled_ppr.ac_reg, "aircraft": cancelled_ppr.ac_reg,
"arrival_time": cancelled_ppr.eta.strftime("%Y-%m-%d %H:%M"), "arrival_time": format_local_datetime(cancelled_ppr.eta),
"departure_time": cancelled_ppr.etd.strftime("%Y-%m-%d %H:%M") if cancelled_ppr.etd else "N/A" "departure_time": format_local_datetime(cancelled_ppr.etd)
} }
) )
+1
View File
@@ -27,6 +27,7 @@ class Settings(BaseSettings):
api_v1_str: str = "/api/v1" api_v1_str: str = "/api/v1"
project_name: str = "Airfield PPR API" project_name: str = "Airfield PPR API"
base_url: str base_url: str
local_timezone: str = "Europe/London"
# UI Configuration # UI Configuration
tag: str = "" tag: str = ""
+3 -3
View File
@@ -10,10 +10,10 @@
<p><strong>PPR Details:</strong></p> <p><strong>PPR Details:</strong></p>
<ul> <ul>
<li>Aircraft: {{ aircraft }}</li> <li>Aircraft: {{ aircraft }}</li>
<li>Original Arrival: {{ arrival_time }}</li> <li>Original Arrival (local time): {{ arrival_time }}</li>
<li>Original Departure: {{ departure_time }}</li> <li>Original Departure (local time): {{ departure_time }}</li>
</ul> </ul>
<p>If this was not intended, please contact us.</p> <p>If this was not intended, please contact us.</p>
<p>Best regards,<br>Swansea Airport Team</p> <p>Best regards,<br>Swansea Airport Team</p>
</body> </body>
</html> </html>
+3 -4
View File
@@ -10,12 +10,11 @@
<p><strong>PPR Details:</strong></p> <p><strong>PPR Details:</strong></p>
<ul> <ul>
<li>Aircraft: {{ aircraft }}</li> <li>Aircraft: {{ aircraft }}</li>
<li>Arrival: {{ arrival_time }}</li> <li>Arrival (local time): {{ arrival_time }}</li>
<li>Departure: {{ departure_time }}</li> <li>Departure (local time): {{ departure_time }}</li>
<li>Purpose: {{ purpose }}</li>
</ul> </ul>
<p>You can <a href="{{ base_url }}/edit.html?token={{ public_token }}">edit or cancel</a> your PPR using this secure link.</p> <p>You can <a href="{{ base_url }}/edit.html?token={{ public_token }}">edit or cancel</a> your PPR using this secure link.</p>
<p>You will receive further updates via email.</p> <p>You will receive further updates via email.</p>
<p>Best regards,<br>Swansea Airport Team</p> <p>Best regards,<br>Swansea Airport Team</p>
</body> </body>
</html> </html>
+21 -1
View File
@@ -124,6 +124,8 @@ def test_public_ppr_create_sends_email_and_generates_token(client, db, ppr_paylo
created = create_response.json() created = create_response.json()
assert created["created_by"] == "public" assert created["created_by"] == "public"
assert sent_email["to_email"] == "pilot@example.com" assert sent_email["to_email"] == "pilot@example.com"
assert sent_email["template_vars"]["arrival_time"] == "2026-06-20 11:00"
assert sent_email["template_vars"]["departure_time"] == "2026-06-20 13:00"
db_ppr = db.query(PPRRecord).filter(PPRRecord.id == created["id"]).one() db_ppr = db.query(PPRRecord).filter(PPRRecord.id == created["id"]).one()
assert db_ppr.public_token assert db_ppr.public_token
@@ -146,7 +148,9 @@ def test_public_ppr_token_edit_and_cancel_paths(client, ppr_factory, db):
assert cancel_response.status_code == 200 assert cancel_response.status_code == 200
assert cancel_response.json()["status"] == "CANCELED" assert cancel_response.json()["status"] == "CANCELED"
assert client.get("/api/v1/pprs/public/edit/public-edit-token").status_code == 400 assert client.get("/api/v1/pprs/public/edit/public-edit-token").status_code == 200
assert client.patch("/api/v1/pprs/public/edit/public-edit-token", json={}).status_code == 400
assert client.delete("/api/v1/pprs/public/cancel/public-edit-token").status_code == 400
assert client.patch("/api/v1/pprs/public/edit/missing-token", json={}).status_code == 404 assert client.patch("/api/v1/pprs/public/edit/missing-token", json={}).status_code == 404
assert client.delete("/api/v1/pprs/public/cancel/missing-token").status_code == 404 assert client.delete("/api/v1/pprs/public/cancel/missing-token").status_code == 404
@@ -179,6 +183,22 @@ def test_activate_rejects_processed_ppr(auth_client, ppr_factory):
assert "cannot be activated" in response.json()["detail"] assert "cannot be activated" in response.json()["detail"]
def test_public_ppr_processed_token_can_view_but_not_edit_or_cancel(client, ppr_factory):
ppr = ppr_factory(status="LANDED", public_token="processed-token")
get_response = client.get("/api/v1/pprs/public/edit/processed-token")
patch_response = client.patch(
"/api/v1/pprs/public/edit/processed-token",
json={"captain": "Too Late"},
)
cancel_response = client.delete("/api/v1/pprs/public/cancel/processed-token")
assert get_response.status_code == 200
assert get_response.json()["id"] == ppr.id
assert patch_response.status_code == 400
assert cancel_response.status_code == 400
def test_invalid_ppr_payload_returns_validation_error(auth_client, ppr_payload): def test_invalid_ppr_payload_returns_validation_error(auth_client, ppr_payload):
ppr_payload["pob_in"] = -1 ppr_payload["pob_in"] = -1
+82
View File
@@ -0,0 +1,82 @@
# Production docker-compose configuration
# This uses an external database and optimized settings
services:
# FastAPI Backend
api:
build: ./backend
restart: always
environment:
DB_HOST: ${DB_HOST}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
DB_PORT: ${DB_PORT}
SECRET_KEY: ${SECRET_KEY}
ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES}
API_V1_STR: ${API_V1_STR}
PROJECT_NAME: ${PROJECT_NAME}
MAIL_HOST: ${MAIL_HOST}
MAIL_PORT: ${MAIL_PORT}
MAIL_USERNAME: ${MAIL_USERNAME}
MAIL_PASSWORD: ${MAIL_PASSWORD}
MAIL_FROM: ${MAIL_FROM}
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
BASE_URL: ${BASE_URL}
REDIS_URL: ${REDIS_URL}
TAG: ${TAG}
TOP_BAR_BASE_COLOR: ${TOP_BAR_BASE_COLOR}
ENVIRONMENT: production
DRONE_REQUEST_TOWER_EMAIL: ${DRONE_REQUEST_TOWER_EMAIL:-}
ports:
- "${API_PORT_EXTERNAL}:8000"
volumes:
- ./backend:/app
- ./db-init:/db-init:ro # Mount CSV data for seeding
- ./web/assets:/web/assets # Mount assets for QR code generation
extra_hosts:
- "host.docker.internal:host-gateway"
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Nginx web server for public frontend
web:
image: nginx:alpine
restart: always
environment:
BASE_URL: ${BASE_URL}
command: >
sh -c "echo 'window.PPR_CONFIG = { apiBase: \"'\$BASE_URL'/api/v1\" };' > /usr/share/nginx/html/config.js &&
nginx -g 'daemon off;'"
ports:
- "${WEB_PORT_EXTERNAL}:80"
volumes:
- ./web:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
networks:
- default
- webapps
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
networks:
default:
webapps:
external: true
+11 -16
View File
@@ -517,7 +517,7 @@
} }
// Sort by call_dt most recent // Sort by call_dt most recent
overflights.sort((a, b) => new Date(b.call_dt) - new Date(a.call_dt)); overflights.sort((a, b) => parseUtcDate(b.call_dt) - parseUtcDate(a.call_dt));
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -633,7 +633,7 @@
departed.sort((a, b) => { departed.sort((a, b) => {
const aTime = a.departed_dt; const aTime = a.departed_dt;
const bTime = b.departed_dt; const bTime = b.departed_dt;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -738,7 +738,7 @@
parked.sort((a, b) => { parked.sort((a, b) => {
if (!a.landed_dt) return 1; if (!a.landed_dt) return 1;
if (!b.landed_dt) return -1; if (!b.landed_dt) return -1;
return new Date(a.landed_dt) - new Date(b.landed_dt); return parseUtcDate(a.landed_dt) - parseUtcDate(b.landed_dt);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -771,16 +771,14 @@
arrivedDisplay = formatTimeOnly(ppr.landed_dt); arrivedDisplay = formatTimeOnly(ppr.landed_dt);
} else { } else {
// Not today - show date (DD/MM) // Not today - show date (DD/MM)
const date = new Date(ppr.landed_dt); arrivedDisplay = formatUtcDayMonth(ppr.landed_dt);
arrivedDisplay = date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
} }
} }
// Format ETD as just the date (DD/MM) // Format ETD as just the date (DD/MM)
let etdDisplay = '-'; let etdDisplay = '-';
if (ppr.etd) { if (ppr.etd) {
const etdDate = new Date(ppr.etd); etdDisplay = formatUtcDayMonth(ppr.etd);
etdDisplay = etdDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
} }
row.innerHTML = ` row.innerHTML = `
@@ -842,7 +840,7 @@
} }
// Sort by ETA date and time // Sort by ETA date and time
upcoming.sort((a, b) => new Date(a.eta) - new Date(b.eta)); upcoming.sort((a, b) => parseUtcDate(a.eta) - parseUtcDate(b.eta));
tbody.innerHTML = ''; tbody.innerHTML = '';
// Don't auto-expand, keep collapsed by default // Don't auto-expand, keep collapsed by default
@@ -856,10 +854,7 @@
} }
// Format date as Day DD/MM (e.g., Wed 11/12) // Format date as Day DD/MM (e.g., Wed 11/12)
const etaDate = new Date(ppr.eta); const dateDisplay = formatUtcWeekdayDayMonth(ppr.eta);
const dayName = etaDate.toLocaleDateString('en-GB', { weekday: 'short' });
const dateStr = etaDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
const dateDisplay = `${dayName} ${dateStr}`;
// Create notes indicator if notes exist // Create notes indicator if notes exist
const notesIndicator = ppr.notes && ppr.notes.trim() ? const notesIndicator = ppr.notes && ppr.notes.trim() ?
@@ -945,7 +940,7 @@
const bTime = b.eta || b.departure_dt; const bTime = b.eta || b.departure_dt;
if (!aTime) return 1; if (!aTime) return 1;
if (!bTime) return -1; if (!bTime) return -1;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
document.getElementById('arrivals-table-content').style.display = 'block'; document.getElementById('arrivals-table-content').style.display = 'block';
@@ -993,7 +988,7 @@
let departureTime = flight.departed_dt || flight.etd; let departureTime = flight.departed_dt || flight.etd;
let etaTime = departureTime; let etaTime = departureTime;
if (departureTime && flight.duration) { if (departureTime && flight.duration) {
const departTime = new Date(departureTime); const departTime = parseUtcDate(departureTime);
etaTime = new Date(departTime.getTime() + flight.duration * 60000).toISOString(); // duration is in minutes etaTime = new Date(departTime.getTime() + flight.duration * 60000).toISOString(); // duration is in minutes
} }
eta = etaTime ? formatTimeOnly(etaTime) : '-'; eta = etaTime ? formatTimeOnly(etaTime) : '-';
@@ -1155,7 +1150,7 @@
const bTime = b.etd || b.created_dt; const bTime = b.etd || b.created_dt;
if (!aTime) return 1; if (!aTime) return 1;
if (!bTime) return -1; if (!bTime) return -1;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -1273,7 +1268,7 @@
const bTime = b.etd || b.created_dt; const bTime = b.etd || b.created_dt;
if (!aTime) return 1; if (!aTime) return 1;
if (!bTime) return -1; if (!bTime) return -1;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
document.getElementById('departures-table-content').style.display = 'block'; document.getElementById('departures-table-content').style.display = 'block';
+11 -19
View File
@@ -508,7 +508,7 @@
} }
// Sort by call_dt most recent // Sort by call_dt most recent
overflights.sort((a, b) => new Date(b.call_dt) - new Date(a.call_dt)); overflights.sort((a, b) => parseUtcDate(b.call_dt) - parseUtcDate(a.call_dt));
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -625,7 +625,7 @@
departed.sort((a, b) => { departed.sort((a, b) => {
const aTime = a.departed_dt; const aTime = a.departed_dt;
const bTime = b.departed_dt; const bTime = b.departed_dt;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -730,7 +730,7 @@
parked.sort((a, b) => { parked.sort((a, b) => {
if (!a.landed_dt) return 1; if (!a.landed_dt) return 1;
if (!b.landed_dt) return -1; if (!b.landed_dt) return -1;
return new Date(a.landed_dt) - new Date(b.landed_dt); return parseUtcDate(a.landed_dt) - parseUtcDate(b.landed_dt);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -763,16 +763,14 @@
arrivedDisplay = formatTimeOnly(ppr.landed_dt); arrivedDisplay = formatTimeOnly(ppr.landed_dt);
} else { } else {
// Not today - show date (DD/MM) // Not today - show date (DD/MM)
const date = new Date(ppr.landed_dt); arrivedDisplay = formatUtcDayMonth(ppr.landed_dt);
arrivedDisplay = date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
} }
} }
// Format ETD as just the date (DD/MM) // Format ETD as just the date (DD/MM)
let etdDisplay = '-'; let etdDisplay = '-';
if (ppr.etd) { if (ppr.etd) {
const etdDate = new Date(ppr.etd); etdDisplay = formatUtcDayMonth(ppr.etd);
etdDisplay = etdDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
} }
row.innerHTML = ` row.innerHTML = `
@@ -834,7 +832,7 @@
} }
// Sort by ETA date and time // Sort by ETA date and time
upcoming.sort((a, b) => new Date(a.eta) - new Date(b.eta)); upcoming.sort((a, b) => parseUtcDate(a.eta) - parseUtcDate(b.eta));
tbody.innerHTML = ''; tbody.innerHTML = '';
// Don't auto-expand, keep collapsed by default // Don't auto-expand, keep collapsed by default
@@ -845,10 +843,7 @@
row.style.cssText = 'font-size: 0.85rem !important;'; row.style.cssText = 'font-size: 0.85rem !important;';
// Format date as Day DD/MM (e.g., Wed 11/12) // Format date as Day DD/MM (e.g., Wed 11/12)
const etaDate = new Date(ppr.eta); const dateDisplay = formatUtcWeekdayDayMonth(ppr.eta);
const dayName = etaDate.toLocaleDateString('en-GB', { weekday: 'short' });
const dateStr = etaDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
const dateDisplay = `${dayName} ${dateStr}`;
// Create notes indicator if notes exist // Create notes indicator if notes exist
const notesIndicator = ppr.notes && ppr.notes.trim() ? const notesIndicator = ppr.notes && ppr.notes.trim() ?
@@ -930,7 +925,7 @@
const bTime = b.eta || b.departure_dt; const bTime = b.eta || b.departure_dt;
if (!aTime) return 1; if (!aTime) return 1;
if (!bTime) return -1; if (!bTime) return -1;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
document.getElementById('arrivals-table-content').style.display = 'block'; document.getElementById('arrivals-table-content').style.display = 'block';
@@ -975,7 +970,7 @@
let departureTime = flight.departed_dt || flight.etd; let departureTime = flight.departed_dt || flight.etd;
let etaTime = departureTime; let etaTime = departureTime;
if (departureTime && flight.duration) { if (departureTime && flight.duration) {
const departTime = new Date(departureTime); const departTime = parseUtcDate(departureTime);
etaTime = new Date(departTime.getTime() + flight.duration * 60000).toISOString(); // duration is in minutes etaTime = new Date(departTime.getTime() + flight.duration * 60000).toISOString(); // duration is in minutes
} }
eta = etaTime ? formatTimeOnly(etaTime) : '-'; eta = etaTime ? formatTimeOnly(etaTime) : '-';
@@ -1126,7 +1121,7 @@
const bTime = b.etd || b.created_dt; const bTime = b.etd || b.created_dt;
if (!aTime) return 1; if (!aTime) return 1;
if (!bTime) return -1; if (!bTime) return -1;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
document.getElementById('departures-table-content').style.display = 'block'; document.getElementById('departures-table-content').style.display = 'block';
@@ -1359,10 +1354,7 @@
} }
function getLocalDateString(date = new Date()) { function getLocalDateString(date = new Date()) {
const year = date.getFullYear(); return date.toISOString().split('T')[0];
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
} }
function isTodayDateTime(value) { function isTodayDateTime(value) {
+3 -4
View File
@@ -966,8 +966,7 @@
if (/^[0-9]{4}$/.test(timeValue)) { if (/^[0-9]{4}$/.test(timeValue)) {
timeValue = timeValue.slice(0, 2) + ':' + timeValue.slice(2); timeValue = timeValue.slice(0, 2) + ':' + timeValue.slice(2);
} }
const datetime = new Date(`${today}T${timeValue}:00`); data[field] = `${today}T${timeValue}:00Z`;
data[field] = datetime.toISOString();
} }
}); });
@@ -1031,8 +1030,8 @@
function setDefaultTimes() { function setDefaultTimes() {
const now = new Date(); const now = new Date();
const futureTime = new Date(now.getTime() + 10 * 60000); // 10 minutes from now const futureTime = new Date(now.getTime() + 10 * 60000); // 10 minutes from now
const futureHours = String(futureTime.getHours()).padStart(2, '0'); const futureHours = String(futureTime.getUTCHours()).padStart(2, '0');
const futureMinutes = String(futureTime.getMinutes()).padStart(2, '0'); const futureMinutes = String(futureTime.getUTCMinutes()).padStart(2, '0');
const futureTimeValue = `${futureHours}:${futureMinutes}`; const futureTimeValue = `${futureHours}:${futureMinutes}`;
const etdFieldIds = ['localETD', 'circuitETD', 'depETD', 'arrETA']; const etdFieldIds = ['localETD', 'circuitETD', 'depETD', 'arrETA'];
+1 -1
View File
@@ -315,7 +315,7 @@
} }
function fromLocalInputValue(value) { function fromLocalInputValue(value) {
return new Date(value).toISOString(); return `${value}:00Z`;
} }
function showMessage(message, isError = false, clear = false) { function showMessage(message, isError = false, clear = false) {
+2 -5
View File
@@ -686,15 +686,12 @@
function addDays(date, days) { function addDays(date, days) {
const next = new Date(date); const next = new Date(date);
next.setDate(next.getDate() + days); next.setUTCDate(next.getUTCDate() + days);
return next; return next;
} }
function getLocalDateString(date) { function getLocalDateString(date) {
const year = date.getFullYear(); return date.toISOString().split('T')[0];
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
} }
function renderRequestList() { function renderRequestList() {
+29 -12
View File
@@ -437,12 +437,12 @@
if (!utcDateStr.includes('T')) { if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T'); utcDateStr = utcDateStr.replace(' ', 'T');
} }
if (!utcDateStr.includes('Z')) { if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
utcDateStr += 'Z'; utcDateStr += 'Z';
} }
const etaDate = new Date(utcDateStr); const etaDate = new Date(utcDateStr);
const etaDateStr = etaDate.toISOString().split('T')[0]; const etaDateStr = formatLocalDateInput(etaDate);
const etaTimeStr = etaDate.toISOString().slice(11, 16); const etaTimeStr = formatLocalTimeInput(etaDate);
document.getElementById('eta-date').value = etaDateStr; document.getElementById('eta-date').value = etaDateStr;
document.getElementById('eta-time').value = etaTimeStr; document.getElementById('eta-time').value = etaTimeStr;
} }
@@ -457,12 +457,12 @@
if (!utcDateStr.includes('T')) { if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T'); utcDateStr = utcDateStr.replace(' ', 'T');
} }
if (!utcDateStr.includes('Z')) { if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
utcDateStr += 'Z'; utcDateStr += 'Z';
} }
const etdDate = new Date(utcDateStr); const etdDate = new Date(utcDateStr);
const etdDateStr = etdDate.toISOString().split('T')[0]; const etdDateStr = formatLocalDateInput(etdDate);
const etdTimeStr = etdDate.toISOString().slice(11, 16); const etdTimeStr = formatLocalTimeInput(etdDate);
document.getElementById('etd-date').value = etdDateStr; document.getElementById('etd-date').value = etdDateStr;
document.getElementById('etd-time').value = etdTimeStr; document.getElementById('etd-time').value = etdTimeStr;
} }
@@ -471,15 +471,35 @@
document.getElementById('email').value = ppr.email || ''; document.getElementById('email').value = ppr.email || '';
document.getElementById('phone').value = ppr.phone || ''; document.getElementById('phone').value = ppr.phone || '';
document.getElementById('notes').value = ppr.notes || ''; document.getElementById('notes').value = ppr.notes || '';
if (['CANCELED', 'DELETED', 'LANDED', '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);
}
} else { } else {
throw new Error('Failed to load PPR data'); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to load PPR data');
} }
} catch (error) { } catch (error) {
console.error('Error loading PPR:', error); console.error('Error loading PPR:', error);
showNotification('Error loading PPR data', true); showNotification(`Error loading PPR: ${error.message}`, true);
} }
} }
function formatLocalDateInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatLocalTimeInput(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
// Aircraft lookup (same as submit form) // Aircraft lookup (same as submit form)
let aircraftLookupTimeout; let aircraftLookupTimeout;
async function handleAircraftLookup(registration) { async function handleAircraftLookup(registration) {
@@ -692,9 +712,6 @@
document.getElementById('ppr-form').addEventListener('submit', async function(e) { document.getElementById('ppr-form').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
// Auto-save any unsaved aircraft types
await autoSaveUnsavedAircraft(this);
const formData = new FormData(this); const formData = new FormData(this);
const pprData = {}; const pprData = {};
@@ -792,4 +809,4 @@
}); });
</script> </script>
</body> </body>
</html> </html>
+9 -2
View File
@@ -631,8 +631,15 @@
if (!utcDateTimeString) return ''; if (!utcDateTimeString) return '';
try { try {
// Parse the ISO datetime string // API datetimes are UTC, but DB-backed values may arrive without a timezone suffix.
const date = new Date(utcDateTimeString); let normalizedDateTime = String(utcDateTimeString).trim();
if (!normalizedDateTime.includes('T')) {
normalizedDateTime = normalizedDateTime.replace(' ', 'T');
}
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(normalizedDateTime)) {
normalizedDateTime += 'Z';
}
const date = new Date(normalizedDateTime);
// Check if valid date // Check if valid date
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
+7 -17
View File
@@ -409,10 +409,10 @@
function setDefaultDates() { function setDefaultDates() {
const today = new Date(); const today = new Date();
const thirtyDaysAgo = new Date(today); const thirtyDaysAgo = new Date(today);
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 30);
document.getElementById('dateFrom').valueAsDate = thirtyDaysAgo; document.getElementById('dateFrom').value = thirtyDaysAgo.toISOString().split('T')[0];
document.getElementById('dateTo').valueAsDate = today; document.getElementById('dateTo').value = today.toISOString().split('T')[0];
} }
async function loadJournalEntries() { async function loadJournalEntries() {
@@ -712,23 +712,13 @@
} }
function formatDateTime(dateString) { function formatDateTime(dateString) {
const date = new Date(dateString); const normalized = dateString.includes('T') ? dateString : dateString.replace(' ', 'T');
return date.toLocaleString('en-US', { const date = new Date(/[zZ]|[+-]\d{2}:?\d{2}$/.test(normalized) ? normalized : `${normalized}Z`);
year: 'numeric', return date.toISOString().slice(0, 19).replace('T', ' ');
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} }
function formatDate(date) { function formatDate(date) {
return date.toLocaleDateString('en-US', { return date.toISOString().slice(0, 10);
year: 'numeric',
month: 'short',
day: '2-digit'
});
} }
function escapeHtml(text) { function escapeHtml(text) {
+22 -24
View File
@@ -647,10 +647,10 @@
// Set date range to this week (Monday to Sunday) // Set date range to this week (Monday to Sunday)
function setDateRangeThisWeek() { function setDateRangeThisWeek() {
const now = new Date(); const now = new Date();
const dayOfWeek = now.getDay(); const dayOfWeek = now.getUTCDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday const diff = now.getUTCDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
const monday = new Date(now.setDate(diff)); const monday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), diff));
const sunday = new Date(now.setDate(diff + 6)); const sunday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), diff + 6));
document.getElementById('date-from').value = monday.toISOString().split('T')[0]; document.getElementById('date-from').value = monday.toISOString().split('T')[0];
document.getElementById('date-to').value = sunday.toISOString().split('T')[0]; document.getElementById('date-to').value = sunday.toISOString().split('T')[0];
@@ -665,8 +665,8 @@
// Set date range to this month // Set date range to this month
function setDateRangeThisMonth() { function setDateRangeThisMonth() {
const now = new Date(); const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); const firstDay = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); const lastDay = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0));
document.getElementById('date-from').value = firstDay.toISOString().split('T')[0]; document.getElementById('date-from').value = firstDay.toISOString().split('T')[0];
document.getElementById('date-to').value = lastDay.toISOString().split('T')[0]; document.getElementById('date-to').value = lastDay.toISOString().split('T')[0];
@@ -812,21 +812,14 @@
let dateRangeText = ''; let dateRangeText = '';
if (dateFrom && dateTo && dateFrom === dateTo) { if (dateFrom && dateTo && dateFrom === dateTo) {
// Single day // Single day
const date = new Date(dateFrom + 'T00:00:00Z'); dateRangeText = `for ${formatDateOnly(dateFrom)}`;
dateRangeText = `for ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
} else if (dateFrom && dateTo) { } else if (dateFrom && dateTo) {
// Date range // Date range
const fromDate = new Date(dateFrom + 'T00:00:00Z'); dateRangeText = `for ${formatDateOnly(dateFrom)} to ${formatDateOnly(dateTo)}`;
const toDate = new Date(dateTo + 'T00:00:00Z');
const fromText = fromDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
const toText = toDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
dateRangeText = `for ${fromText} to ${toText}`;
} else if (dateFrom) { } else if (dateFrom) {
const date = new Date(dateFrom + 'T00:00:00Z'); dateRangeText = `from ${formatDateOnly(dateFrom)}`;
dateRangeText = `from ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
} else if (dateTo) { } else if (dateTo) {
const date = new Date(dateTo + 'T00:00:00Z'); dateRangeText = `until ${formatDateOnly(dateTo)}`;
dateRangeText = `until ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
} }
// Update summary title with date range // Update summary title with date range
@@ -906,11 +899,11 @@
const date = new Date(utcDateStr); const date = new Date(utcDateStr);
// Format as dd/mm/yy hh:mm // Format as dd/mm/yy hh:mm
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const year = String(date.getFullYear()).slice(-2); const year = String(date.getUTCFullYear()).slice(-2);
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`; return `${day}/${month}/${year} ${hours}:${minutes}`;
} }
@@ -927,12 +920,17 @@
const date = new Date(utcDateStr); const date = new Date(utcDateStr);
// Format as hh:mm only // Format as hh:mm only
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${hours}:${minutes}`; return `${hours}:${minutes}`;
} }
function formatDateOnly(dateStr) {
const [year, month, day] = dateStr.split('-');
return `${day}/${month}/${year}`;
}
// Clear filters // Clear filters
function clearFilters() { function clearFilters() {
document.getElementById('status-filter').value = ''; document.getElementById('status-filter').value = '';
+391 -89
View File
@@ -155,6 +155,14 @@
overflow: hidden; overflow: hidden;
} }
.clickable-row {
cursor: pointer;
}
.clickable-row:hover {
background-color: #eef6ff;
}
.table-header { .table-header {
background: #34495e; background: #34495e;
color: white; color: white;
@@ -311,6 +319,98 @@
background-color: #e74c3c; background-color: #e74c3c;
} }
.modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.45);
}
.modal-content {
background: white;
margin: 4% auto;
width: min(920px, calc(100% - 2rem));
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
overflow: hidden;
}
.modal-header {
background: #34495e;
color: white;
padding: 1rem 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 1.2rem;
}
.close {
background: none;
border: none;
color: white;
font-size: 1.8rem;
cursor: pointer;
line-height: 1;
}
.modal-body {
padding: 1.25rem;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
}
.detail-field {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 0.65rem;
background: #fafafa;
}
.detail-label {
font-size: 0.72rem;
text-transform: uppercase;
color: #667085;
font-weight: 700;
margin-bottom: 0.25rem;
}
.detail-value {
overflow-wrap: anywhere;
}
.journal-section {
margin-top: 1rem;
border-top: 1px solid #e5e7eb;
padding-top: 1rem;
}
.journal-entry {
border-left: 3px solid #3498db;
padding: 0.5rem 0.75rem;
background: #f8fafc;
margin-top: 0.5rem;
}
.journal-meta {
font-size: 0.78rem;
color: #667085;
margin-bottom: 0.25rem;
}
/* Responsive design */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
@@ -474,7 +574,7 @@
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Departures</div> <div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Departures</div>
<div class="summary-item-value" style="font-size: 1.1rem;" id="non-ppr-departures">0</div> <div class="summary-item-value" style="font-size: 1.1rem;" id="non-ppr-departures">0</div>
</div> </div>
<div class="summary-item" style="padding: 0.4rem; cursor: pointer;" onclick="filterOtherFlights('OVERFLIGHT')"> <div class="summary-item" style="padding: 0.4rem;">
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Overflights</div> <div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Overflights</div>
<div class="summary-item-value" style="font-size: 1.1rem;" id="overflights-count">0</div> <div class="summary-item-value" style="font-size: 1.1rem;" id="overflights-count">0</div>
</div> </div>
@@ -524,14 +624,12 @@
<th>Callsign</th> <th>Callsign</th>
<th>Captain</th> <th>Captain</th>
<th>From</th> <th>From</th>
<th>ETA</th>
<th>POB In</th>
<th>To</th> <th>To</th>
<th>ETD</th> <th>Takeoff</th>
<th>Landing</th>
<th>POB In</th>
<th>POB Out</th> <th>POB Out</th>
<th>Fuel</th> <th>Fuel</th>
<th>Landed</th>
<th>Departed</th>
<th>Email</th> <th>Email</th>
<th>Phone</th> <th>Phone</th>
<th>Notes</th> <th>Notes</th>
@@ -582,8 +680,8 @@
<th>Callsign</th> <th>Callsign</th>
<th>From</th> <th>From</th>
<th>To</th> <th>To</th>
<th>ETA / ETD / Called</th> <th>Takeoff</th>
<th>Landed / Departed / QSY</th> <th>Landing</th>
<th>Circuits</th> <th>Circuits</th>
</tr> </tr>
</thead> </thead>
@@ -600,6 +698,22 @@
</div> </div>
</div> </div>
<div id="reportDetailModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="report-detail-title">Details</h2>
<button class="close" onclick="closeReportDetailModal()">&times;</button>
</div>
<div class="modal-body">
<div id="report-detail-body" class="detail-grid"></div>
<div class="journal-section">
<h3 style="margin: 0 0 0.5rem 0;">Journal</h3>
<div id="report-detail-journal">Loading...</div>
</div>
</div>
</div>
</div>
<!-- Success Notification --> <!-- Success Notification -->
<div id="notification" class="notification"></div> <div id="notification" class="notification"></div>
@@ -699,10 +813,10 @@
// Set date range to this week (Monday to Sunday) // Set date range to this week (Monday to Sunday)
function setDateRangeThisWeek() { function setDateRangeThisWeek() {
const now = new Date(); const now = new Date();
const dayOfWeek = now.getDay(); const dayOfWeek = now.getUTCDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday const diff = now.getUTCDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
const monday = new Date(now.setDate(diff)); const monday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), diff));
const sunday = new Date(now.setDate(diff + 6)); const sunday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), diff + 6));
document.getElementById('date-from').value = monday.toISOString().split('T')[0]; document.getElementById('date-from').value = monday.toISOString().split('T')[0];
document.getElementById('date-to').value = sunday.toISOString().split('T')[0]; document.getElementById('date-to').value = sunday.toISOString().split('T')[0];
@@ -717,8 +831,8 @@
// Set date range to this month // Set date range to this month
function setDateRangeThisMonth() { function setDateRangeThisMonth() {
const now = new Date(); const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); const firstDay = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); const lastDay = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0));
document.getElementById('date-from').value = firstDay.toISOString().split('T')[0]; document.getElementById('date-from').value = firstDay.toISOString().split('T')[0];
document.getElementById('date-to').value = lastDay.toISOString().split('T')[0]; document.getElementById('date-to').value = lastDay.toISOString().split('T')[0];
@@ -871,9 +985,12 @@
const arrivals = await arrivalsResponse.json(); const arrivals = await arrivalsResponse.json();
otherFlights.push(...arrivals.map(f => ({ otherFlights.push(...arrivals.map(f => ({
...f, ...f,
entityType: 'ARRIVAL',
flightType: 'ARRIVAL', flightType: 'ARRIVAL',
aircraft_type: f.type, aircraft_type: f.type,
timeField: f.eta || f.landed_dt, sortTime: f.landed_dt || f.eta || f.created_dt,
takeoffTime: null,
landingTime: f.landed_dt,
fromField: f.in_from, fromField: f.in_from,
toField: 'EGFH' toField: 'EGFH'
}))); })));
@@ -883,9 +1000,12 @@
const departures = await departuresResponse.json(); const departures = await departuresResponse.json();
otherFlights.push(...departures.map(f => ({ otherFlights.push(...departures.map(f => ({
...f, ...f,
entityType: 'DEPARTURE',
flightType: 'DEPARTURE', flightType: 'DEPARTURE',
aircraft_type: f.type, aircraft_type: f.type,
timeField: f.etd || f.departed_dt, sortTime: f.takeoff_dt || f.departed_dt || f.etd || f.created_dt,
takeoffTime: f.takeoff_dt || f.departed_dt,
landingTime: null,
fromField: 'EGFH', fromField: 'EGFH',
toField: f.out_to toField: f.out_to
}))); })));
@@ -895,10 +1015,13 @@
const localFlights = await localFlightsResponse.json(); const localFlights = await localFlightsResponse.json();
otherFlights.push(...localFlights.map(f => ({ otherFlights.push(...localFlights.map(f => ({
...f, ...f,
entityType: 'LOCAL_FLIGHT',
flightType: f.flight_type === 'CIRCUITS' ? 'CIRCUIT' : f.flight_type, flightType: f.flight_type === 'CIRCUITS' ? 'CIRCUIT' : f.flight_type,
aircraft_type: f.type, aircraft_type: f.type,
circuits: f.circuits, circuits: f.circuits,
timeField: f.departed_dt, sortTime: f.takeoff_dt || f.departed_dt || f.landed_dt || f.etd || f.created_dt,
takeoffTime: f.takeoff_dt || f.departed_dt,
landingTime: f.landed_dt,
fromField: 'EGFH', fromField: 'EGFH',
toField: 'EGFH' toField: 'EGFH'
}))); })));
@@ -908,10 +1031,13 @@
const overflights = await overflightsResponse.json(); const overflights = await overflightsResponse.json();
otherFlights.push(...overflights.map(f => ({ otherFlights.push(...overflights.map(f => ({
...f, ...f,
entityType: 'OVERFLIGHT',
flightType: 'OVERFLIGHT', flightType: 'OVERFLIGHT',
aircraft_type: f.type, aircraft_type: f.type,
circuits: null, circuits: null,
timeField: f.call_dt, sortTime: f.call_dt,
takeoffTime: null,
landingTime: null,
fromField: f.departure_airfield, fromField: f.departure_airfield,
toField: f.destination_airfield, toField: f.destination_airfield,
callsign: f.registration callsign: f.registration
@@ -959,21 +1085,14 @@
let dateRangeText = ''; let dateRangeText = '';
if (dateFrom && dateTo && dateFrom === dateTo) { if (dateFrom && dateTo && dateFrom === dateTo) {
// Single day // Single day
const date = new Date(dateFrom + 'T00:00:00Z'); dateRangeText = `for ${formatDateOnly(dateFrom)}`;
dateRangeText = `for ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
} else if (dateFrom && dateTo) { } else if (dateFrom && dateTo) {
// Date range // Date range
const fromDate = new Date(dateFrom + 'T00:00:00Z'); dateRangeText = `for ${formatDateOnly(dateFrom)} to ${formatDateOnly(dateTo)}`;
const toDate = new Date(dateTo + 'T00:00:00Z');
const fromText = fromDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
const toText = toDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
dateRangeText = `for ${fromText} to ${toText}`;
} else if (dateFrom) { } else if (dateFrom) {
const date = new Date(dateFrom + 'T00:00:00Z'); dateRangeText = `from ${formatDateOnly(dateFrom)}`;
dateRangeText = `from ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
} else if (dateTo) { } else if (dateTo) {
const date = new Date(dateTo + 'T00:00:00Z'); dateRangeText = `until ${formatDateOnly(dateTo)}`;
dateRangeText = `until ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
} }
// Update summary title with date range // Update summary title with date range
@@ -1045,11 +1164,13 @@
return; return;
} }
// Sort by ETA (ascending) // Sort by first actual movement time, then planned times as a fallback.
pprs.sort((a, b) => { pprs.sort((a, b) => {
if (!a.eta) return 1; const aTime = getPPRSortTime(a);
if (!b.eta) return -1; const bTime = getPPRSortTime(b);
return new Date(a.eta) - new Date(b.eta); if (!aTime) return 1;
if (!bTime) return -1;
return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -1057,12 +1178,11 @@
for (const ppr of pprs) { for (const ppr of pprs) {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.className = 'clickable-row';
row.onclick = () => openReportDetail('PPR', ppr.id);
// Format dates const takeoff = ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '-';
const eta = ppr.eta ? formatDateTime(ppr.eta) : '-'; const landing = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
const etd = ppr.etd ? formatDateTime(ppr.etd) : '-';
const landed = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
const departed = ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '-';
const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-'; const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-';
// Status styling // Status styling
@@ -1076,14 +1196,12 @@
<td>${ppr.ac_call || '-'}</td> <td>${ppr.ac_call || '-'}</td>
<td>${ppr.captain}</td> <td>${ppr.captain}</td>
<td>${ppr.in_from}</td> <td>${ppr.in_from}</td>
<td>${eta}</td>
<td>${ppr.pob_in}</td>
<td>${ppr.out_to || '-'}</td> <td>${ppr.out_to || '-'}</td>
<td>${etd}</td> <td>${takeoff}</td>
<td>${landing}</td>
<td>${ppr.pob_in}</td>
<td>${ppr.pob_out || '-'}</td> <td>${ppr.pob_out || '-'}</td>
<td>${ppr.fuel || '-'}</td> <td>${ppr.fuel || '-'}</td>
<td>${landed}</td>
<td>${departed}</td>
<td>${ppr.email || '-'}</td> <td>${ppr.email || '-'}</td>
<td>${ppr.phone || '-'}</td> <td>${ppr.phone || '-'}</td>
<td>${ppr.notes || '-'}</td> <td>${ppr.notes || '-'}</td>
@@ -1176,10 +1294,10 @@
const tbody = document.getElementById('other-flights-table-body'); const tbody = document.getElementById('other-flights-table-body');
const tableInfo = document.getElementById('other-flights-info'); const tableInfo = document.getElementById('other-flights-info');
// Apply filter if one is selected // Overflights are counted in the summary but omitted from the detail table for now.
let filteredFlights = flights; let filteredFlights = flights.filter(flight => flight.flightType !== 'OVERFLIGHT');
if (otherFlightsFilterType) { if (otherFlightsFilterType) {
filteredFlights = flights.filter(flight => flight.flightType === otherFlightsFilterType); filteredFlights = filteredFlights.filter(flight => flight.flightType === otherFlightsFilterType);
} }
tableInfo.textContent = `${filteredFlights.length} flights found` + (otherFlightsFilterType ? ` (filtered by ${otherFlightsFilterType})` : ''); tableInfo.textContent = `${filteredFlights.length} flights found` + (otherFlightsFilterType ? ` (filtered by ${otherFlightsFilterType})` : '');
@@ -1190,13 +1308,13 @@
return; return;
} }
// Sort by time field (ascending) // Sort by the first pertinent movement time.
filteredFlights.sort((a, b) => { filteredFlights.sort((a, b) => {
const aTime = a.timeField; const aTime = a.sortTime;
const bTime = b.timeField; const bTime = b.sortTime;
if (!aTime) return 1; if (!aTime) return 1;
if (!bTime) return -1; if (!bTime) return -1;
return new Date(aTime) - new Date(bTime); return parseUtcDate(aTime) - parseUtcDate(bTime);
}); });
tbody.innerHTML = ''; tbody.innerHTML = '';
@@ -1205,6 +1323,8 @@
for (const flight of filteredFlights) { for (const flight of filteredFlights) {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.className = 'clickable-row';
row.onclick = () => openReportDetail(flight.entityType, flight.id);
const typeLabel = flight.flightType; const typeLabel = flight.flightType;
const registration = flight.registration || '-'; const registration = flight.registration || '-';
@@ -1212,18 +1332,8 @@
const callsign = flight.callsign || '-'; const callsign = flight.callsign || '-';
const from = flight.fromField || '-'; const from = flight.fromField || '-';
const to = flight.toField || '-'; const to = flight.toField || '-';
const timeDisplay = flight.timeField ? formatDateTime(flight.timeField) : '-'; const takeoff = flight.takeoffTime ? formatDateTime(flight.takeoffTime) : '-';
const landing = flight.landingTime ? formatDateTime(flight.landingTime) : '-';
// Different display for different flight types
let actualDisplay = '-';
if (flight.flightType === 'ARRIVAL') {
actualDisplay = flight.landed_dt ? formatDateTime(flight.landed_dt) : '-';
} else if (flight.flightType === 'OVERFLIGHT') {
// For overflights, show qsy_dt (frequency change time)
actualDisplay = flight.qsy_dt ? formatDateTime(flight.qsy_dt) : '-';
} else {
actualDisplay = flight.departed_dt ? formatDateTime(flight.departed_dt) : '-';
}
const status = flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING'); const status = flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING');
const circuits = (flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits > 0 ? flight.circuits : '-') : '-'; const circuits = (flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits > 0 ? flight.circuits : '-') : '-';
@@ -1236,8 +1346,8 @@
<td>${callsign}</td> <td>${callsign}</td>
<td>${from}</td> <td>${from}</td>
<td>${to}</td> <td>${to}</td>
<td>${timeDisplay}</td> <td>${takeoff}</td>
<td>${actualDisplay}</td> <td>${landing}</td>
<td>${circuits}</td> <td>${circuits}</td>
`; `;
@@ -1247,23 +1357,201 @@
function formatDateTime(dateStr) { function formatDateTime(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';
let utcDateStr = dateStr; const date = parseUtcDate(dateStr);
// Format as dd/mm/yy hh:mm
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const year = String(date.getUTCFullYear()).slice(-2);
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
}
function parseUtcDate(dateStr) {
let utcDateStr = String(dateStr).trim();
if (!utcDateStr.includes('T')) { if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T'); utcDateStr = utcDateStr.replace(' ', 'T');
} }
if (!utcDateStr.includes('Z')) { if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
utcDateStr += 'Z'; utcDateStr += 'Z';
} }
const date = new Date(utcDateStr); return new Date(utcDateStr);
}
// Format as dd/mm/yy hh:mm
const day = String(date.getDate()).padStart(2, '0'); function getPPRSortTime(ppr) {
const month = String(date.getMonth() + 1).padStart(2, '0'); return ppr.landed_dt || ppr.departed_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
const year = String(date.getFullYear()).slice(-2); }
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0'); const detailConfig = {
PPR: {
return `${day}/${month}/${year} ${hours}:${minutes}`; endpoint: id => `/api/v1/pprs/${id}`,
journalType: 'PPR',
title: record => `PPR: ${record.ac_reg || '-'}`,
fields: [
['Status', r => r.status],
['Aircraft', r => r.ac_reg],
['Type', r => r.ac_type],
['Callsign', r => r.ac_call],
['Captain', r => r.captain],
['From', r => r.in_from],
['To', r => r.out_to],
['Takeoff', r => formatOptionalDateTime(r.departed_dt)],
['Landing', r => formatOptionalDateTime(r.landed_dt)],
['ETA', r => formatOptionalDateTime(r.eta)],
['ETD', r => formatOptionalDateTime(r.etd)],
['POB In', r => r.pob_in],
['POB Out', r => r.pob_out],
['Fuel', r => r.fuel],
['Email', r => r.email],
['Phone', r => r.phone],
['Submitted', r => formatOptionalDateTime(r.submitted_dt)],
['Created By', r => r.created_by],
['Notes', r => r.notes]
]
},
LOCAL_FLIGHT: {
endpoint: id => `/api/v1/local-flights/${id}`,
journalType: 'LOCAL_FLIGHT',
title: record => `${record.flight_type || 'LOCAL'}: ${record.registration || '-'}`,
fields: [
['Status', r => r.status],
['Aircraft', r => r.registration],
['Type', r => r.type],
['Callsign', r => r.callsign],
['Flight Type', r => r.flight_type],
['From', () => 'EGFH'],
['To', () => 'EGFH'],
['Takeoff', r => formatOptionalDateTime(r.takeoff_dt || r.departed_dt)],
['Landing', r => formatOptionalDateTime(r.landed_dt)],
['ETD', r => formatOptionalDateTime(r.etd)],
['POB', r => r.pob],
['Duration', r => r.duration ? `${r.duration} min` : null],
['Circuits', r => r.circuits],
['Created', r => formatOptionalDateTime(r.created_dt)],
['Created By', r => r.created_by],
['Notes', r => r.notes]
]
},
ARRIVAL: {
endpoint: id => `/api/v1/arrivals/${id}`,
journalType: 'ARRIVAL',
title: record => `Arrival: ${record.registration || record.callsign || '-'}`,
fields: [
['Status', r => r.status],
['Aircraft', r => r.registration],
['Type', r => r.type],
['Callsign', r => r.callsign],
['From', r => r.in_from],
['To', () => 'EGFH'],
['Landing', r => formatOptionalDateTime(r.landed_dt)],
['ETA', r => formatOptionalDateTime(r.eta)],
['POB', r => r.pob],
['Created', r => formatOptionalDateTime(r.created_dt)],
['Created By', r => r.created_by],
['Notes', r => r.notes]
]
},
DEPARTURE: {
endpoint: id => `/api/v1/departures/${id}`,
journalType: 'DEPARTURE',
title: record => `Departure: ${record.registration || '-'}`,
fields: [
['Status', r => r.status],
['Aircraft', r => r.registration],
['Type', r => r.type],
['Callsign', r => r.callsign],
['From', () => 'EGFH'],
['To', r => r.out_to],
['Takeoff', r => formatOptionalDateTime(r.takeoff_dt || r.departed_dt)],
['ETD', r => formatOptionalDateTime(r.etd)],
['POB', r => r.pob],
['Created', r => formatOptionalDateTime(r.created_dt)],
['Created By', r => r.created_by],
['Notes', r => r.notes]
]
}
};
async function openReportDetail(type, id) {
const config = detailConfig[type];
if (!config) return;
document.getElementById('report-detail-title').textContent = 'Loading...';
document.getElementById('report-detail-body').innerHTML = '';
document.getElementById('report-detail-journal').textContent = 'Loading...';
document.getElementById('reportDetailModal').style.display = 'block';
try {
const response = await authenticatedFetch(config.endpoint(id));
if (!response.ok) throw new Error('Unable to load details');
const record = await response.json();
document.getElementById('report-detail-title').textContent = config.title(record);
document.getElementById('report-detail-body').innerHTML = config.fields
.map(([label, getter]) => detailField(label, getter(record)))
.join('');
await loadReportJournal(config.journalType, id);
} catch (error) {
console.error('Error loading report detail:', error);
document.getElementById('report-detail-title').textContent = 'Unable to load details';
document.getElementById('report-detail-body').innerHTML = `<div class="detail-field"><div class="detail-value">${escapeHtml(error.message)}</div></div>`;
document.getElementById('report-detail-journal').textContent = '-';
}
}
async function loadReportJournal(entityType, entityId) {
try {
const response = await authenticatedFetch(`/api/v1/journal/${entityType}/${entityId}`);
if (!response.ok) throw new Error('Unable to load journal');
const data = await response.json();
const entries = data.entries || [];
document.getElementById('report-detail-journal').innerHTML = entries.length
? entries.map(entry => `
<div class="journal-entry">
<div class="journal-meta">${formatOptionalDateTime(entry.entry_dt)} by ${escapeHtml(entry.user || '-')}</div>
<div>${escapeHtml(entry.entry || '-')}</div>
</div>
`).join('')
: '<p>No journal entries yet.</p>';
} catch (error) {
document.getElementById('report-detail-journal').textContent = error.message;
}
}
function closeReportDetailModal() {
document.getElementById('reportDetailModal').style.display = 'none';
}
function detailField(label, value) {
const displayValue = value === null || value === undefined || value === '' ? '-' : value;
return `
<div class="detail-field">
<div class="detail-label">${escapeHtml(label)}</div>
<div class="detail-value">${escapeHtml(displayValue)}</div>
</div>
`;
}
function formatOptionalDateTime(value) {
return value ? formatDateTime(value) : '-';
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
}
function formatDateOnly(dateStr) {
const [year, month, day] = dateStr.split('-');
return `${day}/${month}/${year}`;
} }
// Clear filters // Clear filters
@@ -1283,8 +1571,8 @@
const headers = [ const headers = [
'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain', 'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain',
'From', 'ETA', 'POB In', 'To', 'ETD', 'POB Out', 'Fuel', 'From', 'To', 'Takeoff', 'Landing', 'POB In', 'POB Out', 'Fuel',
'Landed', 'Departed', 'Email', 'Phone', 'Notes', 'Submitted', 'Created By' 'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
]; ];
const csvData = currentPPRs.map(ppr => [ const csvData = currentPPRs.map(ppr => [
@@ -1295,14 +1583,12 @@
ppr.ac_call || '', ppr.ac_call || '',
ppr.captain, ppr.captain,
ppr.in_from, ppr.in_from,
ppr.eta ? formatDateTime(ppr.eta) : '',
ppr.pob_in,
ppr.out_to || '', ppr.out_to || '',
ppr.etd ? formatDateTime(ppr.etd) : '', ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '',
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
ppr.pob_in,
ppr.pob_out || '', ppr.pob_out || '',
ppr.fuel || '', ppr.fuel || '',
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '',
ppr.email || '', ppr.email || '',
ppr.phone || '', ppr.phone || '',
ppr.notes || '', ppr.notes || '',
@@ -1319,22 +1605,26 @@
return; return;
} }
const exportFlights = currentOtherFlights.filter(flight => flight.flightType !== 'OVERFLIGHT');
if (exportFlights.length === 0) {
showNotification('No table data to export', true);
return;
}
const headers = [ const headers = [
'Flight Type', 'Aircraft Registration', 'Aircraft Type', 'Callsign', 'From', 'To', 'Flight Type', 'Aircraft Registration', 'Aircraft Type', 'Callsign', 'From', 'To',
'ETA/ETD', 'Landed/Departed', 'Status', 'Circuits' 'Takeoff', 'Landing', 'Status', 'Circuits'
]; ];
const csvData = currentOtherFlights.map(flight => [ const csvData = exportFlights.map(flight => [
flight.flightType, flight.flightType,
flight.registration || '', flight.registration || '',
flight.aircraft_type || '', flight.aircraft_type || '',
flight.callsign || '', flight.callsign || '',
flight.fromField || '', flight.fromField || '',
flight.toField || '', flight.toField || '',
flight.timeField ? formatDateTime(flight.timeField) : '', flight.takeoffTime ? formatDateTime(flight.takeoffTime) : '',
flight.flightType === 'ARRIVAL' flight.landingTime ? formatDateTime(flight.landingTime) : '',
? (flight.landed_dt ? formatDateTime(flight.landed_dt) : '')
: (flight.departed_dt ? formatDateTime(flight.departed_dt) : ''),
flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING'), flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING'),
(flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits || '') : '' (flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits || '') : ''
]); ]);
@@ -1383,6 +1673,18 @@
} }
}); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('reportDetailModal').style.display === 'block') {
closeReportDetailModal();
}
});
window.addEventListener('click', function(e) {
if (e.target === document.getElementById('reportDetailModal')) {
closeReportDetailModal();
}
});
// Initialize when page loads // Initialize when page loads
document.addEventListener('DOMContentLoaded', initializePage); document.addEventListener('DOMContentLoaded', initializePage);
</script> </script>
+2 -2
View File
@@ -69,7 +69,7 @@
<div id="arrival-airport-lookup-results"></div> <div id="arrival-airport-lookup-results"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="eta">ETA (Local Time) *</label> <label for="eta">ETA (UTC) *</label>
<div style="display: flex; gap: 0.5rem;"> <div style="display: flex; gap: 0.5rem;">
<input type="date" id="eta-date" name="eta-date" required style="flex: 1;"> <input type="date" id="eta-date" name="eta-date" required style="flex: 1;">
<select id="eta-time" name="eta-time" required style="flex: 1;"> <select id="eta-time" name="eta-time" required style="flex: 1;">
@@ -95,7 +95,7 @@
<div id="departure-airport-lookup-results"></div> <div id="departure-airport-lookup-results"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="etd">ETD (Local Time)</label> <label for="etd">ETD (UTC)</label>
<div style="display: flex; gap: 0.5rem;"> <div style="display: flex; gap: 0.5rem;">
<input type="date" id="etd-date" name="etd-date" tabindex="-1" style="flex: 1;"> <input type="date" id="etd-date" name="etd-date" tabindex="-1" style="flex: 1;">
<select id="etd-time" name="etd-time" tabindex="-1" style="flex: 1;"> <select id="etd-time" name="etd-time" tabindex="-1" style="flex: 1;">
+103 -59
View File
@@ -590,33 +590,104 @@
return response; return response;
} }
// Load PPR records - now loads all tables function normalizeUtcDateString(dateStr) {
function formatTimeOnly(dateStr) { if (!dateStr) return null;
if (!dateStr) return '-'; let utcDateStr = String(dateStr).trim();
// Ensure the datetime string is treated as UTC
let utcDateStr = dateStr;
if (!utcDateStr.includes('T')) { if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T'); utcDateStr = utcDateStr.replace(' ', 'T');
} }
if (!utcDateStr.includes('Z')) { if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
utcDateStr += 'Z'; utcDateStr += 'Z';
} }
const date = new Date(utcDateStr); return utcDateStr;
}
function parseUtcDate(dateStr) {
const normalized = normalizeUtcDateString(dateStr);
return normalized ? new Date(normalized) : null;
}
function utcDateOnly(dateStr) {
const date = parseUtcDate(dateStr);
return date && !Number.isNaN(date.getTime()) ? date.toISOString().slice(0, 10) : '';
}
function formatUtcDateInput(date) {
return date.toISOString().slice(0, 10);
}
function formatUtcTimeInput(date) {
return date.toISOString().slice(11, 16); return date.toISOString().slice(11, 16);
} }
function formatUtcDayMonth(dateStr) {
const isoDate = utcDateOnly(dateStr);
return isoDate ? `${isoDate.slice(8, 10)}/${isoDate.slice(5, 7)}` : '-';
}
function formatUtcWeekdayDayMonth(dateStr) {
const date = parseUtcDate(dateStr);
if (!date || Number.isNaN(date.getTime())) return '-';
const dayName = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getUTCDay()];
return `${dayName} ${formatUtcDayMonth(dateStr)}`;
}
function combineUtcDateTimeInput(dateStr, timeStr) {
return `${dateStr}T${timeStr}:00Z`;
}
async function autoSaveUnsavedAircraft(form) {
if (!form || !form.hasAttribute('data-unsaved-aircraft') || !accessToken) return;
const formData = new FormData(form);
const registration = (
formData.get('ac_reg') ||
formData.get('registration') ||
formData.get('local_registration') ||
formData.get('book_in_registration') ||
formData.get('overflight_registration') ||
''
).trim();
const typeCode = (
formData.get('ac_type') ||
formData.get('type') ||
formData.get('local_type') ||
formData.get('book_in_type') ||
formData.get('overflight_type') ||
''
).trim();
if (!registration || !typeCode) return;
try {
const response = await authenticatedFetch('/api/v1/aircraft/user-aircraft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
registration,
type_code: typeCode
})
});
if (response.ok) {
form.removeAttribute('data-unsaved-aircraft');
}
} catch (error) {
console.warn('Could not save user aircraft type:', error);
}
}
// Load PPR records - now loads all tables
function formatTimeOnly(dateStr) {
if (!dateStr) return '-';
const date = parseUtcDate(dateStr);
return date && !Number.isNaN(date.getTime()) ? formatUtcTimeInput(date) : '-';
}
function formatDateTime(dateStr) { function formatDateTime(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';
// Ensure the datetime string is treated as UTC const date = parseUtcDate(dateStr);
let utcDateStr = dateStr; return date && !Number.isNaN(date.getTime()) ? `${formatUtcDateInput(date)} ${formatUtcTimeInput(date)}` : '-';
if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T');
}
if (!utcDateStr.includes('Z')) {
utcDateStr += 'Z';
}
const date = new Date(utcDateStr);
return date.toISOString().slice(0, 10) + ' ' + date.toISOString().slice(11, 16);
} }
// Modal functions // Modal functions
@@ -640,24 +711,10 @@
const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour const eta = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
const etd = new Date(now.getTime() + 2 * 60 * 60 * 1000); // +2 hours const etd = new Date(now.getTime() + 2 * 60 * 60 * 1000); // +2 hours
// Format date and time for separate inputs document.getElementById('eta-date').value = formatUtcDateInput(eta);
function formatDate(date) { document.getElementById('eta-time').value = formatUtcTimeInput(eta);
const year = date.getFullYear(); document.getElementById('etd-date').value = formatUtcDateInput(etd);
const month = String(date.getMonth() + 1).padStart(2, '0'); document.getElementById('etd-time').value = formatUtcTimeInput(etd);
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(Math.ceil(date.getMinutes() / 15) * 15 % 60).padStart(2, '0'); // Round up to next 15-minute interval
return `${hours}:${minutes}`;
}
document.getElementById('eta-date').value = formatDate(eta);
document.getElementById('eta-time').value = formatTime(eta);
document.getElementById('etd-date').value = formatDate(etd);
document.getElementById('etd-time').value = formatTime(etd);
// Clear aircraft lookup results // Clear aircraft lookup results
clearAircraftLookup(); clearAircraftLookup();
@@ -683,15 +740,14 @@
const etaTime = document.getElementById('eta-time').value; const etaTime = document.getElementById('eta-time').value;
if (etaDate && etaTime) { if (etaDate && etaTime) {
// Parse ETA const eta = parseUtcDate(combineUtcDateTimeInput(etaDate, etaTime));
const eta = new Date(`${etaDate}T${etaTime}`);
// Calculate ETD (2 hours after ETA) // Calculate ETD (2 hours after ETA)
const etd = new Date(eta.getTime() + 2 * 60 * 60 * 1000); const etd = new Date(eta.getTime() + 2 * 60 * 60 * 1000);
// Format ETD // Format ETD
const etdDateStr = `${etd.getFullYear()}-${String(etd.getMonth() + 1).padStart(2, '0')}-${String(etd.getDate()).padStart(2, '0')}`; const etdDateStr = formatUtcDateInput(etd);
const etdTimeStr = `${String(etd.getHours()).padStart(2, '0')}:${String(etd.getMinutes()).padStart(2, '0')}`; const etdTimeStr = formatUtcTimeInput(etd);
// Update ETD fields // Update ETD fields
document.getElementById('etd-date').value = etdDateStr; document.getElementById('etd-date').value = etdDateStr;
@@ -754,31 +810,19 @@
Object.keys(ppr).forEach(key => { Object.keys(ppr).forEach(key => {
if (key === 'eta' || key === 'etd') { if (key === 'eta' || key === 'etd') {
if (ppr[key]) { if (ppr[key]) {
// ppr[key] is UTC datetime string from API (naive, assume UTC) const date = parseUtcDate(ppr[key]);
let utcDateStr = ppr[key];
if (!utcDateStr.includes('T')) {
utcDateStr = utcDateStr.replace(' ', 'T');
}
if (!utcDateStr.includes('Z')) {
utcDateStr += 'Z';
}
const date = new Date(utcDateStr); // Now correctly parsed as UTC
// Split into date and time components for separate inputs // Split into date and time components for separate inputs
const dateField = document.getElementById(`${key}-date`); const dateField = document.getElementById(`${key}-date`);
const timeField = document.getElementById(`${key}-time`); const timeField = document.getElementById(`${key}-time`);
if (dateField && timeField) { if (dateField && timeField) {
// Format date const dateValue = formatUtcDateInput(date);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateValue = `${year}-${month}-${day}`;
dateField.value = dateValue; dateField.value = dateValue;
// Format time (round to nearest 15-minute interval) // Format time (round to nearest 15-minute interval)
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getUTCHours()).padStart(2, '0');
const rawMinutes = date.getMinutes(); const rawMinutes = date.getUTCMinutes();
const roundedMinutes = Math.round(rawMinutes / 15) * 15 % 60; const roundedMinutes = Math.round(rawMinutes / 15) * 15 % 60;
const minutes = String(roundedMinutes).padStart(2, '0'); const minutes = String(roundedMinutes).padStart(2, '0');
const timeValue = `${hours}:${minutes}`; const timeValue = `${hours}:${minutes}`;
@@ -1089,12 +1133,12 @@
// Combine date and time for ETA // Combine date and time for ETA
const dateStr = formData.get('eta-date'); const dateStr = formData.get('eta-date');
const timeStr = formData.get('eta-time'); const timeStr = formData.get('eta-time');
pprData.eta = new Date(`${dateStr}T${timeStr}`).toISOString(); pprData.eta = combineUtcDateTimeInput(dateStr, timeStr);
} else if (key === 'etd-date' && formData.get('etd-time')) { } else if (key === 'etd-date' && formData.get('etd-time')) {
// Combine date and time for ETD // Combine date and time for ETD
const dateStr = formData.get('etd-date'); const dateStr = formData.get('etd-date');
const timeStr = formData.get('etd-time'); const timeStr = formData.get('etd-time');
pprData.etd = new Date(`${dateStr}T${timeStr}`).toISOString(); pprData.etd = combineUtcDateTimeInput(dateStr, timeStr);
} else if (key !== 'eta-time' && key !== 'etd-time') { } else if (key !== 'eta-time' && key !== 'etd-time') {
// Skip the time fields as they're handled above // Skip the time fields as they're handled above
pprData[key] = value; pprData[key] = value;
@@ -1856,13 +1900,13 @@
// Parse and populate call_dt // Parse and populate call_dt
if (overflight.call_dt) { if (overflight.call_dt) {
const callDt = new Date(overflight.call_dt); const callDt = parseUtcDate(overflight.call_dt);
document.getElementById('overflight_edit_call_dt').value = callDt.toISOString().slice(0, 16); document.getElementById('overflight_edit_call_dt').value = callDt.toISOString().slice(0, 16);
} }
// Parse and populate qsy_dt if exists // Parse and populate qsy_dt if exists
if (overflight.qsy_dt) { if (overflight.qsy_dt) {
const qsyDt = new Date(overflight.qsy_dt); const qsyDt = parseUtcDate(overflight.qsy_dt);
document.getElementById('overflight_edit_qsy_dt').value = qsyDt.toISOString().slice(0, 16); document.getElementById('overflight_edit_qsy_dt').value = qsyDt.toISOString().slice(0, 16);
} else { } else {
document.getElementById('overflight_edit_qsy_dt').value = ''; document.getElementById('overflight_edit_qsy_dt').value = '';