Compare commits
3 Commits
74c21fe988
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a49dfe219 | |||
| 8d8cb9ccad | |||
| 4b6dd9c93c |
@@ -0,0 +1,34 @@
|
|||||||
|
"""Rename drone request altitude to AGL
|
||||||
|
|
||||||
|
Revision ID: 010_drone_request_agl_altitude
|
||||||
|
Revises: 009_drone_requests
|
||||||
|
Create Date: 2026-06-29 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = '010_drone_request_agl_altitude'
|
||||||
|
down_revision = '009_drone_requests'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
'drone_requests',
|
||||||
|
'maximum_elevation_ft_amsl',
|
||||||
|
new_column_name='maximum_elevation_ft_agl',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
'drone_requests',
|
||||||
|
'maximum_elevation_ft_agl',
|
||||||
|
new_column_name='maximum_elevation_ft_amsl',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Add LOCAL status to PPR records
|
||||||
|
|
||||||
|
Revision ID: 011_ppr_local_status
|
||||||
|
Revises: 010_drone_request_agl_altitude
|
||||||
|
Create Date: 2026-06-29 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = '011_ppr_local_status'
|
||||||
|
down_revision = '010_drone_request_agl_altitude'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("ALTER TABLE submitted CHANGE COLUMN departed_dt takeoff_dt DATETIME NULL")
|
||||||
|
op.execute("ALTER TABLE submitted ADD COLUMN qsy_dt DATETIME NULL AFTER takeoff_dt")
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE submitted MODIFY COLUMN status "
|
||||||
|
"ENUM('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') "
|
||||||
|
"NOT NULL DEFAULT 'NEW'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("UPDATE submitted SET status = 'LANDED' WHERE status = 'LOCAL'")
|
||||||
|
op.execute("ALTER TABLE submitted DROP COLUMN qsy_dt")
|
||||||
|
op.execute("ALTER TABLE submitted CHANGE COLUMN takeoff_dt departed_dt DATETIME NULL")
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE submitted MODIFY COLUMN status "
|
||||||
|
"ENUM('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED','ACTIVATED') "
|
||||||
|
"NOT NULL DEFAULT 'NEW'"
|
||||||
|
)
|
||||||
@@ -52,7 +52,7 @@ async def _send_drone_email(drone_request, subject: str, message: str):
|
|||||||
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
||||||
"maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
|
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
|
||||||
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -70,7 +70,7 @@ async def _send_drone_submitted_email(drone_request):
|
|||||||
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
||||||
"maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
|
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
|
||||||
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -93,7 +93,7 @@ async def _send_drone_tower_notification(drone_request):
|
|||||||
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
||||||
"maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
|
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
|
||||||
"inside_frz": "Yes" if drone_request.location_inside_frz else "No",
|
"inside_frz": "Yes" if drone_request.location_inside_frz else "No",
|
||||||
"notes": drone_request.applicant_notes,
|
"notes": drone_request.applicant_notes,
|
||||||
"requests_url": f"{settings.base_url}/drone-requests",
|
"requests_url": f"{settings.base_url}/drone-requests",
|
||||||
@@ -115,7 +115,7 @@ async def _send_drone_approved_email(drone_request, message: Optional[str] = Non
|
|||||||
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
|
||||||
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
|
||||||
"maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
|
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
|
||||||
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ async def bulk_log_movement(
|
|||||||
else:
|
else:
|
||||||
ppr.out_to = entry.to_location or ppr.out_to
|
ppr.out_to = entry.to_location or ppr.out_to
|
||||||
ppr.pob_out = entry.pob or ppr.pob_out
|
ppr.pob_out = entry.pob or ppr.pob_out
|
||||||
ppr.departed_dt = timestamp
|
ppr.takeoff_dt = timestamp
|
||||||
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED):
|
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED):
|
||||||
ppr.status = PPRStatus.DEPARTED
|
ppr.status = PPRStatus.DEPARTED
|
||||||
if entry.notes:
|
if entry.notes:
|
||||||
|
|||||||
@@ -222,13 +222,21 @@ async def update_ppr_status(
|
|||||||
|
|
||||||
# Send real-time update
|
# Send real-time update
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
event_timestamp = None
|
||||||
|
if ppr.status == PPRStatus.LANDED and ppr.landed_dt:
|
||||||
|
event_timestamp = ppr.landed_dt.isoformat()
|
||||||
|
elif ppr.status == PPRStatus.LOCAL and ppr.takeoff_dt:
|
||||||
|
event_timestamp = ppr.takeoff_dt.isoformat()
|
||||||
|
elif ppr.status == PPRStatus.DEPARTED and ppr.qsy_dt:
|
||||||
|
event_timestamp = ppr.qsy_dt.isoformat()
|
||||||
|
|
||||||
await request.app.state.connection_manager.broadcast({
|
await request.app.state.connection_manager.broadcast({
|
||||||
"type": "status_update",
|
"type": "status_update",
|
||||||
"data": {
|
"data": {
|
||||||
"id": ppr.id,
|
"id": ppr.id,
|
||||||
"ac_reg": ppr.ac_reg,
|
"ac_reg": ppr.ac_reg,
|
||||||
"status": ppr.status.value,
|
"status": ppr.status.value,
|
||||||
"timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else (ppr.departed_dt.isoformat() if ppr.departed_dt else None)
|
"timestamp": event_timestamp
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -348,7 +356,7 @@ async def update_ppr_public(
|
|||||||
detail="Invalid or expired token"
|
detail="Invalid or expired token"
|
||||||
)
|
)
|
||||||
# Only allow editing if not already processed
|
# Only allow editing if not already processed
|
||||||
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
|
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.LOCAL, 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 cannot be edited at this stage"
|
||||||
@@ -373,7 +381,7 @@ async def cancel_ppr_public(
|
|||||||
detail="Invalid or expired token"
|
detail="Invalid or expired token"
|
||||||
)
|
)
|
||||||
# Only allow canceling if not already processed
|
# Only allow canceling if not already processed
|
||||||
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
|
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.LOCAL, PPRStatus.DEPARTED]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="PPR cannot be cancelled at this stage"
|
detail="PPR cannot be cancelled at this stage"
|
||||||
|
|||||||
@@ -145,7 +145,8 @@ async def get_public_departures(db: Session = Depends(get_db)):
|
|||||||
'ac_type': departure.ac_type,
|
'ac_type': departure.ac_type,
|
||||||
'out_to': departure.out_to,
|
'out_to': departure.out_to,
|
||||||
'etd': departure.etd,
|
'etd': departure.etd,
|
||||||
'departed_dt': departure.departed_dt,
|
'takeoff_dt': departure.takeoff_dt,
|
||||||
|
'qsy_dt': departure.qsy_dt,
|
||||||
'status': departure.status.value,
|
'status': departure.status.value,
|
||||||
'isLocalFlight': False,
|
'isLocalFlight': False,
|
||||||
'isDeparture': False
|
'isDeparture': False
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class CRUDPPR:
|
|||||||
PPRRecord.status == PPRStatus.NEW,
|
PPRRecord.status == PPRStatus.NEW,
|
||||||
PPRRecord.status == PPRStatus.CONFIRMED,
|
PPRRecord.status == PPRStatus.CONFIRMED,
|
||||||
PPRRecord.status == PPRStatus.LANDED,
|
PPRRecord.status == PPRStatus.LANDED,
|
||||||
|
PPRRecord.status == PPRStatus.LOCAL,
|
||||||
PPRRecord.status == PPRStatus.DEPARTED
|
PPRRecord.status == PPRStatus.DEPARTED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -71,6 +72,7 @@ class CRUDPPR:
|
|||||||
func.date(PPRRecord.etd) == today,
|
func.date(PPRRecord.etd) == today,
|
||||||
or_(
|
or_(
|
||||||
PPRRecord.status == PPRStatus.LANDED,
|
PPRRecord.status == PPRStatus.LANDED,
|
||||||
|
PPRRecord.status == PPRStatus.LOCAL,
|
||||||
PPRRecord.status == PPRStatus.DEPARTED
|
PPRRecord.status == PPRStatus.DEPARTED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -151,8 +153,10 @@ class CRUDPPR:
|
|||||||
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
current_time = timestamp if timestamp is not None else datetime.utcnow()
|
||||||
if status == PPRStatus.LANDED:
|
if status == PPRStatus.LANDED:
|
||||||
db_obj.landed_dt = current_time
|
db_obj.landed_dt = current_time
|
||||||
|
elif status == PPRStatus.LOCAL:
|
||||||
|
db_obj.takeoff_dt = current_time
|
||||||
elif status == PPRStatus.DEPARTED:
|
elif status == PPRStatus.DEPARTED:
|
||||||
db_obj.departed_dt = current_time
|
db_obj.qsy_dt = current_time
|
||||||
|
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class DroneRequest(Base):
|
|||||||
estimated_completion_time = Column(String(8), nullable=True)
|
estimated_completion_time = Column(String(8), nullable=True)
|
||||||
estimated_takeoff_at = Column(DateTime, nullable=False, index=True)
|
estimated_takeoff_at = Column(DateTime, nullable=False, index=True)
|
||||||
estimated_completion_at = Column(DateTime, nullable=False, index=True)
|
estimated_completion_at = Column(DateTime, nullable=False, index=True)
|
||||||
maximum_elevation_ft_amsl = Column(Integer, nullable=False)
|
maximum_elevation_ft_agl = Column(Integer, nullable=False)
|
||||||
|
|
||||||
location_description = Column(Text, nullable=True)
|
location_description = Column(Text, nullable=True)
|
||||||
location_latitude = Column(Float, nullable=False)
|
location_latitude = Column(Float, nullable=False)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PPRStatus(str, Enum):
|
|||||||
CONFIRMED = "CONFIRMED"
|
CONFIRMED = "CONFIRMED"
|
||||||
CANCELED = "CANCELED"
|
CANCELED = "CANCELED"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
DELETED = "DELETED"
|
DELETED = "DELETED"
|
||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
ACTIVATED = "ACTIVATED"
|
ACTIVATED = "ACTIVATED"
|
||||||
@@ -40,7 +41,8 @@ class PPRRecord(Base):
|
|||||||
phone = Column(String(16), nullable=True)
|
phone = Column(String(16), nullable=True)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
landed_dt = Column(DateTime, nullable=True)
|
landed_dt = Column(DateTime, nullable=True)
|
||||||
departed_dt = Column(DateTime, nullable=True)
|
takeoff_dt = Column(DateTime, nullable=True)
|
||||||
|
qsy_dt = Column(DateTime, nullable=True)
|
||||||
created_by = Column(String(16), nullable=True, index=True)
|
created_by = Column(String(16), nullable=True, index=True)
|
||||||
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
|
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
|
||||||
acknowledged_dt = Column(DateTime, nullable=True)
|
acknowledged_dt = Column(DateTime, nullable=True)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import date, datetime
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, EmailStr, Field, validator
|
from pydantic import AliasChoices, BaseModel, EmailStr, Field, validator
|
||||||
|
|
||||||
|
|
||||||
class DroneRequestStatus(str, Enum):
|
class DroneRequestStatus(str, Enum):
|
||||||
@@ -20,7 +20,11 @@ class DroneRequestBase(BaseModel):
|
|||||||
flight_date: Optional[date] = None
|
flight_date: Optional[date] = None
|
||||||
estimated_takeoff_time: Optional[str] = Field(None, max_length=8)
|
estimated_takeoff_time: Optional[str] = Field(None, max_length=8)
|
||||||
estimated_completion_time: Optional[str] = Field(None, max_length=8)
|
estimated_completion_time: Optional[str] = Field(None, max_length=8)
|
||||||
maximum_elevation_ft_amsl: int = Field(..., ge=0)
|
maximum_elevation_ft_agl: int = Field(
|
||||||
|
...,
|
||||||
|
ge=0,
|
||||||
|
validation_alias=AliasChoices("maximum_elevation_ft_agl", "maximum_elevation_ft_amsl"),
|
||||||
|
)
|
||||||
location_description: Optional[str] = None
|
location_description: Optional[str] = None
|
||||||
location_latitude: float = Field(..., ge=-90, le=90)
|
location_latitude: float = Field(..., ge=-90, le=90)
|
||||||
location_longitude: float = Field(..., ge=-180, le=180)
|
location_longitude: float = Field(..., ge=-180, le=180)
|
||||||
@@ -68,7 +72,11 @@ class DroneRequestUpdate(BaseModel):
|
|||||||
estimated_completion_time: Optional[str] = Field(None, max_length=8)
|
estimated_completion_time: Optional[str] = Field(None, max_length=8)
|
||||||
estimated_takeoff_at: Optional[datetime] = None
|
estimated_takeoff_at: Optional[datetime] = None
|
||||||
estimated_completion_at: Optional[datetime] = None
|
estimated_completion_at: Optional[datetime] = None
|
||||||
maximum_elevation_ft_amsl: Optional[int] = Field(None, ge=0)
|
maximum_elevation_ft_agl: Optional[int] = Field(
|
||||||
|
None,
|
||||||
|
ge=0,
|
||||||
|
validation_alias=AliasChoices("maximum_elevation_ft_agl", "maximum_elevation_ft_amsl"),
|
||||||
|
)
|
||||||
location_description: Optional[str] = None
|
location_description: Optional[str] = None
|
||||||
location_latitude: Optional[float] = Field(None, ge=-90, le=90)
|
location_latitude: Optional[float] = Field(None, ge=-90, le=90)
|
||||||
location_longitude: Optional[float] = Field(None, ge=-180, le=180)
|
location_longitude: Optional[float] = Field(None, ge=-180, le=180)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PPRStatus(str, Enum):
|
|||||||
CONFIRMED = "CONFIRMED"
|
CONFIRMED = "CONFIRMED"
|
||||||
CANCELED = "CANCELED"
|
CANCELED = "CANCELED"
|
||||||
LANDED = "LANDED"
|
LANDED = "LANDED"
|
||||||
|
LOCAL = "LOCAL"
|
||||||
DELETED = "DELETED"
|
DELETED = "DELETED"
|
||||||
DEPARTED = "DEPARTED"
|
DEPARTED = "DEPARTED"
|
||||||
ACTIVATED = "ACTIVATED"
|
ACTIVATED = "ACTIVATED"
|
||||||
@@ -85,7 +86,8 @@ class PPRInDBBase(PPRBase):
|
|||||||
id: int
|
id: int
|
||||||
status: PPRStatus
|
status: PPRStatus
|
||||||
landed_dt: Optional[datetime] = None
|
landed_dt: Optional[datetime] = None
|
||||||
departed_dt: Optional[datetime] = None
|
takeoff_dt: Optional[datetime] = None
|
||||||
|
qsy_dt: Optional[datetime] = None
|
||||||
created_by: Optional[str] = None
|
created_by: Optional[str] = None
|
||||||
submitted_dt: datetime
|
submitted_dt: datetime
|
||||||
acknowledged_dt: Optional[datetime] = None
|
acknowledged_dt: Optional[datetime] = None
|
||||||
@@ -111,7 +113,8 @@ class PPRPublic(BaseModel):
|
|||||||
out_to: Optional[str] = None
|
out_to: Optional[str] = None
|
||||||
etd: Optional[datetime] = None
|
etd: Optional[datetime] = None
|
||||||
landed_dt: Optional[datetime] = None
|
landed_dt: Optional[datetime] = None
|
||||||
departed_dt: Optional[datetime] = None
|
takeoff_dt: Optional[datetime] = None
|
||||||
|
qsy_dt: Optional[datetime] = None
|
||||||
submitted_dt: datetime
|
submitted_dt: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
||||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td>
|
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_agl }} ft AGL</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
||||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td>
|
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_agl }} ft AGL</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Completion</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ completion_time }}</td></tr>
|
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Completion</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ completion_time }}</td></tr>
|
||||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Location</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ location }}</td></tr>
|
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Location</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ location }}</td></tr>
|
||||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Inside FRZ</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ inside_frz }}</td></tr>
|
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Inside FRZ</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ inside_frz }}</td></tr>
|
||||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Max elevation</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td></tr>
|
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Max elevation</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_agl }} ft AGL</td></tr>
|
||||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Applicant notes</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ notes or '-' }}</td></tr>
|
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Applicant notes</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ notes or '-' }}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
||||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td>
|
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_agl }} ft AGL</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ DROP TABLE IF EXISTS `submitted`;
|
|||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
CREATE TABLE `submitted` (
|
CREATE TABLE `submitted` (
|
||||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`status` enum('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
|
`status` enum('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
|
||||||
`ac_reg` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
`ac_reg` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||||
`ac_type` varchar(32) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
`ac_type` varchar(32) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||||
`ac_call` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
`ac_call` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||||
@@ -97,7 +97,8 @@ CREATE TABLE `submitted` (
|
|||||||
`phone` varchar(16) DEFAULT NULL,
|
`phone` varchar(16) DEFAULT NULL,
|
||||||
`notes` varchar(2000) DEFAULT NULL,
|
`notes` varchar(2000) DEFAULT NULL,
|
||||||
`landed_dt` datetime DEFAULT NULL,
|
`landed_dt` datetime DEFAULT NULL,
|
||||||
`departed_dt` datetime DEFAULT NULL,
|
`takeoff_dt` datetime DEFAULT NULL,
|
||||||
|
`qsy_dt` datetime DEFAULT NULL,
|
||||||
`created_by` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
`created_by` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||||
`submitted_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`submitted_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY `id` (`id`)
|
UNIQUE KEY `id` (`id`)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def drone_payload(**overrides):
|
|||||||
"estimated_completion_time": "10:30",
|
"estimated_completion_time": "10:30",
|
||||||
"estimated_takeoff_at": "2026-06-20T10:00:00",
|
"estimated_takeoff_at": "2026-06-20T10:00:00",
|
||||||
"estimated_completion_at": "2026-06-20T10:30:00",
|
"estimated_completion_at": "2026-06-20T10:30:00",
|
||||||
"maximum_elevation_ft_amsl": 250,
|
"maximum_elevation_ft_agl": 250,
|
||||||
"location_description": "North apron",
|
"location_description": "North apron",
|
||||||
"location_latitude": 51.623389,
|
"location_latitude": 51.623389,
|
||||||
"location_longitude": -4.069231,
|
"location_longitude": -4.069231,
|
||||||
@@ -81,6 +81,23 @@ def test_public_drone_request_create_edit_cancel_and_journal(client, db, monkeyp
|
|||||||
assert client.delete("/api/v1/drone-requests/public/cancel/missing-token").status_code == 404
|
assert client.delete("/api/v1/drone-requests/public/cancel/missing-token").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_drone_request_accepts_legacy_amsl_altitude_key(client, db, monkeypatch):
|
||||||
|
async def fake_send_email(**kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.api.endpoints.drone_requests.email_service.send_email", fake_send_email)
|
||||||
|
|
||||||
|
payload = drone_payload()
|
||||||
|
payload["maximum_elevation_ft_amsl"] = payload.pop("maximum_elevation_ft_agl")
|
||||||
|
|
||||||
|
create_response = client.post("/api/v1/drone-requests/public", json=payload)
|
||||||
|
|
||||||
|
assert create_response.status_code == 200
|
||||||
|
assert create_response.json()["maximum_elevation_ft_agl"] == 250
|
||||||
|
db_request = db.query(DroneRequest).filter(DroneRequest.id == create_response.json()["id"]).one()
|
||||||
|
assert db_request.maximum_elevation_ft_agl == 250
|
||||||
|
|
||||||
|
|
||||||
def test_authenticated_drone_request_list_update_status_comment_and_journal(auth_client, db, monkeypatch):
|
def test_authenticated_drone_request_list_update_status_comment_and_journal(auth_client, db, monkeypatch):
|
||||||
sent_emails = []
|
sent_emails = []
|
||||||
|
|
||||||
@@ -100,7 +117,7 @@ def test_authenticated_drone_request_list_update_status_comment_and_journal(auth
|
|||||||
get_response = auth_client.get(f"/api/v1/drone-requests/{created['id']}")
|
get_response = auth_client.get(f"/api/v1/drone-requests/{created['id']}")
|
||||||
update_response = auth_client.patch(
|
update_response = auth_client.patch(
|
||||||
f"/api/v1/drone-requests/{created['id']}",
|
f"/api/v1/drone-requests/{created['id']}",
|
||||||
json={"operator_comments": "Needs tower review", "maximum_elevation_ft_amsl": 200},
|
json={"operator_comments": "Needs tower review", "maximum_elevation_ft_agl": 200},
|
||||||
)
|
)
|
||||||
status_response = auth_client.patch(
|
status_response = auth_client.patch(
|
||||||
f"/api/v1/drone-requests/{created['id']}/status",
|
f"/api/v1/drone-requests/{created['id']}/status",
|
||||||
@@ -116,7 +133,7 @@ def test_authenticated_drone_request_list_update_status_comment_and_journal(auth
|
|||||||
assert [request["id"] for request in list_response.json()] == [created["id"]]
|
assert [request["id"] for request in list_response.json()] == [created["id"]]
|
||||||
assert get_response.status_code == 200
|
assert get_response.status_code == 200
|
||||||
assert update_response.status_code == 200
|
assert update_response.status_code == 200
|
||||||
assert update_response.json()["maximum_elevation_ft_amsl"] == 200
|
assert update_response.json()["maximum_elevation_ft_agl"] == 200
|
||||||
assert status_response.status_code == 200
|
assert status_response.status_code == 200
|
||||||
assert status_response.json()["status"] == "APPROVED"
|
assert status_response.json()["status"] == "APPROVED"
|
||||||
assert status_response.json()["operator_comments"] == "Approved below 200ft"
|
assert status_response.json()["operator_comments"] == "Approved below 200ft"
|
||||||
@@ -133,7 +150,7 @@ def test_authenticated_drone_request_list_update_status_comment_and_journal(auth
|
|||||||
def test_drone_request_not_found_and_validation_paths(auth_client, client):
|
def test_drone_request_not_found_and_validation_paths(auth_client, client):
|
||||||
invalid_response = client.post(
|
invalid_response = client.post(
|
||||||
"/api/v1/drone-requests/public",
|
"/api/v1/drone-requests/public",
|
||||||
json=drone_payload(operator_name=" ", location_latitude=100, maximum_elevation_ft_amsl=-1),
|
json=drone_payload(operator_name=" ", location_latitude=100, maximum_elevation_ft_agl=-1),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert invalid_response.status_code == 422
|
assert invalid_response.status_code == 422
|
||||||
|
|||||||
@@ -43,6 +43,34 @@ def test_authenticated_user_can_create_read_update_and_audit_ppr(auth_client, pp
|
|||||||
assert any("Status changed from NEW to LANDED" in entry for entry in entries)
|
assert any("Status changed from NEW to LANDED" in entry for entry in entries)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ppr_departure_lifecycle_goes_landed_local_departed(auth_client, ppr_payload):
|
||||||
|
created = auth_client.post("/api/v1/pprs/", json=ppr_payload).json()
|
||||||
|
|
||||||
|
landed_response = auth_client.patch(
|
||||||
|
f"/api/v1/pprs/{created['id']}/status",
|
||||||
|
json={"status": "LANDED", "timestamp": "2026-06-20T10:30:00"},
|
||||||
|
)
|
||||||
|
local_response = auth_client.patch(
|
||||||
|
f"/api/v1/pprs/{created['id']}/status",
|
||||||
|
json={"status": "LOCAL", "timestamp": "2026-06-20T12:55:00"},
|
||||||
|
)
|
||||||
|
departed_response = auth_client.patch(
|
||||||
|
f"/api/v1/pprs/{created['id']}/status",
|
||||||
|
json={"status": "DEPARTED", "timestamp": "2026-06-20T13:05:00"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert landed_response.status_code == 200
|
||||||
|
assert local_response.status_code == 200
|
||||||
|
assert local_response.json()["status"] == "LOCAL"
|
||||||
|
assert local_response.json()["landed_dt"] == "2026-06-20T10:30:00"
|
||||||
|
assert local_response.json()["takeoff_dt"] == "2026-06-20T12:55:00"
|
||||||
|
assert local_response.json()["qsy_dt"] is None
|
||||||
|
assert departed_response.status_code == 200
|
||||||
|
assert departed_response.json()["status"] == "DEPARTED"
|
||||||
|
assert departed_response.json()["takeoff_dt"] == "2026-06-20T12:55:00"
|
||||||
|
assert departed_response.json()["qsy_dt"] == "2026-06-20T13:05:00"
|
||||||
|
|
||||||
|
|
||||||
def test_ppr_list_supports_status_date_and_pagination_filters(auth_client, ppr_factory):
|
def test_ppr_list_supports_status_date_and_pagination_filters(auth_client, ppr_factory):
|
||||||
ppr_factory(
|
ppr_factory(
|
||||||
ac_reg="G-NEW1",
|
ac_reg="G-NEW1",
|
||||||
|
|||||||
+3
-2
@@ -22,7 +22,7 @@ CREATE TABLE users (
|
|||||||
-- Main PPR submissions table with improvements
|
-- Main PPR submissions table with improvements
|
||||||
CREATE TABLE submitted (
|
CREATE TABLE submitted (
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
status ENUM('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') NOT NULL DEFAULT 'NEW',
|
status ENUM('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') NOT NULL DEFAULT 'NEW',
|
||||||
ac_reg VARCHAR(16) NOT NULL,
|
ac_reg VARCHAR(16) NOT NULL,
|
||||||
ac_type VARCHAR(32) NOT NULL,
|
ac_type VARCHAR(32) NOT NULL,
|
||||||
ac_call VARCHAR(16) DEFAULT NULL,
|
ac_call VARCHAR(16) DEFAULT NULL,
|
||||||
@@ -38,7 +38,8 @@ CREATE TABLE submitted (
|
|||||||
phone VARCHAR(16) DEFAULT NULL,
|
phone VARCHAR(16) DEFAULT NULL,
|
||||||
notes TEXT DEFAULT NULL,
|
notes TEXT DEFAULT NULL,
|
||||||
landed_dt DATETIME DEFAULT NULL,
|
landed_dt DATETIME DEFAULT NULL,
|
||||||
departed_dt DATETIME DEFAULT NULL,
|
takeoff_dt DATETIME DEFAULT NULL,
|
||||||
|
qsy_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,
|
||||||
acknowledged_dt DATETIME DEFAULT NULL,
|
acknowledged_dt DATETIME DEFAULT NULL,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def drone_payload(operator_name):
|
|||||||
"estimated_completion_time": "10:30",
|
"estimated_completion_time": "10:30",
|
||||||
"estimated_takeoff_at": "2026-06-21T10:00:00",
|
"estimated_takeoff_at": "2026-06-21T10:00:00",
|
||||||
"estimated_completion_at": "2026-06-21T10:30:00",
|
"estimated_completion_at": "2026-06-21T10:30:00",
|
||||||
"maximum_elevation_ft_amsl": 200,
|
"maximum_elevation_ft_agl": 200,
|
||||||
"location_description": "E2E north apron survey",
|
"location_description": "E2E north apron survey",
|
||||||
"location_latitude": 51.623389,
|
"location_latitude": 51.623389,
|
||||||
"location_longitude": -4.069231,
|
"location_longitude": -4.069231,
|
||||||
|
|||||||
+138
-36
@@ -49,11 +49,11 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<!-- Local Flights Table -->
|
<!-- Local Traffic Table -->
|
||||||
<div class="ppr-table">
|
<div class="ppr-table">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<span>🛩️ Today's Local Flights - <span id="local-flights-count">0</span></span>
|
<span>🛩️ Local Traffic - <span id="local-flights-count">0</span></span>
|
||||||
<span class="info-icon" onclick="showTableHelp('local-flights')" title="What is this?">ℹ️</span>
|
<span class="info-icon" onclick="showTableHelp('local-flights')" title="What is this?">ℹ️</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="local-flights-no-data" class="no-data" style="display: none;">
|
<div id="local-flights-no-data" class="no-data" style="display: none;">
|
||||||
<h3>No Local Flights</h3>
|
<h3>No Local Traffic</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -321,6 +321,50 @@
|
|||||||
|
|
||||||
<script src="shared.js"></script>
|
<script src="shared.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
function normalizeUtcDateString(dateStr) {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
let utcDateStr = String(dateStr).trim();
|
||||||
|
if (!utcDateStr.includes('T')) {
|
||||||
|
utcDateStr = utcDateStr.replace(' ', 'T');
|
||||||
|
}
|
||||||
|
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
|
||||||
|
utcDateStr += 'Z';
|
||||||
|
}
|
||||||
|
return utcDateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUtcDate(dateStr) {
|
||||||
|
const normalized = normalizeUtcDateString(dateStr);
|
||||||
|
return normalized ? new Date(normalized) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUtcDateInput(date) {
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUtcTimeInput(date) {
|
||||||
|
return date.toISOString().slice(11, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeOnly(dateStr) {
|
||||||
|
const date = parseUtcDate(dateStr);
|
||||||
|
return date && !Number.isNaN(date.getTime()) ? formatUtcTimeInput(date) : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUtcDayMonth(dateStr) {
|
||||||
|
const date = parseUtcDate(dateStr);
|
||||||
|
if (!date || Number.isNaN(date.getTime())) return '-';
|
||||||
|
const isoDate = formatUtcDateInput(date);
|
||||||
|
return `${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)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPPRs() {
|
async function loadPPRs() {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
@@ -403,12 +447,11 @@
|
|||||||
document.getElementById('departures-no-data').style.display = 'none';
|
document.getElementById('departures-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load PPR departures and airport departures simultaneously
|
// Load PPR departures and airport departures that are still pending departure
|
||||||
const [pprResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
|
const [pprResponse, depBookedOutResponse, depOutGroundResponse] = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000')
|
||||||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!pprResponse.ok) {
|
if (!pprResponse.ok) {
|
||||||
@@ -418,10 +461,9 @@
|
|||||||
const allPPRs = await pprResponse.json();
|
const allPPRs = await pprResponse.json();
|
||||||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||||||
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
||||||
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
|
|
||||||
|
|
||||||
// Combine departures
|
// Combine departures
|
||||||
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
|
const allDepartures = [...depBookedOut, ...depOutGround];
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
// Filter for PPR departures with ETD today and LANDED status only
|
// Filter for PPR departures with ETD today and LANDED status only
|
||||||
@@ -434,7 +476,7 @@
|
|||||||
return etdDate === today;
|
return etdDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
|
// Add departures to other airports that are not yet airborne locally
|
||||||
const depDepartures = allDepartures.map(flight => ({
|
const depDepartures = allDepartures.map(flight => ({
|
||||||
...flight,
|
...flight,
|
||||||
isDeparture: true // Flag to distinguish from PPR
|
isDeparture: true // Flag to distinguish from PPR
|
||||||
@@ -458,19 +500,41 @@
|
|||||||
document.getElementById('local-flights-no-data').style.display = 'none';
|
document.getElementById('local-flights-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch('/api/v1/local-flights/?limit=1000');
|
const [localResponse, pprResponse, depResponse] = await Promise.all([
|
||||||
|
authenticatedFetch('/api/v1/local-flights/?limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/pprs/?status=LOCAL&limit=1000'),
|
||||||
|
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
||||||
|
]);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!localResponse.ok || !pprResponse.ok || !depResponse.ok) {
|
||||||
throw new Error('Failed to fetch local flights');
|
throw new Error('Failed to fetch local flights');
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const localFlights = (await response.json()).filter(flight => {
|
const localFlights = (await localResponse.json()).filter(flight => {
|
||||||
if (!flight.created_dt || ['CANCELLED', 'LANDED'].includes(flight.status)) return false;
|
if (!flight.created_dt || ['CANCELLED', 'LANDED'].includes(flight.status)) return false;
|
||||||
return flight.created_dt.split('T')[0] === today;
|
return flight.created_dt.split('T')[0] === today;
|
||||||
});
|
});
|
||||||
|
const pprLocalTraffic = (await pprResponse.json())
|
||||||
|
.filter(ppr => {
|
||||||
|
const dateFields = [ppr.etd, ppr.landed_dt, ppr.submitted_dt];
|
||||||
|
return dateFields.some(value => value && value.split('T')[0] === today);
|
||||||
|
})
|
||||||
|
.map(ppr => ({
|
||||||
|
...ppr,
|
||||||
|
isPPRLocalTraffic: true
|
||||||
|
}));
|
||||||
|
const departureLocalTraffic = (await depResponse.json())
|
||||||
|
.filter(departure => {
|
||||||
|
const dateFields = [departure.created_dt, departure.etd, departure.takeoff_dt, departure.departed_dt];
|
||||||
|
return dateFields.some(value => value && value.split('T')[0] === today);
|
||||||
|
})
|
||||||
|
.map(departure => ({
|
||||||
|
...departure,
|
||||||
|
isDepartureLocalTraffic: true
|
||||||
|
}));
|
||||||
|
|
||||||
displayLocalFlights(localFlights);
|
displayLocalFlights([...localFlights, ...pprLocalTraffic, ...departureLocalTraffic]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading local flights:', error);
|
console.error('Error loading local flights:', error);
|
||||||
if (error.message !== 'Session expired. Please log in again.') {
|
if (error.message !== 'Session expired. Please log in again.') {
|
||||||
@@ -566,7 +630,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Load departed aircraft (DEPARTED status with departed_dt today)
|
// Load departed aircraft (DEPARTED status with QSY/departed time today)
|
||||||
async function loadDeparted() {
|
async function loadDeparted() {
|
||||||
document.getElementById('departed-loading').style.display = 'block';
|
document.getElementById('departed-loading').style.display = 'block';
|
||||||
document.getElementById('departed-table-content').style.display = 'none';
|
document.getElementById('departed-table-content').style.display = 'none';
|
||||||
@@ -587,10 +651,10 @@
|
|||||||
|
|
||||||
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
|
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
|
||||||
const departed = allPPRs.filter(ppr => {
|
const departed = allPPRs.filter(ppr => {
|
||||||
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
|
if (!ppr.qsy_dt || ppr.status !== 'DEPARTED') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const departedDate = ppr.departed_dt.split('T')[0];
|
const departedDate = ppr.qsy_dt.split('T')[0];
|
||||||
return departedDate === today;
|
return departedDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -631,8 +695,8 @@
|
|||||||
|
|
||||||
// Sort by departed time
|
// Sort by departed time
|
||||||
departed.sort((a, b) => {
|
departed.sort((a, b) => {
|
||||||
const aTime = a.departed_dt;
|
const aTime = a.isDeparture ? a.departed_dt : a.qsy_dt;
|
||||||
const bTime = b.departed_dt;
|
const bTime = b.isDeparture ? b.departed_dt : b.qsy_dt;
|
||||||
return parseUtcDate(aTime) - parseUtcDate(bTime);
|
return parseUtcDate(aTime) - parseUtcDate(bTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -677,7 +741,7 @@
|
|||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_call || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_call || '-'}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.qsy_dt)}</td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
@@ -1156,24 +1220,52 @@
|
|||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('local-flights-table-content').style.display = 'block';
|
document.getElementById('local-flights-table-content').style.display = 'block';
|
||||||
|
|
||||||
const circuitCounts = await loadLocalFlightCircuitCounts(localFlights);
|
const circuitCounts = await loadLocalFlightCircuitCounts(localFlights.filter(flight => !flight.isPPRLocalTraffic && !flight.isDepartureLocalTraffic));
|
||||||
|
|
||||||
for (const flight of localFlights) {
|
for (const flight of localFlights) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.onclick = () => openLocalFlightEditModal(flight.id);
|
const isPPR = flight.isPPRLocalTraffic;
|
||||||
|
const isDeparture = flight.isDepartureLocalTraffic;
|
||||||
|
row.onclick = () => isPPR ? openPPRModal(flight.id) : (isDeparture ? openDepartureEditModal(flight.id) : openLocalFlightEditModal(flight.id));
|
||||||
|
|
||||||
const aircraftDisplay = flight.callsign && flight.callsign.trim()
|
const aircraftDisplay = isPPR
|
||||||
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
|
? (flight.ac_call && flight.ac_call.trim()
|
||||||
: `<strong>${flight.registration}</strong>`;
|
? `<strong>${flight.ac_call}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.ac_reg}</span>`
|
||||||
const typeIcon = flight.submitted_via === 'PUBLIC'
|
: `<strong>${flight.ac_reg}</strong>`)
|
||||||
|
: isDeparture
|
||||||
|
? (flight.callsign && flight.callsign.trim()
|
||||||
|
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
|
||||||
|
: `<strong>${flight.registration}</strong>`)
|
||||||
|
: (flight.callsign && flight.callsign.trim()
|
||||||
|
? `<strong>${flight.callsign}</strong><br><span style="font-size: 0.8em; color: #666; font-style: italic;">${flight.registration}</span>`
|
||||||
|
: `<strong>${flight.registration}</strong>`);
|
||||||
|
const typeIcon = isPPR
|
||||||
|
? '<span style="color: #032cfc; font-weight: bold; font-size: 0.9em;" title="From PPR">P</span>'
|
||||||
|
: isDeparture
|
||||||
|
? (flight.submitted_via === 'PUBLIC'
|
||||||
|
? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>'
|
||||||
|
: '<span style="color: #6f42c1; font-weight: bold; font-size: 0.9em;" title="Airport departure">D</span>')
|
||||||
|
: flight.submitted_via === 'PUBLIC'
|
||||||
? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>'
|
? '<span style="color: #b8860b; font-weight: bold; font-size: 0.9em;" title="Submitted by Pilot Online">O</span>'
|
||||||
: '<span style="color: #228b22; font-weight: bold; font-size: 0.9em;" title="Local flight">L</span>';
|
: '<span style="color: #228b22; font-weight: bold; font-size: 0.9em;" title="Local flight">L</span>';
|
||||||
const flightType = flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
|
const flightType = isPPR ? (flight.out_to ? `To ${flight.out_to}` : 'PPR Departure') : isDeparture ? (flight.out_to ? `To ${flight.out_to}` : 'Departure') : flight.flight_type === 'CIRCUITS' ? 'Circuits' : flight.flight_type === 'LOCAL' ? 'Local' : 'Departure';
|
||||||
const etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
const etd = flight.etd ? formatTimeOnly(flight.etd) : (flight.created_dt ? formatTimeOnly(flight.created_dt) : '-');
|
||||||
const circuits = circuitCounts[flight.id] ?? flight.circuits ?? 0;
|
const circuits = (isPPR || isDeparture) ? '-' : circuitCounts[flight.id] ?? flight.circuits ?? 0;
|
||||||
|
|
||||||
let actionButtons = '';
|
let actionButtons = '';
|
||||||
if (flight.status === 'BOOKED_OUT') {
|
if (isPPR) {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
||||||
|
QSY
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (isDeparture) {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); currentDepartureId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, false, true)" title="Mark as Departed">
|
||||||
|
QSY
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'BOOKED_OUT') {
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
|
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('GROUND', ${flight.id}, true)" title="Contact Pilot">
|
||||||
CONTACT
|
CONTACT
|
||||||
@@ -1202,10 +1294,10 @@
|
|||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${aircraftDisplay}</td>
|
<td>${aircraftDisplay}</td>
|
||||||
<td style="text-align: center; width: 30px;">${typeIcon}</td>
|
<td style="text-align: center; width: 30px;">${typeIcon}</td>
|
||||||
<td>${flight.type || '-'}</td>
|
<td>${isPPR ? flight.ac_type || '-' : flight.type || '-'}</td>
|
||||||
<td>${flightType}</td>
|
<td>${flightType}</td>
|
||||||
<td>${etd}</td>
|
<td>${etd}</td>
|
||||||
<td>${flight.pob || '-'}</td>
|
<td>${isPPR ? (flight.pob_out || flight.pob_in || '-') : (flight.pob || '-')}</td>
|
||||||
<td>${localFlightStatusBadge(flight.status)}</td>
|
<td>${localFlightStatusBadge(flight.status)}</td>
|
||||||
<td>${circuits}</td>
|
<td>${circuits}</td>
|
||||||
<td style="white-space: nowrap;">${actionButtons}</td>
|
<td style="white-space: nowrap;">${actionButtons}</td>
|
||||||
@@ -1451,11 +1543,21 @@
|
|||||||
fuel = flight.fuel || '-';
|
fuel = flight.fuel || '-';
|
||||||
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
||||||
|
|
||||||
actionButtons = `
|
if (flight.status === 'LANDED') {
|
||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
actionButtons = `
|
||||||
TAKE OFF
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('LOCAL', ${flight.id})" title="Mark as Local">
|
||||||
</button>
|
TAKE OFF
|
||||||
`;
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'LOCAL') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
||||||
|
QSY
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
actionButtons = '<span style="color: #999;">-</span>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
|
|||||||
+111
-41
@@ -255,11 +255,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 1: Local Area -->
|
<!-- Row 1: Local Traffic -->
|
||||||
<div class="atc-section">
|
<div class="atc-section">
|
||||||
<h2>📍 Local Area <span class="count" id="local-count">0</span></h2>
|
<h2>📍 Local Traffic <span class="count" id="local-count">0</span></h2>
|
||||||
<div class="aircraft-list" id="local-list">
|
<div class="aircraft-list" id="local-list">
|
||||||
<div class="no-aircraft">No aircraft in local area</div>
|
<div class="no-aircraft">No local traffic</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -300,6 +300,50 @@
|
|||||||
|
|
||||||
<script src="shared.js"></script>
|
<script src="shared.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
function normalizeUtcDateString(dateStr) {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
let utcDateStr = String(dateStr).trim();
|
||||||
|
if (!utcDateStr.includes('T')) {
|
||||||
|
utcDateStr = utcDateStr.replace(' ', 'T');
|
||||||
|
}
|
||||||
|
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(utcDateStr)) {
|
||||||
|
utcDateStr += 'Z';
|
||||||
|
}
|
||||||
|
return utcDateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUtcDate(dateStr) {
|
||||||
|
const normalized = normalizeUtcDateString(dateStr);
|
||||||
|
return normalized ? new Date(normalized) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUtcDateInput(date) {
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUtcTimeInput(date) {
|
||||||
|
return date.toISOString().slice(11, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeOnly(dateStr) {
|
||||||
|
const date = parseUtcDate(dateStr);
|
||||||
|
return date && !Number.isNaN(date.getTime()) ? formatUtcTimeInput(date) : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUtcDayMonth(dateStr) {
|
||||||
|
const date = parseUtcDate(dateStr);
|
||||||
|
if (!date || Number.isNaN(date.getTime())) return '-';
|
||||||
|
const isoDate = formatUtcDateInput(date);
|
||||||
|
return `${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)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPPRs() {
|
async function loadPPRs() {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
@@ -401,15 +445,13 @@
|
|||||||
document.getElementById('departures-no-data').style.display = 'none';
|
document.getElementById('departures-no-data').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load PPR departures, local flight departures, and airport departures simultaneously
|
// Load PPR departures, local flight departures, and airport departures that are still pending departure
|
||||||
const [pprResponse, localBookedOutResponse, localOutGroundResponse, localLocalResponse, depBookedOutResponse, depOutGroundResponse, depLocalResponse] = await Promise.all([
|
const [pprResponse, localBookedOutResponse, localOutGroundResponse, depBookedOutResponse, depOutGroundResponse] = await Promise.all([
|
||||||
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
authenticatedFetch('/api/v1/local-flights/?status=BOOKED_OUT&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000'),
|
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
|
||||||
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
authenticatedFetch('/api/v1/departures/?status=BOOKED_OUT&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000')
|
||||||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000')
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!pprResponse.ok) {
|
if (!pprResponse.ok) {
|
||||||
@@ -419,15 +461,13 @@
|
|||||||
const allPPRs = await pprResponse.json();
|
const allPPRs = await pprResponse.json();
|
||||||
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
const localBookedOut = localBookedOutResponse.ok ? await localBookedOutResponse.json() : [];
|
||||||
const localOutGround = localOutGroundResponse.ok ? await localOutGroundResponse.json() : [];
|
const localOutGround = localOutGroundResponse.ok ? await localOutGroundResponse.json() : [];
|
||||||
const localLocal = localLocalResponse.ok ? await localLocalResponse.json() : [];
|
|
||||||
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
const depBookedOut = depBookedOutResponse.ok ? await depBookedOutResponse.json() : [];
|
||||||
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
const depOutGround = depOutGroundResponse.ok ? await depOutGroundResponse.json() : [];
|
||||||
const depLocal = depLocalResponse.ok ? await depLocalResponse.json() : [];
|
|
||||||
|
|
||||||
// Combine local flights
|
// Combine local flights
|
||||||
const allLocalFlights = [...localBookedOut, ...localOutGround, ...localLocal];
|
const allLocalFlights = [...localBookedOut, ...localOutGround];
|
||||||
// Combine departures
|
// Combine departures
|
||||||
const allDepartures = [...depBookedOut, ...depOutGround, ...depLocal];
|
const allDepartures = [...depBookedOut, ...depOutGround];
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
// Filter for PPR departures with ETD today and LANDED status only
|
// Filter for PPR departures with ETD today and LANDED status only
|
||||||
@@ -440,7 +480,7 @@
|
|||||||
return etdDate === today;
|
return etdDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add local flights (GROUND and LOCAL status - ready to go) - only those booked out today
|
// Add local flights that are not yet airborne locally - only those booked out today
|
||||||
const localDepartures = allLocalFlights
|
const localDepartures = allLocalFlights
|
||||||
.filter(flight => {
|
.filter(flight => {
|
||||||
// Only include flights booked out today (created_dt)
|
// Only include flights booked out today (created_dt)
|
||||||
@@ -454,7 +494,7 @@
|
|||||||
}));
|
}));
|
||||||
departures.push(...localDepartures);
|
departures.push(...localDepartures);
|
||||||
|
|
||||||
// Add departures to other airports (BOOKED_OUT, GROUND, and LOCAL status)
|
// Add departures to other airports that are not yet airborne locally
|
||||||
const depDepartures = allDepartures.map(flight => ({
|
const depDepartures = allDepartures.map(flight => ({
|
||||||
...flight,
|
...flight,
|
||||||
isDeparture: true // Flag to distinguish from PPR
|
isDeparture: true // Flag to distinguish from PPR
|
||||||
@@ -558,7 +598,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Load departed aircraft (DEPARTED status with departed_dt today)
|
// Load departed aircraft (DEPARTED status with QSY/departed time today)
|
||||||
async function loadDeparted() {
|
async function loadDeparted() {
|
||||||
document.getElementById('departed-loading').style.display = 'block';
|
document.getElementById('departed-loading').style.display = 'block';
|
||||||
document.getElementById('departed-table-content').style.display = 'none';
|
document.getElementById('departed-table-content').style.display = 'none';
|
||||||
@@ -579,10 +619,10 @@
|
|||||||
|
|
||||||
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
|
// Filter for PPRs departed today (only PPR'd departures, exclude local/circuits)
|
||||||
const departed = allPPRs.filter(ppr => {
|
const departed = allPPRs.filter(ppr => {
|
||||||
if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
|
if (!ppr.qsy_dt || ppr.status !== 'DEPARTED') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const departedDate = ppr.departed_dt.split('T')[0];
|
const departedDate = ppr.qsy_dt.split('T')[0];
|
||||||
return departedDate === today;
|
return departedDate === today;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -623,8 +663,8 @@
|
|||||||
|
|
||||||
// Sort by departed time
|
// Sort by departed time
|
||||||
departed.sort((a, b) => {
|
departed.sort((a, b) => {
|
||||||
const aTime = a.departed_dt;
|
const aTime = a.isDeparture ? a.departed_dt : a.qsy_dt;
|
||||||
const bTime = b.departed_dt;
|
const bTime = b.isDeparture ? b.departed_dt : b.qsy_dt;
|
||||||
return parseUtcDate(aTime) - parseUtcDate(bTime);
|
return parseUtcDate(aTime) - parseUtcDate(bTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -669,7 +709,7 @@
|
|||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important; text-align: center; width: 30px;"><span style="color: #032cfc; font-weight: bold;" title="From PPR">P</span></td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_call || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.ac_call || '-'}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${flight.out_to || '-'}</td>
|
||||||
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.departed_dt)}</td>
|
<td style="padding: 0.3rem 0.4rem !important; font-size: 0.85rem !important;">${formatTimeOnly(flight.qsy_dt)}</td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
@@ -1044,7 +1084,7 @@
|
|||||||
T&G
|
T&G
|
||||||
</button>`;
|
</button>`;
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Area">
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); updateArrivalStatusFromTable(${flight.id}, 'LOCAL')" title="Move to Local Traffic">
|
||||||
LOCAL
|
LOCAL
|
||||||
</button>
|
</button>
|
||||||
${circuitButton}
|
${circuitButton}
|
||||||
@@ -1172,12 +1212,13 @@
|
|||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
} else if (flight.status === 'GROUND') {
|
} else if (flight.status === 'GROUND') {
|
||||||
|
const takeoffStatus = flight.flight_type === 'CIRCUITS' ? 'CIRCUIT' : 'LOCAL';
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('DEPARTED', ${flight.id}, true)" title="Mark as Departed">
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showTimestampModal('${takeoffStatus}', ${flight.id}, true)" title="Mark as airborne">
|
||||||
TAKE OFF
|
TAKE OFF
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
} else if (flight.status === 'DEPARTED') {
|
} else if (['DEPARTED', 'LOCAL', 'CIRCUIT', 'CIRCUIT_DOWNWIND', 'CIRCUIT_BASE', 'CIRCUIT_FINAL'].includes(flight.status)) {
|
||||||
// Allow touch and go for all local flight types
|
// Allow touch and go for all local flight types
|
||||||
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
let circuitButton = `<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); currentLocalFlightId = ${flight.id}; showCircuitModal()" title="Record Touch & Go">
|
||||||
T&G
|
T&G
|
||||||
@@ -1249,11 +1290,21 @@
|
|||||||
fuel = flight.fuel || '-';
|
fuel = flight.fuel || '-';
|
||||||
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
landedDt = flight.landed_dt ? formatTimeOnly(flight.landed_dt) : '-';
|
||||||
|
|
||||||
actionButtons = `
|
if (flight.status === 'LANDED') {
|
||||||
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
actionButtons = `
|
||||||
TAKE OFF
|
<button class="btn btn-primary btn-icon" onclick="event.stopPropagation(); showTimestampModal('LOCAL', ${flight.id})" title="Mark as Local">
|
||||||
</button>
|
TAKE OFF
|
||||||
`;
|
</button>
|
||||||
|
`;
|
||||||
|
} else if (flight.status === 'LOCAL') {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-success btn-icon" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${flight.id})" title="Mark as Departed">
|
||||||
|
QSY
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
actionButtons = '<span style="color: #999;">-</span>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
@@ -1368,19 +1419,26 @@
|
|||||||
// Load departing aircraft (ready to take off)
|
// Load departing aircraft (ready to take off)
|
||||||
async function loadDepartingAircraft() {
|
async function loadDepartingAircraft() {
|
||||||
try {
|
try {
|
||||||
const [groundDeparturesResponse, groundLocalResponse] = await Promise.all([
|
const [pprResponse, groundDeparturesResponse, groundLocalResponse] = await Promise.all([
|
||||||
|
authenticatedFetch('/api/v1/pprs/?limit=1000'),
|
||||||
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
authenticatedFetch('/api/v1/departures/?status=GROUND&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000')
|
authenticatedFetch('/api/v1/local-flights/?status=GROUND&limit=1000')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let groundAircraft = [];
|
let groundAircraft = [];
|
||||||
if (groundDeparturesResponse.ok) groundAircraft = await groundDeparturesResponse.json();
|
if (pprResponse.ok) {
|
||||||
|
const today = getLocalDateString();
|
||||||
|
groundAircraft = (await pprResponse.json())
|
||||||
|
.filter(ppr => ppr.status === 'LANDED' && ppr.etd && ppr.etd.split('T')[0] === today)
|
||||||
|
.map(ppr => ({ ...ppr, isPPR: true }));
|
||||||
|
}
|
||||||
|
if (groundDeparturesResponse.ok) groundAircraft = groundAircraft.concat(await groundDeparturesResponse.json());
|
||||||
if (groundLocalResponse.ok) groundAircraft = groundAircraft.concat((await groundLocalResponse.json()).map(l => ({ ...l, isLocalFlight: true })));
|
if (groundLocalResponse.ok) groundAircraft = groundAircraft.concat((await groundLocalResponse.json()).map(l => ({ ...l, isLocalFlight: true })));
|
||||||
groundAircraft = groundAircraft.filter(ac => isTodayRecord(ac, ['created_dt', 'etd']));
|
groundAircraft = groundAircraft.filter(ac => isTodayRecord(ac, ['created_dt', 'etd']));
|
||||||
|
|
||||||
displayDepartingAircraft(groundAircraft.map(ac => ({
|
displayDepartingAircraft(groundAircraft.map(ac => ({
|
||||||
...ac,
|
...ac,
|
||||||
isDeparture: !ac.isLocalFlight
|
isDeparture: !ac.isLocalFlight && !ac.isPPR
|
||||||
})));
|
})));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading departing aircraft:', error);
|
console.error('Error loading departing aircraft:', error);
|
||||||
@@ -1403,6 +1461,7 @@
|
|||||||
const type = ac.ac_type || ac.type;
|
const type = ac.ac_type || ac.type;
|
||||||
const dest = ac.out_to;
|
const dest = ac.out_to;
|
||||||
const isLocal = ac.isLocalFlight;
|
const isLocal = ac.isLocalFlight;
|
||||||
|
const isPPR = ac.isPPR;
|
||||||
|
|
||||||
// All aircraft in awaiting departure are in GROUND status
|
// All aircraft in awaiting departure are in GROUND status
|
||||||
let takeoffOnclick, buttonText, buttonTitle, clickType;
|
let takeoffOnclick, buttonText, buttonTitle, clickType;
|
||||||
@@ -1413,13 +1472,18 @@
|
|||||||
buttonText = 'TAKE OFF';
|
buttonText = 'TAKE OFF';
|
||||||
buttonTitle = takeoffTitle;
|
buttonTitle = takeoffTitle;
|
||||||
clickType = 'local';
|
clickType = 'local';
|
||||||
|
} else if (isPPR) {
|
||||||
|
takeoffOnclick = `event.stopPropagation(); showTimestampModal('LOCAL', ${ac.id})`;
|
||||||
|
buttonText = 'TAKE OFF';
|
||||||
|
buttonTitle = 'Mark as Local';
|
||||||
|
clickType = 'ppr';
|
||||||
} else {
|
} else {
|
||||||
takeoffOnclick = `event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('LOCAL', ${ac.id}, false, true)`;
|
takeoffOnclick = `event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('LOCAL', ${ac.id}, false, true)`;
|
||||||
buttonText = 'TAKE OFF';
|
buttonText = 'TAKE OFF';
|
||||||
buttonTitle = 'Mark as Local';
|
buttonTitle = 'Mark as Local';
|
||||||
clickType = 'departure';
|
clickType = 'departure';
|
||||||
}
|
}
|
||||||
const itemClass = isLocal ? 'local-flight' : 'departure';
|
const itemClass = isLocal ? 'local-flight' : (isPPR ? 'departure' : 'departure');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${clickType}')">
|
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${clickType}')">
|
||||||
@@ -1437,6 +1501,7 @@
|
|||||||
async function loadLocalAircraft() {
|
async function loadLocalAircraft() {
|
||||||
try {
|
try {
|
||||||
const response = await Promise.all([
|
const response = await Promise.all([
|
||||||
|
authenticatedFetch('/api/v1/pprs/?status=LOCAL&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
authenticatedFetch('/api/v1/local-flights/?status=LOCAL&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'),
|
authenticatedFetch('/api/v1/departures/?status=LOCAL&limit=1000'),
|
||||||
authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'),
|
authenticatedFetch('/api/v1/arrivals/?status=LOCAL&limit=1000'),
|
||||||
@@ -1444,12 +1509,14 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let locals = [];
|
let locals = [];
|
||||||
if (response[0].ok) locals = (await response[0].json()).map(l => ({ ...l, isLocalFlight: true }));
|
if (response[0].ok) locals = (await response[0].json()).map(p => ({ ...p, isPPR: true }));
|
||||||
if (response[1].ok) locals = locals.concat((await response[1].json()).map(d => ({ ...d, isDeparture: true })));
|
if (response[1].ok) locals = locals.concat((await response[1].json()).map(l => ({ ...l, isLocalFlight: true })));
|
||||||
if (response[2].ok) locals = locals.concat((await response[2].json()).map(a => ({ ...a, isArrival: true })));
|
if (response[2].ok) locals = locals.concat((await response[2].json()).map(d => ({ ...d, isDeparture: true })));
|
||||||
if (response[3].ok) locals = locals.concat((await response[3].json()).map(o => ({ ...o, isOverflight: true })));
|
if (response[3].ok) locals = locals.concat((await response[3].json()).map(a => ({ ...a, isArrival: true })));
|
||||||
|
if (response[4].ok) locals = locals.concat((await response[4].json()).map(o => ({ ...o, isOverflight: true })));
|
||||||
locals = locals.filter(ac => {
|
locals = locals.filter(ac => {
|
||||||
if (ac.isOverflight) return true;
|
if (ac.isOverflight) return true;
|
||||||
|
if (ac.isPPR) return isTodayRecord(ac, ['etd', 'landed_dt', 'submitted_dt']);
|
||||||
if (ac.isLocalFlight) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
|
if (ac.isLocalFlight) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
|
||||||
if (ac.isArrival) return isTodayRecord(ac, ['created_dt', 'eta']);
|
if (ac.isArrival) return isTodayRecord(ac, ['created_dt', 'eta']);
|
||||||
if (ac.isDeparture) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
|
if (ac.isDeparture) return isTodayRecord(ac, ['created_dt', 'etd', 'takeoff_dt', 'departed_dt']);
|
||||||
@@ -1469,7 +1536,7 @@
|
|||||||
countEl.textContent = aircraft.length;
|
countEl.textContent = aircraft.length;
|
||||||
|
|
||||||
if (aircraft.length === 0) {
|
if (aircraft.length === 0) {
|
||||||
container.innerHTML = '<div class="no-aircraft">No aircraft in local area</div>';
|
container.innerHTML = '<div class="no-aircraft">No local traffic</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,9 +1545,12 @@
|
|||||||
const type = ac.type || ac.ac_type || ac.aircraft_type || '';
|
const type = ac.type || ac.ac_type || ac.aircraft_type || '';
|
||||||
const dest = ac.out_to;
|
const dest = ac.out_to;
|
||||||
const isDeparture = ac.isDeparture;
|
const isDeparture = ac.isDeparture;
|
||||||
|
const isPPR = ac.isPPR;
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
if (isDeparture) {
|
if (isPPR) {
|
||||||
|
buttons = `<button class="status-btn" onclick="event.stopPropagation(); showTimestampModal('DEPARTED', ${ac.id})">QSY</button>`;
|
||||||
|
} else if (isDeparture) {
|
||||||
// Departure in LOCAL status - show QSY and REJOIN buttons
|
// Departure in LOCAL status - show QSY and REJOIN buttons
|
||||||
buttons = `
|
buttons = `
|
||||||
<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>
|
<button class="status-btn" onclick="event.stopPropagation(); currentDepartureId = '${ac.id}'; showTimestampModal('DEPARTED', ${ac.id}, false, true)">QSY</button>
|
||||||
@@ -1493,9 +1563,9 @@
|
|||||||
// Overflight in ACTIVE status - show QSY button
|
// Overflight in ACTIVE status - show QSY button
|
||||||
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentOverflightId = '${ac.id}'; showOverflightQSYModal()">QSY</button>`;
|
buttons = `<button class="status-btn" onclick="event.stopPropagation(); currentOverflightId = '${ac.id}'; showOverflightQSYModal()">QSY</button>`;
|
||||||
}
|
}
|
||||||
const itemClass = isDeparture ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
|
const itemClass = isDeparture || isPPR ? 'departure' : (ac.isArrival ? 'inbound' : (ac.isOverflight ? 'overflight' : 'local-flight'));
|
||||||
const detailsText = isDeparture ? `${type}${dest ? ` → ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} → ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` → ${dest}` : ` Local Flight`}`));
|
const detailsText = isDeparture || isPPR ? `${type}${dest ? ` → ${dest}` : ` (Local)`}` : (ac.isOverflight ? `${ac.departure_airfield || '?'} → ${ac.destination_airfield || '?'}` : (ac.isArrival ? `${type} from ${ac.in_from || '?'}` : `${type}${dest ? ` → ${dest}` : ` Local Flight`}`));
|
||||||
const entityType = isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local'));
|
const entityType = isPPR ? 'ppr' : (isDeparture ? 'departure' : (ac.isArrival ? 'arrival' : (ac.isOverflight ? 'overflight' : 'local')));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
|
<div class="aircraft-item ${itemClass}" onclick="handleATCClick('${ac.id}', '${entityType}')">
|
||||||
|
|||||||
@@ -149,8 +149,8 @@
|
|||||||
<input id="estimated_completion_at" type="datetime-local" required>
|
<input id="estimated_completion_at" type="datetime-local" required>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="maximum_elevation_ft_amsl">Maximum elevation ft AMSL</label>
|
<label for="maximum_elevation_ft_agl">Maximum elevation ft AGL</label>
|
||||||
<input id="maximum_elevation_ft_amsl" type="number" min="0" required>
|
<input id="maximum_elevation_ft_agl" type="number" min="0" required>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="location_inside_frz">Inside FRZ</label>
|
<label for="location_inside_frz">Inside FRZ</label>
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
setValue('phone', request.phone);
|
setValue('phone', request.phone);
|
||||||
setValue('estimated_takeoff_at', toLocalInputValue(request.estimated_takeoff_at));
|
setValue('estimated_takeoff_at', toLocalInputValue(request.estimated_takeoff_at));
|
||||||
setValue('estimated_completion_at', toLocalInputValue(request.estimated_completion_at));
|
setValue('estimated_completion_at', toLocalInputValue(request.estimated_completion_at));
|
||||||
setValue('maximum_elevation_ft_amsl', request.maximum_elevation_ft_amsl);
|
setValue('maximum_elevation_ft_agl', request.maximum_elevation_ft_agl);
|
||||||
setValue('location_inside_frz', request.location_inside_frz ? 'Yes' : 'No');
|
setValue('location_inside_frz', request.location_inside_frz ? 'Yes' : 'No');
|
||||||
setValue('location_latitude', request.location_latitude);
|
setValue('location_latitude', request.location_latitude);
|
||||||
setValue('location_longitude', request.location_longitude);
|
setValue('location_longitude', request.location_longitude);
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
phone: value('phone') || null,
|
phone: value('phone') || null,
|
||||||
estimated_takeoff_at: fromLocalInputValue(value('estimated_takeoff_at')),
|
estimated_takeoff_at: fromLocalInputValue(value('estimated_takeoff_at')),
|
||||||
estimated_completion_at: fromLocalInputValue(value('estimated_completion_at')),
|
estimated_completion_at: fromLocalInputValue(value('estimated_completion_at')),
|
||||||
maximum_elevation_ft_amsl: Number(value('maximum_elevation_ft_amsl')),
|
maximum_elevation_ft_agl: Number(value('maximum_elevation_ft_agl')),
|
||||||
location_latitude: Number(value('location_latitude')),
|
location_latitude: Number(value('location_latitude')),
|
||||||
location_longitude: Number(value('location_longitude')),
|
location_longitude: Number(value('location_longitude')),
|
||||||
location_description: value('location_description') || null,
|
location_description: value('location_description') || null,
|
||||||
|
|||||||
@@ -795,7 +795,7 @@
|
|||||||
${field('Flyer ID', selectedRequest.flyer_id)}
|
${field('Flyer ID', selectedRequest.flyer_id)}
|
||||||
${field('Takeoff', formatDateTime(selectedRequest.estimated_takeoff_at))}
|
${field('Takeoff', formatDateTime(selectedRequest.estimated_takeoff_at))}
|
||||||
${field('Completion', formatDateTime(selectedRequest.estimated_completion_at))}
|
${field('Completion', formatDateTime(selectedRequest.estimated_completion_at))}
|
||||||
${field('Max Elevation', `${selectedRequest.maximum_elevation_ft_amsl} ft AMSL`)}
|
${field('Max Elevation', `${selectedRequest.maximum_elevation_ft_agl} ft AGL`)}
|
||||||
${field('Inside FRZ', selectedRequest.location_inside_frz === null ? '-' : (selectedRequest.location_inside_frz ? 'Yes' : 'No'))}
|
${field('Inside FRZ', selectedRequest.location_inside_frz === null ? '-' : (selectedRequest.location_inside_frz ? 'Yes' : 'No'))}
|
||||||
${field('Latitude', selectedRequest.location_latitude)}
|
${field('Latitude', selectedRequest.location_latitude)}
|
||||||
${field('Longitude', selectedRequest.location_longitude)}
|
${field('Longitude', selectedRequest.location_longitude)}
|
||||||
@@ -862,7 +862,7 @@
|
|||||||
addLayer(L.marker(point).addTo(map).bindPopup(`
|
addLayer(L.marker(point).addTo(map).bindPopup(`
|
||||||
<strong>${escapeHtml(selectedRequest.reference_number)}</strong><br>
|
<strong>${escapeHtml(selectedRequest.reference_number)}</strong><br>
|
||||||
${escapeHtml(selectedRequest.operator_name)}<br>
|
${escapeHtml(selectedRequest.operator_name)}<br>
|
||||||
${selectedRequest.maximum_elevation_ft_amsl} ft AMSL
|
${selectedRequest.maximum_elevation_ft_agl} ft AGL
|
||||||
`));
|
`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -472,7 +472,7 @@
|
|||||||
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)) {
|
if (['CANCELED', 'DELETED', 'LANDED', 'LOCAL', 'DEPARTED'].includes(ppr.status)) {
|
||||||
document.getElementById('update-btn').disabled = true;
|
document.getElementById('update-btn').disabled = true;
|
||||||
document.getElementById('cancel-btn').disabled = true;
|
document.getElementById('cancel-btn').disabled = true;
|
||||||
showNotification('This PPR can no longer be edited or cancelled online.', true);
|
showNotification('This PPR can no longer be edited or cancelled online.', true);
|
||||||
|
|||||||
+3
-3
@@ -829,10 +829,10 @@
|
|||||||
const toDisplay = await getAirportName(departure.out_to || '');
|
const toDisplay = await getAirportName(departure.out_to || '');
|
||||||
|
|
||||||
let timeDisplay, sortTime;
|
let timeDisplay, sortTime;
|
||||||
if (departure.status === 'DEPARTED' && departure.departed_dt) {
|
if (departure.status === 'DEPARTED' && departure.qsy_dt) {
|
||||||
const time = convertToLocalTime(departure.departed_dt);
|
const time = convertToLocalTime(departure.qsy_dt);
|
||||||
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">DEPARTED</span></div>`;
|
timeDisplay = `<div style="display: flex; align-items: center; gap: 8px;"><span style="color: #3498db; font-weight: bold;">${time}</span><span style="font-size: 0.7em; background: #3498db; color: white; padding: 2px 4px; border-radius: 3px; white-space: nowrap;">DEPARTED</span></div>`;
|
||||||
sortTime = departure.departed_dt;
|
sortTime = departure.qsy_dt;
|
||||||
} else {
|
} else {
|
||||||
timeDisplay = convertToLocalTime(departure.etd);
|
timeDisplay = convertToLocalTime(departure.etd);
|
||||||
sortTime = departure.etd;
|
sortTime = departure.etd;
|
||||||
|
|||||||
+7
-5
@@ -1181,7 +1181,7 @@
|
|||||||
row.className = 'clickable-row';
|
row.className = 'clickable-row';
|
||||||
row.onclick = () => openReportDetail('PPR', ppr.id);
|
row.onclick = () => openReportDetail('PPR', ppr.id);
|
||||||
|
|
||||||
const takeoff = ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '-';
|
const takeoff = ppr.takeoff_dt ? formatDateTime(ppr.takeoff_dt) : '-';
|
||||||
const landing = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
|
const landing = ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '-';
|
||||||
const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-';
|
const submitted = ppr.submitted_dt ? formatDateTime(ppr.submitted_dt) : '-';
|
||||||
|
|
||||||
@@ -1381,7 +1381,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPPRSortTime(ppr) {
|
function getPPRSortTime(ppr) {
|
||||||
return ppr.landed_dt || ppr.departed_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
|
return ppr.landed_dt || ppr.qsy_dt || ppr.takeoff_dt || ppr.eta || ppr.etd || ppr.submitted_dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailConfig = {
|
const detailConfig = {
|
||||||
@@ -1397,7 +1397,8 @@
|
|||||||
['Captain', r => r.captain],
|
['Captain', r => r.captain],
|
||||||
['From', r => r.in_from],
|
['From', r => r.in_from],
|
||||||
['To', r => r.out_to],
|
['To', r => r.out_to],
|
||||||
['Takeoff', r => formatOptionalDateTime(r.departed_dt)],
|
['Takeoff', r => formatOptionalDateTime(r.takeoff_dt)],
|
||||||
|
['QSY', r => formatOptionalDateTime(r.qsy_dt)],
|
||||||
['Landing', r => formatOptionalDateTime(r.landed_dt)],
|
['Landing', r => formatOptionalDateTime(r.landed_dt)],
|
||||||
['ETA', r => formatOptionalDateTime(r.eta)],
|
['ETA', r => formatOptionalDateTime(r.eta)],
|
||||||
['ETD', r => formatOptionalDateTime(r.etd)],
|
['ETD', r => formatOptionalDateTime(r.etd)],
|
||||||
@@ -1571,7 +1572,7 @@
|
|||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain',
|
'ID', 'Status', 'Aircraft Reg', 'Aircraft Type', 'Callsign', 'Captain',
|
||||||
'From', 'To', 'Takeoff', 'Landing', 'POB In', 'POB Out', 'Fuel',
|
'From', 'To', 'Takeoff', 'QSY', 'Landing', 'POB In', 'POB Out', 'Fuel',
|
||||||
'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
|
'Email', 'Phone', 'Notes', 'Submitted', 'Created By'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1584,7 +1585,8 @@
|
|||||||
ppr.captain,
|
ppr.captain,
|
||||||
ppr.in_from,
|
ppr.in_from,
|
||||||
ppr.out_to || '',
|
ppr.out_to || '',
|
||||||
ppr.departed_dt ? formatDateTime(ppr.departed_dt) : '',
|
ppr.takeoff_dt ? formatDateTime(ppr.takeoff_dt) : '',
|
||||||
|
ppr.qsy_dt ? formatDateTime(ppr.qsy_dt) : '',
|
||||||
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
|
ppr.landed_dt ? formatDateTime(ppr.landed_dt) : '',
|
||||||
ppr.pob_in,
|
ppr.pob_in,
|
||||||
ppr.pob_out || '',
|
ppr.pob_out || '',
|
||||||
|
|||||||
+14
-3
@@ -777,6 +777,9 @@
|
|||||||
|
|
||||||
const ppr = await response.json();
|
const ppr = await response.json();
|
||||||
populateForm(ppr);
|
populateForm(ppr);
|
||||||
|
const departedBtn = document.getElementById('btn-departed');
|
||||||
|
departedBtn.textContent = '🛫 Depart';
|
||||||
|
departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
|
||||||
|
|
||||||
// Show/hide quick action buttons based on current status
|
// Show/hide quick action buttons based on current status
|
||||||
if (ppr.status === 'NEW') {
|
if (ppr.status === 'NEW') {
|
||||||
@@ -789,8 +792,16 @@
|
|||||||
document.getElementById('btn-cancel').style.display = 'inline-block';
|
document.getElementById('btn-cancel').style.display = 'inline-block';
|
||||||
} else if (ppr.status === 'LANDED') {
|
} else if (ppr.status === 'LANDED') {
|
||||||
document.getElementById('btn-landed').style.display = 'none';
|
document.getElementById('btn-landed').style.display = 'none';
|
||||||
document.getElementById('btn-departed').style.display = 'inline-block';
|
departedBtn.style.display = 'inline-block';
|
||||||
|
departedBtn.textContent = '🛫 Take Off';
|
||||||
|
departedBtn.setAttribute('onclick', "showTimestampModal('LOCAL')");
|
||||||
document.getElementById('btn-cancel').style.display = 'inline-block';
|
document.getElementById('btn-cancel').style.display = 'inline-block';
|
||||||
|
} else if (ppr.status === 'LOCAL') {
|
||||||
|
document.getElementById('btn-landed').style.display = 'none';
|
||||||
|
departedBtn.style.display = 'inline-block';
|
||||||
|
departedBtn.textContent = 'QSY';
|
||||||
|
departedBtn.setAttribute('onclick', "showTimestampModal('DEPARTED')");
|
||||||
|
document.getElementById('btn-cancel').style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
// DEPARTED, CANCELED, DELETED - hide all quick actions and cancel button
|
// DEPARTED, CANCELED, DELETED - hide all quick actions and cancel button
|
||||||
document.querySelector('.quick-actions').style.display = 'none';
|
document.querySelector('.quick-actions').style.display = 'none';
|
||||||
@@ -2657,8 +2668,8 @@
|
|||||||
text: "Displays visiting aircraft and airport departures that are ready to leave Swansea today. Local flights are shown in their own table."
|
text: "Displays visiting aircraft and airport departures that are ready to leave Swansea today. Local flights are shown in their own table."
|
||||||
},
|
},
|
||||||
"local-flights": {
|
"local-flights": {
|
||||||
title: "Today's Local Flights",
|
title: "Local Traffic",
|
||||||
text: "Displays local and circuit flights booked out today, with shortcuts for contact, takeoff, circuit work, touch-and-go, and landing."
|
text: "Displays local traffic booked out today, including local flights, circuits, and PPR departures that are airborne locally before QSY."
|
||||||
},
|
},
|
||||||
overflights: {
|
overflights: {
|
||||||
title: "Active Overflights",
|
title: "Active Overflights",
|
||||||
|
|||||||
Reference in New Issue
Block a user