// (c) 2026 TWWIM UG. All rights reserved. (www.twwim.com) import { useEffect, useMemo, useState } from 'react'; import { ArrowLeft, Globe, Database, FileText, HelpCircle, Layout, FlaskConical } from 'lucide-react'; import { Link } from '@tanstack/react-router'; import { useTenant, useTenants } from './hooks'; import { KnowledgeTab } from '@/features/knowledge/components/KnowledgeTab'; import { PlaygroundTab } from '@/features/knowledge/components/PlaygroundTab'; import { useKnowledgeStats } from '@/features/knowledge/hooks'; import { useTranslation } from '@/i18n/TranslationProvider'; import { tokenStorage } from '@/infrastructure/storage/LocalTokenStorage'; import { isWordpressEmbed, isWooCommerceActive, getWpDomain } from '@/lib/wp-integration'; // Eager imports — both small enough that bundle-size wins from lazy are // negligible, and bundling them into the main chunk avoids React-instance // mismatch errors (#321) that surface when other WP plugins (WPBakery / // RevSlider) load their own React on the same wp-admin page and a Suspense- // resolved chunk picks up the wrong copy. Standalone CP build never renders // WOO_TAB (showWooTab is false) so the unused imports are tree-shaken. import { WooCommerceIcon } from './components/WooCommerceIcon'; import WooCommerceSyncTab from './components/WooCommerceSyncTab'; type TabId = 'DOCUMENT' | 'FAQ' | 'PLAYGROUND' | 'WOO'; interface Tab { id: TabId; label: string; icon: React.ReactNode; } const BASE_TABS: Tab[] = [ { id: 'DOCUMENT', label: 'Documents', icon: }, { id: 'FAQ', label: 'FAQ', icon: }, { id: 'PLAYGROUND', label: 'Playground', icon: }, ]; /** Pre-computed at module load — true only in WP plugin build with WC active. */ const WP_EMBED = isWordpressEmbed() && isWooCommerceActive(); export type TenantPageVariant = 'detail' | 'knowledge'; interface TenantPageProps { tenantId?: string; variant?: TenantPageVariant; onTenantChange?: (tenantId: string) => void; } export function TenantPage({ tenantId, variant = 'detail', onTenantChange }: TenantPageProps) { const { t } = useTranslation(); const isKnowledge = variant === 'knowledge'; const tenantsQuery = useTenants(); const tenants = useMemo(() => tenantsQuery.data?.tenants ?? [], [tenantsQuery.data?.tenants]); const resolvedTenantId = useMemo(() => { if (!isKnowledge) return tenantId; if (tenantId && tenants.some((tn) => tn.id === tenantId)) return tenantId; return tenants[0]?.id; }, [isKnowledge, tenantId, tenants]); useEffect(() => { if (!isKnowledge || !onTenantChange) return; if (resolvedTenantId && resolvedTenantId !== tenantId) { onTenantChange(resolvedTenantId); } }, [isKnowledge, onTenantChange, resolvedTenantId, tenantId]); const { data: tenant, isLoading, error } = useTenant(resolvedTenantId ?? ''); const { data: stats } = useKnowledgeStats(resolvedTenantId ?? ''); const [activeTab, setActiveTab] = useState('DOCUMENT'); // WooCommerce tab is only meaningful for the tenant matching the current // WP site domain. Switching tenants (Knowledge variant has a dropdown) // can hide the tab — fall back to the Documents tab if it disappears // while selected. const showWooTab = WP_EMBED && tenant?.domain === getWpDomain(); const tabs = useMemo(() => { if (!showWooTab) return BASE_TABS; return [ ...BASE_TABS, { id: 'WOO', label: 'WooCommerce', icon: }, ]; }, [showWooTab]); useEffect(() => { if (activeTab === 'WOO' && !showWooTab) setActiveTab('DOCUMENT'); }, [activeTab, showWooTab]); const listLoading = isKnowledge && tenantsQuery.isLoading; const noTenants = isKnowledge && !tenantsQuery.isLoading && tenants.length === 0; if (listLoading || (resolvedTenantId && isLoading)) { return (
); } if (noTenants) { return (

{t('knowledge.noTenantsTitle')}

{t('knowledge.noTenantsHint')}

{t('knowledge.goToTenants')}
); } if (error || !tenant) { return (

{error?.message || t('knowledge.tenantNotFound')}

{!isKnowledge && ( {t('knowledge.backToTenants')} )}
); } const productCount = stats?.byType?.PRODUCT ?? 0; const pageCount = stats?.byType?.PAGE ?? 0; const documentCount = stats?.byType?.DOCUMENT ?? 0; const documentsCapRaw = tokenStorage.getCapability('knowledge_files_max'); const documentsCap = documentsCapRaw ? parseInt(documentsCapRaw, 10) : 0; const documentsRemaining = Math.max(0, documentsCap - documentCount); return (
{/* Header */}
{!isKnowledge && ( )} {isKnowledge ? (
{tenant.domain} {tenant.status}
) : (

{tenant.name}

{tenant.domain} {tenant.status}
)}
{/* Synced-content summary cards */}
} label={t('knowledge.productsCard')} hint={t('knowledge.productsCardHint')} count={productCount} /> } label={t('knowledge.pagesCard')} hint={t('knowledge.pagesCardHint')} count={pageCount} /> } label={t('knowledge.documentsQuotaCard')} used={documentCount} cap={documentsCap} usedLabel={t('knowledge.quotaUsed')} leftLabel={t('knowledge.quotaLeft')} remaining={documentsRemaining} />
{/* Tab bar */}
{/* Tab content */} {activeTab === 'WOO' ? ( ) : activeTab === 'PLAYGROUND' ? ( ) : ( )}
); } function StatCard({ icon, label, hint, count, }: { icon: React.ReactNode; label: string; hint: string; count: number; }) { return (
{icon}
{count.toLocaleString()} {label}

{hint}

); } function QuotaCard({ icon, label, used, cap, usedLabel, leftLabel, remaining, }: { icon: React.ReactNode; label: string; used: number; cap: number; usedLabel: string; leftLabel: string; remaining: number; }) { const pct = cap > 0 ? Math.min(100, Math.round((used / cap) * 100)) : 0; const barColor = pct >= 100 ? 'bg-red-500' : pct >= 80 ? 'bg-amber-500' : 'bg-cyan-500'; return (
{icon}
{label} {used.toLocaleString()} / {cap.toLocaleString()}
{usedLabel}: {used.toLocaleString()} {leftLabel}: {remaining.toLocaleString()}
); }