""" Endpoints para gestión de viajes. """ from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.core.database import get_db from app.core.security import get_current_user from app.models.usuario import Usuario from app.models.viaje import Viaje from app.schemas.viaje import ( ViajeResponse, ViajeResumen, ViajeConParadas, ViajeReplayData, ParadaResponse, ) from app.schemas.ubicacion import UbicacionResponse from app.services.viaje_service import ViajeService router = APIRouter(prefix="/viajes", tags=["Viajes"]) @router.get("", response_model=List[ViajeResumen]) async def listar_viajes( vehiculo_id: Optional[int] = None, conductor_id: Optional[int] = None, estado: Optional[str] = None, desde: Optional[datetime] = None, hasta: Optional[datetime] = None, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Lista viajes con filtros opcionales. Args: vehiculo_id: Filtrar por vehículo. conductor_id: Filtrar por conductor. estado: Filtrar por estado. desde: Fecha inicio. hasta: Fecha fin. skip: Registros a saltar. limit: Límite de registros. Returns: Lista de viajes. """ query = ( select(Viaje) .options( selectinload(Viaje.vehiculo), selectinload(Viaje.conductor), ) .order_by(Viaje.inicio_tiempo.desc()) ) if vehiculo_id: query = query.where(Viaje.vehiculo_id == vehiculo_id) if conductor_id: query = query.where(Viaje.conductor_id == conductor_id) if estado: query = query.where(Viaje.estado == estado) if desde: query = query.where(Viaje.inicio_tiempo >= desde) if hasta: query = query.where(Viaje.inicio_tiempo <= hasta) query = query.offset(skip).limit(limit) result = await db.execute(query) viajes = result.scalars().all() return [ ViajeResumen( id=v.id, vehiculo_id=v.vehiculo_id, vehiculo_nombre=v.vehiculo.nombre if v.vehiculo else None, vehiculo_placa=v.vehiculo.placa if v.vehiculo else None, conductor_nombre=v.conductor.nombre_completo if v.conductor else None, inicio_tiempo=v.inicio_tiempo, fin_tiempo=v.fin_tiempo, inicio_direccion=v.inicio_direccion, fin_direccion=v.fin_direccion, distancia_km=v.distancia_km, duracion_formateada=v.duracion_formateada, estado=v.estado, ) for v in viajes ] @router.get("/{viaje_id}", response_model=ViajeConParadas) async def obtener_viaje( viaje_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene un viaje por su ID con paradas. Args: viaje_id: ID del viaje. Returns: Viaje con paradas. """ result = await db.execute( select(Viaje) .options( selectinload(Viaje.vehiculo), selectinload(Viaje.conductor), selectinload(Viaje.paradas), ) .where(Viaje.id == viaje_id) ) viaje = result.scalar_one_or_none() if not viaje: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Viaje con id {viaje_id} no encontrado", ) return ViajeConParadas( id=viaje.id, vehiculo_id=viaje.vehiculo_id, conductor_id=viaje.conductor_id, proposito=viaje.proposito, notas=viaje.notas, inicio_tiempo=viaje.inicio_tiempo, fin_tiempo=viaje.fin_tiempo, inicio_lat=viaje.inicio_lat, inicio_lng=viaje.inicio_lng, inicio_direccion=viaje.inicio_direccion, fin_lat=viaje.fin_lat, fin_lng=viaje.fin_lng, fin_direccion=viaje.fin_direccion, distancia_km=viaje.distancia_km, duracion_segundos=viaje.duracion_segundos, tiempo_movimiento_segundos=viaje.tiempo_movimiento_segundos, tiempo_parado_segundos=viaje.tiempo_parado_segundos, velocidad_promedio=viaje.velocidad_promedio, velocidad_maxima=viaje.velocidad_maxima, combustible_usado=viaje.combustible_usado, rendimiento=viaje.rendimiento, odometro_inicio=viaje.odometro_inicio, odometro_fin=viaje.odometro_fin, estado=viaje.estado, puntos_gps=viaje.puntos_gps, duracion_formateada=viaje.duracion_formateada, en_curso=viaje.en_curso, creado_en=viaje.creado_en, actualizado_en=viaje.actualizado_en, paradas=[ ParadaResponse( id=p.id, viaje_id=p.viaje_id, vehiculo_id=p.vehiculo_id, inicio_tiempo=p.inicio_tiempo, fin_tiempo=p.fin_tiempo, duracion_segundos=p.duracion_segundos, lat=p.lat, lng=p.lng, direccion=p.direccion, tipo=p.tipo, motor_apagado=p.motor_apagado, poi_id=p.poi_id, geocerca_id=p.geocerca_id, en_curso=p.en_curso, notas=p.notas, duracion_formateada=p.duracion_formateada, ) for p in viaje.paradas ], ) @router.get("/{viaje_id}/replay") async def obtener_replay_viaje( viaje_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene datos para replay de un viaje. Args: viaje_id: ID del viaje. Returns: Viaje con ubicaciones y paradas. """ viaje_service = ViajeService(db) datos = await viaje_service.obtener_replay_viaje(viaje_id) if not datos: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Viaje con id {viaje_id} no encontrado", ) viaje = datos["viaje"] return { "viaje": { "id": viaje.id, "vehiculo_id": viaje.vehiculo_id, "inicio_tiempo": viaje.inicio_tiempo, "fin_tiempo": viaje.fin_tiempo, "inicio_lat": viaje.inicio_lat, "inicio_lng": viaje.inicio_lng, "fin_lat": viaje.fin_lat, "fin_lng": viaje.fin_lng, "distancia_km": viaje.distancia_km, "duracion_formateada": viaje.duracion_formateada, "estado": viaje.estado, }, "ubicaciones": [ { "tiempo": u.tiempo, "lat": u.lat, "lng": u.lng, "velocidad": u.velocidad, "rumbo": u.rumbo, "motor_encendido": u.motor_encendido, } for u in datos["ubicaciones"] ], "paradas": [ { "id": p.id, "inicio_tiempo": p.inicio_tiempo, "fin_tiempo": p.fin_tiempo, "duracion_formateada": p.duracion_formateada, "lat": p.lat, "lng": p.lng, "direccion": p.direccion, "tipo": p.tipo, } for p in datos["paradas"] ], } @router.get("/{viaje_id}/geojson") async def obtener_viaje_geojson( viaje_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene la ruta de un viaje en formato GeoJSON. Args: viaje_id: ID del viaje. Returns: GeoJSON LineString de la ruta. """ viaje_service = ViajeService(db) datos = await viaje_service.obtener_replay_viaje(viaje_id) if not datos: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Viaje con id {viaje_id} no encontrado", ) viaje = datos["viaje"] ubicaciones = datos["ubicaciones"] # Crear LineString coordinates = [[u.lng, u.lat] for u in ubicaciones] return { "type": "Feature", "geometry": { "type": "LineString", "coordinates": coordinates, }, "properties": { "viaje_id": viaje.id, "vehiculo_id": viaje.vehiculo_id, "inicio_tiempo": viaje.inicio_tiempo.isoformat(), "fin_tiempo": viaje.fin_tiempo.isoformat() if viaje.fin_tiempo else None, "distancia_km": viaje.distancia_km, "estado": viaje.estado, }, } @router.get("/activos", response_model=List[ViajeResumen]) async def listar_viajes_activos_simple( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """Lista viajes actualmente en curso.""" result = await db.execute( select(Viaje) .options( selectinload(Viaje.vehiculo), selectinload(Viaje.conductor), ) .where(Viaje.estado == "en_curso") .order_by(Viaje.inicio_tiempo.desc()) ) viajes = result.scalars().all() return [ ViajeResumen( id=v.id, vehiculo_id=v.vehiculo_id, vehiculo_nombre=v.vehiculo.nombre if v.vehiculo else None, vehiculo_placa=v.vehiculo.placa if v.vehiculo else None, conductor_nombre=v.conductor.nombre_completo if v.conductor else None, inicio_tiempo=v.inicio_tiempo, fin_tiempo=v.fin_tiempo, inicio_direccion=v.inicio_direccion, fin_direccion=v.fin_direccion, distancia_km=v.distancia_km, duracion_formateada=v.duracion_formateada, estado=v.estado, ) for v in viajes ] @router.post("/iniciar") async def iniciar_viaje( data: dict, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """Inicia un nuevo viaje manualmente.""" from datetime import timezone viaje = Viaje( vehiculo_id=data.get("vehiculo_id"), conductor_id=data.get("conductor_id"), proposito=data.get("proposito"), notas=data.get("notas"), inicio_tiempo=datetime.now(timezone.utc), inicio_lat=data.get("lat"), inicio_lng=data.get("lng"), estado="en_curso", ) db.add(viaje) await db.commit() await db.refresh(viaje) return {"id": viaje.id, "estado": viaje.estado} @router.get("/activos/lista", response_model=List[ViajeResumen]) async def listar_viajes_activos( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Lista viajes actualmente en curso. Returns: Lista de viajes en curso. """ result = await db.execute( select(Viaje) .options( selectinload(Viaje.vehiculo), selectinload(Viaje.conductor), ) .where(Viaje.estado == "en_curso") .order_by(Viaje.inicio_tiempo.desc()) ) viajes = result.scalars().all() return [ ViajeResumen( id=v.id, vehiculo_id=v.vehiculo_id, vehiculo_nombre=v.vehiculo.nombre if v.vehiculo else None, vehiculo_placa=v.vehiculo.placa if v.vehiculo else None, conductor_nombre=v.conductor.nombre_completo if v.conductor else None, inicio_tiempo=v.inicio_tiempo, fin_tiempo=v.fin_tiempo, inicio_direccion=v.inicio_direccion, fin_direccion=v.fin_direccion, distancia_km=v.distancia_km, duracion_formateada=v.duracion_formateada, estado=v.estado, ) for v in viajes ]