Improve enterprise tier child tenants access
This commit is contained in:
@@ -360,7 +360,11 @@ class ApiClient {
|
||||
}
|
||||
|
||||
setTenantId(tenantId: string | null) {
|
||||
console.log('🔧 [API Client] setTenantId called with:', tenantId);
|
||||
console.log('🔧 [API Client] Previous tenantId was:', this.tenantId);
|
||||
console.trace('📍 [API Client] setTenantId call stack:');
|
||||
this.tenantId = tenantId;
|
||||
console.log('✅ [API Client] tenantId is now:', this.tenantId);
|
||||
}
|
||||
|
||||
setDemoSessionId(sessionId: string | null) {
|
||||
|
||||
79
frontend/src/api/hooks/usePremises.ts
Normal file
79
frontend/src/api/hooks/usePremises.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Hook for premises (child tenants) management
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import { tenantService } from '../services/tenant';
|
||||
import type { TenantResponse } from '../types/tenant';
|
||||
|
||||
export interface PremisesFilters {
|
||||
search?: string;
|
||||
status?: 'active' | 'inactive' | '';
|
||||
}
|
||||
|
||||
export interface PremisesStats {
|
||||
total: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child tenants (premises) for a parent tenant
|
||||
*/
|
||||
export const usePremises = (
|
||||
parentTenantId: string,
|
||||
filters?: PremisesFilters,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<TenantResponse[]> => {
|
||||
return useQuery({
|
||||
queryKey: ['premises', parentTenantId, filters],
|
||||
queryFn: async () => {
|
||||
const response = await tenantService.getChildTenants(parentTenantId);
|
||||
|
||||
let filtered = response;
|
||||
|
||||
// Apply search filter
|
||||
if (filters?.search) {
|
||||
const searchLower = filters.search.toLowerCase();
|
||||
filtered = filtered.filter(tenant =>
|
||||
tenant.name.toLowerCase().includes(searchLower) ||
|
||||
tenant.city?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply status filter
|
||||
if (filters?.status === 'active') {
|
||||
filtered = filtered.filter(tenant => tenant.is_active);
|
||||
} else if (filters?.status === 'inactive') {
|
||||
filtered = filtered.filter(tenant => !tenant.is_active);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
staleTime: 60000, // 1 min cache
|
||||
enabled: (options?.enabled ?? true) && !!parentTenantId,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get premises statistics
|
||||
*/
|
||||
export const usePremisesStats = (
|
||||
parentTenantId: string,
|
||||
options?: { enabled?: boolean }
|
||||
): UseQueryResult<PremisesStats> => {
|
||||
return useQuery({
|
||||
queryKey: ['premises', 'stats', parentTenantId],
|
||||
queryFn: async () => {
|
||||
const response = await tenantService.getChildTenants(parentTenantId);
|
||||
|
||||
return {
|
||||
total: response.length,
|
||||
active: response.filter(t => t.is_active).length,
|
||||
inactive: response.filter(t => !t.is_active).length,
|
||||
};
|
||||
},
|
||||
staleTime: 60000,
|
||||
enabled: (options?.enabled ?? true) && !!parentTenantId,
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useCallback, forwardRef } from 'react';
|
||||
import React, { useState, useCallback, forwardRef, useEffect } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { useHasAccess } from '../../../hooks/useAccessControl';
|
||||
import { useTenant } from '../../../stores/tenant.store';
|
||||
import { Header } from '../Header';
|
||||
import { Sidebar } from '../Sidebar';
|
||||
import { Footer } from '../Footer';
|
||||
@@ -77,10 +79,29 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
|
||||
const authLoading = false; // Since we're in a protected route, auth loading should be false
|
||||
const { resolvedTheme } = useTheme();
|
||||
const hasAccess = useHasAccess(); // Check both authentication and demo mode
|
||||
const location = useLocation();
|
||||
const { parentTenant, restoreParentTenant } = useTenant();
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(initialSidebarCollapsed);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isRestoringTenant, setIsRestoringTenant] = useState(false);
|
||||
|
||||
// Auto-restore parent tenant context when navigating away from premises detail view
|
||||
useEffect(() => {
|
||||
// If we have a parent tenant stored (meaning we switched to a child premise)
|
||||
// and we're NOT on the premises page, restore the parent tenant
|
||||
if (parentTenant && !location.pathname.includes('/enterprise/premises')) {
|
||||
console.log('[AppShell] Restoring parent tenant context - navigated away from premises');
|
||||
setIsRestoringTenant(true);
|
||||
restoreParentTenant().finally(() => {
|
||||
// Small delay to ensure all components have the updated tenant context
|
||||
setTimeout(() => {
|
||||
setIsRestoringTenant(false);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}, [location.pathname, parentTenant, restoreParentTenant]);
|
||||
|
||||
// Sidebar control functions
|
||||
const toggleSidebar = useCallback(() => {
|
||||
@@ -188,6 +209,24 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state during tenant restoration to prevent race condition
|
||||
// CRITICAL: Also check if parentTenant exists and we're NOT on premises page
|
||||
// This catches the case where navigation happens before the useEffect runs
|
||||
const isNavigatingAwayFromChild = parentTenant && !location.pathname.includes('/enterprise/premises');
|
||||
|
||||
if (isRestoringTenant || isNavigatingAwayFromChild) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[var(--bg-primary)]">
|
||||
{loadingComponent || (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
|
||||
<p className="text-[var(--text-secondary)]">Restaurando contexto...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error && ErrorBoundary) {
|
||||
return <ErrorBoundary error={error}>{children}</ErrorBoundary>;
|
||||
|
||||
@@ -50,7 +50,8 @@ import {
|
||||
Layers,
|
||||
Lightbulb,
|
||||
Activity,
|
||||
List
|
||||
List,
|
||||
Building
|
||||
} from 'lucide-react';
|
||||
|
||||
export interface SidebarProps {
|
||||
@@ -138,6 +139,7 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
events: Activity,
|
||||
list: List,
|
||||
distribution: Truck,
|
||||
premises: Building,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -196,6 +198,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
|
||||
const pathMappings: Record<string, string> = {
|
||||
'/app/dashboard': 'navigation.dashboard',
|
||||
'/app/enterprise/premises': 'navigation.premises',
|
||||
'/app/operations': 'navigation.operations',
|
||||
'/app/operations/procurement': 'navigation.procurement',
|
||||
'/app/operations/production': 'navigation.production',
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTenant } from '../../stores/tenant.store';
|
||||
import { useSubscription } from '../../api/hooks/subscription';
|
||||
import { showToast } from '../../utils/toast';
|
||||
import { ChevronDown, Building2, Check, AlertCircle, Plus, X } from 'lucide-react';
|
||||
|
||||
@@ -36,6 +37,8 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
clearError,
|
||||
} = useTenant();
|
||||
|
||||
const { subscriptionInfo } = useSubscription();
|
||||
|
||||
// NOTE: Removed duplicate loadUserTenants() useEffect
|
||||
// Tenant loading is already handled by useTenantInitializer at app level (stores/useTenantInitializer.ts)
|
||||
// This was causing duplicate /tenants API calls on every dashboard load
|
||||
@@ -167,6 +170,11 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
navigate('/app/onboarding?new=true');
|
||||
};
|
||||
|
||||
// Don't render for enterprise tier users (they use Premises page instead)
|
||||
if (subscriptionInfo?.plan === 'enterprise') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't render if no tenants available
|
||||
if (!availableTenants || availableTenants.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"pos": "Point of Sale",
|
||||
"distribution": "Distribution",
|
||||
"central_baker": "Central Baker",
|
||||
"premises": "Premises",
|
||||
"analytics": "Analytics",
|
||||
"production_analytics": "Production Dashboard",
|
||||
"procurement_analytics": "Procurement Dashboard",
|
||||
|
||||
47
frontend/src/locales/en/premises.json
Normal file
47
frontend/src/locales/en/premises.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"title": "Premises",
|
||||
"description": "Manage all your premises from one place",
|
||||
"stats": {
|
||||
"total_premises": "Total Premises",
|
||||
"active_premises": "Active Premises",
|
||||
"inactive_premises": "Inactive Premises"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search by name or city..."
|
||||
},
|
||||
"filters": {
|
||||
"status": "Status",
|
||||
"all_statuses": "All statuses",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive"
|
||||
},
|
||||
"card": {
|
||||
"location": "Location",
|
||||
"more_details": "More Details",
|
||||
"status_active": "Active",
|
||||
"status_inactive": "Inactive"
|
||||
},
|
||||
"detail": {
|
||||
"back_to_list": "Back to list",
|
||||
"tabs": {
|
||||
"procurement": "Procurement",
|
||||
"production": "Production",
|
||||
"pos": "Point of Sale",
|
||||
"suppliers": "Suppliers",
|
||||
"inventory": "Inventory",
|
||||
"recipes": "Recipes",
|
||||
"orders": "Orders",
|
||||
"machinery": "Machinery",
|
||||
"quality_templates": "Quality Templates",
|
||||
"team": "Team",
|
||||
"ai_models": "AI Models",
|
||||
"sustainability": "Sustainability",
|
||||
"settings": "Settings"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "No premises",
|
||||
"description": "No premises found matching your search"
|
||||
},
|
||||
"loading": "Loading premises..."
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"pos": "Punto de Venta",
|
||||
"distribution": "Distribución",
|
||||
"central_baker": "Central Baker",
|
||||
"premises": "Locales",
|
||||
"analytics": "Análisis",
|
||||
"production_analytics": "Dashboard de Producción",
|
||||
"procurement_analytics": "Dashboard de Compras",
|
||||
|
||||
47
frontend/src/locales/es/premises.json
Normal file
47
frontend/src/locales/es/premises.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"title": "Locales",
|
||||
"description": "Gestiona todos tus locales desde un solo lugar",
|
||||
"stats": {
|
||||
"total_premises": "Total de Locales",
|
||||
"active_premises": "Locales Activos",
|
||||
"inactive_premises": "Locales Inactivos"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar por nombre o ciudad..."
|
||||
},
|
||||
"filters": {
|
||||
"status": "Estado",
|
||||
"all_statuses": "Todos los estados",
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo"
|
||||
},
|
||||
"card": {
|
||||
"location": "Ubicación",
|
||||
"more_details": "Más Detalles",
|
||||
"status_active": "Activo",
|
||||
"status_inactive": "Inactivo"
|
||||
},
|
||||
"detail": {
|
||||
"back_to_list": "Volver a la lista",
|
||||
"tabs": {
|
||||
"procurement": "Compras",
|
||||
"production": "Producción",
|
||||
"pos": "Punto de Venta",
|
||||
"suppliers": "Proveedores",
|
||||
"inventory": "Inventario",
|
||||
"recipes": "Recetas",
|
||||
"orders": "Pedidos",
|
||||
"machinery": "Maquinaria",
|
||||
"quality_templates": "Plantillas de Calidad",
|
||||
"team": "Trabajadores",
|
||||
"ai_models": "Modelos IA",
|
||||
"sustainability": "Sostenibilidad",
|
||||
"settings": "Ajustes"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "No hay locales",
|
||||
"description": "No se encontraron locales que coincidan con tu búsqueda"
|
||||
},
|
||||
"loading": "Cargando locales..."
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
"orders": "Eskaerak",
|
||||
"procurement": "Erosketak",
|
||||
"pos": "Salmenta-puntua",
|
||||
"distribution": "Banaketa",
|
||||
"central_baker": "Okinleku Zentrala",
|
||||
"premises": "Lokalak",
|
||||
"analytics": "Analisiak",
|
||||
"production_analytics": "Ekoizpen Aginte-panela",
|
||||
"procurement_analytics": "Erosketa Aginte-panela",
|
||||
|
||||
47
frontend/src/locales/eu/premises.json
Normal file
47
frontend/src/locales/eu/premises.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"title": "Lokalak",
|
||||
"description": "Kudeatu zure lokal guztiak leku batetik",
|
||||
"stats": {
|
||||
"total_premises": "Lokal Guztira",
|
||||
"active_premises": "Lokal Aktiboak",
|
||||
"inactive_premises": "Lokal Ez-aktiboak"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Bilatu izenaz edo hiriaz..."
|
||||
},
|
||||
"filters": {
|
||||
"status": "Egoera",
|
||||
"all_statuses": "Egoera guztiak",
|
||||
"active": "Aktiboa",
|
||||
"inactive": "Ez-aktiboa"
|
||||
},
|
||||
"card": {
|
||||
"location": "Kokalekua",
|
||||
"more_details": "Xehetasun Gehiago",
|
||||
"status_active": "Aktiboa",
|
||||
"status_inactive": "Ez-aktiboa"
|
||||
},
|
||||
"detail": {
|
||||
"back_to_list": "Zerrendara itzuli",
|
||||
"tabs": {
|
||||
"procurement": "Erosketak",
|
||||
"production": "Ekoizpena",
|
||||
"pos": "Salmenta-puntua",
|
||||
"suppliers": "Hornitzaileak",
|
||||
"inventory": "Inbentarioa",
|
||||
"recipes": "Errezetak",
|
||||
"orders": "Eskaerak",
|
||||
"machinery": "Makinaria",
|
||||
"quality_templates": "Kalitate Txantiloiak",
|
||||
"team": "Langileak",
|
||||
"ai_models": "IA Ereduak",
|
||||
"sustainability": "Jasangarritasuna",
|
||||
"settings": "Ezarpenak"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Ez dago lokalik",
|
||||
"description": "Ez da zure bilaketarekin bat datorren lokalik aurkitu"
|
||||
},
|
||||
"loading": "Lokalak kargatzen..."
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import alertsEs from './es/alerts.json';
|
||||
import onboardingEs from './es/onboarding.json';
|
||||
import setupWizardEs from './es/setup_wizard.json';
|
||||
import contactEs from './es/contact.json';
|
||||
import premisesEs from './es/premises.json';
|
||||
|
||||
// English translations
|
||||
import commonEn from './en/common.json';
|
||||
@@ -55,6 +56,7 @@ import alertsEn from './en/alerts.json';
|
||||
import onboardingEn from './en/onboarding.json';
|
||||
import setupWizardEn from './en/setup_wizard.json';
|
||||
import contactEn from './en/contact.json';
|
||||
import premisesEn from './en/premises.json';
|
||||
|
||||
// Basque translations
|
||||
import commonEu from './eu/common.json';
|
||||
@@ -84,6 +86,7 @@ import alertsEu from './eu/alerts.json';
|
||||
import onboardingEu from './eu/onboarding.json';
|
||||
import setupWizardEu from './eu/setup_wizard.json';
|
||||
import contactEu from './eu/contact.json';
|
||||
import premisesEu from './eu/premises.json';
|
||||
|
||||
// Translation resources by language
|
||||
export const resources = {
|
||||
@@ -115,6 +118,7 @@ export const resources = {
|
||||
onboarding: onboardingEs,
|
||||
setup_wizard: setupWizardEs,
|
||||
contact: contactEs,
|
||||
premises: premisesEs,
|
||||
},
|
||||
en: {
|
||||
common: commonEn,
|
||||
@@ -144,6 +148,7 @@ export const resources = {
|
||||
onboarding: onboardingEn,
|
||||
setup_wizard: setupWizardEn,
|
||||
contact: contactEn,
|
||||
premises: premisesEn,
|
||||
},
|
||||
eu: {
|
||||
common: commonEu,
|
||||
@@ -173,6 +178,7 @@ export const resources = {
|
||||
onboarding: onboardingEu,
|
||||
setup_wizard: setupWizardEu,
|
||||
contact: contactEu,
|
||||
premises: premisesEu,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -209,7 +215,7 @@ export const languageConfig = {
|
||||
};
|
||||
|
||||
// Namespaces available in translations
|
||||
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production', 'equipment', 'landing', 'settings', 'ajustes', 'reasoning', 'wizards', 'subscription', 'purchase_orders', 'help', 'features', 'about', 'demo', 'blog', 'alerts', 'onboarding', 'setup_wizard', 'contact'] as const;
|
||||
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production', 'equipment', 'landing', 'settings', 'ajustes', 'reasoning', 'wizards', 'subscription', 'purchase_orders', 'help', 'features', 'about', 'demo', 'blog', 'alerts', 'onboarding', 'setup_wizard', 'contact', 'premises'] as const;
|
||||
export type Namespace = typeof namespaces[number];
|
||||
|
||||
// Helper function to get language display name
|
||||
|
||||
347
frontend/src/pages/app/enterprise/premises/PremisesPage.tsx
Normal file
347
frontend/src/pages/app/enterprise/premises/PremisesPage.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Building2,
|
||||
Eye,
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
ShoppingCart,
|
||||
Factory,
|
||||
Store,
|
||||
Users,
|
||||
Package,
|
||||
ChefHat,
|
||||
ClipboardList,
|
||||
Cog,
|
||||
ClipboardCheck,
|
||||
UserCircle,
|
||||
BrainCircuit,
|
||||
Leaf,
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
|
||||
// UI Components
|
||||
import {
|
||||
StatsGrid,
|
||||
StatusCard,
|
||||
getStatusColor,
|
||||
SearchAndFilter,
|
||||
EmptyState,
|
||||
type FilterConfig
|
||||
} from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
// Hooks
|
||||
import { usePremises, usePremisesStats } from '../../../../api/hooks/usePremises';
|
||||
import { useTenant } from '../../../../stores/tenant.store';
|
||||
import type { TenantResponse } from '../../../../api/types/tenant';
|
||||
|
||||
// Lazy-loaded page components for tabs
|
||||
const ProcurementPage = React.lazy(() => import('../../operations/procurement/ProcurementPage'));
|
||||
const ProductionPage = React.lazy(() => import('../../operations/production/ProductionPage'));
|
||||
const POSPage = React.lazy(() => import('../../operations/pos/POSPage'));
|
||||
const SuppliersPage = React.lazy(() => import('../../operations/suppliers/SuppliersPage'));
|
||||
const InventoryPage = React.lazy(() => import('../../operations/inventory/InventoryPage'));
|
||||
const RecipesPage = React.lazy(() => import('../../operations/recipes/RecipesPage'));
|
||||
const OrdersPage = React.lazy(() => import('../../operations/orders/OrdersPage'));
|
||||
const MaquinariaPage = React.lazy(() => import('../../operations/maquinaria/MaquinariaPage'));
|
||||
const QualityTemplatesPage = React.lazy(() => import('../../database/quality-templates/QualityTemplatesPage'));
|
||||
const TeamPage = React.lazy(() => import('../../settings/team/TeamPage'));
|
||||
const ModelsConfigPage = React.lazy(() => import('../../database/models/ModelsConfigPage'));
|
||||
const SustainabilityPage = React.lazy(() => import('../../database/sustainability/SustainabilityPage'));
|
||||
const BakerySettingsPage = React.lazy(() => import('../../settings/bakery/BakerySettingsPage'));
|
||||
|
||||
interface SelectedPremise {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TabKey = 'procurement' | 'production' | 'pos' | 'suppliers' | 'inventory' |
|
||||
'recipes' | 'orders' | 'machinery' | 'quality_templates' |
|
||||
'team' | 'ai_models' | 'sustainability' | 'settings';
|
||||
|
||||
const PremisesPage: React.FC = () => {
|
||||
const { t } = useTranslation(['premises', 'common']);
|
||||
|
||||
// Tenant store
|
||||
const {
|
||||
currentTenant,
|
||||
parentTenant,
|
||||
switchToChildTenant,
|
||||
restoreParentTenant
|
||||
} = useTenant();
|
||||
|
||||
// State
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'active' | 'inactive' | ''>('');
|
||||
const [selectedPremise, setSelectedPremise] = useState<SelectedPremise | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('procurement');
|
||||
|
||||
// Get parent tenant ID (enterprise tenant)
|
||||
// If we have a parentTenant stored, use it; otherwise use currentTenant
|
||||
const enterpriseTenantId = parentTenant?.id || currentTenant?.id || '';
|
||||
|
||||
// Hooks
|
||||
const { data: premises = [], isLoading: isPremisesLoading } = usePremises(
|
||||
enterpriseTenantId,
|
||||
{ search: searchTerm, status: statusFilter },
|
||||
{ enabled: !!enterpriseTenantId && !selectedPremise }
|
||||
);
|
||||
|
||||
const { data: stats, isLoading: isStatsLoading } = usePremisesStats(
|
||||
enterpriseTenantId,
|
||||
{ enabled: !!enterpriseTenantId && !selectedPremise }
|
||||
);
|
||||
|
||||
// Handle selecting a premise for detail view
|
||||
const handleSelectPremise = useCallback(async (premiseId: string, premiseName: string) => {
|
||||
// Find the child tenant in the premises list
|
||||
const childTenant = premises.find(p => p.id === premiseId);
|
||||
|
||||
if (!childTenant) {
|
||||
console.error('[PremisesPage] Child tenant not found:', premiseId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PremisesPage] Selecting premise:', premiseName, premiseId);
|
||||
|
||||
// Switch to child tenant (this automatically stores parent tenant)
|
||||
const success = await switchToChildTenant(childTenant);
|
||||
|
||||
if (!success) {
|
||||
console.error('[PremisesPage] Failed to switch to child tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PremisesPage] Tenant switch successful, showing detail view');
|
||||
|
||||
// Small delay to ensure API client is updated before rendering child components
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Set selected premise for detail view
|
||||
setSelectedPremise({ id: premiseId, name: premiseName });
|
||||
setActiveTab('procurement'); // Reset to first tab
|
||||
}, [premises, switchToChildTenant]);
|
||||
|
||||
// Handle returning to list view
|
||||
const handleBackToList = useCallback(async () => {
|
||||
// Restore parent tenant context
|
||||
await restoreParentTenant();
|
||||
|
||||
// Clear selected premise
|
||||
setSelectedPremise(null);
|
||||
}, [restoreParentTenant]);
|
||||
|
||||
// Filter configuration
|
||||
const filterConfig: FilterConfig[] = [
|
||||
{
|
||||
key: 'status',
|
||||
type: 'dropdown',
|
||||
label: t('premises:filters.status'),
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as 'active' | 'inactive' | ''),
|
||||
placeholder: t('premises:filters.all_statuses'),
|
||||
options: [
|
||||
{ value: 'active', label: t('premises:filters.active') },
|
||||
{ value: 'inactive', label: t('premises:filters.inactive') }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Get status indicator config for a premise
|
||||
const getPremiseStatusConfig = (isActive: boolean) => ({
|
||||
color: isActive ? getStatusColor('approved') : getStatusColor('cancelled'),
|
||||
text: isActive ? t('premises:card.status_active') : t('premises:card.status_inactive'),
|
||||
icon: isActive ? CheckCircle : XCircle,
|
||||
isCritical: false
|
||||
});
|
||||
|
||||
// Render tab content
|
||||
const renderTabContent = (tab: TabKey) => {
|
||||
const TabComponent = {
|
||||
procurement: ProcurementPage,
|
||||
production: ProductionPage,
|
||||
pos: POSPage,
|
||||
suppliers: SuppliersPage,
|
||||
inventory: InventoryPage,
|
||||
recipes: RecipesPage,
|
||||
orders: OrdersPage,
|
||||
machinery: MaquinariaPage,
|
||||
quality_templates: QualityTemplatesPage,
|
||||
team: TeamPage,
|
||||
ai_models: ModelsConfigPage,
|
||||
sustainability: SustainabilityPage,
|
||||
settings: BakerySettingsPage,
|
||||
}[tab];
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
}>
|
||||
<TabComponent />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
// Detail View
|
||||
if (selectedPremise) {
|
||||
const tabs: Array<{ key: TabKey; label: string; icon: React.ElementType }> = [
|
||||
{ key: 'procurement', label: t('premises:detail.tabs.procurement'), icon: ShoppingCart },
|
||||
{ key: 'production', label: t('premises:detail.tabs.production'), icon: Factory },
|
||||
{ key: 'pos', label: t('premises:detail.tabs.pos'), icon: Store },
|
||||
{ key: 'suppliers', label: t('premises:detail.tabs.suppliers'), icon: Users },
|
||||
{ key: 'inventory', label: t('premises:detail.tabs.inventory'), icon: Package },
|
||||
{ key: 'recipes', label: t('premises:detail.tabs.recipes'), icon: ChefHat },
|
||||
{ key: 'orders', label: t('premises:detail.tabs.orders'), icon: ClipboardList },
|
||||
{ key: 'machinery', label: t('premises:detail.tabs.machinery'), icon: Cog },
|
||||
{ key: 'quality_templates', label: t('premises:detail.tabs.quality_templates'), icon: ClipboardCheck },
|
||||
{ key: 'team', label: t('premises:detail.tabs.team'), icon: UserCircle },
|
||||
{ key: 'ai_models', label: t('premises:detail.tabs.ai_models'), icon: BrainCircuit },
|
||||
{ key: 'sustainability', label: t('premises:detail.tabs.sustainability'), icon: Leaf },
|
||||
{ key: 'settings', label: t('premises:detail.tabs.settings'), icon: Settings }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Back button and header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={handleBackToList}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
{t('premises:detail.back_to_list')}
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{selectedPremise.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)] overflow-x-auto">
|
||||
<div className="flex gap-1 -mb-px min-w-max px-2">
|
||||
{tabs.map(tab => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2
|
||||
transition-all duration-200 whitespace-nowrap rounded-t-lg
|
||||
${activeTab === tab.key
|
||||
? 'border-[var(--color-primary)] text-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||
: 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div>
|
||||
{renderTabContent(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// List View
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
title={t('premises:title')}
|
||||
description={t('premises:description')}
|
||||
/>
|
||||
|
||||
{/* Stats */}
|
||||
<StatsGrid
|
||||
columns={3}
|
||||
stats={[
|
||||
{
|
||||
title: t('premises:stats.total_premises'),
|
||||
value: stats?.total ?? 0,
|
||||
icon: Building2,
|
||||
variant: 'info'
|
||||
},
|
||||
{
|
||||
title: t('premises:stats.active_premises'),
|
||||
value: stats?.active ?? 0,
|
||||
icon: CheckCircle,
|
||||
variant: 'success'
|
||||
},
|
||||
{
|
||||
title: t('premises:stats.inactive_premises'),
|
||||
value: stats?.inactive ?? 0,
|
||||
icon: XCircle,
|
||||
variant: 'warning'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder={t('premises:search.placeholder')}
|
||||
filters={filterConfig}
|
||||
/>
|
||||
|
||||
{/* Premises Grid */}
|
||||
{isPremisesLoading || isStatsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-8 h-8 border-4 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin"></div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{t('premises:loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : premises.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Building2}
|
||||
title={t('premises:empty.title')}
|
||||
description={t('premises:empty.description')}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{premises.map((premise: TenantResponse) => (
|
||||
<StatusCard
|
||||
key={premise.id}
|
||||
id={premise.id}
|
||||
title={premise.name}
|
||||
subtitle={premise.address || premise.city}
|
||||
statusIndicator={getPremiseStatusConfig(premise.is_active)}
|
||||
primaryValue={premise.city || '-'}
|
||||
primaryValueLabel={t('premises:card.location')}
|
||||
metadata={[
|
||||
premise.business_type ? `${premise.business_type}` : '',
|
||||
premise.phone || ''
|
||||
].filter(Boolean)}
|
||||
actions={[
|
||||
{
|
||||
label: t('premises:card.more_details'),
|
||||
icon: Eye,
|
||||
onClick: () => handleSelectPremise(premise.id, premise.name),
|
||||
priority: 'primary'
|
||||
}
|
||||
]}
|
||||
onClick={() => handleSelectPremise(premise.id, premise.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PremisesPage;
|
||||
1
frontend/src/pages/app/enterprise/premises/index.ts
Normal file
1
frontend/src/pages/app/enterprise/premises/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './PremisesPage';
|
||||
@@ -36,6 +36,9 @@ const POSPage = React.lazy(() => import('../pages/app/operations/pos/POSPage'));
|
||||
const MaquinariaPage = React.lazy(() => import('../pages/app/operations/maquinaria/MaquinariaPage'));
|
||||
const DistributionPage = React.lazy(() => import('../pages/app/operations/distribution/DistributionPage'));
|
||||
|
||||
// Enterprise pages
|
||||
const PremisesPage = React.lazy(() => import('../pages/app/enterprise/premises/PremisesPage'));
|
||||
|
||||
// Analytics pages
|
||||
const ProductionAnalyticsPage = React.lazy(() => import('../pages/app/analytics/ProductionAnalyticsPage'));
|
||||
const ProcurementAnalyticsPage = React.lazy(() => import('../pages/app/analytics/ProcurementAnalyticsPage'));
|
||||
@@ -118,6 +121,18 @@ export const AppRouter: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Enterprise Routes - Multi-tenant management */}
|
||||
<Route
|
||||
path="/app/enterprise/premises"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<PremisesPage />
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Operations Routes - Business Operations Only */}
|
||||
<Route
|
||||
path="/app/operations/procurement"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthUser, useIsAuthenticated, useAuthLoading } from '../stores';
|
||||
import { useCurrentTenantAccess, useTenantPermissions } from '../stores/tenant.store';
|
||||
import { useCurrentTenant, useCurrentTenantAccess, useTenantPermissions } from '../stores/tenant.store';
|
||||
import { useHasAccess, useIsDemoMode } from '../hooks/useAccessControl';
|
||||
import { RouteConfig, canAccessRoute, ROUTES } from './routes.config';
|
||||
import { checkCombinedPermission, type User, type TenantAccess } from '../utils/permissions';
|
||||
@@ -135,6 +135,7 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
const user = useAuthUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const isLoading = useAuthLoading();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
const { hasPermission } = useTenantPermissions();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -111,6 +111,9 @@ export const ROUTES = {
|
||||
POS_WEBHOOKS: '/pos/webhooks',
|
||||
POS_SETTINGS: '/pos/settings',
|
||||
|
||||
// Enterprise
|
||||
PREMISES: '/app/enterprise/premises',
|
||||
|
||||
// Analytics
|
||||
ANALYTICS_EVENTS: '/app/analytics/events',
|
||||
|
||||
@@ -248,6 +251,22 @@ export const routesConfig: RouteConfig[] = [
|
||||
},
|
||||
},
|
||||
|
||||
// Enterprise Section - Multi-tenant management
|
||||
{
|
||||
path: ROUTES.PREMISES,
|
||||
name: 'Premises',
|
||||
component: 'PremisesPage',
|
||||
title: 'Locales',
|
||||
icon: 'premises',
|
||||
requiresAuth: true,
|
||||
requiredSubscriptionFeature: 'multi_tenant_management',
|
||||
showInNavigation: true,
|
||||
meta: {
|
||||
headerTitle: 'Locales',
|
||||
preload: false,
|
||||
},
|
||||
},
|
||||
|
||||
// Operations Section - Business Operations Only
|
||||
{
|
||||
path: '/app/operations',
|
||||
|
||||
@@ -7,21 +7,25 @@ import { TENANT_ROLES, GLOBAL_USER_ROLES } from '../types/roles';
|
||||
export interface TenantState {
|
||||
// State
|
||||
currentTenant: TenantResponse | null;
|
||||
parentTenant: TenantResponse | null; // Enterprise parent tenant for child premise navigation
|
||||
availableTenants: TenantResponse[] | null;
|
||||
currentTenantAccess: TenantAccessResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => void;
|
||||
setAvailableTenants: (tenants: TenantResponse[]) => void;
|
||||
switchTenant: (tenantId: string) => Promise<boolean>;
|
||||
switchToChildTenant: (childTenant: TenantResponse) => Promise<boolean>; // Switch to child while remembering parent
|
||||
restoreParentTenant: () => Promise<boolean>; // Restore parent tenant context
|
||||
loadUserTenants: () => Promise<void>;
|
||||
loadChildTenants: () => Promise<void>; // Load child tenants for enterprise users
|
||||
loadCurrentTenantAccess: () => 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;
|
||||
@@ -32,6 +36,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
currentTenant: null,
|
||||
parentTenant: null,
|
||||
availableTenants: null,
|
||||
currentTenantAccess: null,
|
||||
isLoading: false,
|
||||
@@ -55,15 +60,15 @@ export const useTenantStore = create<TenantState>()(
|
||||
switchTenant: async (tenantId: string): Promise<boolean> => {
|
||||
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 });
|
||||
@@ -77,6 +82,111 @@ export const useTenantStore = create<TenantState>()(
|
||||
}
|
||||
},
|
||||
|
||||
switchToChildTenant: async (childTenant: TenantResponse): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const { currentTenant, availableTenants } = get();
|
||||
|
||||
console.log('[Tenant Store] Switching to child tenant:', {
|
||||
from: currentTenant?.id,
|
||||
fromName: currentTenant?.name,
|
||||
to: childTenant.id,
|
||||
toName: childTenant.name
|
||||
});
|
||||
|
||||
// Store current tenant as parent (for enterprise users navigating to child premises)
|
||||
if (currentTenant) {
|
||||
set({ parentTenant: currentTenant });
|
||||
console.log('[Tenant Store] Stored parent tenant:', currentTenant.id);
|
||||
}
|
||||
|
||||
// Add child to availableTenants if not already present
|
||||
if (availableTenants && !availableTenants.find(t => t.id === childTenant.id)) {
|
||||
set({ availableTenants: [...availableTenants, childTenant] });
|
||||
}
|
||||
|
||||
// CRITICAL: Directly update API client BEFORE updating state
|
||||
console.log('[Tenant Store] Directly updating API client with child tenant ID:', childTenant.id);
|
||||
const { apiClient } = await import('../api/client');
|
||||
apiClient.setTenantId(childTenant.id);
|
||||
|
||||
// Verify the API client was updated
|
||||
const verifiedTenantId = apiClient.getTenantId();
|
||||
console.log('[Tenant Store] Verified API client tenant ID:', verifiedTenantId);
|
||||
|
||||
if (verifiedTenantId !== childTenant.id) {
|
||||
console.error('[Tenant Store] API client tenant ID mismatch! Expected:', childTenant.id, 'Got:', verifiedTenantId);
|
||||
set({ isLoading: false, error: 'Failed to update API client tenant ID' });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now update the store state
|
||||
// IMPORTANT: We've already updated the API client above, so we just update the state
|
||||
// We DON'T call setCurrentTenant action because that would trigger tenantService.setCurrentTenant
|
||||
// which would call apiClient.setTenantId again
|
||||
set({ currentTenant: childTenant, currentTenantAccess: null });
|
||||
|
||||
console.log('[Tenant Store] Switch complete. Current tenant is now:', get().currentTenant?.id);
|
||||
console.log('[Tenant Store] Final API client tenant ID verification:', apiClient.getTenantId());
|
||||
|
||||
set({ isLoading: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Tenant Store] Failed to switch to child tenant:', error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to switch to child tenant',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
restoreParentTenant: async (): Promise<boolean> => {
|
||||
try {
|
||||
const { parentTenant } = get();
|
||||
|
||||
if (!parentTenant) {
|
||||
console.warn('[Tenant Store] No parent tenant to restore');
|
||||
return false;
|
||||
}
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
console.log('[Tenant Store] Restoring parent tenant:', parentTenant.id, parentTenant.name);
|
||||
|
||||
// CRITICAL: Directly update API client BEFORE updating state
|
||||
const { apiClient } = await import('../api/client');
|
||||
apiClient.setTenantId(parentTenant.id);
|
||||
|
||||
// Verify the API client was updated
|
||||
const verifiedTenantId = apiClient.getTenantId();
|
||||
console.log('[Tenant Store] Verified API client tenant ID after restore:', verifiedTenantId);
|
||||
|
||||
if (verifiedTenantId !== parentTenant.id) {
|
||||
console.error('[Tenant Store] API client tenant ID mismatch! Expected:', parentTenant.id, 'Got:', verifiedTenantId);
|
||||
set({ isLoading: false, error: 'Failed to update API client tenant ID' });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now update the store state
|
||||
set({ currentTenant: parentTenant, currentTenantAccess: null });
|
||||
|
||||
// Clear parent tenant reference
|
||||
set({ parentTenant: null, isLoading: false });
|
||||
|
||||
console.log('[Tenant Store] Parent tenant restored successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Tenant Store] Failed to restore parent tenant:', error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to restore parent tenant',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
loadUserTenants: async (): Promise<void> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
@@ -129,6 +239,35 @@ export const useTenantStore = create<TenantState>()(
|
||||
}
|
||||
},
|
||||
|
||||
loadChildTenants: async (): Promise<void> => {
|
||||
try {
|
||||
const { currentTenant, availableTenants } = get();
|
||||
|
||||
if (!currentTenant) {
|
||||
console.warn('No current tenant to load children for');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch child tenants
|
||||
const children = await tenantService.getChildTenants(currentTenant.id);
|
||||
|
||||
if (children && children.length > 0) {
|
||||
// Add child tenants to availableTenants if not already present
|
||||
const currentAvailable = availableTenants || [];
|
||||
const newTenants = children.filter(
|
||||
child => !currentAvailable.find(t => t.id === child.id)
|
||||
);
|
||||
|
||||
if (newTenants.length > 0) {
|
||||
set({ availableTenants: [...currentAvailable, ...newTenants] });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load child tenants:', error);
|
||||
// Don't set error state - this is optional enhancement
|
||||
}
|
||||
},
|
||||
|
||||
loadCurrentTenantAccess: async (): Promise<void> => {
|
||||
try {
|
||||
const { currentTenant } = get();
|
||||
@@ -146,6 +285,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
clearTenants: () => {
|
||||
set({
|
||||
currentTenant: null,
|
||||
parentTenant: null,
|
||||
availableTenants: null,
|
||||
currentTenantAccess: null,
|
||||
error: null,
|
||||
@@ -213,6 +353,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
currentTenant: state.currentTenant,
|
||||
parentTenant: state.parentTenant,
|
||||
availableTenants: state.availableTenants,
|
||||
currentTenantAccess: state.currentTenantAccess,
|
||||
}),
|
||||
@@ -231,6 +372,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
// 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 useParentTenant = () => useTenantStore((state) => state.parentTenant);
|
||||
export const useAvailableTenants = () => useTenantStore((state) => state.availableTenants);
|
||||
export const useCurrentTenantAccess = () => useTenantStore((state) => state.currentTenantAccess);
|
||||
export const useTenantLoading = () => useTenantStore((state) => state.isLoading);
|
||||
@@ -241,7 +383,10 @@ export const useTenantActions = () => useTenantStore((state) => ({
|
||||
setCurrentTenant: state.setCurrentTenant,
|
||||
setAvailableTenants: state.setAvailableTenants,
|
||||
switchTenant: state.switchTenant,
|
||||
switchToChildTenant: state.switchToChildTenant,
|
||||
restoreParentTenant: state.restoreParentTenant,
|
||||
loadUserTenants: state.loadUserTenants,
|
||||
loadChildTenants: state.loadChildTenants,
|
||||
loadCurrentTenantAccess: state.loadCurrentTenantAccess,
|
||||
clearTenants: state.clearTenants,
|
||||
clearError: state.clearError,
|
||||
@@ -257,15 +402,17 @@ export const useTenantPermissions = () => useTenantStore((state) => ({
|
||||
// Combined hook for convenience
|
||||
export const useTenant = () => {
|
||||
const currentTenant = useCurrentTenant();
|
||||
const parentTenant = useParentTenant();
|
||||
const availableTenants = useAvailableTenants();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
const isLoading = useTenantLoading();
|
||||
const error = useTenantError();
|
||||
const actions = useTenantActions();
|
||||
const permissions = useTenantPermissions();
|
||||
|
||||
|
||||
return {
|
||||
currentTenant,
|
||||
parentTenant,
|
||||
availableTenants,
|
||||
currentTenantAccess,
|
||||
isLoading,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useIsAuthenticated } from './auth.store';
|
||||
import { useTenantActions, useAvailableTenants, useCurrentTenant } from './tenant.store';
|
||||
import { useTenantActions, useAvailableTenants, useCurrentTenant, useParentTenant } from './tenant.store';
|
||||
import { useIsDemoMode, useDemoSessionId, useDemoAccountType } from '../hooks/useAccessControl';
|
||||
import { useSubscription } from '../api/hooks/subscription';
|
||||
import { SUBSCRIPTION_TIERS, SubscriptionTier } from '../api/types/subscription';
|
||||
|
||||
/**
|
||||
@@ -54,7 +55,9 @@ export const useTenantInitializer = () => {
|
||||
const demoAccountType = useDemoAccountType();
|
||||
const availableTenants = useAvailableTenants();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { loadUserTenants, setCurrentTenant, setAvailableTenants } = useTenantActions();
|
||||
const parentTenant = useParentTenant(); // Track if we're viewing a child tenant
|
||||
const { loadUserTenants, loadChildTenants, setCurrentTenant, setAvailableTenants } = useTenantActions();
|
||||
const { subscriptionInfo } = useSubscription();
|
||||
|
||||
// Load tenants for authenticated users (but not demo users - they have special initialization below)
|
||||
useEffect(() => {
|
||||
@@ -63,6 +66,25 @@ export const useTenantInitializer = () => {
|
||||
}
|
||||
}, [isAuthenticated, availableTenants, loadUserTenants, isDemoMode]);
|
||||
|
||||
// Load child tenants for enterprise users
|
||||
useEffect(() => {
|
||||
if (
|
||||
isAuthenticated &&
|
||||
!isDemoMode &&
|
||||
currentTenant &&
|
||||
availableTenants &&
|
||||
subscriptionInfo?.plan === 'enterprise'
|
||||
) {
|
||||
// Only load if we haven't loaded child tenants yet
|
||||
// Check if availableTenants only contains the parent (length === 1)
|
||||
const hasOnlyParent = availableTenants.length === 1 && availableTenants[0].id === currentTenant.id;
|
||||
|
||||
if (hasOnlyParent) {
|
||||
loadChildTenants();
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, isDemoMode, currentTenant, availableTenants, subscriptionInfo, loadChildTenants]);
|
||||
|
||||
// Set up mock tenant for demo mode with appropriate subscription tier
|
||||
useEffect(() => {
|
||||
if (isDemoMode && demoSessionId) {
|
||||
@@ -106,8 +128,10 @@ export const useTenantInitializer = () => {
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Only set current tenant if not already valid
|
||||
if (!isValidDemoTenant) {
|
||||
// Only set current tenant if not already valid AND we're not viewing a child tenant
|
||||
// CRITICAL: If parentTenant exists, it means we're viewing a child tenant from the Premises page
|
||||
// and we should NOT overwrite the current tenant
|
||||
if (!isValidDemoTenant && !parentTenant) {
|
||||
// Set the demo tenant as current
|
||||
setCurrentTenant(mockTenant);
|
||||
|
||||
@@ -155,5 +179,5 @@ export const useTenantInitializer = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, setCurrentTenant, setAvailableTenants]);
|
||||
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, parentTenant, setCurrentTenant, setAvailableTenants]);
|
||||
};
|
||||
Reference in New Issue
Block a user