Descarga de archivos implementada
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { auth } from '$lib/stores/auth.js';
|
||||
|
||||
|
||||
export let toggleSidebar: () => void;
|
||||
|
||||
|
||||
function handleLogout() {
|
||||
auth.logout();
|
||||
}
|
||||
|
||||
|
||||
let isMenuOpen = false;
|
||||
|
||||
|
||||
function toggleMenu() {
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
@@ -23,15 +23,20 @@
|
||||
class="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500 lg:hidden"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="hidden lg:block">
|
||||
<h1 class="text-lg font-semibold text-gray-900">ServiceManager Internal</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- User menu -->
|
||||
@@ -46,15 +51,23 @@
|
||||
</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>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</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}
|
||||
@@ -62,7 +75,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>
|
||||
@@ -82,11 +95,11 @@
|
||||
|
||||
<!-- Backdrop for mobile menu -->
|
||||
{#if isMenuOpen}
|
||||
<div
|
||||
class="fixed inset-0 z-40 lg:hidden"
|
||||
role="dialog"
|
||||
tabindex="0"
|
||||
on:click={() => isMenuOpen = false}
|
||||
on:keydown={(e) => e.key === 'Escape' && (isMenuOpen = false)}
|
||||
></div>
|
||||
{/if}
|
||||
<div
|
||||
class="fixed inset-0 z-40 lg:hidden"
|
||||
role="dialog"
|
||||
tabindex="0"
|
||||
on:click={() => (isMenuOpen = false)}
|
||||
on:keydown={e => e.key === 'Escape' && (isMenuOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { auth } from '$lib/stores/auth.js';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { auth } from '$lib/stores/auth.js';
|
||||
|
||||
export let open = false;
|
||||
|
||||
|
||||
// Navigation items based on user role
|
||||
$: navigation = getNavigationForRole($auth.user?.role);
|
||||
|
||||
|
||||
function getNavigationForRole(role: string | undefined) {
|
||||
const baseNavigation = [
|
||||
{
|
||||
@@ -20,7 +20,7 @@
|
||||
icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
if (role === 'ADMIN' || role === 'SUPPORT_MANAGER') {
|
||||
baseNavigation.push(
|
||||
{
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (role === 'ADMIN') {
|
||||
baseNavigation.push(
|
||||
{
|
||||
@@ -65,32 +65,51 @@
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return baseNavigation;
|
||||
}
|
||||
|
||||
|
||||
function isCurrentPage(href: string) {
|
||||
return $page.url.pathname === href ||
|
||||
($page.url.pathname.startsWith(href) && href !== '/');
|
||||
return $page.url.pathname === href || ($page.url.pathname.startsWith(href) && href !== '/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Mobile sidebar backdrop -->
|
||||
{#if open}
|
||||
<div class="fixed inset-0 z-40 lg:hidden">
|
||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-75" role="dialog" tabindex="0" on:click={() => open = false} on:keydown={(e) => e.key === 'Escape' && (open = false)}></div>
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-75"
|
||||
role="dialog"
|
||||
tabindex="0"
|
||||
on:click={() => (open = false)}
|
||||
on:keydown={e => e.key === 'Escape' && (open = false)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform {open ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0">
|
||||
<div
|
||||
class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform {open
|
||||
? 'translate-x-0'
|
||||
: '-translate-x-full'} transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center justify-between h-16 px-6 bg-primary-600">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<svg
|
||||
class="w-5 h-5 text-primary-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-white">
|
||||
@@ -98,28 +117,33 @@
|
||||
<p class="text-xs text-primary-100">Panel Interno</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
on:click={() => open = false}
|
||||
on:click={() => (open = false)}
|
||||
class="p-2 rounded-md text-primary-100 hover:text-white hover:bg-primary-500 lg:hidden"
|
||||
>
|
||||
<svg class="w-5 h-5" 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" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 px-4 py-6 space-y-2">
|
||||
{#each navigation as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors {
|
||||
isCurrentPage(item.href)
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||
}"
|
||||
on:click={() => open = false}
|
||||
class="flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors {isCurrentPage(
|
||||
item.href
|
||||
)
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'}"
|
||||
on:click={() => (open = false)}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={item.icon} />
|
||||
@@ -128,7 +152,7 @@
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- User info -->
|
||||
<div class="px-4 py-4 border-t border-gray-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
@@ -139,15 +163,20 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">
|
||||
{$auth.user?.first_name} {$auth.user?.last_name}
|
||||
{$auth.user?.first_name}
|
||||
{$auth.user?.last_name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 truncate">
|
||||
{$auth.user?.role === 'ADMIN' ? 'Administrador' :
|
||||
$auth.user?.role === 'SUPPORT_MANAGER' ? 'Gerente de Soporte' :
|
||||
$auth.user?.role === 'AGENT' ? 'Agente' : 'Auditor'}
|
||||
{$auth.user?.role === 'ADMIN'
|
||||
? 'Administrador'
|
||||
: $auth.user?.role === 'SUPPORT_MANAGER'
|
||||
? 'Gerente de Soporte'
|
||||
: $auth.user?.role === 'AGENT'
|
||||
? 'Agente'
|
||||
: 'Auditor'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,14 @@ interface RequestOptions extends RequestInit {
|
||||
|
||||
async function request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { params, ...init } = options;
|
||||
|
||||
|
||||
let url = `${API_BASE}${endpoint}`;
|
||||
if (params) {
|
||||
// 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()}`;
|
||||
@@ -42,9 +42,9 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
||||
if (response.status === 401) {
|
||||
// Token expired or invalid
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('internal_auth_token');
|
||||
localStorage.removeItem('internal_auth_user');
|
||||
window.location.href = '/login';
|
||||
localStorage.removeItem('internal_auth_token');
|
||||
localStorage.removeItem('internal_auth_user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
@@ -56,25 +56,68 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
||||
|
||||
// Handle empty responses (like 204 No Content)
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function downloadFile(endpoint: string, filename: string): Promise<void> {
|
||||
const authState = get(auth);
|
||||
const token = authState.token || (typeof window !== 'undefined' ? localStorage.getItem('internal_auth_token') : null);
|
||||
|
||||
const headers = new Headers();
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('internal_auth_token');
|
||||
localStorage.removeItem('internal_auth_user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Download error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(endpoint: string, params?: Record<string, string>) =>
|
||||
get: <T>(endpoint: string, params?: Record<string, string>) =>
|
||||
request<T>(endpoint, { method: 'GET', params }),
|
||||
|
||||
post: <T>(endpoint: string, body: any) =>
|
||||
|
||||
post: <T>(endpoint: string, body: any) =>
|
||||
request<T>(endpoint, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
put: <T>(endpoint: string, body: any) =>
|
||||
|
||||
put: <T>(endpoint: string, body: any) =>
|
||||
request<T>(endpoint, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
|
||||
patch: <T>(endpoint: string, body: any) =>
|
||||
|
||||
patch: <T>(endpoint: string, body: any) =>
|
||||
request<T>(endpoint, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
|
||||
delete: <T>(endpoint: string) =>
|
||||
request<T>(endpoint, { method: 'DELETE' })
|
||||
|
||||
delete: <T>(endpoint: string) =>
|
||||
request<T>(endpoint, { method: 'DELETE' }),
|
||||
|
||||
downloadFile: (endpoint: string, filename: string) =>
|
||||
downloadFile(endpoint, filename)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user