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:
502
backend/app/api/v1/geocercas.py
Normal file
502
backend/app/api/v1/geocercas.py
Normal file
@@ -0,0 +1,502 @@
|
||||
"""
|
||||
Endpoints para gestión de geocercas.
|
||||
"""
|
||||
|
||||
import json
|
||||
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.geocerca import Geocerca
|
||||
from app.models.vehiculo import Vehiculo
|
||||
from app.schemas.geocerca import (
|
||||
GeocercaCircularCreate,
|
||||
GeocercaPoligonoCreate,
|
||||
GeocercaUpdate,
|
||||
GeocercaResponse,
|
||||
GeocercaConVehiculos,
|
||||
AsignarVehiculosRequest,
|
||||
VerificarPuntoRequest,
|
||||
VerificarPuntoResponse,
|
||||
)
|
||||
from app.services.geocerca_service import GeocercaService
|
||||
|
||||
router = APIRouter(prefix="/geocercas", tags=["Geocercas"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[GeocercaResponse])
|
||||
async def listar_geocercas(
|
||||
activa: Optional[bool] = None,
|
||||
tipo: Optional[str] = None,
|
||||
categoria: Optional[str] = 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 todas las geocercas.
|
||||
|
||||
Args:
|
||||
activa: Filtrar por estado.
|
||||
tipo: Filtrar por tipo (circular/poligono).
|
||||
categoria: Filtrar por categoría.
|
||||
skip: Registros a saltar.
|
||||
limit: Límite de registros.
|
||||
|
||||
Returns:
|
||||
Lista de geocercas.
|
||||
"""
|
||||
query = select(Geocerca).order_by(Geocerca.nombre)
|
||||
|
||||
if activa is not None:
|
||||
query = query.where(Geocerca.activa == activa)
|
||||
if tipo:
|
||||
query = query.where(Geocerca.tipo == tipo)
|
||||
if categoria:
|
||||
query = query.where(Geocerca.categoria == categoria)
|
||||
|
||||
query = query.offset(skip).limit(limit)
|
||||
|
||||
result = await db.execute(query)
|
||||
geocercas = result.scalars().all()
|
||||
|
||||
return [
|
||||
GeocercaResponse(
|
||||
id=g.id,
|
||||
nombre=g.nombre,
|
||||
descripcion=g.descripcion,
|
||||
tipo=g.tipo,
|
||||
color=g.color,
|
||||
opacidad=g.opacidad,
|
||||
color_borde=g.color_borde,
|
||||
categoria=g.categoria,
|
||||
centro_lat=g.centro_lat,
|
||||
centro_lng=g.centro_lng,
|
||||
radio_metros=g.radio_metros,
|
||||
coordenadas_json=g.coordenadas_json,
|
||||
alerta_entrada=g.alerta_entrada,
|
||||
alerta_salida=g.alerta_salida,
|
||||
velocidad_maxima=g.velocidad_maxima,
|
||||
horario_json=g.horario_json,
|
||||
activa=g.activa,
|
||||
aplica_todos_vehiculos=g.aplica_todos_vehiculos,
|
||||
creado_en=g.creado_en,
|
||||
actualizado_en=g.actualizado_en,
|
||||
)
|
||||
for g in geocercas
|
||||
]
|
||||
|
||||
|
||||
@router.get("/geojson")
|
||||
async def obtener_geocercas_geojson(
|
||||
activa: bool = True,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Obtiene todas las geocercas en formato GeoJSON.
|
||||
|
||||
Args:
|
||||
activa: Solo geocercas activas.
|
||||
|
||||
Returns:
|
||||
FeatureCollection GeoJSON.
|
||||
"""
|
||||
query = select(Geocerca)
|
||||
if activa:
|
||||
query = query.where(Geocerca.activa == True)
|
||||
|
||||
result = await db.execute(query)
|
||||
geocercas = result.scalars().all()
|
||||
|
||||
features = [g.to_geojson() for g in geocercas]
|
||||
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": features,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{geocerca_id}", response_model=GeocercaConVehiculos)
|
||||
async def obtener_geocerca(
|
||||
geocerca_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Obtiene una geocerca por su ID.
|
||||
|
||||
Args:
|
||||
geocerca_id: ID de la geocerca.
|
||||
|
||||
Returns:
|
||||
Geocerca con vehículos asignados.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Geocerca)
|
||||
.options(selectinload(Geocerca.vehiculos_asignados))
|
||||
.where(Geocerca.id == geocerca_id)
|
||||
)
|
||||
geocerca = result.scalar_one_or_none()
|
||||
|
||||
if not geocerca:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Geocerca con id {geocerca_id} no encontrada",
|
||||
)
|
||||
|
||||
from app.schemas.vehiculo import VehiculoResumen
|
||||
|
||||
return GeocercaConVehiculos(
|
||||
id=geocerca.id,
|
||||
nombre=geocerca.nombre,
|
||||
descripcion=geocerca.descripcion,
|
||||
tipo=geocerca.tipo,
|
||||
color=geocerca.color,
|
||||
opacidad=geocerca.opacidad,
|
||||
color_borde=geocerca.color_borde,
|
||||
categoria=geocerca.categoria,
|
||||
centro_lat=geocerca.centro_lat,
|
||||
centro_lng=geocerca.centro_lng,
|
||||
radio_metros=geocerca.radio_metros,
|
||||
coordenadas_json=geocerca.coordenadas_json,
|
||||
alerta_entrada=geocerca.alerta_entrada,
|
||||
alerta_salida=geocerca.alerta_salida,
|
||||
velocidad_maxima=geocerca.velocidad_maxima,
|
||||
horario_json=geocerca.horario_json,
|
||||
activa=geocerca.activa,
|
||||
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
|
||||
creado_en=geocerca.creado_en,
|
||||
actualizado_en=geocerca.actualizado_en,
|
||||
vehiculos_asignados=[
|
||||
VehiculoResumen.model_validate(v)
|
||||
for v in geocerca.vehiculos_asignados
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/circular", response_model=GeocercaResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def crear_geocerca_circular(
|
||||
geocerca_data: GeocercaCircularCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Crea una geocerca circular.
|
||||
|
||||
Args:
|
||||
geocerca_data: Datos de la geocerca.
|
||||
|
||||
Returns:
|
||||
Geocerca creada.
|
||||
"""
|
||||
geocerca = Geocerca(
|
||||
nombre=geocerca_data.nombre,
|
||||
descripcion=geocerca_data.descripcion,
|
||||
tipo="circular",
|
||||
centro_lat=geocerca_data.centro_lat,
|
||||
centro_lng=geocerca_data.centro_lng,
|
||||
radio_metros=geocerca_data.radio_metros,
|
||||
color=geocerca_data.color,
|
||||
opacidad=geocerca_data.opacidad,
|
||||
color_borde=geocerca_data.color_borde,
|
||||
categoria=geocerca_data.categoria,
|
||||
alerta_entrada=geocerca_data.alerta_entrada,
|
||||
alerta_salida=geocerca_data.alerta_salida,
|
||||
velocidad_maxima=geocerca_data.velocidad_maxima,
|
||||
horario_json=geocerca_data.horario_json,
|
||||
)
|
||||
|
||||
db.add(geocerca)
|
||||
await db.commit()
|
||||
await db.refresh(geocerca)
|
||||
|
||||
# Asignar vehículos si se especificaron
|
||||
if geocerca_data.vehiculos_ids:
|
||||
result = await db.execute(
|
||||
select(Vehiculo).where(Vehiculo.id.in_(geocerca_data.vehiculos_ids))
|
||||
)
|
||||
vehiculos = result.scalars().all()
|
||||
geocerca.vehiculos_asignados = list(vehiculos)
|
||||
await db.commit()
|
||||
|
||||
return GeocercaResponse(
|
||||
id=geocerca.id,
|
||||
nombre=geocerca.nombre,
|
||||
descripcion=geocerca.descripcion,
|
||||
tipo=geocerca.tipo,
|
||||
color=geocerca.color,
|
||||
opacidad=geocerca.opacidad,
|
||||
color_borde=geocerca.color_borde,
|
||||
categoria=geocerca.categoria,
|
||||
centro_lat=geocerca.centro_lat,
|
||||
centro_lng=geocerca.centro_lng,
|
||||
radio_metros=geocerca.radio_metros,
|
||||
coordenadas_json=geocerca.coordenadas_json,
|
||||
alerta_entrada=geocerca.alerta_entrada,
|
||||
alerta_salida=geocerca.alerta_salida,
|
||||
velocidad_maxima=geocerca.velocidad_maxima,
|
||||
horario_json=geocerca.horario_json,
|
||||
activa=geocerca.activa,
|
||||
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
|
||||
creado_en=geocerca.creado_en,
|
||||
actualizado_en=geocerca.actualizado_en,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/poligono", response_model=GeocercaResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def crear_geocerca_poligono(
|
||||
geocerca_data: GeocercaPoligonoCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Crea una geocerca poligonal.
|
||||
|
||||
Args:
|
||||
geocerca_data: Datos de la geocerca.
|
||||
|
||||
Returns:
|
||||
Geocerca creada.
|
||||
"""
|
||||
geocerca = Geocerca(
|
||||
nombre=geocerca_data.nombre,
|
||||
descripcion=geocerca_data.descripcion,
|
||||
tipo="poligono",
|
||||
coordenadas_json=json.dumps(geocerca_data.coordenadas),
|
||||
color=geocerca_data.color,
|
||||
opacidad=geocerca_data.opacidad,
|
||||
color_borde=geocerca_data.color_borde,
|
||||
categoria=geocerca_data.categoria,
|
||||
alerta_entrada=geocerca_data.alerta_entrada,
|
||||
alerta_salida=geocerca_data.alerta_salida,
|
||||
velocidad_maxima=geocerca_data.velocidad_maxima,
|
||||
horario_json=geocerca_data.horario_json,
|
||||
)
|
||||
|
||||
db.add(geocerca)
|
||||
await db.commit()
|
||||
await db.refresh(geocerca)
|
||||
|
||||
# Asignar vehículos si se especificaron
|
||||
if geocerca_data.vehiculos_ids:
|
||||
result = await db.execute(
|
||||
select(Vehiculo).where(Vehiculo.id.in_(geocerca_data.vehiculos_ids))
|
||||
)
|
||||
vehiculos = result.scalars().all()
|
||||
geocerca.vehiculos_asignados = list(vehiculos)
|
||||
await db.commit()
|
||||
|
||||
return GeocercaResponse(
|
||||
id=geocerca.id,
|
||||
nombre=geocerca.nombre,
|
||||
descripcion=geocerca.descripcion,
|
||||
tipo=geocerca.tipo,
|
||||
color=geocerca.color,
|
||||
opacidad=geocerca.opacidad,
|
||||
color_borde=geocerca.color_borde,
|
||||
categoria=geocerca.categoria,
|
||||
centro_lat=geocerca.centro_lat,
|
||||
centro_lng=geocerca.centro_lng,
|
||||
radio_metros=geocerca.radio_metros,
|
||||
coordenadas_json=geocerca.coordenadas_json,
|
||||
alerta_entrada=geocerca.alerta_entrada,
|
||||
alerta_salida=geocerca.alerta_salida,
|
||||
velocidad_maxima=geocerca.velocidad_maxima,
|
||||
horario_json=geocerca.horario_json,
|
||||
activa=geocerca.activa,
|
||||
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
|
||||
creado_en=geocerca.creado_en,
|
||||
actualizado_en=geocerca.actualizado_en,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{geocerca_id}", response_model=GeocercaResponse)
|
||||
async def actualizar_geocerca(
|
||||
geocerca_id: int,
|
||||
geocerca_data: GeocercaUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Actualiza una geocerca.
|
||||
|
||||
Args:
|
||||
geocerca_id: ID de la geocerca.
|
||||
geocerca_data: Datos a actualizar.
|
||||
|
||||
Returns:
|
||||
Geocerca actualizada.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Geocerca).where(Geocerca.id == geocerca_id)
|
||||
)
|
||||
geocerca = result.scalar_one_or_none()
|
||||
|
||||
if not geocerca:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Geocerca con id {geocerca_id} no encontrada",
|
||||
)
|
||||
|
||||
update_data = geocerca_data.model_dump(exclude_unset=True)
|
||||
|
||||
# Manejar coordenadas si es polígono
|
||||
if "coordenadas" in update_data and update_data["coordenadas"]:
|
||||
update_data["coordenadas_json"] = json.dumps(update_data.pop("coordenadas"))
|
||||
|
||||
for field, value in update_data.items():
|
||||
if hasattr(geocerca, field):
|
||||
setattr(geocerca, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(geocerca)
|
||||
|
||||
return GeocercaResponse(
|
||||
id=geocerca.id,
|
||||
nombre=geocerca.nombre,
|
||||
descripcion=geocerca.descripcion,
|
||||
tipo=geocerca.tipo,
|
||||
color=geocerca.color,
|
||||
opacidad=geocerca.opacidad,
|
||||
color_borde=geocerca.color_borde,
|
||||
categoria=geocerca.categoria,
|
||||
centro_lat=geocerca.centro_lat,
|
||||
centro_lng=geocerca.centro_lng,
|
||||
radio_metros=geocerca.radio_metros,
|
||||
coordenadas_json=geocerca.coordenadas_json,
|
||||
alerta_entrada=geocerca.alerta_entrada,
|
||||
alerta_salida=geocerca.alerta_salida,
|
||||
velocidad_maxima=geocerca.velocidad_maxima,
|
||||
horario_json=geocerca.horario_json,
|
||||
activa=geocerca.activa,
|
||||
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
|
||||
creado_en=geocerca.creado_en,
|
||||
actualizado_en=geocerca.actualizado_en,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{geocerca_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def eliminar_geocerca(
|
||||
geocerca_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Elimina una geocerca (soft delete - desactiva).
|
||||
|
||||
Args:
|
||||
geocerca_id: ID de la geocerca.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Geocerca).where(Geocerca.id == geocerca_id)
|
||||
)
|
||||
geocerca = result.scalar_one_or_none()
|
||||
|
||||
if not geocerca:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Geocerca con id {geocerca_id} no encontrada",
|
||||
)
|
||||
|
||||
geocerca.activa = False
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.post("/{geocerca_id}/vehiculos")
|
||||
async def asignar_vehiculos(
|
||||
geocerca_id: int,
|
||||
request: AsignarVehiculosRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Asigna vehículos a una geocerca.
|
||||
|
||||
Args:
|
||||
geocerca_id: ID de la geocerca.
|
||||
request: Lista de IDs de vehículos.
|
||||
|
||||
Returns:
|
||||
Confirmación.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Geocerca)
|
||||
.options(selectinload(Geocerca.vehiculos_asignados))
|
||||
.where(Geocerca.id == geocerca_id)
|
||||
)
|
||||
geocerca = result.scalar_one_or_none()
|
||||
|
||||
if not geocerca:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Geocerca con id {geocerca_id} no encontrada",
|
||||
)
|
||||
|
||||
# Obtener vehículos
|
||||
result = await db.execute(
|
||||
select(Vehiculo).where(Vehiculo.id.in_(request.vehiculos_ids))
|
||||
)
|
||||
vehiculos = result.scalars().all()
|
||||
|
||||
if request.reemplazar:
|
||||
geocerca.vehiculos_asignados = list(vehiculos)
|
||||
else:
|
||||
for v in vehiculos:
|
||||
if v not in geocerca.vehiculos_asignados:
|
||||
geocerca.vehiculos_asignados.append(v)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": "Vehículos asignados correctamente",
|
||||
"total_asignados": len(geocerca.vehiculos_asignados),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{geocerca_id}/verificar", response_model=VerificarPuntoResponse)
|
||||
async def verificar_punto(
|
||||
geocerca_id: int,
|
||||
request: VerificarPuntoRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Verifica si un punto está dentro de una geocerca.
|
||||
|
||||
Args:
|
||||
geocerca_id: ID de la geocerca.
|
||||
request: Coordenadas del punto.
|
||||
|
||||
Returns:
|
||||
Resultado de la verificación.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Geocerca).where(Geocerca.id == geocerca_id)
|
||||
)
|
||||
geocerca = result.scalar_one_or_none()
|
||||
|
||||
if not geocerca:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Geocerca con id {geocerca_id} no encontrada",
|
||||
)
|
||||
|
||||
geocerca_service = GeocercaService(db)
|
||||
dentro, distancia = await geocerca_service.verificar_punto_en_geocerca(
|
||||
request.lat, request.lng, geocerca_id
|
||||
)
|
||||
|
||||
return VerificarPuntoResponse(
|
||||
dentro=dentro,
|
||||
geocerca_id=geocerca_id,
|
||||
geocerca_nombre=geocerca.nombre,
|
||||
distancia_metros=distancia,
|
||||
)
|
||||
Reference in New Issue
Block a user