feat: Implement client and provider management dashboard
- Added data table component for displaying clients and providers with infinite scroll functionality. - Created dropdown actions for each client/provider including copy ID, copy RFC, view details, edit, toggle status, and delete. - Implemented dialogs for creating, editing, viewing details, and deleting clients/providers. - Integrated API calls for fetching, creating, editing, deleting, and toggling status of clients/providers. - Enhanced error handling and loading states for better user experience. - Updated server-side logic to handle pagination and company selection for client/provider data retrieval.
This commit is contained in:
536
backend/api/v1/modules/a76/clients_and_providers/service.py
Normal file
536
backend/api/v1/modules/a76/clients_and_providers/service.py
Normal file
@@ -0,0 +1,536 @@
|
||||
"""
|
||||
Capa de servicio para lógica de negocio de clientes y proveedores
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from .dto import (
|
||||
ClientProviderBasicDTO,
|
||||
ClientProviderCreateDTO,
|
||||
ClientProviderListDTO,
|
||||
ClientProviderResponseDTO,
|
||||
ClientProviderUpdateDTO,
|
||||
)
|
||||
from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientProviderService:
|
||||
"""Servicio para gestión de clientes y proveedores"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
# Métodos para TenantCRUDRoutes
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[ClientProvider], int]:
|
||||
"""Get all clients/providers for a tenant/company with pagination"""
|
||||
query = db.query(ClientProvider).filter(
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("search"):
|
||||
search_pattern = f"%{filters['search']}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
ClientProvider.name.ilike(search_pattern),
|
||||
ClientProvider.short_name.ilike(search_pattern),
|
||||
ClientProvider.rfc.ilike(search_pattern),
|
||||
)
|
||||
)
|
||||
if filters.get("client_or_provider"):
|
||||
query = query.filter(
|
||||
ClientProvider.client_or_provider == filters["client_or_provider"]
|
||||
)
|
||||
if filters.get("status"):
|
||||
enabled = 1 if filters["status"] == "enabled" else 0
|
||||
query = query.filter(ClientProvider.enabled_disabled == enabled)
|
||||
|
||||
total = query.count()
|
||||
clients = query.options(
|
||||
joinedload(ClientProvider.address),
|
||||
joinedload(ClientProvider.programs)
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
return clients, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, client_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[ClientProvider]:
|
||||
"""Get client/provider by ID"""
|
||||
return (
|
||||
db.query(ClientProvider)
|
||||
.options(
|
||||
joinedload(ClientProvider.address),
|
||||
joinedload(ClientProvider.programs)
|
||||
)
|
||||
.filter(
|
||||
ClientProvider.id == client_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
client_data: ClientProviderCreateDTO,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> ClientProvider:
|
||||
"""Create a new client/provider"""
|
||||
try:
|
||||
# Create main client/provider
|
||||
data_dict = client_data.model_dump(exclude={"address", "programs"})
|
||||
db_client = ClientProvider(
|
||||
**data_dict,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
|
||||
db.add(db_client)
|
||||
db.flush()
|
||||
|
||||
# Create address if provided
|
||||
if client_data.address:
|
||||
db_address = ClientProviderAddress(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
client_id=db_client.id,
|
||||
**client_data.address.model_dump(exclude_unset=True),
|
||||
)
|
||||
db.add(db_address)
|
||||
|
||||
# Create programs if provided
|
||||
if client_data.programs:
|
||||
db_programs = ClientProviderPrograms(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
client_id=db_client.id,
|
||||
**client_data.programs.model_dump(exclude_unset=True),
|
||||
)
|
||||
db.add(db_programs)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_client)
|
||||
|
||||
return db_client
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError creating client/provider: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Client/Provider already exists"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating client/provider: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error creating client/provider"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
client_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
client_data: ClientProviderUpdateDTO,
|
||||
) -> Optional[ClientProvider]:
|
||||
"""Update a client/provider"""
|
||||
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
|
||||
if not client:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Update main fields
|
||||
update_data = client_data.model_dump(
|
||||
exclude_unset=True, exclude={"address", "programs"}
|
||||
)
|
||||
for field, value in update_data.items():
|
||||
setattr(client, field, value)
|
||||
|
||||
# Update address
|
||||
if client_data.address:
|
||||
if client.address:
|
||||
address_data = client_data.address.model_dump(exclude_unset=True)
|
||||
for field, value in address_data.items():
|
||||
setattr(client.address, field, value)
|
||||
else:
|
||||
db_address = ClientProviderAddress(
|
||||
client_id=client.id,
|
||||
tenant_id=tenant_id,
|
||||
**client_data.address.model_dump(exclude_unset=True),
|
||||
)
|
||||
db.add(db_address)
|
||||
|
||||
# Update programs
|
||||
if client_data.programs:
|
||||
if client.programs:
|
||||
programs_data = client_data.programs.model_dump(exclude_unset=True)
|
||||
for field, value in programs_data.items():
|
||||
setattr(client.programs, field, value)
|
||||
else:
|
||||
db_programs = ClientProviderPrograms(
|
||||
client_id=client.id,
|
||||
tenant_id=tenant_id,
|
||||
**client_data.programs.model_dump(exclude_unset=True),
|
||||
)
|
||||
db.add(db_programs)
|
||||
|
||||
db.commit()
|
||||
db.refresh(client)
|
||||
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating client/provider {client_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error updating client/provider"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session, client_id: int, tenant_id: int, company_id: int
|
||||
) -> bool:
|
||||
"""Delete a client/provider"""
|
||||
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
|
||||
if not client:
|
||||
return False
|
||||
|
||||
try:
|
||||
db.delete(client)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting client/provider {client_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error deleting client/provider"
|
||||
)
|
||||
|
||||
# Legacy methods for custom endpoints
|
||||
|
||||
def create_clients_and_providers_legacy(
|
||||
self, company_id: int, client_data: ClientProviderCreateDTO
|
||||
) -> ClientProviderResponseDTO:
|
||||
"""Legacy method for creating client/provider"""
|
||||
try:
|
||||
# Create main client/provider
|
||||
data_dict = client_data.model_dump(exclude={"address", "programs"})
|
||||
db_client = ClientProvider(**data_dict)
|
||||
self.db.add(db_client)
|
||||
self.db.flush()
|
||||
|
||||
# Create address if provided
|
||||
if client_data.address:
|
||||
db_address = ClientProviderAddress(
|
||||
company_id=company_id,
|
||||
client_id=db_client.id,
|
||||
**client_data.address.model_dump(exclude_unset=True),
|
||||
)
|
||||
self.db.add(db_address)
|
||||
|
||||
# Create programs if provided
|
||||
if client_data.programs:
|
||||
db_programs = ClientProviderPrograms(
|
||||
company_id=company_id,
|
||||
client_id=db_client.id,
|
||||
**client_data.programs.model_dump(exclude_unset=True),
|
||||
)
|
||||
self.db.add(db_programs)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return self._get_client_with_relations(db_client.id)
|
||||
|
||||
except IntegrityError as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"IntegrityError creating client/provider: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Client/Provider with this ID already exists"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error creating client/provider: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error creating client/provider"
|
||||
)
|
||||
|
||||
def get_clients_and_providers(
|
||||
self, client_id: str
|
||||
) -> Optional[ClientProviderResponseDTO]:
|
||||
"""
|
||||
Obtiene un cliente/proveedor por ID
|
||||
|
||||
Args:
|
||||
client_id: ID del cliente/proveedor
|
||||
|
||||
Returns:
|
||||
ClientProviderResponseDTO o None si no existe
|
||||
"""
|
||||
return self._get_client_with_relations(client_id)
|
||||
|
||||
def _get_client_with_relations(
|
||||
self, client_id: str
|
||||
) -> Optional[ClientProviderResponseDTO]:
|
||||
"""Método privado para obtener cliente con relaciones"""
|
||||
client = (
|
||||
self.db.query(ClientProvider)
|
||||
.options(
|
||||
joinedload(ClientProvider.address), joinedload(ClientProvider.programs)
|
||||
)
|
||||
.filter(ClientProvider.client_id == client_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not client:
|
||||
return None
|
||||
return ClientProviderResponseDTO.model_validate(client)
|
||||
|
||||
def list_clients_providers(
|
||||
self,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
client_or_provider: Optional[str] = None,
|
||||
enabled_only: bool = False,
|
||||
) -> ClientProviderListDTO:
|
||||
"""
|
||||
Lista clientes/proveedores con filtros
|
||||
|
||||
Args:
|
||||
skip: Número de registros a omitir
|
||||
limit: Número máximo de registros a retornar
|
||||
search: Texto de búsqueda (nombre, RFC, ID)
|
||||
client_or_provider: Filtrar por tipo (client=Cliente, provider=Proveedor)
|
||||
enabled_only: Si True, solo retorna activos
|
||||
|
||||
Returns:
|
||||
ClientProviderListDTO con la lista paginada
|
||||
"""
|
||||
query = self.db.query(ClientProvider)
|
||||
|
||||
# Aplicar filtros
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
ClientProvider.name.ilike(search_pattern),
|
||||
ClientProvider.short_name.ilike(search_pattern),
|
||||
ClientProvider.rfc.ilike(search_pattern),
|
||||
ClientProvider.client_id.ilike(search_pattern),
|
||||
)
|
||||
)
|
||||
|
||||
if client_or_provider:
|
||||
query = query.filter(
|
||||
ClientProvider.client_or_provider == client_or_provider
|
||||
)
|
||||
|
||||
if enabled_only:
|
||||
query = query.filter(ClientProvider.enabled_disabled == 1)
|
||||
|
||||
# Contar total
|
||||
total = query.count()
|
||||
|
||||
# Aplicar paginación
|
||||
clients = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Convertir a DTOs básicos
|
||||
client_dtos = [
|
||||
ClientProviderBasicDTO.model_validate(client) for client in clients
|
||||
]
|
||||
|
||||
return ClientProviderListDTO(
|
||||
clients=client_dtos,
|
||||
total=total,
|
||||
page=(skip // limit) + 1 if limit > 0 else 1,
|
||||
size=len(client_dtos),
|
||||
)
|
||||
|
||||
def update_clients_and_providers(
|
||||
self, client_id: str, client_data: ClientProviderUpdateDTO
|
||||
) -> Optional[ClientProviderResponseDTO]:
|
||||
"""
|
||||
Actualiza un cliente/proveedor
|
||||
|
||||
Args:
|
||||
client_id: ID del cliente/proveedor a actualizar
|
||||
client_data: Datos a actualizar
|
||||
|
||||
Returns:
|
||||
ClientProviderResponseDTO actualizado o None si no existe
|
||||
"""
|
||||
client = (
|
||||
self.db.query(ClientProvider)
|
||||
.filter(ClientProvider.client_id == client_id)
|
||||
.first()
|
||||
)
|
||||
if not client:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Actualizar campos del cliente principal
|
||||
update_data = client_data.model_dump(
|
||||
exclude_unset=True, exclude={"address", "programs"}
|
||||
)
|
||||
for field, value in update_data.items():
|
||||
setattr(client, field, value)
|
||||
|
||||
# Actualizar dirección
|
||||
if client_data.address:
|
||||
address = (
|
||||
self.db.query(ClientProviderAddress)
|
||||
.filter(ClientProviderAddress.client_id == client_id)
|
||||
.first()
|
||||
)
|
||||
if address:
|
||||
# Actualizar dirección existente
|
||||
address_data = client_data.address.model_dump(exclude_unset=True)
|
||||
for field, value in address_data.items():
|
||||
setattr(address, field, value)
|
||||
else:
|
||||
# Crear nueva dirección
|
||||
address = ClientProviderAddress(
|
||||
client_id=client_id,
|
||||
**client_data.address.model_dump(exclude_unset=True),
|
||||
)
|
||||
self.db.add(address)
|
||||
|
||||
# Actualizar programas
|
||||
if client_data.programs:
|
||||
programs = (
|
||||
self.db.query(ClientProviderPrograms)
|
||||
.filter(ClientProviderPrograms.client_id == client_id)
|
||||
.first()
|
||||
)
|
||||
if programs:
|
||||
# Actualizar programas existentes
|
||||
programs_data = client_data.programs.model_dump(exclude_unset=True)
|
||||
for field, value in programs_data.items():
|
||||
setattr(programs, field, value)
|
||||
else:
|
||||
# Crear nuevos programas
|
||||
programs = ClientProviderPrograms(
|
||||
client_id=client_id,
|
||||
**client_data.programs.model_dump(exclude_unset=True),
|
||||
)
|
||||
self.db.add(programs)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return self._get_client_with_relations(client_id)
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error updating client/provider {client_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error updating client/provider"
|
||||
)
|
||||
|
||||
def delete_clients_and_providers(self, client_id: str) -> bool:
|
||||
"""
|
||||
Elimina un cliente/proveedor
|
||||
|
||||
Args:
|
||||
client_id: ID del cliente/proveedor a eliminar
|
||||
|
||||
Returns:
|
||||
True si se eliminó, False si no existe
|
||||
"""
|
||||
client = (
|
||||
self.db.query(ClientProvider)
|
||||
.filter(ClientProvider.client_id == client_id)
|
||||
.first()
|
||||
)
|
||||
if not client:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.db.delete(client) # Las relaciones se eliminan en cascada
|
||||
self.db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error deleting client/provider {client_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error deleting client/provider"
|
||||
)
|
||||
|
||||
def get_clients_only(
|
||||
self, skip: int = 0, limit: int = 100
|
||||
) -> List[ClientProviderBasicDTO]:
|
||||
"""Obtiene solo clientes (client)"""
|
||||
query = self.db.query(ClientProvider).filter(
|
||||
ClientProvider.client_or_provider == "client"
|
||||
)
|
||||
clients = query.offset(skip).limit(limit).all()
|
||||
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
|
||||
|
||||
def get_providers_only(
|
||||
self, skip: int = 0, limit: int = 100
|
||||
) -> List[ClientProviderBasicDTO]:
|
||||
"""Obtiene solo proveedores (provider)"""
|
||||
query = self.db.query(ClientProvider).filter(
|
||||
ClientProvider.client_or_provider == "provider"
|
||||
)
|
||||
providers = query.offset(skip).limit(limit).all()
|
||||
return [
|
||||
ClientProviderBasicDTO.model_validate(provider) for provider in providers
|
||||
]
|
||||
|
||||
def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]:
|
||||
"""Busca clientes/proveedores por RFC"""
|
||||
clients = (
|
||||
self.db.query(ClientProvider)
|
||||
.filter(ClientProvider.rfc.ilike(f"%{rfc}%"))
|
||||
.all()
|
||||
)
|
||||
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
|
||||
|
||||
def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]:
|
||||
"""Cambia el estado habilitado/deshabilitado"""
|
||||
client = (
|
||||
self.db.query(ClientProvider)
|
||||
.filter(ClientProvider.client_id == client_id)
|
||||
.first()
|
||||
)
|
||||
if not client:
|
||||
return None
|
||||
|
||||
# Toggle status (1 = habilitado, 0 = deshabilitado)
|
||||
client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0
|
||||
|
||||
try:
|
||||
self.db.commit()
|
||||
logger.info(
|
||||
f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}"
|
||||
)
|
||||
return self._get_client_with_relations(client_id)
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error toggling status for {client_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error updating status")
|
||||
Reference in New Issue
Block a user