Drone flights and Bulk Logging WIPs

This commit is contained in:
2026-06-19 17:27:33 -04:00
parent 1952b89ecf
commit 78d738b0ee
18 changed files with 2051 additions and 70 deletions
+204 -11
View File
@@ -266,6 +266,85 @@
font-size: 0.85rem;
}
.match-strips {
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-top: 0.55rem;
}
.match-strip-preview {
background: transparent;
border: 0;
display: block;
padding: 0;
text-align: left;
}
.match-strip {
display: inline-block;
min-width: 760px;
padding: 0.75rem;
}
.match-strip .bulk-field {
min-width: 0;
}
.match-strip .strip-value {
background: rgba(255,255,255,0.35);
border-radius: 4px;
color: #111;
font-size: 0.95rem;
font-weight: 700;
min-height: 2rem;
padding: 0.45rem 0.55rem;
text-transform: uppercase;
}
.match-strip .callsign-preview {
align-items: center;
justify-content: center;
}
.match-strip .callsign-preview label {
display: none;
}
.match-strip .callsign-preview .strip-value {
align-items: center;
background: transparent;
display: flex;
font-size: 1.2rem;
justify-content: center;
min-height: 100%;
padding: 0;
text-align: center;
}
.match-strip.strip-kind-local {
padding: 0;
}
.match-strip.strip-kind-local .strip-value {
background: rgba(255,255,255,0.28);
border-radius: 0;
min-height: 0;
padding: 0.2rem;
}
.match-strip.strip-kind-local .strip-registration .strip-value {
background: rgba(255,255,255,0.55);
outline: 2px solid rgba(0,0,0,0.2);
width: 100%;
}
.match-strip.strip-kind-local .strip-registration.callsign-preview .strip-value {
background: transparent;
font-size: 1.35rem;
outline: 0;
}
.movement-table table {
min-width: 1050px;
}
@@ -640,6 +719,10 @@
function applySuggestion(suggestion) {
if (!suggestion.source) return;
if (document.getElementById('flight-kind').value === 'LOCAL') {
fillIfEmpty('aircraft-type', suggestion.aircraft_type);
return;
}
if (suggestion.movement_id) document.getElementById('matched-movement-id').value = suggestion.movement_id;
if (suggestion.ppr_id) document.getElementById('matched-ppr-id').value = suggestion.ppr_id;
fillSuggestedTime(suggestion.movement_time);
@@ -675,30 +758,138 @@
function renderContext(context) {
const panel = document.getElementById('context-panel');
const pprs = context.pprs || [];
const localFlights = context.local_flights || [];
const movements = context.movements || [];
if (!pprs.length && !movements.length) {
if (!pprs.length && !localFlights.length) {
hideContext();
return;
}
panel.className = `context-panel ${movements.length ? 'warning' : ''}`;
panel.className = 'context-panel';
const parts = [];
if (movements.length) {
parts.push(`<strong>Existing movement found.</strong> Saving will update it instead of adding another.`);
parts.push(movements.map(m => `<span class="match-pill">${formatTime(m.timestamp)} ${m.movement_type} #${m.id}</span>`).join(''));
}
if (pprs.length) {
parts.push(`<strong>Matching PPR${pprs.length > 1 ? 's' : ''}:</strong>`);
parts.push(pprs.map(p => {
const encoded = encodeURIComponent(JSON.stringify(p));
return `<button type="button" class="match-pill" onclick='selectPPR("${encoded}")'>#${p.id} ${p.aircraft_registration} ${p.from_location || ''}${p.to_location ? ' to ' + p.to_location : ''}</button>`;
}).join(''));
parts.push(renderPPRMatchStrips(pprs));
}
if (localFlights.length) {
parts.push(`<strong>Existing local flight${localFlights.length > 1 ? 's' : ''}:</strong>`);
parts.push(renderLocalFlightMatchStrips(localFlights));
}
panel.innerHTML = parts.join('<br>');
panel.style.display = 'block';
}
function renderPPRMatchStrips(pprs) {
const kind = document.getElementById('flight-kind').value;
return `<div class="match-strips">${pprs.map(ppr => renderPPRMatchStrip(ppr, kind)).join('')}</div>`;
}
function renderLocalFlightMatchStrips(localFlights) {
return `<div class="match-strips">${localFlights.map(local => renderLocalFlightMatchStrip(local)).join('')}</div>`;
}
function renderPPRMatchStrip(ppr, kind) {
return `
<div class="match-strip-preview" aria-label="Matching PPR ${escapeHtml(ppr.id)} for ${escapeHtml(ppr.aircraft_registration || 'aircraft')}">
<div class="virtual-strip match-strip ${stripClassForKind(kind)} strip-kind-${kind.toLowerCase()}">
${kind === 'LOCAL' ? renderLocalPPRStrip(ppr) : renderStandardPPRStrip(ppr, kind)}
</div>
</div>
`;
}
function renderLocalFlightMatchStrip(local) {
return `
<div class="match-strip-preview" aria-label="Existing local flight ${escapeHtml(local.id)} for ${escapeHtml(local.aircraft_registration || 'aircraft')}">
<div class="virtual-strip match-strip strip-pink strip-kind-local">
${renderExistingLocalStrip(local)}
</div>
</div>
`;
}
function renderStandardPPRStrip(ppr, kind) {
const isArrival = kind === 'ARRIVAL';
const isDeparture = kind === 'DEPARTURE';
const isOverflight = kind === 'OVERFLIGHT';
const pob = kind === 'ARRIVAL' ? ppr.pob_in : (ppr.pob_out || ppr.pob_in);
return `
<div class="bulk-grid">
${stripField('Registration', ppr.aircraft_registration)}
${isArrival ? stripField('LDG', compactIsoTime(ppr.eta)) : ''}
${isDeparture ? stripField('T/O', compactIsoTime(ppr.etd)) : ''}
${isOverflight ? stripField('Contact time', '') : ''}
${isOverflight ? stripField('QSY time', '') : ''}
${stripField('Type', ppr.aircraft_type)}
${stripField('C/SIGN', ppr.callsign || ppr.aircraft_registration, 'callsign-preview')}
${['ARRIVAL', 'OVERFLIGHT'].includes(kind) ? stripField('From', ppr.from_location) : ''}
${['DEPARTURE', 'OVERFLIGHT'].includes(kind) ? stripField('To', ppr.to_location) : ''}
${stripField('POB', pob)}
${['ARRIVAL', 'DEPARTURE'].includes(kind) ? stripField('Runway', '') : ''}
${stripField('Wind', '')}
${stripField('Pressure', '')}
${stripField('Notes', ppr.notes, 'notes-field')}
</div>
`;
}
function renderLocalPPRStrip(ppr) {
return `
<div class="bulk-grid">
<div class="bulk-field strip-field local-slash-cell"></div>
${stripField('LOC / CCTS', 'Local', 'local-nature-cell')}
${stripField('Type', ppr.aircraft_type, 'type-cell')}
${stripField('C/SIGN', ppr.callsign || ppr.aircraft_registration, 'strip-registration callsign-preview')}
${stripField('T/O', compactIsoTime(ppr.etd), 'local-to-cell')}
${stripField('LDG', compactIsoTime(ppr.eta), 'local-ldg-cell')}
${stripField('CCTS', '', 'local-circuit-cell')}
${stripField('POB', ppr.pob_out || ppr.pob_in, 'pob-cell')}
${stripField('Notes', ppr.notes, 'notes-field')}
</div>
`;
}
function renderExistingLocalStrip(local) {
return `
<div class="bulk-grid">
<div class="bulk-field strip-field local-slash-cell"></div>
${stripField('LOC / CCTS', local.flight_type || 'Local', 'local-nature-cell')}
${stripField('Type', local.aircraft_type, 'type-cell')}
${stripField('C/SIGN', local.callsign || local.aircraft_registration, 'strip-registration callsign-preview')}
${stripField('T/O', compactIsoTime(local.takeoff_time || local.departed_time || local.etd), 'local-to-cell')}
${stripField('LDG', compactIsoTime(local.landing_time), 'local-ldg-cell')}
${stripField('CCTS', local.circuits, 'local-circuit-cell')}
${stripField('POB', local.pob, 'pob-cell')}
${stripField('Notes', local.notes, 'notes-field')}
</div>
`;
}
function stripField(label, value, extraClass = '') {
return `
<div class="bulk-field ${extraClass}">
<label>${escapeHtml(label)}</label>
<div class="strip-value">${escapeHtml(value || '')}</div>
</div>
`;
}
function compactIsoTime(value) {
if (!value) return '';
return compactTime(value.slice(11, 16));
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
}
function hideContext() {
const panel = document.getElementById('context-panel');
panel.style.display = 'none';
@@ -717,6 +908,8 @@
document.getElementById('pob').value = kind === 'ARRIVAL' ? (ppr.pob_in || '') : (ppr.pob_out || ppr.pob_in || '');
if (kind === 'ARRIVAL' && ppr.eta) document.getElementById('landing-time').value = compactTime(ppr.eta.slice(11, 16));
if (kind === 'DEPARTURE' && ppr.etd) document.getElementById('takeoff-time').value = compactTime(ppr.etd.slice(11, 16));
if (kind === 'LOCAL' && ppr.etd) document.getElementById('local-takeoff-time').value = compactTime(ppr.etd.slice(11, 16));
if (kind === 'LOCAL' && ppr.eta) document.getElementById('local-landing-time').value = compactTime(ppr.eta.slice(11, 16));
document.getElementById('notes').value = ppr.notes || '';
}
@@ -745,7 +938,7 @@
pressure_setting: nullableValue('pressure-setting'),
notes: nullableValue('notes'),
ppr_id: nullableNumber('matched-ppr-id'),
movement_id: nullableNumber('matched-movement-id')
movement_id: kind === 'LOCAL' ? null : nullableNumber('matched-movement-id')
};
const response = await authFetch(`${API_BASE}/movements/bulk-log`, {