Email notification

This commit is contained in:
James Pattinson
2025-10-25 13:31:03 +00:00
parent 91e820b9a8
commit d5f05941c9
9 changed files with 880 additions and 3 deletions

View File

@@ -8,6 +8,8 @@ from app.crud.crud_journal import journal as crud_journal
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal
from app.models.ppr import User from app.models.ppr import User
from app.core.utils import get_client_ip from app.core.utils import get_client_ip
from app.core.email import email_service
from app.core.config import settings
router = APIRouter() router = APIRouter()
@@ -78,6 +80,23 @@ async def create_public_ppr(
} }
}) })
# Send email if email provided
if ppr_in.email:
await email_service.send_email(
to_email=ppr_in.email,
subject="PPR Submitted Successfully",
template_name="ppr_submitted.html",
template_vars={
"name": ppr_in.captain,
"aircraft": ppr_in.ac_reg,
"arrival_time": ppr_in.eta.strftime("%Y-%m-%d %H:%M"),
"departure_time": ppr_in.etd.strftime("%Y-%m-%d %H:%M") if ppr_in.etd else "N/A",
"purpose": ppr_in.notes or "N/A",
"public_token": ppr.public_token,
"base_url": settings.base_url
}
)
return ppr return ppr
@@ -199,6 +218,20 @@ async def update_ppr_status(
} }
}) })
# Send email if cancelled and email provided
if status_update.status == PPRStatus.CANCELED and ppr.email:
await email_service.send_email(
to_email=ppr.email,
subject="PPR Cancelled",
template_name="ppr_cancelled.html",
template_vars={
"name": ppr.captain,
"aircraft": ppr.ac_reg,
"arrival_time": ppr.eta.strftime("%Y-%m-%d %H:%M"),
"departure_time": ppr.etd.strftime("%Y-%m-%d %H:%M") if ppr.etd else "N/A"
}
)
return ppr return ppr
@@ -231,6 +264,100 @@ async def delete_ppr(
return ppr return ppr
@router.get("/public/edit/{token}", response_model=PPR)
async def get_ppr_for_edit(
token: str,
db: Session = Depends(get_db)
):
"""Get PPR details for public editing using token"""
ppr = crud_ppr.get_by_public_token(db, token)
if not ppr:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid or expired token"
)
# Only allow editing if not already processed
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be edited at this stage"
)
return ppr
@router.patch("/public/edit/{token}", response_model=PPR)
async def update_ppr_public(
token: str,
ppr_in: PPRUpdate,
request: Request,
db: Session = Depends(get_db)
):
"""Update PPR publicly using token"""
ppr = crud_ppr.get_by_public_token(db, token)
if not ppr:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid or expired token"
)
# Only allow editing if not already processed
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be edited at this stage"
)
client_ip = get_client_ip(request)
updated_ppr = crud_ppr.update(db, db_obj=ppr, obj_in=ppr_in, user="public", user_ip=client_ip)
return updated_ppr
@router.delete("/public/cancel/{token}", response_model=PPR)
async def cancel_ppr_public(
token: str,
request: Request,
db: Session = Depends(get_db)
):
"""Cancel PPR publicly using token"""
ppr = crud_ppr.get_by_public_token(db, token)
if not ppr:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid or expired token"
)
# Only allow canceling if not already processed
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be cancelled at this stage"
)
client_ip = get_client_ip(request)
# Cancel by setting status to CANCELED
cancelled_ppr = crud_ppr.update_status(
db,
ppr_id=ppr.id,
status=PPRStatus.CANCELED,
user="public",
user_ip=client_ip
)
# Send cancellation email if email provided
if cancelled_ppr.email:
await email_service.send_email(
to_email=cancelled_ppr.email,
subject="PPR Cancelled",
template_name="ppr_cancelled.html",
template_vars={
"name": cancelled_ppr.captain,
"aircraft": cancelled_ppr.ac_reg,
"arrival_time": cancelled_ppr.eta.strftime("%Y-%m-%d %H:%M"),
"departure_time": cancelled_ppr.etd.strftime("%Y-%m-%d %H:%M") if cancelled_ppr.etd else "N/A"
}
)
return cancelled_ppr
@router.get("/{ppr_id}/journal", response_model=List[Journal]) @router.get("/{ppr_id}/journal", response_model=List[Journal])
async def get_ppr_journal( async def get_ppr_journal(
ppr_id: int, ppr_id: int,

48
backend/app/core/email.py Normal file
View File

@@ -0,0 +1,48 @@
import aiosmtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from jinja2 import Environment, FileSystemLoader
import os
from app.core.config import settings
class EmailService:
def __init__(self):
self.smtp_host = settings.mail_host
self.smtp_port = settings.mail_port
self.smtp_user = settings.mail_username
self.smtp_password = settings.mail_password
self.from_email = settings.mail_from
self.from_name = settings.mail_from_name
# Set up Jinja2 environment for templates
template_dir = os.path.join(os.path.dirname(__file__), '..', 'templates')
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
async def send_email(self, to_email: str, subject: str, template_name: str, template_vars: dict):
# Render the template
template = self.jinja_env.get_template(template_name)
html_content = template.render(**template_vars)
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = f"{self.from_name} <{self.from_email}>"
msg['To'] = to_email
# Attach HTML content
html_part = MIMEText(html_content, 'html')
msg.attach(html_part)
# Send email
try:
async with aiosmtplib.SMTP(hostname=self.smtp_host, port=self.smtp_port, use_tls=True) as smtp:
await smtp.login(self.smtp_user, self.smtp_password)
await smtp.send_message(msg)
except Exception as e:
# Log error, but for now, print
print(f"Failed to send email: {e}")
# In production, use logging
email_service = EmailService()

View File

@@ -2,6 +2,7 @@ from typing import List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func, desc from sqlalchemy import and_, or_, func, desc
from datetime import date, datetime from datetime import date, datetime
import secrets
from app.models.ppr import PPRRecord, PPRStatus from app.models.ppr import PPRRecord, PPRStatus
from app.schemas.ppr import PPRCreate, PPRUpdate from app.schemas.ppr import PPRCreate, PPRUpdate
from app.crud.crud_journal import journal as crud_journal from app.crud.crud_journal import journal as crud_journal
@@ -11,6 +12,9 @@ class CRUDPPR:
def get(self, db: Session, ppr_id: int) -> Optional[PPRRecord]: def get(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
return db.query(PPRRecord).filter(PPRRecord.id == ppr_id).first() return db.query(PPRRecord).filter(PPRRecord.id == ppr_id).first()
def get_by_public_token(self, db: Session, token: str) -> Optional[PPRRecord]:
return db.query(PPRRecord).filter(PPRRecord.public_token == token).first()
def get_multi( def get_multi(
self, self,
db: Session, db: Session,
@@ -67,7 +71,8 @@ class CRUDPPR:
db_obj = PPRRecord( db_obj = PPRRecord(
**obj_in.dict(), **obj_in.dict(),
created_by=created_by, created_by=created_by,
status=PPRStatus.NEW status=PPRStatus.NEW,
public_token=secrets.token_urlsafe(64)
) )
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()

View File

@@ -42,6 +42,7 @@ class PPRRecord(Base):
departed_dt = Column(DateTime, nullable=True) departed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True) created_by = Column(String(16), nullable=True)
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp()) submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp())
public_token = Column(String(128), nullable=True, unique=True)
class User(Base): class User(Base):

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>PPR Cancelled</title>
</head>
<body>
<h1>PPR Cancelled</h1>
<p>Dear {{ name }},</p>
<p>Your Prior Permission Request (PPR) has been cancelled.</p>
<p><strong>PPR Details:</strong></p>
<ul>
<li>Aircraft: {{ aircraft }}</li>
<li>Original Arrival: {{ arrival_time }}</li>
<li>Original Departure: {{ departure_time }}</li>
</ul>
<p>If this was not intended, please contact us.</p>
<p>Best regards,<br>Swansea Airport Team</p>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>PPR Submitted</title>
</head>
<body>
<h1>PPR Submitted Successfully</h1>
<p>Dear {{ name }},</p>
<p>Your Prior Permission Request (PPR) has been submitted.</p>
<p><strong>PPR Details:</strong></p>
<ul>
<li>Aircraft: {{ aircraft }}</li>
<li>Arrival: {{ arrival_time }}</li>
<li>Departure: {{ departure_time }}</li>
<li>Purpose: {{ purpose }}</li>
</ul>
<p>You can <a href="{{ base_url }}/edit?token={{ public_token }}">edit or cancel</a> your PPR using this secure link.</p>
<p>You will receive further updates via email.</p>
<p>Best regards,<br>Swansea Airport Team</p>
</body>
</html>

View File

@@ -15,3 +15,5 @@ pytest==7.4.3
pytest-asyncio==0.21.1 pytest-asyncio==0.21.1
httpx==0.25.2 httpx==0.25.2
redis==5.0.1 redis==5.0.1
aiosmtplib==3.0.1
jinja2==3.1.2

View File

@@ -41,6 +41,7 @@ CREATE TABLE submitted (
departed_dt DATETIME DEFAULT NULL, departed_dt DATETIME DEFAULT NULL,
created_by VARCHAR(16) DEFAULT NULL, created_by VARCHAR(16) DEFAULT NULL,
submitted_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, submitted_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
public_token VARCHAR(128) DEFAULT NULL UNIQUE,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Indexes for better performance -- Indexes for better performance
@@ -49,7 +50,8 @@ CREATE TABLE submitted (
INDEX idx_etd (etd), INDEX idx_etd (etd),
INDEX idx_ac_reg (ac_reg), INDEX idx_ac_reg (ac_reg),
INDEX idx_submitted_dt (submitted_dt), INDEX idx_submitted_dt (submitted_dt),
INDEX idx_created_by (created_by) INDEX idx_created_by (created_by),
INDEX idx_public_token (public_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Activity journal table with foreign key -- Activity journal table with foreign key

652
web/edit.html Normal file
View File

@@ -0,0 +1,652 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Swansea PPR Request</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #3498db;
}
.header h1 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.header p {
color: #666;
font-size: 1.1rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-group label {
font-weight: 600;
margin-bottom: 0.3rem;
color: #555;
}
.form-group input, .form-group select, .form-group textarea {
padding: 0.6rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.required::after {
content: " *";
color: #e74c3c;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: center;
padding-top: 1rem;
border-top: 1px solid #eee;
margin-top: 2rem;
}
.btn {
padding: 0.8rem 2rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
background-color: #27ae60;
color: white;
padding: 12px 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease;
font-weight: 500;
}
.notification.show {
opacity: 1;
transform: translateY(0);
}
.notification.error {
background-color: #e74c3c;
}
.loading {
display: none;
text-align: center;
margin-top: 1rem;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.success-message {
display: none;
text-align: center;
padding: 2rem;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 4px;
color: #155724;
margin-top: 2rem;
}
.airport-lookup-results {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 0.9rem;
min-height: 20px;
border: 1px solid #e9ecef;
}
.aircraft-lookup-results {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 0.9rem;
min-height: 20px;
border: 1px solid #e9ecef;
}
.lookup-match {
padding: 0.3rem;
background-color: #e8f5e8;
border: 1px solid #c3e6c3;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-weight: bold;
}
.lookup-no-match {
color: #6c757d;
font-style: italic;
}
.lookup-searching {
color: #007bff;
}
.aircraft-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: white;
}
.aircraft-option {
padding: 0.5rem;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.aircraft-option:hover {
background-color: #f8f9fa;
}
.aircraft-option:last-child {
border-bottom: none;
}
.aircraft-code {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #495057;
}
.aircraft-details {
color: #6c757d;
font-size: 0.85rem;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>✏️ Edit Swansea PPR Request</h1>
<p>Update your Prior Permission Required (PPR) request details below.</p>
</div>
<form id="ppr-form">
<div class="form-grid">
<div class="form-group">
<label for="ac_reg" class="required">Aircraft Registration</label>
<input type="text" id="ac_reg" name="ac_reg" required oninput="handleAircraftLookup(this.value)">
<div id="aircraft-lookup-results" class="aircraft-lookup-results"></div>
</div>
<div class="form-group">
<label for="ac_type" class="required">Aircraft Type</label>
<input type="text" id="ac_type" name="ac_type" required>
</div>
<div class="form-group">
<label for="ac_call">Callsign</label>
<input type="text" id="ac_call" name="ac_call" placeholder="If different from registration">
</div>
<div class="form-group">
<label for="captain" class="required">Captain/Pilot Name</label>
<input type="text" id="captain" name="captain" required>
</div>
<div class="form-group">
<label for="in_from" class="required">Arriving From</label>
<input type="text" id="in_from" name="in_from" required placeholder="ICAO Code or Airport Name" oninput="handleArrivalAirportLookup(this.value)">
<div id="arrival-airport-lookup-results" class="airport-lookup-results"></div>
</div>
<div class="form-group">
<label for="eta" class="required">Estimated Time of Arrival (Local Time)</label>
<input type="datetime-local" id="eta" name="eta" required>
</div>
<div class="form-group">
<label for="pob_in" class="required">Persons on Board (Arrival)</label>
<input type="number" id="pob_in" name="pob_in" required min="1">
</div>
<div class="form-group">
<label for="fuel">Fuel Required</label>
<select id="fuel" name="fuel">
<option value="">None</option>
<option value="100LL">100LL</option>
<option value="JET A1">JET A1</option>
<option value="FULL">Full Tanks</option>
</select>
</div>
<div class="form-group">
<label for="out_to">Departing To</label>
<input type="text" id="out_to" name="out_to" placeholder="ICAO Code or Airport Name" oninput="handleDepartureAirportLookup(this.value)">
<div id="departure-airport-lookup-results" class="airport-lookup-results"></div>
</div>
<div class="form-group">
<label for="etd">Estimated Time of Departure (Local Time)</label>
<input type="datetime-local" id="etd" name="etd">
</div>
<div class="form-group">
<label for="pob_out">Persons on Board (Departure)</label>
<input type="number" id="pob_out" name="pob_out" min="1">
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email">
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="tel" id="phone" name="phone">
</div>
<div class="form-group full-width">
<label for="notes">Additional Notes</label>
<textarea id="notes" name="notes" rows="4" placeholder="Any special requirements, handling instructions, or additional information..."></textarea>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="update-btn">
Update PPR Request
</button>
<button type="button" class="btn btn-danger" id="cancel-btn">
Cancel PPR Request
</button>
</div>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
Processing your request...
</div>
<div class="success-message" id="success-message">
<h3>✅ PPR Request Updated Successfully!</h3>
<p>Your changes have been saved.</p>
</div>
<div class="success-message" id="cancel-message">
<h3>❌ PPR Request Cancelled</h3>
<p>Your PPR request has been cancelled.</p>
</div>
</div>
<!-- Success Notification -->
<div id="notification" class="notification"></div>
<script>
// Get token from URL
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
alert('Invalid link. No token provided.');
window.location.href = '/';
}
// Notification system
function showNotification(message, isError = false) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = 'notification' + (isError ? ' error' : '');
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
}, 5000);
}
// Load PPR data
async function loadPPRData() {
try {
const response = await fetch(`/api/v1/pprs/public/edit/${token}`);
if (response.ok) {
const ppr = await response.json();
// Populate form
document.getElementById('ac_reg').value = ppr.ac_reg || '';
document.getElementById('ac_type').value = ppr.ac_type || '';
document.getElementById('ac_call').value = ppr.ac_call || '';
document.getElementById('captain').value = ppr.captain || '';
document.getElementById('in_from').value = ppr.in_from || '';
document.getElementById('eta').value = ppr.eta ? new Date(ppr.eta).toISOString().slice(0, 16) : '';
document.getElementById('pob_in').value = ppr.pob_in || '';
document.getElementById('fuel').value = ppr.fuel || '';
document.getElementById('out_to').value = ppr.out_to || '';
document.getElementById('etd').value = ppr.etd ? new Date(ppr.etd).toISOString().slice(0, 16) : '';
document.getElementById('pob_out').value = ppr.pob_out || '';
document.getElementById('email').value = ppr.email || '';
document.getElementById('phone').value = ppr.phone || '';
document.getElementById('notes').value = ppr.notes || '';
} else {
throw new Error('Failed to load PPR data');
}
} catch (error) {
console.error('Error loading PPR:', error);
showNotification('Error loading PPR data', true);
}
}
// Aircraft lookup (same as submit form)
let aircraftLookupTimeout;
async function handleAircraftLookup(registration) {
clearTimeout(aircraftLookupTimeout);
const resultsDiv = document.getElementById('aircraft-lookup-results');
const regField = document.getElementById('ac_reg');
if (!registration || registration.length < 4) {
resultsDiv.innerHTML = '';
return;
}
resultsDiv.innerHTML = '<span class="lookup-searching">Searching...</span>';
aircraftLookupTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/aircraft/public/lookup/${registration.toUpperCase()}`);
if (response.ok) {
const data = await response.json();
if (data && data.length > 0) {
if (data.length === 1) {
const aircraft = data[0];
regField.value = aircraft.registration || registration.toUpperCase();
resultsDiv.innerHTML = `
<div class="lookup-match">
${aircraft.registration || registration.toUpperCase()} - ${aircraft.type_code || 'Unknown'} ${aircraft.model ? `(${aircraft.model})` : ''}
</div>
`;
if (!document.getElementById('ac_type').value) {
document.getElementById('ac_type').value = aircraft.type_code || '';
}
} else if (data.length <= 10) {
resultsDiv.innerHTML = `
<div class="aircraft-list">
${data.map(aircraft => `
<div class="aircraft-option" onclick="selectAircraft('${aircraft.registration || registration.toUpperCase()}', '${aircraft.type_code || ''}')">
<div class="aircraft-code">${aircraft.registration || registration.toUpperCase()}</div>
<div class="aircraft-details">${aircraft.type_code || 'Unknown'} ${aircraft.model ? `(${aircraft.model})` : ''}</div>
</div>
`).join('')}
</div>
`;
} else {
resultsDiv.innerHTML = '<span class="lookup-no-match">Too many matches, please be more specific</span>';
}
} else {
resultsDiv.innerHTML = '<span class="lookup-no-match">No aircraft found with this registration</span>';
}
} else {
resultsDiv.innerHTML = '';
}
} catch (error) {
console.error('Aircraft lookup error:', error);
resultsDiv.innerHTML = '';
}
}, 500);
}
function selectAircraft(registration, typeCode) {
document.getElementById('ac_reg').value = registration;
document.getElementById('ac_type').value = typeCode;
document.getElementById('aircraft-lookup-results').innerHTML = '';
document.getElementById('ac_reg').blur();
}
document.getElementById('ac_reg').addEventListener('blur', function() {
setTimeout(() => {
document.getElementById('aircraft-lookup-results').innerHTML = '';
}, 150);
});
// Airport lookup functions (same as submit)
let arrivalAirportLookupTimeout;
async function handleArrivalAirportLookup(query) {
clearTimeout(arrivalAirportLookupTimeout);
const resultsDiv = document.getElementById('arrival-airport-lookup-results');
if (!query || query.length < 2) {
resultsDiv.innerHTML = '';
return;
}
resultsDiv.innerHTML = '<span class="lookup-searching">Searching...</span>';
arrivalAirportLookupTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/airport/public/lookup/${query.toUpperCase()}`);
if (response.ok) {
const data = await response.json();
if (data && data.length > 0) {
const airport = data[0];
resultsDiv.innerHTML = `
<div class="lookup-match">
${airport.icao}/${airport.iata || ''} - ${airport.name}, ${airport.country}
</div>
`;
} else {
resultsDiv.innerHTML = '<span class="lookup-no-match">No airport found</span>';
}
} else {
resultsDiv.innerHTML = '';
}
} catch (error) {
console.error('Arrival airport lookup error:', error);
resultsDiv.innerHTML = '';
}
}, 500);
}
let departureAirportLookupTimeout;
async function handleDepartureAirportLookup(query) {
clearTimeout(departureAirportLookupTimeout);
const resultsDiv = document.getElementById('departure-airport-lookup-results');
if (!query || query.length < 2) {
resultsDiv.innerHTML = '';
return;
}
resultsDiv.innerHTML = '<span class="lookup-searching">Searching...</span>';
departureAirportLookupTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/airport/public/lookup/${query.toUpperCase()}`);
if (response.ok) {
const data = await response.json();
if (data && data.length > 0) {
const airport = data[0];
resultsDiv.innerHTML = `
<div class="lookup-match">
${airport.icao}/${airport.iata || ''} - ${airport.name}, ${airport.country}
</div>
`;
} else {
resultsDiv.innerHTML = '<span class="lookup-no-match">No airport found</span>';
}
} else {
resultsDiv.innerHTML = '';
}
} catch (error) {
console.error('Departure airport lookup error:', error);
resultsDiv.innerHTML = '';
}
}, 500);
}
// Form submission (update)
document.getElementById('ppr-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const pprData = {};
formData.forEach((value, key) => {
if (value.trim() !== '') {
if (key === 'pob_in' || key === 'pob_out') {
pprData[key] = parseInt(value);
} else if (key === 'eta' || key === 'etd') {
pprData[key] = new Date(value).toISOString();
} else {
pprData[key] = value;
}
}
});
document.getElementById('loading').style.display = 'block';
document.getElementById('update-btn').disabled = true;
document.getElementById('update-btn').textContent = 'Updating...';
try {
const response = await fetch(`/api/v1/pprs/public/edit/${token}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(pprData)
});
if (response.ok) {
document.getElementById('ppr-form').style.display = 'none';
document.getElementById('success-message').style.display = 'block';
showNotification('PPR request updated successfully!');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Update failed: ${response.status}`);
}
} catch (error) {
console.error('Error updating PPR:', error);
showNotification(`Error updating PPR: ${error.message}`, true);
} finally {
document.getElementById('loading').style.display = 'none';
document.getElementById('update-btn').disabled = false;
document.getElementById('update-btn').textContent = 'Update PPR Request';
}
});
// Cancel button
document.getElementById('cancel-btn').addEventListener('click', async function() {
if (!confirm('Are you sure you want to cancel this PPR request?')) {
return;
}
document.getElementById('loading').style.display = 'block';
this.disabled = true;
this.textContent = 'Cancelling...';
try {
const response = await fetch(`/api/v1/pprs/public/cancel/${token}`, {
method: 'DELETE'
});
if (response.ok) {
document.getElementById('ppr-form').style.display = 'none';
document.getElementById('cancel-message').style.display = 'block';
showNotification('PPR request cancelled successfully!');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Cancellation failed: ${response.status}`);
}
} catch (error) {
console.error('Error cancelling PPR:', error);
showNotification(`Error cancelling PPR: ${error.message}`, true);
} finally {
document.getElementById('loading').style.display = 'none';
this.disabled = false;
this.textContent = 'Cancel PPR Request';
}
});
// Load data on page load
loadPPRData();
</script>
</body>
</html>