Descarga de archivos implementada

This commit is contained in:
2026-02-10 13:39:39 -07:00
parent e766e5b747
commit a8e7af87dc
11 changed files with 832 additions and 449 deletions

View File

@@ -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 @@
<!-- Navigation -->
{#if showNavigation && $auth.isAuthenticated}
<nav class="hidden md:flex space-x-8">
<a href="/tickets" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
<a
href="/tickets"
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
>
Mis Tickets
</a>
<a href="/tickets/new" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
<a
href="/tickets/new"
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
>
Crear Ticket
</a>
</nav>
@@ -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()}
>
<div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<span class="text-primary-600 text-sm font-medium">
@@ -68,13 +74,16 @@
</span>
</div>
<span class="hidden sm:block text-sm">
{$auth.user?.first_name} {$auth.user?.last_name}
{$auth.user?.first_name}
{$auth.user?.last_name}
</span>
<Icon name="chevronDown" size="w-4 h-4" />
</button>
{#if isMenuOpen}
<div class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50">
<div
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50"
>
<div class="py-1">
<div class="px-4 py-2 text-xs text-gray-500 border-b border-gray-200">
{$auth.user?.email}
@@ -82,7 +91,7 @@
<a
href="/profile"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
on:click={() => isMenuOpen = false}
on:click={() => (isMenuOpen = false)}
>
Mi Perfil
</a>
@@ -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
</button>
@@ -100,10 +109,7 @@
{/if}
</div>
{:else}
<a
href="/login"
class="text-gray-700 hover:text-primary-600 text-sm font-medium"
>
<a href="/login" class="text-gray-700 hover:text-primary-600 text-sm font-medium">
Iniciar Sesión
</a>
{/if}
@@ -114,10 +120,16 @@
{#if showNavigation && $auth.isAuthenticated}
<div class="md:hidden border-t border-gray-200 py-2">
<nav class="flex space-x-4">
<a href="/tickets" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
<a
href="/tickets"
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
>
Mis Tickets
</a>
<a href="/tickets/new" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
<a
href="/tickets/new"
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
>
Crear Ticket
</a>
</nav>
@@ -128,8 +140,5 @@
<!-- Backdrop for mobile menu -->
{#if isMenuOpen}
<div
class="fixed inset-0 z-40 md:hidden"
on:click={() => isMenuOpen = false}
></div>
{/if}
<div class="fixed inset-0 z-40 md:hidden" on:click={() => (isMenuOpen = false)} />
{/if}

View File

@@ -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);
}
};
}