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.
This commit is contained in:
FlotillasGPS Developer
2026-01-21 08:18:00 +00:00
commit 51d78bacf4
248 changed files with 50171 additions and 0 deletions

View File

@@ -0,0 +1,370 @@
"""
Módulo de schemas Pydantic.
Exporta todos los schemas para facilitar importaciones.
"""
from app.schemas.base import (
BaseSchema,
TimestampSchema,
PaginatedResponse,
MessageResponse,
ErrorResponse,
GeoJSONPoint,
GeoJSONFeature,
GeoJSONFeatureCollection,
CoordenadasSchema,
RangoFechasSchema,
)
from app.schemas.usuario import (
UsuarioCreate,
UsuarioUpdate,
UsuarioUpdatePassword,
UsuarioResponse,
LoginRequest,
LoginResponse,
RefreshTokenRequest,
TokenResponse,
)
from app.schemas.grupo_vehiculos import (
GrupoVehiculosCreate,
GrupoVehiculosUpdate,
GrupoVehiculosResponse,
GrupoVehiculosConVehiculos,
)
from app.schemas.conductor import (
ConductorCreate,
ConductorUpdate,
ConductorResponse,
ConductorResumen,
ConductorEstadisticas,
)
from app.schemas.vehiculo import (
VehiculoCreate,
VehiculoUpdate,
VehiculoResponse,
VehiculoResumen,
VehiculoConRelaciones,
VehiculoUbicacionActual,
VehiculoEstadisticas,
)
from app.schemas.dispositivo import (
DispositivoCreate,
DispositivoUpdate,
DispositivoResponse,
DispositivoResumen,
DispositivoConVehiculo,
)
from app.schemas.ubicacion import (
UbicacionCreate,
UbicacionBulkCreate,
UbicacionResponse,
UbicacionConVehiculo,
HistorialUbicacionesRequest,
HistorialUbicacionesResponse,
OsmAndLocationCreate,
TraccarLocationCreate,
)
from app.schemas.viaje import (
ViajeCreate,
ViajeUpdate,
ViajeResponse,
ViajeResumen,
ViajeConParadas,
ViajeReplayData,
ParadaCreate,
ParadaUpdate,
ParadaResponse,
ParadaResumen,
)
from app.schemas.alerta import (
TipoAlertaCreate,
TipoAlertaUpdate,
TipoAlertaResponse,
AlertaCreate,
AlertaUpdate,
AlertaResponse,
AlertaConTipo,
AlertaConRelaciones,
AlertaResumen,
AlertasEstadisticas,
AlertaAtenderRequest,
)
from app.schemas.geocerca import (
GeocercaCircularCreate,
GeocercaPoligonoCreate,
GeocercaUpdate,
GeocercaResponse,
GeocercaConVehiculos,
GeocercaGeoJSON,
AsignarVehiculosRequest,
VerificarPuntoRequest,
VerificarPuntoResponse,
)
from app.schemas.poi import (
POICreate,
POIUpdate,
POIResponse,
POIResumen,
POICercano,
BuscarPOIsCercanosRequest,
BuscarPOIsCercanosResponse,
CategoriasPOIResponse,
)
from app.schemas.combustible import (
CargaCombustibleCreate,
CargaCombustibleUpdate,
CargaCombustibleResponse,
CargaCombustibleConRelaciones,
RendimientoCombustible,
ReporteConsumoVehiculo,
ReporteConsumoFlota,
)
from app.schemas.mantenimiento import (
TipoMantenimientoCreate,
TipoMantenimientoUpdate,
TipoMantenimientoResponse,
MantenimientoCreate,
MantenimientoUpdate,
MantenimientoResponse,
MantenimientoConRelaciones,
MantenimientoResumen,
ProximosMantenimientos,
CompletarMantenimientoRequest,
)
from app.schemas.video import (
CamaraCreate,
CamaraUpdate,
CamaraResponse,
CamaraConVehiculo,
CamaraStreamURL,
GrabacionCreate,
GrabacionResponse,
GrabacionResumen,
EventoVideoCreate,
EventoVideoUpdate,
EventoVideoResponse,
EventoVideoConRelaciones,
EventoVideoResumen,
TiposEventoVideoResponse,
)
from app.schemas.mensaje import (
MensajeCreate,
MensajeEnviarAConductores,
MensajeUpdate,
MensajeResponse,
MensajeConConductor,
MensajeResumen,
ConversacionConductor,
MensajesNoLeidosResponse,
ResponderMensajeRequest,
)
from app.schemas.configuracion import (
ConfiguracionCreate,
ConfiguracionUpdate,
ConfiguracionResponse,
ConfiguracionResumen,
ConfiguracionesPorCategoria,
ConfiguracionesResponse,
ActualizarConfiguracionesRequest,
ConfiguracionesAlertasResponse,
ConfiguracionesViajesResponse,
ConfiguracionesNotificacionesResponse,
ConfiguracionesMapaResponse,
)
from app.schemas.reporte import (
DashboardResumen,
DashboardGrafico,
ReporteRequest,
ReporteResponse,
ReporteViajesResumen,
ReporteAlertasResumen,
ReporteCombustibleResumen,
ReporteMantenimientoResumen,
ReporteUbicacionesResumen,
EstadisticasFlota,
KPIsFlota,
)
__all__ = [
# Base
"BaseSchema",
"TimestampSchema",
"PaginatedResponse",
"MessageResponse",
"ErrorResponse",
"GeoJSONPoint",
"GeoJSONFeature",
"GeoJSONFeatureCollection",
"CoordenadasSchema",
"RangoFechasSchema",
# Usuario
"UsuarioCreate",
"UsuarioUpdate",
"UsuarioUpdatePassword",
"UsuarioResponse",
"LoginRequest",
"LoginResponse",
"RefreshTokenRequest",
"TokenResponse",
# Grupo Vehículos
"GrupoVehiculosCreate",
"GrupoVehiculosUpdate",
"GrupoVehiculosResponse",
"GrupoVehiculosConVehiculos",
# Conductor
"ConductorCreate",
"ConductorUpdate",
"ConductorResponse",
"ConductorResumen",
"ConductorEstadisticas",
# Vehículo
"VehiculoCreate",
"VehiculoUpdate",
"VehiculoResponse",
"VehiculoResumen",
"VehiculoConRelaciones",
"VehiculoUbicacionActual",
"VehiculoEstadisticas",
# Dispositivo
"DispositivoCreate",
"DispositivoUpdate",
"DispositivoResponse",
"DispositivoResumen",
"DispositivoConVehiculo",
# Ubicación
"UbicacionCreate",
"UbicacionBulkCreate",
"UbicacionResponse",
"UbicacionConVehiculo",
"HistorialUbicacionesRequest",
"HistorialUbicacionesResponse",
"OsmAndLocationCreate",
"TraccarLocationCreate",
# Viaje
"ViajeCreate",
"ViajeUpdate",
"ViajeResponse",
"ViajeResumen",
"ViajeConParadas",
"ViajeReplayData",
"ParadaCreate",
"ParadaUpdate",
"ParadaResponse",
"ParadaResumen",
# Alerta
"TipoAlertaCreate",
"TipoAlertaUpdate",
"TipoAlertaResponse",
"AlertaCreate",
"AlertaUpdate",
"AlertaResponse",
"AlertaConTipo",
"AlertaConRelaciones",
"AlertaResumen",
"AlertasEstadisticas",
"AlertaAtenderRequest",
# Geocerca
"GeocercaCircularCreate",
"GeocercaPoligonoCreate",
"GeocercaUpdate",
"GeocercaResponse",
"GeocercaConVehiculos",
"GeocercaGeoJSON",
"AsignarVehiculosRequest",
"VerificarPuntoRequest",
"VerificarPuntoResponse",
# POI
"POICreate",
"POIUpdate",
"POIResponse",
"POIResumen",
"POICercano",
"BuscarPOIsCercanosRequest",
"BuscarPOIsCercanosResponse",
"CategoriasPOIResponse",
# Combustible
"CargaCombustibleCreate",
"CargaCombustibleUpdate",
"CargaCombustibleResponse",
"CargaCombustibleConRelaciones",
"RendimientoCombustible",
"ReporteConsumoVehiculo",
"ReporteConsumoFlota",
# Mantenimiento
"TipoMantenimientoCreate",
"TipoMantenimientoUpdate",
"TipoMantenimientoResponse",
"MantenimientoCreate",
"MantenimientoUpdate",
"MantenimientoResponse",
"MantenimientoConRelaciones",
"MantenimientoResumen",
"ProximosMantenimientos",
"CompletarMantenimientoRequest",
# Video
"CamaraCreate",
"CamaraUpdate",
"CamaraResponse",
"CamaraConVehiculo",
"CamaraStreamURL",
"GrabacionCreate",
"GrabacionResponse",
"GrabacionResumen",
"EventoVideoCreate",
"EventoVideoUpdate",
"EventoVideoResponse",
"EventoVideoConRelaciones",
"EventoVideoResumen",
"TiposEventoVideoResponse",
# Mensaje
"MensajeCreate",
"MensajeEnviarAConductores",
"MensajeUpdate",
"MensajeResponse",
"MensajeConConductor",
"MensajeResumen",
"ConversacionConductor",
"MensajesNoLeidosResponse",
"ResponderMensajeRequest",
# Configuración
"ConfiguracionCreate",
"ConfiguracionUpdate",
"ConfiguracionResponse",
"ConfiguracionResumen",
"ConfiguracionesPorCategoria",
"ConfiguracionesResponse",
"ActualizarConfiguracionesRequest",
"ConfiguracionesAlertasResponse",
"ConfiguracionesViajesResponse",
"ConfiguracionesNotificacionesResponse",
"ConfiguracionesMapaResponse",
# Reportes
"DashboardResumen",
"DashboardGrafico",
"ReporteRequest",
"ReporteResponse",
"ReporteViajesResumen",
"ReporteAlertasResumen",
"ReporteCombustibleResumen",
"ReporteMantenimientoResumen",
"ReporteUbicacionesResumen",
"EstadisticasFlota",
"KPIsFlota",
]

