FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con: - Backend FastAPI con PostgreSQL/TimescaleDB - Frontend React con TypeScript y TailwindCSS - App movil React Native con Expo - Soporte para dispositivos GPS, Meshtastic y celulares - Video streaming en vivo con MediaMTX - Geocercas, alertas, viajes y reportes - Autenticacion JWT y WebSockets en tiempo real Documentacion completa y guias de usuario incluidas.
This commit is contained in:
339
backend/app/api/v1/viajes.py
Normal file
339
backend/app/api/v1/viajes.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
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/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
|
||||
]
|
||||
Reference in New Issue
Block a user