hotfix: Advanced filtering system v1.4.1.1

This commit is contained in:
2026-02-10 08:49:37 -07:00
parent 94e9d91586
commit 5cff309422
2 changed files with 245 additions and 56 deletions

View File

@@ -217,12 +217,18 @@ async def get_all_tickets_admin(
status_filter: Optional[str] = None, status_filter: Optional[str] = None,
priority_filter: Optional[str] = None, priority_filter: Optional[str] = None,
tenant_id_filter: Optional[str] = None, tenant_id_filter: Optional[str] = None,
category_filter: Optional[str] = None,
assigned_to_filter: Optional[str] = None,
search: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
""" """
Obtener todos los tickets de todos los tenants (solo para administradores) Obtener todos los tickets de todos los tenants (solo para administradores)
Incluye información del tenant y usuario que creó el ticket Incluye información del tenant y usuario que creó el ticket
Filtros: estado, prioridad, tenant, categoría, asignado a, búsqueda de texto y fechas
""" """
# Verificar que el usuario sea administrador # Verificar que el usuario sea administrador
if current_user.role not in ["ADMIN", "SUPPORT_MANAGER"]: if current_user.role not in ["ADMIN", "SUPPORT_MANAGER"]:
@@ -269,6 +275,57 @@ async def get_all_tickets_admin(
detail="Invalid tenant ID format" detail="Invalid tenant ID format"
) )
if category_filter:
try:
category_uuid = uuid.UUID(category_filter)
query = query.where(Ticket.category_id == category_uuid)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid category ID format"
)
if assigned_to_filter:
try:
assigned_uuid = uuid.UUID(assigned_to_filter)
query = query.where(Ticket.assigned_to == assigned_uuid)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid assigned user ID format"
)
if search:
# Búsqueda de texto en subject y description
search_pattern = f"%{search}%"
query = query.where(
(Ticket.subject.ilike(search_pattern)) |
(Ticket.description.ilike(search_pattern))
)
if date_from:
try:
from datetime import datetime
date_from_parsed = datetime.fromisoformat(date_from)
query = query.where(Ticket.created_at >= date_from_parsed)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid date_from format. Use YYYY-MM-DD"
)
if date_to:
try:
from datetime import datetime, timedelta
# Agregar 1 día para incluir todo el día final
date_to_parsed = datetime.fromisoformat(date_to) + timedelta(days=1)
query = query.where(Ticket.created_at < date_to_parsed)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid date_to format. Use YYYY-MM-DD"
)
query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit) query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(query) result = await db.execute(query)

View File