View File

@@ -0,0 +1,172 @@
"""
Schemas Pydantic para Alerta y Tipo de Alerta.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
# ============================================================================
# Schemas de Tipo de Alerta
# ============================================================================
class TipoAlertaBase(BaseSchema):
"""Schema base de tipo de alerta."""
codigo: str = Field(..., min_length=2, max_length=50)
nombre: str = Field(..., min_length=2, max_length=100)
descripcion: Optional[str] = None
severidad_default: str = Field(default="media", pattern="^(baja|media|alta|critica)$")
icono: Optional[str] = Field(None, max_length=50)
color: str = Field(default="#EF4444", pattern=r"^#[0-9A-Fa-f]{6}$")
class TipoAlertaCreate(TipoAlertaBase):
"""Schema para crear tipo de alerta."""
notificar_email: bool = False
notificar_push: bool = True
notificar_sms: bool = False
prioridad: int = Field(default=50, ge=1, le=100)
class TipoAlertaUpdate(BaseSchema):
"""Schema para actualizar tipo de alerta."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
descripcion: Optional[str] = None
severidad_default: Optional[str] = Field(None, pattern="^(baja|media|alta|critica)$")
icono: Optional[str] = Field(None, max_length=50)
color: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
notificar_email: Optional[bool] = None
notificar_push: Optional[bool] = None
notificar_sms: Optional[bool] = None
prioridad: Optional[int] = Field(None, ge=1, le=100)
activo: Optional[bool] = None
class TipoAlertaResponse(TipoAlertaBase, TimestampSchema):
"""Schema de respuesta de tipo de alerta."""
id: int
notificar_email: bool
notificar_push: bool
notificar_sms: bool
prioridad: int
activo: bool
# ============================================================================
# Schemas de Alerta
# ============================================================================
class AlertaBase(BaseSchema):
"""Schema base de alerta."""
tipo_alerta_id: int
severidad: str = Field(default="media", pattern="^(baja|media|alta|critica)$")
mensaje: str = Field(..., min_length=5, max_length=500)
descripcion: Optional[str] = None
class AlertaCreate(AlertaBase):
"""Schema para crear alerta manualmente."""
vehiculo_id: Optional[int] = None
conductor_id: Optional[int] = None
dispositivo_id: Optional[int] = None
lat: Optional[float] = Field(None, ge=-90, le=90)
lng: Optional[float] = Field(None, ge=-180, le=180)
direccion: Optional[str] = Field(None, max_length=255)
velocidad: Optional[float] = Field(None, ge=0)
valor: Optional[float] = None
umbral: Optional[float] = None
datos_extra: Optional[str] = None # JSON
class AlertaUpdate(BaseSchema):
"""Schema para actualizar alerta (marcar atendida)."""
atendida: Optional[bool] = None
notas_atencion: Optional[str] = None
class AlertaResponse(AlertaBase, TimestampSchema):
"""Schema de respuesta de alerta."""
id: int
vehiculo_id: Optional[int] = None
conductor_id: Optional[int] = None
dispositivo_id: Optional[int] = None
lat: Optional[float] = None
lng: Optional[float] = None
direccion: Optional[str] = None
velocidad: Optional[float] = None
valor: Optional[float] = None
umbral: Optional[float] = None
datos_extra: Optional[str] = None
atendida: bool
atendida_por_id: Optional[int] = None
atendida_en: Optional[datetime] = None
notas_atencion: Optional[str] = None
notificacion_email_enviada: bool
notificacion_push_enviada: bool
notificacion_sms_enviada: bool
# Calculado
es_critica: bool
class AlertaConTipo(AlertaResponse):
"""Schema de alerta con información del tipo."""
tipo_alerta: TipoAlertaResponse
class AlertaConRelaciones(AlertaResponse):
"""Schema de alerta con todas las relaciones."""
tipo_alerta: TipoAlertaResponse
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
conductor_nombre: Optional[str] = None
class AlertaResumen(BaseSchema):
"""Schema resumido de alerta para listas."""
id: int
tipo_codigo: str
tipo_nombre: str
severidad: str
mensaje: str
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
creado_en: datetime
atendida: bool
class AlertasEstadisticas(BaseSchema):
"""Estadísticas de alertas."""
total: int
pendientes: int
atendidas: int
criticas: int
altas: int
medias: int
bajas: int
por_tipo: List[dict] # [{codigo, nombre, cantidad}]
por_vehiculo: List[dict] # [{vehiculo_id, nombre, cantidad}]
class AlertaAtenderRequest(BaseSchema):
"""Schema para marcar alerta como atendida."""
notas_atencion: Optional[str] = None

View File

@@ -0,0 +1,97 @@
"""
Schemas base y utilidades comunes para Pydantic.
"""
from datetime import datetime
from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel, ConfigDict
class BaseSchema(BaseModel):
"""Schema base con configuración común."""
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True,
use_enum_values=True,
json_encoders={datetime: lambda v: v.isoformat()},
)
class TimestampSchema(BaseSchema):
"""Schema con campos de timestamp."""
creado_en: datetime
actualizado_en: datetime
# Type variable para paginación genérica
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
"""Respuesta paginada genérica."""
items: List[T]
total: int
page: int
page_size: int
pages: int
@property
def has_next(self) -> bool:
return self.page < self.pages
@property
def has_prev(self) -> bool:
return self.page > 1
class MessageResponse(BaseModel):
"""Respuesta simple con mensaje."""
message: str
success: bool = True
class ErrorResponse(BaseModel):
"""Respuesta de error."""
error: dict
class GeoJSONPoint(BaseModel):
"""Schema para punto GeoJSON."""
type: str = "Point"
coordinates: List[float] # [lng, lat]
class GeoJSONFeature(BaseModel):
"""Schema para feature GeoJSON."""
type: str = "Feature"
geometry: dict
properties: dict
class GeoJSONFeatureCollection(BaseModel):
"""Schema para colección de features GeoJSON."""
type: str = "FeatureCollection"
features: List[GeoJSONFeature]
class CoordenadasSchema(BaseModel):
"""Schema para coordenadas simples."""
lat: float
lng: float
class RangoFechasSchema(BaseModel):
"""Schema para filtros de rango de fechas."""
desde: Optional[datetime] = None
hasta: Optional[datetime] = None

View File

