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>
This commit is contained in:
@@ -36,6 +36,58 @@ router = APIRouter(prefix="/alertas", tags=["Alertas"])
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@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,
|
||||
|
||||
138
backend/app/api/v1/combustible.py
Normal file
138
backend/app/api/v1/combustible.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Endpoints para gestión de combustible.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, func
|
||||
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.carga_combustible import CargaCombustible
|
||||
|
||||
router = APIRouter(prefix="/combustible", tags=["Combustible"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def listar_cargas(
|
||||
vehiculo_id: Optional[int] = None,
|
||||
vehiculoId: Optional[int] = None,
|
||||
desde: Optional[datetime] = None,
|
||||
hasta: Optional[datetime] = None,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
pageSize: int = Query(None, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista las cargas de combustible."""
|
||||
# Handle both vehiculo_id and vehiculoId params
|
||||
vid = vehiculo_id or vehiculoId
|
||||
actual_limit = pageSize or limit
|
||||
|
||||
query = select(CargaCombustible).options(selectinload(CargaCombustible.vehiculo))
|
||||
|
||||
if vid:
|
||||
query = query.where(CargaCombustible.vehiculo_id == vid)
|
||||
if desde:
|
||||
query = query.where(CargaCombustible.fecha >= desde)
|
||||
if hasta:
|
||||
query = query.where(CargaCombustible.fecha <= hasta)
|
||||
|
||||
query = query.offset(skip).limit(actual_limit).order_by(CargaCombustible.fecha.desc())
|
||||
|
||||
result = await db.execute(query)
|
||||
cargas = result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": c.id,
|
||||
"vehiculoId": c.vehiculo_id,
|
||||
"vehiculo_id": c.vehiculo_id,
|
||||
"vehiculo": {
|
||||
"id": c.vehiculo.id,
|
||||
"nombre": c.vehiculo.nombre,
|
||||
"placa": c.vehiculo.placa,
|
||||
} if c.vehiculo else None,
|
||||
"fecha": c.fecha,
|
||||
"litros": c.litros,
|
||||
"costo": c.total or 0,
|
||||
"costo_total": c.total or 0,
|
||||
"precioLitro": c.precio_litro or 0,
|
||||
"odometro": c.odometro,
|
||||
"tipo": c.tipo_combustible or "gasolina",
|
||||
"tipo_combustible": c.tipo_combustible,
|
||||
"gasolinera": c.estacion,
|
||||
"estacion": c.estacion,
|
||||
"rendimiento": None, # Calculated separately
|
||||
"lleno": c.tanque_lleno,
|
||||
}
|
||||
for c in cargas
|
||||
],
|
||||
"total": len(cargas),
|
||||
"page": skip // actual_limit + 1,
|
||||
"pageSize": actual_limit,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def obtener_estadisticas(
|
||||
vehiculo_id: Optional[int] = None,
|
||||
periodo: str = "mes",
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene estadísticas de combustible."""
|
||||
return {
|
||||
"totalLitros": 0,
|
||||
"costoTotal": 0,
|
||||
"rendimientoPromedio": 0,
|
||||
"cargas": 0,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{carga_id}")
|
||||
async def obtener_carga(
|
||||
carga_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene una carga de combustible por ID."""
|
||||
result = await db.execute(
|
||||
select(CargaCombustible)
|
||||
.options(selectinload(CargaCombustible.vehiculo))
|
||||
.where(CargaCombustible.id == carga_id)
|
||||
)
|
||||
carga = result.scalar_one_or_none()
|
||||
|
||||
if not carga:
|
||||
return {"detail": "Carga no encontrada"}
|
||||
|
||||
return {
|
||||
"id": carga.id,
|
||||
"vehiculoId": carga.vehiculo_id,
|
||||
"vehiculo_id": carga.vehiculo_id,
|
||||
"vehiculo": {
|
||||
"id": carga.vehiculo.id,
|
||||
"nombre": carga.vehiculo.nombre,
|
||||
"placa": carga.vehiculo.placa,
|
||||
} if carga.vehiculo else None,
|
||||
"fecha": carga.fecha,
|
||||
"litros": carga.litros,
|
||||
"costo": carga.total or 0,
|
||||
"costo_total": carga.total or 0,
|
||||
"precioLitro": carga.precio_litro or 0,
|
||||
"odometro": carga.odometro,
|
||||
"tipo": carga.tipo_combustible or "gasolina",
|
||||
"tipo_combustible": carga.tipo_combustible,
|
||||
"gasolinera": carga.estacion,
|
||||
"estacion": carga.estacion,
|
||||
"rendimiento": None,
|
||||
"lleno": carga.tanque_lleno,
|
||||
}
|
||||
@@ -75,6 +75,36 @@ async def listar_conductores(
|
||||
]
|
||||
|
||||
|
||||
@router.get("/all")
|
||||
async def listar_todos_conductores(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista todos los conductores activos."""
|
||||
result = await db.execute(select(Conductor).where(Conductor.activo == True))
|
||||
conductores = result.scalars().all()
|
||||
return [
|
||||
{"id": c.id, "nombre": c.nombre, "apellido": c.apellido, "telefono": c.telefono}
|
||||
for c in conductores
|
||||
]
|
||||
|
||||
|
||||
@router.get("/disponibles")
|
||||
async def listar_conductores_disponibles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista conductores disponibles (sin vehículo asignado)."""
|
||||
result = await db.execute(
|
||||
select(Conductor).where(Conductor.activo == True, Conductor.vehiculo_actual_id == None)
|
||||
)
|
||||
conductores = result.scalars().all()
|
||||
return [
|
||||
{"id": c.id, "nombre": c.nombre, "apellido": c.apellido}
|
||||
for c in conductores
|
||||
]
|
||||
|
||||
|
||||
@router.get("/{conductor_id}", response_model=ConductorResponse)
|
||||
async def obtener_conductor(
|
||||
conductor_id: int,
|
||||
|
||||
58
backend/app/api/v1/configuracion.py
Normal file
58
backend/app/api/v1/configuracion.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Endpoints para configuración del sistema."""
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.usuario import Usuario
|
||||
from app.models.configuracion import Configuracion
|
||||
|
||||
router = APIRouter(prefix="/configuracion", tags=["Configuracion"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def obtener_configuracion(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene la configuración del sistema."""
|
||||
result = await db.execute(select(Configuracion).limit(1))
|
||||
config = result.scalar_one_or_none()
|
||||
if not config:
|
||||
return {
|
||||
"nombre_empresa": "Atlas GPS",
|
||||
"timezone": "America/Mexico_City",
|
||||
"unidad_distancia": "km",
|
||||
"unidad_velocidad": "km/h",
|
||||
"limite_velocidad_default": 120,
|
||||
"alerta_bateria_baja": 20,
|
||||
"alerta_sin_senal_minutos": 30,
|
||||
}
|
||||
return {
|
||||
"nombre_empresa": config.valor if config.clave == "nombre_empresa" else "Atlas GPS",
|
||||
"timezone": "America/Mexico_City",
|
||||
"unidad_distancia": "km",
|
||||
"unidad_velocidad": "km/h",
|
||||
}
|
||||
|
||||
|
||||
@router.patch("")
|
||||
async def actualizar_configuracion(
|
||||
data: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Actualiza la configuración del sistema."""
|
||||
for clave, valor in data.items():
|
||||
result = await db.execute(select(Configuracion).where(Configuracion.clave == clave))
|
||||
config = result.scalar_one_or_none()
|
||||
if config:
|
||||
config.valor = str(valor)
|
||||
else:
|
||||
config = Configuracion(clave=clave, valor=str(valor))
|
||||
db.add(config)
|
||||
await db.commit()
|
||||
return {"message": "Configuración actualizada"}
|
||||
@@ -30,6 +30,17 @@ 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,
|
||||
|
||||
92
backend/app/api/v1/mantenimiento.py
Normal file
92
backend/app/api/v1/mantenimiento.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Endpoints para gestión de mantenimiento."""
|
||||
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.usuario import Usuario
|
||||
from app.models.mantenimiento import Mantenimiento
|
||||
|
||||
router = APIRouter(prefix="/mantenimiento", tags=["Mantenimiento"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def listar_mantenimientos(
|
||||
vehiculo_id: Optional[int] = None,
|
||||
estado: 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 los mantenimientos."""
|
||||
query = select(Mantenimiento)
|
||||
if vehiculo_id:
|
||||
query = query.where(Mantenimiento.vehiculo_id == vehiculo_id)
|
||||
if estado:
|
||||
query = query.where(Mantenimiento.estado == estado)
|
||||
query = query.offset(skip).limit(limit).order_by(Mantenimiento.fecha_programada.desc())
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
return {"items": [{"id": m.id, "vehiculo_id": m.vehiculo_id, "tipo": m.tipo_mantenimiento_id,
|
||||
"fecha_programada": m.fecha_programada, "estado": m.estado} for m in items],
|
||||
"total": len(items)}
|
||||
|
||||
|
||||
@router.get("/proximos")
|
||||
async def obtener_proximos(
|
||||
dias: int = 30,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene mantenimientos próximos."""
|
||||
ahora = datetime.now(timezone.utc)
|
||||
limite = ahora + timedelta(days=dias)
|
||||
query = select(Mantenimiento).where(
|
||||
and_(
|
||||
Mantenimiento.fecha_programada >= ahora,
|
||||
Mantenimiento.fecha_programada <= limite,
|
||||
Mantenimiento.estado == 'pendiente'
|
||||
)
|
||||
).order_by(Mantenimiento.fecha_programada)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
return [{"id": m.id, "vehiculo_id": m.vehiculo_id, "tipo": m.tipo_mantenimiento_id,
|
||||
"fecha_programada": m.fecha_programada} for m in items]
|
||||
|
||||
|
||||
@router.get("/vencidos")
|
||||
async def obtener_vencidos(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene mantenimientos vencidos."""
|
||||
ahora = datetime.now(timezone.utc)
|
||||
query = select(Mantenimiento).where(
|
||||
and_(
|
||||
Mantenimiento.fecha_programada < ahora,
|
||||
Mantenimiento.estado == 'pendiente'
|
||||
)
|
||||
).order_by(Mantenimiento.fecha_programada)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
return [{"id": m.id, "vehiculo_id": m.vehiculo_id, "tipo": m.tipo_mantenimiento_id,
|
||||
"fecha_programada": m.fecha_programada} for m in items]
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def crear_mantenimiento(
|
||||
data: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Crea un nuevo mantenimiento."""
|
||||
mant = Mantenimiento(**data)
|
||||
db.add(mant)
|
||||
await db.commit()
|
||||
await db.refresh(mant)
|
||||
return {"id": mant.id}
|
||||
77
backend/app/api/v1/pois.py
Normal file
77
backend/app/api/v1/pois.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Endpoints para gestión de POIs (Puntos de Interés)."""
|
||||
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.usuario import Usuario
|
||||
from app.models.poi import POI
|
||||
|
||||
router = APIRouter(prefix="/pois", tags=["POIs"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def listar_pois(
|
||||
categoria: Optional[str] = None,
|
||||
activo: Optional[bool] = True,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista los POIs."""
|
||||
query = select(POI)
|
||||
if categoria:
|
||||
query = query.where(POI.categoria == categoria)
|
||||
if activo is not None:
|
||||
query = query.where(POI.activo == activo)
|
||||
query = query.offset(skip).limit(limit)
|
||||
result = await db.execute(query)
|
||||
pois = result.scalars().all()
|
||||
return [{"id": p.id, "nombre": p.nombre, "categoria": p.categoria,
|
||||
"latitud": p.latitud, "longitud": p.longitud, "direccion": p.direccion,
|
||||
"activo": p.activo} for p in pois]
|
||||
|
||||
|
||||
@router.get("/all")
|
||||
async def listar_todos_pois(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista todos los POIs activos."""
|
||||
result = await db.execute(select(POI).where(POI.activo == True))
|
||||
pois = result.scalars().all()
|
||||
return [{"id": p.id, "nombre": p.nombre, "categoria": p.categoria,
|
||||
"latitud": p.latitud, "longitud": p.longitud, "direccion": p.direccion} for p in pois]
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def crear_poi(
|
||||
data: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Crea un nuevo POI."""
|
||||
poi = POI(**data)
|
||||
db.add(poi)
|
||||
await db.commit()
|
||||
await db.refresh(poi)
|
||||
return {"id": poi.id, "nombre": poi.nombre}
|
||||
|
||||
|
||||
@router.get("/{poi_id}")
|
||||
async def obtener_poi(
|
||||
poi_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene un POI por ID."""
|
||||
result = await db.execute(select(POI).where(POI.id == poi_id))
|
||||
poi = result.scalar_one_or_none()
|
||||
if not poi:
|
||||
raise HTTPException(status_code=404, detail="POI no encontrado")
|
||||
return {"id": poi.id, "nombre": poi.nombre, "categoria": poi.categoria,
|
||||
"latitud": poi.latitud, "longitud": poi.longitud}
|
||||
@@ -22,6 +22,82 @@ from app.services.reporte_service import ReporteService
|
||||
router = APIRouter(prefix="/reportes", tags=["Reportes"])
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def obtener_estadisticas_reportes(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene estadísticas de reportes generados."""
|
||||
return {
|
||||
"total_generados": 0,
|
||||
"ultimo_mes": 0,
|
||||
"por_tipo": {
|
||||
"viajes": 0,
|
||||
"alertas": 0,
|
||||
"combustible": 0,
|
||||
"mantenimiento": 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
async def listar_plantillas_reportes(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista las plantillas de reportes disponibles."""
|
||||
return [
|
||||
{"id": "viajes", "nombre": "Reporte de Viajes", "descripcion": "Detalle de viajes realizados"},
|
||||
{"id": "alertas", "nombre": "Reporte de Alertas", "descripcion": "Resumen de alertas generadas"},
|
||||
{"id": "combustible", "nombre": "Reporte de Combustible", "descripcion": "Consumo y cargas de combustible"},
|
||||
{"id": "mantenimiento", "nombre": "Reporte de Mantenimiento", "descripcion": "Estado de mantenimientos"},
|
||||
{"id": "resumen", "nombre": "Reporte Resumen", "descripcion": "Resumen general de la flota"},
|
||||
]
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
async def previsualizar_reporte(
|
||||
request: ReporteRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Previsualiza un reporte sin generarlo completamente."""
|
||||
reporte_service = ReporteService(db)
|
||||
datos = await reporte_service._recopilar_datos_reporte(request)
|
||||
return {
|
||||
"preview": True,
|
||||
"tipo": request.tipo,
|
||||
"registros": len(datos.get("datos", [])) if isinstance(datos, dict) else 0,
|
||||
"datos_muestra": datos[:10] if isinstance(datos, list) else datos,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/programados")
|
||||
async def listar_reportes_programados(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista los reportes programados."""
|
||||
# Por ahora retorna lista vacía, la funcionalidad completa requiere tabla de reportes programados
|
||||
return {"items": [], "total": 0}
|
||||
|
||||
|
||||
@router.post("/programar")
|
||||
async def programar_reporte(
|
||||
data: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Programa un nuevo reporte."""
|
||||
# Por ahora solo retorna confirmación, la funcionalidad completa requiere tabla de reportes programados
|
||||
return {
|
||||
"message": "Reporte programado",
|
||||
"tipo": data.get("tipo"),
|
||||
"frecuencia": data.get("frecuencia"),
|
||||
"email_destino": data.get("email_destino"),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dashboard", response_model=DashboardResumen)
|
||||
async def obtener_dashboard(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
|
||||
@@ -15,6 +15,11 @@ from app.api.v1.alertas import router as alertas_router
|
||||
from app.api.v1.geocercas import router as geocercas_router
|
||||
from app.api.v1.dispositivos import router as dispositivos_router
|
||||
from app.api.v1.reportes import router as reportes_router
|
||||
from app.api.v1.combustible import router as combustible_router
|
||||
from app.api.v1.pois import router as pois_router
|
||||
from app.api.v1.mantenimiento import router as mantenimiento_router
|
||||
from app.api.v1.video import router as video_router
|
||||
from app.api.v1.configuracion import router as configuracion_router
|
||||
|
||||
# Router principal
|
||||
api_router = APIRouter()
|
||||
@@ -29,20 +34,8 @@ api_router.include_router(alertas_router)
|
||||
api_router.include_router(geocercas_router)
|
||||
api_router.include_router(dispositivos_router)
|
||||
api_router.include_router(reportes_router)
|
||||
|
||||
# TODO: Agregar cuando se completen
|
||||
# from app.api.v1.pois import router as pois_router
|
||||
# from app.api.v1.combustible import router as combustible_router
|
||||
# from app.api.v1.mantenimiento import router as mantenimiento_router
|
||||
# from app.api.v1.video import router as video_router
|
||||
# from app.api.v1.mensajes import router as mensajes_router
|
||||
# from app.api.v1.configuracion import router as configuracion_router
|
||||
# from app.api.v1.meshtastic import router as meshtastic_router
|
||||
|
||||
# api_router.include_router(pois_router)
|
||||
# api_router.include_router(combustible_router)
|
||||
# api_router.include_router(mantenimiento_router)
|
||||
# api_router.include_router(video_router)
|
||||
# api_router.include_router(mensajes_router)
|
||||
# api_router.include_router(configuracion_router)
|
||||
# api_router.include_router(meshtastic_router)
|
||||
api_router.include_router(combustible_router)
|
||||
api_router.include_router(pois_router)
|
||||
api_router.include_router(mantenimiento_router)
|
||||
api_router.include_router(video_router)
|
||||
api_router.include_router(configuracion_router)
|
||||
|
||||
@@ -31,7 +31,7 @@ from app.services.ubicacion_service import UbicacionService
|
||||
router = APIRouter(prefix="/vehiculos", tags=["Vehiculos"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[VehiculoResumen])
|
||||
@router.get("")
|
||||
async def listar_vehiculos(
|
||||
activo: Optional[bool] = None,
|
||||
en_servicio: Optional[bool] = None,
|
||||
@@ -39,6 +39,8 @@ async def listar_vehiculos(
|
||||
buscar: Optional[str] = None,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
page: int = Query(None, ge=1),
|
||||
pageSize: int = Query(None, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
@@ -54,8 +56,12 @@ async def listar_vehiculos(
|
||||
limit: Límite de registros.
|
||||
|
||||
Returns:
|
||||
Lista de vehículos.
|
||||
Lista de vehículos paginada.
|
||||
"""
|
||||
# Handle pagination params
|
||||
actual_limit = pageSize or limit
|
||||
actual_skip = ((page - 1) * actual_limit) if page else skip
|
||||
|
||||
query = select(Vehiculo)
|
||||
|
||||
if activo is not None:
|
||||
@@ -70,14 +76,55 @@ async def listar_vehiculos(
|
||||
(Vehiculo.placa.ilike(f"%{buscar}%"))
|
||||
)
|
||||
|
||||
query = query.offset(skip).limit(limit).order_by(Vehiculo.nombre)
|
||||
# Get total count
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
query = query.offset(actual_skip).limit(actual_limit).order_by(Vehiculo.nombre)
|
||||
|
||||
result = await db.execute(query)
|
||||
vehiculos = result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": [VehiculoResumen.model_validate(v) for v in vehiculos],
|
||||
"total": total,
|
||||
"page": (actual_skip // actual_limit) + 1,
|
||||
"pageSize": actual_limit,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/all")
|
||||
async def listar_todos_vehiculos(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Lista todos los vehículos activos (sin paginación).
|
||||
Para uso en mapas, selectores, etc.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Vehiculo)
|
||||
.where(Vehiculo.activo == True)
|
||||
.order_by(Vehiculo.nombre)
|
||||
)
|
||||
vehiculos = result.scalars().all()
|
||||
return [VehiculoResumen.model_validate(v) for v in vehiculos]
|
||||
|
||||
|
||||
@router.get("/ubicaciones/actuales", response_model=List[VehiculoUbicacionActual])
|
||||
async def obtener_ubicaciones_actuales(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Obtiene las ubicaciones actuales de todos los vehículos.
|
||||
Alias para /ubicaciones.
|
||||
"""
|
||||
ubicacion_service = UbicacionService(db)
|
||||
return await ubicacion_service.obtener_ubicaciones_flota()
|
||||
|
||||
|
||||
@router.get("/ubicaciones", response_model=List[VehiculoUbicacionActual])
|
||||
async def obtener_ubicaciones_flota(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -93,6 +140,54 @@ async def obtener_ubicaciones_flota(
|
||||
return await ubicacion_service.obtener_ubicaciones_flota()
|
||||
|
||||
|
||||
@router.get("/fleet/stats")
|
||||
async def obtener_estadisticas_flota(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Obtiene estadísticas generales de la flota.
|
||||
|
||||
Returns:
|
||||
Estadísticas de la flota.
|
||||
"""
|
||||
# Total de vehículos
|
||||
result = await db.execute(select(func.count(Vehiculo.id)))
|
||||
total = result.scalar() or 0
|
||||
|
||||
# Activos
|
||||
result = await db.execute(
|
||||
select(func.count(Vehiculo.id)).where(Vehiculo.activo == True)
|
||||
)
|
||||
activos = result.scalar() or 0
|
||||
|
||||
# Inactivos
|
||||
inactivos = total - activos
|
||||
|
||||
# En servicio
|
||||
result = await db.execute(
|
||||
select(func.count(Vehiculo.id)).where(Vehiculo.en_servicio == True)
|
||||
)
|
||||
en_servicio = result.scalar() or 0
|
||||
|
||||
# Alertas activas
|
||||
result = await db.execute(
|
||||
select(func.count(Alerta.id)).where(Alerta.atendida == False)
|
||||
)
|
||||
alertas_activas = result.scalar() or 0
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"activos": activos,
|
||||
"inactivos": inactivos,
|
||||
"mantenimiento": 0,
|
||||
"enMovimiento": 0,
|
||||
"detenidos": en_servicio,
|
||||
"sinSenal": 0,
|
||||
"alertasActivas": alertas_activas,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{vehiculo_id}", response_model=VehiculoConRelaciones)
|
||||
async def obtener_vehiculo(
|
||||
vehiculo_id: int,
|
||||
|
||||
@@ -298,6 +298,66 @@ async def obtener_viaje_geojson(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/activos", response_model=List[ViajeResumen])
|
||||
async def listar_viajes_activos_simple(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista viajes actualmente en curso."""
|
||||
result = await db.execute(
|
||||
select(Viaje)
|
||||
.options(
|
||||
selectinload(Viaje.vehiculo),
|
||||
selectinload(Viaje.conductor),
|
||||
)
|
||||
.where(Viaje.estado == "en_curso")
|
||||
.order_by(Viaje.inicio_tiempo.desc())
|
||||
)
|
||||
viajes = result.scalars().all()
|
||||
|
||||
return [
|
||||
ViajeResumen(
|
||||
id=v.id,
|
||||
vehiculo_id=v.vehiculo_id,
|
||||
vehiculo_nombre=v.vehiculo.nombre if v.vehiculo else None,
|
||||
vehiculo_placa=v.vehiculo.placa if v.vehiculo else None,
|
||||
conductor_nombre=v.conductor.nombre_completo if v.conductor else None,
|
||||
inicio_tiempo=v.inicio_tiempo,
|
||||
fin_tiempo=v.fin_tiempo,
|
||||
inicio_direccion=v.inicio_direccion,
|
||||
fin_direccion=v.fin_direccion,
|
||||
distancia_km=v.distancia_km,
|
||||
duracion_formateada=v.duracion_formateada,
|
||||
estado=v.estado,
|
||||
)
|
||||
for v in viajes
|
||||
]
|
||||
|
||||
|
||||
@router.post("/iniciar")
|
||||
async def iniciar_viaje(
|
||||
data: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Inicia un nuevo viaje manualmente."""
|
||||
from datetime import timezone
|
||||
viaje = Viaje(
|
||||
vehiculo_id=data.get("vehiculo_id"),
|
||||
conductor_id=data.get("conductor_id"),
|
||||
proposito=data.get("proposito"),
|
||||
notas=data.get("notas"),
|
||||
inicio_tiempo=datetime.now(timezone.utc),
|
||||
inicio_lat=data.get("lat"),
|
||||
inicio_lng=data.get("lng"),
|
||||
estado="en_curso",
|
||||
)
|
||||
db.add(viaje)
|
||||
await db.commit()
|
||||
await db.refresh(viaje)
|
||||
return {"id": viaje.id, "estado": viaje.estado}
|
||||
|
||||
|
||||
@router.get("/activos/lista", response_model=List[ViajeResumen])
|
||||
async def listar_viajes_activos(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
|
||||
47
backend/app/api/v1/video.py
Normal file
47
backend/app/api/v1/video.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Endpoints para gestión de video."""
|
||||
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.usuario import Usuario
|
||||
from app.models.camara import Camara
|
||||
|
||||
router = APIRouter(prefix="/video", tags=["Video"])
|
||||
|
||||
|
||||
@router.get("/camaras")
|
||||
async def listar_camaras(
|
||||
vehiculo_id: Optional[int] = None,
|
||||
activa: Optional[bool] = True,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Lista las cámaras."""
|
||||
query = select(Camara)
|
||||
if vehiculo_id:
|
||||
query = query.where(Camara.vehiculo_id == vehiculo_id)
|
||||
if activa is not None:
|
||||
query = query.where(Camara.activa == activa)
|
||||
result = await db.execute(query)
|
||||
camaras = result.scalars().all()
|
||||
return [{"id": c.id, "vehiculo_id": c.vehiculo_id, "nombre": c.nombre,
|
||||
"tipo": c.tipo, "url_stream": c.url_stream, "activa": c.activa} for c in camaras]
|
||||
|
||||
|
||||
@router.get("/camaras/{camara_id}")
|
||||
async def obtener_camara(
|
||||
camara_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Usuario = Depends(get_current_user),
|
||||
):
|
||||
"""Obtiene una cámara por ID."""
|
||||
result = await db.execute(select(Camara).where(Camara.id == camara_id))
|
||||
camara = result.scalar_one_or_none()
|
||||
if not camara:
|
||||
return {"detail": "Cámara no encontrada"}
|
||||
return {"id": camara.id, "vehiculo_id": camara.vehiculo_id, "nombre": camara.nombre,
|
||||
"tipo": camara.tipo, "url_stream": camara.url_stream}
|
||||
Reference in New Issue
Block a user