@@ -20,6 +20,14 @@
let filterStatus = ''; let filterStatus = '';
let filterPriority = ''; let filterPriority = '';
let filterTenant = ''; // Nuevo filtro por cliente/tenant let filterTenant = ''; // Nuevo filtro por cliente/tenant
let filterCategory = ''; // Filtro por categoría
let filterAssignedTo = ''; // Filtro por asignado a
let searchText = ''; // Búsqueda por texto
let filterDateFrom = ''; // Fecha desde
let filterDateTo = ''; // Fecha hasta
// Estado de filtros
$: activeFiltersCount = [filterTenant, filterStatus, filterPriority, filterCategory, filterAssignedTo, searchText, filterDateFrom, filterDateTo].filter(f => f && f.trim()).length;
// Form para editar // Form para editar
let editFormData = { let editFormData = {
@@ -61,6 +69,11 @@
if (filterStatus) ticketParams.status_filter = filterStatus; if (filterStatus) ticketParams.status_filter = filterStatus;
if (filterPriority) ticketParams.priority_filter = filterPriority; if (filterPriority) ticketParams.priority_filter = filterPriority;
if (filterTenant) ticketParams.tenant_id_filter = filterTenant; if (filterTenant) ticketParams.tenant_id_filter = filterTenant;
if (filterCategory) ticketParams.category_filter = filterCategory;
if (filterAssignedTo) ticketParams.assigned_to_filter = filterAssignedTo;
if (searchText) ticketParams.search = searchText;
if (filterDateFrom) ticketParams.date_from = filterDateFrom;
if (filterDateTo) ticketParams.date_to = filterDateTo;
const [ticketsData, categoriesData, systemsData, usersData, tenantsData] = await Promise.all([ const [ticketsData, categoriesData, systemsData, usersData, tenantsData] = await Promise.all([
// Usar el nuevo endpoint administrativo // Usar el nuevo endpoint administrativo
@@ -87,6 +100,28 @@
loadData(); loadData();
} }
// Limpiar todos los filtros
function clearFilters() {
filterStatus = '';
filterPriority = '';
filterTenant = '';
filterCategory = '';
filterAssignedTo = '';
searchText = '';
filterDateFrom = '';
filterDateTo = '';
applyFilters();
}
// Búsqueda en tiempo real (debounced)
let searchTimeout;
function handleSearchInput() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
applyFilters();
}, 500);
}
function openCreateModal() { function openCreateModal() {
selectedTicket = null; selectedTicket = null;
formData = { formData = {
@@ -225,12 +260,31 @@
</script> </script>
<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex-auto"> <div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">Tickets de Soporte</h1> <h1 class="text-xl font-semibold text-gray-900">Tickets de Soporte</h1>
<p class="mt-2 text-sm text-gray-700">Gestión de tickets del sistema de mesa de ayuda.</p> <p class="mt-2 text-sm text-gray-700">
Gestión de tickets del sistema de mesa de ayuda.
{#if activeFiltersCount > 0}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">
{activeFiltersCount} filtro{activeFiltersCount !== 1 ? 's' : ''} activo{activeFiltersCount !== 1 ? 's' : ''}
</span>
{/if}
</p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none space-x-3">
{#if activeFiltersCount > 0}
<button
type="button"
on:click={clearFilters}
class="inline-flex items-center justify-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Limpiar filtros
</button>
{/if}
<button <button
type="button" type="button"
on:click={openCreateModal} on:click={openCreateModal}
@@ -241,61 +295,139 @@
</div> </div>
</div> </div>
<!-- Filtros --> <!-- Filtros Avanzados -->
<div class="mt-6 bg-white shadow sm:rounded-lg p-4"> <div class="mt-6 bg-white shadow sm:rounded-lg">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4"> <div class="px-4 py-5 sm:p-6">
<div> <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Filtros</h3>
<label for="filterTenant" class="block text-sm font-medium text-gray-700">Cliente/Empresa</label>
<select <!-- Primera fila - Búsqueda y Filtros principales -->
id="filterTenant" <div class="grid grid-cols-1 gap-4 sm:grid-cols-4 mb-4">
bind:value={filterTenant} <!-- Búsqueda por texto -->
on:change={applyFilters} <div class="sm:col-span-2">
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2" <label for="searchText" class="block text-sm font-medium text-gray-700 mb-1">Búsqueda</label>
> <div class="relative">
<option value="">Todos los clientes</option> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
{#each tenants as tenant} <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<option value={tenant.id}>{tenant.name}</option> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
{/each} </svg>
</select> </div>
<input
type="text"
id="searchText"
bind:value={searchText}
on:input={handleSearchInput}
placeholder="Buscar en título o descripción..."
class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
<!-- Cliente/Empresa -->
<div>
<label for="filterTenant" class="block text-sm font-medium text-gray-700 mb-1">Cliente/Empresa</label>
<select
id="filterTenant"
bind:value={filterTenant}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<option value="">Todos los clientes</option>
{#each tenants as tenant}
<option value={tenant.id}>{tenant.name}</option>
{/each}
</select>
</div>
<!-- Estado -->
<div>
<label for="filterStatus" class="block text-sm font-medium text-gray-700 mb-1">Estado</label>
<select
id="filterStatus"
bind:value={filterStatus}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<option value="">Todos</option>
{#each STATUSES as status}
<option value={status.value}>{status.label}</option>
{/each}
</select>
</div>
</div> </div>
<div> <!-- Segunda fila - Filtros secundarios -->
<label for="filterStatus" class="block text-sm font-medium text-gray-700">Estado</label> <div class="grid grid-cols-1 gap-4 sm:grid-cols-5">
<select <!-- Prioridad -->
id="filterStatus" <div>
bind:value={filterStatus} <label for="filterPriority" class="block text-sm font-medium text-gray-700 mb-1">Prioridad</label>
on:change={applyFilters} <select
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2" id="filterPriority"
> bind:value={filterPriority}
<option value="">Todos</option> on:change={applyFilters}
{#each STATUSES as status} class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
<option value={status.value}>{status.label}</option> >
{/each} <option value="">Todas</option>
</select> {#each PRIORITIES as priority}
</div> <option value={priority.value}>{priority.label}</option>
{/each}
</select>
</div>
<div> <!-- Categoría -->
<label for="filterPriority" class="block text-sm font-medium text-gray-700">Prioridad</label> <div>
<select <label for="filterCategory" class="block text-sm font-medium text-gray-700 mb-1">Categoría</label>
id="filterPriority" <select
bind:value={filterPriority} id="filterCategory"
on:change={applyFilters} bind:value={filterCategory}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2" on:change={applyFilters}
> class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
<option value="">Todas</option> >
{#each PRIORITIES as priority} <option value="">Todas</option>
<option value={priority.value}>{priority.label}</option> {#each categories as category}
{/each} <option value={category.id}>{category.name}</option>
</select> {/each}
</div> </select>
</div>
<div class="flex items-end"> <!-- Asignado a -->
<button <div>
on:click={loadData} <label for="filterAssignedTo" class="block text-sm font-medium text-gray-700 mb-1">Asignado a</label>
class="w-full inline-flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" <select
> id="filterAssignedTo"
Actualizar bind:value={filterAssignedTo}
</button> on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<option value="">Todos</option>
{#each users.filter(u => u.role === 'AGENT' || u.role === 'SUPPORT_MANAGER' || u.role === 'ADMIN') as user}
<option value={user.id}>{user.first_name} {user.last_name}</option>
{/each}
</select>
</div>
<!-- Fecha desde -->
<div>
<label for="filterDateFrom" class="block text-sm font-medium text-gray-700 mb-1">Desde</label>
<input
type="date"
id="filterDateFrom"
bind:value={filterDateFrom}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
<!-- Fecha hasta -->
<div>
<label for="filterDateTo" class="block text-sm font-medium text-gray-700 mb-1">Hasta</label>
<input
type="date"
id="filterDateTo"
bind:value={filterDateTo}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div> </div>
</div> </div>
</div> </div>