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:
370
backend/app/schemas/__init__.py
Normal file
370
backend/app/schemas/__init__.py
Normal 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",
|
||||
]
|
||||
172
backend/app/schemas/alerta.py
Normal file
172
backend/app/schemas/alerta.py
Normal 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
|
||||
97
backend/app/schemas/base.py
Normal file
97
backend/app/schemas/base.py
Normal 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
|
||||
136
backend/app/schemas/combustible.py
Normal file
136
backend/app/schemas/combustible.py
Normal 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}]
|
||||
94
backend/app/schemas/conductor.py
Normal file
94
backend/app/schemas/conductor.py
Normal 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
|
||||
108
backend/app/schemas/configuracion.py
Normal file
108
backend/app/schemas/configuracion.py
Normal 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
|
||||
92
backend/app/schemas/dispositivo.py
Normal file
92
backend/app/schemas/dispositivo.py
Normal 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
|
||||
160
backend/app/schemas/geocerca.py
Normal file
160
backend/app/schemas/geocerca.py
Normal 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()
|
||||
52
backend/app/schemas/grupo_vehiculos.py
Normal file
52
backend/app/schemas/grupo_vehiculos.py
Normal 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()
|
||||
198
backend/app/schemas/mantenimiento.py
Normal file
198
backend/app/schemas/mantenimiento.py
Normal 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
|
||||
105
backend/app/schemas/mensaje.py
Normal file
105
backend/app/schemas/mensaje.py
Normal 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
120
backend/app/schemas/poi.py
Normal 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}]
|
||||
217
backend/app/schemas/reporte.py
Normal file
217
backend/app/schemas/reporte.py
Normal 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
|
||||
139
backend/app/schemas/ubicacion.py
Normal file
139
backend/app/schemas/ubicacion.py
Normal 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
|
||||
116
backend/app/schemas/usuario.py
Normal file
116
backend/app/schemas/usuario.py
Normal 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
|
||||
176
backend/app/schemas/vehiculo.py
Normal file
176
backend/app/schemas/vehiculo.py
Normal 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()
|
||||
168
backend/app/schemas/viaje.py
Normal file
168
backend/app/schemas/viaje.py
Normal 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()
|
||||
264
backend/app/schemas/video.py
Normal file
264
backend/app/schemas/video.py
Normal 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}]
|
||||
Reference in New Issue
Block a user