@burdenoff/microfe-admin 2026.829.1 → 2026.829.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantMembersPanel } from '../components/TenantMembersPanel';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { PagePurpose, NextSteps, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { tWithFallback } from '../utils/i18n';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const managementFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const detailFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.detail, true);\n const editFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.edit, true);\n const pageFeatureEnabled = managementFeatureEnabled && detailFeatureEnabled;\n const canWriteTenants = canManageTenants && editFeatureEnabled;\n const canLifecycleTenants = canManageTenants && managementFeatureEnabled;\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n if (!permissionsLoading && !canViewTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {null}\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={tr('admin.tenantDetail.featureDisabledTitle', 'Tenant management unavailable')}\n description={tr(\n 'admin.tenantDetail.featureDisabled',\n 'This feature is currently disabled.'\n )}\n >\n {null}\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canWriteTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canLifecycleTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-status-warning-text\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-status-success-text\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.tenantDetail.purpose',\n 'Everything about a single tenant in one place — their profile, the people inside it, and their single sign-on setup. From here you manage that one customer account: review who has access, adjust their SSO, and suspend or reactivate them when needed.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey={`admin-tenant-detail`}\n steps={[\n {\n id: 'edit-tenant',\n label: tr('admin.tenantDetail.nextSteps.edit', 'Edit tenant details'),\n description: tr(\n 'admin.tenantDetail.nextSteps.editDesc',\n 'Update the name, domain or plan for this tenant.'\n ),\n onClick: () => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`)),\n done: !canWriteTenants,\n },\n {\n id: 'review-members',\n label: tr('admin.tenantDetail.nextSteps.members', 'Review members'),\n description: tr(\n 'admin.tenantDetail.nextSteps.membersDesc',\n 'Check who has access inside this tenant.'\n ),\n href: '#tenant-members',\n },\n {\n id: 'configure-sso',\n label: tr('admin.tenantDetail.nextSteps.sso', 'Configure SSO'),\n description: tr(\n 'admin.tenantDetail.nextSteps.ssoDesc',\n 'Set up single sign-on so their team logs in with your IdP.'\n ),\n href: '#tenant-sso',\n },\n ]}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Overview — the primary emphasis zone for this page. */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-text-primary\">{tenant.name}</h2>\n <p className=\"text-sm text-text-secondary\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-action-primary-bg/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <div id=\"tenant-sso\">\n <TenantSsoPanel tenantId={tenant.id} canManage={canWriteTenants} />\n </div>\n\n {/* Tenant Members (BOFF-2652) — gates createOrganization on DEDICATED tenants */}\n <div id=\"tenant-members\">\n <TenantMembersPanel\n tenantId={tenant.id}\n tenantType={tenant.type ?? ''}\n canManage={canWriteTenants}\n />\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantMembersPanel } from '../components/TenantMembersPanel';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { PagePurpose, NextSteps, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useTr } from '../hooks/useTr';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const tr = useTr();\n const managementFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const detailFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.detail, true);\n const editFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.edit, true);\n const pageFeatureEnabled = managementFeatureEnabled && detailFeatureEnabled;\n const canWriteTenants = canManageTenants && editFeatureEnabled;\n const canLifecycleTenants = canManageTenants && managementFeatureEnabled;\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n if (!permissionsLoading && !canViewTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {null}\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={tr('admin.tenantDetail.featureDisabledTitle', 'Tenant management unavailable')}\n description={tr(\n 'admin.tenantDetail.featureDisabled',\n 'This feature is currently disabled.'\n )}\n >\n {null}\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canWriteTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canLifecycleTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-status-warning-text\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-status-success-text\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.tenantDetail.purpose',\n 'Everything about a single tenant in one place — their profile, the people inside it, and their single sign-on setup. From here you manage that one customer account: review who has access, adjust their SSO, and suspend or reactivate them when needed.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey={`admin-tenant-detail`}\n steps={[\n {\n id: 'edit-tenant',\n label: tr('admin.tenantDetail.nextSteps.edit', 'Edit tenant details'),\n description: tr(\n 'admin.tenantDetail.nextSteps.editDesc',\n 'Update the name, domain or plan for this tenant.'\n ),\n onClick: () => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`)),\n done: !canWriteTenants,\n },\n {\n id: 'review-members',\n label: tr('admin.tenantDetail.nextSteps.members', 'Review members'),\n description: tr(\n 'admin.tenantDetail.nextSteps.membersDesc',\n 'Check who has access inside this tenant.'\n ),\n href: '#tenant-members',\n },\n {\n id: 'configure-sso',\n label: tr('admin.tenantDetail.nextSteps.sso', 'Configure SSO'),\n description: tr(\n 'admin.tenantDetail.nextSteps.ssoDesc',\n 'Set up single sign-on so their team logs in with your IdP.'\n ),\n href: '#tenant-sso',\n },\n ]}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Overview — the primary emphasis zone for this page. */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-text-primary\">{tenant.name}</h2>\n <p className=\"text-sm text-text-secondary\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-action-primary-bg/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <div id=\"tenant-sso\">\n <TenantSsoPanel tenantId={tenant.id} canManage={canWriteTenants} />\n </div>\n\n {/* Tenant Members (BOFF-2652) — gates createOrganization on DEDICATED tenants */}\n <div id=\"tenant-members\">\n <TenantMembersPanel\n tenantId={tenant.id}\n tenantType={tenant.type ?? ''}\n canManage={canWriteTenants}\n />\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -5,30 +5,29 @@ import { Avatar as r, Button as i, EmptyState as ee, Input as te } from "../comp
5
5
  import { PageLayout as a } from "../components/PageLayout.js";
