Compare commits
2 Commits
870bc0649b
...
74c21fe988
| Author | SHA1 | Date | |
|---|---|---|---|
| 74c21fe988 | |||
| c2e4d2adeb |
@@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -10,9 +10,8 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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'];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
+28
-11
@@ -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 = {};
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -631,15 +631,15 @@
|
|||||||
if (!utcDateTimeString) return '';
|
if (!utcDateTimeString) return '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// API datetimes are UTC; normalize naive strings before local display.
|
// API datetimes are UTC, but DB-backed values may arrive without a timezone suffix.
|
||||||
let utcDateStr = utcDateTimeString;
|
let normalizedDateTime = String(utcDateTimeString).trim();
|
||||||
if (!utcDateStr.includes('T')) {
|
if (!normalizedDateTime.includes('T')) {
|
||||||
utcDateStr = utcDateStr.replace(' ', 'T');
|
normalizedDateTime = normalizedDateTime.replace(' ', 'T');
|
||||||
}
|
}
|
||||||
if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(utcDateStr)) {
|
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(normalizedDateTime)) {
|
||||||
utcDateStr += 'Z';
|
normalizedDateTime += 'Z';
|
||||||
}
|
}
|
||||||
const date = new Date(utcDateStr);
|
const date = new Date(normalizedDateTime);
|
||||||
|
|
||||||
// Check if valid date
|
// Check if valid date
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
|
|||||||
+7
-17
@@ -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
@@ -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 = '';
|
||||||
|
|||||||
+389
-87
@@ -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()">×</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
|
function getPPRSortTime(ppr) {
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
return ppr.landed_dt || ppr.departed_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
}
|
||||||
const year = String(date.getFullYear()).slice(-2);
|
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
const detailConfig = {
|
||||||
|
PPR: {
|
||||||
|
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 => ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
}[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>
|
||||||
|
|||||||
@@ -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;">
|
||||||
|
|||||||
+101
-70
@@ -590,50 +590,106 @@
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load PPR records - now loads all tables
|
|
||||||
function formatTimeOnly(dateStr) {
|
|
||||||
if (!dateStr) return '-';
|
|
||||||
// Ensure the datetime string is treated as UTC
|
|
||||||
let utcDateStr = dateStr;
|
|
||||||
if (!utcDateStr.includes('T')) {
|
|
||||||
utcDateStr = utcDateStr.replace(' ', 'T');
|
|
||||||
}
|
|
||||||
if (!utcDateStr.includes('Z')) {
|
|
||||||
utcDateStr += 'Z';
|
|
||||||
}
|
|
||||||
const date = new Date(utcDateStr);
|
|
||||||
return date.toISOString().slice(11, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateTime(dateStr) {
|
|
||||||
if (!dateStr) return '-';
|
|
||||||
// Ensure the datetime string is treated as UTC
|
|
||||||
let utcDateStr = dateStr;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeUtcDateString(dateStr) {
|
function normalizeUtcDateString(dateStr) {
|
||||||
let utcDateStr = dateStr;
|
if (!dateStr) return null;
|
||||||
|
let utcDateStr = String(dateStr).trim();
|
||||||
if (!utcDateStr.includes('T')) {
|
if (!utcDateStr.includes('T')) {
|
||||||
utcDateStr = utcDateStr.replace(' ', 'T');
|
utcDateStr = utcDateStr.replace(' ', 'T');
|
||||||
}
|
}
|
||||||
if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(utcDateStr)) {
|
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
|
||||||
utcDateStr += 'Z';
|
utcDateStr += 'Z';
|
||||||
}
|
}
|
||||||
return utcDateStr;
|
return utcDateStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function utcInputToIso(dateStr, timeStr) {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
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) {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
const date = parseUtcDate(dateStr);
|
||||||
|
return date && !Number.isNaN(date.getTime()) ? `${formatUtcDateInput(date)} ${formatUtcTimeInput(date)}` : '-';
|
||||||
|
}
|
||||||
|
|
||||||
// Modal functions
|
// Modal functions
|
||||||
function openNewPPRModal() {
|
function openNewPPRModal() {
|
||||||
isNewPPR = true;
|
isNewPPR = true;
|
||||||
@@ -655,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();
|
||||||
@@ -698,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;
|
||||||
@@ -769,20 +810,14 @@
|
|||||||
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]);
|
||||||
const utcDateStr = normalizeUtcDateString(ppr[key]);
|
|
||||||
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.getUTCFullYear();
|
|
||||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getUTCDate()).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)
|
||||||
@@ -1098,16 +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 = isNewPPR
|
pprData.eta = combineUtcDateTimeInput(dateStr, timeStr);
|
||||||
? new Date(`${dateStr}T${timeStr}`).toISOString()
|
|
||||||
: utcInputToIso(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 = isNewPPR
|
pprData.etd = combineUtcDateTimeInput(dateStr, timeStr);
|
||||||
? new Date(`${dateStr}T${timeStr}`).toISOString()
|
|
||||||
: utcInputToIso(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;
|
||||||
@@ -1869,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 = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user