""" 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("/configuracion") async def obtener_configuracion_alertas( db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """Obtiene la configuración de alertas.""" result = await db.execute(select(TipoAlerta).order_by(TipoAlerta.prioridad)) tipos = result.scalars().all() return { "tipos": [ { "id": t.id, "codigo": t.codigo, "nombre": t.nombre, "severidad_default": t.severidad_default, "activo": t.activo, "notificar_email": t.notificar_email, "notificar_push": t.notificar_push, "notificar_sms": t.notificar_sms, } for t in tipos ], "notificaciones": { "email_habilitado": True, "push_habilitado": True, "sms_habilitado": False, } } @router.put("/configuracion") async def actualizar_configuracion_alertas( data: dict, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """Actualiza la configuración de alertas.""" if "tipos" in data: for tipo_data in data["tipos"]: if "id" in tipo_data: result = await db.execute( select(TipoAlerta).where(TipoAlerta.id == tipo_data["id"]) ) tipo = result.scalar_one_or_none() if tipo: for field in ["activo", "notificar_email", "notificar_push", "notificar_sms"]: if field in tipo_data: setattr(tipo, field, tipo_data[field]) await db.commit() return {"message": "Configuración actualizada"} @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}