diff --git a/backend/app/api/v1/endpoints/tickets.py b/backend/app/api/v1/endpoints/tickets.py index f380ab5..1ed5df4 100644 --- a/backend/app/api/v1/endpoints/tickets.py +++ b/backend/app/api/v1/endpoints/tickets.py @@ -210,6 +210,148 @@ async def get_tickets( ] +@router.get("/admin/all", response_model=List[dict]) +async def get_all_tickets_admin( + skip: int = 0, + limit: int = 100, + status_filter: Optional[str] = None, + priority_filter: Optional[str] = None, + tenant_id_filter: Optional[str] = None, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Obtener todos los tickets de todos los tenants (solo para administradores) + Incluye información del tenant y usuario que creó el ticket + """ + # Verificar que el usuario sea administrador + if current_user.role not in ["ADMIN", "SUPPORT_MANAGER"]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="No tienes permisos para acceder a esta función" + ) + + # Query base con joins para obtener información del tenant y usuario creador + query = select(Ticket, Tenant, User).join( + Tenant, Ticket.tenant_id == Tenant.id + ).join( + User, Ticket.created_by == User.id + ) + + # Aplicar filtros + if status_filter: + try: + status_enum = TicketStatus[status_filter.upper()] + query = query.where(Ticket.status == status_enum) + except KeyError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid status: {status_filter}" + ) + + if priority_filter: + try: + priority_enum = TicketPriority[priority_filter.upper()] + query = query.where(Ticket.priority == priority_enum) + except KeyError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid priority: {priority_filter}" + ) + + if tenant_id_filter: + try: + tenant_uuid = uuid.UUID(tenant_id_filter) + query = query.where(Ticket.tenant_id == tenant_uuid) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid tenant ID format" + ) + + query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit) + + result = await db.execute(query) + rows = result.all() + + return [ + { + "id": str(ticket.id), + "ticket_number": ticket.ticket_number, + "subject": ticket.subject, + "title": ticket.subject, + "description": ticket.description, + "status": ticket.status.value, + "priority": ticket.priority.value, + "category_id": str(ticket.category_id) if ticket.category_id else None, + "affected_system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None, + "created_by": str(ticket.created_by), + "assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None, + "created_at": ticket.created_at, + "updated_at": ticket.updated_at, + # Información del tenant/cliente + "tenant": { + "id": str(tenant.id), + "name": tenant.name, + "slug": tenant.slug, + "contact_email": tenant.contact_email + }, + # Información del usuario creador + "created_by_user": { + "id": str(user.id), + "email": user.email, + "first_name": user.first_name, + "last_name": user.last_name, + "role": user.role.value if hasattr(user.role, 'value') else str(user.role) + } + } + for ticket, tenant, user in rows + ] + + +@router.get("/{ticket_id}", response_model=TicketResponse) +async def get_ticket( + ticket_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + + if status_filter: + try: + status_enum = TicketStatus[status_filter.upper()] + query = query.where(Ticket.status == status_enum) + except KeyError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid status: {status_filter}" + ) + + query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit) + + result = await db.execute(query) + tickets = result.scalars().all() + + # ✅ CORREGIDO: Usar affected_system_id + return [ + { + "id": str(t.id), + "ticket_number": t.ticket_number, + "subject": t.subject, + "title": t.subject, + "description": t.description, + "status": t.status.value, + "priority": t.priority.value, + "category_id": str(t.category_id) if t.category_id else None, + "affected_system_id": str(t.affected_system_id) if t.affected_system_id else None, # ✅ CORREGIDO + "created_by": str(t.created_by), + "assigned_to": str(t.assigned_to) if t.assigned_to else None, + "created_at": t.created_at, + "updated_at": t.updated_at + } + for t in tickets + ] + + @router.get("/{ticket_id}", response_model=TicketResponse) async def get_ticket( ticket_id: str, diff --git a/frontend-client/src/routes/tickets/[id]/+page.svelte b/frontend-client/src/routes/tickets/[id]/+page.svelte index 02114cd..18de429 100644 --- a/frontend-client/src/routes/tickets/[id]/+page.svelte +++ b/frontend-client/src/routes/tickets/[id]/+page.svelte @@ -14,7 +14,8 @@ let closeResolution = ''; let fileInput: HTMLInputElement; let isUploading = false; - let pollingInterval: any = null; + let pollingInterval: any = null; + let showAttachments = true; // Variable para controlar la visibilidad de attachments async function loadComments() { try { @@ -257,42 +258,59 @@ {#if $tickets.attachments.length > 0}
-

Archivos Adjuntos

-
-
-
- {#each $tickets.attachments as attachment} -
-
-
- - - -
-
-

- {attachment.original_filename} -

-

- {Math.round(attachment.size_bytes / 1024)} KB • - Subido por {attachment.uploaded_by_name} • - {formatDate(attachment.uploaded_at)} -

-
-
- - - - - -
- {/each} +
+

+ Archivos Adjuntos + + {$tickets.attachments.length} archivo{$tickets.attachments.length !== 1 ? 's' : ''} + +

+
+ {#if showAttachments} +
+
+ {#each $tickets.attachments as attachment} +
+
+
+ + + +
+
+

+ {attachment.original_filename} +

+

+ {Math.round(attachment.size_bytes / 1024)} KB • + Subido por {attachment.uploaded_by_name} • + {formatDate(attachment.uploaded_at)} +

+
+
+ + + + + +
+ {/each} +
+
+ {/if}
{/if} diff --git a/frontend-internal/src/lib/utils/api.ts b/frontend-internal/src/lib/utils/api.ts index a5321e6..98314b2 100644 --- a/frontend-internal/src/lib/utils/api.ts +++ b/frontend-internal/src/lib/utils/api.ts @@ -12,8 +12,15 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi let url = `${API_BASE}${endpoint}`; if (params) { - const searchParams = new URLSearchParams(params); - url += `?${searchParams.toString()}`; + // Filtrar parámetros undefined y null + const filteredParams = Object.entries(params) + .filter(([key, value]) => value !== undefined && value !== null && value !== '') + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + + if (Object.keys(filteredParams).length > 0) { + const searchParams = new URLSearchParams(filteredParams); + url += `?${searchParams.toString()}`; + } } const authState = get(auth); diff --git a/frontend-internal/src/routes/tickets/+page.svelte b/frontend-internal/src/routes/tickets/+page.svelte index af116ca..55a38a8 100644 --- a/frontend-internal/src/routes/tickets/+page.svelte +++ b/frontend-internal/src/routes/tickets/+page.svelte @@ -9,6 +9,7 @@ let categories = []; let systems = []; let users = []; + let tenants = []; // Nueva lista de tenants let isLoading = false; let showModal = false; let showEditModal = false; @@ -18,6 +19,7 @@ // Filtros let filterStatus = ''; let filterPriority = ''; + let filterTenant = ''; // Nuevo filtro por cliente/tenant // Form para editar let editFormData = { @@ -50,25 +52,29 @@ { value: 'URGENT', label: 'Urgente', color: 'red' } ]; - // Ajustar la función loadData para asegurar que los filtros se envíen correctamente + // Ajustar la función loadData para usar el endpoint administrativo con filtros async function loadData() { isLoading = true; try { - const [ticketsData, categoriesData, systemsData, usersData] = await Promise.all([ - api.get('/tickets/', { - params: { - status: filterStatus || undefined, - priority: filterPriority || undefined - } - }), + // Preparar parámetros filtrando valores vacíos + const ticketParams = {}; + if (filterStatus) ticketParams.status_filter = filterStatus; + if (filterPriority) ticketParams.priority_filter = filterPriority; + if (filterTenant) ticketParams.tenant_id_filter = filterTenant; + + const [ticketsData, categoriesData, systemsData, usersData, tenantsData] = await Promise.all([ + // Usar el nuevo endpoint administrativo + api.get('/tickets/admin/all', ticketParams), api.get('/categories/'), api.get('/systems/'), - api.get('/users/') + api.get('/users/'), + api.get('/tenants/') // Cargar lista de tenants ]); tickets = ticketsData; categories = categoriesData; systems = systemsData; users = usersData; + tenants = tenantsData; } catch (e) { toast.error('Error cargando datos: ' + (e.message || 'Error desconocido')); } finally { @@ -237,7 +243,22 @@
-
+
+
+ + +
+