@@ -0,0 +1,136 @@
"""
Schemas Pydantic para Carga de Combustible.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class CargaCombustibleBase(BaseSchema):
"""Schema base de carga de combustible."""
vehiculo_id: int
fecha: datetime
litros: float = Field(..., gt=0)
precio_litro: Optional[float] = Field(None, ge=0)
tipo_combustible: Optional[str] = Field(None, max_length=20)
class CargaCombustibleCreate(CargaCombustibleBase):
"""Schema para crear carga de combustible."""
conductor_id: Optional[int] = None
total: Optional[float] = Field(None, ge=0)
odometro: Optional[float] = Field(None, ge=0)
estacion: Optional[str] = Field(None, max_length=100)
estacion_direccion: Optional[str] = Field(None, max_length=255)
lat: Optional[float] = Field(None, ge=-90, le=90)
lng: Optional[float] = Field(None, ge=-180, le=180)
tanque_lleno: bool = True
metodo_pago: Optional[str] = Field(None, max_length=50)
numero_factura: Optional[str] = Field(None, max_length=50)
notas: Optional[str] = None
class CargaCombustibleUpdate(BaseSchema):
"""Schema para actualizar carga de combustible."""
fecha: Optional[datetime] = None
litros: Optional[float] = Field(None, gt=0)
precio_litro: Optional[float] = Field(None, ge=0)
total: Optional[float] = Field(None, ge=0)
tipo_combustible: Optional[str] = Field(None, max_length=20)
odometro: Optional[float] = Field(None, ge=0)
estacion: Optional[str] = Field(None, max_length=100)
estacion_direccion: Optional[str] = Field(None, max_length=255)
tanque_lleno: Optional[bool] = None
metodo_pago: Optional[str] = Field(None, max_length=50)
numero_factura: Optional[str] = Field(None, max_length=50)
notas: Optional[str] = None
class CargaCombustibleResponse(CargaCombustibleBase, TimestampSchema):
"""Schema de respuesta de carga de combustible."""
id: int
conductor_id: Optional[int] = None
total: Optional[float] = None
odometro: Optional[float] = None
estacion: Optional[str] = None
estacion_direccion: Optional[str] = None
lat: Optional[float] = None
lng: Optional[float] = None
tanque_lleno: bool
metodo_pago: Optional[str] = None
numero_factura: Optional[str] = None
notas: Optional[str] = None
class CargaCombustibleConRelaciones(CargaCombustibleResponse):
"""Schema con información del vehículo y conductor."""
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
conductor_nombre: Optional[str] = None
class RendimientoCombustible(BaseSchema):
"""Schema para rendimiento de combustible entre cargas."""
carga_id: int
fecha: datetime
litros: float
distancia_km: float
rendimiento_km_litro: float
costo_por_km: Optional[float] = None
class ReporteConsumoVehiculo(BaseSchema):
"""Schema para reporte de consumo de un vehículo."""
vehiculo_id: int
vehiculo_nombre: str
vehiculo_placa: str
periodo_inicio: datetime
periodo_fin: datetime
# Totales
total_litros: float
total_cargas: int
total_costo: float
distancia_recorrida_km: float
# Promedios
rendimiento_promedio: float # km/litro
costo_promedio_litro: float
costo_por_km: float
# Detalle de cargas
cargas: List[CargaCombustibleResponse]
class ReporteConsumoFlota(BaseSchema):
"""Schema para reporte de consumo de toda la flota."""
periodo_inicio: datetime
periodo_fin: datetime
# Totales flota
total_litros: float
total_cargas: int
total_costo: float
total_vehiculos: int
# Promedios flota
rendimiento_promedio_flota: float
costo_promedio_flota: float
# Por vehículo
por_vehiculo: List[dict] # [{vehiculo_id, nombre, placa, litros, costo, rendimiento}]
# Por tipo de combustible
por_tipo_combustible: List[dict] # [{tipo, litros, costo}]

View File

@@ -0,0 +1,94 @@
"""
Schemas Pydantic para Conductor.
"""
from datetime import date
from typing import Optional
from pydantic import EmailStr, Field
from app.schemas.base import BaseSchema, TimestampSchema
class ConductorBase(BaseSchema):
"""Schema base de conductor."""
nombre: str = Field(..., min_length=2, max_length=100)
apellido: str = Field(..., min_length=2, max_length=100)
telefono: Optional[str] = Field(None, max_length=20)
email: Optional[EmailStr] = None
documento_tipo: Optional[str] = Field(None, max_length=20)
documento_numero: Optional[str] = Field(None, max_length=50)
licencia_numero: Optional[str] = Field(None, max_length=50)
licencia_tipo: Optional[str] = Field(None, max_length=20)
licencia_vencimiento: Optional[date] = None
fecha_nacimiento: Optional[date] = None
direccion: Optional[str] = None
contacto_emergencia: Optional[str] = Field(None, max_length=100)
telefono_emergencia: Optional[str] = Field(None, max_length=20)
fecha_contratacion: Optional[date] = None
numero_empleado: Optional[str] = Field(None, max_length=50)
class ConductorCreate(ConductorBase):
"""Schema para crear conductor."""
pass
class ConductorUpdate(BaseSchema):
"""Schema para actualizar conductor."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
apellido: Optional[str] = Field(None, min_length=2, max_length=100)
telefono: Optional[str] = Field(None, max_length=20)
email: Optional[EmailStr] = None
documento_tipo: Optional[str] = Field(None, max_length=20)
documento_numero: Optional[str] = Field(None, max_length=50)
licencia_numero: Optional[str] = Field(None, max_length=50)
licencia_tipo: Optional[str] = Field(None, max_length=20)
licencia_vencimiento: Optional[date] = None
foto_url: Optional[str] = None
fecha_nacimiento: Optional[date] = None
direccion: Optional[str] = None
contacto_emergencia: Optional[str] = Field(None, max_length=100)
telefono_emergencia: Optional[str] = Field(None, max_length=20)
fecha_contratacion: Optional[date] = None
numero_empleado: Optional[str] = Field(None, max_length=50)
activo: Optional[bool] = None
notas: Optional[str] = None
class ConductorResponse(ConductorBase, TimestampSchema):
"""Schema de respuesta de conductor."""
id: int
foto_url: Optional[str] = None
activo: bool
notas: Optional[str] = None
nombre_completo: str
licencia_vigente: bool
class ConductorResumen(BaseSchema):
"""Schema resumido de conductor."""
id: int
nombre_completo: str
telefono: Optional[str] = None
licencia_vigente: bool
activo: bool
class ConductorEstadisticas(BaseSchema):
"""Estadísticas de un conductor."""
conductor_id: int
nombre_completo: str
total_viajes: int
distancia_total_km: float
tiempo_conduccion_horas: float
velocidad_promedio: float
alertas_total: int
alertas_velocidad: int
calificacion: Optional[float] = None # Score calculado

View File

@@ -0,0 +1,108 @@
"""
Schemas Pydantic para Configuración.
"""
from typing import Any, Dict, List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class ConfiguracionBase(BaseSchema):
"""Schema base de configuración."""
clave: str = Field(..., min_length=2, max_length=100)
categoria: str = Field(default="general", max_length=50)
descripcion: Optional[str] = None
class ConfiguracionCreate(ConfiguracionBase):
"""Schema para crear configuración."""
valor: Any # Se convertirá a JSON
tipo_dato: str = Field(default="string", pattern="^(string|number|boolean|json|array)$")
sensible: bool = False
editable: bool = True
class ConfiguracionUpdate(BaseSchema):
"""Schema para actualizar configuración."""
valor: Any
descripcion: Optional[str] = None
class ConfiguracionResponse(ConfiguracionBase, TimestampSchema):
"""Schema de respuesta de configuración."""
valor_json: str
tipo_dato: str
sensible: bool
editable: bool
# Valor parseado
valor: Optional[Any] = None
class ConfiguracionResumen(BaseSchema):
"""Schema resumido de configuración."""
clave: str
categoria: str
valor: Any
tipo_dato: str
editable: bool
class ConfiguracionesPorCategoria(BaseSchema):
"""Schema con configuraciones agrupadas por categoría."""
categoria: str
configuraciones: List[ConfiguracionResumen]
class ConfiguracionesResponse(BaseSchema):
"""Schema de respuesta con todas las configuraciones."""
categorias: List[str]
configuraciones: Dict[str, List[ConfiguracionResumen]]
class ActualizarConfiguracionesRequest(BaseSchema):
"""Schema para actualizar múltiples configuraciones."""
configuraciones: Dict[str, Any] # {clave: valor}
class ConfiguracionesAlertasResponse(BaseSchema):
"""Schema específico para configuraciones de alertas."""
velocidad_maxima: int
parada_minutos: int
bateria_minima: int
sin_señal_minutos: int
motor_encendido_minutos: int
class ConfiguracionesViajesResponse(BaseSchema):
"""Schema específico para configuraciones de viajes."""
velocidad_minima: float
parada_minutos: int
class ConfiguracionesNotificacionesResponse(BaseSchema):
"""Schema específico para configuraciones de notificaciones."""
email_habilitado: bool
push_habilitado: bool
destinatarios: List[str]
class ConfiguracionesMapaResponse(BaseSchema):
"""Schema específico para configuraciones de mapa."""
centro_lat: float
centro_lng: float
zoom_default: int

View File

@@ -0,0 +1,92 @@
"""
Schemas Pydantic para Dispositivo GPS/Tracker.
"""
from datetime import datetime
from typing import Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class DispositivoBase(BaseSchema):
"""Schema base de dispositivo."""
tipo: str = Field(default="gps", max_length=50)
identificador: str = Field(..., min_length=1, max_length=100)
nombre: Optional[str] = Field(None, max_length=100)
marca: Optional[str] = Field(None, max_length=50)
modelo: Optional[str] = Field(None, max_length=50)
numero_serie: Optional[str] = Field(None, max_length=100)
telefono_sim: Optional[str] = Field(None, max_length=20)
operador_sim: Optional[str] = Field(None, max_length=50)
iccid: Optional[str] = Field(None, max_length=25)
imei: Optional[str] = Field(None, max_length=20)
protocolo: str = Field(default="osmand", max_length=50)
intervalo_reporte: int = Field(default=30, ge=1, le=3600)
class DispositivoCreate(DispositivoBase):
"""Schema para crear dispositivo."""
vehiculo_id: int
class DispositivoUpdate(BaseSchema):
"""Schema para actualizar dispositivo."""
tipo: Optional[str] = Field(None, max_length=50)
nombre: Optional[str] = Field(None, max_length=100)
marca: Optional[str] = Field(None, max_length=50)
modelo: Optional[str] = Field(None, max_length=50)
numero_serie: Optional[str] = Field(None, max_length=100)
telefono_sim: Optional[str] = Field(None, max_length=20)
operador_sim: Optional[str] = Field(None, max_length=50)
iccid: Optional[str] = Field(None, max_length=25)
imei: Optional[str] = Field(None, max_length=20)
protocolo: Optional[str] = Field(None, max_length=50)
intervalo_reporte: Optional[int] = Field(None, ge=1, le=3600)
configuracion: Optional[str] = None
firmware_version: Optional[str] = Field(None, max_length=50)
activo: Optional[bool] = None
notas: Optional[str] = None
class DispositivoResponse(DispositivoBase, TimestampSchema):
"""Schema de respuesta de dispositivo."""
id: int
vehiculo_id: int
ultimo_contacto: Optional[datetime] = None
bateria: Optional[float] = None
señal_gsm: Optional[int] = None
satelites: Optional[int] = None
configuracion: Optional[str] = None
firmware_version: Optional[str] = None
activo: bool
conectado: bool
notas: Optional[str] = None
# Calculado
esta_online: bool
class DispositivoResumen(BaseSchema):
"""Schema resumido de dispositivo."""
id: int
identificador: str
tipo: str
protocolo: str
activo: bool
conectado: bool
ultimo_contacto: Optional[datetime] = None
bateria: Optional[float] = None
class DispositivoConVehiculo(DispositivoResponse):
"""Schema de dispositivo con información del vehículo."""
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None

View File

