Files
ATLAS/backend/app/api/v1/geocercas.py
ATLAS Admin e59aa2a742 feat: Complete ATLAS system installation and API fixes
## 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>
2026-01-25 03:04:23 +00:00

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,
)