/** * Store para manejar la compañía activa del usuario * Permite cambiar entre las compañías que pertenecen al tenant */ import { browser } from '$app/environment'; interface Company { id: number; name: string; rfc?: string; logo?: string; tenant_id: number; } class CompanyStore { private _activeCompany = $state(null); private _companies = $state([]); private _loading = $state(false); private _currentTenantId = $state(null); get activeCompany() { return this._activeCompany; } get companies() { return this._companies; } get loading() { return this._loading; } /** * Carga las compañías del tenant del usuario desde el backend * @param preloadedCompanies - Compañías pre-cargadas desde el servidor (SSR) */ async loadCompanies(preloadedCompanies?: Company[], activeCompanyId?: number) { // Si tenemos compañías pre-cargadas, usarlas directamente if (preloadedCompanies && preloadedCompanies.length > 0) { // Detectar si el tenant ha cambiado const newTenantId = preloadedCompanies[0].tenant_id; // Si el tenant cambió, limpiar el store primero if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) { this.clear(); } this._currentTenantId = newTenantId; this._companies = preloadedCompanies; // Si hay compañías y no hay una activa, seleccionar la correcta. // IMPORTANTE: `setActiveCompany` es async (dispara syncCompanyPermissions); // hay que `await` para que el caller (initialize → markPermissionsHydrated) // no marque la hidratación antes de que los permisos del backend lleguen. if (this._companies.length > 0 && !this._activeCompany) { // Prioridad 1: ID pasado por parámetro (desde SSR/Cookie) if (activeCompanyId) { const company = this._companies.find(c => c.id === activeCompanyId); if (company) { await this.setActiveCompany(company, true); return; } } // Prioridad 2: Intentar restaurar de localStorage (Browser only) if (typeof window !== 'undefined') { const savedId = localStorage.getItem('activeCompanyId'); if (savedId) { const company = this._companies.find(c => c.id === parseInt(savedId)); if (company) { await this.setActiveCompany(company, true); // silent=true para inicialización return; } } } // Prioridad 3: Si no hay nada, seleccionar la primera await this.setActiveCompany(this._companies[0], true); // silent=true para inicialización } return; } // Si no hay datos pre-cargados, hacer fetch (fallback) // Solo en el navegador, nunca durante SSR if (!browser) { return; } this._loading = true; try { // Importamos dinámicamente para evitar dependencias circulares si las hubiera const { api } = await import('$lib/api'); const response = await api.get('/v1/auth/my-companies'); if (response.data) { const newCompanies = response.data; // Detectar si el tenant ha cambiado if (newCompanies.length > 0) { const newTenantId = newCompanies[0].tenant_id; // Si el tenant cambió, limpiar el store primero if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) { this.clear(); } this._currentTenantId = newTenantId; } this._companies = newCompanies; // Si hay compañías y no hay una activa, seleccionar la primera. // `await` necesario para esperar a syncCompanyPermissions antes de // que la inicialización del layout marque permissionsHydrated. if (this._companies.length > 0 && !this._activeCompany) { await this.setActiveCompany(this._companies[0], true); // silent=true para inicialización } } else { console.error('Error loading companies:', response.error); if (response.status === 402) { const { toast } = await import('svelte-sonner'); toast.error('Licencia inactiva', { duration: 8000, description: response.error || 'Tu licencia no está activa para este tenant. Contacta al administrador.' }); this.clear(); } else if (response.status === 401) { this.clear(); } } } catch (error) { console.error('Error loading companies:', error); } finally { this._loading = false; } } /** * Establece la compañía activa * @param company - La compañía a establecer como activa * @param silent - Si es true, no dispara el evento companyChanged (para inicialización) */ async setActiveCompany(company: Company, silent: boolean = false) { const previousCompanyId = this._activeCompany?.id; this._activeCompany = company; // Guardar en localStorage para persistencia if (typeof window !== 'undefined') { localStorage.setItem('activeCompanyId', company.id.toString()); } // Guardar en cookie para acceso desde el servidor (SSR) // Usar el endpoint del servidor para garantizar que la cookie esté disponible en SSR if (browser) { try { await fetch('/api-sveltekit/company/set-active', { method: 'POST', headers: { 'Content-Type': 'application/json' }, // tenant_id de la compañía → override para que el backend escale por // ese tenant (necesario cuando el usuario es hub_admin sin tenant en el token). body: JSON.stringify({ companyId: company.id, tenantId: company.tenant_id }), credentials: 'include' }); } catch (error) { console.error('Error setting active company cookie:', error); } try { const { syncCompanyPermissions } = await import('$lib/auth'); await syncCompanyPermissions(company.id); } catch (e) { console.warn('syncCompanyPermissions:', e); } } // Despachar evento personalizado solo si: // 1. No es silent (no es inicialización) // 2. Y realmente cambió la compañía (el ID es diferente) if (!silent && typeof window !== 'undefined' && previousCompanyId !== company.id) { window.dispatchEvent(new CustomEvent('companyChanged', { detail: { companyId: company.id } })); } } /** * Actualiza los datos de une empresa en el store localmente * Útil para reflejar cambios inmediatos (ej: cambio de logo) sin recargar */ updateCompany(id: number, data: Partial) { // 1. Actualizar en la lista const index = this._companies.findIndex(c => c.id === id); if (index !== -1) { this._companies[index] = { ...this._companies[index], ...data }; // 2. Si es la activa, actualizar también if (this._activeCompany?.id === id) { this._activeCompany = { ...this._activeCompany, ...data }; // Actualizar persistencia si es necesario if (typeof window !== 'undefined') { // Disparar evento para notificar cambios a componentes que no usan el store reactivo directo (si los hay) window.dispatchEvent(new CustomEvent('companyChanged', { detail: { companyId: id } })); } } } } /** * Restaura la compañía activa desde localStorage */ restoreActiveCompany() { if (typeof window !== 'undefined') { const savedId = localStorage.getItem('activeCompanyId'); if (savedId && this._companies.length > 0) { const company = this._companies.find(c => c.id === parseInt(savedId)); if (company) { this._activeCompany = company; } } } } /** * Limpia el store (útil al cambiar de tenant o cerrar sesión) */ clear() { this._activeCompany = null; this._companies = []; this._loading = false; this._currentTenantId = null; // Limpiar localStorage if (typeof window !== 'undefined') { localStorage.removeItem('activeCompanyId'); } // Limpiar cookie if (typeof document !== 'undefined') { document.cookie = 'active_company_id=; path=/; max-age=0'; } } /** * Inicializa el store cargando las compañías * @param preloadedCompanies - Compañías pre-cargadas desde el servidor (SSR) */ async initialize(preloadedCompanies?: Company[], activeCompanyId?: number) { await this.loadCompanies(preloadedCompanies, activeCompanyId); // Si no hay compañías pre-cargadas, intentar restaurar de localStorage if (!preloadedCompanies) { this.restoreActiveCompany(); } } } export const companyStore = new CompanyStore();