## 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>
400 lines
12 KiB
Python
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
|
|
]
|