From a8e7af87dc6a97a02c221b8f23624c14ca1234b8 Mon Sep 17 00:00:00 2001 From: icamarillo Date: Tue, 10 Feb 2026 13:39:39 -0700 Subject: [PATCH] Descarga de archivos implementada --- backend/app/api/v1/endpoints/tickets.py | 23 +- .../src/lib/components/Header.svelte | 55 +-- frontend-client/src/lib/stores/tickets.ts | 33 ++ frontend-client/src/routes/+page.svelte | 120 +++--- frontend-client/src/routes/login/+page.svelte | 367 ++++++++++-------- .../src/routes/profile/+page.svelte | 18 +- .../src/routes/tickets/[id]/+page.svelte | 282 +++++++++----- .../src/lib/components/Header.svelte | 51 ++- .../src/lib/components/Sidebar.svelte | 89 +++-- frontend-internal/src/lib/utils/api.ts | 75 +++- .../src/routes/tickets/[id]/+page.svelte | 168 ++++++-- 11 files changed, 832 insertions(+), 449 deletions(-) diff --git a/backend/app/api/v1/endpoints/tickets.py b/backend/app/api/v1/endpoints/tickets.py index 887b5f9..48f534f 100644 --- a/backend/app/api/v1/endpoints/tickets.py +++ b/backend/app/api/v1/endpoints/tickets.py @@ -865,10 +865,16 @@ async def download_attachment( current_tenant: Tenant = Depends(get_current_tenant) ): """Descargar un archivo adjunto""" + import logging + logger = logging.getLogger(__name__) + + logger.info(f"Download request - ticket_id: {ticket_id}, attachment_id: {attachment_id}") + try: ticket_uuid = uuid.UUID(ticket_id) attachment_uuid = uuid.UUID(attachment_id) except ValueError: + logger.error(f"Invalid UUID format - ticket_id: {ticket_id}, attachment_id: {attachment_id}") raise HTTPException(status_code=400, detail="ID inválido") # Verificar ticket @@ -878,6 +884,7 @@ async def download_attachment( ticket = result.scalar_one_or_none() if not ticket: + logger.error(f"Ticket not found - ticket_id: {ticket_id}") raise HTTPException(status_code=404, detail="Ticket no encontrado") # Obtener attachment @@ -888,12 +895,26 @@ async def download_attachment( attachment = result.scalar_one_or_none() if not attachment: + logger.error(f"Attachment not found - attachment_id: {attachment_id}") raise HTTPException(status_code=404, detail="Adjunto no encontrado") + logger.info(f"Attachment found - file_path: {attachment.file_path}, original_filename: {attachment.original_filename}") + # Obtener path del archivo - file_path = file_handler.get_file_path(attachment.file_path) + try: + file_path = file_handler.get_file_path(attachment.file_path) + logger.info(f"Absolute file path: {file_path}") + + if not file_path.exists(): + logger.error(f"File does not exist at path: {file_path}") + raise HTTPException(status_code=404, detail="Archivo no encontrado en el sistema") + + except Exception as e: + logger.error(f"Error getting file path: {str(e)}") + raise # Retornar archivo + logger.info(f"Returning file: {attachment.original_filename}") return FileResponse( path=file_path, filename=attachment.original_filename, diff --git a/frontend-client/src/lib/components/Header.svelte b/frontend-client/src/lib/components/Header.svelte index c810bd7..2ab1ab1 100644 --- a/frontend-client/src/lib/components/Header.svelte +++ b/frontend-client/src/lib/components/Header.svelte @@ -2,20 +2,20 @@ import { onMount } from 'svelte'; import { auth } from '$lib/stores/auth.js'; import Icon from './Icon.svelte'; - + export let showLogo = true; export let showNavigation = true; - + let isMenuOpen = false; - + onMount(() => { auth.init(); }); - + function toggleMenu() { isMenuOpen = !isMenuOpen; } - + function handleLogout() { auth.logout(); isMenuOpen = false; @@ -43,10 +43,16 @@ {#if showNavigation && $auth.isAuthenticated} @@ -60,7 +66,7 @@ on:click={toggleMenu} class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-md p-2" tabindex="0" - on:keydown={(e) => e.key === 'Enter' && toggleMenu()} + on:keydown={e => e.key === 'Enter' && toggleMenu()} >
@@ -68,13 +74,16 @@
{#if isMenuOpen} -
+
{$auth.user?.email} @@ -82,7 +91,7 @@ isMenuOpen = false} + on:click={() => (isMenuOpen = false)} > Mi Perfil @@ -91,7 +100,7 @@ class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="button" tabindex="0" - on:keydown={(e) => e.key === 'Enter' && handleLogout()} + on:keydown={e => e.key === 'Enter' && handleLogout()} > Cerrar Sesión @@ -100,10 +109,7 @@ {/if}
{:else} - + Iniciar Sesión {/if} @@ -114,10 +120,16 @@ {#if showNavigation && $auth.isAuthenticated}
@@ -128,8 +140,5 @@ {#if isMenuOpen} -
isMenuOpen = false} - >
-{/if} \ No newline at end of file +
(isMenuOpen = false)} /> +{/if} diff --git a/frontend-client/src/lib/stores/tickets.ts b/frontend-client/src/lib/stores/tickets.ts index 527567f..0ced967 100644 --- a/frontend-client/src/lib/stores/tickets.ts +++ b/frontend-client/src/lib/stores/tickets.ts @@ -328,6 +328,39 @@ function createTicketsStore() { comments: [], attachments: [] })); + }, + + // Download attachment + downloadAttachment: async (ticketId: string, attachmentId: string, filename: string) => { + const authState = get(auth); + + if (!authState.token || !authState.user) { + throw new Error('Not authenticated'); + } + + const response = await fetch(`/api/v1/tickets/${ticketId}/attachments/${attachmentId}/download`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${authState.token}`, + 'X-Tenant-ID': authState.user.tenant_id + } + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Download failed' })); + throw new Error(error.detail || 'Download failed'); + } + + // Crear blob y descargar + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); } }; } diff --git a/frontend-client/src/routes/+page.svelte b/frontend-client/src/routes/+page.svelte index 1e6d7cf..2cc0b23 100644 --- a/frontend-client/src/routes/+page.svelte +++ b/frontend-client/src/routes/+page.svelte @@ -4,14 +4,14 @@ import { tickets } from '$lib/stores/tickets.js'; import { goto } from '$app/navigation'; import Icon from '$lib/components/Icon.svelte'; - + onMount(() => { // Redirect if not authenticated if (!$auth.isAuthenticated) { goto('/login'); return; } - + // Load user's tickets tickets.loadTickets(); }); @@ -26,102 +26,108 @@

