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