""" Endpoints para gestión de vehículos. """ from datetime import datetime, timezone from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select, func 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.vehiculo import Vehiculo from app.models.viaje import Viaje from app.models.alerta import Alerta from app.schemas.vehiculo import ( VehiculoCreate, VehiculoUpdate, VehiculoResponse, VehiculoResumen, VehiculoConRelaciones, VehiculoUbicacionActual, VehiculoEstadisticas, ) from app.schemas.base import PaginatedResponse from app.services.ubicacion_service import UbicacionService router = APIRouter(prefix="/vehiculos", tags=["Vehiculos"]) @router.get("") async def listar_vehiculos( activo: Optional[bool] = None, en_servicio: Optional[bool] = None, grupo_id: Optional[int] = None, buscar: Optional[str] = None, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), page: int = Query(None, ge=1), pageSize: int = Query(None, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Lista todos los vehículos con filtros opcionales. Args: activo: Filtrar por estado activo. en_servicio: Filtrar por en servicio. grupo_id: Filtrar por grupo. buscar: Búsqueda por nombre o placa. skip: Registros a saltar. limit: Límite de registros. Returns: Lista de vehículos paginada. """ # Handle pagination params actual_limit = pageSize or limit actual_skip = ((page - 1) * actual_limit) if page else skip query = select(Vehiculo) if activo is not None: query = query.where(Vehiculo.activo == activo) if en_servicio is not None: query = query.where(Vehiculo.en_servicio == en_servicio) if grupo_id: query = query.where(Vehiculo.grupo_id == grupo_id) if buscar: query = query.where( (Vehiculo.nombre.ilike(f"%{buscar}%")) | (Vehiculo.placa.ilike(f"%{buscar}%")) ) # Get total count count_query = select(func.count()).select_from(query.subquery()) total_result = await db.execute(count_query) total = total_result.scalar() or 0 query = query.offset(actual_skip).limit(actual_limit).order_by(Vehiculo.nombre) result = await db.execute(query) vehiculos = result.scalars().all() return { "items": [VehiculoResumen.model_validate(v) for v in vehiculos], "total": total, "page": (actual_skip // actual_limit) + 1, "pageSize": actual_limit, } @router.get("/all") async def listar_todos_vehiculos( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Lista todos los vehículos activos (sin paginación). Para uso en mapas, selectores, etc. """ result = await db.execute( select(Vehiculo) .where(Vehiculo.activo == True) .order_by(Vehiculo.nombre) ) vehiculos = result.scalars().all() return [VehiculoResumen.model_validate(v) for v in vehiculos] @router.get("/ubicaciones/actuales", response_model=List[VehiculoUbicacionActual]) async def obtener_ubicaciones_actuales( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene las ubicaciones actuales de todos los vehículos. Alias para /ubicaciones. """ ubicacion_service = UbicacionService(db) return await ubicacion_service.obtener_ubicaciones_flota() @router.get("/ubicaciones", response_model=List[VehiculoUbicacionActual]) async def obtener_ubicaciones_flota( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene las ubicaciones actuales de todos los vehículos. Returns: Lista de ubicaciones actuales. """ ubicacion_service = UbicacionService(db) return await ubicacion_service.obtener_ubicaciones_flota() @router.get("/fleet/stats") async def obtener_estadisticas_flota( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene estadísticas generales de la flota. Returns: Estadísticas de la flota. """ # Total de vehículos result = await db.execute(select(func.count(Vehiculo.id))) total = result.scalar() or 0 # Activos result = await db.execute( select(func.count(Vehiculo.id)).where(Vehiculo.activo == True) ) activos = result.scalar() or 0 # Inactivos inactivos = total - activos # En servicio result = await db.execute( select(func.count(Vehiculo.id)).where(Vehiculo.en_servicio == True) ) en_servicio = result.scalar() or 0 # Alertas activas result = await db.execute( select(func.count(Alerta.id)).where(Alerta.atendida == False) ) alertas_activas = result.scalar() or 0 return { "total": total, "activos": activos, "inactivos": inactivos, "mantenimiento": 0, "enMovimiento": 0, "detenidos": en_servicio, "sinSenal": 0, "alertasActivas": alertas_activas, } @router.get("/{vehiculo_id}", response_model=VehiculoConRelaciones) async def obtener_vehiculo( vehiculo_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene un vehículo por su ID con todas sus relaciones. Args: vehiculo_id: ID del vehículo. Returns: Vehículo con relaciones. """ result = await db.execute( select(Vehiculo) .options( selectinload(Vehiculo.conductor), selectinload(Vehiculo.grupo), selectinload(Vehiculo.dispositivos), ) .where(Vehiculo.id == vehiculo_id) ) vehiculo = result.scalar_one_or_none() if not vehiculo: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Vehículo con id {vehiculo_id} no encontrado", ) return VehiculoConRelaciones.model_validate(vehiculo) @router.post("", response_model=VehiculoResponse, status_code=status.HTTP_201_CREATED) async def crear_vehiculo( vehiculo_data: VehiculoCreate, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Crea un nuevo vehículo. Args: vehiculo_data: Datos del vehículo. Returns: Vehículo creado. """ # Verificar que la placa no exista result = await db.execute( select(Vehiculo).where(Vehiculo.placa == vehiculo_data.placa) ) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Ya existe un vehículo con la placa {vehiculo_data.placa}", ) # Verificar VIN si se proporciona if vehiculo_data.vin: result = await db.execute( select(Vehiculo).where(Vehiculo.vin == vehiculo_data.vin) ) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Ya existe un vehículo con el VIN {vehiculo_data.vin}", ) vehiculo = Vehiculo(**vehiculo_data.model_dump()) vehiculo.odometro_actual = vehiculo_data.odometro_inicial db.add(vehiculo) await db.commit() await db.refresh(vehiculo) return VehiculoResponse.model_validate(vehiculo) @router.put("/{vehiculo_id}", response_model=VehiculoResponse) async def actualizar_vehiculo( vehiculo_id: int, vehiculo_data: VehiculoUpdate, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Actualiza un vehículo existente. Args: vehiculo_id: ID del vehículo. vehiculo_data: Datos a actualizar. Returns: Vehículo actualizado. """ result = await db.execute( select(Vehiculo).where(Vehiculo.id == vehiculo_id) ) vehiculo = result.scalar_one_or_none() if not vehiculo: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Vehículo con id {vehiculo_id} no encontrado", ) # Verificar placa única si se cambia if vehiculo_data.placa and vehiculo_data.placa != vehiculo.placa: result = await db.execute( select(Vehiculo).where(Vehiculo.placa == vehiculo_data.placa) ) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Ya existe un vehículo con la placa {vehiculo_data.placa}", ) update_data = vehiculo_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(vehiculo, field, value) await db.commit() await db.refresh(vehiculo) return VehiculoResponse.model_validate(vehiculo) @router.delete("/{vehiculo_id}", status_code=status.HTTP_204_NO_CONTENT) async def eliminar_vehiculo( vehiculo_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Elimina un vehículo (soft delete - desactiva). Args: vehiculo_id: ID del vehículo. """ result = await db.execute( select(Vehiculo).where(Vehiculo.id == vehiculo_id) ) vehiculo = result.scalar_one_or_none() if not vehiculo: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Vehículo con id {vehiculo_id} no encontrado", ) # Soft delete vehiculo.activo = False vehiculo.en_servicio = False await db.commit() @router.get("/{vehiculo_id}/ubicacion") async def obtener_ubicacion_actual( vehiculo_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene la ubicación actual de un vehículo. Args: vehiculo_id: ID del vehículo. Returns: Última ubicación conocida. """ ubicacion_service = UbicacionService(db) ubicacion = await ubicacion_service.obtener_ultima_ubicacion(vehiculo_id) if not ubicacion: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No hay ubicación registrada para este vehículo", ) return ubicacion @router.get("/{vehiculo_id}/historial") async def obtener_historial_ubicaciones( vehiculo_id: int, desde: datetime, hasta: datetime, simplificar: bool = True, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene el historial de ubicaciones de un vehículo. Args: vehiculo_id: ID del vehículo. desde: Fecha/hora de inicio. hasta: Fecha/hora de fin. simplificar: Simplificar la ruta. Returns: Historial de ubicaciones con estadísticas. """ ubicacion_service = UbicacionService(db) return await ubicacion_service.obtener_historial( vehiculo_id, desde, hasta, simplificar ) @router.get("/{vehiculo_id}/viajes", response_model=List[dict]) async def obtener_viajes_vehiculo( vehiculo_id: int, desde: Optional[datetime] = None, hasta: Optional[datetime] = None, limite: int = Query(50, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene los viajes de un vehículo. Args: vehiculo_id: ID del vehículo. desde: Fecha inicio (opcional). hasta: Fecha fin (opcional). limite: Límite de resultados. Returns: Lista de viajes. """ from app.services.viaje_service import ViajeService viaje_service = ViajeService(db) viajes = await viaje_service.obtener_viajes_vehiculo( vehiculo_id, desde, hasta, limite ) return [ { "id": v.id, "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("/{vehiculo_id}/alertas", response_model=List[dict]) async def obtener_alertas_vehiculo( vehiculo_id: int, atendidas: Optional[bool] = None, limite: int = Query(50, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene las alertas de un vehículo. Args: vehiculo_id: ID del vehículo. atendidas: Filtrar por estado de atención. limite: Límite de resultados. Returns: Lista de alertas. """ query = ( select(Alerta) .where(Alerta.vehiculo_id == vehiculo_id) .order_by(Alerta.creado_en.desc()) .limit(limite) ) if atendidas is not None: query = query.where(Alerta.atendida == atendidas) result = await db.execute(query) alertas = result.scalars().all() return [ { "id": a.id, "tipo_alerta_id": a.tipo_alerta_id, "severidad": a.severidad, "mensaje": a.mensaje, "creado_en": a.creado_en, "atendida": a.atendida, } for a in alertas ] @router.get("/{vehiculo_id}/estadisticas", response_model=VehiculoEstadisticas) async def obtener_estadisticas_vehiculo( vehiculo_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene estadísticas de un vehículo. Args: vehiculo_id: ID del vehículo. Returns: Estadísticas del vehículo. """ result = await db.execute( select(Vehiculo).where(Vehiculo.id == vehiculo_id) ) vehiculo = result.scalar_one_or_none() if not vehiculo: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Vehículo con id {vehiculo_id} no encontrado", ) ahora = datetime.now(timezone.utc) inicio_hoy = ahora.replace(hour=0, minute=0, second=0, microsecond=0) inicio_semana = ahora - timedelta(days=7) inicio_mes = ahora - timedelta(days=30) # Distancia hoy result = await db.execute( select(func.coalesce(func.sum(Viaje.distancia_km), 0)) .where(Viaje.vehiculo_id == vehiculo_id) .where(Viaje.inicio_tiempo >= inicio_hoy) ) distancia_hoy = result.scalar() or 0 # Distancia semana result = await db.execute( select(func.coalesce(func.sum(Viaje.distancia_km), 0)) .where(Viaje.vehiculo_id == vehiculo_id) .where(Viaje.inicio_tiempo >= inicio_semana) ) distancia_semana = result.scalar() or 0 # Distancia mes result = await db.execute( select(func.coalesce(func.sum(Viaje.distancia_km), 0)) .where(Viaje.vehiculo_id == vehiculo_id) .where(Viaje.inicio_tiempo >= inicio_mes) ) distancia_mes = result.scalar() or 0 # Alertas activas result = await db.execute( select(func.count(Alerta.id)) .where(Alerta.vehiculo_id == vehiculo_id) .where(Alerta.atendida == False) ) alertas_activas = result.scalar() or 0 # Alertas mes result = await db.execute( select(func.count(Alerta.id)) .where(Alerta.vehiculo_id == vehiculo_id) .where(Alerta.creado_en >= inicio_mes) ) alertas_mes = result.scalar() or 0 return VehiculoEstadisticas( vehiculo_id=vehiculo.id, nombre=vehiculo.nombre, placa=vehiculo.placa, distancia_hoy_km=float(distancia_hoy), distancia_semana_km=float(distancia_semana), distancia_mes_km=float(distancia_mes), distancia_total_km=vehiculo.distancia_recorrida, tiempo_movimiento_hoy_min=0, # TODO: Calcular tiempo_parado_hoy_min=0, # TODO: Calcular alertas_activas=alertas_activas, alertas_mes=alertas_mes, mantenimientos_vencidos=0, # TODO: Calcular )