Improve enterprise tier child tenants access
This commit is contained in:
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';
|
||||
Reference in New Issue
Block a user