## 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>
514 lines
15 KiB
Python
514 lines
15 KiB
Python
"""
|
|
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("/all")
|
|
async def listar_todas_geocercas(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: Usuario = Depends(get_current_user),
|
|
):
|
|
"""Lista todas las geocercas activas."""
|
|
result = await db.execute(select(Geocerca).where(Geocerca.activa == True))
|
|
geocercas = result.scalars().all()
|
|
return [{"id": g.id, "nombre": g.nombre, "tipo": g.tipo, "color": g.color} for g in 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,
|
|
)
|