- Bienvenido, {$auth.user?.first_name} {$auth.user?.last_name} + Bienvenido, {$auth.user?.first_name} + {$auth.user?.last_name}

- Gestiona tus tickets de soporte de manera eficiente. Crea nuevos tickets, - da seguimiento a los existentes y mantente actualizado con el estado de tus solicitudes. + Gestiona tus tickets de soporte de manera eficiente. Crea nuevos tickets, da seguimiento a + los existentes y mantente actualizado con el estado de tus solicitudes.

- +
- +
-
+

Crear Ticket

-

- Reporta un problema o solicita soporte técnico -

+

Reporta un problema o solicita soporte técnico

- - + +
-
+

Mis Tickets

-

- Consulta el estado de todos tus tickets -

+

Consulta el estado de todos tus tickets

- - + + - +

Tickets Recientes

Últimos tickets que has creado o actualizado

- +
{#if $tickets.isLoading}
-
+

Cargando tickets...

{:else if $tickets.error}
-
+
- +

Error al cargar los tickets

-
{:else if $tickets.tickets.length === 0}
-
- - +
+ +

No tienes tickets creados

- - Crear tu primer ticket - + Crear tu primer ticket
{:else}
@@ -144,17 +150,23 @@
- {ticket.status === 'NEW' ? 'Nuevo' : - ticket.status === 'IN_PROGRESS' ? 'En Progreso' : - ticket.status === 'WAITING_FOR_CLIENT' ? 'Esperando Cliente' : - ticket.status === 'RESOLVED' ? 'Resuelto' : - ticket.status === 'CLOSED' ? 'Cerrado' : 'Reabierto'} + {ticket.status === 'NEW' + ? 'Nuevo' + : ticket.status === 'IN_PROGRESS' + ? 'En Progreso' + : ticket.status === 'WAITING_FOR_CLIENT' + ? 'Esperando Cliente' + : ticket.status === 'RESOLVED' + ? 'Resuelto' + : ticket.status === 'CLOSED' + ? 'Cerrado' + : 'Reabierto'}
{/each} - + {#if $tickets.tickets.length > 5}
@@ -176,4 +188,4 @@ overflow: hidden; line-clamp: 2; /* Propiedad estándar para compatibilidad */ } - \ No newline at end of file + diff --git a/frontend-client/src/routes/login/+page.svelte b/frontend-client/src/routes/login/+page.svelte index 8a2c6a0..f1fcc14 100644 --- a/frontend-client/src/routes/login/+page.svelte +++ b/frontend-client/src/routes/login/+page.svelte @@ -4,7 +4,7 @@ import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import Icon from '$lib/components/Icon.svelte'; - + let email = ''; let password = ''; let totpCode = ''; @@ -12,23 +12,23 @@ let showTwoFactor = false; let errorMessage = ''; let showPassword = false; - + onMount(() => { // Redirect if already authenticated if ($auth.isAuthenticated) { goto('/'); } }); - + async function handleLogin() { if (!email || !password) { errorMessage = 'Por favor completa todos los campos'; return; } - + isLoading = true; errorMessage = ''; - + try { await auth.login({ email, @@ -36,12 +36,12 @@ tenant_slug: 'aduanasoft', // Default tenant for now totp_code: totpCode || undefined }); - + toast.success('¡Bienvenido! Has iniciado sesión correctamente'); goto('/'); } catch (error: any) { console.error('Login error:', error); - + // Check if 2FA is required if (error.message.includes('two-factor') || error.message.includes('2FA')) { showTwoFactor = true; @@ -54,7 +54,7 @@ isLoading = false; } } - + function handleKeyDown(event: KeyboardEvent) { if (event.key === 'Enter') { handleLogin(); @@ -62,182 +62,213 @@ } -
- - +

{$tickets.currentTicket.description}

- + {#if $tickets.currentTicket.resolution}

Resolución:

@@ -253,7 +271,7 @@ {/if}
- + {#if $tickets.attachments.length > 0}
@@ -261,14 +279,18 @@

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

- @@ -278,11 +300,23 @@
{#each $tickets.attachments as attachment} -
+
- - + +
@@ -290,22 +324,26 @@ {attachment.original_filename}

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

- handleDownloadAttachment(attachment)} class="btn-ghost p-2 hover:bg-blue-100 rounded-md transition-colors" - target="_blank" title="Descargar archivo" > - + - +
{/each}
@@ -313,7 +351,7 @@ {/if}
{/if} - +
@@ -327,11 +365,18 @@ {:else}
{#each $tickets.comments as comment} - {console.log('Comment:', comment)} + {console.log('Comment:', comment)}
-
+
- {comment.author_name ? comment.author_name.split(' ').map(n => n[0]).join('') : '??'} + {comment.author_name + ? comment.author_name + .split(' ') + .map(n => n[0]) + .join('') + : '??'}
@@ -356,7 +401,7 @@ {/each}
{/if} - +
@@ -366,8 +411,8 @@ placeholder="Escribe tu comentario o respuesta..." bind:value={newComment} disabled={isSubmittingComment} - > - + /> +
{#if isUploading} -
+
{:else} - + {/if} Adjuntar archivo
- +
- +
@@ -426,35 +476,44 @@
ID del Ticket
-
#{$tickets.currentTicket.id.substring(0, 8)}
+
+ #{$tickets.currentTicket.id.substring(0, 8)} +
- +
Categoría
-
{$tickets.currentTicket.category_name || 'Sin categoría'}
+
+ {$tickets.currentTicket.category_name || 'Sin categoría'} +
- + {#if $tickets.currentTicket.assigned_to_name}
Asignado a
{$tickets.currentTicket.assigned_to_name}
{/if} - +
Creado
{formatDate($tickets.currentTicket.created_at)}
- +
Última actualización
{formatDate($tickets.currentTicket.updated_at)}
- + {#if $tickets.currentTicket.due_date}
Fecha límite
-
+
{formatDate($tickets.currentTicket.due_date)} {#if new Date($tickets.currentTicket.due_date) < new Date()} ¡Vencido! @@ -472,29 +531,50 @@ {#if showCloseDialog}
-
-