230 lines
8.0 KiB
Python
230 lines
8.0 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlmodel import Session, select
|
|
from typing import List, Optional
|
|
from sqlalchemy import func
|
|
|
|
from app.core.database import get_session
|
|
from app.models.route import Route
|
|
from app.models.route_stop import RouteStop
|
|
from app.schemas.route import RouteResponse, RouteCreate, RouteUpdate
|
|
from app.schemas.route_stop import RouteStopCreate, RouteStopUpdate
|
|
from app.api.deps import get_current_admin
|
|
|
|
router = APIRouter(prefix="/api/routes", tags=["routes"])
|
|
|
|
|
|
@router.get("", response_model=List[RouteResponse])
|
|
async def get_routes(
|
|
origin_city: Optional[str] = Query(None),
|
|
destination_city: Optional[str] = Query(None),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
"""Get all routes with optional filtering by origin and destination city."""
|
|
statement = select(Route)
|
|
|
|
if origin_city:
|
|
statement = statement.where(Route.origin_city.contains(origin_city))
|
|
|
|
if destination_city:
|
|
statement = statement.where(Route.destination_city.contains(destination_city))
|
|
|
|
routes = session.exec(statement).all()
|
|
return routes
|
|
|
|
|
|
@router.get("/{route_id}", response_model=RouteResponse)
|
|
async def get_route(route_id: str, session: Session = Depends(get_session)):
|
|
"""Get a single route by ID."""
|
|
route = session.get(Route, route_id)
|
|
if not route:
|
|
raise HTTPException(status_code=404, detail="Route not found")
|
|
return route
|
|
|
|
|
|
@router.get("/{route_id}/stops")
|
|
async def get_route_stops(route_id: str, session: Session = Depends(get_session)):
|
|
"""Get all stops for a route."""
|
|
from app.models.route_stop import RouteStop
|
|
from app.models.bus_stop import BusStop
|
|
|
|
statement = select(RouteStop, BusStop).join(
|
|
BusStop, RouteStop.stop_id == BusStop.id
|
|
).where(RouteStop.route_id == route_id).order_by(RouteStop.stop_order)
|
|
|
|
results = session.exec(statement).all()
|
|
# Merge RouteStop data into BusStop response
|
|
stops = []
|
|
for route_stop, bus_stop in results:
|
|
stop_data = bus_stop.model_dump()
|
|
stop_data['stop_order'] = route_stop.stop_order
|
|
stop_data['travel_time_minutes'] = route_stop.travel_time_minutes
|
|
stop_data['stop_delay_minutes'] = route_stop.stop_delay_minutes
|
|
stop_data['is_pickup_point'] = route_stop.is_pickup_point
|
|
stop_data['is_dropoff_point'] = route_stop.is_dropoff_point
|
|
stops.append(stop_data)
|
|
|
|
return stops
|
|
|
|
@router.post("", response_model=RouteResponse)
|
|
async def create_route(
|
|
route: RouteCreate,
|
|
session: Session = Depends(get_session),
|
|
_: bool = Depends(get_current_admin)
|
|
):
|
|
"""Create a new route (Admin only)."""
|
|
db_route = Route.model_validate(route)
|
|
session.add(db_route)
|
|
session.commit()
|
|
session.refresh(db_route)
|
|
return db_route
|
|
|
|
@router.put("/{route_id}", response_model=RouteResponse)
|
|
async def update_route(
|
|
route_id: str,
|
|
route_update: RouteUpdate,
|
|
session: Session = Depends(get_session),
|
|
_: bool = Depends(get_current_admin)
|
|
):
|
|
"""Update a route (Admin only)."""
|
|
db_route = session.get(Route, route_id)
|
|
if not db_route:
|
|
raise HTTPException(status_code=404, detail="Route not found")
|
|
|
|
route_data = route_update.model_dump(exclude_unset=True)
|
|
for key, value in route_data.items():
|
|
setattr(db_route, key, value)
|
|
|
|
session.add(db_route)
|
|
session.commit()
|
|
session.refresh(db_route)
|
|
return db_route
|
|
|
|
@router.delete("/{route_id}")
|
|
async def delete_route(
|
|
route_id: str,
|
|
session: Session = Depends(get_session),
|
|
_: bool = Depends(get_current_admin)
|
|
):
|
|
"""Delete a route (Admin only)."""
|
|
db_route = session.get(Route, route_id)
|
|
if not db_route:
|
|
raise HTTPException(status_code=404, detail="Route not found")
|
|
|
|
session.delete(db_route)
|
|
session.commit()
|
|
return {"ok": True}
|
|
|
|
# Route Stop Management with Cascade
|
|
|
|
@router.post("/{route_id}/stops")
|
|
async def add_stop_to_route(
|
|
route_id: str,
|
|
stop_data: RouteStopCreate,
|
|
session: Session = Depends(get_session),
|
|
_: bool = Depends(get_current_admin)
|
|
):
|
|
"""Add a stop to a route with cascading order adjustment."""
|
|
# 1. Check if route exists
|
|
route = session.get(Route, route_id)
|
|
if not route:
|
|
raise HTTPException(status_code=404, detail="Route not found")
|
|
|
|
# 2. Determine stop order
|
|
if stop_data.stop_order is None:
|
|
# Append to end
|
|
max_order = session.exec(select(func.max(RouteStop.stop_order)).where(RouteStop.route_id == route_id)).one()
|
|
# handle case where max_order is None (no stops)
|
|
stop_data.stop_order = (max_order or 0) + 1
|
|
else:
|
|
# Shift existing stops equal to or greater than new order
|
|
existing_stops = session.exec(
|
|
select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_order >= stop_data.stop_order)
|
|
).all()
|
|
for stop in existing_stops:
|
|
stop.stop_order += 1
|
|
session.add(stop)
|
|
|
|
# 3. Create new RouteStop
|
|
new_stop = RouteStop(
|
|
route_id=route_id,
|
|
stop_id=stop_data.stop_id,
|
|
stop_order=stop_data.stop_order,
|
|
travel_time_minutes=stop_data.travel_time_minutes,
|
|
stop_delay_minutes=stop_data.stop_delay_minutes or 0,
|
|
is_pickup_point=stop_data.is_pickup_point,
|
|
is_dropoff_point=stop_data.is_dropoff_point
|
|
)
|
|
session.add(new_stop)
|
|
session.commit()
|
|
session.refresh(new_stop)
|
|
return new_stop
|
|
|
|
@router.put("/{route_id}/stops/{stop_id}")
|
|
async def update_route_stop_order(
|
|
route_id: str,
|
|
stop_id: str,
|
|
update_data: RouteStopUpdate,
|
|
session: Session = Depends(get_session),
|
|
_: bool = Depends(get_current_admin)
|
|
):
|
|
"""Update a route stop, potentially reordering others."""
|
|
# This assumes we find the connection by route_id and stop_id.
|
|
# NOTE: If a stop is on a route multiple times, this logic needs ID, but for now assuming unique stop per route.
|
|
# Actually RouteStop has its own ID but we are using stop_id in path. Let's find the RouteStop entry.
|
|
|
|
route_stop = session.exec(
|
|
select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_id == stop_id)
|
|
).first()
|
|
|
|
if not route_stop:
|
|
raise HTTPException(status_code=404, detail="Stop not found on this route")
|
|
|
|
old_order = route_stop.stop_order
|
|
|
|
# Update fields
|
|
if update_data.is_pickup_point is not None:
|
|
route_stop.is_pickup_point = update_data.is_pickup_point
|
|
if update_data.is_dropoff_point is not None:
|
|
route_stop.is_dropoff_point = update_data.is_dropoff_point
|
|
if update_data.travel_time_minutes is not None:
|
|
route_stop.travel_time_minutes = update_data.travel_time_minutes
|
|
if update_data.stop_delay_minutes is not None:
|
|
route_stop.stop_delay_minutes = update_data.stop_delay_minutes
|
|
|
|
# Reordering logic
|
|
if update_data.stop_order is not None and update_data.stop_order != old_order:
|
|
new_order = update_data.stop_order
|
|
|
|
if new_order > old_order:
|
|
# Moving down: shift stops between old+1 and new DOWN (-1)
|
|
stops_to_shift = session.exec(
|
|
select(RouteStop).where(
|
|
RouteStop.route_id == route_id,
|
|
RouteStop.stop_order > old_order,
|
|
RouteStop.stop_order <= new_order
|
|
)
|
|
).all()
|
|
for s in stops_to_shift:
|
|
s.stop_order -= 1
|
|
session.add(s)
|
|
else:
|
|
# Moving up: shift stops between new and old-1 UP (+1)
|
|
stops_to_shift = session.exec(
|
|
select(RouteStop).where(
|
|
RouteStop.route_id == route_id,
|
|
RouteStop.stop_order >= new_order,
|
|
RouteStop.stop_order < old_order
|
|
)
|
|
).all()
|
|
for s in stops_to_shift:
|
|
s.stop_order += 1
|
|
session.add(s)
|
|
|
|
route_stop.stop_order = new_order
|
|
|
|
session.add(route_stop)
|
|
session.commit()
|
|
session.refresh(route_stop)
|
|
return route_stop
|
|
|