6
6
  import { TenantStatusBadge as ne } from "../components/StatusBadge.js";
7
7
  import { formatBytes as re, formatDate as ie, formatNumber as o } from "../utils/formatters.js";
8
- import { DataTable as ae, Pagination as oe } from "../components/DataTable.js";
9
- import { tWithFallback as s } from "../utils/i18n.js";
10
- import { TenantForm as se } from "../components/forms/TenantForm.js";
11
- import { RowActions as ce, createRowActions as le } from "../components/RowActions.js";
12
- import { PermissionGuard as c } from "../components/PermissionGuard.js";
8
+ import { DataTable as ae, Pagination as s } from "../components/DataTable.js";
9
+ import { TenantForm as c } from "../components/forms/TenantForm.js";
10
+ import { RowActions as oe, createRowActions as l } from "../components/RowActions.js";
11
+ import { PermissionGuard as u } from "../components/PermissionGuard.js";
13
12
  import "../components/index.js";
14
- import { joinPath as l } from "../utils/navigation.js";
15
- import { mapTenantToUi as u } from "../utils/mappers.js";
16
- import { useActivateTenantMutation as ue, useCreateTenantMutation as de, useListTenantsQuery as fe, useSuspendTenantMutation as pe, useUpdateTenantMutation as me } from "../../generated/global-operations.js";
17
- import { useAdminStore as he } from "../store/adminStore.js";
18
- import { emitTenantLifecycle as d } from "../utils/tenantEvents.js";
19
- import { TENANT_FEATURE_FLAGS as f } from "../constants/featureFlags.js";
20
- import { useCallback as p, useMemo as m, useState as h } from "react";
21
- import { Building2 as g, Plus as ge, Search as _ } from "lucide-react";
22
- import { useNavigate as v } from "react-router";
13
+ import { joinPath as d } from "../utils/navigation.js";
14
+ import { mapTenantToUi as se } from "../utils/mappers.js";
15
+ import { useActivateTenantMutation as ce, useCreateTenantMutation as le, useListTenantsQuery as ue, useSuspendTenantMutation as de, useUpdateTenantMutation as fe } from "../../generated/global-operations.js";
16
+ import { useTr as pe } from "../hooks/useTr.js";
17
+ import { useAdminStore as me } from "../store/adminStore.js";
18
+ import { emitTenantLifecycle as f } from "../utils/tenantEvents.js";
19
+ import { TENANT_FEATURE_FLAGS as p } from "../constants/featureFlags.js";
20
+ import { useCallback as m, useMemo as h, useState as g } from "react";
21
+ import { Building2 as _, Plus as he, Search as v } from "lucide-react";
22
+ import { useNavigate as ge } from "react-router";
23
23
  import { jsx as y, jsxs as b } from "react/jsx-runtime";
24
24
  import { useFeatureFlag as x } from "@burdenoff/fe-libs/shared/feature-flags";
