250 lines
8.2 KiB
Svelte
250 lines
8.2 KiB
Svelte
<script lang="ts">
|
|
import * as Avatar from '$lib/components/ui/avatar/index.js';
|
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
|
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
|
import BadgeCheckIcon from '@lucide/svelte/icons/badge-check';
|
|
import BellIcon from '@lucide/svelte/icons/bell';
|
|
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
|
import CreditCardIcon from '@lucide/svelte/icons/credit-card';
|
|
import LogOutIcon from '@lucide/svelte/icons/log-out';
|
|
import LanguagesIcon from '@lucide/svelte/icons/languages';
|
|
import MoonIcon from '@lucide/svelte/icons/moon';
|
|
import SunIcon from '@lucide/svelte/icons/sun';
|
|
import ArrowLeftRightIcon from '@lucide/svelte/icons/arrow-left-right';
|
|
import BuildingIcon from '@lucide/svelte/icons/building';
|
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
import { logout } from '$lib/auth';
|
|
import { cookieName } from '$lib/paraglide/runtime';
|
|
import { page } from '$app/state';
|
|
import { goto } from '$app/navigation';
|
|
import { browser } from '$app/environment';
|
|
import { getBackendAssetUrl } from '$lib/utils';
|
|
import AppVersion from '$lib/components/app-version.svelte';
|
|
|
|
let { user, tenants = [] }: {
|
|
user: { name: string; email: string; avatar: string };
|
|
tenants: { id: number; name: string; slug: string }[];
|
|
} = $props();
|
|
const sidebar = useSidebar();
|
|
|
|
// Tenant activo (viene en el JWT como atributo tenant_slug)
|
|
let currentTenantSlug = $derived((page.data.user as any)?.tenant_slug ?? '');
|
|
|
|
// Estado de cambio de tenant
|
|
let switchingTenant = $state(false);
|
|
|
|
// URL completa del avatar
|
|
let avatarUrl = $derived(getBackendAssetUrl(user.avatar) || '/avatars/default.jpg');
|
|
|
|
// Iniciales del usuario (2 primeras letras)
|
|
let initials = $derived(user.name.slice(0, 2).toUpperCase());
|
|
|
|
// Estado reactivo del idioma actual
|
|
let currentLocale = $derived(page.data.locale || 'en');
|
|
|
|
// Estado reactivo del tema actual
|
|
let isDarkMode = $state(false);
|
|
|
|
// Inicializar el estado del tema al montar el componente
|
|
$effect(() => {
|
|
if (browser) {
|
|
// Cargar la preferencia guardada o usar el valor actual del HTML
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme) {
|
|
isDarkMode = savedTheme === 'dark';
|
|
if (savedTheme === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
} else {
|
|
// Si no hay preferencia guardada, usar el valor actual
|
|
isDarkMode = document.documentElement.classList.contains('dark');
|
|
// Guardar el estado actual
|
|
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
|
}
|
|
}
|
|
});
|
|
|
|
async function handleLogout() {
|
|
await logout();
|
|
}
|
|
|
|
async function switchTenant(slug: string) {
|
|
if (slug === currentTenantSlug || switchingTenant) return;
|
|
switchingTenant = true;
|
|
try {
|
|
const res = await fetch('/api-sveltekit/auth/switch-tenant', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tenant_slug: slug })
|
|
});
|
|
if (res.ok) {
|
|
// Recargar el dashboard con los nuevos tokens
|
|
window.location.href = '/dashboard';
|
|
} else {
|
|
console.error('Error al cambiar de organización');
|
|
}
|
|
} finally {
|
|
switchingTenant = false;
|
|
}
|
|
}
|
|
|
|
function navigateToAccount() {
|
|
goto('/dashboard/account');
|
|
}
|
|
|
|
function toggleLanguage() {
|
|
if (!browser) return;
|
|
|
|
// Leer la cookie actual para obtener el idioma real
|
|
const cookies = document.cookie.split(';').map((c) => c.trim());
|
|
const localeCookie = cookies.find((c) => c.startsWith(`${cookieName}=`));
|
|
const current = localeCookie ? localeCookie.split('=')[1] : 'en';
|
|
|
|
// Alternar el idioma
|
|
const newLocale = current === 'en' ? 'es' : 'en';
|
|
|
|
// Establecer la cookie del idioma
|
|
document.cookie = `${cookieName}=${newLocale}; path=/; max-age=34560000; SameSite=Lax`;
|
|
|
|
// Recargar la página para que el servidor procese el nuevo idioma
|
|
window.location.reload();
|
|
}
|
|
|
|
function toggleTheme() {
|
|
if (!browser) return;
|
|
|
|
const html = document.documentElement;
|
|
const newTheme = html.classList.contains('dark') ? 'light' : 'dark';
|
|
|
|
if (newTheme === 'dark') {
|
|
html.classList.add('dark');
|
|
} else {
|
|
html.classList.remove('dark');
|
|
}
|
|
|
|
// Guardar la preferencia en localStorage
|
|
localStorage.setItem('theme', newTheme);
|
|
isDarkMode = newTheme === 'dark';
|
|
}
|
|
</script>
|
|
|
|
<Sidebar.Menu>
|
|
<Sidebar.MenuItem>
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
{#snippet child({ props })}
|
|
<Sidebar.MenuButton
|
|
size="lg"
|
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
{...props}
|
|
>
|
|
<Avatar.Root class="size-8 rounded-lg">
|
|
<Avatar.Image src={avatarUrl} alt={user.name} />
|
|
<Avatar.Fallback class="rounded-lg">{initials}</Avatar.Fallback>
|
|
</Avatar.Root>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-medium">{user.name}</span>
|
|
<span class="truncate text-xs">{user.email}</span>
|
|
</div>
|
|
<ChevronsUpDownIcon class="ml-auto size-4" />
|
|
</Sidebar.MenuButton>
|
|
{/snippet}
|
|
</DropdownMenu.Trigger>
|
|
<DropdownMenu.Content
|
|
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
|
|
side={sidebar.isMobile ? 'bottom' : 'right'}
|
|
align="end"
|
|
sideOffset={4}
|
|
>
|
|
<DropdownMenu.Label class="p-0 font-normal">
|
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
<Avatar.Root class="size-8 rounded-lg">
|
|
<Avatar.Image src={avatarUrl} alt={user.name} />
|
|
<Avatar.Fallback class="rounded-lg">{initials}</Avatar.Fallback>
|
|
</Avatar.Root>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-medium">{user.name}</span>
|
|
<span class="truncate text-xs">{user.email}</span>
|
|
</div>
|
|
</div>
|
|
</DropdownMenu.Label>
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.Group>
|
|
<DropdownMenu.Item onclick={navigateToAccount}>
|
|
<BadgeCheckIcon />
|
|
Account
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item>
|
|
<CreditCardIcon />
|
|
Billing
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item>
|
|
<BellIcon />
|
|
Notifications
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Group>
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.Item onclick={toggleLanguage}>
|
|
<LanguagesIcon />
|
|
Language: {currentLocale.toUpperCase()}
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item onclick={toggleTheme}>
|
|
{#if isDarkMode}
|
|
<SunIcon />
|
|
Light Mode
|
|
{:else}
|
|
<MoonIcon />
|
|
Dark Mode
|
|
{/if}
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Separator />
|
|
{#if tenants.length > 1}
|
|
<DropdownMenu.Sub>
|
|
<DropdownMenu.SubTrigger class="gap-2">
|
|
{#if switchingTenant}
|
|
<svg class="animate-spin size-4 shrink-0" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
|
</svg>
|
|
{:else}
|
|
<ArrowLeftRightIcon class="size-4" />
|
|
{/if}
|
|
Cambiar organización
|
|
</DropdownMenu.SubTrigger>
|
|
<DropdownMenu.SubContent class="min-w-44">
|
|
<DropdownMenu.Label class="text-xs text-muted-foreground pb-1">
|
|
Mis organizaciones
|
|
</DropdownMenu.Label>
|
|
{#each tenants as tenant (tenant.id)}
|
|
<DropdownMenu.Item
|
|
class="gap-2 cursor-pointer"
|
|
disabled={switchingTenant}
|
|
onclick={() => switchTenant(tenant.slug)}
|
|
>
|
|
<BuildingIcon class="size-4 shrink-0 text-muted-foreground" />
|
|
<span class="flex-1 truncate">{tenant.name}</span>
|
|
{#if tenant.slug === currentTenantSlug}
|
|
<CheckIcon class="size-3.5 shrink-0 text-blue-600" />
|
|
{/if}
|
|
</DropdownMenu.Item>
|
|
{/each}
|
|
</DropdownMenu.SubContent>
|
|
</DropdownMenu.Sub>
|
|
{/if}
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.Item onclick={handleLogout}>
|
|
<LogOutIcon />
|
|
Log out
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Separator />
|
|
<div class="px-2 py-2">
|
|
<AppVersion />
|
|
</div>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
</Sidebar.MenuItem>
|
|
</Sidebar.Menu>
|