Las compañías (a76.company) se gestionan en el CRM, ligadas a un tenant del Workspace. Cambios: - my-companies: por MEMBRESÍA (user_tenants ∪ user_company_roles), así el hub_admin sin tenant en el token ve las compañías que creó/se le asignaron; para usuarios con tenant, autocrea una por defecto en el primer acceso. - POST /auth/companies: da de alta una compañía bajo un tenant + asigna al usuario. GET /auth/assignable-tenants: tenants elegibles (hub_admin: todos; usuario: el suyo). - security.validate_access_to_resource: si el token no trae tenant (hub_admin), resuelve el tenant desde la compañía activa (a76.company.tenant_id) → puede operar por compañía seleccionada. - set-active: fija sso_tenant_id/pub con el tenant de la compañía (override). - Pantalla "Compañías" (nav) para crear/listar/seleccionar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
260 lines
8.0 KiB
TypeScript
260 lines
8.0 KiB
TypeScript
/**
|
|
* 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<Company | null>(null);
|
|
private _companies = $state<Company[]>([]);
|
|
private _loading = $state(false);
|
|
private _currentTenantId = $state<number | null>(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<Company[]>('/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<Company>) {
|
|
// 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();
|