Files
ATLAS/backend/app/api/v1/viajes.py
ATLAS Admin e59aa2a742 feat: Complete ATLAS system installation and API fixes
## Backend Changes
- Add new API endpoints: combustible, pois, mantenimiento, video, configuracion
- Fix vehiculos endpoint to return paginated response with items array
- Add /vehiculos/all endpoint for non-paginated list
- Add /geocercas/all endpoint
- Add /alertas/configuracion GET/PUT endpoints
- Add /viajes/activos and /viajes/iniciar endpoints
- Add /reportes/stats, /reportes/templates, /reportes/preview endpoints
- Add /conductores/all and /conductores/disponibles endpoints
- Update router.py to include all new modules

## Frontend Changes
- Fix authentication token handling (snake_case vs camelCase)
- Update vehiculosApi.listAll to use /vehiculos/all
- Fix FuelGauge component usage in Combustible page
- Fix chart component exports (named + default exports)
- Update API client for proper token refresh

## Infrastructure
- Rename services from ADAN to ATLAS
- Configure Cloudflare tunnel for atlas.consultoria-as.com
- Update systemd service files
- Configure PostgreSQL with TimescaleDB
- Configure Redis, Mosquitto, Traccar, MediaMTX

## Documentation
- Update installation guides
- Update API reference
- Rename all ADAN references to ATLAS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 03:04:23 +00:00

400 lines
12 KiB
Python

"""
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
]