Improve enterprise tier child tenants access

This commit is contained in:
Urtzi Alfaro
2026-01-07 16:01:19 +01:00
parent 2c1fc756a1
commit 560c7ba86f
19 changed files with 854 additions and 15 deletions

View 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;

View File

@@ -0,0 +1 @@
export { default } from './PremisesPage';