@@ -0,0 +1,160 @@
"""
Schemas Pydantic para Geocerca.
"""
from typing import List, Optional
from pydantic import Field, field_validator
from app.schemas.base import BaseSchema, TimestampSchema
class GeocercaBase(BaseSchema):
"""Schema base de geocerca."""
nombre: str = Field(..., min_length=2, max_length=100)
descripcion: Optional[str] = None
tipo: str = Field(default="circular", pattern="^(circular|poligono)$")
color: str = Field(default="#3B82F6", pattern=r"^#[0-9A-Fa-f]{6}$")
opacidad: float = Field(default=0.3, ge=0, le=1)
color_borde: str = Field(default="#1D4ED8", pattern=r"^#[0-9A-Fa-f]{6}$")
categoria: Optional[str] = Field(None, max_length=50)
class GeocercaCircularCreate(GeocercaBase):
"""Schema para crear geocerca circular."""
tipo: str = "circular"
centro_lat: float = Field(..., ge=-90, le=90)
centro_lng: float = Field(..., ge=-180, le=180)
radio_metros: float = Field(..., gt=0, le=100000)
# Configuración de alertas
alerta_entrada: bool = True
alerta_salida: bool = True
velocidad_maxima: Optional[float] = Field(None, ge=0)
# Horario (opcional, JSON)
horario_json: Optional[str] = None
# Vehículos asignados (opcional, vacío = todos)
vehiculos_ids: Optional[List[int]] = None
class GeocercaPoligonoCreate(GeocercaBase):
"""Schema para crear geocerca poligonal."""
tipo: str = "poligono"
coordenadas: List[List[float]] # [[lat, lng], [lat, lng], ...]
# Configuración de alertas
alerta_entrada: bool = True
alerta_salida: bool = True
velocidad_maxima: Optional[float] = Field(None, ge=0)
# Horario (opcional, JSON)
horario_json: Optional[str] = None
# Vehículos asignados (opcional, vacío = todos)
vehiculos_ids: Optional[List[int]] = None
@field_validator("coordenadas")
@classmethod
def validate_coordenadas(cls, v: List[List[float]]) -> List[List[float]]:
if len(v) < 3:
raise ValueError("Un polígono debe tener al menos 3 puntos")
for coord in v:
if len(coord) != 2:
raise ValueError("Cada coordenada debe tener [lat, lng]")
if not (-90 <= coord[0] <= 90):
raise ValueError("Latitud debe estar entre -90 y 90")
if not (-180 <= coord[1] <= 180):
raise ValueError("Longitud debe estar entre -180 y 180")
return v
class GeocercaUpdate(BaseSchema):
"""Schema para actualizar geocerca."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
descripcion: Optional[str] = None
color: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
opacidad: Optional[float] = Field(None, ge=0, le=1)
color_borde: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
categoria: Optional[str] = Field(None, max_length=50)
# Para circular
centro_lat: Optional[float] = Field(None, ge=-90, le=90)
centro_lng: Optional[float] = Field(None, ge=-180, le=180)
radio_metros: Optional[float] = Field(None, gt=0, le=100000)
# Para polígono
coordenadas: Optional[List[List[float]]] = None
# Configuración
alerta_entrada: Optional[bool] = None
alerta_salida: Optional[bool] = None
velocidad_maxima: Optional[float] = Field(None, ge=0)
horario_json: Optional[str] = None
activa: Optional[bool] = None
class GeocercaResponse(GeocercaBase, TimestampSchema):
"""Schema de respuesta de geocerca."""
id: int
centro_lat: Optional[float] = None
centro_lng: Optional[float] = None
radio_metros: Optional[float] = None
coordenadas_json: Optional[str] = None
alerta_entrada: bool
alerta_salida: bool
velocidad_maxima: Optional[float] = None
horario_json: Optional[str] = None
activa: bool
# Calculado
aplica_todos_vehiculos: bool
class GeocercaConVehiculos(GeocercaResponse):
"""Schema de geocerca con lista de vehículos asignados."""
vehiculos_asignados: List["VehiculoResumen"] = []
class GeocercaGeoJSON(BaseSchema):
"""Schema de geocerca en formato GeoJSON."""
type: str = "Feature"
geometry: dict
properties: dict
class AsignarVehiculosRequest(BaseSchema):
"""Schema para asignar vehículos a una geocerca."""
vehiculos_ids: List[int]
reemplazar: bool = False # True = reemplaza todos, False = agrega a existentes
class VerificarPuntoRequest(BaseSchema):
"""Schema para verificar si un punto está dentro de una geocerca."""
lat: float = Field(..., ge=-90, le=90)
lng: float = Field(..., ge=-180, le=180)
class VerificarPuntoResponse(BaseSchema):
"""Schema de respuesta de verificación de punto."""
dentro: bool
geocerca_id: int
geocerca_nombre: str
distancia_metros: Optional[float] = None # Distancia al borde si está fuera
# Import fix
from app.schemas.vehiculo import VehiculoResumen # noqa: E402
GeocercaConVehiculos.model_rebuild()

View File

@@ -0,0 +1,52 @@
"""
Schemas Pydantic para Grupo de Vehículos.
"""
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class GrupoVehiculosBase(BaseSchema):
"""Schema base de grupo de vehículos."""
nombre: str = Field(..., min_length=2, max_length=100)
descripcion: Optional[str] = None
color: str = Field(default="#3B82F6", pattern=r"^#[0-9A-Fa-f]{6}$")
icono: Optional[str] = Field(None, max_length=50)
class GrupoVehiculosCreate(GrupoVehiculosBase):
"""Schema para crear grupo de vehículos."""
pass
class GrupoVehiculosUpdate(BaseSchema):
"""Schema para actualizar grupo de vehículos."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
descripcion: Optional[str] = None
color: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
icono: Optional[str] = Field(None, max_length=50)
class GrupoVehiculosResponse(GrupoVehiculosBase, TimestampSchema):
"""Schema de respuesta de grupo de vehículos."""
id: int
cantidad_vehiculos: Optional[int] = None
class GrupoVehiculosConVehiculos(GrupoVehiculosResponse):
"""Schema con lista de vehículos del grupo."""
vehiculos: List["VehiculoResumen"] = []
# Import circular fix
from app.schemas.vehiculo import VehiculoResumen # noqa: E402
GrupoVehiculosConVehiculos.model_rebuild()

View File

