Mejoras en Módulo de Tickets: - Implementado sistema de filtros funcional por estado y prioridad - Tabla compacta estilo auditoría (50% más espacio visible) - Backend actualizado: parámetros 'status' y 'priority' con validación - Interfaz más limpia con labels reducidos y 2 columnas de filtros - Eliminación de columna SLA duplicada en tabla Correcciones Backend: - Endpoint /v1/tickets/: filtros 'status' y 'priority' funcionan correctamente - Endpoint /v1/sla/violations: timezone UTC y eager loading con selectinload - Endpoint /v1/client-profile/: generación explícita de UUID - Migración fix_client_profiles_timestamps aplicada Mejoras UI Frontend: - Tabla tickets: encabezados uppercase text-xs, celdas px-3 py-2 - Toggle de estado activo/inactivo en gestión de tenants (tabla + modal) - Badges más compactos con rounded-full - Botones de acciones con separador visual y transiciones - Filtros con URLSearchParams para construcción correcta de queries Arquitectura: - SQLAlchemy: eager loading para evitar N+1 queries - Timezone handling: datetime.now(timezone.utc) para comparaciones - Svelte reactivity: keyed loops y spread operator para forzar updates - API client: endpoint con query string completo Estado del sistema: Totalmente funcional para producción MVP
302 lines
9.1 KiB
Python
302 lines
9.1 KiB
Python
"""
|
|
Client Profile Endpoints - ServiceManagerWeb
|
|
Endpoints para gestión del perfil empresarial de clientes
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, or_
|
|
from typing import Optional
|
|
|
|
from app.core.database import get_db
|
|
from app.api.deps import get_current_user, get_current_tenant
|
|
from app.api.schemas.client_profile import (
|
|
ClientProfileCreate,
|
|
ClientProfileUpdate,
|
|
ClientProfileResponse,
|
|
ClientProfileSummary
|
|
)
|
|
from app.models.user import User
|
|
from app.models.tenant import Tenant
|
|
from app.models.client_profile import ClientProfile
|
|
import uuid
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=ClientProfileResponse)
|
|
async def get_current_client_profile(
|
|
current_user: User = Depends(get_current_user),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Obtener el perfil empresarial del tenant actual.
|
|
|
|
**Permisos**: CLIENT_ADMIN, CLIENT_USER
|
|
"""
|
|
# Solo clientes pueden acceder
|
|
if current_user.role not in ['CLIENT_ADMIN', 'CLIENT_USER']:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Solo los clientes pueden acceder al perfil empresarial"
|
|
)
|
|
|
|
# Buscar perfil existente
|
|
result = await db.execute(
|
|
select(ClientProfile).where(ClientProfile.tenant_id == current_tenant.id)
|
|
)
|
|
profile = result.scalar_one_or_none()
|
|
|
|
if not profile:
|
|
# Si no existe, crear uno vacío con valores por defecto explícitos
|
|
profile = ClientProfile(
|
|
id=uuid.uuid4(),
|
|
tenant_id=current_tenant.id
|
|
)
|
|
db.add(profile)
|
|
await db.commit()
|
|
await db.refresh(profile)
|
|
|
|
return profile
|
|
|
|
|
|
@router.post("/", response_model=ClientProfileResponse)
|
|
async def create_or_update_client_profile(
|
|
profile_data: ClientProfileCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Crear o actualizar el perfil empresarial del tenant actual.
|
|
|
|
**Permisos**: CLIENT_ADMIN
|
|
"""
|
|
# Solo CLIENT_ADMIN puede modificar el perfil
|
|
if current_user.role != 'CLIENT_ADMIN':
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Solo los administradores de cliente pueden modificar el perfil empresarial"
|
|
)
|
|
|
|
# Buscar perfil existente
|
|
result = await db.execute(
|
|
select(ClientProfile).where(ClientProfile.tenant_id == current_tenant.id)
|
|
)
|
|
existing_profile = result.scalar_one_or_none()
|
|
|
|
if existing_profile:
|
|
# Actualizar perfil existente
|
|
update_data = profile_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(existing_profile, field, value)
|
|
|
|
profile = existing_profile
|
|
else:
|
|
# Crear nuevo perfil
|
|
profile = ClientProfile(
|
|
tenant_id=current_tenant.id,
|
|
**profile_data.dict()
|
|
)
|
|
db.add(profile)
|
|
|
|
await db.commit()
|
|
await db.refresh(profile)
|
|
|
|
return profile
|
|
|
|
|
|
@router.patch("/", response_model=ClientProfileResponse)
|
|
async def update_client_profile(
|
|
profile_data: ClientProfileUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Actualizar parcialmente el perfil empresarial del tenant actual.
|
|
|
|
**Permisos**: CLIENT_ADMIN
|
|
"""
|
|
# Solo CLIENT_ADMIN puede modificar el perfil
|
|
if current_user.role != 'CLIENT_ADMIN':
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Solo los administradores de cliente pueden modificar el perfil empresarial"
|
|
)
|
|
|
|
# Buscar perfil existente
|
|
result = await db.execute(
|
|
select(ClientProfile).where(ClientProfile.tenant_id == current_tenant.id)
|
|
)
|
|
profile = result.scalar_one_or_none()
|
|
|
|
if not profile:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Perfil empresarial no encontrado"
|
|
)
|
|
|
|
# Actualizar solo campos proporcionados
|
|
update_data = profile_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(profile, field, value)
|
|
|
|
await db.commit()
|
|
await db.refresh(profile)
|
|
|
|
return profile
|
|
|
|
|
|
@router.delete("/")
|
|
async def delete_client_profile(
|
|
current_user: User = Depends(get_current_user),
|
|
current_tenant: Tenant = Depends(get_current_tenant),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Eliminar el perfil empresarial del tenant actual.
|
|
|
|
**Permisos**: CLIENT_ADMIN
|
|
"""
|
|
# Solo CLIENT_ADMIN puede eliminar el perfil
|
|
if current_user.role != 'CLIENT_ADMIN':
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Solo los administradores de cliente pueden eliminar el perfil empresarial"
|
|
)
|
|
|
|
# Buscar perfil existente
|
|
result = await db.execute(
|
|
select(ClientProfile).where(ClientProfile.tenant_id == current_tenant.id)
|
|
)
|
|
profile = result.scalar_one_or_none()
|
|
|
|
if not profile:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Perfil empresarial no encontrado"
|
|
)
|
|
|
|
await db.delete(profile)
|
|
await db.commit()
|
|
|
|
return {"message": "Perfil empresarial eliminado exitosamente"}
|
|
|
|
|
|
# === ENDPOINTS ADMINISTRATIVOS (Solo para ADMIN y SUPPORT_MANAGER) ===
|
|
|
|
@router.get("/admin/list", response_model=list[ClientProfileSummary])
|
|
async def list_all_client_profiles(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
search: Optional[str] = None,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Listar todos los perfiles empresariales (solo para administradores).
|
|
|
|
**Permisos**: ADMIN, SUPPORT_MANAGER
|
|
"""
|
|
# Solo personal interno puede ver todos los perfiles
|
|
if current_user.role not in ['ADMIN', 'SUPPORT_MANAGER']:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Acceso denegado"
|
|
)
|
|
|
|
query = select(ClientProfile)
|
|
|
|
# Filtro de búsqueda
|
|
if search:
|
|
search_filter = or_(
|
|
ClientProfile.business_name.ilike(f"%{search}%"),
|
|
ClientProfile.commercial_name.ilike(f"%{search}%"),
|
|
ClientProfile.rfc.ilike(f"%{search}%"),
|
|
ClientProfile.client_code.ilike(f"%{search}%")
|
|
)
|
|
query = query.where(search_filter)
|
|
|
|
# Paginación
|
|
query = query.offset(skip).limit(limit)
|
|
query = query.order_by(ClientProfile.created_at.desc())
|
|
|
|
result = await db.execute(query)
|
|
profiles = result.scalars().all()
|
|
|
|
return profiles
|
|
|
|
|
|
@router.get("/admin/{tenant_id}", response_model=ClientProfileResponse)
|
|
async def get_client_profile_by_tenant(
|
|
tenant_id: uuid.UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Obtener perfil empresarial de un tenant específico (solo para administradores).
|
|
|
|
**Permisos**: ADMIN, SUPPORT_MANAGER
|
|
"""
|
|
# Solo personal interno puede ver perfiles de otros tenants
|
|
if current_user.role not in ['ADMIN', 'SUPPORT_MANAGER']:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Acceso denegado"
|
|
)
|
|
|
|
result = await db.execute(
|
|
select(ClientProfile).where(ClientProfile.tenant_id == tenant_id)
|
|
)
|
|
profile = result.scalar_one_or_none()
|
|
|
|
if not profile:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Perfil empresarial no encontrado"
|
|
)
|
|
|
|
return profile
|
|
|
|
|
|
@router.patch("/admin/{tenant_id}", response_model=ClientProfileResponse)
|
|
async def update_client_profile_by_admin(
|
|
tenant_id: uuid.UUID,
|
|
profile_data: ClientProfileUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Actualizar perfil empresarial de un tenant específico (solo para administradores).
|
|
|
|
**Permisos**: ADMIN, SUPPORT_MANAGER
|
|
"""
|
|
# Solo personal interno puede modificar perfiles de otros tenants
|
|
if current_user.role not in ['ADMIN', 'SUPPORT_MANAGER']:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Acceso denegado"
|
|
)
|
|
|
|
result = await db.execute(
|
|
select(ClientProfile).where(ClientProfile.tenant_id == tenant_id)
|
|
)
|
|
profile = result.scalar_one_or_none()
|
|
|
|
if not profile:
|
|
# Crear perfil si no existe
|
|
profile = ClientProfile(tenant_id=tenant_id, **profile_data.dict(exclude_unset=True))
|
|
db.add(profile)
|
|
else:
|
|
# Actualizar perfil existente
|
|
update_data = profile_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(profile, field, value)
|
|
|
|
await db.commit()
|
|
await db.refresh(profile)
|
|
|
|
return profile |