hotfix: Advanced filtering system v1.4.1.1
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user