@@ -0,0 +1,198 @@
"""
Schemas Pydantic para Mantenimiento y Tipo de Mantenimiento.
"""
from datetime import date, datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
# ============================================================================
# Schemas de Tipo de Mantenimiento
# ============================================================================
class TipoMantenimientoBase(BaseSchema):
"""Schema base de tipo de mantenimiento."""
nombre: str = Field(..., min_length=2, max_length=100)
descripcion: Optional[str] = None
codigo: Optional[str] = Field(None, max_length=20)
categoria: str = Field(default="preventivo", pattern="^(preventivo|correctivo|predictivo)$")
class TipoMantenimientoCreate(TipoMantenimientoBase):
"""Schema para crear tipo de mantenimiento."""
intervalo_km: Optional[int] = Field(None, gt=0)
intervalo_dias: Optional[int] = Field(None, gt=0)
costo_estimado: Optional[float] = Field(None, ge=0)
duracion_estimada_horas: Optional[float] = Field(None, ge=0)
prioridad: int = Field(default=50, ge=1, le=100)
requiere_inmovilizacion: bool = False
class TipoMantenimientoUpdate(BaseSchema):
"""Schema para actualizar tipo de mantenimiento."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
descripcion: Optional[str] = None
codigo: Optional[str] = Field(None, max_length=20)
categoria: Optional[str] = Field(None, pattern="^(preventivo|correctivo|predictivo)$")
intervalo_km: Optional[int] = Field(None, gt=0)
intervalo_dias: Optional[int] = Field(None, gt=0)
costo_estimado: Optional[float] = Field(None, ge=0)
duracion_estimada_horas: Optional[float] = Field(None, ge=0)
prioridad: Optional[int] = Field(None, ge=1, le=100)
requiere_inmovilizacion: Optional[bool] = None
activo: Optional[bool] = None
class TipoMantenimientoResponse(TipoMantenimientoBase, TimestampSchema):
"""Schema de respuesta de tipo de mantenimiento."""
id: int
intervalo_km: Optional[int] = None
intervalo_dias: Optional[int] = None
costo_estimado: Optional[float] = None
duracion_estimada_horas: Optional[float] = None
prioridad: int
requiere_inmovilizacion: bool
activo: bool
# ============================================================================
# Schemas de Mantenimiento
# ============================================================================
class MantenimientoBase(BaseSchema):
"""Schema base de mantenimiento."""
vehiculo_id: int
tipo_mantenimiento_id: int
fecha_programada: date
class MantenimientoCreate(MantenimientoBase):
"""Schema para crear/programar mantenimiento."""
odometro_programado: Optional[float] = Field(None, ge=0)
costo_estimado: Optional[float] = Field(None, ge=0)
proveedor: Optional[str] = Field(None, max_length=100)
proveedor_direccion: Optional[str] = Field(None, max_length=255)
proveedor_telefono: Optional[str] = Field(None, max_length=20)
descripcion: Optional[str] = None
notas: Optional[str] = None
class MantenimientoUpdate(BaseSchema):
"""Schema para actualizar mantenimiento."""
estado: Optional[str] = Field(None, pattern="^(programado|en_proceso|completado|cancelado|vencido)$")
fecha_programada: Optional[date] = None
fecha_realizada: Optional[date] = None
odometro_programado: Optional[float] = Field(None, ge=0)
odometro_realizado: Optional[float] = Field(None, ge=0)
costo_estimado: Optional[float] = Field(None, ge=0)
costo_real: Optional[float] = Field(None, ge=0)
costo_mano_obra: Optional[float] = Field(None, ge=0)
costo_refacciones: Optional[float] = Field(None, ge=0)
proveedor: Optional[str] = Field(None, max_length=100)
proveedor_direccion: Optional[str] = Field(None, max_length=255)
proveedor_telefono: Optional[str] = Field(None, max_length=20)
numero_factura: Optional[str] = Field(None, max_length=50)
numero_orden: Optional[str] = Field(None, max_length=50)
descripcion: Optional[str] = None
trabajos_realizados: Optional[str] = None
refacciones_usadas: Optional[str] = None
tecnico: Optional[str] = Field(None, max_length=100)
proximo_km: Optional[float] = Field(None, ge=0)
proxima_fecha: Optional[date] = None
notas: Optional[str] = None
class MantenimientoResponse(MantenimientoBase, TimestampSchema):
"""Schema de respuesta de mantenimiento."""
id: int
estado: str
fecha_realizada: Optional[date] = None
odometro_programado: Optional[float] = None
odometro_realizado: Optional[float] = None
costo_estimado: Optional[float] = None
costo_real: Optional[float] = None
costo_mano_obra: Optional[float] = None
costo_refacciones: Optional[float] = None
proveedor: Optional[str] = None
proveedor_direccion: Optional[str] = None
proveedor_telefono: Optional[str] = None
numero_factura: Optional[str] = None
numero_orden: Optional[str] = None
descripcion: Optional[str] = None
trabajos_realizados: Optional[str] = None
refacciones_usadas: Optional[str] = None
tecnico: Optional[str] = None
proximo_km: Optional[float] = None
proxima_fecha: Optional[date] = None
archivos_adjuntos: Optional[str] = None
recordatorio_enviado: bool
notas: Optional[str] = None
# Calculados
esta_vencido: bool
dias_para_vencimiento: Optional[int] = None
class MantenimientoConRelaciones(MantenimientoResponse):
"""Schema con información del vehículo y tipo."""
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
tipo_mantenimiento_nombre: Optional[str] = None
tipo_mantenimiento_categoria: Optional[str] = None
class MantenimientoResumen(BaseSchema):
"""Schema resumido de mantenimiento."""
id: int
vehiculo_id: int
vehiculo_nombre: str
vehiculo_placa: str
tipo_mantenimiento_nombre: str
estado: str
fecha_programada: date
dias_para_vencimiento: Optional[int] = None
esta_vencido: bool
class ProximosMantenimientos(BaseSchema):
"""Schema para próximos mantenimientos."""
vencidos: List[MantenimientoResumen]
proximos_7_dias: List[MantenimientoResumen]
proximos_30_dias: List[MantenimientoResumen]
class CompletarMantenimientoRequest(BaseSchema):
"""Schema para completar un mantenimiento."""
fecha_realizada: date
odometro_realizado: Optional[float] = Field(None, ge=0)
costo_real: Optional[float] = Field(None, ge=0)
costo_mano_obra: Optional[float] = Field(None, ge=0)
costo_refacciones: Optional[float] = Field(None, ge=0)
trabajos_realizados: Optional[str] = None
refacciones_usadas: Optional[str] = None
tecnico: Optional[str] = Field(None, max_length=100)
numero_factura: Optional[str] = Field(None, max_length=50)
notas: Optional[str] = None
# Próximo mantenimiento
programar_siguiente: bool = False
proximo_km: Optional[float] = Field(None, ge=0)
proxima_fecha: Optional[date] = None

View File

@@ -0,0 +1,105 @@
"""
Schemas Pydantic para Mensaje.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class MensajeBase(BaseSchema):
"""Schema base de mensaje."""
asunto: Optional[str] = Field(None, max_length=200)
contenido: str = Field(..., min_length=1)
class MensajeCreate(MensajeBase):
"""Schema para crear/enviar mensaje."""
conductor_id: int
tipo: str = Field(default="texto", pattern="^(texto|alerta|instruccion|emergencia)$")
prioridad: str = Field(default="normal", pattern="^(baja|normal|alta|urgente)$")
adjuntos: Optional[List[str]] = None # Lista de URLs
class MensajeEnviarAConductores(BaseSchema):
"""Schema para enviar mensaje a múltiples conductores."""
conductores_ids: List[int]
asunto: Optional[str] = Field(None, max_length=200)
contenido: str = Field(..., min_length=1)
tipo: str = Field(default="texto", pattern="^(texto|alerta|instruccion|emergencia)$")
prioridad: str = Field(default="normal", pattern="^(baja|normal|alta|urgente)$")
class MensajeUpdate(BaseSchema):
"""Schema para actualizar mensaje."""
leido: Optional[bool] = None
eliminado_por_admin: Optional[bool] = None
eliminado_por_conductor: Optional[bool] = None
class MensajeResponse(MensajeBase, TimestampSchema):
"""Schema de respuesta de mensaje."""
id: int
conductor_id: int
de_admin: bool
usuario_id: Optional[int] = None
tipo: str
prioridad: str
leido: bool
leido_en: Optional[datetime] = None
adjuntos: Optional[str] = None
respuesta_a_id: Optional[int] = None
eliminado_por_admin: bool
eliminado_por_conductor: bool
class MensajeConConductor(MensajeResponse):
"""Schema con información del conductor."""
conductor_nombre: Optional[str] = None
usuario_nombre: Optional[str] = None
class MensajeResumen(BaseSchema):
"""Schema resumido de mensaje."""
id: int
conductor_id: int
conductor_nombre: str
de_admin: bool
asunto: Optional[str] = None
tipo: str
prioridad: str
leido: bool
creado_en: datetime
class ConversacionConductor(BaseSchema):
"""Schema para conversación con un conductor."""
conductor_id: int
conductor_nombre: str
mensajes: List[MensajeResponse]
no_leidos: int
class MensajesNoLeidosResponse(BaseSchema):
"""Schema con conteo de mensajes no leídos."""
total_no_leidos: int
por_conductor: List[dict] # [{conductor_id, nombre, cantidad}]
class ResponderMensajeRequest(BaseSchema):
"""Schema para responder a un mensaje."""
contenido: str = Field(..., min_length=1)
adjuntos: Optional[List[str]] = None

120
backend/app/schemas/poi.py Normal file
View File

