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:
ATLAS Admin
2026-01-25 03:04:23 +00:00
parent 0dfce3ce20
commit e59aa2a742
73 changed files with 4415 additions and 450 deletions

View File

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

View 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,
}

View File

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

View 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"}

View File

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

View 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}

View 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}

View File

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

View File

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

View File

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

View File

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

View 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}