Start integrating the onboarding flow with backend 1
This commit is contained in:
230
frontend/src/stores/tenant.store.ts
Normal file
230
frontend/src/stores/tenant.store.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { tenantService, TenantResponse } from '../services/api/tenant.service';
|
||||
import { useAuthUser } from './auth.store';
|
||||
|
||||
export interface TenantState {
|
||||
// State
|
||||
currentTenant: TenantResponse | null;
|
||||
availableTenants: TenantResponse[] | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => void;
|
||||
switchTenant: (tenantId: string) => Promise<boolean>;
|
||||
loadUserTenants: () => Promise<void>;
|
||||
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<TenantState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
currentTenant: null,
|
||||
availableTenants: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => {
|
||||
set({ currentTenant: tenant });
|
||||
// Update API client with new tenant ID
|
||||
tenantService.setCurrentTenant(tenant);
|
||||
},
|
||||
|
||||
switchTenant: async (tenantId: string): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const { availableTenants } = get();
|
||||
|
||||
// Find tenant in available tenants first
|
||||
const targetTenant = availableTenants?.find(t => t.id === tenantId);
|
||||
if (!targetTenant) {
|
||||
throw new Error('Tenant not found in available tenants');
|
||||
}
|
||||
|
||||
// Switch tenant using service
|
||||
const response = await tenantService.switchTenant(tenantId);
|
||||
|
||||
if (response.success && response.data?.tenant) {
|
||||
get().setCurrentTenant(response.data.tenant);
|
||||
set({ isLoading: false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to switch tenant');
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to switch tenant',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
loadUserTenants: async (): Promise<void> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
// Get current user to determine user ID
|
||||
const user = useAuthUser.getState?.() || JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state?.user;
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const response = await tenantService.getUserTenants(user.id);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const tenants = Array.isArray(response.data) ? response.data : [response.data];
|
||||
|
||||
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 {
|
||||
throw new Error(response.error || 'Failed to load user tenants');
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to load tenants',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearTenants: () => {
|
||||
set({
|
||||
currentTenant: null,
|
||||
availableTenants: 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 } = get();
|
||||
if (!currentTenant) return false;
|
||||
|
||||
// Get user to determine role within this tenant
|
||||
const user = useAuthUser.getState?.() || JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state?.user;
|
||||
|
||||
// Admin role has all permissions
|
||||
if (user?.role === 'admin') return true;
|
||||
|
||||
// TODO: Implement proper tenant-based permissions
|
||||
// For now, use basic role-based permissions
|
||||
switch (user?.role) {
|
||||
case 'admin':
|
||||
return true;
|
||||
case 'manager':
|
||||
return ['inventory', 'production', 'sales', 'reports'].some(resource =>
|
||||
permission.startsWith(resource)
|
||||
);
|
||||
case 'baker':
|
||||
return ['production', 'inventory'].some(resource =>
|
||||
permission.startsWith(resource)
|
||||
) && !permission.includes(':delete');
|
||||
case 'staff':
|
||||
return ['inventory', 'sales'].some(resource =>
|
||||
permission.startsWith(resource)
|
||||
) && permission.includes(':read');
|
||||
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,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Initialize API client with stored tenant when store rehydrates
|
||||
if (state?.currentTenant) {
|
||||
import('../services/api/client').then(({ apiClient }) => {
|
||||
apiClient.setTenantId(state.currentTenant!.id);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Selectors for common use cases
|
||||
export const useCurrentTenant = () => useTenantStore((state) => state.currentTenant);
|
||||
export const useAvailableTenants = () => useTenantStore((state) => state.availableTenants);
|
||||
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,
|
||||
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 isLoading = useTenantLoading();
|
||||
const error = useTenantError();
|
||||
const actions = useTenantActions();
|
||||
const permissions = useTenantPermissions();
|
||||
|
||||
return {
|
||||
currentTenant,
|
||||
availableTenants,
|
||||
isLoading,
|
||||
error,
|
||||
...actions,
|
||||
...permissions,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user