@@ -0,0 +1,120 @@
"""
Schemas Pydantic para POI (Punto de Interés).
"""
from typing import List, Optional
from pydantic import EmailStr, Field
from app.schemas.base import BaseSchema, TimestampSchema
class POIBase(BaseSchema):
"""Schema base de POI."""
nombre: str = Field(..., min_length=2, max_length=100)
descripcion: Optional[str] = None
categoria: str = Field(default="otro", max_length=50)
lat: float = Field(..., ge=-90, le=90)
lng: float = Field(..., ge=-180, le=180)
direccion: Optional[str] = Field(None, max_length=255)
ciudad: Optional[str] = Field(None, max_length=100)
estado: Optional[str] = Field(None, max_length=100)
codigo_postal: Optional[str] = Field(None, max_length=10)
radio_metros: float = Field(default=100.0, gt=0, le=10000)
class POICreate(POIBase):
"""Schema para crear POI."""
telefono: Optional[str] = Field(None, max_length=20)
email: Optional[EmailStr] = None
contacto_nombre: Optional[str] = Field(None, max_length=100)
horario_json: Optional[str] = None
icono: Optional[str] = Field(None, max_length=50)
color: str = Field(default="#10B981", pattern=r"^#[0-9A-Fa-f]{6}$")
codigo_externo: Optional[str] = Field(None, max_length=50)
notas: Optional[str] = None
class POIUpdate(BaseSchema):
"""Schema para actualizar POI."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
descripcion: Optional[str] = None
categoria: Optional[str] = Field(None, max_length=50)
lat: Optional[float] = Field(None, ge=-90, le=90)
lng: Optional[float] = Field(None, ge=-180, le=180)
direccion: Optional[str] = Field(None, max_length=255)
ciudad: Optional[str] = Field(None, max_length=100)
estado: Optional[str] = Field(None, max_length=100)
codigo_postal: Optional[str] = Field(None, max_length=10)
radio_metros: Optional[float] = Field(None, gt=0, le=10000)
telefono: Optional[str] = Field(None, max_length=20)
email: Optional[EmailStr] = None
contacto_nombre: Optional[str] = Field(None, max_length=100)
horario_json: Optional[str] = None
icono: Optional[str] = Field(None, max_length=50)
color: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
codigo_externo: Optional[str] = Field(None, max_length=50)
activo: Optional[bool] = None
notas: Optional[str] = None
class POIResponse(POIBase, TimestampSchema):
"""Schema de respuesta de POI."""
id: int
telefono: Optional[str] = None
email: Optional[str] = None
contacto_nombre: Optional[str] = None
horario_json: Optional[str] = None
icono: Optional[str] = None
color: str
codigo_externo: Optional[str] = None
activo: bool
notas: Optional[str] = None
class POIResumen(BaseSchema):
"""Schema resumido de POI."""
id: int
nombre: str
categoria: str
lat: float
lng: float
icono: Optional[str] = None
color: str
class POICercano(POIResumen):
"""Schema de POI con distancia."""
distancia_metros: float
class BuscarPOIsCercanosRequest(BaseSchema):
"""Schema para buscar POIs cercanos."""
lat: float = Field(..., ge=-90, le=90)
lng: float = Field(..., ge=-180, le=180)
radio_metros: float = Field(default=1000, gt=0, le=50000)
categoria: Optional[str] = None
limite: int = Field(default=10, ge=1, le=100)
class BuscarPOIsCercanosResponse(BaseSchema):
"""Schema de respuesta de búsqueda de POIs cercanos."""
centro_lat: float
centro_lng: float
radio_metros: float
total: int
pois: List[POICercano]
class CategoriasPOIResponse(BaseSchema):
"""Schema de respuesta con categorías de POI."""
categorias: List[dict] # [{codigo, nombre, icono, color}]

View File

@@ -0,0 +1,217 @@
"""
Schemas Pydantic para Reportes y Dashboard.
"""
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema
class DashboardResumen(BaseSchema):
"""Schema para datos del dashboard principal."""
# Contadores
total_vehiculos: int
vehiculos_activos: int
vehiculos_en_movimiento: int
vehiculos_detenidos: int
vehiculos_sin_señal: int
total_conductores: int
conductores_activos: int
# Alertas
alertas_pendientes: int
alertas_criticas: int
alertas_hoy: int
# Viajes de hoy
viajes_hoy: int
distancia_hoy_km: float
# Mantenimiento
mantenimientos_vencidos: int
mantenimientos_proximos: int
# Última actualización
actualizado_en: datetime
class DashboardGrafico(BaseSchema):
"""Schema para datos de gráficos del dashboard."""
# Distancia por día (últimos 7 días)
distancia_diaria: List[dict] # [{fecha, km}]
# Viajes por día (últimos 7 días)
viajes_diarios: List[dict] # [{fecha, cantidad}]
# Alertas por tipo (últimos 7 días)
alertas_por_tipo: List[dict] # [{tipo, cantidad}]
# Consumo de combustible (últimos 30 días)
consumo_combustible: List[dict] # [{fecha, litros}]
class ReporteRequest(BaseSchema):
"""Schema para solicitar generación de reporte."""
tipo: str = Field(
...,
pattern="^(viajes|alertas|combustible|mantenimiento|ubicaciones|resumen)$"
)
formato: str = Field(default="pdf", pattern="^(pdf|excel|csv)$")
fecha_inicio: datetime
fecha_fin: datetime
vehiculos_ids: Optional[List[int]] = None # None = todos
conductores_ids: Optional[List[int]] = None
parametros: Optional[Dict[str, Any]] = None
class ReporteResponse(BaseSchema):
"""Schema de respuesta de generación de reporte."""
id: str # UUID del reporte
tipo: str
formato: str
estado: str # pendiente, procesando, completado, error
archivo_url: Optional[str] = None
creado_en: datetime
completado_en: Optional[datetime] = None
error: Optional[str] = None
class ReporteViajesResumen(BaseSchema):
"""Schema para reporte de viajes."""
periodo_inicio: datetime
periodo_fin: datetime
total_viajes: int
distancia_total_km: float
tiempo_total_conduccion: str
velocidad_promedio: float
por_vehiculo: List[dict]
por_conductor: List[dict]
viajes: List[dict]
class ReporteAlertasResumen(BaseSchema):
"""Schema para reporte de alertas."""
periodo_inicio: datetime
periodo_fin: datetime
total_alertas: int
atendidas: int
pendientes: int
por_tipo: List[dict]
por_severidad: List[dict]
por_vehiculo: List[dict]
alertas: List[dict]
class ReporteCombustibleResumen(BaseSchema):
"""Schema para reporte de combustible."""
periodo_inicio: datetime
periodo_fin: datetime
total_litros: float
total_costo: float
rendimiento_promedio: float
por_vehiculo: List[dict]
cargas: List[dict]
class ReporteMantenimientoResumen(BaseSchema):
"""Schema para reporte de mantenimiento."""
periodo_inicio: datetime
periodo_fin: datetime
total_mantenimientos: int
completados: int
pendientes: int
vencidos: int
costo_total: float
por_tipo: List[dict]
por_vehiculo: List[dict]
mantenimientos: List[dict]
class ReporteUbicacionesResumen(BaseSchema):
"""Schema para reporte de ubicaciones/recorridos."""
periodo_inicio: datetime
periodo_fin: datetime
vehiculo_id: int
vehiculo_nombre: str
total_puntos: int
distancia_km: float
# Ruta en GeoJSON
ruta_geojson: dict
class EstadisticasFlota(BaseSchema):
"""Schema para estadísticas generales de la flota."""
periodo: str # diario, semanal, mensual
# Distancia
distancia_total_km: float
distancia_promedio_vehiculo_km: float
# Tiempo
tiempo_conduccion_total_horas: float
tiempo_ocioso_total_horas: float
# Combustible
combustible_total_litros: float
costo_combustible_total: float
rendimiento_promedio: float
# Alertas
alertas_total: int
alertas_por_vehiculo_promedio: float
# Mantenimiento
costo_mantenimiento_total: float
# Top vehículos
top_distancia: List[dict] # [{vehiculo_id, nombre, km}]
top_alertas: List[dict] # [{vehiculo_id, nombre, cantidad}]
top_combustible: List[dict] # [{vehiculo_id, nombre, litros}]
class KPIsFlota(BaseSchema):
"""Schema para KPIs de la flota."""
# Utilización
porcentaje_utilizacion: float # % de vehículos en uso
horas_promedio_uso_diario: float
# Eficiencia
km_por_litro_flota: float
costo_por_km: float
# Seguridad
alertas_por_1000km: float
excesos_velocidad_por_1000km: float
# Mantenimiento
porcentaje_mantenimientos_a_tiempo: float
costo_mantenimiento_por_km: float
# Disponibilidad
porcentaje_disponibilidad: float # % de tiempo operativo
# Comparación con periodo anterior
variacion_km: float # % vs periodo anterior
variacion_combustible: float
variacion_alertas: float
variacion_costo: float

View File

@@ -0,0 +1,139 @@
"""
Schemas Pydantic para Ubicación GPS.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, GeoJSONFeature
class UbicacionBase(BaseSchema):
"""Schema base de ubicación."""
lat: float = Field(..., ge=-90, le=90)
lng: float = Field(..., ge=-180, le=180)
velocidad: Optional[float] = Field(None, ge=0)
rumbo: Optional[float] = Field(None, ge=0, le=360)
altitud: Optional[float] = None
precision: Optional[float] = Field(None, ge=0)
satelites: Optional[int] = Field(None, ge=0)
class UbicacionCreate(UbicacionBase):
"""Schema para crear/recibir ubicación."""
vehiculo_id: Optional[int] = None # Puede venir por identificador de dispositivo
dispositivo_id: Optional[str] = None # Identificador del dispositivo
tiempo: Optional[datetime] = None # Si no se envía, usa timestamp del servidor
fuente: str = Field(default="gps", max_length=20)
bateria_dispositivo: Optional[float] = Field(None, ge=0, le=100)
bateria_vehiculo: Optional[float] = None
motor_encendido: Optional[bool] = None
odometro: Optional[float] = Field(None, ge=0)
hdop: Optional[float] = None
# Datos OBD opcionales
rpm: Optional[int] = Field(None, ge=0)
temperatura_motor: Optional[float] = None
nivel_combustible: Optional[float] = Field(None, ge=0, le=100)
class UbicacionBulkCreate(BaseSchema):
"""Schema para recibir múltiples ubicaciones."""
ubicaciones: List[UbicacionCreate]
class UbicacionResponse(UbicacionBase):
"""Schema de respuesta de ubicación."""
tiempo: datetime
vehiculo_id: int
fuente: str
bateria_dispositivo: Optional[float] = None
motor_encendido: Optional[bool] = None
odometro: Optional[float] = None
class UbicacionConVehiculo(UbicacionResponse):
"""Schema de ubicación con información del vehículo."""
vehiculo_nombre: str
vehiculo_placa: str
vehiculo_color: str
class HistorialUbicacionesRequest(BaseSchema):
"""Schema para solicitar historial de ubicaciones."""
vehiculo_id: int
desde: datetime
hasta: datetime
simplificar: bool = True # Simplificar ruta con Douglas-Peucker
intervalo_segundos: Optional[int] = None # Muestreo por intervalo
class HistorialUbicacionesResponse(BaseSchema):
"""Schema de respuesta de historial de ubicaciones."""
vehiculo_id: int
desde: datetime
hasta: datetime
total_puntos: int
distancia_km: float
tiempo_movimiento_segundos: int
velocidad_promedio: Optional[float] = None
velocidad_maxima: Optional[float] = None
ubicaciones: List[UbicacionResponse]
class UbicacionGeoJSON(GeoJSONFeature):
"""Schema de ubicación en formato GeoJSON."""
pass
class RutaGeoJSON(BaseSchema):
"""Schema de ruta completa en formato GeoJSON LineString."""
type: str = "Feature"
geometry: dict # LineString
properties: dict
# Schema para recibir ubicaciones de OsmAnd/Traccar
class OsmAndLocationCreate(BaseSchema):
"""Schema para ubicaciones recibidas de OsmAnd."""
id: str # Device identifier
lat: float
lon: float
timestamp: Optional[int] = None # Unix timestamp
speed: Optional[float] = None # km/h
bearing: Optional[float] = None # degrees
altitude: Optional[float] = None # meters
accuracy: Optional[float] = None # meters
batt: Optional[float] = None # battery percentage
class TraccarLocationCreate(BaseSchema):
"""Schema para ubicaciones recibidas de Traccar."""
id: int # Device ID in Traccar
deviceId: int
protocol: str
serverTime: datetime
deviceTime: datetime
fixTime: datetime
valid: bool
latitude: float
longitude: float
altitude: Optional[float] = None
speed: Optional[float] = None # knots
course: Optional[float] = None
address: Optional[str] = None
accuracy: Optional[float] = None
attributes: Optional[dict] = None

View File

