/** * TenantsList Component * * Tenant list with read-only view and creation. * Tenants are immutable after creation — no edit or delete. * * @component * @layer Presentation */ import { useState, useMemo } from 'react'; import { Search, Plus, Building2, Globe, Key, AlertTriangle, Database, Megaphone } from 'lucide-react'; import { Link } from '@tanstack/react-router'; import { TenantEdit } from './TenantEdit'; import type { TenantCreateResult } from './TenantEdit'; import { TenantDetail } from './TenantDetail'; import { Tenant, TenantStatus } from '@/domain/entities/Tenant'; import { useTenants } from './hooks'; import { tokenStorage } from '@/infrastructure/storage/LocalTokenStorage'; import { useTranslation } from '@/i18n/TranslationProvider'; import { wpAutoSaveKey } from '@/lib/wp-integration'; // WooCommerce sync UI lives in the Knowledge page tab bar (TenantPage WOO // tab) — see apps/dashboard/src/features/tenants/components/WooCommerceSyncTab.tsx. // The standalone modal that used to live here was removed when the tab was // added; one entry point is enough. // Import status configs for UI (these should remain in types for presentation layer) const TENANT_STATUS_CONFIG = { [TenantStatus.ACTIVE]: { labelKey: 'common.active', color: 'green' as const }, [TenantStatus.SUSPENDED]: { labelKey: 'common.suspended', color: 'yellow' as const }, [TenantStatus.ARCHIVED]: { labelKey: 'common.archived', color: 'gray' as const }, }; /** * Status Badge Component */ function StatusBadge({ status }: { status: TenantStatus }) { const { t } = useTranslation(); const config = TENANT_STATUS_CONFIG[status]; const colorClasses = { green: 'bg-green-100 text-green-800 border-green-200', yellow: 'bg-yellow-100 text-yellow-800 border-yellow-200', gray: 'bg-gray-100 text-gray-700 border-gray-200', }; return ( {t(config.labelKey)} ); } /** * TenantsList Component */ export function TenantsList() { const { t } = useTranslation(); // Use TanStack Query hooks for tenant data const { data, isLoading, error, refetch } = useTenants(); // Extract tenants from query result const tenants = data?.tenants || []; // Check integrations capability limit const integrationsLimit = tokenStorage.getCapability('integrations'); const isUnlimited = integrationsLimit === 'unlimited'; const maxTenants = integrationsLimit && !isUnlimited ? parseInt(integrationsLimit, 10) : null; const atLimit = maxTenants !== null && tenants.length >= maxTenants; // UI State const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('ALL'); const [isCreating, setIsCreating] = useState(false); const [viewingTenant, setViewingTenant] = useState(null); // Filtered tenants using domain entity methods const filteredTenants = useMemo(() => { return tenants.filter((tenant) => { // Search filter using domain entity getters const matchesSearch = searchQuery === '' || tenant.name.toLowerCase().includes(searchQuery.toLowerCase()) || tenant.domain.toLowerCase().includes(searchQuery.toLowerCase()); // Status filter using domain entity getters const matchesStatus = statusFilter === 'ALL' || tenant.status === statusFilter; return matchesSearch && matchesStatus; }); }, [tenants, searchQuery, statusFilter]); // Handlers const handleCreateTenant = () => { setIsCreating(true); }; const handleSaveComplete = (result?: TenantCreateResult) => { refetch(); setIsCreating(false); // After creation, auto-open TenantDetail if (result) { setViewingTenant(result.tenant); // WP embed: auto-save production key to wp_options if domain matches if (result.productionKey) { wpAutoSaveKey(result.tenant.domain, result.productionKey); } } }; const handleCancelEdit = () => { setIsCreating(false); }; // Loading state if (isLoading) { return (
); } // Error state if (error) { return (

{t('tenants.errorLoading', { error: error.message })}

); } return (
{/* Filters and Search */}
{/* Search and Create Button Row */}
{/* Search Input */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
{/* Create Button */}
{/* Limit Warning */} {atLimit && (
{t('tenants.limitWarning', { max: maxTenants, label: maxTenants === 1 ? t('tenants.website') : t('tenants.websites') })}
)} {/* Filters Row */}
{/* Status Filter */}
{/* Results Count */}
{filteredTenants.length} {filteredTenants.length === 1 ? t('tenants.tenant') : t('tenants.tenants')}
{/* Tenants Table or Empty State */} {filteredTenants.length === 0 ? (

{searchQuery || statusFilter !== 'ALL' ? t('tenants.noTenantsFound') : t('tenants.noTenantsYet')}

{searchQuery || statusFilter !== 'ALL' ? t('tenants.adjustFilters') : t('tenants.getStarted')}

{!searchQuery && statusFilter === 'ALL' && !atLimit && ( )} {!searchQuery && statusFilter === 'ALL' && atLimit && (

{t('tenants.limitWarningShort', { max: maxTenants })}

)}
) : (
{filteredTenants.map((tenant) => ( ))}
{t('common.name')} {t('common.domain')} {t('common.status')} {t('common.actions')}
{tenant.name} {tenant.domain}
)} {/* Create Modal */} {isCreating && ( )} {/* Tenant Detail Modal */} {viewingTenant && ( { setViewingTenant(null); }} onUpdate={refetch} /> )}
); }