25
- import { useI18n as _e } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
26
- import { EmphasisPanel as ve, PagePurpose as ye } from "@burdenoff/fe-libs/ui";
27
- import { useEventBus as be } from "@burdenoff/fe-libs/shared/events";
25
+ import { EmphasisPanel as _e, PagePurpose as ve } from "@burdenoff/fe-libs/ui";
26
+ import { useEventBus as ye } from "@burdenoff/fe-libs/shared/events";
28
27
  //#region src/admin/pages/TenantsListPage.tsx
29
28
  var S = 20;
30
29
  function C() {
31
- let { canViewTenants: C, canManageTenants: w, isLoading: T } = n(), E = v(), { basePath: D = "" } = t(), { tenantFilter: O, updateTenantFilter: k, resetTenantFilter: A } = he(), j = e(), { t: M } = _e(), N = x(f.management, !0), xe = x(f.list, !0), Se = x(f.create, !0), P = x(f.edit, !0), F = be(), I = p((e, t, n) => s(M, e, t, n), [M]), [L, R] = h(1), [Ce, z] = h(!1), [we, B] = h(void 0), Te = m(() => {
30
+ let { canViewTenants: C, canManageTenants: w, isLoading: T } = n(), E = ge(), { basePath: D = "" } = t(), { tenantFilter: O, updateTenantFilter: k, resetTenantFilter: A } = me(), j = e(), M = pe(), N = x(p.management, !0), P = x(p.list, !0), be = x(p.create, !0), F = x(p.edit, !0), I = ye(), [L, R] = g(1), [xe, z] = g(!1), [Se, B] = g(void 0), Ce = h(() => {
32
31
  let e = {};
33
32
  O.search && (e.search = O.search);
34
33
  let t = {
@@ -37,57 +36,57 @@ function C() {
37
36
  deleted: "DELETED"
38
37
  };
39
38
  return O.status !== "all" && t[O.status] && (e.status = t[O.status]), e;
40
- }, [O.search, O.status]), V = N && xe, H = w && N, Ee = T || !C || !V, { data: U, loading: De, error: W, refetch: G } = fe({
39
+ }, [O.search, O.status]), V = N && P, H = w && N, we = T || !C || !V, { data: U, loading: Te, error: W, refetch: G } = ue({
41
40
  variables: {
42
- filter: Te,
41
+ filter: Ce,
43
42
  limit: S,
44
43
  offset: (L - 1) * S
45
44
  },
46
45
  fetchPolicy: "cache-and-network",
47
- skip: Ee
48
- }), [K, { loading: Oe }] = de({ onCompleted: () => {
46
+ skip: we
47
+ }), [K, { loading: Ee }] = le({ onCompleted: () => {
49
48
  G();
50
- } }), [q, { loading: ke }] = me({ onCompleted: () => {
49
+ } }), [q, { loading: De }] = fe({ onCompleted: () => {
51
50
  G();
52
- } }), [J] = pe({ onCompleted: () => {
51
+ } }), [J] = de({ onCompleted: () => {
53
52
  G();
54
- } }), [Y] = ue({ onCompleted: () => {
53
+ } }), [Y] = ce({ onCompleted: () => {
55
54
  G();
56
- } }), Ae = m(() => (U?.tenants?.items ?? []).map(u), [U]), X = U?.tenants?.total ?? 0, je = Math.ceil(X / S), Me = p(() => {
55
+ } }), Oe = h(() => (U?.tenants?.items ?? []).map(se), [U]), X = U?.tenants?.total ?? 0, ke = Math.ceil(X / S), Ae = m(() => {
57
56
  B(void 0), z(!0);
58
- }, []), Z = p((e) => {
57
+ }, []), Z = m((e) => {
59
58
  B(e), z(!0);
60
- }, []), Q = p(async (e) => {
59
+ }, []), Q = m(async (e) => {
61
60
  try {
62
- await J({ variables: { id: e.id } }), d(F, "tenant.tenant.suspended", {
61
+ await J({ variables: { id: e.id } }), f(I, "tenant.tenant.suspended", {
63
62
  tenantId: e.id,
64
63
  tenantSlug: e.slug,
65
64
  status: "SUSPENDED"
66
- }), j.success(I("admin.tenants.toast.suspendedTitle", "Tenant suspended"), I("admin.tenants.toast.suspendedDescription", "{{name}} has been suspended.", { name: e.name }));
65
+ }), j.success(M("admin.tenants.toast.suspendedTitle", "Tenant suspended"), M("admin.tenants.toast.suspendedDescription", "{{name}} has been suspended.", { name: e.name }));
67
66
  } catch {
68
- j.error(I("admin.tenants.toast.errorTitle", "Error"), I("admin.tenants.toast.suspendFailed", "Failed to suspend tenant."));
67
+ j.error(M("admin.tenants.toast.errorTitle", "Error"), M("admin.tenants.toast.suspendFailed", "Failed to suspend tenant."));
69
68
  }
70
69
  }, [
71
70
  J,
72
71
  j,
73
- I,
74
- F
75
- ]), $ = p(async (e) => {
72
+ M,
73
+ I
74
+ ]), $ = m(async (e) => {
76
75
  try {
77
- await Y({ variables: { id: e.id } }), d(F, "tenant.tenant.activated", {
76
+ await Y({ variables: { id: e.id } }), f(I, "tenant.tenant.activated", {
78
77
  tenantId: e.id,
79
78
  tenantSlug: e.slug,
80
79
  status: "ACTIVE"
81
- }), j.success(I("admin.tenants.toast.activatedTitle", "Tenant activated"), I("admin.tenants.toast.activatedDescription", "{{name}} has been activated.", { name: e.name }));
80
+ }), j.success(M("admin.tenants.toast.activatedTitle", "Tenant activated"), M("admin.tenants.toast.activatedDescription", "{{name}} has been activated.", { name: e.name }));
82
81
  } catch {
83
- j.error(I("admin.tenants.toast.errorTitle", "Error"), I("admin.tenants.toast.activateFailed", "Failed to activate tenant."));
82
+ j.error(M("admin.tenants.toast.errorTitle", "Error"), M("admin.tenants.toast.activateFailed", "Failed to activate tenant."));
84
83
  }
85
84
  }, [
86
85
  Y,
87
86
  j,
88
- I,
89
- F
90
- ]), Ne = p(async (e) => {
87
+ M,
88
+ I
89
+ ]), je = m(async (e) => {
91
90
  let t = "id" in e && e.id;
92
91
  try {
93
92
  if (t) {
@@ -103,10 +102,10 @@ function C() {
103
102
  }
104
103
  }
105
104
  } }), n = t.data?.updateTenant?.id ?? e.id, r = t.data?.updateTenant?.slug;
106
- d(F, "tenant.tenant.updated", {
105
+ f(I, "tenant.tenant.updated", {
107
106
  tenantId: n,
108
107
  tenantSlug: r
109
- }), j.success(I("admin.tenants.toast.updatedTitle", "Tenant updated"), I("admin.tenants.toast.updatedDescription", "{{name}} has been updated.", { name: e.name ?? "" }));
108
+ }), j.success(M("admin.tenants.toast.updatedTitle", "Tenant updated"), M("admin.tenants.toast.updatedDescription", "{{name}} has been updated.", { name: e.name ?? "" }));
110
109
  } else {
111
110
  let t = (await K({ variables: { input: {
112
111
  name: e.name,
@@ -118,32 +117,32 @@ function C() {
118
117
  plan: e.plan
119
118
  }
120
119
  } } })).data?.createTenant;
121
- t?.id && d(F, "tenant.tenant.created", {
120
+ t?.id && f(I, "tenant.tenant.created", {
122
121
  tenantId: t.id,
123
122
  tenantSlug: t.slug
124
- }), j.success(I("admin.tenants.toast.createdTitle", "Tenant created"), I("admin.tenants.toast.createdDescription", "{{name}} has been created.", { name: e.name ?? "" }));
123
+ }), j.success(M("admin.tenants.toast.createdTitle", "Tenant created"), M("admin.tenants.toast.createdDescription", "{{name}} has been created.", { name: e.name ?? "" }));
125
124
  }
126
125
  z(!1);
127
126
  } catch (e) {
128
127
  let n = e instanceof Error ? e.message : "Unknown error";
129
- throw j.error(t ? I("admin.tenants.toast.updateFailedTitle", "Update failed") : I("admin.tenants.toast.createFailedTitle", "Create failed"), n), e;
128
+ throw j.error(t ? M("admin.tenants.toast.updateFailedTitle", "Update failed") : M("admin.tenants.toast.createFailedTitle", "Create failed"), n), e;
130
129
  }
131
130
  }, [
132
131
  K,
133
132
  q,
134
133
  j,
135
- I,
136
- F
137
- ]), Pe = (e) => {
138
- E(l(D, `tenants/${e.id}`));
139
- }, Fe = p((e) => {
140
- let t = le({
141
- onView: () => E(l(D, `tenants/${e.id}`)),
142
- onEdit: w && P ? () => Z(e) : void 0
134
+ M,
135
+ I
136
+ ]), Me = (e) => {
137
+ E(d(D, `tenants/${e.id}`));
138
+ }, Ne = m((e) => {
139
+ let t = l({
140
+ onView: () => E(d(D, `tenants/${e.id}`)),
141
+ onEdit: w && F ? () => Z(e) : void 0
143
142
  });
144
143
  return H && t.push({
145
144
  key: e.status === "active" ? "suspend" : "activate",
146
- label: e.status === "active" ? I("admin.tenants.actions.suspend", "Suspend") : I("admin.tenants.actions.activate", "Activate"),
145
+ label: e.status === "active" ? M("admin.tenants.actions.suspend", "Suspend") : M("admin.tenants.actions.activate", "Activate"),
147
146
  separator: !0,
148
147
  onClick: e.status === "active" ? () => Q(e) : () => $(e)
149
148
  }), t;
@@ -152,15 +151,15 @@ function C() {
152
151
  D,
153
152
  w,
154
153
  H,
155
- P,
154
+ F,
156
155
  Z,
157
156
  Q,
158
157
  $,
159
- I
160
- ]), Ie = [
158
+ M
159
+ ]), Pe = [
161
160
  {
162
161
  key: "name",
163
- header: I("admin.tenants.table.tenant", "Tenant"),
162
+ header: M("admin.tenants.table.tenant", "Tenant"),
164
163
  sortable: !0,
165
164
  render: (e) => /* @__PURE__ */ b("div", {
166
165
  className: "flex items-center gap-3",
@@ -181,18 +180,18 @@ function C() {
181
180
  },
182
181
  {
183
182
  key: "status",
184
- header: I("admin.tenants.table.status", "Status"),
183
+ header: M("admin.tenants.table.status", "Status"),
185
184
  width: "120px",
186
185
  render: (e) => /* @__PURE__ */ y(ne, { status: e.status })
187
186
  },
188
187
  {
189
188
  key: "plan",
190
- header: I("admin.tenants.table.plan", "Plan"),
189
+ header: M("admin.tenants.table.plan", "Plan"),
191
190
  width: "100px"
192
191
  },
193
192
  {
194
193
  key: "userCount",
195
- header: I("admin.tenants.table.users", "Users"),
194
+ header: M("admin.tenants.table.users", "Users"),
196
195
  width: "80px",
197
196
  align: "right",
198
197
  sortable: !0,
@@ -200,14 +199,14 @@ function C() {
200
199
  },
201
200
  {
202
201
  key: "storageUsed",
203
- header: I("admin.tenants.table.storage", "Storage"),
202
+ header: M("admin.tenants.table.storage", "Storage"),
204
203
  width: "100px",
205
204
  align: "right",
206
205
  render: (e) => re(e.storageUsed)
207
206
  },
208
207
  {
209
208
  key: "createdAt",
210
- header: I("admin.tenants.table.created", "Created"),
209
+ header: M("admin.tenants.table.created", "Created"),
211
210
  width: "120px",
212
211
  sortable: !0,
213
212
  render: (e) => ie(e.createdAt)
@@ -217,55 +216,55 @@ function C() {
217
216
  header: "",
218
217
  width: "50px",
219
218
  align: "right",
220
- render: (e) => /* @__PURE__ */ y(ce, {
221
- actions: Fe(e),
222
- ariaLabel: I("admin.tenants.table.actionsFor", "Actions for {{name}}", { name: e.name })
219
+ render: (e) => /* @__PURE__ */ y(oe, {
220
+ actions: Ne(e),
221
+ ariaLabel: M("admin.tenants.table.actionsFor", "Actions for {{name}}", { name: e.name })
223
222
  })
224
223
  }
225
224
  ];
226
- return !T && !C ? /* @__PURE__ */ y(c, {
225
+ return !T && !C ? /* @__PURE__ */ y(u, {
227
226
  allowed: !1,
228
227
  isLoading: !1,
229
- pageTitle: I("admin.tenants.title", "Tenants"),
230
- denyMessage: I("admin.tenants.denyMessage", "You don't have permission to view tenants."),
228
+ pageTitle: M("admin.tenants.title", "Tenants"),
229
+ denyMessage: M("admin.tenants.denyMessage", "You don't have permission to view tenants."),
231
230
  children: null
232
- }) : V ? /* @__PURE__ */ y(c, {
231
+ }) : V ? /* @__PURE__ */ y(u, {
233
232
  allowed: C,
234
233
  isLoading: T,
235
- pageTitle: I("admin.tenants.title", "Tenants"),
236
- denyMessage: I("admin.tenants.denyMessage", "You don't have permission to view tenants."),
234
+ pageTitle: M("admin.tenants.title", "Tenants"),
235
+ denyMessage: M("admin.tenants.denyMessage", "You don't have permission to view tenants."),
237
236
  children: /* @__PURE__ */ b(a, {
238
- title: I("admin.tenants.title", "Tenants"),
239
- gradientWord: I("admin.tenants.title", "Tenants"),
240
- description: X ? I("admin.tenants.descriptionWithTotal", "Manage all tenants on the platform · {{total}} total", { total: X }) : I("admin.tenants.description", "Manage all tenants on the platform"),
241
- actions: w && Se ? /* @__PURE__ */ b(i, {
242
- onClick: Me,
243
- disabled: Oe || ke,
244
- children: [/* @__PURE__ */ y(ge, { className: "mr-2 h-4 w-4" }), I("admin.tenants.actions.createTenant", "Create Tenant")]
237
+ title: M("admin.tenants.title", "Tenants"),
238
+ gradientWord: M("admin.tenants.title", "Tenants"),
239
+ description: X ? M("admin.tenants.descriptionWithTotal", "Manage all tenants on the platform · {{total}} total", { total: X }) : M("admin.tenants.description", "Manage all tenants on the platform"),
240
+ actions: w && be ? /* @__PURE__ */ b(i, {
241
+ onClick: Ae,
242
+ disabled: Ee || De,
243
+ children: [/* @__PURE__ */ y(he, { className: "mr-2 h-4 w-4" }), M("admin.tenants.actions.createTenant", "Create Tenant")]
245
244
  }) : void 0,
246
245
  children: [/* @__PURE__ */ b("div", {
247
246
  className: "space-y-4",
248
247
  children: [
249
- /* @__PURE__ */ y(ye, { children: I("admin.tenants.purpose", "Tenants are the isolated customer accounts on the platform — each with its own users, data, plan and billing. Use this page to find a tenant, onboard a new one, and suspend or reactivate access when a customer churns or returns.") }),
250
- /* @__PURE__ */ b(ve, {
248
+ /* @__PURE__ */ y(ve, { children: M("admin.tenants.purpose", "Tenants are the isolated customer accounts on the platform — each with its own users, data, plan and billing. Use this page to find a tenant, onboard a new one, and suspend or reactivate access when a customer churns or returns.") }),
249
+ /* @__PURE__ */ b(_e, {
251
250
  className: "flex items-center gap-4 p-5",
252
251
  children: [/* @__PURE__ */ y("div", {
253
252
  className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
254
- children: /* @__PURE__ */ y(g, { className: "h-6 w-6" })
253
+ children: /* @__PURE__ */ y(_, { className: "h-6 w-6" })
255
254
  }), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("div", {
256
255
  className: "text-3xl font-bold tabular-nums text-text-primary",
257
256
  children: o(X)
258
257
  }), /* @__PURE__ */ y("p", {
259
258
  className: "text-sm text-text-secondary",
260
- children: I("admin.tenants.emphasis.label", "tenants on the platform")
259
+ children: M("admin.tenants.emphasis.label", "tenants on the platform")
261
260
  })] })]
262
261
  }),
263
262
  /* @__PURE__ */ b("div", {
264
263
  className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
265
264
  children: [/* @__PURE__ */ b("div", {
266
265
  className: "relative max-w-sm flex-1",
267
- children: [/* @__PURE__ */ y(_, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ y(te, {
268
- placeholder: I("admin.tenants.searchPlaceholder", "Search tenants..."),
266
+ children: [/* @__PURE__ */ y(v, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ y(te, {
267
+ placeholder: M("admin.tenants.searchPlaceholder", "Search tenants..."),
269
268
  value: O.search,
270
269
  onChange: (e) => {
271
270
  R(1), k({ search: e.target.value });
@@ -283,19 +282,19 @@ function C() {
283
282
  children: [
284
283
  /* @__PURE__ */ y("option", {
285
284
  value: "all",
286
- children: I("admin.tenants.filters.status.all", "All Status")
285
+ children: M("admin.tenants.filters.status.all", "All Status")
287
286
  }),
288
287
  /* @__PURE__ */ y("option", {
289
288
  value: "active",
290
- children: I("admin.tenants.filters.status.active", "Active")
289
+ children: M("admin.tenants.filters.status.active", "Active")
291
290
  }),
292
291
  /* @__PURE__ */ y("option", {
293
292
  value: "suspended",
294
- children: I("admin.tenants.filters.status.suspended", "Suspended")
293
+ children: M("admin.tenants.filters.status.suspended", "Suspended")
295
294
  }),
296
295
  /* @__PURE__ */ y("option", {
297
296
  value: "deleted",
298
- children: I("admin.tenants.filters.status.deleted", "Deleted")
297
+ children: M("admin.tenants.filters.status.deleted", "Deleted")
299
298
  })
300
299
  ]
301
300
  }), (O.search || O.status !== "all") && /* @__PURE__ */ y(i, {
@@ -303,62 +302,62 @@ function C() {
303
302
  onClick: () => {
304
303
  R(1), A();
305
304
  },
306
- children: I("admin.tenants.filters.clear", "Clear")
305
+ children: M("admin.tenants.filters.clear", "Clear")
307
306
  })]
308
307
  })]
309
308
  }),
310
309
  W && /* @__PURE__ */ b("div", {
311
310
  className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
312
311
  children: [
313
- I("admin.tenants.error.loadFailed", "Failed to load tenants"),
312
+ M("admin.tenants.error.loadFailed", "Failed to load tenants"),
314
313
  ": ",
315
314
  W.message
316
315
  ]
317
316
  }),
318
317
  /* @__PURE__ */ y(ae, {
319
- columns: Ie,
320
- data: Ae,
318
+ columns: Pe,
319
+ data: Oe,
321
320
  keyExtractor: (e) => e.id,
322
- onRowClick: Pe,
321
+ onRowClick: Me,
323
322
  sortBy: O.sortBy,
324
323
  sortOrder: O.sortOrder,
325
324
  onSort: (e, t) => k({
326
325
  sortBy: e,
327
326
  sortOrder: t
328
327
  }),
329
- loading: De,
328
+ loading: Te,
330
329
  emptyState: /* @__PURE__ */ y(ee, {
331
330
  illustration: "empty-search",
332
- icon: /* @__PURE__ */ y(g, { className: "h-8 w-8" }),
333
- title: I("admin.tenants.empty.title", "No tenants found"),
334
- description: I("admin.tenants.empty.description", "Try adjusting your search or filters"),
331
+ icon: /* @__PURE__ */ y(_, { className: "h-8 w-8" }),
332
+ title: M("admin.tenants.empty.title", "No tenants found"),
333
+ description: M("admin.tenants.empty.description", "Try adjusting your search or filters"),
335
334
  action: /* @__PURE__ */ y(i, {
336
335
  variant: "outline",
337
336
  onClick: () => {
338
337
  R(1), A();
339
338
  },
340
- children: I("admin.tenants.empty.clearFilters", "Clear filters")
339
+ children: M("admin.tenants.empty.clearFilters", "Clear filters")
341
340
  })
342
341
  })
343
342
  }),
344
- X > S && /* @__PURE__ */ y(oe, {
343
+ X > S && /* @__PURE__ */ y(s, {
345
344
  page: L,
346
- totalPages: je,
345
+ totalPages: ke,
347
346
  totalItems: X,
348
347
  pageSize: S,
349
348
  onPageChange: R
350
349
  })
351
350
  ]
352
- }), /* @__PURE__ */ y(se, {
353
- open: Ce,
351
+ }), /* @__PURE__ */ y(c, {
352
+ open: xe,
354
353
  onClose: () => z(!1),
355
- tenant: we,
356
- onSave: Ne
354
+ tenant: Se,
355
+ onSave: je
357
356
  })]
358
357
  })
359
358
  }) : /* @__PURE__ */ y(a, {
360
- title: I("admin.tenants.featureDisabledTitle", "Tenant management unavailable"),
361
- description: I("admin.tenants.featureDisabled", "This feature is currently disabled."),
359
+ title: M("admin.tenants.featureDisabledTitle", "Tenant management unavailable"),
360
+ description: M("admin.tenants.featureDisabled", "This feature is currently disabled."),
362
361
  children: null
363
362
  });
364
363
  }