Reporting and TZ updates
This commit is contained in:
+391
-89
@@ -155,6 +155,14 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable-row:hover {
|
||||
background-color: #eef6ff;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
@@ -311,6 +319,98 @@
|
||||
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 */
|
||||
@media (max-width: 768px) {
|
||||
.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-value" style="font-size: 1.1rem;" id="non-ppr-departures">0</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-value" style="font-size: 1.1rem;" id="overflights-count">0</div>
|
||||
</div>
|
||||
@@ -524,14 +624,12 @@
|
||||
<th>Callsign</th>
|
||||
<th>Captain</th>
|
||||
<th>From</th>
|
||||
<th>ETA</th>
|
||||
<th>POB In</th>
|
||||
<th>To</th>
|
||||
<th>ETD</th>
|
||||
<th>Takeoff</th>
|
||||
<th>Landing</th>
|
||||
<th>POB In</th>
|
||||
<th>POB Out</th>
|
||||
<th>Fuel</th>
|
||||
<th>Landed</th>
|
||||
<th>Departed</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Notes</th>
|
||||
@@ -582,8 +680,8 @@
|
||||
<th>Callsign</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>ETA / ETD / Called</th>
|
||||
<th>Landed / Departed / QSY</th>
|
||||
<th>Takeoff</th>
|
||||
<th>Landing</th>
|
||||
<th>Circuits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -600,6 +698,22 @@
|
||||
</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 -->
|
||||
<div id="notification" class="notification"></div>
|
||||
|
||||
@@ -699,10 +813,10 @@
|
||||
// Set date range to this week (Monday to Sunday)
|
||||
function setDateRangeThisWeek() {
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getDay();
|
||||
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
|
||||
const monday = new Date(now.setDate(diff));
|
||||
const sunday = new Date(now.setDate(diff + 6));
|
||||
const dayOfWeek = now.getUTCDay();
|
||||
const diff = now.getUTCDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
|
||||
const monday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), diff));
|
||||
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-to').value = sunday.toISOString().split('T')[0];
|
||||
@@ -717,8 +831,8 @@
|
||||
// Set date range to this month
|
||||
function setDateRangeThisMonth() {
|
||||
const now = new Date();
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
const firstDay = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||
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-to').value = lastDay.toISOString().split('T')[0];
|
||||
@@ -871,9 +985,12 @@
|
||||
const arrivals = await arrivalsResponse.json();
|
||||
otherFlights.push(...arrivals.map(f => ({
|
||||
...f,
|
||||
entityType: 'ARRIVAL',
|
||||
flightType: 'ARRIVAL',
|
||||
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,
|
||||
toField: 'EGFH'
|
||||
})));
|
||||
@@ -883,9 +1000,12 @@
|
||||
const departures = await departuresResponse.json();
|
||||
otherFlights.push(...departures.map(f => ({
|
||||
...f,
|
||||
entityType: 'DEPARTURE',
|
||||
flightType: 'DEPARTURE',
|
||||
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',
|
||||
toField: f.out_to
|
||||
})));
|
||||
@@ -895,10 +1015,13 @@
|
||||
const localFlights = await localFlightsResponse.json();
|
||||
otherFlights.push(...localFlights.map(f => ({
|
||||
...f,
|
||||
entityType: 'LOCAL_FLIGHT',
|
||||
flightType: f.flight_type === 'CIRCUITS' ? 'CIRCUIT' : f.flight_type,
|
||||
aircraft_type: f.type,
|
||||
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',
|
||||
toField: 'EGFH'
|
||||
})));
|
||||
@@ -908,10 +1031,13 @@
|
||||
const overflights = await overflightsResponse.json();
|
||||
otherFlights.push(...overflights.map(f => ({
|
||||
...f,
|
||||
entityType: 'OVERFLIGHT',
|
||||
flightType: 'OVERFLIGHT',
|
||||
aircraft_type: f.type,
|
||||
circuits: null,
|
||||
timeField: f.call_dt,
|
||||
sortTime: f.call_dt,
|
||||
takeoffTime: null,
|
||||
landingTime: null,
|
||||
fromField: f.departure_airfield,
|
||||
toField: f.destination_airfield,
|
||||
callsign: f.registration
|
||||
@@ -959,21 +1085,14 @@
|
||||
let dateRangeText = '';
|
||||
if (dateFrom && dateTo && dateFrom === dateTo) {
|
||||
// Single day
|
||||
const date = new Date(dateFrom + 'T00:00:00Z');
|
||||
dateRangeText = `for ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
|
||||
dateRangeText = `for ${formatDateOnly(dateFrom)}`;
|
||||
} else if (dateFrom && dateTo) {
|
||||
// Date range
|
||||
const fromDate = new Date(dateFrom + 'T00:00:00Z');
|
||||
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}`;
|
||||
dateRangeText = `for ${formatDateOnly(dateFrom)} to ${formatDateOnly(dateTo)}`;
|
||||
} else if (dateFrom) {
|
||||
const date = new Date(dateFrom + 'T00:00:00Z');
|
||||
dateRangeText = `from ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
|
||||
dateRangeText = `from ${formatDateOnly(dateFrom)}`;
|
||||
} else if (dateTo) {
|
||||
const date = new Date(dateTo + 'T00:00:00Z');
|
||||
dateRangeText = `until ${date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}`;
|
||||
dateRangeText = `until ${formatDateOnly(dateTo)}`;
|
||||
}
|
||||
|
||||
// Update summary title with date range
|
||||
@@ -1045,11 +1164,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by ETA (ascending)
|
||||
// Sort by first actual movement time, then planned times as a fallback.
|
||||
pprs.sort((a, b) => {
|
||||
if (!a.eta) return 1;
|
||||
if (!b.eta) return -1;
|
||||
return new Date(a.eta) - new Date(b.eta);
|
||||
const aTime = getPPRSortTime(a);
|
||||
const bTime = getPPRSortTime(b);
|
||||
if (!aTime) return 1;
|
||||
if (!bTime) return -1;
|
||||
return parseUtcDate(aTime) - parseUtcDate(bTime);
|
||||
});
|
||||
|
||||
tbody.innerHTML = '';
|
||||
@@ -1057,12 +1178,11 @@
|
||||
|
||||
for (const ppr of pprs) {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'clickable-row';
|
||||
row.onclick = () => openReportDetail('PPR', ppr.id);
|
||||
|
||||
// Format dates
|
||||
const eta = ppr.eta ? formatDateTime(ppr.eta) : '-';
|
||||
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 takeoff = ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '-';
|
||||
const landing = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
|
||||
const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-';
|
||||
|
||||
// Status styling
|
||||
@@ -1076,14 +1196,12 @@
|
||||
<td>${ppr.ac_call || '-'}</td>
|
||||
<td>${ppr.captain}</td>
|
||||
<td>${ppr.in_from}</td>
|
||||
<td>${eta}</td>
|
||||
<td>${ppr.pob_in}</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.fuel || '-'}</td>
|
||||
<td>${landed}</td>
|
||||
<td>${departed}</td>
|
||||
<td>${ppr.email || '-'}</td>
|
||||
<td>${ppr.phone || '-'}</td>
|
||||
<td>${ppr.notes || '-'}</td>
|
||||
@@ -1176,10 +1294,10 @@
|
||||
const tbody = document.getElementById('other-flights-table-body');
|
||||
const tableInfo = document.getElementById('other-flights-info');
|
||||
|
||||
// Apply filter if one is selected
|
||||
let filteredFlights = flights;
|
||||
// Overflights are counted in the summary but omitted from the detail table for now.
|
||||
let filteredFlights = flights.filter(flight => flight.flightType !== 'OVERFLIGHT');
|
||||
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})` : '');
|
||||
@@ -1190,13 +1308,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by time field (ascending)
|
||||
// Sort by the first pertinent movement time.
|
||||
filteredFlights.sort((a, b) => {
|
||||
const aTime = a.timeField;
|
||||
const bTime = b.timeField;
|
||||
const aTime = a.sortTime;
|
||||
const bTime = b.sortTime;
|
||||
if (!aTime) return 1;
|
||||
if (!bTime) return -1;
|
||||
return new Date(aTime) - new Date(bTime);
|
||||
return parseUtcDate(aTime) - parseUtcDate(bTime);
|
||||
});
|
||||
|
||||
tbody.innerHTML = '';
|
||||
@@ -1205,6 +1323,8 @@
|
||||
|
||||
for (const flight of filteredFlights) {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'clickable-row';
|
||||
row.onclick = () => openReportDetail(flight.entityType, flight.id);
|
||||
|
||||
const typeLabel = flight.flightType;
|
||||
const registration = flight.registration || '-';
|
||||
@@ -1212,18 +1332,8 @@
|
||||
const callsign = flight.callsign || '-';
|
||||
const from = flight.fromField || '-';
|
||||
const to = flight.toField || '-';
|
||||
const timeDisplay = flight.timeField ? formatDateTime(flight.timeField) : '-';
|
||||
|
||||
// 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 takeoff = flight.takeoffTime ? formatDateTime(flight.takeoffTime) : '-';
|
||||
const landing = flight.landingTime ? formatDateTime(flight.landingTime) : '-';
|
||||
|
||||
const status = flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING');
|
||||
const circuits = (flight.flightType === 'CIRCUIT' || flight.flightType === 'LOCAL') ? (flight.circuits > 0 ? flight.circuits : '-') : '-';
|
||||
@@ -1236,8 +1346,8 @@
|
||||
<td>${callsign}</td>
|
||||
<td>${from}</td>
|
||||
<td>${to}</td>
|
||||
<td>${timeDisplay}</td>
|
||||
<td>${actualDisplay}</td>
|
||||
<td>${takeoff}</td>
|
||||
<td>${landing}</td>
|
||||
<td>${circuits}</td>
|
||||
`;
|
||||
|
||||
@@ -1247,23 +1357,201 @@
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
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')) {
|
||||
utcDateStr = utcDateStr.replace(' ', 'T');
|
||||
}
|
||||
if (!utcDateStr.includes('Z')) {
|
||||
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
|
||||
utcDateStr += 'Z';
|
||||
}
|
||||
const date = new Date(utcDateStr);
|
||||
|
||||
// Format as dd/mm/yy hh:mm
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
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}`;
|
||||
return new Date(utcDateStr);
|
||||
}
|
||||
|
||||
function getPPRSortTime(ppr) {
|
||||
return ppr.landed_dt || ppr.departed_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1283,8 +1571,8 @@
|
||||
|
||||
const headers = [
|
||||
'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain',
|
||||
'From', 'ETA', 'POB In', 'To', 'ETD', 'POB Out', 'Fuel',
|
||||
'Landed', 'Departed', 'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
|
||||
'From', 'To', 'Takeoff', 'Landing', 'POB In', 'POB Out', 'Fuel',
|
||||
'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
|
||||
];
|
||||
|
||||
const csvData = currentPPRs.map(ppr => [
|
||||
@@ -1295,14 +1583,12 @@
|
||||
ppr.ac_call || '',
|
||||
ppr.captain,
|
||||
ppr.in_from,
|
||||
ppr.eta ? formatDateTime(ppr.eta) : '',
|
||||
ppr.pob_in,
|
||||
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.fuel || '',
|
||||
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
|
||||
ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '',
|
||||
ppr.email || '',
|
||||
ppr.phone || '',
|
||||
ppr.notes || '',
|
||||
@@ -1319,22 +1605,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const exportFlights = currentOtherFlights.filter(flight => flight.flightType !== 'OVERFLIGHT');
|
||||
if (exportFlights.length === 0) {
|
||||
showNotification('No table data to export', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = [
|
||||
'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.registration || '',
|
||||
flight.aircraft_type || '',
|
||||
flight.callsign || '',
|
||||
flight.fromField || '',
|
||||
flight.toField || '',
|
||||
flight.timeField ? formatDateTime(flight.timeField) : '',
|
||||
flight.flightType === 'ARRIVAL'
|
||||
? (flight.landed_dt ? formatDateTime(flight.landed_dt) : '')
|
||||
: (flight.departed_dt ? formatDateTime(flight.departed_dt) : ''),
|
||||
flight.takeoffTime ? formatDateTime(flight.takeoffTime) : '',
|
||||
flight.landingTime ? formatDateTime(flight.landingTime) : '',
|
||||
flight.status || (flight.flightType === 'CIRCUIT' ? 'COMPLETED' : 'PENDING'),
|
||||
(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
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user