@@ -0,0 +1,116 @@
"""
Schemas Pydantic para Usuario.
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
from app.schemas.base import BaseSchema, TimestampSchema
class UsuarioBase(BaseSchema):
"""Schema base de usuario."""
email: EmailStr
nombre: str = Field(..., min_length=2, max_length=100)
apellido: Optional[str] = Field(None, max_length=100)
telefono: Optional[str] = Field(None, max_length=20)
class UsuarioCreate(UsuarioBase):
"""Schema para crear usuario."""
password: str = Field(..., min_length=8, max_length=100)
es_admin: bool = False
@field_validator("password")
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("La contraseña debe tener al menos 8 caracteres")
if not any(c.isupper() for c in v):
raise ValueError("La contraseña debe tener al menos una mayúscula")
if not any(c.islower() for c in v):
raise ValueError("La contraseña debe tener al menos una minúscula")
if not any(c.isdigit() for c in v):
raise ValueError("La contraseña debe tener al menos un número")
return v
class UsuarioUpdate(BaseSchema):
"""Schema para actualizar usuario."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
apellido: Optional[str] = Field(None, max_length=100)
telefono: Optional[str] = Field(None, max_length=20)
avatar_url: Optional[str] = None
preferencias: Optional[str] = None
class UsuarioUpdatePassword(BaseModel):
"""Schema para cambiar contraseña."""
password_actual: str
password_nuevo: str = Field(..., min_length=8, max_length=100)
@field_validator("password_nuevo")
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("La contraseña debe tener al menos 8 caracteres")
return v
class UsuarioResponse(UsuarioBase, TimestampSchema):
"""Schema de respuesta de usuario."""
id: int
es_admin: bool
activo: bool
ultimo_acceso: Optional[datetime] = None
avatar_url: Optional[str] = None
class UsuarioInDB(UsuarioResponse):
"""Schema interno con hash de password."""
password_hash: str
# ============================================================================
# Schemas de Autenticación
# ============================================================================
class LoginRequest(BaseModel):
"""Schema para solicitud de login."""
email: EmailStr
password: str
class LoginResponse(BaseModel):
"""Schema de respuesta de login."""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int
user: UsuarioResponse
class RefreshTokenRequest(BaseModel):
"""Schema para refresh token."""
refresh_token: str
class TokenResponse(BaseModel):
"""Schema de respuesta de tokens."""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int

View File

@@ -0,0 +1,176 @@
"""
Schemas Pydantic para Vehículo.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class VehiculoBase(BaseSchema):
"""Schema base de vehículo."""
nombre: str = Field(..., min_length=2, max_length=100)
placa: str = Field(..., min_length=2, max_length=20)
vin: Optional[str] = Field(None, max_length=17)
numero_economico: Optional[str] = Field(None, max_length=50)
marca: Optional[str] = Field(None, max_length=50)
modelo: Optional[str] = Field(None, max_length=50)
año: Optional[int] = Field(None, ge=1900, le=2100)
color: Optional[str] = Field(None, max_length=30)
tipo: Optional[str] = Field(None, max_length=50)
capacidad_carga_kg: Optional[float] = Field(None, ge=0)
capacidad_pasajeros: Optional[int] = Field(None, ge=0)
capacidad_combustible_litros: Optional[float] = Field(None, ge=0)
tipo_combustible: Optional[str] = Field(None, max_length=20)
odometro_inicial: float = Field(default=0.0, ge=0)
class VehiculoCreate(VehiculoBase):
"""Schema para crear vehículo."""
conductor_id: Optional[int] = None
grupo_id: Optional[int] = None
icono: Optional[str] = Field(None, max_length=50)
color_marcador: str = Field(default="#3B82F6", pattern=r"^#[0-9A-Fa-f]{6}$")
class VehiculoUpdate(BaseSchema):
"""Schema para actualizar vehículo."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
placa: Optional[str] = Field(None, min_length=2, max_length=20)
vin: Optional[str] = Field(None, max_length=17)
numero_economico: Optional[str] = Field(None, max_length=50)
marca: Optional[str] = Field(None, max_length=50)
modelo: Optional[str] = Field(None, max_length=50)
año: Optional[int] = Field(None, ge=1900, le=2100)
color: Optional[str] = Field(None, max_length=30)
tipo: Optional[str] = Field(None, max_length=50)
capacidad_carga_kg: Optional[float] = Field(None, ge=0)
capacidad_pasajeros: Optional[int] = Field(None, ge=0)
capacidad_combustible_litros: Optional[float] = Field(None, ge=0)
tipo_combustible: Optional[str] = Field(None, max_length=20)
conductor_id: Optional[int] = None
grupo_id: Optional[int] = None
icono: Optional[str] = Field(None, max_length=50)
color_marcador: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
activo: Optional[bool] = None
en_servicio: Optional[bool] = None
notas: Optional[str] = None
class VehiculoResponse(VehiculoBase, TimestampSchema):
"""Schema de respuesta de vehículo."""
id: int
odometro_actual: float
icono: Optional[str] = None
color_marcador: str
conductor_id: Optional[int] = None
grupo_id: Optional[int] = None
activo: bool
en_servicio: bool
notas: Optional[str] = None
# Última ubicación
ultima_lat: Optional[float] = None
ultima_lng: Optional[float] = None
ultima_velocidad: Optional[float] = None
ultimo_rumbo: Optional[float] = None
ultima_ubicacion_tiempo: Optional[datetime] = None
motor_encendido: Optional[bool] = None
# Calculados
distancia_recorrida: float
class VehiculoResumen(BaseSchema):
"""Schema resumido de vehículo para listas."""
id: int
nombre: str
placa: str
marca: Optional[str] = None
modelo: Optional[str] = None
color_marcador: str
activo: bool
en_servicio: bool
# Estado actual
ultima_lat: Optional[float] = None
ultima_lng: Optional[float] = None
ultima_velocidad: Optional[float] = None
motor_encendido: Optional[bool] = None
ultima_ubicacion_tiempo: Optional[datetime] = None
class VehiculoConRelaciones(VehiculoResponse):
"""Schema de vehículo con relaciones expandidas."""
conductor: Optional["ConductorResumen"] = None
grupo: Optional["GrupoVehiculosResponse"] = None
dispositivos: List["DispositivoResumen"] = []
class VehiculoUbicacionActual(BaseSchema):
"""Schema para ubicación actual de vehículo (dashboard/mapa)."""
id: int
nombre: str
placa: str
color_marcador: str
icono: Optional[str] = None
# Ubicación
lat: Optional[float] = None
lng: Optional[float] = None
velocidad: Optional[float] = None
rumbo: Optional[float] = None
tiempo: Optional[datetime] = None
# Estado
motor_encendido: Optional[bool] = None
en_movimiento: bool = False
conductor_nombre: Optional[str] = None
class VehiculoEstadisticas(BaseSchema):
"""Estadísticas de un vehículo."""
vehiculo_id: int
nombre: str
placa: str
# Distancia
distancia_hoy_km: float
distancia_semana_km: float
distancia_mes_km: float
distancia_total_km: float
# Tiempo
tiempo_movimiento_hoy_min: int
tiempo_parado_hoy_min: int
# Combustible
consumo_mes_litros: Optional[float] = None
rendimiento_km_litro: Optional[float] = None
# Alertas
alertas_activas: int
alertas_mes: int
# Mantenimiento
proximo_mantenimiento: Optional[datetime] = None
mantenimientos_vencidos: int
# Import circular fix
from app.schemas.conductor import ConductorResumen # noqa: E402
from app.schemas.grupo_vehiculos import GrupoVehiculosResponse # noqa: E402
from app.schemas.dispositivo import DispositivoResumen # noqa: E402
VehiculoConRelaciones.model_rebuild()

View File

