@burdenoff/microfe-admin 2026.625.1 → 2026.712.1

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.
Files changed (77) hide show
  1. package/dist/admin/AdminRoot.js +2 -2
  2. package/dist/admin/AdminRoot.js.map +1 -1
  3. package/dist/admin/AdminRoutes.js +130 -78
  4. package/dist/admin/AdminRoutes.js.map +1 -1
  5. package/dist/admin/components/AccessDenied.js +2 -2
  6. package/dist/admin/components/AccessDenied.js.map +1 -1
  7. package/dist/admin/components/ActivityFeed.js +4 -4
  8. package/dist/admin/components/ActivityFeed.js.map +1 -1
  9. package/dist/admin/components/Charts.js.map +1 -1
  10. package/dist/admin/components/ConfirmDialog.js +1 -1
  11. package/dist/admin/components/ConfirmDialog.js.map +1 -1
  12. package/dist/admin/components/DataTable.js.map +1 -1
  13. package/dist/admin/components/FormSheet.js +1 -1
  14. package/dist/admin/components/FormSheet.js.map +1 -1
  15. package/dist/admin/components/TenantSsoPanel.js +1 -1
  16. package/dist/admin/components/TenantSsoPanel.js.map +1 -1
  17. package/dist/admin/components/Toast.js +11 -11
  18. package/dist/admin/components/Toast.js.map +1 -1
  19. package/dist/admin/components/forms/SubscriptionForm.js +2 -2
  20. package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
  21. package/dist/admin/components/forms/TenantForm.js +1 -1
  22. package/dist/admin/components/forms/UserForm.js +1 -1
  23. package/dist/admin/components/ui.js +3 -3
  24. package/dist/admin/components/ui.js.map +1 -1
  25. package/dist/admin/constants/featureFlags.js +12 -0
  26. package/dist/admin/constants/featureFlags.js.map +1 -0
  27. package/dist/admin/constants/index.js +33 -24
  28. package/dist/admin/constants/index.js.map +1 -1
  29. package/dist/admin/pages/AuditLogsPage.js +92 -77
  30. package/dist/admin/pages/AuditLogsPage.js.map +1 -1
  31. package/dist/admin/pages/BillingPage.js +95 -51
  32. package/dist/admin/pages/BillingPage.js.map +1 -1
  33. package/dist/admin/pages/CommunicationsPage.js +97 -92
  34. package/dist/admin/pages/CommunicationsPage.js.map +1 -1
  35. package/dist/admin/pages/ContentPagesPage.js +274 -259
  36. package/dist/admin/pages/ContentPagesPage.js.map +1 -1
  37. package/dist/admin/pages/DashboardPage.js +113 -84
  38. package/dist/admin/pages/DashboardPage.js.map +1 -1
  39. package/dist/admin/pages/FeatureFlagsPage.js +93 -75
  40. package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
  41. package/dist/admin/pages/OrganizationsListPage.js +105 -87
  42. package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
  43. package/dist/admin/pages/ProductsPage.js +138 -130
  44. package/dist/admin/pages/ProductsPage.js.map +1 -1
  45. package/dist/admin/pages/ReferralsPage.js +70 -52
  46. package/dist/admin/pages/ReferralsPage.js.map +1 -1
  47. package/dist/admin/pages/RolesPermissionsPage.js +136 -118
  48. package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
  49. package/dist/admin/pages/SREPage.js +34 -32
  50. package/dist/admin/pages/SREPage.js.map +1 -1
  51. package/dist/admin/pages/SandboxSettingsPage.js +360 -322
  52. package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
  53. package/dist/admin/pages/SettingsPage.js +64 -61
  54. package/dist/admin/pages/SettingsPage.js.map +1 -1
  55. package/dist/admin/pages/SyncFailuresPage.js +120 -118
  56. package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
  57. package/dist/admin/pages/TenantDetailPage.js +286 -231
  58. package/dist/admin/pages/TenantDetailPage.js.map +1 -1
  59. package/dist/admin/pages/TenantFormRoutePage.js +78 -73
  60. package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
  61. package/dist/admin/pages/TenantsListPage.js +182 -155
  62. package/dist/admin/pages/TenantsListPage.js.map +1 -1
  63. package/dist/admin/pages/UsersListPage.js +128 -113
  64. package/dist/admin/pages/UsersListPage.js.map +1 -1
  65. package/dist/admin/pages/WaitlistPage.js +78 -63
  66. package/dist/admin/pages/WaitlistPage.js.map +1 -1
  67. package/dist/admin/pages/WorkflowsPage.js +107 -102
  68. package/dist/admin/pages/WorkflowsPage.js.map +1 -1
  69. package/dist/admin/pages/WorkspacesListPage.js +61 -49
  70. package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
  71. package/dist/node_modules/@hookform/resolvers/dist/resolvers.js +2 -2
  72. package/dist/node_modules/@hookform/resolvers/dist/resolvers.js.map +1 -1
  73. package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js +35 -26
  74. package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js.map +1 -1
  75. package/dist/node_modules/react-hook-form/dist/index.esm.js +609 -516
  76. package/dist/node_modules/react-hook-form/dist/index.esm.js.map +1 -1
  77. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"TenantsListPage.js","names":[],"sources":["../../../src/admin/pages/TenantsListPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Plus, Search, Building2 } from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminStore } from '../store/adminStore';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Input,\n DataTable,\n Pagination,\n TenantStatusBadge,\n EmptyState,\n Avatar,\n TenantForm,\n RowActions,\n createRowActions,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate, formatNumber, formatBytes } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant, TenantFilter } from '../types';\nimport type { CreateTenantInput, UpdateTenantInput } from '../schemas';\nimport {\n useListTenantsQuery,\n useCreateTenantMutation,\n useUpdateTenantMutation,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport type { RowAction } from '../components/RowActions';\nimport type { TenantStatus as GqlTenantStatus } from '../../generated/global-types';\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';\n\nconst PAGE_SIZE = 20;\n\n/**\n * Tenants list page\n */\nexport function TenantsListPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const { tenantFilter, updateTenantFilter, resetTenantFilter } = useAdminStore();\n const toast = useToast();\n const { t } = useI18n();\n // Page-level feature flags. Default-allow keeps prior behavior intact.\n // Both list-page entry points (the Create CTA and the row Edit action that\n // opens the local TenantForm modal) have to honor the flags — otherwise\n // turning the edit flag off would still let an admin edit via the modal\n // even though the route-level gate denies /admin/tenants/:id/edit.\n const createFlagEnabled = useFeatureFlag('admin.tenant.create', true);\n const editFlagEnabled = useFeatureFlag('admin.tenant.edit', true);\n const events = useEventBus();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const [page, setPage] = useState(1);\n\n // Form state\n const [formOpen, setFormOpen] = useState(false);\n const [editingTenant, setEditingTenant] = useState<Tenant | undefined>(undefined);\n\n // Build filter for GraphQL — only pass supported fields\n const gqlFilter = useMemo(() => {\n const f: { search?: string; status?: GqlTenantStatus } = {};\n if (tenantFilter.search) f.search = tenantFilter.search;\n const statusMap: Record<string, GqlTenantStatus> = {\n active: 'ACTIVE' as GqlTenantStatus,\n suspended: 'SUSPENDED' as GqlTenantStatus,\n deleted: 'DELETED' as GqlTenantStatus,\n };\n if (tenantFilter.status !== 'all' && statusMap[tenantFilter.status]) {\n f.status = statusMap[tenantFilter.status];\n }\n return f;\n }, [tenantFilter.search, tenantFilter.status]);\n\n const skipQueries = permissionsLoading || !canViewTenants;\n\n // Query tenants from backend\n const { data, loading, error, refetch } = useListTenantsQuery({\n variables: {\n filter: gqlFilter,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n // Mutations\n const [createTenant, { loading: creating }] = useCreateTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n const [updateTenant, { loading: updating }] = useUpdateTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n const [suspendTenant] = useSuspendTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n const [activateTenant] = useActivateTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n\n const tenants: Tenant[] = useMemo(() => (data?.tenants?.items ?? []).map(mapTenantToUi), [data]);\n\n const total = data?.tenants?.total ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const handleCreateTenant = useCallback(() => {\n setEditingTenant(undefined);\n setFormOpen(true);\n }, []);\n\n const handleEditTenant = useCallback((tenant: Tenant) => {\n setEditingTenant(tenant);\n setFormOpen(true);\n }, []);\n\n const handleSuspendTenant = useCallback(\n async (tenant: Tenant) => {\n try {\n await suspendTenant({ variables: { id: tenant.id } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId: tenant.id,\n tenantSlug: tenant.slug,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenants.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenants.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant.name,\n })\n );\n } catch {\n toast.error(\n tr('admin.tenants.toast.errorTitle', 'Error'),\n tr('admin.tenants.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n },\n [suspendTenant, toast, tr, events]\n );\n\n const handleActivateTenant = useCallback(\n async (tenant: Tenant) => {\n try {\n await activateTenant({ variables: { id: tenant.id } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId: tenant.id,\n tenantSlug: tenant.slug,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenants.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenants.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant.name,\n })\n );\n } catch {\n toast.error(\n tr('admin.tenants.toast.errorTitle', 'Error'),\n tr('admin.tenants.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n },\n [activateTenant, toast, tr, events]\n );\n\n const handleSaveTenant = useCallback(\n async (data: CreateTenantInput | UpdateTenantInput) => {\n const isEditing = 'id' in data && data.id;\n try {\n if (isEditing) {\n const updated = await updateTenant({\n variables: {\n id: (data as UpdateTenantInput).id,\n input: {\n name: data.name,\n customDomain: data.domain,\n metadata: {\n ownerEmail: data.ownerEmail,\n billingEmail: data.billingEmail,\n plan: data.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const updatedId = updated.data?.updateTenant?.id ?? (data as UpdateTenantInput).id;\n // Use the slug from the persisted record. The update mutation does\n // not accept slug as input, so reading it from the form would emit\n // a value that may not match what the backend actually has.\n const persistedSlug = updated.data?.updateTenant?.slug;\n emitTenantLifecycle(events, 'tenant.tenant.updated', {\n tenantId: updatedId,\n tenantSlug: persistedSlug,\n });\n toast.success(\n tr('admin.tenants.toast.updatedTitle', 'Tenant updated'),\n tr('admin.tenants.toast.updatedDescription', '{{name}} has been updated.', {\n name: data.name ?? '',\n })\n );\n } else {\n const created = await createTenant({\n variables: {\n input: {\n name: data.name!,\n slug: data.slug!,\n customDomain: data.domain,\n metadata: {\n ownerEmail: data.ownerEmail,\n billingEmail: data.billingEmail,\n plan: data.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const createdRecord = created.data?.createTenant;\n if (createdRecord?.id) {\n // Slug from the server-confirmed record so listeners never see a\n // form value the backend rejected (e.g. uniqueness collision).\n emitTenantLifecycle(events, 'tenant.tenant.created', {\n tenantId: createdRecord.id,\n tenantSlug: createdRecord.slug,\n });\n }\n toast.success(\n tr('admin.tenants.toast.createdTitle', 'Tenant created'),\n tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', {\n name: data.name ?? '',\n })\n );\n }\n setFormOpen(false);\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Unknown error';\n toast.error(\n isEditing\n ? tr('admin.tenants.toast.updateFailedTitle', 'Update failed')\n : tr('admin.tenants.toast.createFailedTitle', 'Create failed'),\n msg\n );\n // Rethrow so TenantForm.onSubmit's await rejects and the modal stays\n // open with the user's input intact. Symmetric to the rethrow already\n // applied in TenantFormRoutePage.handleSave.\n throw err;\n }\n },\n [createTenant, updateTenant, toast, tr, events]\n );\n\n const handleRowClick = (tenant: Tenant) => {\n navigate(joinPath(basePath, `tenants/${tenant.id}`));\n };\n\n const getRowActions = useCallback(\n (tenant: Tenant): RowAction[] => {\n const actions = createRowActions({\n onView: () => navigate(joinPath(basePath, `tenants/${tenant.id}`)),\n onEdit: canManageTenants && editFlagEnabled ? () => handleEditTenant(tenant) : undefined,\n });\n if (canManageTenants) {\n actions.push({\n key: tenant.status === 'active' ? 'suspend' : 'activate',\n label:\n tenant.status === 'active'\n ? tr('admin.tenants.actions.suspend', 'Suspend')\n : tr('admin.tenants.actions.activate', 'Activate'),\n separator: true,\n onClick:\n tenant.status === 'active'\n ? () => handleSuspendTenant(tenant)\n : () => handleActivateTenant(tenant),\n });\n }\n return actions;\n },\n [\n navigate,\n basePath,\n canManageTenants,\n editFlagEnabled,\n handleEditTenant,\n handleSuspendTenant,\n handleActivateTenant,\n tr,\n ]\n );\n\n const columns = [\n {\n key: 'name',\n header: tr('admin.tenants.table.tenant', 'Tenant'),\n sortable: true,\n render: (tenant: Tenant) => (\n <div className=\"flex items-center gap-3\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"sm\" />\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-text-primary\">{tenant.name}</div>\n <div className=\"truncate text-xs text-text-secondary\">{tenant.slug}</div>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('admin.tenants.table.status', 'Status'),\n width: '120px',\n render: (tenant: Tenant) => <TenantStatusBadge status={tenant.status} />,\n },\n {\n key: 'plan',\n header: tr('admin.tenants.table.plan', 'Plan'),\n width: '100px',\n },\n {\n key: 'userCount',\n header: tr('admin.tenants.table.users', 'Users'),\n width: '80px',\n align: 'right' as const,\n sortable: true,\n render: (tenant: Tenant) => formatNumber(tenant.userCount),\n },\n {\n key: 'storageUsed',\n header: tr('admin.tenants.table.storage', 'Storage'),\n width: '100px',\n align: 'right' as const,\n render: (tenant: Tenant) => formatBytes(tenant.storageUsed),\n },\n {\n key: 'createdAt',\n header: tr('admin.tenants.table.created', 'Created'),\n width: '120px',\n sortable: true,\n render: (tenant: Tenant) => formatDate(tenant.createdAt),\n },\n {\n key: 'actions',\n header: '',\n width: '50px',\n align: 'right' as const,\n render: (tenant: Tenant) => (\n <RowActions\n actions={getRowActions(tenant)}\n ariaLabel={tr('admin.tenants.table.actionsFor', 'Actions for {{name}}', {\n name: tenant.name,\n })}\n />\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenants.title', 'Tenants')}\n denyMessage={tr('admin.tenants.denyMessage', \"You don't have permission to view tenants.\")}\n >\n <PageLayout\n title={tr('admin.tenants.title', 'Tenants')}\n gradientWord={tr('admin.tenants.title', 'Tenants')}\n description={\n total\n ? tr(\n 'admin.tenants.descriptionWithTotal',\n 'Manage all tenants on the platform · {{total}} total',\n { total }\n )\n : tr('admin.tenants.description', 'Manage all tenants on the platform')\n }\n actions={\n canManageTenants && createFlagEnabled ? (\n <Button onClick={handleCreateTenant} disabled={creating || updating}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenants.actions.createTenant', 'Create Tenant')}\n </Button>\n ) : undefined\n }\n >\n <div className=\"space-y-4\">\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder={tr('admin.tenants.searchPlaceholder', 'Search tenants...')}\n value={tenantFilter.search}\n onChange={(e) => {\n setPage(1);\n updateTenantFilter({ search: e.target.value });\n }}\n className=\"pl-9\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={tenantFilter.status}\n onChange={(e) => {\n setPage(1);\n updateTenantFilter({ status: e.target.value as TenantFilter['status'] });\n }}\n className=\"h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\"\n >\n <option value=\"all\">{tr('admin.tenants.filters.status.all', 'All Status')}</option>\n <option value=\"active\">\n {tr('admin.tenants.filters.status.active', 'Active')}\n </option>\n <option value=\"suspended\">\n {tr('admin.tenants.filters.status.suspended', 'Suspended')}\n </option>\n <option value=\"deleted\">\n {tr('admin.tenants.filters.status.deleted', 'Deleted')}\n </option>\n </select>\n {(tenantFilter.search || tenantFilter.status !== 'all') && (\n <Button\n variant=\"ghost\"\n onClick={() => {\n setPage(1);\n resetTenantFilter();\n }}\n >\n {tr('admin.tenants.filters.clear', 'Clear')}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error state */}\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {tr('admin.tenants.error.loadFailed', 'Failed to load tenants')}: {error.message}\n </div>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={tenants}\n keyExtractor={(t) => t.id}\n onRowClick={handleRowClick}\n sortBy={tenantFilter.sortBy}\n sortOrder={tenantFilter.sortOrder}\n onSort={(column, order) =>\n updateTenantFilter({\n sortBy: column as TenantFilter['sortBy'],\n sortOrder: order,\n })\n }\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building2 className=\"h-8 w-8\" />}\n title={tr('admin.tenants.empty.title', 'No tenants found')}\n description={tr(\n 'admin.tenants.empty.description',\n 'Try adjusting your search or filters'\n )}\n action={\n <Button\n variant=\"outline\"\n onClick={() => {\n setPage(1);\n resetTenantFilter();\n }}\n >\n {tr('admin.tenants.empty.clearFilters', 'Clear filters')}\n </Button>\n }\n />\n }\n />\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n\n {/* Tenant Form */}\n <TenantForm\n open={formOpen}\n onClose={() => setFormOpen(false)}\n tenant={editingTenant}\n onSave={handleSaveTenant}\n />\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TenantsListPage.js","names":[],"sources":["../../../src/admin/pages/TenantsListPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Plus, Search, Building2 } from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminStore } from '../store/adminStore';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Input,\n DataTable,\n Pagination,\n TenantStatusBadge,\n EmptyState,\n Avatar,\n TenantForm,\n RowActions,\n createRowActions,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate, formatNumber, formatBytes } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant, TenantFilter } from '../types';\nimport type { CreateTenantInput, UpdateTenantInput } from '../schemas';\nimport {\n useListTenantsQuery,\n useCreateTenantMutation,\n useUpdateTenantMutation,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport type { RowAction } from '../components/RowActions';\nimport type { TenantStatus as GqlTenantStatus } from '../../generated/global-types';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\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\nconst PAGE_SIZE = 20;\n\n/**\n * Tenants list page\n */\nexport function TenantsListPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const { tenantFilter, updateTenantFilter, resetTenantFilter } = useAdminStore();\n const toast = useToast();\n const { t } = useI18n();\n // Page-level feature flags. Default-allow keeps prior behavior intact.\n // Both list-page entry points (the Create CTA and the row Edit action that\n // opens the local TenantForm modal) have to honor the flags — otherwise\n // turning the edit flag off would still let an admin edit via the modal\n // even though the route-level gate denies /admin/tenants/:id/edit.\n const managementFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const listFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.list, true);\n const createFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.create, true);\n const editFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.edit, true);\n const events = useEventBus();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const [page, setPage] = useState(1);\n\n // Form state\n const [formOpen, setFormOpen] = useState(false);\n const [editingTenant, setEditingTenant] = useState<Tenant | undefined>(undefined);\n\n // Build filter for GraphQL — only pass supported fields\n const gqlFilter = useMemo(() => {\n const f: { search?: string; status?: GqlTenantStatus } = {};\n if (tenantFilter.search) f.search = tenantFilter.search;\n const statusMap: Record<string, GqlTenantStatus> = {\n active: 'ACTIVE' as GqlTenantStatus,\n suspended: 'SUSPENDED' as GqlTenantStatus,\n deleted: 'DELETED' as GqlTenantStatus,\n };\n if (tenantFilter.status !== 'all' && statusMap[tenantFilter.status]) {\n f.status = statusMap[tenantFilter.status];\n }\n return f;\n }, [tenantFilter.search, tenantFilter.status]);\n\n const pageEnabled = managementFlagEnabled && listFlagEnabled;\n const canLifecycleTenants = canManageTenants && managementFlagEnabled;\n const skipQueries = permissionsLoading || !canViewTenants || !pageEnabled;\n\n // Query tenants from backend\n const { data, loading, error, refetch } = useListTenantsQuery({\n variables: {\n filter: gqlFilter,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n // Mutations\n const [createTenant, { loading: creating }] = useCreateTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n const [updateTenant, { loading: updating }] = useUpdateTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n const [suspendTenant] = useSuspendTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n const [activateTenant] = useActivateTenantMutation({\n onCompleted: () => {\n void refetch();\n },\n });\n\n const tenants: Tenant[] = useMemo(() => (data?.tenants?.items ?? []).map(mapTenantToUi), [data]);\n\n const total = data?.tenants?.total ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const handleCreateTenant = useCallback(() => {\n setEditingTenant(undefined);\n setFormOpen(true);\n }, []);\n\n const handleEditTenant = useCallback((tenant: Tenant) => {\n setEditingTenant(tenant);\n setFormOpen(true);\n }, []);\n\n const handleSuspendTenant = useCallback(\n async (tenant: Tenant) => {\n try {\n await suspendTenant({ variables: { id: tenant.id } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId: tenant.id,\n tenantSlug: tenant.slug,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenants.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenants.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant.name,\n })\n );\n } catch {\n toast.error(\n tr('admin.tenants.toast.errorTitle', 'Error'),\n tr('admin.tenants.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n },\n [suspendTenant, toast, tr, events]\n );\n\n const handleActivateTenant = useCallback(\n async (tenant: Tenant) => {\n try {\n await activateTenant({ variables: { id: tenant.id } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId: tenant.id,\n tenantSlug: tenant.slug,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenants.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenants.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant.name,\n })\n );\n } catch {\n toast.error(\n tr('admin.tenants.toast.errorTitle', 'Error'),\n tr('admin.tenants.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n },\n [activateTenant, toast, tr, events]\n );\n\n const handleSaveTenant = useCallback(\n async (data: CreateTenantInput | UpdateTenantInput) => {\n const isEditing = 'id' in data && data.id;\n try {\n if (isEditing) {\n const updated = await updateTenant({\n variables: {\n id: (data as UpdateTenantInput).id,\n input: {\n name: data.name,\n customDomain: data.domain,\n metadata: {\n ownerEmail: data.ownerEmail,\n billingEmail: data.billingEmail,\n plan: data.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const updatedId = updated.data?.updateTenant?.id ?? (data as UpdateTenantInput).id;\n // Use the slug from the persisted record. The update mutation does\n // not accept slug as input, so reading it from the form would emit\n // a value that may not match what the backend actually has.\n const persistedSlug = updated.data?.updateTenant?.slug;\n emitTenantLifecycle(events, 'tenant.tenant.updated', {\n tenantId: updatedId,\n tenantSlug: persistedSlug,\n });\n toast.success(\n tr('admin.tenants.toast.updatedTitle', 'Tenant updated'),\n tr('admin.tenants.toast.updatedDescription', '{{name}} has been updated.', {\n name: data.name ?? '',\n })\n );\n } else {\n const created = await createTenant({\n variables: {\n input: {\n name: data.name!,\n slug: data.slug!,\n customDomain: data.domain,\n metadata: {\n ownerEmail: data.ownerEmail,\n billingEmail: data.billingEmail,\n plan: data.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const createdRecord = created.data?.createTenant;\n if (createdRecord?.id) {\n // Slug from the server-confirmed record so listeners never see a\n // form value the backend rejected (e.g. uniqueness collision).\n emitTenantLifecycle(events, 'tenant.tenant.created', {\n tenantId: createdRecord.id,\n tenantSlug: createdRecord.slug,\n });\n }\n toast.success(\n tr('admin.tenants.toast.createdTitle', 'Tenant created'),\n tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', {\n name: data.name ?? '',\n })\n );\n }\n setFormOpen(false);\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Unknown error';\n toast.error(\n isEditing\n ? tr('admin.tenants.toast.updateFailedTitle', 'Update failed')\n : tr('admin.tenants.toast.createFailedTitle', 'Create failed'),\n msg\n );\n // Rethrow so TenantForm.onSubmit's await rejects and the modal stays\n // open with the user's input intact. Symmetric to the rethrow already\n // applied in TenantFormRoutePage.handleSave.\n throw err;\n }\n },\n [createTenant, updateTenant, toast, tr, events]\n );\n\n const handleRowClick = (tenant: Tenant) => {\n navigate(joinPath(basePath, `tenants/${tenant.id}`));\n };\n\n const getRowActions = useCallback(\n (tenant: Tenant): RowAction[] => {\n const actions = createRowActions({\n onView: () => navigate(joinPath(basePath, `tenants/${tenant.id}`)),\n onEdit: canManageTenants && editFlagEnabled ? () => handleEditTenant(tenant) : undefined,\n });\n if (canLifecycleTenants) {\n actions.push({\n key: tenant.status === 'active' ? 'suspend' : 'activate',\n label:\n tenant.status === 'active'\n ? tr('admin.tenants.actions.suspend', 'Suspend')\n : tr('admin.tenants.actions.activate', 'Activate'),\n separator: true,\n onClick:\n tenant.status === 'active'\n ? () => handleSuspendTenant(tenant)\n : () => handleActivateTenant(tenant),\n });\n }\n return actions;\n },\n [\n navigate,\n basePath,\n canManageTenants,\n canLifecycleTenants,\n editFlagEnabled,\n handleEditTenant,\n handleSuspendTenant,\n handleActivateTenant,\n tr,\n ]\n );\n\n const columns = [\n {\n key: 'name',\n header: tr('admin.tenants.table.tenant', 'Tenant'),\n sortable: true,\n render: (tenant: Tenant) => (\n <div className=\"flex items-center gap-3\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"sm\" />\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-text-primary\">{tenant.name}</div>\n <div className=\"truncate text-xs text-text-secondary\">{tenant.slug}</div>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('admin.tenants.table.status', 'Status'),\n width: '120px',\n render: (tenant: Tenant) => <TenantStatusBadge status={tenant.status} />,\n },\n {\n key: 'plan',\n header: tr('admin.tenants.table.plan', 'Plan'),\n width: '100px',\n },\n {\n key: 'userCount',\n header: tr('admin.tenants.table.users', 'Users'),\n width: '80px',\n align: 'right' as const,\n sortable: true,\n render: (tenant: Tenant) => formatNumber(tenant.userCount),\n },\n {\n key: 'storageUsed',\n header: tr('admin.tenants.table.storage', 'Storage'),\n width: '100px',\n align: 'right' as const,\n render: (tenant: Tenant) => formatBytes(tenant.storageUsed),\n },\n {\n key: 'createdAt',\n header: tr('admin.tenants.table.created', 'Created'),\n width: '120px',\n sortable: true,\n render: (tenant: Tenant) => formatDate(tenant.createdAt),\n },\n {\n key: 'actions',\n header: '',\n width: '50px',\n align: 'right' as const,\n render: (tenant: Tenant) => (\n <RowActions\n actions={getRowActions(tenant)}\n ariaLabel={tr('admin.tenants.table.actionsFor', 'Actions for {{name}}', {\n name: tenant.name,\n })}\n />\n ),\n },\n ];\n\n if (!permissionsLoading && !canViewTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={tr('admin.tenants.title', 'Tenants')}\n denyMessage={tr('admin.tenants.denyMessage', \"You don't have permission to view tenants.\")}\n >\n {null}\n </PermissionGuard>\n );\n }\n\n if (!pageEnabled) {\n return (\n <PageLayout\n title={tr('admin.tenants.featureDisabledTitle', 'Tenant management unavailable')}\n description={tr('admin.tenants.featureDisabled', 'This feature is currently disabled.')}\n >\n {null}\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenants.title', 'Tenants')}\n denyMessage={tr('admin.tenants.denyMessage', \"You don't have permission to view tenants.\")}\n >\n <PageLayout\n title={tr('admin.tenants.title', 'Tenants')}\n gradientWord={tr('admin.tenants.title', 'Tenants')}\n description={\n total\n ? tr(\n 'admin.tenants.descriptionWithTotal',\n 'Manage all tenants on the platform · {{total}} total',\n { total }\n )\n : tr('admin.tenants.description', 'Manage all tenants on the platform')\n }\n actions={\n canManageTenants && createFlagEnabled ? (\n <Button onClick={handleCreateTenant} disabled={creating || updating}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenants.actions.createTenant', 'Create Tenant')}\n </Button>\n ) : undefined\n }\n >\n <div className=\"space-y-4\">\n <PagePurpose>\n {tr(\n 'admin.tenants.purpose',\n '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.'\n )}\n </PagePurpose>\n\n {/* Emphasis: the headline tenant count — the page's most important stat. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <Building2 className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">\n {formatNumber(total)}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr('admin.tenants.emphasis.label', 'tenants on the platform')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder={tr('admin.tenants.searchPlaceholder', 'Search tenants...')}\n value={tenantFilter.search}\n onChange={(e) => {\n setPage(1);\n updateTenantFilter({ search: e.target.value });\n }}\n className=\"pl-9\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={tenantFilter.status}\n onChange={(e) => {\n setPage(1);\n updateTenantFilter({ status: e.target.value as TenantFilter['status'] });\n }}\n className=\"h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\"\n >\n <option value=\"all\">{tr('admin.tenants.filters.status.all', 'All Status')}</option>\n <option value=\"active\">\n {tr('admin.tenants.filters.status.active', 'Active')}\n </option>\n <option value=\"suspended\">\n {tr('admin.tenants.filters.status.suspended', 'Suspended')}\n </option>\n <option value=\"deleted\">\n {tr('admin.tenants.filters.status.deleted', 'Deleted')}\n </option>\n </select>\n {(tenantFilter.search || tenantFilter.status !== 'all') && (\n <Button\n variant=\"ghost\"\n onClick={() => {\n setPage(1);\n resetTenantFilter();\n }}\n >\n {tr('admin.tenants.filters.clear', 'Clear')}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error state */}\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {tr('admin.tenants.error.loadFailed', 'Failed to load tenants')}: {error.message}\n </div>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={tenants}\n keyExtractor={(t) => t.id}\n onRowClick={handleRowClick}\n sortBy={tenantFilter.sortBy}\n sortOrder={tenantFilter.sortOrder}\n onSort={(column, order) =>\n updateTenantFilter({\n sortBy: column as TenantFilter['sortBy'],\n sortOrder: order,\n })\n }\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building2 className=\"h-8 w-8\" />}\n title={tr('admin.tenants.empty.title', 'No tenants found')}\n description={tr(\n 'admin.tenants.empty.description',\n 'Try adjusting your search or filters'\n )}\n action={\n <Button\n variant=\"outline\"\n onClick={() => {\n setPage(1);\n resetTenantFilter();\n }}\n >\n {tr('admin.tenants.empty.clearFilters', 'Clear filters')}\n </Button>\n }\n />\n }\n />\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n\n {/* Tenant Form */}\n <TenantForm\n open={formOpen}\n onClose={() => setFormOpen(false)}\n tenant={editingTenant}\n onSave={handleSaveTenant}\n />\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -4,89 +4,90 @@ import { useAdminPermissions as n } from "../hooks/useAdminPermissions.js";
4
4
  import { Avatar as r, Button as i, EmptyState as a, Input as o } from "../components/ui.js";
5
5
  import { PageLayout as s } from "../components/PageLayout.js";
6
6
  import { UserStatusBadge as c } from "../components/StatusBadge.js";
7
- import { formatDate as ee, formatRelativeTime as l } from "../utils/formatters.js";
8
- import { DataTable as u, Pagination as d } from "../components/DataTable.js";
7
+ import { formatDate as l, formatNumber as u, formatRelativeTime as ee } from "../utils/formatters.js";
8
+ import { DataTable as te, Pagination as d } from "../components/DataTable.js";
9
9
  import { tWithFallback as f } from "../utils/i18n.js";
10
10
  import { RowActions as p } from "../components/RowActions.js";
11
11
  import { PermissionGuard as m } from "../components/PermissionGuard.js";
12
12
  import "../components/index.js";
13
13
  import { joinPath as h } from "../utils/navigation.js";
14
14
  import { mapUserToUi as g } from "../utils/mappers.js";
15
- import { useListUsersQuery as _, useUpdateUserMutation as v } from "../../generated/global-operations.js";
16
- import { useAdminStore as y } from "../store/adminStore.js";
17
- import { UserStatus as b } from "../../generated/global-types.js";
18
- import { useCallback as x, useMemo as S, useState as C } from "react";
19
- import { Search as w, Users as T } from "lucide-react";
20
- import { useNavigate as E } from "react-router-dom";
21
- import { jsx as D, jsxs as O } from "react/jsx-runtime";
22
- import { useI18n as k } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
15
+ import { useListUsersQuery as ne, useUpdateUserMutation as _ } from "../../generated/global-operations.js";
16
+ import { useAdminStore as v } from "../store/adminStore.js";
17
+ import { UserStatus as y } from "../../generated/global-types.js";
18
+ import { useCallback as b, useMemo as x, useState as S } from "react";
19
+ import { Search as C, Users as w } from "lucide-react";
20
+ import { useNavigate as T } from "react-router-dom";
21
+ import { jsx as E, jsxs as D } from "react/jsx-runtime";
22
+ import { useI18n as O } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
23
+ import { EmphasisPanel as k, PagePurpose as A } from "@burdenoff/fe-libs/ui";
23
24
  //#region src/admin/pages/UsersListPage.tsx
24
- var A = 20;
25
- function j() {
26
- let { canViewUsers: j, canUpdateUsers: M, isLoading: N } = n(), P = E(), { basePath: F = "" } = t(), { userFilter: I, updateUserFilter: L } = y(), R = e(), { t: z } = k(), B = x((e, t, n) => f(z, e, t, n), [z]), [V, H] = C(1), U = S(() => {
25
+ var j = 20;
26
+ function M() {
27
+ let { canViewUsers: M, canUpdateUsers: N, isLoading: P } = n(), F = T(), { basePath: I = "" } = t(), { userFilter: L, updateUserFilter: R } = v(), z = e(), { t: B } = O(), V = b((e, t, n) => f(B, e, t, n), [B]), [H, U] = S(1), W = x(() => {
27
28
  let e = {};
28
- I.search && (e.search = I.search);
29
+ L.search && (e.search = L.search);
29
30
  let t = {
30
- active: b.Active,
31
- inactive: b.Inactive,
32
- blocked: b.Suspended
31
+ active: y.Active,
32
+ inactive: y.Inactive,
33
+ blocked: y.Suspended
33
34
  };
34
- return I.status !== "all" && t[I.status] && (e.status = t[I.status]), e;
35
- }, [I.search, I.status]), W = N || !j, { data: G, loading: K, error: q, refetch: J } = _({
35
+ return L.status !== "all" && t[L.status] && (e.status = t[L.status]), e;
36
+ }, [L.search, L.status]), G = P || !M, { data: K, loading: q, error: J, refetch: Y } = ne({
36
37
  variables: {
37
- filter: U,
38
+ filter: W,
38
39
  pagination: {
39
- limit: A,
40
- offset: (V - 1) * A
40
+ limit: j,
41
+ offset: (H - 1) * j
41
42
  }
42
43
  },
43
44
  fetchPolicy: "cache-and-network",
44
- skip: W
45
- }), [Y, { loading: X }] = v({
45
+ skip: G
46
+ }), [X, { loading: Z }] = _({
46
47
  onCompleted: () => {
47
- R.success(B("admin.users.toast.updatedTitle", "User updated"), B("admin.users.toast.updatedDescription", "User status has been successfully updated.")), J();
48
+ z.success(V("admin.users.toast.updatedTitle", "User updated"), V("admin.users.toast.updatedDescription", "User status has been successfully updated.")), Y();
48
49
  },
49
50
  onError: (e) => {
50
- R.error(B("admin.users.toast.updateFailedTitle", "Update failed"), e.message);
51
+ z.error(V("admin.users.toast.updateFailedTitle", "Update failed"), e.message);
51
52
  }
52
- }), Z = x(async (e) => {
53
- let t = e.status === "active" ? b.Inactive : b.Active;
54
- await Y({ variables: {
53
+ }), Q = b(async (e) => {
54
+ let t = e.status === "active" ? y.Inactive : y.Active;
55
+ await X({ variables: {
55
56
  id: e.id,
56
57
  input: { status: t }
57
58
  } });
58
- }, [Y]), Q = S(() => (G?.users?.users ?? []).map(g), [G]), $ = G?.users?.totalCount ?? 0, te = Math.ceil($ / A), ne = x((e) => {
59
+ }, [X]), re = x(() => (K?.users?.users ?? []).map(g), [K]), $ = K?.users?.totalCount ?? 0, ie = Math.ceil($ / j), ae = b((e) => {
59
60
  let t = [{
60
61
  key: "view",
61
- label: B("admin.users.actions.viewDetails", "View Details"),
62
- onClick: () => P(h(F, `users/${e.id}`))
62
+ label: V("admin.users.actions.viewDetails", "View Details"),
63
+ onClick: () => F(h(I, `users/${e.id}`))
63
64
  }];
64
- return M && t.push({
65
+ return N && t.push({
65
66
  key: "toggle-status",
66
- label: e.status === "active" ? B("admin.users.actions.disableUser", "Disable User") : B("admin.users.actions.enableUser", "Enable User"),
67
- onClick: () => Z(e)
67
+ label: e.status === "active" ? V("admin.users.actions.disableUser", "Disable User") : V("admin.users.actions.enableUser", "Enable User"),
68
+ onClick: () => Q(e)
68
69
  }), t;
69
70
  }, [
70
- P,
71
71
  F,
72
- M,
73
- Z,
74
- B
75
- ]), re = [
72
+ I,
73
+ N,
74
+ Q,
75
+ V
76
+ ]), oe = [
76
77
  {
77
78
  key: "name",
78
- header: B("admin.users.table.user", "User"),
79
- render: (e) => /* @__PURE__ */ O("div", {
79
+ header: V("admin.users.table.user", "User"),
80
+ render: (e) => /* @__PURE__ */ D("div", {
80
81
  className: "flex items-center gap-3",
81
- children: [/* @__PURE__ */ D(r, {
82
+ children: [/* @__PURE__ */ E(r, {
82
83
  fallback: (e.firstName?.[0] ?? e.email[0]).toUpperCase(),
83
84
  size: "sm"
84
- }), /* @__PURE__ */ O("div", {
85
+ }), /* @__PURE__ */ D("div", {
85
86
  className: "min-w-0",
86
- children: [/* @__PURE__ */ D("div", {
87
+ children: [/* @__PURE__ */ E("div", {
87
88
  className: "truncate font-medium text-text-primary",
88
89
  children: e.name || e.email
89
- }), /* @__PURE__ */ D("div", {
90
+ }), /* @__PURE__ */ E("div", {
90
91
  className: "truncate text-xs text-text-secondary",
91
92
  children: e.email
92
93
  })]
@@ -95,151 +96,165 @@ function j() {
95
96
  },
96
97
  {
97
98
  key: "status",
98
- header: B("admin.users.table.status", "Status"),
99
+ header: V("admin.users.table.status", "Status"),
99
100
  width: "120px",
100
- render: (e) => /* @__PURE__ */ D(c, { status: e.status })
101
+ render: (e) => /* @__PURE__ */ E(c, { status: e.status })
101
102
  },
102
103
  {
103
104
  key: "role",
104
- header: B("admin.users.table.role", "Role"),
105
+ header: V("admin.users.table.role", "Role"),
105
106
  width: "120px"
106
107
  },
107
108
  {
108
109
  key: "tenantId",
109
- header: B("admin.users.table.tenant", "Tenant"),
110
+ header: V("admin.users.table.tenant", "Tenant"),
110
111
  width: "150px",
111
- render: (e) => /* @__PURE__ */ D("span", {
112
+ render: (e) => /* @__PURE__ */ E("span", {
112
113
  className: "text-xs text-text-secondary",
113
114
  children: e.tenantId || "—"
114
115
  })
115
116
  },
116
117
  {
117
118
  key: "createdAt",
118
- header: B("admin.users.table.joined", "Joined"),
119
+ header: V("admin.users.table.joined", "Joined"),
119
120
  width: "120px",
120
121
  sortable: !0,
121
- render: (e) => ee(e.createdAt)
122
+ render: (e) => l(e.createdAt)
122
123
  },
123
124
  {
124
125
  key: "lastLoginAt",
125
- header: B("admin.users.table.lastLogin", "Last Login"),
126
+ header: V("admin.users.table.lastLogin", "Last Login"),
126
127
  width: "130px",
127
- render: (e) => e.lastLoginAt ? l(e.lastLoginAt) : B("admin.users.table.never", "Never")
128
+ render: (e) => e.lastLoginAt ? ee(e.lastLoginAt) : V("admin.users.table.never", "Never")
128
129
  },
129
130
  {
130
131
  key: "actions",
131
132
  header: "",
132
133
  width: "50px",
133
134
  align: "right",
134
- render: (e) => /* @__PURE__ */ D(p, {
135
- actions: ne(e),
136
- ariaLabel: B("admin.users.table.actionsFor", "Actions for {{email}}", { email: e.email })
135
+ render: (e) => /* @__PURE__ */ E(p, {
136
+ actions: ae(e),
137
+ ariaLabel: V("admin.users.table.actionsFor", "Actions for {{email}}", { email: e.email })
137
138
  })
138
139
  }
139
140
  ];
140
- return /* @__PURE__ */ D(m, {
141
- allowed: j,
142
- isLoading: N,
143
- pageTitle: B("admin.users.title", "Users"),
144
- denyMessage: B("admin.users.denyMessage", "You don't have permission to view users."),
145
- children: /* @__PURE__ */ D(s, {
146
- title: B("admin.users.title", "Users"),
147
- gradientWord: B("admin.users.title", "Users"),
148
- description: $ ? B("admin.users.descriptionWithTotal", "Manage all users on the platform · {{total}} total", { total: $ }) : B("admin.users.description", "Manage all users on the platform"),
149
- children: /* @__PURE__ */ O("div", {
141
+ return /* @__PURE__ */ E(m, {
142
+ allowed: M,
143
+ isLoading: P,
144
+ pageTitle: V("admin.users.title", "Users"),
145
+ denyMessage: V("admin.users.denyMessage", "You don't have permission to view users."),
146
+ children: /* @__PURE__ */ E(s, {
147
+ title: V("admin.users.title", "Users"),
148
+ gradientWord: V("admin.users.title", "Users"),
149
+ description: $ ? V("admin.users.descriptionWithTotal", "Manage all users on the platform · {{total}} total", { total: $ }) : V("admin.users.description", "Manage all users on the platform"),
150
+ children: /* @__PURE__ */ D("div", {
150
151
  className: "space-y-4",
151
152
  children: [
152
- /* @__PURE__ */ O("div", {
153
+ /* @__PURE__ */ E(A, { children: V("admin.users.purpose", "Every person with an account across all tenants lives here. Use this page to find someone by name or email, check when they last signed in, and enable or disable their access when they join, leave, or need to be locked out.") }),
154
+ /* @__PURE__ */ D(k, {
155
+ className: "flex items-center gap-4 p-5",
156
+ children: [/* @__PURE__ */ E("div", {
157
+ className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
158
+ children: /* @__PURE__ */ E(w, { className: "h-6 w-6" })
159
+ }), /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("div", {
160
+ className: "text-3xl font-bold tabular-nums text-text-primary",
161
+ children: u($)
162
+ }), /* @__PURE__ */ E("p", {
163
+ className: "text-sm text-text-secondary",
164
+ children: V("admin.users.emphasis.label", "users across all tenants")
165
+ })] })]
166
+ }),
167
+ /* @__PURE__ */ D("div", {
153
168
  className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
154
- children: [/* @__PURE__ */ O("div", {
169
+ children: [/* @__PURE__ */ D("div", {
155
170
  className: "relative max-w-sm flex-1",
156
- children: [/* @__PURE__ */ D(w, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ D(o, {
157
- placeholder: B("admin.users.searchPlaceholder", "Search by email or name..."),
158
- value: I.search,
171
+ children: [/* @__PURE__ */ E(C, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ E(o, {
172
+ placeholder: V("admin.users.searchPlaceholder", "Search by email or name..."),
173
+ value: L.search,
159
174
  onChange: (e) => {
160
- H(1), L({ search: e.target.value });
175
+ U(1), R({ search: e.target.value });
161
176
  },
162
177
  className: "pl-9"
163
178
  })]
164
- }), /* @__PURE__ */ O("div", {
179
+ }), /* @__PURE__ */ D("div", {
165
180
  className: "flex items-center gap-2",
166
- children: [/* @__PURE__ */ O("select", {
167
- value: I.status,
181
+ children: [/* @__PURE__ */ D("select", {
182
+ value: L.status,
168
183
  onChange: (e) => {
169
- H(1), L({ status: e.target.value });
184
+ U(1), R({ status: e.target.value });
170
185
  },
171
186
  className: "h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2",
172
187
  children: [
173
- /* @__PURE__ */ D("option", {
188
+ /* @__PURE__ */ E("option", {
174
189
  value: "all",
175
- children: B("admin.users.filters.status.all", "All Status")
190
+ children: V("admin.users.filters.status.all", "All Status")
176
191
  }),
177
- /* @__PURE__ */ D("option", {
192
+ /* @__PURE__ */ E("option", {
178
193
  value: "active",
179
- children: B("admin.users.filters.status.active", "Active")
194
+ children: V("admin.users.filters.status.active", "Active")
180
195
  }),
181
- /* @__PURE__ */ D("option", {
196
+ /* @__PURE__ */ E("option", {
182
197
  value: "inactive",
183
- children: B("admin.users.filters.status.inactive", "Inactive")
198
+ children: V("admin.users.filters.status.inactive", "Inactive")
184
199
  }),
185
- /* @__PURE__ */ D("option", {
200
+ /* @__PURE__ */ E("option", {
186
201
  value: "blocked",
187
- children: B("admin.users.filters.status.suspended", "Suspended")
202
+ children: V("admin.users.filters.status.suspended", "Suspended")
188
203
  })
189
204
  ]
190
- }), (I.search || I.status !== "all") && /* @__PURE__ */ D(i, {
205
+ }), (L.search || L.status !== "all") && /* @__PURE__ */ E(i, {
191
206
  variant: "ghost",
192
207
  onClick: () => {
193
- H(1), L({
208
+ U(1), R({
194
209
  search: "",
195
210
  status: "all"
196
211
  });
197
212
  },
198
- children: B("admin.users.filters.clear", "Clear")
213
+ children: V("admin.users.filters.clear", "Clear")
199
214
  })]
200
215
  })]
201
216
  }),
202
- q && /* @__PURE__ */ O("div", {
217
+ J && /* @__PURE__ */ D("div", {
203
218
  className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
204
219
  children: [
205
- B("admin.users.error.loadFailed", "Failed to load users"),
220
+ V("admin.users.error.loadFailed", "Failed to load users"),
206
221
  ": ",
207
- q.message
222
+ J.message
208
223
  ]
209
224
  }),
210
- /* @__PURE__ */ D(u, {
211
- columns: re,
212
- data: Q,
225
+ /* @__PURE__ */ E(te, {
226
+ columns: oe,
227
+ data: re,
213
228
  keyExtractor: (e) => e.id,
214
- onRowClick: (e) => P(h(F, `users/${e.id}`)),
215
- sortBy: I.sortBy,
216
- sortOrder: I.sortOrder,
217
- onSort: (e, t) => L({
229
+ onRowClick: (e) => F(h(I, `users/${e.id}`)),
230
+ sortBy: L.sortBy,
231
+ sortOrder: L.sortOrder,
232
+ onSort: (e, t) => R({
218
233
  sortBy: e,
219
234
  sortOrder: t
220
235
  }),
221
- loading: K || X,
222
- emptyState: /* @__PURE__ */ D(a, {
236
+ loading: q || Z,
237
+ emptyState: /* @__PURE__ */ E(a, {
223
238
  illustration: "empty-people",
224
- icon: /* @__PURE__ */ D(T, { className: "h-8 w-8" }),
225
- title: B("admin.users.empty.title", "No users found"),
226
- description: B("admin.users.empty.description", "Try adjusting your search or filters"),
227
- action: /* @__PURE__ */ D(i, {
239
+ icon: /* @__PURE__ */ E(w, { className: "h-8 w-8" }),
240
+ title: V("admin.users.empty.title", "No users found"),
241
+ description: V("admin.users.empty.description", "Try adjusting your search or filters"),
242
+ action: /* @__PURE__ */ E(i, {
228
243
  variant: "outline",
229
- onClick: () => L({
244
+ onClick: () => R({
230
245
  search: "",
231
246
  status: "all"
232
247
  }),
233
- children: B("admin.users.empty.clearFilters", "Clear filters")
248
+ children: V("admin.users.empty.clearFilters", "Clear filters")
234
249
  })
235
250
  })
236
251
  }),
237
- $ > A && /* @__PURE__ */ D(d, {
238
- page: V,
239
- totalPages: te,
252
+ $ > j && /* @__PURE__ */ E(d, {
253
+ page: H,
254
+ totalPages: ie,
240
255
  totalItems: $,
241
- pageSize: A,
242
- onPageChange: H
256
+ pageSize: j,
257
+ onPageChange: U
243
258
  })
244
259
  ]
245
260
  })
@@ -247,6 +262,6 @@ function j() {
247
262
  });
248
263
  }
249
264
  //#endregion
250
- export { j as UsersListPage };
265
+ export { M as UsersListPage };
251
266
 
252
267
  //# sourceMappingURL=UsersListPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"UsersListPage.js","names":[],"sources":["../../../src/admin/pages/UsersListPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Search, Users as UsersIcon } from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminStore } from '../store/adminStore';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n UserStatusBadge,\n EmptyState,\n Avatar,\n Button,\n RowActions,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate, formatRelativeTime } from '../utils/formatters';\nimport { mapUserToUi } from '../utils/mappers';\nimport type { User, UserFilter } from '../types';\nimport { useListUsersQuery, useUpdateUserMutation } from '../../generated/global-operations';\nimport { UserStatus as GqlUserStatus } from '../../generated/global-types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\nconst PAGE_SIZE = 20;\n\n/**\n * Users list page — connected to real backend with enable/disable actions\n */\nexport function UsersListPage() {\n const { canViewUsers, canUpdateUsers, isLoading: permissionsLoading } = useAdminPermissions();\n\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const { userFilter, updateUserFilter } = useAdminStore();\n const toast = useToast();\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const [page, setPage] = useState(1);\n\n // Build GraphQL filter\n const gqlFilter = useMemo(() => {\n const f: {\n search?: string;\n status?: GqlUserStatus;\n } = {};\n if (userFilter.search) f.search = userFilter.search;\n const statusMap: Record<string, GqlUserStatus> = {\n active: GqlUserStatus.Active,\n inactive: GqlUserStatus.Inactive,\n blocked: GqlUserStatus.Suspended,\n };\n if (userFilter.status !== 'all' && statusMap[userFilter.status]) {\n f.status = statusMap[userFilter.status];\n }\n return f;\n }, [userFilter.search, userFilter.status]);\n\n const skipQueries = permissionsLoading || !canViewUsers;\n\n const { data, loading, error, refetch } = useListUsersQuery({\n variables: {\n filter: gqlFilter,\n pagination: {\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [updateUser, { loading: isUpdating }] = useUpdateUserMutation({\n onCompleted: () => {\n toast.success(\n tr('admin.users.toast.updatedTitle', 'User updated'),\n tr('admin.users.toast.updatedDescription', 'User status has been successfully updated.')\n );\n void refetch();\n },\n onError: (err) => {\n toast.error(tr('admin.users.toast.updateFailedTitle', 'Update failed'), err.message);\n },\n });\n\n const handleToggleStatus = useCallback(\n async (user: User) => {\n const newStatus = user.status === 'active' ? GqlUserStatus.Inactive : GqlUserStatus.Active;\n await updateUser({\n variables: {\n id: user.id,\n input: {\n status: newStatus,\n },\n },\n });\n },\n [updateUser]\n );\n\n const users: User[] = useMemo(() => (data?.users?.users ?? []).map(mapUserToUi), [data]);\n\n const total = data?.users?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const getRowActions = useCallback(\n (user: User) => {\n const actions = [\n {\n key: 'view',\n label: tr('admin.users.actions.viewDetails', 'View Details'),\n onClick: () => navigate(joinPath(basePath, `users/${user.id}`)),\n },\n ];\n\n if (canUpdateUsers) {\n actions.push({\n key: 'toggle-status',\n label:\n user.status === 'active'\n ? tr('admin.users.actions.disableUser', 'Disable User')\n : tr('admin.users.actions.enableUser', 'Enable User'),\n onClick: () => handleToggleStatus(user),\n });\n }\n\n return actions;\n },\n [navigate, basePath, canUpdateUsers, handleToggleStatus, tr]\n );\n\n const columns = [\n {\n key: 'name',\n header: tr('admin.users.table.user', 'User'),\n render: (user: User) => (\n <div className=\"flex items-center gap-3\">\n <Avatar fallback={(user.firstName?.[0] ?? user.email[0]).toUpperCase()} size=\"sm\" />\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-text-primary\">{user.name || user.email}</div>\n <div className=\"truncate text-xs text-text-secondary\">{user.email}</div>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('admin.users.table.status', 'Status'),\n width: '120px',\n render: (user: User) => <UserStatusBadge status={user.status} />,\n },\n {\n key: 'role',\n header: tr('admin.users.table.role', 'Role'),\n width: '120px',\n },\n {\n key: 'tenantId',\n header: tr('admin.users.table.tenant', 'Tenant'),\n width: '150px',\n render: (user: User) => (\n <span className=\"text-xs text-text-secondary\">{user.tenantId || '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: tr('admin.users.table.joined', 'Joined'),\n width: '120px',\n sortable: true,\n render: (user: User) => formatDate(user.createdAt),\n },\n {\n key: 'lastLoginAt',\n header: tr('admin.users.table.lastLogin', 'Last Login'),\n width: '130px',\n render: (user: User) =>\n user.lastLoginAt\n ? formatRelativeTime(user.lastLoginAt)\n : tr('admin.users.table.never', 'Never'),\n },\n {\n key: 'actions',\n header: '',\n width: '50px',\n align: 'right' as const,\n render: (user: User) => (\n <RowActions\n actions={getRowActions(user)}\n ariaLabel={tr('admin.users.table.actionsFor', 'Actions for {{email}}', {\n email: user.email,\n })}\n />\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewUsers}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.users.title', 'Users')}\n denyMessage={tr('admin.users.denyMessage', \"You don't have permission to view users.\")}\n >\n <PageLayout\n title={tr('admin.users.title', 'Users')}\n gradientWord={tr('admin.users.title', 'Users')}\n description={\n total\n ? tr(\n 'admin.users.descriptionWithTotal',\n 'Manage all users on the platform · {{total}} total',\n { total }\n )\n : tr('admin.users.description', 'Manage all users on the platform')\n }\n >\n <div className=\"space-y-4\">\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder={tr('admin.users.searchPlaceholder', 'Search by email or name...')}\n value={userFilter.search}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ search: e.target.value });\n }}\n className=\"pl-9\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={userFilter.status}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ status: e.target.value as UserFilter['status'] });\n }}\n className=\"h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\"\n >\n <option value=\"all\">{tr('admin.users.filters.status.all', 'All Status')}</option>\n <option value=\"active\">{tr('admin.users.filters.status.active', 'Active')}</option>\n <option value=\"inactive\">\n {tr('admin.users.filters.status.inactive', 'Inactive')}\n </option>\n <option value=\"blocked\">\n {tr('admin.users.filters.status.suspended', 'Suspended')}\n </option>\n </select>\n {(userFilter.search || userFilter.status !== 'all') && (\n <Button\n variant=\"ghost\"\n onClick={() => {\n setPage(1);\n updateUserFilter({ search: '', status: 'all' });\n }}\n >\n {tr('admin.users.filters.clear', 'Clear')}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {tr('admin.users.error.loadFailed', 'Failed to load users')}: {error.message}\n </div>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={users}\n keyExtractor={(u) => u.id}\n onRowClick={(u) => navigate(joinPath(basePath, `users/${u.id}`))}\n sortBy={userFilter.sortBy}\n sortOrder={userFilter.sortOrder}\n onSort={(column, order) =>\n updateUserFilter({\n sortBy: column as UserFilter['sortBy'],\n sortOrder: order,\n })\n }\n loading={loading || isUpdating}\n emptyState={\n <EmptyState\n illustration=\"empty-people\"\n icon={<UsersIcon className=\"h-8 w-8\" />}\n title={tr('admin.users.empty.title', 'No users found')}\n description={tr(\n 'admin.users.empty.description',\n 'Try adjusting your search or filters'\n )}\n action={\n <Button\n variant=\"outline\"\n onClick={() => updateUserFilter({ search: '', status: 'all' })}\n >\n {tr('admin.users.empty.clearFilters', 'Clear filters')}\n </Button>\n }\n />\n }\n />\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"UsersListPage.js","names":[],"sources":["../../../src/admin/pages/UsersListPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Search, Users as UsersIcon } from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminStore } from '../store/adminStore';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n UserStatusBadge,\n EmptyState,\n Avatar,\n Button,\n RowActions,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate, formatRelativeTime, formatNumber } from '../utils/formatters';\nimport { mapUserToUi } from '../utils/mappers';\nimport type { User, UserFilter } from '../types';\nimport { useListUsersQuery, useUpdateUserMutation } from '../../generated/global-operations';\nimport { UserStatus as GqlUserStatus } from '../../generated/global-types';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\nconst PAGE_SIZE = 20;\n\n/**\n * Users list page — connected to real backend with enable/disable actions\n */\nexport function UsersListPage() {\n const { canViewUsers, canUpdateUsers, isLoading: permissionsLoading } = useAdminPermissions();\n\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const { userFilter, updateUserFilter } = useAdminStore();\n const toast = useToast();\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const [page, setPage] = useState(1);\n\n // Build GraphQL filter\n const gqlFilter = useMemo(() => {\n const f: {\n search?: string;\n status?: GqlUserStatus;\n } = {};\n if (userFilter.search) f.search = userFilter.search;\n const statusMap: Record<string, GqlUserStatus> = {\n active: GqlUserStatus.Active,\n inactive: GqlUserStatus.Inactive,\n blocked: GqlUserStatus.Suspended,\n };\n if (userFilter.status !== 'all' && statusMap[userFilter.status]) {\n f.status = statusMap[userFilter.status];\n }\n return f;\n }, [userFilter.search, userFilter.status]);\n\n const skipQueries = permissionsLoading || !canViewUsers;\n\n const { data, loading, error, refetch } = useListUsersQuery({\n variables: {\n filter: gqlFilter,\n pagination: {\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [updateUser, { loading: isUpdating }] = useUpdateUserMutation({\n onCompleted: () => {\n toast.success(\n tr('admin.users.toast.updatedTitle', 'User updated'),\n tr('admin.users.toast.updatedDescription', 'User status has been successfully updated.')\n );\n void refetch();\n },\n onError: (err) => {\n toast.error(tr('admin.users.toast.updateFailedTitle', 'Update failed'), err.message);\n },\n });\n\n const handleToggleStatus = useCallback(\n async (user: User) => {\n const newStatus = user.status === 'active' ? GqlUserStatus.Inactive : GqlUserStatus.Active;\n await updateUser({\n variables: {\n id: user.id,\n input: {\n status: newStatus,\n },\n },\n });\n },\n [updateUser]\n );\n\n const users: User[] = useMemo(() => (data?.users?.users ?? []).map(mapUserToUi), [data]);\n\n const total = data?.users?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const getRowActions = useCallback(\n (user: User) => {\n const actions = [\n {\n key: 'view',\n label: tr('admin.users.actions.viewDetails', 'View Details'),\n onClick: () => navigate(joinPath(basePath, `users/${user.id}`)),\n },\n ];\n\n if (canUpdateUsers) {\n actions.push({\n key: 'toggle-status',\n label:\n user.status === 'active'\n ? tr('admin.users.actions.disableUser', 'Disable User')\n : tr('admin.users.actions.enableUser', 'Enable User'),\n onClick: () => handleToggleStatus(user),\n });\n }\n\n return actions;\n },\n [navigate, basePath, canUpdateUsers, handleToggleStatus, tr]\n );\n\n const columns = [\n {\n key: 'name',\n header: tr('admin.users.table.user', 'User'),\n render: (user: User) => (\n <div className=\"flex items-center gap-3\">\n <Avatar fallback={(user.firstName?.[0] ?? user.email[0]).toUpperCase()} size=\"sm\" />\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-text-primary\">{user.name || user.email}</div>\n <div className=\"truncate text-xs text-text-secondary\">{user.email}</div>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('admin.users.table.status', 'Status'),\n width: '120px',\n render: (user: User) => <UserStatusBadge status={user.status} />,\n },\n {\n key: 'role',\n header: tr('admin.users.table.role', 'Role'),\n width: '120px',\n },\n {\n key: 'tenantId',\n header: tr('admin.users.table.tenant', 'Tenant'),\n width: '150px',\n render: (user: User) => (\n <span className=\"text-xs text-text-secondary\">{user.tenantId || '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: tr('admin.users.table.joined', 'Joined'),\n width: '120px',\n sortable: true,\n render: (user: User) => formatDate(user.createdAt),\n },\n {\n key: 'lastLoginAt',\n header: tr('admin.users.table.lastLogin', 'Last Login'),\n width: '130px',\n render: (user: User) =>\n user.lastLoginAt\n ? formatRelativeTime(user.lastLoginAt)\n : tr('admin.users.table.never', 'Never'),\n },\n {\n key: 'actions',\n header: '',\n width: '50px',\n align: 'right' as const,\n render: (user: User) => (\n <RowActions\n actions={getRowActions(user)}\n ariaLabel={tr('admin.users.table.actionsFor', 'Actions for {{email}}', {\n email: user.email,\n })}\n />\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewUsers}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.users.title', 'Users')}\n denyMessage={tr('admin.users.denyMessage', \"You don't have permission to view users.\")}\n >\n <PageLayout\n title={tr('admin.users.title', 'Users')}\n gradientWord={tr('admin.users.title', 'Users')}\n description={\n total\n ? tr(\n 'admin.users.descriptionWithTotal',\n 'Manage all users on the platform · {{total}} total',\n { total }\n )\n : tr('admin.users.description', 'Manage all users on the platform')\n }\n >\n <div className=\"space-y-4\">\n <PagePurpose>\n {tr(\n 'admin.users.purpose',\n 'Every person with an account across all tenants lives here. Use this page to find someone by name or email, check when they last signed in, and enable or disable their access when they join, leave, or need to be locked out.'\n )}\n </PagePurpose>\n\n {/* Emphasis: total user count — the headline stat for this page. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <UsersIcon className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">\n {formatNumber(total)}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr('admin.users.emphasis.label', 'users across all tenants')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder={tr('admin.users.searchPlaceholder', 'Search by email or name...')}\n value={userFilter.search}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ search: e.target.value });\n }}\n className=\"pl-9\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={userFilter.status}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ status: e.target.value as UserFilter['status'] });\n }}\n className=\"h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\"\n >\n <option value=\"all\">{tr('admin.users.filters.status.all', 'All Status')}</option>\n <option value=\"active\">{tr('admin.users.filters.status.active', 'Active')}</option>\n <option value=\"inactive\">\n {tr('admin.users.filters.status.inactive', 'Inactive')}\n </option>\n <option value=\"blocked\">\n {tr('admin.users.filters.status.suspended', 'Suspended')}\n </option>\n </select>\n {(userFilter.search || userFilter.status !== 'all') && (\n <Button\n variant=\"ghost\"\n onClick={() => {\n setPage(1);\n updateUserFilter({ search: '', status: 'all' });\n }}\n >\n {tr('admin.users.filters.clear', 'Clear')}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {tr('admin.users.error.loadFailed', 'Failed to load users')}: {error.message}\n </div>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={users}\n keyExtractor={(u) => u.id}\n onRowClick={(u) => navigate(joinPath(basePath, `users/${u.id}`))}\n sortBy={userFilter.sortBy}\n sortOrder={userFilter.sortOrder}\n onSort={(column, order) =>\n updateUserFilter({\n sortBy: column as UserFilter['sortBy'],\n sortOrder: order,\n })\n }\n loading={loading || isUpdating}\n emptyState={\n <EmptyState\n illustration=\"empty-people\"\n icon={<UsersIcon className=\"h-8 w-8\" />}\n title={tr('admin.users.empty.title', 'No users found')}\n description={tr(\n 'admin.users.empty.description',\n 'Try adjusting your search or filters'\n )}\n action={\n <Button\n variant=\"outline\"\n onClick={() => updateUserFilter({ search: '', status: 'all' })}\n >\n {tr('admin.users.empty.clearFilters', 'Clear filters')}\n </Button>\n }\n />\n }\n />\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}