648 lines
26 KiB
Python
648 lines
26 KiB
Python
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, or_
|
|
from datetime import date, datetime, time
|
|
from app.api.deps import get_db, get_current_operator_user, get_current_read_user
|
|
from app.crud.crud_movement import movement as crud_movement
|
|
from app.crud.crud_journal import journal as crud_journal
|
|
from app.schemas.movement import BulkMovementContext, BulkMovementLog, BulkMovementResult, Movement, MovementCreate
|
|
from app.models.ppr import User
|
|
from app.models.arrival import Arrival, ArrivalStatus, SubmissionSource as ArrivalSubmissionSource
|
|
from app.models.circuit import Circuit
|
|
from app.models.departure import Departure, DepartureStatus, SubmissionSource as DepartureSubmissionSource
|
|
from app.models.journal import EntityType
|
|
from app.models.local_flight import LocalFlight, LocalFlightStatus, LocalFlightType, SubmissionSource as LocalSubmissionSource
|
|
from app.models.movement import Movement as MovementModel, MovementType
|
|
from app.models.overflight import Overflight, OverflightStatus
|
|
from app.models.ppr import PPRRecord, PPRStatus
|
|
from app.core.utils import get_client_ip
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=List[Movement])
|
|
async def get_movements(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
movement_type: Optional[MovementType] = None,
|
|
aircraft_registration: Optional[str] = None,
|
|
date_from: Optional[date] = None,
|
|
date_to: Optional[date] = None,
|
|
entity_type: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_read_user)
|
|
):
|
|
"""Get movement records with optional filtering"""
|
|
movements = crud_movement.get_multi(
|
|
db, skip=skip, limit=limit, movement_type=movement_type,
|
|
aircraft_registration=aircraft_registration, date_from=date_from,
|
|
date_to=date_to, entity_type=entity_type
|
|
)
|
|
return movements
|
|
|
|
|
|
def _clean_reg(registration: str) -> str:
|
|
return (registration or "").strip().upper()
|
|
|
|
|
|
def _clean_alnum(value: str) -> str:
|
|
return "".join(char for char in (value or "").upper() if char.isalnum())
|
|
|
|
|
|
def _sql_clean_alnum(column):
|
|
return func.upper(func.replace(func.replace(column, "-", ""), " ", ""))
|
|
|
|
|
|
def _combine_date_time(movement_date: date, movement_time: str) -> datetime:
|
|
if not movement_time:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="A time is required for this strip"
|
|
)
|
|
try:
|
|
parsed_time = time.fromisoformat(movement_time)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="movement_time must be HH:MM or HH:MM:SS"
|
|
)
|
|
return datetime.combine(movement_date, parsed_time)
|
|
|
|
|
|
def _kind_to_movement_type(flight_kind: str) -> MovementType:
|
|
kind = (flight_kind or "").strip().upper()
|
|
if kind == "ARRIVAL":
|
|
return MovementType.LANDING
|
|
if kind == "DEPARTURE":
|
|
return MovementType.TAKEOFF
|
|
if kind == "LOCAL":
|
|
return MovementType.TAKEOFF
|
|
if kind == "OVERFLIGHT":
|
|
return MovementType.OVERFLIGHT
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="flight_kind must be ARRIVAL, DEPARTURE, LOCAL or OVERFLIGHT"
|
|
)
|
|
|
|
|
|
def _strip_entity_type(flight_kind: str) -> Optional[str]:
|
|
kind = (flight_kind or "").strip().upper()
|
|
return {
|
|
"LOCAL": "LOCAL_FLIGHT",
|
|
"OVERFLIGHT": "OVERFLIGHT",
|
|
}.get(kind)
|
|
|
|
|
|
def _compatible_entity_types(flight_kind: str) -> List[str]:
|
|
kind = (flight_kind or "").strip().upper()
|
|
if kind == "ARRIVAL":
|
|
return ["PPR", "ARRIVAL"]
|
|
if kind == "DEPARTURE":
|
|
return ["PPR", "DEPARTURE"]
|
|
if kind == "LOCAL":
|
|
return ["LOCAL_FLIGHT"]
|
|
if kind == "OVERFLIGHT":
|
|
return ["OVERFLIGHT"]
|
|
return []
|
|
|
|
|
|
def _ppr_to_dict(ppr: PPRRecord) -> dict:
|
|
return {
|
|
"id": ppr.id,
|
|
"status": ppr.status.value,
|
|
"aircraft_registration": ppr.ac_reg,
|
|
"aircraft_type": ppr.ac_type,
|
|
"callsign": ppr.ac_call,
|
|
"from_location": ppr.in_from,
|
|
"to_location": ppr.out_to,
|
|
"eta": ppr.eta.isoformat() if ppr.eta else None,
|
|
"etd": ppr.etd.isoformat() if ppr.etd else None,
|
|
"pob_in": ppr.pob_in,
|
|
"pob_out": ppr.pob_out,
|
|
"notes": ppr.notes,
|
|
}
|
|
|
|
|
|
def _movement_to_dict(movement: MovementModel) -> dict:
|
|
return {
|
|
"id": movement.id,
|
|
"movement_type": movement.movement_type.value,
|
|
"aircraft_registration": movement.aircraft_registration,
|
|
"aircraft_type": movement.aircraft_type,
|
|
"callsign": movement.callsign,
|
|
"timestamp": movement.timestamp.isoformat() if movement.timestamp else None,
|
|
"entity_type": movement.entity_type,
|
|
"entity_id": movement.entity_id,
|
|
"from_location": movement.from_location,
|
|
"to_location": movement.to_location,
|
|
"runway": movement.runway,
|
|
"wind": movement.wind,
|
|
"pressure_setting": movement.pressure_setting,
|
|
"notes": movement.notes,
|
|
}
|
|
|
|
|
|
def _local_flight_to_dict(local: LocalFlight) -> dict:
|
|
return {
|
|
"id": local.id,
|
|
"aircraft_registration": local.registration,
|
|
"aircraft_type": local.type,
|
|
"callsign": local.callsign,
|
|
"pob": local.pob,
|
|
"flight_type": local.flight_type.value if local.flight_type else None,
|
|
"status": local.status.value if local.status else None,
|
|
"etd": local.etd.isoformat() if local.etd else None,
|
|
"takeoff_time": local.takeoff_dt.isoformat() if local.takeoff_dt else None,
|
|
"departed_time": local.departed_dt.isoformat() if local.departed_dt else None,
|
|
"landing_time": local.landed_dt.isoformat() if local.landed_dt else None,
|
|
"circuits": local.circuits,
|
|
"notes": local.notes,
|
|
}
|
|
|
|
|
|
def _build_suggestion(pprs: List[PPRRecord], movements: List[MovementModel], flight_kind: str) -> dict:
|
|
if movements:
|
|
movement = movements[0]
|
|
return {
|
|
"source": "movement",
|
|
"movement_id": movement.id,
|
|
"aircraft_registration": movement.aircraft_registration,
|
|
"aircraft_type": movement.aircraft_type,
|
|
"callsign": movement.callsign,
|
|
"movement_time": movement.timestamp.strftime("%H:%M") if movement.timestamp else None,
|
|
"from_location": movement.from_location,
|
|
"to_location": movement.to_location,
|
|
"runway": movement.runway,
|
|
"wind": movement.wind,
|
|
"pressure_setting": movement.pressure_setting,
|
|
"notes": movement.notes,
|
|
}
|
|
|
|
if pprs:
|
|
ppr = pprs[0]
|
|
is_arrival = flight_kind.upper() == "ARRIVAL"
|
|
timestamp = ppr.eta if is_arrival else ppr.etd
|
|
return {
|
|
"source": "ppr",
|
|
"ppr_id": ppr.id,
|
|
"aircraft_registration": ppr.ac_reg,
|
|
"aircraft_type": ppr.ac_type,
|
|
"callsign": ppr.ac_call,
|
|
"movement_time": timestamp.strftime("%H:%M") if timestamp else None,
|
|
"from_location": ppr.in_from,
|
|
"to_location": ppr.out_to,
|
|
"pob": ppr.pob_in if is_arrival else (ppr.pob_out or ppr.pob_in),
|
|
"notes": ppr.notes,
|
|
}
|
|
|
|
return {}
|
|
|
|
|
|
def _movement_for_entity(
|
|
db: Session,
|
|
entity_type: str,
|
|
entity_id: int,
|
|
movement_type: MovementType
|
|
) -> Optional[MovementModel]:
|
|
return db.query(MovementModel).filter(
|
|
MovementModel.entity_type == entity_type,
|
|
MovementModel.entity_id == entity_id,
|
|
MovementModel.movement_type == movement_type
|
|
).order_by(MovementModel.timestamp).first()
|
|
|
|
|
|
def _create_or_update_movement(db: Session, movement_data: MovementCreate) -> MovementModel:
|
|
existing = _movement_for_entity(
|
|
db,
|
|
movement_data.entity_type,
|
|
movement_data.entity_id,
|
|
movement_data.movement_type
|
|
)
|
|
if existing:
|
|
return crud_movement.update(db, existing, movement_data)
|
|
return crud_movement.create(db, movement_data)
|
|
|
|
|
|
def _clear_local_circuit_details(
|
|
db: Session,
|
|
local_flight: LocalFlight
|
|
) -> None:
|
|
db.query(Circuit).filter(Circuit.local_flight_id == local_flight.id).delete()
|
|
db.query(MovementModel).filter(
|
|
MovementModel.entity_type == "LOCAL_FLIGHT",
|
|
MovementModel.entity_id == local_flight.id,
|
|
MovementModel.movement_type == MovementType.TOUCH_AND_GO
|
|
).delete()
|
|
db.commit()
|
|
|
|
|
|
@router.get("/bulk-context", response_model=BulkMovementContext)
|
|
async def get_bulk_movement_context(
|
|
target_date: date,
|
|
aircraft_registration: str,
|
|
flight_kind: str = "ARRIVAL",
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_read_user)
|
|
):
|
|
"""Find same-day PPRs and movements that may match a bulk entry strip."""
|
|
clean_reg = _clean_reg(aircraft_registration)
|
|
clean_lookup = _clean_alnum(aircraft_registration)
|
|
movement_type = _kind_to_movement_type(flight_kind)
|
|
entity_type_filter = _strip_entity_type(flight_kind)
|
|
|
|
pprs = []
|
|
local_flights = []
|
|
if clean_lookup and flight_kind.upper() != "LOCAL":
|
|
pprs = db.query(PPRRecord).filter(
|
|
_sql_clean_alnum(PPRRecord.ac_reg).like(f"{clean_lookup}%"),
|
|
or_(
|
|
func.date(PPRRecord.eta) == target_date,
|
|
func.date(PPRRecord.etd) == target_date
|
|
),
|
|
PPRRecord.status != PPRStatus.DELETED
|
|
).order_by(PPRRecord.eta).limit(10).all()
|
|
|
|
if clean_lookup and flight_kind.upper() == "LOCAL":
|
|
local_flights = db.query(LocalFlight).filter(
|
|
_sql_clean_alnum(LocalFlight.registration).like(f"{clean_lookup}%"),
|
|
or_(
|
|
func.date(LocalFlight.takeoff_dt) == target_date,
|
|
func.date(LocalFlight.departed_dt) == target_date,
|
|
func.date(LocalFlight.landed_dt) == target_date,
|
|
func.date(LocalFlight.etd) == target_date,
|
|
func.date(LocalFlight.created_dt) == target_date
|
|
),
|
|
LocalFlight.flight_type.in_([LocalFlightType.LOCAL, LocalFlightType.CIRCUITS]),
|
|
LocalFlight.status != LocalFlightStatus.CANCELLED
|
|
).order_by(LocalFlight.takeoff_dt, LocalFlight.etd, LocalFlight.created_dt).limit(10).all()
|
|
|
|
movements = []
|
|
if clean_lookup:
|
|
movements = db.query(MovementModel).filter(
|
|
func.date(MovementModel.timestamp) == target_date,
|
|
_sql_clean_alnum(MovementModel.aircraft_registration).like(f"{clean_lookup}%"),
|
|
MovementModel.movement_type == movement_type
|
|
).order_by(MovementModel.timestamp.desc()).limit(10).all()
|
|
compatible_types = _compatible_entity_types(flight_kind)
|
|
if compatible_types:
|
|
movements = [movement for movement in movements if movement.entity_type in compatible_types]
|
|
|
|
return BulkMovementContext(
|
|
pprs=[_ppr_to_dict(ppr) for ppr in pprs],
|
|
local_flights=[_local_flight_to_dict(local) for local in local_flights],
|
|
movements=[_movement_to_dict(movement) for movement in movements],
|
|
suggested=_build_suggestion(pprs, movements, flight_kind)
|
|
)
|
|
|
|
|
|
@router.post("/bulk-log", response_model=BulkMovementResult)
|
|
async def bulk_log_movement(
|
|
request: Request,
|
|
entry: BulkMovementLog,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_operator_user)
|
|
):
|
|
"""Create or update one same-day movement from a paper strip."""
|
|
client_ip = get_client_ip(request)
|
|
username = current_user.username
|
|
clean_reg = _clean_reg(entry.aircraft_registration)
|
|
clean_lookup = _clean_alnum(entry.aircraft_registration)
|
|
if not clean_reg:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Aircraft registration is required")
|
|
|
|
movement_type = _kind_to_movement_type(entry.flight_kind)
|
|
flight_kind = entry.flight_kind.strip().upper()
|
|
primary_time = (
|
|
entry.landing_time if flight_kind == "ARRIVAL"
|
|
else entry.takeoff_time if flight_kind in ("DEPARTURE", "LOCAL")
|
|
else entry.contact_time if flight_kind == "OVERFLIGHT"
|
|
else entry.movement_time
|
|
) or entry.movement_time
|
|
timestamp = _combine_date_time(entry.movement_date, primary_time)
|
|
existing_movement = crud_movement.get(db, entry.movement_id) if entry.movement_id and flight_kind != "LOCAL" else None
|
|
if not existing_movement and flight_kind != "LOCAL":
|
|
existing_movement = crud_movement.find_daily_match(
|
|
db,
|
|
entry.movement_date,
|
|
clean_reg,
|
|
movement_type,
|
|
entity_type=_strip_entity_type(flight_kind),
|
|
entity_types=None if _strip_entity_type(flight_kind) else _compatible_entity_types(flight_kind)
|
|
)
|
|
if existing_movement and existing_movement.entity_type not in _compatible_entity_types(flight_kind):
|
|
existing_movement = None
|
|
|
|
if flight_kind == "LOCAL":
|
|
takeoff_dt = _combine_date_time(entry.movement_date, entry.takeoff_time)
|
|
landing_dt = _combine_date_time(entry.movement_date, entry.landing_time)
|
|
if landing_dt < takeoff_dt:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="LOCAL landing time cannot be before takeoff time"
|
|
)
|
|
local_type = LocalFlightType.CIRCUITS if (entry.local_nature or "").upper() == "CIRCUITS" else LocalFlightType.LOCAL
|
|
local = LocalFlight(
|
|
registration=clean_reg,
|
|
type=entry.aircraft_type or "",
|
|
callsign=entry.callsign,
|
|
pob=entry.pob,
|
|
flight_type=local_type,
|
|
status=LocalFlightStatus.LANDED,
|
|
duration=int((landing_dt - takeoff_dt).total_seconds() / 60) if landing_dt > takeoff_dt else None,
|
|
circuits=entry.circuits or 0,
|
|
notes=entry.notes,
|
|
etd=takeoff_dt,
|
|
departed_dt=takeoff_dt,
|
|
takeoff_dt=takeoff_dt,
|
|
landed_dt=landing_dt,
|
|
created_by=username,
|
|
submitted_via=LocalSubmissionSource.ADMIN
|
|
)
|
|
db.add(local)
|
|
db.commit()
|
|
db.refresh(local)
|
|
action = "created"
|
|
crud_journal.log_change(db, EntityType.LOCAL_FLIGHT, local.id, "Local strip created from bulk flight log", username, client_ip)
|
|
|
|
takeoff_movement = _create_or_update_movement(db, MovementCreate(
|
|
movement_type=MovementType.TAKEOFF,
|
|
aircraft_registration=clean_reg,
|
|
aircraft_type=local.type,
|
|
callsign=local.callsign,
|
|
timestamp=takeoff_dt,
|
|
entity_type="LOCAL_FLIGHT",
|
|
entity_id=local.id,
|
|
runway=entry.runway,
|
|
wind=entry.wind,
|
|
pressure_setting=entry.pressure_setting,
|
|
created_by=username,
|
|
ip_address=client_ip,
|
|
notes=entry.notes
|
|
))
|
|
_create_or_update_movement(db, MovementCreate(
|
|
movement_type=MovementType.LANDING,
|
|
aircraft_registration=clean_reg,
|
|
aircraft_type=local.type,
|
|
callsign=local.callsign,
|
|
timestamp=landing_dt,
|
|
entity_type="LOCAL_FLIGHT",
|
|
entity_id=local.id,
|
|
runway=entry.runway,
|
|
wind=entry.wind,
|
|
pressure_setting=entry.pressure_setting,
|
|
created_by=username,
|
|
ip_address=client_ip,
|
|
notes=entry.notes
|
|
))
|
|
_clear_local_circuit_details(db, local)
|
|
crud_journal.log_change(db, EntityType.LOCAL_FLIGHT, local.id, f"Bulk local strip {action}: takeoff {takeoff_dt.strftime('%H:%M')}, landing {landing_dt.strftime('%H:%M')}, circuits {entry.circuits or 0}", username, client_ip)
|
|
return BulkMovementResult(action=action, movement=takeoff_movement, entity_type="LOCAL_FLIGHT", entity_id=local.id, message=f"Local strip {action} for {clean_reg}")
|
|
|
|
if flight_kind == "OVERFLIGHT":
|
|
contact_dt = _combine_date_time(entry.movement_date, entry.contact_time)
|
|
qsy_dt = _combine_date_time(entry.movement_date, entry.qsy_time) if entry.qsy_time else None
|
|
overflight = None
|
|
if existing_movement and existing_movement.entity_type == "OVERFLIGHT":
|
|
overflight = db.query(Overflight).filter(Overflight.id == existing_movement.entity_id).first()
|
|
if not overflight:
|
|
overflight = db.query(Overflight).filter(
|
|
_sql_clean_alnum(Overflight.registration) == clean_lookup,
|
|
func.date(Overflight.call_dt) == entry.movement_date
|
|
).first()
|
|
if not overflight:
|
|
overflight = Overflight(
|
|
registration=clean_reg,
|
|
pob=entry.pob,
|
|
type=entry.aircraft_type,
|
|
departure_airfield=entry.from_location,
|
|
destination_airfield=entry.to_location,
|
|
status=OverflightStatus.INACTIVE if qsy_dt else OverflightStatus.ACTIVE,
|
|
call_dt=contact_dt,
|
|
qsy_dt=qsy_dt,
|
|
notes=entry.notes,
|
|
created_by=username
|
|
)
|
|
db.add(overflight)
|
|
db.commit()
|
|
db.refresh(overflight)
|
|
action = "created"
|
|
crud_journal.log_change(db, EntityType.OVERFLIGHT, overflight.id, "Overflight strip created from bulk flight log", username, client_ip)
|
|
else:
|
|
overflight.registration = clean_reg
|
|
overflight.pob = entry.pob
|
|
overflight.type = entry.aircraft_type
|
|
overflight.departure_airfield = entry.from_location
|
|
overflight.destination_airfield = entry.to_location
|
|
overflight.status = OverflightStatus.INACTIVE if qsy_dt else OverflightStatus.ACTIVE
|
|
overflight.call_dt = contact_dt
|
|
overflight.qsy_dt = qsy_dt
|
|
overflight.notes = entry.notes
|
|
db.add(overflight)
|
|
db.commit()
|
|
db.refresh(overflight)
|
|
action = "updated"
|
|
|
|
movement = _create_or_update_movement(db, MovementCreate(
|
|
movement_type=MovementType.OVERFLIGHT,
|
|
aircraft_registration=clean_reg,
|
|
aircraft_type=entry.aircraft_type,
|
|
callsign=entry.callsign,
|
|
timestamp=contact_dt,
|
|
entity_type="OVERFLIGHT",
|
|
entity_id=overflight.id,
|
|
from_location=entry.from_location,
|
|
to_location=entry.to_location,
|
|
runway=entry.runway,
|
|
wind=entry.wind,
|
|
pressure_setting=entry.pressure_setting,
|
|
created_by=username,
|
|
ip_address=client_ip,
|
|
notes=entry.notes
|
|
))
|
|
crud_journal.log_change(db, EntityType.OVERFLIGHT, overflight.id, f"Bulk overflight strip {action}: contact {contact_dt.strftime('%H:%M')}" + (f", QSY {qsy_dt.strftime('%H:%M')}" if qsy_dt else ""), username, client_ip)
|
|
return BulkMovementResult(action=action, movement=movement, entity_type="OVERFLIGHT", entity_id=overflight.id, message=f"Overflight strip {action} for {clean_reg}")
|
|
|
|
ppr = None
|
|
if entry.ppr_id:
|
|
ppr = db.query(PPRRecord).filter(PPRRecord.id == entry.ppr_id).first()
|
|
if not ppr and existing_movement and existing_movement.entity_type == "PPR":
|
|
ppr = db.query(PPRRecord).filter(PPRRecord.id == existing_movement.entity_id).first()
|
|
if not ppr:
|
|
ppr = db.query(PPRRecord).filter(
|
|
_sql_clean_alnum(PPRRecord.ac_reg) == clean_lookup,
|
|
or_(
|
|
func.date(PPRRecord.eta) == entry.movement_date,
|
|
func.date(PPRRecord.etd) == entry.movement_date
|
|
),
|
|
PPRRecord.status != PPRStatus.DELETED
|
|
).order_by(PPRRecord.eta).first()
|
|
|
|
entity_type = existing_movement.entity_type if existing_movement else None
|
|
entity_id = existing_movement.entity_id if existing_movement else None
|
|
|
|
if not entity_type:
|
|
if ppr:
|
|
entity_type = "PPR"
|
|
entity_id = ppr.id
|
|
elif movement_type == MovementType.LANDING:
|
|
arrival = Arrival(
|
|
registration=clean_reg,
|
|
type=entry.aircraft_type,
|
|
callsign=entry.callsign,
|
|
pob=entry.pob or 1,
|
|
in_from=entry.from_location or "ZZZZ",
|
|
status=ArrivalStatus.LANDED,
|
|
notes=entry.notes,
|
|
eta=timestamp,
|
|
landed_dt=timestamp,
|
|
created_by=username,
|
|
submitted_via=ArrivalSubmissionSource.ADMIN
|
|
)
|
|
db.add(arrival)
|
|
db.commit()
|
|
db.refresh(arrival)
|
|
entity_type = "ARRIVAL"
|
|
entity_id = arrival.id
|
|
crud_journal.log_change(db, EntityType.ARRIVAL, arrival.id, "Arrival created from bulk flight log", username, client_ip)
|
|
else:
|
|
departure = Departure(
|
|
registration=clean_reg,
|
|
type=entry.aircraft_type,
|
|
callsign=entry.callsign,
|
|
pob=entry.pob or 1,
|
|
out_to=entry.to_location or "ZZZZ",
|
|
status=DepartureStatus.DEPARTED,
|
|
notes=entry.notes,
|
|
etd=timestamp,
|
|
takeoff_dt=timestamp,
|
|
departed_dt=timestamp,
|
|
created_by=username,
|
|
submitted_via=DepartureSubmissionSource.ADMIN
|
|
)
|
|
db.add(departure)
|
|
db.commit()
|
|
db.refresh(departure)
|
|
entity_type = "DEPARTURE"
|
|
entity_id = departure.id
|
|
crud_journal.log_change(db, EntityType.DEPARTURE, departure.id, "Departure created from bulk flight log", username, client_ip)
|
|
|
|
movement_data = MovementCreate(
|
|
movement_type=movement_type,
|
|
aircraft_registration=clean_reg,
|
|
aircraft_type=entry.aircraft_type,
|
|
callsign=entry.callsign,
|
|
timestamp=timestamp,
|
|
entity_type=entity_type,
|
|
entity_id=entity_id,
|
|
from_location=entry.from_location,
|
|
to_location=entry.to_location,
|
|
runway=entry.runway,
|
|
wind=entry.wind,
|
|
pressure_setting=entry.pressure_setting,
|
|
created_by=username,
|
|
ip_address=client_ip,
|
|
notes=entry.notes
|
|
)
|
|
|
|
if existing_movement:
|
|
movement = crud_movement.update(db, existing_movement, movement_data)
|
|
action = "updated"
|
|
else:
|
|
movement = crud_movement.create(db, movement_data)
|
|
action = "created"
|
|
|
|
if entity_type == "PPR" and ppr:
|
|
ppr.ac_type = entry.aircraft_type or ppr.ac_type
|
|
ppr.ac_call = entry.callsign or ppr.ac_call
|
|
if movement_type == MovementType.LANDING:
|
|
ppr.in_from = entry.from_location or ppr.in_from
|
|
ppr.pob_in = entry.pob or ppr.pob_in
|
|
ppr.landed_dt = timestamp
|
|
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED, PPRStatus.DEPARTED):
|
|
ppr.status = PPRStatus.LANDED
|
|
else:
|
|
ppr.out_to = entry.to_location or ppr.out_to
|
|
ppr.pob_out = entry.pob or ppr.pob_out
|
|
ppr.takeoff_dt = timestamp
|
|
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED):
|
|
ppr.status = PPRStatus.DEPARTED
|
|
if entry.notes:
|
|
ppr.notes = entry.notes
|
|
db.add(ppr)
|
|
db.commit()
|
|
crud_journal.log_change(
|
|
db,
|
|
EntityType.PPR,
|
|
ppr.id,
|
|
f"Bulk flight log {action}: {movement_type.value} at {timestamp.strftime('%Y-%m-%d %H:%M')}",
|
|
username,
|
|
client_ip
|
|
)
|
|
elif entity_type == "ARRIVAL":
|
|
arrival = db.query(Arrival).filter(Arrival.id == entity_id).first()
|
|
if arrival:
|
|
arrival.registration = clean_reg
|
|
arrival.type = entry.aircraft_type
|
|
arrival.callsign = entry.callsign
|
|
arrival.pob = entry.pob or arrival.pob
|
|
arrival.in_from = entry.from_location or arrival.in_from
|
|
arrival.eta = timestamp
|
|
arrival.landed_dt = timestamp
|
|
arrival.status = ArrivalStatus.LANDED
|
|
arrival.notes = entry.notes
|
|
db.add(arrival)
|
|
db.commit()
|
|
crud_journal.log_change(db, EntityType.ARRIVAL, arrival.id, f"Bulk flight log {action}: landing at {timestamp.strftime('%Y-%m-%d %H:%M')}", username, client_ip)
|
|
elif entity_type == "DEPARTURE":
|
|
departure = db.query(Departure).filter(Departure.id == entity_id).first()
|
|
if departure:
|
|
departure.registration = clean_reg
|
|
departure.type = entry.aircraft_type
|
|
departure.callsign = entry.callsign
|
|
departure.pob = entry.pob or departure.pob
|
|
departure.out_to = entry.to_location or departure.out_to
|
|
departure.etd = timestamp
|
|
departure.takeoff_dt = timestamp
|
|
departure.departed_dt = timestamp
|
|
departure.status = DepartureStatus.DEPARTED
|
|
departure.notes = entry.notes
|
|
db.add(departure)
|
|
db.commit()
|
|
crud_journal.log_change(db, EntityType.DEPARTURE, departure.id, f"Bulk flight log {action}: takeoff at {timestamp.strftime('%Y-%m-%d %H:%M')}", username, client_ip)
|
|
|
|
if hasattr(request.app.state, 'connection_manager'):
|
|
await request.app.state.connection_manager.broadcast({
|
|
"type": "movement_bulk_logged",
|
|
"data": {
|
|
"id": movement.id,
|
|
"aircraft_registration": movement.aircraft_registration,
|
|
"movement_type": movement.movement_type.value,
|
|
"action": action
|
|
}
|
|
})
|
|
|
|
return BulkMovementResult(
|
|
action=action,
|
|
movement=movement,
|
|
entity_type=entity_type,
|
|
entity_id=entity_id,
|
|
message=f"Movement {action} for {clean_reg}"
|
|
)
|
|
|
|
|
|
@router.get("/{movement_id}", response_model=Movement)
|
|
async def get_movement(
|
|
movement_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_read_user)
|
|
):
|
|
"""Get a specific movement record"""
|
|
movement = crud_movement.get(db, movement_id=movement_id)
|
|
if not movement:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Movement record not found"
|
|
)
|
|
return movement
|