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
+334
View File
@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drone Flight Request</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #f5f5f5;
color: #263645;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.5;
}
.container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin: 2rem auto;
max-width: 900px;
padding: 2rem;
}
.header {
border-bottom: 2px solid #3498db;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
}
.header h1 { color: #2c3e50; margin-bottom: 0.4rem; }
.status {
border-radius: 999px;
color: white;
display: inline-flex;
font-size: 0.8rem;
font-weight: 700;
margin-top: 0.5rem;
padding: 0.3rem 0.7rem;
}
.status-NEW { background: #3498db; }
.status-APPROVED { background: #27ae60; }
.status-DENIED { background: #c0392b; }
.status-PENDING { background: #f39c12; }
.status-CANCELED { background: #7f8c8d; }
.status-INFLIGHT { background: #8e44ad; }
.status-COMPLETED { background: #2c3e50; }
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.full { grid-column: 1 / -1; }
label {
color: #555;
display: block;
font-weight: 600;
margin-bottom: 0.25rem;
}
input, textarea {
border: 1px solid #d6dce2;
border-radius: 5px;
font: inherit;
padding: 0.65rem;
width: 100%;
}
textarea { min-height: 110px; resize: vertical; }
.actions {
border-top: 1px solid #e6e9ec;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
}
.btn {
border: 0;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
padding: 0.8rem 1.4rem;
}
.btn-primary { background: #3498db; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn-secondary { background: #95a5a6; color: white; }
.message {
border-radius: 5px;
display: none;
margin-bottom: 1rem;
padding: 0.85rem 1rem;
}
.message.ok { background: #d4edda; color: #155724; display: block; }
.message.error { background: #f8d7da; color: #721c24; display: block; }
.read-only {
background: #eef1f4;
border-radius: 5px;
color: #607080;
margin-bottom: 1rem;
padding: 0.85rem 1rem;
}
@media (max-width: 720px) {
.container { margin: 0; min-height: 100vh; border-radius: 0; }
.grid { grid-template-columns: 1fr; }
.full { grid-column: auto; }
}
</style>
</head>
<body>
<main class="container">
<div class="header">
<h1>Drone Flight Request</h1>
<p id="summary">Loading request...</p>
<span id="status" class="status status-NEW" style="display: none;"></span>
</div>
<div id="message" class="message"></div>
<div id="locked" class="read-only" style="display: none;">This request can be viewed, but can no longer be edited or cancelled.</div>
<form id="request-form" style="display: none;">
<div class="grid">
<div>
<label for="operator_name">Operator name</label>
<input id="operator_name" required>
</div>
<div>
<label for="operator_id">Operator ID</label>
<input id="operator_id">
</div>
<div>
<label for="flyer_name">Flyer name</label>
<input id="flyer_name">
</div>
<div>
<label for="flyer_id">Flyer ID</label>
<input id="flyer_id">
</div>
<div>
<label for="email">Email</label>
<input id="email" type="email" required>
</div>
<div>
<label for="phone">Phone</label>
<input id="phone">
</div>
<div>
<label for="estimated_takeoff_at">Estimated takeoff</label>
<input id="estimated_takeoff_at" type="datetime-local" required>
</div>
<div>
<label for="estimated_completion_at">Estimated completion</label>
<input id="estimated_completion_at" type="datetime-local" required>
</div>
<div>
<label for="maximum_elevation_ft_amsl">Maximum elevation ft AMSL</label>
<input id="maximum_elevation_ft_amsl" type="number" min="0" required>
</div>
<div>
<label for="location_inside_frz">Inside FRZ</label>
<input id="location_inside_frz" readonly>
</div>
<div>
<label for="location_latitude">Latitude</label>
<input id="location_latitude" type="number" step="0.000001" required>
</div>
<div>
<label for="location_longitude">Longitude</label>
<input id="location_longitude" type="number" step="0.000001" required>
</div>
<div class="full">
<label for="location_description">Location description</label>
<input id="location_description">
</div>
<div class="full">
<label for="notes">Notes</label>
<textarea id="notes"></textarea>
</div>
<div class="full">
<label for="operator_comments">Airport comments</label>
<textarea id="operator_comments" readonly></textarea>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" id="save-btn" type="submit">Save Changes</button>
<button class="btn btn-danger" id="cancel-btn" type="button" onclick="cancelRequest()">Cancel Request</button>
<button class="btn btn-secondary" type="button" onclick="loadRequest()">Reload</button>
</div>
</form>
</main>
<script>
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
let currentRequest = null;
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('request-form').addEventListener('submit', saveRequest);
if (!token) {
showMessage('Missing secure request token.', true);
return;
}
loadRequest();
});
async function loadRequest() {
try {
const response = await fetch(`/api/v1/drone-requests/public/edit/${encodeURIComponent(token)}`);
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Unable to load request');
currentRequest = data;
populateForm(data);
showMessage('', false, true);
} catch (err) {
showMessage(err.message, true);
}
}
function populateForm(request) {
document.getElementById('summary').textContent = `${request.reference_number} - ${request.operator_name}`;
const status = document.getElementById('status');
status.textContent = request.status;
status.className = `status status-${request.status}`;
status.style.display = 'inline-flex';
setValue('operator_name', request.operator_name);
setValue('operator_id', request.operator_id);
setValue('flyer_name', request.flyer_name);
setValue('flyer_id', request.flyer_id);
setValue('email', request.email);
setValue('phone', request.phone);
setValue('estimated_takeoff_at', toLocalInputValue(request.estimated_takeoff_at));
setValue('estimated_completion_at', toLocalInputValue(request.estimated_completion_at));
setValue('maximum_elevation_ft_amsl', request.maximum_elevation_ft_amsl);
setValue('location_inside_frz', request.location_inside_frz ? 'Yes' : 'No');
setValue('location_latitude', request.location_latitude);
setValue('location_longitude', request.location_longitude);
setValue('location_description', request.location_description);
setValue('notes', request.notes);
setValue('operator_comments', request.operator_comments);
const locked = !['NEW', 'PENDING', 'APPROVED'].includes(request.status);
document.getElementById('locked').style.display = locked ? 'block' : 'none';
document.getElementById('save-btn').disabled = locked;
document.getElementById('cancel-btn').disabled = locked;
document.querySelectorAll('#request-form input, #request-form textarea').forEach(input => {
if (input.id !== 'operator_comments' && input.id !== 'location_inside_frz') {
input.readOnly = locked;
}
});
document.getElementById('request-form').style.display = 'block';
}
async function saveRequest(event) {
event.preventDefault();
const payload = {
operator_name: value('operator_name'),
operator_id: value('operator_id') || null,
flyer_name: value('flyer_name') || null,
flyer_id: value('flyer_id') || null,
email: value('email'),
phone: value('phone') || null,
estimated_takeoff_at: fromLocalInputValue(value('estimated_takeoff_at')),
estimated_completion_at: fromLocalInputValue(value('estimated_completion_at')),
maximum_elevation_ft_amsl: Number(value('maximum_elevation_ft_amsl')),
location_latitude: Number(value('location_latitude')),
location_longitude: Number(value('location_longitude')),
location_description: value('location_description') || null,
notes: value('notes') || null
};
try {
const response = await fetch(`/api/v1/drone-requests/public/edit/${encodeURIComponent(token)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Unable to save request');
currentRequest = data;
populateForm(data);
showMessage('Request updated.');
} catch (err) {
showMessage(err.message, true);
}
}
async function cancelRequest() {
if (!confirm('Cancel this drone flight request?')) return;
try {
const response = await fetch(`/api/v1/drone-requests/public/cancel/${encodeURIComponent(token)}`, {
method: 'DELETE'
});
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Unable to cancel request');
currentRequest = data;
populateForm(data);
showMessage('Request cancelled.');
} catch (err) {
showMessage(err.message, true);
}
}
function setValue(id, value) {
document.getElementById(id).value = value == null ? '' : value;
}
function value(id) {
return document.getElementById(id).value.trim();
}
function toLocalInputValue(value) {
if (!value) return '';
const normalized = value.includes('T') ? value : value.replace(' ', 'T');
const date = new Date(normalized.endsWith('Z') ? normalized : `${normalized}Z`);
return date.toISOString().slice(0, 16);
}
function fromLocalInputValue(value) {
return new Date(value).toISOString();
}
function showMessage(message, isError = false, clear = false) {
const element = document.getElementById('message');
if (clear || !message) {
element.textContent = '';
element.className = 'message';
return;
}
element.textContent = message;
element.className = `message ${isError ? 'error' : 'ok'}`;
}
</script>
</body>
</html>