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:
FlotillasGPS Developer
2026-01-21 08:18:00 +00:00
commit 51d78bacf4
248 changed files with 50171 additions and 0 deletions

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