feature/app-selector
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
|
||||
import {
|
||||
extractAllowedSystemsFromToken,
|
||||
fetchAllowedSystems,
|
||||
isValidSystem,
|
||||
mergeTokenClaims,
|
||||
setActiveSystemCookie
|
||||
} from '$lib/server/system-gate';
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request }) => {
|
||||
try {
|
||||
const { system } = await request.json();
|
||||
|
||||
if (!isValidSystem(system)) {
|
||||
return json({ error: 'Sistema inválido' }, { status: 400 });
|
||||
}
|
||||
|
||||
const accessToken = getAccessTokenFromCookies(cookies);
|
||||
if (!accessToken) {
|
||||
return json({ error: 'No autenticado' }, { status: 401 });
|
||||
}
|
||||
|
||||
const tokenClaims = mergeTokenClaims(null, accessToken);
|
||||
let allowedSystems = extractAllowedSystemsFromToken(tokenClaims);
|
||||
|
||||
if (allowedSystems.length === 0) {
|
||||
const rawCompanyId = cookies.get('active_company_id');
|
||||
const companyId = rawCompanyId ? Number.parseInt(rawCompanyId, 10) : NaN;
|
||||
if (Number.isFinite(companyId)) {
|
||||
allowedSystems = await fetchAllowedSystems(cookies, fetch, companyId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowedSystems.includes(system)) {
|
||||
return json({ error: 'No tienes acceso a ese sistema' }, { status: 403 });
|
||||
}
|
||||
|
||||
setActiveSystemCookie(cookies, system);
|
||||
|
||||
return json({ success: true, system });
|
||||
} catch {
|
||||
return json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { setAccessTokenCookies } from '$lib/server/access-token-cookie';
|
||||
import { redirectToWorkspaceLogin } from '$lib/server/workspace-auth';
|
||||
import { isValidSystem, setActiveSystemCookie } from '$lib/server/system-gate';
|
||||
|
||||
// Disable client-side rendering to prevent SvelteKit from making a second
|
||||
// __data.json request that would consume the one-time relay token twice.
|
||||
@@ -15,7 +16,8 @@ export const csr = false;
|
||||
|
||||
export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
const relayToken = url.searchParams.get('relay');
|
||||
console.log('[SSO] relay token presente:', !!relayToken);
|
||||
const requestedSystem = url.searchParams.get('active_system');
|
||||
console.log('[SSO] relay token presente:', !!relayToken, '| active_system:', requestedSystem ?? '(none)');
|
||||
|
||||
if (!relayToken) {
|
||||
redirectToWorkspaceLogin(cookies, url);
|
||||
@@ -183,7 +185,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
}
|
||||
console.log('[SSO] cookies configuradas, redirigiendo a /dashboard');
|
||||
console.log('[SSO] cookies configuradas, preparando redirect a /dashboard con active_system:', requestedSystem ?? '(none)');
|
||||
|
||||
// Ejecutar lazy-link server-side: crear UserTenant si hay invite pendiente.
|
||||
// Se llama con el Bearer token recién obtenido. Best-effort, no bloquea el SSO.
|
||||
@@ -202,5 +204,9 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
}).catch(() => {});
|
||||
} catch { /* non-blocking */ }
|
||||
|
||||
if (isValidSystem(requestedSystem)) {
|
||||
setActiveSystemCookie(cookies, requestedSystem);
|
||||
}
|
||||
|
||||
throw redirect(303, '/dashboard');
|
||||
};
|
||||
|
||||
@@ -10,6 +10,14 @@ import {
|
||||
import {
|
||||
redirectToWorkspaceLogin
|
||||
} from '$lib/server/workspace-auth';
|
||||
import {
|
||||
extractAllowedSystemsFromToken,
|
||||
mergeTokenClaims,
|
||||
resolveActiveCompanyId,
|
||||
resolveSystemGate,
|
||||
setActiveSystemCookie,
|
||||
redirectToWorkspaceBase
|
||||
} from '$lib/server/system-gate';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
// Verificar si existe el token en las cookies
|
||||
@@ -31,15 +39,22 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
|
||||
const userData = await validateAuth(cookies, fetch, redirectOnFail);
|
||||
|
||||
// Si la cookie active_company_id apunta a una compañía que ya no existe, limpiarla
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
if (cookieCompanyId) {
|
||||
const cookieId = parseInt(cookieCompanyId);
|
||||
const stillExists = companies.some((c) => c.id === cookieId);
|
||||
if (!stillExists) {
|
||||
cookies.delete('active_company_id', { path: '/' });
|
||||
}
|
||||
const tokenClaims = mergeTokenClaims(userData, accessToken);
|
||||
const gate = resolveSystemGate({
|
||||
tokenClaims,
|
||||
cookieSystem: cookies.get('active_system') ?? null,
|
||||
requestedSystem: url.searchParams.get('active_system') ?? null
|
||||
});
|
||||
const activeCompanyId = resolveActiveCompanyId(cookies, companies);
|
||||
const allowedSystemsFromToken = extractAllowedSystemsFromToken(tokenClaims);
|
||||
if (gate.action === 'redirect_workspace') {
|
||||
redirectToWorkspaceBase();
|
||||
}
|
||||
// Si el JWT no trae el claim allowed_systems pero el gate resolvió un sistema,
|
||||
// usar el sistema activo como mínimo para que el store pueda inicializarse.
|
||||
const allowedSystems =
|
||||
allowedSystemsFromToken.length > 0 ? allowedSystemsFromToken : [gate.activeSystem];
|
||||
setActiveSystemCookie(cookies, gate.activeSystem);
|
||||
|
||||
// Cargar los tenants del usuario desde Hub (fuente de verdad multi-tenant)
|
||||
let userTenants: { id: number; name: string; slug: string }[] = [];
|
||||
@@ -59,14 +74,13 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
// No bloquear el dashboard si falla la carga de tenants
|
||||
}
|
||||
|
||||
// Obtener la compañía activa de la cookie para persistencia
|
||||
const activeCompanyId = cookies.get('active_company_id');
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
user: { ...userData, token: accessToken },
|
||||
user: { ...userData, token: accessToken, allowedSystems },
|
||||
companies,
|
||||
activeCompanyId: activeCompanyId ? parseInt(activeCompanyId) : undefined,
|
||||
activeCompanyId: activeCompanyId ?? undefined,
|
||||
activeSystem: gate.activeSystem,
|
||||
allowedSystems,
|
||||
userTenants,
|
||||
error: undefined
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { invalidateAll, goto } from '$app/navigation';
|
||||
import type { LayoutData } from './$types';
|
||||
import AppSidebar from '$lib/components/sidebar/app-sidebar.svelte';
|
||||
import AppLauncher from '$lib/components/sidebar/app-launcher.svelte';
|
||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
@@ -21,6 +22,7 @@
|
||||
import { authStore, markPermissionsHydrated } from '$lib/auth';
|
||||
import { logout, getKeycloakInstance } from '$lib/auth';
|
||||
import LicenseErrorScreen from '$lib/components/license-error-screen.svelte';
|
||||
import { systemStore, type SystemType } from '$lib/stores/system.svelte';
|
||||
|
||||
type LicenseError = { type: string; message: string; status: number };
|
||||
let { data, children }: { data: LayoutData & { licenseError?: LicenseError }; children: any } = $props();
|
||||
@@ -90,17 +92,26 @@
|
||||
legacyAvatarUrl: data.user.legacyAvatarUrl ?? data.user.legacy_avatar_url ?? data.user.avatar_url ?? null,
|
||||
tenantId: data.user.tenant_id,
|
||||
roles: data.user.roles ?? [],
|
||||
permissions: data.user.permissions ?? []
|
||||
permissions: data.user.permissions ?? [],
|
||||
allowedSystems: data.allowedSystems ?? data.user.allowedSystems ?? []
|
||||
});
|
||||
}
|
||||
|
||||
syncAuthStoreFromData();
|
||||
systemStore.initialize(
|
||||
(data.allowedSystems ?? []) as SystemType[],
|
||||
(data.activeSystem ?? null) as string | null
|
||||
);
|
||||
|
||||
// Re-sincronizar si data.user cambia (por ejemplo, tras invalidateAll o cambio de compañía)
|
||||
$effect(() => {
|
||||
// Dependencia explícita para que el effect reaccione al swap de data.user
|
||||
data.user;
|
||||
syncAuthStoreFromData();
|
||||
systemStore.initialize(
|
||||
(data.allowedSystems ?? []) as SystemType[],
|
||||
(data.activeSystem ?? null) as string | null
|
||||
);
|
||||
});
|
||||
|
||||
// ── Manejar expiración de sesión ────────────────────────────────────────
|
||||
@@ -181,20 +192,12 @@
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1" />
|
||||
<Separator orientation="vertical" class="mr-2 data-[orientation=vertical]:h-4" />
|
||||
<!--
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="/dashboard">Dashboard</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block" />
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>Inicio</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
-->
|
||||
</div>
|
||||
{#if systemStore.allowedSystems.length > 0}
|
||||
<div class="ml-auto px-4">
|
||||
<AppLauncher />
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
<div
|
||||
id="dashboard-main-content"
|
||||
|
||||
@@ -7,6 +7,8 @@ export const load: LayoutLoad = async ({ data }) => {
|
||||
companies: data.companies,
|
||||
authenticated: data.authenticated,
|
||||
userTenants: data.userTenants ?? [],
|
||||
activeCompanyId: data.activeCompanyId
|
||||
activeCompanyId: data.activeCompanyId,
|
||||
activeSystem: data.activeSystem ?? null,
|
||||
allowedSystems: data.allowedSystems ?? []
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { classesApi, type A76Class } from '$lib/api/dashboard/a76/classes';
|
||||
import { faClassesApi, type FAClass } from '$lib/api/dashboard/a24/fa_classes';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { systemStore } from '$lib/stores/system.svelte';
|
||||
import DataTable from '$lib/components/dashboard/goods/classes/data-table.svelte';
|
||||
import { columns } from '$lib/components/dashboard/goods/classes/columns';
|
||||
import { currentUser } from '$lib/auth';
|
||||
@@ -66,6 +67,9 @@
|
||||
let isSaving = $state(false);
|
||||
let sorting = $state<import("@tanstack/table-core").SortingState>([]);
|
||||
let status = $state<number>(200);
|
||||
const activeSystem = $derived(systemStore.activeSystem);
|
||||
const isInventoryMode = $derived(activeSystem === 'inventory');
|
||||
const isFixedAssetMode = $derived(activeSystem !== 'inventory');
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(canViewGoodsClasses($currentUser));
|
||||
@@ -139,7 +143,7 @@
|
||||
const seq = ++loadSeq;
|
||||
isLoading = true;
|
||||
try {
|
||||
const response = await classesApi.getWithFAData({
|
||||
const commonParams = {
|
||||
company_id: companyId,
|
||||
page: currentPage,
|
||||
page_size: PAGE_SIZE,
|
||||
@@ -155,7 +159,10 @@
|
||||
material_key: debouncedFilters.material_key.trim()
|
||||
}),
|
||||
...(debouncedFilters.fraction.trim() && { fraction: debouncedFilters.fraction.trim() })
|
||||
});
|
||||
};
|
||||
const response = isInventoryMode
|
||||
? await classesApi.list(commonParams)
|
||||
: await classesApi.getWithFAData(commonParams);
|
||||
|
||||
if (seq !== loadSeq) return;
|
||||
|
||||
@@ -174,7 +181,9 @@
|
||||
} else if (error?.status) {
|
||||
status = error.status;
|
||||
}
|
||||
toast.error('Error al cargar las clases de activo fijo');
|
||||
toast.error(
|
||||
isInventoryMode ? 'Error al cargar las clases de inventario' : 'Error al cargar las clases de activo fijo'
|
||||
);
|
||||
} finally {
|
||||
if (seq === loadSeq) {
|
||||
isLoading = false;
|
||||
@@ -266,7 +275,11 @@
|
||||
|
||||
function handleRowDoubleClick(cls: A76Class) {
|
||||
if (!canEdit) {
|
||||
toast.error('No tienes permiso para editar clases de activo fijo');
|
||||
toast.error(
|
||||
isInventoryMode
|
||||
? 'No tienes permiso para editar clases de inventario'
|
||||
: 'No tienes permiso para editar clases de activo fijo'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const fixedClass = cls as FixedAssetClassExtended;
|
||||
@@ -389,8 +402,14 @@
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Clases de Activo Fijo</h1>
|
||||
<p class="text-muted-foreground">Gestiona y consulta las clases de activo fijo</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
{isInventoryMode ? 'Clases de Inventario' : 'Clases de Activo Fijo'}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
{isInventoryMode
|
||||
? 'Gestiona y consulta las clases del sistema de inventario'
|
||||
: 'Gestiona y consulta las clases de activo fijo'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={handleRefresh} disabled={isLoading}>
|
||||
@@ -649,7 +668,9 @@
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<Dialog.Header class="border-b p-6 pb-4">
|
||||
<Dialog.Title>{selectedClass ? 'Editar' : 'Nueva'} Clase de Activo Fijo</Dialog.Title>
|
||||
<Dialog.Title>
|
||||
{selectedClass ? 'Editar' : 'Nueva'} {isInventoryMode ? 'Clase de Inventario' : 'Clase de Activo Fijo'}
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<!-- Mensaje de error de validación -->
|
||||
@@ -685,6 +706,7 @@
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<FixedAssetClassForm
|
||||
mode={isInventoryMode ? 'inventory' : 'fixed_asset'}
|
||||
initialData={selectedClass}
|
||||
externalError={validationError}
|
||||
onClearError={() => (validationError = '')}
|
||||
@@ -736,50 +758,52 @@
|
||||
companyId
|
||||
);
|
||||
|
||||
// También actualizar la extensión FA
|
||||
if (selectedClass.fa_class_id) {
|
||||
await faClassesApi.update(
|
||||
selectedClass.fa_class_id,
|
||||
{
|
||||
depreciation_rate:
|
||||
cleanData.annual_depreciation_rate !== undefined &&
|
||||
cleanData.annual_depreciation_rate !== null &&
|
||||
cleanData.annual_depreciation_rate !== ''
|
||||
? Number(cleanData.annual_depreciation_rate)
|
||||
: cleanData.depreciation_rate !== undefined &&
|
||||
cleanData.depreciation_rate !== null &&
|
||||
cleanData.depreciation_rate != null
|
||||
? Number(cleanData.depreciation_rate)
|
||||
: null,
|
||||
import_tariff_code: mxFraccionNorm || null,
|
||||
import_tariff_type: cleanData.import_tariff_type || null,
|
||||
export_tariff_code: cleanData.export_tariff_code || null,
|
||||
export_tariff_type: cleanData.export_tariff_type || null,
|
||||
fda_code: (cleanData.fda_key ?? cleanData.fda_code) || null,
|
||||
eccn_code: cleanData.eccn_code || null
|
||||
},
|
||||
companyId
|
||||
);
|
||||
} else {
|
||||
await faClassesApi.create(
|
||||
{
|
||||
class_id: selectedClass.id,
|
||||
depreciation_rate:
|
||||
cleanData.annual_depreciation_rate !== undefined &&
|
||||
cleanData.annual_depreciation_rate !== null &&
|
||||
cleanData.annual_depreciation_rate !== ''
|
||||
? Number(cleanData.annual_depreciation_rate)
|
||||
: cleanData.depreciation_rate !== undefined &&
|
||||
cleanData.depreciation_rate !== null &&
|
||||
cleanData.depreciation_rate != null
|
||||
? Number(cleanData.depreciation_rate)
|
||||
: null,
|
||||
fda_code: (cleanData.fda_key ?? cleanData.fda_code) || null,
|
||||
eccn_code: cleanData.eccn_code || null,
|
||||
class_enabled: true
|
||||
},
|
||||
companyId
|
||||
);
|
||||
if (isFixedAssetMode) {
|
||||
// También actualizar la extensión FA cuando estamos en SCAF
|
||||
if (selectedClass.fa_class_id) {
|
||||
await faClassesApi.update(
|
||||
selectedClass.fa_class_id,
|
||||
{
|
||||
depreciation_rate:
|
||||
cleanData.annual_depreciation_rate !== undefined &&
|
||||
cleanData.annual_depreciation_rate !== null &&
|
||||
cleanData.annual_depreciation_rate !== ''
|
||||
? Number(cleanData.annual_depreciation_rate)
|
||||
: cleanData.depreciation_rate !== undefined &&
|
||||
cleanData.depreciation_rate !== null &&
|
||||
cleanData.depreciation_rate != null
|
||||
? Number(cleanData.depreciation_rate)
|
||||
: null,
|
||||
import_tariff_code: mxFraccionNorm || null,
|
||||
import_tariff_type: cleanData.import_tariff_type || null,
|
||||
export_tariff_code: cleanData.export_tariff_code || null,
|
||||
export_tariff_type: cleanData.export_tariff_type || null,
|
||||
fda_code: (cleanData.fda_key ?? cleanData.fda_code) || null,
|
||||
eccn_code: cleanData.eccn_code || null
|
||||
},
|
||||
companyId
|
||||
);
|
||||
} else {
|
||||
await faClassesApi.create(
|
||||
{
|
||||
class_id: selectedClass.id,
|
||||
depreciation_rate:
|
||||
cleanData.annual_depreciation_rate !== undefined &&
|
||||
cleanData.annual_depreciation_rate !== null &&
|
||||
cleanData.annual_depreciation_rate !== ''
|
||||
? Number(cleanData.annual_depreciation_rate)
|
||||
: cleanData.depreciation_rate !== undefined &&
|
||||
cleanData.depreciation_rate !== null &&
|
||||
cleanData.depreciation_rate != null
|
||||
? Number(cleanData.depreciation_rate)
|
||||
: null,
|
||||
fda_code: (cleanData.fda_key ?? cleanData.fda_code) || null,
|
||||
eccn_code: cleanData.eccn_code || null,
|
||||
class_enabled: true
|
||||
},
|
||||
companyId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ¡IMPORTANTE! fetchApi NO lanza excepciones, retorna { error, status }
|
||||
@@ -815,7 +839,9 @@
|
||||
class_enabled: true
|
||||
};
|
||||
|
||||
response = await classesApi.createFA(payload, companyId);
|
||||
response = isInventoryMode
|
||||
? await classesApi.create(payload, companyId)
|
||||
: await classesApi.createFA(payload, companyId);
|
||||
|
||||
if (response.error) {
|
||||
console.error('❌ Error del servidor:', response.error);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { obtenerAtajosListaMercancias } from '$lib/config/shortcuts/dashboard/goods/list';
|
||||
import { currentUser } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
import { systemStore, SYSTEM_LABELS } from '$lib/stores/system.svelte';
|
||||
import {
|
||||
canCreateGoodsParts,
|
||||
canDeleteGoodsParts,
|
||||
@@ -36,6 +37,9 @@
|
||||
let status = $state<number>(200);
|
||||
let showDeleteDialog = $state(false);
|
||||
|
||||
const activeSystem = $derived(systemStore.activeSystem);
|
||||
const activeSystemLabel = $derived(activeSystem ? SYSTEM_LABELS[activeSystem] : null);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(canViewGoodsParts($currentUser));
|
||||
const canCreate = $derived(canCreateGoodsParts($currentUser));
|
||||
@@ -187,7 +191,11 @@
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Catálogo de Partes</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona y consulta las partes de inventario y activo fijo
|
||||
{#if activeSystemLabel}
|
||||
{activeSystemLabel.name} ({activeSystemLabel.code})
|
||||
{:else}
|
||||
Gestiona y consulta las partes de inventario y activo fijo
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user