Files
ATLAS/backend/app/api/v1/alertas.py
FlotillasGPS Developer 51d78bacf4 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.
2026-01-21 08:18:00 +00:00

403 lines
11 KiB
Python

"""
Endpoints para gestión de alertas.
"""
from datetime import datetime, timezone
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.alerta import Alerta
from app.models.tipo_alerta import TipoAlerta
from app.schemas.alerta import (
TipoAlertaCreate,
TipoAlertaUpdate,
TipoAlertaResponse,
AlertaCreate,
AlertaResponse,
AlertaConRelaciones,
AlertaResumen,
AlertasEstadisticas,
AlertaAtenderRequest,
)
from app.services.alerta_service import AlertaService
router = APIRouter(prefix="/alertas", tags=["Alertas"])
# ============================================================================
# Tipos de Alerta
# ============================================================================
@router.get("/tipos", response_model=List[TipoAlertaResponse])
async def listar_tipos_alerta(
activo: Optional[bool] = None,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Lista todos los tipos de alerta.
Args:
activo: Filtrar por estado activo.
Returns:
Lista de tipos de alerta.
"""
query = select(TipoAlerta).order_by(TipoAlerta.prioridad)
if activo is not None:
query = query.where(TipoAlerta.activo == activo)
result = await db.execute(query)
tipos = result.scalars().all()
return [TipoAlertaResponse.model_validate(t) for t in tipos]
@router.post("/tipos", response_model=TipoAlertaResponse, status_code=status.HTTP_201_CREATED)
async def crear_tipo_alerta(
tipo_data: TipoAlertaCreate,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Crea un nuevo tipo de alerta.
Args:
tipo_data: Datos del tipo de alerta.
Returns:
Tipo de alerta creado.
"""
# Verificar código único
result = await db.execute(
select(TipoAlerta).where(TipoAlerta.codigo == tipo_data.codigo)
)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Ya existe un tipo de alerta con el código {tipo_data.codigo}",
)
tipo = TipoAlerta(**tipo_data.model_dump())
db.add(tipo)
await db.commit()
await db.refresh(tipo)
return TipoAlertaResponse.model_validate(tipo)
@router.put("/tipos/{tipo_id}", response_model=TipoAlertaResponse)
async def actualizar_tipo_alerta(
tipo_id: int,
tipo_data: TipoAlertaUpdate,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Actualiza un tipo de alerta.
Args:
tipo_id: ID del tipo.
tipo_data: Datos a actualizar.
Returns:
Tipo actualizado.
"""
result = await db.execute(
select(TipoAlerta).where(TipoAlerta.id == tipo_id)
)
tipo = result.scalar_one_or_none()
if not tipo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tipo de alerta con id {tipo_id} no encontrado",
)
update_data = tipo_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(tipo, field, value)
await db.commit()
await db.refresh(tipo)
return TipoAlertaResponse.model_validate(tipo)
# ============================================================================
# Alertas
# ============================================================================
@router.get("", response_model=List[AlertaResumen])
async def listar_alertas(
vehiculo_id: Optional[int] = None,
tipo_alerta_id: Optional[int] = None,
severidad: Optional[str] = None,
atendida: Optional[bool] = 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 alertas con filtros opcionales.
Args:
vehiculo_id: Filtrar por vehículo.
tipo_alerta_id: Filtrar por tipo.
severidad: Filtrar por severidad.
atendida: Filtrar por estado de atención.
desde: Fecha inicio.
hasta: Fecha fin.
skip: Registros a saltar.
limit: Límite de registros.
Returns:
Lista de alertas.
"""
query = (
select(Alerta)
.options(selectinload(Alerta.tipo_alerta))
.order_by(Alerta.creado_en.desc())
)
if vehiculo_id:
query = query.where(Alerta.vehiculo_id == vehiculo_id)
if tipo_alerta_id:
query = query.where(Alerta.tipo_alerta_id == tipo_alerta_id)
if severidad:
query = query.where(Alerta.severidad == severidad)
if atendida is not None:
query = query.where(Alerta.atendida == atendida)
if desde:
query = query.where(Alerta.creado_en >= desde)
if hasta:
query = query.where(Alerta.creado_en <= hasta)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
alertas = result.scalars().all()
return [
AlertaResumen(
id=a.id,
tipo_codigo=a.tipo_alerta.codigo if a.tipo_alerta else "",
tipo_nombre=a.tipo_alerta.nombre if a.tipo_alerta else "",
severidad=a.severidad,
mensaje=a.mensaje,
vehiculo_nombre=a.vehiculo.nombre if a.vehiculo else None,
vehiculo_placa=a.vehiculo.placa if a.vehiculo else None,
creado_en=a.creado_en,
atendida=a.atendida,
)
for a in alertas
]
@router.get("/pendientes", response_model=List[AlertaResumen])
async def listar_alertas_pendientes(
severidad: Optional[str] = None,
limit: int = Query(50, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Lista alertas pendientes de atender.
Args:
severidad: Filtrar por severidad.
limit: Límite de resultados.
Returns:
Lista de alertas pendientes.
"""
alerta_service = AlertaService(db)
alertas = await alerta_service.obtener_alertas_pendientes(
severidad=severidad,
limite=limit,
)
# Cargar relaciones
for a in alertas:
await db.refresh(a, ["tipo_alerta", "vehiculo"])
return [
AlertaResumen(
id=a.id,
tipo_codigo=a.tipo_alerta.codigo if a.tipo_alerta else "",
tipo_nombre=a.tipo_alerta.nombre if a.tipo_alerta else "",
severidad=a.severidad,
mensaje=a.mensaje,
vehiculo_nombre=a.vehiculo.nombre if a.vehiculo else None,
vehiculo_placa=a.vehiculo.placa if a.vehiculo else None,
creado_en=a.creado_en,
atendida=a.atendida,
)
for a in alertas
]
@router.get("/estadisticas", response_model=AlertasEstadisticas)
async def obtener_estadisticas_alertas(
desde: Optional[datetime] = None,
hasta: Optional[datetime] = None,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Obtiene estadísticas de alertas.
Args:
desde: Fecha inicio (opcional).
hasta: Fecha fin (opcional).
Returns:
Estadísticas de alertas.
"""
alerta_service = AlertaService(db)
stats = await alerta_service.obtener_estadisticas(desde, hasta)
return AlertasEstadisticas(
total=stats["total"],
pendientes=stats["pendientes"],
atendidas=stats["atendidas"],
criticas=stats["criticas"],
altas=stats["altas"],
medias=stats["medias"],
bajas=stats["bajas"],
por_tipo=stats["por_tipo"],
por_vehiculo=[], # TODO: Agregar en servicio
)
@router.get("/{alerta_id}", response_model=AlertaConRelaciones)
async def obtener_alerta(
alerta_id: int,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Obtiene una alerta por su ID.
Args:
alerta_id: ID de la alerta.
Returns:
Alerta con relaciones.
"""
result = await db.execute(
select(Alerta)
.options(
selectinload(Alerta.tipo_alerta),
selectinload(Alerta.vehiculo),
selectinload(Alerta.conductor),
)
.where(Alerta.id == alerta_id)
)
alerta = result.scalar_one_or_none()
if not alerta:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Alerta con id {alerta_id} no encontrada",
)
return AlertaConRelaciones(
id=alerta.id,
tipo_alerta_id=alerta.tipo_alerta_id,
severidad=alerta.severidad,
mensaje=alerta.mensaje,
descripcion=alerta.descripcion,
vehiculo_id=alerta.vehiculo_id,
conductor_id=alerta.conductor_id,
dispositivo_id=alerta.dispositivo_id,
lat=alerta.lat,
lng=alerta.lng,
direccion=alerta.direccion,
velocidad=alerta.velocidad,
valor=alerta.valor,
umbral=alerta.umbral,
datos_extra=alerta.datos_extra,
atendida=alerta.atendida,
atendida_por_id=alerta.atendida_por_id,
atendida_en=alerta.atendida_en,
notas_atencion=alerta.notas_atencion,
notificacion_email_enviada=alerta.notificacion_email_enviada,
notificacion_push_enviada=alerta.notificacion_push_enviada,
notificacion_sms_enviada=alerta.notificacion_sms_enviada,
creado_en=alerta.creado_en,
actualizado_en=alerta.actualizado_en,
es_critica=alerta.es_critica,
tipo_alerta=TipoAlertaResponse.model_validate(alerta.tipo_alerta),
vehiculo_nombre=alerta.vehiculo.nombre if alerta.vehiculo else None,
vehiculo_placa=alerta.vehiculo.placa if alerta.vehiculo else None,
conductor_nombre=alerta.conductor.nombre_completo if alerta.conductor else None,
)
@router.post("", response_model=AlertaResponse, status_code=status.HTTP_201_CREATED)
async def crear_alerta(
alerta_data: AlertaCreate,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Crea una alerta manualmente.
Args:
alerta_data: Datos de la alerta.
Returns:
Alerta creada.
"""
alerta_service = AlertaService(db)
alerta = await alerta_service.crear_alerta(alerta_data)
return AlertaResponse.model_validate(alerta)
@router.put("/{alerta_id}/atender")
async def atender_alerta(
alerta_id: int,
request: AlertaAtenderRequest,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Marca una alerta como atendida.
Args:
alerta_id: ID de la alerta.
request: Notas de atención.
Returns:
Alerta actualizada.
"""
alerta_service = AlertaService(db)
alerta = await alerta_service.marcar_atendida(
alerta_id,
current_user.id,
request.notas_atencion,
)
if not alerta:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Alerta con id {alerta_id} no encontrada",
)
return {"message": "Alerta marcada como atendida", "alerta_id": alerta_id}