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.
403 lines
11 KiB
Python
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}
|