245 lines
8.5 KiB
Svelte
245 lines
8.5 KiB
Svelte
<script lang="ts">
|
|
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 ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
|
import BuildingIcon from '@lucide/svelte/icons/building';
|
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
import { companyStore } from '$lib/stores/company.svelte';
|
|
import { getBackendAssetUrl } from '$lib/utils';
|
|
import { invalidateAll } from '$app/navigation';
|
|
|
|
interface Tenant {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
}
|
|
|
|
let { userTenants = [] }: { userTenants: Tenant[] } = $props();
|
|
|
|
const sidebar = useSidebar();
|
|
|
|
let switchingTenant = $state(false);
|
|
|
|
// Derivar la URL del logo usando el endpoint específico
|
|
let activeCompanyLogoUrl = $derived(
|
|
companyStore.activeCompany?.logo
|
|
? getBackendAssetUrl(
|
|
`v1/a76/company/${companyStore.activeCompany.id}/logo/image?t=${Date.now()}`
|
|
)
|
|
: null
|
|
);
|
|
|
|
// Iniciales de la compañía activa (2 primeras letras)
|
|
let activeCompanyInitials = $derived(
|
|
companyStore.activeCompany?.name?.slice(0, 2).toUpperCase() || 'CO'
|
|
);
|
|
|
|
function readCookie(name: string): string | null {
|
|
if (typeof document === 'undefined') return null;
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop()?.split(';').shift() ?? null;
|
|
return null;
|
|
}
|
|
|
|
let activeTenantPubId = $derived.by<number | null>(() => {
|
|
const fromCookie = readCookie('sso_tenant_pub');
|
|
if (fromCookie && !Number.isNaN(Number(fromCookie))) return Number(fromCookie);
|
|
return null;
|
|
});
|
|
|
|
async function switchTenant(tenant: Tenant) {
|
|
if (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_id: tenant.id }),
|
|
credentials: 'include',
|
|
});
|
|
if (res.ok) {
|
|
companyStore.clear();
|
|
await invalidateAll();
|
|
} else {
|
|
const err = await res.json().catch(() => ({}));
|
|
console.error('[team-switcher] switch-tenant error:', err);
|
|
}
|
|
} catch (e) {
|
|
console.error('[team-switcher] fetch error:', e);
|
|
} finally {
|
|
switchingTenant = false;
|
|
}
|
|
}
|
|
|
|
function normalizeIdentity(value: string | undefined | null): string {
|
|
return (value ?? '').trim().toLowerCase();
|
|
}
|
|
|
|
let tenantIdentitySet = $derived.by(() => {
|
|
const set = new Set<string>();
|
|
for (const tenant of userTenants) {
|
|
set.add(normalizeIdentity(tenant.name));
|
|
set.add(normalizeIdentity(tenant.slug));
|
|
}
|
|
set.delete('');
|
|
return set;
|
|
});
|
|
|
|
// Excluir del listado de companias cualquier registro que realmente represente al tenant.
|
|
let myCompanies = $derived(
|
|
companyStore.companies.filter((company) => !tenantIdentitySet.has(normalizeIdentity(company.name)))
|
|
);
|
|
|
|
$effect(() => {
|
|
const active = companyStore.activeCompany;
|
|
if (!active) return;
|
|
if (!tenantIdentitySet.has(normalizeIdentity(active.name))) return;
|
|
if (myCompanies.length === 0) return;
|
|
void companyStore.setActiveCompany(myCompanies[0], true);
|
|
});
|
|
</script>
|
|
|
|
<Sidebar.Menu>
|
|
<Sidebar.MenuItem>
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
{#snippet child({ props })}
|
|
<Sidebar.MenuButton
|
|
{...props}
|
|
size="lg"
|
|
class="relative overflow-hidden group-has-data-[state=collapsed]/sidebar-wrapper:aspect-square group-has-data-[state=collapsed]/sidebar-wrapper:h-10 group-has-data-[state=collapsed]/sidebar-wrapper:w-10 group-has-data-[state=collapsed]/sidebar-wrapper:justify-center group-has-data-[state=collapsed]/sidebar-wrapper:gap-0 group-has-data-[state=collapsed]/sidebar-wrapper:rounded-lg data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
>
|
|
<div
|
|
class="flex aspect-square size-8 items-center justify-center overflow-hidden rounded-lg bg-sidebar-primary text-sidebar-primary-foreground group-has-data-[state=collapsed]/sidebar-wrapper:hidden"
|
|
>
|
|
{#if activeCompanyLogoUrl}
|
|
<img
|
|
src={activeCompanyLogoUrl}
|
|
alt={companyStore.activeCompany?.name || 'Company'}
|
|
class="size-full object-cover"
|
|
onerror={(e) => {
|
|
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
{:else}
|
|
<span class="text-sm font-semibold text-white">{activeCompanyInitials}</span>
|
|
{/if}
|
|
</div>
|
|
<div
|
|
class="grid min-w-0 flex-1 text-left text-sm leading-tight group-has-data-[state=collapsed]/sidebar-wrapper:hidden"
|
|
>
|
|
<span class="truncate font-medium">
|
|
{companyStore.activeCompany?.name || 'Seleccionar compañía'}
|
|
</span>
|
|
{#if companyStore.activeCompany?.rfc}
|
|
<span class="truncate text-xs text-muted-foreground">
|
|
{companyStore.activeCompany.rfc}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
<ChevronsUpDownIcon
|
|
class="ml-auto size-4 group-has-data-[state=collapsed]/sidebar-wrapper:hidden"
|
|
/>
|
|
|
|
<!-- Isotipo compacto visible solo en modo colapsado -->
|
|
<div
|
|
class="relative z-10 hidden size-8 items-center justify-center overflow-hidden rounded-md bg-sidebar-primary text-sm font-semibold text-sidebar-foreground shadow-sm ring-1 ring-sidebar-border/40 group-has-data-[state=collapsed]/sidebar-wrapper:flex"
|
|
>
|
|
{#if activeCompanyLogoUrl}
|
|
<img
|
|
src={activeCompanyLogoUrl}
|
|
alt={companyStore.activeCompany?.name || 'Company'}
|
|
class="size-full object-cover"
|
|
onerror={(e) => {
|
|
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
{:else}
|
|
{activeCompanyInitials}
|
|
{/if}
|
|
</div>
|
|
</Sidebar.MenuButton>
|
|
{/snippet}
|
|
</DropdownMenu.Trigger>
|
|
<DropdownMenu.Content
|
|
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
|
|
align="start"
|
|
side={sidebar.isMobile ? 'bottom' : 'right'}
|
|
sideOffset={4}
|
|
>
|
|
<DropdownMenu.Label class="text-xs text-muted-foreground">Tenant</DropdownMenu.Label>
|
|
{#if userTenants.length === 0}
|
|
<DropdownMenu.Item disabled class="gap-2 p-2">
|
|
<span class="text-muted-foreground">Sin tenant asignado</span>
|
|
</DropdownMenu.Item>
|
|
{:else}
|
|
{#each userTenants as tenant (tenant.id)}
|
|
<DropdownMenu.Item
|
|
onSelect={() => switchTenant(tenant)}
|
|
class="cursor-pointer gap-2 p-2"
|
|
disabled={switchingTenant}
|
|
>
|
|
<div class="flex size-6 items-center justify-center rounded-md border bg-muted">
|
|
<BuildingIcon class="size-3.5" />
|
|
</div>
|
|
<span class="truncate font-medium">{tenant.name}</span>
|
|
{#if activeTenantPubId === tenant.id}
|
|
<CheckIcon class="ml-auto size-4 text-primary" />
|
|
{/if}
|
|
</DropdownMenu.Item>
|
|
{/each}
|
|
{/if}
|
|
<DropdownMenu.Separator />
|
|
|
|
<DropdownMenu.Label class="text-xs text-muted-foreground">Mis compañías</DropdownMenu.Label>
|
|
|
|
{#if companyStore.loading}
|
|
<DropdownMenu.Item disabled class="gap-2 p-2">
|
|
<span class="text-muted-foreground">Cargando...</span>
|
|
</DropdownMenu.Item>
|
|
{:else if myCompanies.length === 0}
|
|
<DropdownMenu.Item disabled class="gap-2 p-2">
|
|
<span class="text-muted-foreground">No tienes compañías disponibles</span>
|
|
</DropdownMenu.Item>
|
|
{:else}
|
|
{#each myCompanies as company, index (company.id)}
|
|
<DropdownMenu.Item
|
|
onSelect={() => companyStore.setActiveCompany(company)}
|
|
class="cursor-pointer gap-2 p-2"
|
|
>
|
|
<div
|
|
class="flex size-6 items-center justify-center overflow-hidden rounded-md border"
|
|
>
|
|
{#if company.logo}
|
|
<img
|
|
src={getBackendAssetUrl(`v1/a76/company/${company.id}/logo/image`)}
|
|
alt={company.name}
|
|
class="size-full rounded object-cover"
|
|
/>
|
|
{:else}
|
|
<span class="text-xs font-semibold">{company.name.slice(0, 2).toUpperCase()}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
<span class="truncate font-medium">{company.name}</span>
|
|
{#if company.rfc}
|
|
<span class="truncate text-xs text-muted-foreground">{company.rfc}</span>
|
|
{/if}
|
|
</div>
|
|
{#if companyStore.activeCompany?.id === company.id}
|
|
<CheckIcon class="ml-auto size-4 text-primary" />
|
|
{/if}
|
|
{#if index < 9}
|
|
<DropdownMenu.Shortcut>⌘{index + 1}</DropdownMenu.Shortcut>
|
|
{/if}
|
|
</DropdownMenu.Item>
|
|
{/each}
|
|
{/if}
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
</Sidebar.MenuItem>
|
|
</Sidebar.Menu>
|