import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { tenantService, type TenantResponse, type TenantAccessResponse } from '../api'; import { useAuthUser } from './auth.store'; import { TENANT_ROLES, GLOBAL_USER_ROLES } from '../types/roles'; export interface TenantState { // State currentTenant: TenantResponse | null; availableTenants: TenantResponse[] | null; currentTenantAccess: TenantAccessResponse | null; isLoading: boolean; error: string | null; // Actions setCurrentTenant: (tenant: TenantResponse) => void; switchTenant: (tenantId: string) => Promise; loadUserTenants: () => Promise; loadCurrentTenantAccess: () => Promise; clearTenants: () => void; clearError: () => void; setLoading: (loading: boolean) => void; // Permission helpers (migrated from BakeryContext) hasPermission: (permission: string) => boolean; canAccess: (resource: string, action: string) => boolean; } export const useTenantStore = create()( persist( (set, get) => ({ // Initial state currentTenant: null, availableTenants: null, currentTenantAccess: null, isLoading: false, error: null, // Actions setCurrentTenant: (tenant: TenantResponse) => { set({ currentTenant: tenant, currentTenantAccess: null }); // Update API client with new tenant ID if (tenant) { tenantService.setCurrentTenant(tenant); // Load tenant access info get().loadCurrentTenantAccess(); } }, switchTenant: async (tenantId: string): Promise => { try { set({ isLoading: true, error: null }); const { availableTenants } = get(); // Find tenant in available tenants const targetTenant = availableTenants?.find(t => t.id === tenantId); if (!targetTenant) { throw new Error('Tenant not found in available tenants'); } // Switch tenant (frontend-only operation) get().setCurrentTenant(targetTenant); set({ isLoading: false }); return true; } catch (error) { set({ isLoading: false, error: error instanceof Error ? error.message : 'Failed to switch tenant', }); return false; } }, loadUserTenants: async (): Promise => { try { set({ isLoading: true, error: null }); // Get current user to determine user ID const authState = JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state; const user = authState?.user; if (!user?.id) { throw new Error('User not authenticated'); } const response = await tenantService.getUserTenants(user.id); if (response) { const tenants = Array.isArray(response) ? response : [response]; set({ availableTenants: tenants, isLoading: false }); // If no current tenant is set, set the first one as current const { currentTenant } = get(); if (!currentTenant && tenants.length > 0) { get().setCurrentTenant(tenants[0]); } } else { // No tenants found - this is fine for users who haven't completed onboarding set({ availableTenants: [], isLoading: false, error: null }); } } catch (error) { // Handle 404 gracefully - user might not have created any tenants yet if (error && typeof error === 'object' && 'status' in error && error.status === 404) { set({ availableTenants: [], isLoading: false, error: null, }); } else { set({ isLoading: false, error: error instanceof Error ? error.message : 'Failed to load tenants', }); } } }, loadCurrentTenantAccess: async (): Promise => { try { const { currentTenant } = get(); if (!currentTenant) return; const accessInfo = await tenantService.getCurrentUserTenantAccess(currentTenant.id); set({ currentTenantAccess: accessInfo }); } catch (error) { // Don't set error state for access loading failures - just log console.warn('Failed to load tenant access:', error); set({ currentTenantAccess: null }); } }, clearTenants: () => { set({ currentTenant: null, availableTenants: null, currentTenantAccess: null, error: null, }); tenantService.clearCurrentTenant(); }, clearError: () => { set({ error: null }); }, setLoading: (loading: boolean) => { set({ isLoading: loading }); }, // Permission helpers (migrated from BakeryContext) hasPermission: (permission: string): boolean => { const { currentTenant, currentTenantAccess } = get(); if (!currentTenant || !currentTenantAccess || !currentTenantAccess.has_access) { return false; } // Check if user has specific permission in their tenant permissions array if (currentTenantAccess.permissions?.includes(permission)) { return true; } // Check if user has broader permissions that include this one if (currentTenantAccess.permissions?.includes('*') || currentTenantAccess.permissions?.includes('admin')) { return true; } // Role-based fallback for common permissions based on tenant role const tenantRole = currentTenantAccess.role; switch (tenantRole) { case TENANT_ROLES.OWNER: case TENANT_ROLES.ADMIN: return true; case TENANT_ROLES.MEMBER: // Members can read and write but not delete or manage users return !permission.includes('delete') && !permission.includes('admin'); case TENANT_ROLES.VIEWER: // Viewers can only read return permission.includes('read') || permission.includes('view'); default: return false; } }, canAccess: (resource: string, action: string): boolean => { const { hasPermission } = get(); // Check specific permission if (hasPermission(`${resource}:${action}`)) return true; // Check wildcard permissions if (hasPermission(`${resource}:*`)) return true; return false; }, }), { name: 'tenant-storage', storage: createJSONStorage(() => localStorage), partialize: (state) => ({ currentTenant: state.currentTenant, availableTenants: state.availableTenants, currentTenantAccess: state.currentTenantAccess, }), onRehydrateStorage: () => (state) => { // Initialize API client with stored tenant when store rehydrates if (state?.currentTenant) { import('../api').then(({ apiClient }) => { apiClient.setTenantId(state.currentTenant!.id); }); } }, } ) ); // Selectors for common use cases // Note: For getting tenant ID, prefer using useTenantId() from hooks/useTenantId.ts export const useCurrentTenant = () => useTenantStore((state) => state.currentTenant); export const useAvailableTenants = () => useTenantStore((state) => state.availableTenants); export const useCurrentTenantAccess = () => useTenantStore((state) => state.currentTenantAccess); export const useTenantLoading = () => useTenantStore((state) => state.isLoading); export const useTenantError = () => useTenantStore((state) => state.error); // Hook for tenant actions export const useTenantActions = () => useTenantStore((state) => ({ setCurrentTenant: state.setCurrentTenant, switchTenant: state.switchTenant, loadUserTenants: state.loadUserTenants, loadCurrentTenantAccess: state.loadCurrentTenantAccess, clearTenants: state.clearTenants, clearError: state.clearError, setLoading: state.setLoading, })); // Hook for tenant permissions (replaces useBakeryPermissions) export const useTenantPermissions = () => useTenantStore((state) => ({ hasPermission: state.hasPermission, canAccess: state.canAccess, })); // Combined hook for convenience export const useTenant = () => { const currentTenant = useCurrentTenant(); const availableTenants = useAvailableTenants(); const currentTenantAccess = useCurrentTenantAccess(); const isLoading = useTenantLoading(); const error = useTenantError(); const actions = useTenantActions(); const permissions = useTenantPermissions(); return { currentTenant, availableTenants, currentTenantAccess, isLoading, error, ...actions, ...permissions, }; };