@@ -0,0 +1,168 @@
"""
Schemas Pydantic para Viaje y Parada.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
class ViajeBase(BaseSchema):
"""Schema base de viaje."""
vehiculo_id: int
conductor_id: Optional[int] = None
proposito: Optional[str] = Field(None, max_length=100)
notas: Optional[str] = None
class ViajeCreate(ViajeBase):
"""Schema para crear viaje manualmente."""
inicio_tiempo: datetime
inicio_lat: float
inicio_lng: float
inicio_direccion: Optional[str] = None
class ViajeUpdate(BaseSchema):
"""Schema para actualizar viaje."""
conductor_id: Optional[int] = None
proposito: Optional[str] = Field(None, max_length=100)
notas: Optional[str] = None
estado: Optional[str] = Field(None, pattern="^(en_curso|completado|cancelado)$")
class ViajeResponse(ViajeBase, TimestampSchema):
"""Schema de respuesta de viaje."""
id: int
inicio_tiempo: datetime
fin_tiempo: Optional[datetime] = None
inicio_lat: float
inicio_lng: float
inicio_direccion: Optional[str] = None
fin_lat: Optional[float] = None
fin_lng: Optional[float] = None
fin_direccion: Optional[str] = None
distancia_km: Optional[float] = None
duracion_segundos: Optional[int] = None
tiempo_movimiento_segundos: Optional[int] = None
tiempo_parado_segundos: Optional[int] = None
velocidad_promedio: Optional[float] = None
velocidad_maxima: Optional[float] = None
combustible_usado: Optional[float] = None
rendimiento: Optional[float] = None
odometro_inicio: Optional[float] = None
odometro_fin: Optional[float] = None
estado: str
puntos_gps: int
# Calculados
duracion_formateada: str
en_curso: bool
class ViajeResumen(BaseSchema):
"""Schema resumido de viaje para listas."""
id: int
vehiculo_id: int
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
conductor_nombre: Optional[str] = None
inicio_tiempo: datetime
fin_tiempo: Optional[datetime] = None
inicio_direccion: Optional[str] = None
fin_direccion: Optional[str] = None
distancia_km: Optional[float] = None
duracion_formateada: str
estado: str
class ViajeConParadas(ViajeResponse):
"""Schema de viaje con lista de paradas."""
paradas: List["ParadaResponse"] = []
class ViajeReplayData(BaseSchema):
"""Schema para datos de replay de viaje."""
viaje: ViajeResponse
ubicaciones: List["UbicacionResponse"]
paradas: List["ParadaResponse"]
# ============================================================================
# Schemas de Parada
# ============================================================================
class ParadaBase(BaseSchema):
"""Schema base de parada."""
lat: float = Field(..., ge=-90, le=90)
lng: float = Field(..., ge=-180, le=180)
tipo: str = Field(default="desconocido", max_length=50)
notas: Optional[str] = None
class ParadaCreate(ParadaBase):
"""Schema para crear parada manualmente."""
viaje_id: Optional[int] = None
vehiculo_id: int
inicio_tiempo: datetime
fin_tiempo: Optional[datetime] = None
direccion: Optional[str] = Field(None, max_length=255)
motor_apagado: Optional[bool] = None
class ParadaUpdate(BaseSchema):
"""Schema para actualizar parada."""
tipo: Optional[str] = Field(None, max_length=50)
direccion: Optional[str] = Field(None, max_length=255)
notas: Optional[str] = None
motor_apagado: Optional[bool] = None
class ParadaResponse(ParadaBase):
"""Schema de respuesta de parada."""
id: int
viaje_id: Optional[int] = None
vehiculo_id: int
inicio_tiempo: datetime
fin_tiempo: Optional[datetime] = None
duracion_segundos: Optional[int] = None
direccion: Optional[str] = None
motor_apagado: Optional[bool] = None
poi_id: Optional[int] = None
geocerca_id: Optional[int] = None
en_curso: bool
# Calculado
duracion_formateada: str
class ParadaResumen(BaseSchema):
"""Schema resumido de parada."""
id: int
vehiculo_id: int
inicio_tiempo: datetime
duracion_formateada: str
tipo: str
direccion: Optional[str] = None
# Import fix
from app.schemas.ubicacion import UbicacionResponse # noqa: E402
ViajeReplayData.model_rebuild()

View File

@@ -0,0 +1,264 @@
"""
Schemas Pydantic para Cámara, Grabación y Evento de Video.
"""
from datetime import datetime
from typing import List, Optional
from pydantic import Field
from app.schemas.base import BaseSchema, TimestampSchema
# ============================================================================
# Schemas de Cámara
# ============================================================================
class CamaraBase(BaseSchema):
"""Schema base de cámara."""
nombre: str = Field(..., min_length=2, max_length=100)
posicion: str = Field(default="frontal", max_length=50)
tipo: str = Field(default="ip", max_length=50)
class CamaraCreate(CamaraBase):
"""Schema para crear cámara."""
vehiculo_id: int
marca: Optional[str] = Field(None, max_length=50)
modelo: Optional[str] = Field(None, max_length=50)
numero_serie: Optional[str] = Field(None, max_length=100)
resolucion: Optional[str] = Field(None, max_length=20)
url_stream: Optional[str] = Field(None, max_length=500)
puerto: Optional[int] = Field(None, ge=1, le=65535)
protocolo: str = Field(default="rtsp", max_length=20)
usuario: Optional[str] = Field(None, max_length=100)
password: Optional[str] = Field(None, max_length=100) # Se encriptará
mediamtx_path: Optional[str] = Field(None, max_length=100)
grabacion_continua: bool = False
grabacion_evento: bool = True
duracion_pre_evento: int = Field(default=10, ge=0, le=60)
duracion_post_evento: int = Field(default=20, ge=0, le=120)
deteccion_colision: bool = False
deteccion_distraccion: bool = False
deteccion_fatiga: bool = False
deteccion_cambio_carril: bool = False
notas: Optional[str] = None
class CamaraUpdate(BaseSchema):
"""Schema para actualizar cámara."""
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
posicion: Optional[str] = Field(None, max_length=50)
tipo: Optional[str] = Field(None, max_length=50)
marca: Optional[str] = Field(None, max_length=50)
modelo: Optional[str] = Field(None, max_length=50)
numero_serie: Optional[str] = Field(None, max_length=100)
resolucion: Optional[str] = Field(None, max_length=20)
url_stream: Optional[str] = Field(None, max_length=500)
puerto: Optional[int] = Field(None, ge=1, le=65535)
protocolo: Optional[str] = Field(None, max_length=20)
usuario: Optional[str] = Field(None, max_length=100)
password: Optional[str] = Field(None, max_length=100)
mediamtx_path: Optional[str] = Field(None, max_length=100)
grabacion_continua: Optional[bool] = None
grabacion_evento: Optional[bool] = None
duracion_pre_evento: Optional[int] = Field(None, ge=0, le=60)
duracion_post_evento: Optional[int] = Field(None, ge=0, le=120)
deteccion_colision: Optional[bool] = None
deteccion_distraccion: Optional[bool] = None
deteccion_fatiga: Optional[bool] = None
deteccion_cambio_carril: Optional[bool] = None
activa: Optional[bool] = None
notas: Optional[str] = None
class CamaraResponse(CamaraBase, TimestampSchema):
"""Schema de respuesta de cámara."""
id: int
vehiculo_id: int
marca: Optional[str] = None
modelo: Optional[str] = None
numero_serie: Optional[str] = None
resolucion: Optional[str] = None
url_stream: Optional[str] = None
puerto: Optional[int] = None
protocolo: str
usuario: Optional[str] = None
# password no se expone
mediamtx_path: Optional[str] = None
estado: str
activa: bool
ultima_conexion: Optional[datetime] = None
grabacion_continua: bool
grabacion_evento: bool
duracion_pre_evento: int
duracion_post_evento: int
deteccion_colision: bool
deteccion_distraccion: bool
deteccion_fatiga: bool
deteccion_cambio_carril: bool
notas: Optional[str] = None
class CamaraConVehiculo(CamaraResponse):
"""Schema de cámara con información del vehículo."""
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
class CamaraStreamURL(BaseSchema):
"""Schema con URLs de streaming de una cámara."""
camara_id: int
camara_nombre: str
rtsp_url: Optional[str] = None
hls_url: Optional[str] = None
webrtc_url: Optional[str] = None
estado: str
# ============================================================================
# Schemas de Grabación
# ============================================================================
class GrabacionBase(BaseSchema):
"""Schema base de grabación."""
camara_id: int
vehiculo_id: int
inicio_tiempo: datetime
tipo: str = Field(default="continua", max_length=50)
class GrabacionCreate(GrabacionBase):
"""Schema para crear registro de grabación."""
archivo_url: str = Field(..., max_length=500)
archivo_nombre: str = Field(..., max_length=255)
formato: str = Field(default="mp4", max_length=10)
class GrabacionResponse(GrabacionBase, TimestampSchema):
"""Schema de respuesta de grabación."""
id: int
fin_tiempo: Optional[datetime] = None
duracion_segundos: Optional[int] = None
archivo_url: str
archivo_nombre: str
tamaño_mb: Optional[float] = None
formato: str
resolucion: Optional[str] = None
evento_video_id: Optional[int] = None
lat: Optional[float] = None
lng: Optional[float] = None
estado: str
thumbnail_url: Optional[str] = None
notas: Optional[str] = None
# Calculado
duracion_formateada: str
class GrabacionResumen(BaseSchema):
"""Schema resumido de grabación."""
id: int
camara_id: int
vehiculo_id: int
inicio_tiempo: datetime
duracion_formateada: str
tipo: str
thumbnail_url: Optional[str] = None
# ============================================================================
# Schemas de Evento de Video
# ============================================================================
class EventoVideoBase(BaseSchema):
"""Schema base de evento de video."""
camara_id: int
vehiculo_id: int
tipo: str = Field(..., max_length=50)
severidad: str = Field(default="media", pattern="^(baja|media|alta|critica)$")
tiempo: datetime
class EventoVideoCreate(EventoVideoBase):
"""Schema para crear evento de video."""
lat: Optional[float] = Field(None, ge=-90, le=90)
lng: Optional[float] = Field(None, ge=-180, le=180)
velocidad: Optional[float] = Field(None, ge=0)
descripcion: Optional[str] = None
confianza: Optional[float] = Field(None, ge=0, le=100)
datos_extra: Optional[str] = None
snapshot_url: Optional[str] = Field(None, max_length=500)
clip_url: Optional[str] = Field(None, max_length=500)
clip_duracion: Optional[int] = Field(None, ge=0)
class EventoVideoUpdate(BaseSchema):
"""Schema para actualizar evento de video."""
revisado: Optional[bool] = None
notas_revision: Optional[str] = None
falso_positivo: Optional[bool] = None
class EventoVideoResponse(EventoVideoBase, TimestampSchema):
"""Schema de respuesta de evento de video."""
id: int
lat: Optional[float] = None
lng: Optional[float] = None
velocidad: Optional[float] = None
descripcion: Optional[str] = None
confianza: Optional[float] = None
datos_extra: Optional[str] = None
revisado: bool
revisado_por_id: Optional[int] = None
revisado_en: Optional[datetime] = None
notas_revision: Optional[str] = None
falso_positivo: bool
snapshot_url: Optional[str] = None
clip_url: Optional[str] = None
clip_duracion: Optional[int] = None
class EventoVideoConRelaciones(EventoVideoResponse):
"""Schema con información de cámara y vehículo."""
camara_nombre: Optional[str] = None
vehiculo_nombre: Optional[str] = None
vehiculo_placa: Optional[str] = None
class EventoVideoResumen(BaseSchema):
"""Schema resumido de evento de video."""
id: int
tipo: str
severidad: str
tiempo: datetime
vehiculo_nombre: str
camara_nombre: str
revisado: bool
falso_positivo: bool
snapshot_url: Optional[str] = None
class TiposEventoVideoResponse(BaseSchema):
"""Schema con tipos de eventos de video disponibles."""
tipos: List[dict] # [{codigo, nombre, severidad}]