@burdenoff/microfe-admin 2026.720.1 → 2026.801.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.
@@ -4,7 +4,7 @@ import { DashboardPage as n } from "./pages/DashboardPage.js";
4
4
  import { TENANT_FEATURE_FLAGS as r } from "./constants/featureFlags.js";
5
5
  import "./pages/index.js";
6
6
  import { Suspense as i, lazy as a } from "react";
7
- import { Navigate as o, Route as s, Routes as c } from "react-router-dom";
7
+ import { Navigate as o, Route as s, Routes as c } from "react-router";
8
8
  import { jsx as l, jsxs as u } from "react/jsx-runtime";
9
9
  import { FeatureGate as d } from "@burdenoff/fe-libs/shared/feature-flags";
10
10
  import { useI18n as f } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
@@ -1 +1 @@
1
- {"version":3,"file":"AdminRoutes.js","names":[],"sources":["../../src/admin/AdminRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { Navigate, Route, Routes } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DashboardPage } from './pages';\nimport { useAdmin } from './providers/AdminProvider';\nimport { TENANT_FEATURE_FLAGS } from './constants/featureFlags';\nimport { tWithFallback } from './utils/i18n';\n\n// Lazy loaded pages\nconst TenantsListPage = lazy(() =>\n import('./pages/TenantsListPage').then((m) => ({ default: m.TenantsListPage }))\n);\nconst TenantDetailPage = lazy(() =>\n import('./pages/TenantDetailPage').then((m) => ({ default: m.TenantDetailPage }))\n);\nconst TenantFormRoutePage = lazy(() =>\n import('./pages/TenantFormRoutePage').then((m) => ({ default: m.TenantFormRoutePage }))\n);\nconst UsersListPage = lazy(() =>\n import('./pages/UsersListPage').then((m) => ({ default: m.UsersListPage }))\n);\nconst BillingPage = lazy(() =>\n import('./pages/BillingPage').then((m) => ({ default: m.BillingPage }))\n);\nconst SettingsPage = lazy(() =>\n import('./pages/SettingsPage').then((m) => ({ default: m.SettingsPage }))\n);\nconst CommunicationsPage = lazy(() =>\n import('./pages/CommunicationsPage').then((m) => ({ default: m.CommunicationsPage }))\n);\nconst SREPage = lazy(() => import('./pages/SREPage').then((m) => ({ default: m.SREPage })));\nconst SyncFailuresPage = lazy(() =>\n import('./pages/SyncFailuresPage').then((m) => ({ default: m.SyncFailuresPage }))\n);\nconst AuditLogsPage = lazy(() =>\n import('./pages/AuditLogsPage').then((m) => ({ default: m.AuditLogsPage }))\n);\nconst SandboxSettingsPage = lazy(() =>\n import('./pages/SandboxSettingsPage').then((m) => ({ default: m.SandboxSettingsPage }))\n);\nconst OrganizationsListPage = lazy(() =>\n import('./pages/OrganizationsListPage').then((m) => ({ default: m.OrganizationsListPage }))\n);\nconst WorkspacesListPage = lazy(() =>\n import('./pages/WorkspacesListPage').then((m) => ({ default: m.WorkspacesListPage }))\n);\nconst FeatureFlagsPage = lazy(() =>\n import('./pages/FeatureFlagsPage').then((m) => ({ default: m.FeatureFlagsPage }))\n);\nconst RolesPermissionsPage = lazy(() =>\n import('./pages/RolesPermissionsPage').then((m) => ({ default: m.RolesPermissionsPage }))\n);\nconst WorkflowsPage = lazy(() =>\n import('./pages/WorkflowsPage').then((m) => ({ default: m.WorkflowsPage }))\n);\nconst ProductsPage = lazy(() =>\n import('./pages/ProductsPage').then((m) => ({ default: m.ProductsPage }))\n);\nconst ContentPagesPage = lazy(() =>\n import('./pages/ContentPagesPage').then((m) => ({ default: m.ContentPagesPage }))\n);\nconst WaitlistPage = lazy(() =>\n import('./pages/WaitlistPage').then((m) => ({ default: m.WaitlistPage }))\n);\nconst ReferralsPage = lazy(() =>\n import('./pages/ReferralsPage').then((m) => ({ default: m.ReferralsPage }))\n);\n\n// Loading fallback component\nfunction PageLoader() {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\" />\n </div>\n );\n}\n\nfunction FeatureDisabledFallback() {\n const { t } = useI18n();\n\n return (\n <div className=\"flex min-h-[400px] items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"mb-2 text-lg font-semibold text-foreground\">\n {tWithFallback(\n t,\n 'admin.routes.tenantFeatureDisabledTitle',\n 'Tenant management unavailable'\n )}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'admin.routes.tenantFeatureDisabled',\n 'This feature is currently disabled.'\n )}\n </p>\n </div>\n </div>\n );\n}\n\nfunction gated(flag: string, element: ReactNode, defaultValue: boolean): ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\nfunction gatedAll(\n gates: Array<{ flag: string; defaultValue: boolean }>,\n element: ReactElement\n): ReactElement {\n return gates.reduceRight((child, gate) => gated(gate.flag, child, gate.defaultValue), element);\n}\n\n/**\n * Internal router for the Admin microfrontend.\n * This component handles routing within the microfrontend using React Router v7.\n *\n * Routes:\n * - / → Dashboard (redirect)\n * - /dashboard → Dashboard\n * - /tenants → Tenants list\n * - /tenants/new → Create new tenant\n * - /tenants/:tenantId → Tenant detail\n * - /tenants/:tenantId/edit → Edit tenant\n * - /users → Users list\n * - /users/:userId → User detail\n * - /billing → Billing overview\n * - /billing/subscriptions → Subscriptions list\n * - /billing/invoices → Invoices list\n * - /billing/plans → Plans management\n * - /support → Support overview\n * - /support/tickets → Tickets list\n * - /support/tickets/:ticketId → Ticket detail\n * - /products → Products management\n * - /communications → Communications center\n * - /sre → SRE Monitoring\n * - /audit → Audit logs\n * - /sandbox → Sandbox settings\n * - /settings → Settings\n */\nexport function AdminRoutes() {\n const { basePath } = useAdmin();\n const base = basePath || '/admin';\n\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect to dashboard */}\n <Route index element={<Navigate to={`${base}/dashboard`} replace />} />\n\n {/* Dashboard - eager loaded for fast initial render */}\n <Route path=\"dashboard\" element={<DashboardPage />} />\n\n {/* Tenants */}\n <Route\n path=\"tenants\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.list, defaultValue: true },\n ],\n <TenantsListPage />\n )}\n />\n <Route\n path=\"tenants/new\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.create, defaultValue: true },\n ],\n <TenantFormRoutePage mode=\"create\" />\n )}\n />\n <Route\n path=\"tenants/:tenantId\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.detail, defaultValue: true },\n ],\n <TenantDetailPage />\n )}\n />\n <Route\n path=\"tenants/:tenantId/edit\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.edit, defaultValue: true },\n ],\n <TenantFormRoutePage mode=\"edit\" />\n )}\n />\n\n {/* Organizations */}\n <Route path=\"organizations\" element={<OrganizationsListPage />} />\n\n {/* Workspaces */}\n <Route path=\"workspaces\" element={<WorkspacesListPage />} />\n\n {/* Users */}\n <Route path=\"users\" element={<UsersListPage />} />\n <Route path=\"users/:userId\" element={<UsersListPage />} />\n\n {/* Roles & Permissions */}\n <Route path=\"roles\" element={<RolesPermissionsPage />} />\n\n {/* Feature Flags */}\n <Route path=\"feature-flags\" element={<FeatureFlagsPage />} />\n\n {/* Workflows */}\n <Route path=\"workflows\" element={<WorkflowsPage />} />\n\n {/* Billing */}\n <Route path=\"billing\" element={<BillingPage />} />\n <Route path=\"billing/subscriptions\" element={<BillingPage />} />\n <Route path=\"billing/invoices\" element={<BillingPage />} />\n <Route path=\"billing/plans\" element={<BillingPage />} />\n\n {/* Support — redirects to shell-level /support/* route (microfe-support MFE) */}\n <Route path=\"support/*\" element={<Navigate to=\"/support\" replace />} />\n\n {/* Communications */}\n <Route path=\"communications\" element={<CommunicationsPage />} />\n\n {/* SRE Monitoring */}\n <Route path=\"sre\" element={<SREPage />} />\n\n {/* Sync Failures (cross-layer sync outbox) */}\n <Route path=\"sync-failures\" element={<SyncFailuresPage />} />\n\n {/* Audit Logs */}\n <Route path=\"audit\" element={<AuditLogsPage />} />\n\n {/* Referrals */}\n <Route path=\"referrals\" element={<ReferralsPage />} />\n\n {/* Sandbox */}\n <Route path=\"sandbox\" element={<SandboxSettingsPage />} />\n\n {/* Products */}\n <Route path=\"products\" element={<ProductsPage />} />\n\n {/* Content (blog / case studies / changelog) */}\n <Route path=\"content\" element={<ContentPagesPage />} />\n\n {/* Waitlist */}\n <Route path=\"waitlist\" element={<WaitlistPage />} />\n\n {/* Settings */}\n <Route path=\"settings\" element={<SettingsPage />} />\n\n {/* Catch-all route - redirect to dashboard (absolute path to prevent infinite loops) */}\n <Route path=\"*\" element={<Navigate to={`${base}/dashboard`} replace />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AdminRoutes.js","names":[],"sources":["../../src/admin/AdminRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DashboardPage } from './pages';\nimport { useAdmin } from './providers/AdminProvider';\nimport { TENANT_FEATURE_FLAGS } from './constants/featureFlags';\nimport { tWithFallback } from './utils/i18n';\n\n// Lazy loaded pages\nconst TenantsListPage = lazy(() =>\n import('./pages/TenantsListPage').then((m) => ({ default: m.TenantsListPage }))\n);\nconst TenantDetailPage = lazy(() =>\n import('./pages/TenantDetailPage').then((m) => ({ default: m.TenantDetailPage }))\n);\nconst TenantFormRoutePage = lazy(() =>\n import('./pages/TenantFormRoutePage').then((m) => ({ default: m.TenantFormRoutePage }))\n);\nconst UsersListPage = lazy(() =>\n import('./pages/UsersListPage').then((m) => ({ default: m.UsersListPage }))\n);\nconst BillingPage = lazy(() =>\n import('./pages/BillingPage').then((m) => ({ default: m.BillingPage }))\n);\nconst SettingsPage = lazy(() =>\n import('./pages/SettingsPage').then((m) => ({ default: m.SettingsPage }))\n);\nconst CommunicationsPage = lazy(() =>\n import('./pages/CommunicationsPage').then((m) => ({ default: m.CommunicationsPage }))\n);\nconst SREPage = lazy(() => import('./pages/SREPage').then((m) => ({ default: m.SREPage })));\nconst SyncFailuresPage = lazy(() =>\n import('./pages/SyncFailuresPage').then((m) => ({ default: m.SyncFailuresPage }))\n);\nconst AuditLogsPage = lazy(() =>\n import('./pages/AuditLogsPage').then((m) => ({ default: m.AuditLogsPage }))\n);\nconst SandboxSettingsPage = lazy(() =>\n import('./pages/SandboxSettingsPage').then((m) => ({ default: m.SandboxSettingsPage }))\n);\nconst OrganizationsListPage = lazy(() =>\n import('./pages/OrganizationsListPage').then((m) => ({ default: m.OrganizationsListPage }))\n);\nconst WorkspacesListPage = lazy(() =>\n import('./pages/WorkspacesListPage').then((m) => ({ default: m.WorkspacesListPage }))\n);\nconst FeatureFlagsPage = lazy(() =>\n import('./pages/FeatureFlagsPage').then((m) => ({ default: m.FeatureFlagsPage }))\n);\nconst RolesPermissionsPage = lazy(() =>\n import('./pages/RolesPermissionsPage').then((m) => ({ default: m.RolesPermissionsPage }))\n);\nconst WorkflowsPage = lazy(() =>\n import('./pages/WorkflowsPage').then((m) => ({ default: m.WorkflowsPage }))\n);\nconst ProductsPage = lazy(() =>\n import('./pages/ProductsPage').then((m) => ({ default: m.ProductsPage }))\n);\nconst ContentPagesPage = lazy(() =>\n import('./pages/ContentPagesPage').then((m) => ({ default: m.ContentPagesPage }))\n);\nconst WaitlistPage = lazy(() =>\n import('./pages/WaitlistPage').then((m) => ({ default: m.WaitlistPage }))\n);\nconst ReferralsPage = lazy(() =>\n import('./pages/ReferralsPage').then((m) => ({ default: m.ReferralsPage }))\n);\n\n// Loading fallback component\nfunction PageLoader() {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\" />\n </div>\n );\n}\n\nfunction FeatureDisabledFallback() {\n const { t } = useI18n();\n\n return (\n <div className=\"flex min-h-[400px] items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"mb-2 text-lg font-semibold text-foreground\">\n {tWithFallback(\n t,\n 'admin.routes.tenantFeatureDisabledTitle',\n 'Tenant management unavailable'\n )}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'admin.routes.tenantFeatureDisabled',\n 'This feature is currently disabled.'\n )}\n </p>\n </div>\n </div>\n );\n}\n\nfunction gated(flag: string, element: ReactNode, defaultValue: boolean): ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\nfunction gatedAll(\n gates: Array<{ flag: string; defaultValue: boolean }>,\n element: ReactElement\n): ReactElement {\n return gates.reduceRight((child, gate) => gated(gate.flag, child, gate.defaultValue), element);\n}\n\n/**\n * Internal router for the Admin microfrontend.\n * This component handles routing within the microfrontend using React Router v7.\n *\n * Routes:\n * - / → Dashboard (redirect)\n * - /dashboard → Dashboard\n * - /tenants → Tenants list\n * - /tenants/new → Create new tenant\n * - /tenants/:tenantId → Tenant detail\n * - /tenants/:tenantId/edit → Edit tenant\n * - /users → Users list\n * - /users/:userId → User detail\n * - /billing → Billing overview\n * - /billing/subscriptions → Subscriptions list\n * - /billing/invoices → Invoices list\n * - /billing/plans → Plans management\n * - /support → Support overview\n * - /support/tickets → Tickets list\n * - /support/tickets/:ticketId → Ticket detail\n * - /products → Products management\n * - /communications → Communications center\n * - /sre → SRE Monitoring\n * - /audit → Audit logs\n * - /sandbox → Sandbox settings\n * - /settings → Settings\n */\nexport function AdminRoutes() {\n const { basePath } = useAdmin();\n const base = basePath || '/admin';\n\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect to dashboard */}\n <Route index element={<Navigate to={`${base}/dashboard`} replace />} />\n\n {/* Dashboard - eager loaded for fast initial render */}\n <Route path=\"dashboard\" element={<DashboardPage />} />\n\n {/* Tenants */}\n <Route\n path=\"tenants\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.list, defaultValue: true },\n ],\n <TenantsListPage />\n )}\n />\n <Route\n path=\"tenants/new\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.create, defaultValue: true },\n ],\n <TenantFormRoutePage mode=\"create\" />\n )}\n />\n <Route\n path=\"tenants/:tenantId\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.detail, defaultValue: true },\n ],\n <TenantDetailPage />\n )}\n />\n <Route\n path=\"tenants/:tenantId/edit\"\n element={gatedAll(\n [\n { flag: TENANT_FEATURE_FLAGS.management, defaultValue: true },\n { flag: TENANT_FEATURE_FLAGS.edit, defaultValue: true },\n ],\n <TenantFormRoutePage mode=\"edit\" />\n )}\n />\n\n {/* Organizations */}\n <Route path=\"organizations\" element={<OrganizationsListPage />} />\n\n {/* Workspaces */}\n <Route path=\"workspaces\" element={<WorkspacesListPage />} />\n\n {/* Users */}\n <Route path=\"users\" element={<UsersListPage />} />\n <Route path=\"users/:userId\" element={<UsersListPage />} />\n\n {/* Roles & Permissions */}\n <Route path=\"roles\" element={<RolesPermissionsPage />} />\n\n {/* Feature Flags */}\n <Route path=\"feature-flags\" element={<FeatureFlagsPage />} />\n\n {/* Workflows */}\n <Route path=\"workflows\" element={<WorkflowsPage />} />\n\n {/* Billing */}\n <Route path=\"billing\" element={<BillingPage />} />\n <Route path=\"billing/subscriptions\" element={<BillingPage />} />\n <Route path=\"billing/invoices\" element={<BillingPage />} />\n <Route path=\"billing/plans\" element={<BillingPage />} />\n\n {/* Support — redirects to shell-level /support/* route (microfe-support MFE) */}\n <Route path=\"support/*\" element={<Navigate to=\"/support\" replace />} />\n\n {/* Communications */}\n <Route path=\"communications\" element={<CommunicationsPage />} />\n\n {/* SRE Monitoring */}\n <Route path=\"sre\" element={<SREPage />} />\n\n {/* Sync Failures (cross-layer sync outbox) */}\n <Route path=\"sync-failures\" element={<SyncFailuresPage />} />\n\n {/* Audit Logs */}\n <Route path=\"audit\" element={<AuditLogsPage />} />\n\n {/* Referrals */}\n <Route path=\"referrals\" element={<ReferralsPage />} />\n\n {/* Sandbox */}\n <Route path=\"sandbox\" element={<SandboxSettingsPage />} />\n\n {/* Products */}\n <Route path=\"products\" element={<ProductsPage />} />\n\n {/* Content (blog / case studies / changelog) */}\n <Route path=\"content\" element={<ContentPagesPage />} />\n\n {/* Waitlist */}\n <Route path=\"waitlist\" element={<WaitlistPage />} />\n\n {/* Settings */}\n <Route path=\"settings\" element={<SettingsPage />} />\n\n {/* Catch-all route - redirect to dashboard (absolute path to prevent infinite loops) */}\n <Route path=\"*\" element={<Navigate to={`${base}/dashboard`} replace />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { cn as e } from "../utils/cn.js";
2
2
  import { Card as t, CardContent as n, CardHeader as r, CardTitle as i, Skeleton as a } from "./ui.js";
3
3
  import { formatRelativeTime as o } from "../utils/formatters.js";
4
4
  import { Activity as s, AlertTriangle as c, Ban as l, CheckCircle as u, CreditCard as d, Mail as f, Plus as p, Rocket as m, Settings as h, Shield as g, UserPlus as _, XCircle as v } from "lucide-react";
5
- import { Link as y } from "react-router-dom";
5
+ import { Link as y } from "react-router";
6
6
  import { jsx as b, jsxs as x } from "react/jsx-runtime";
7
7
  //#region src/admin/components/ActivityFeed.tsx
8
8
  function S({ activities: e, maxItems: a = 10, loading: o, viewAllLink: s = "/activity", className: c }) {
@@ -1 +1 @@
1
- {"version":3,"file":"ActivityFeed.js","names":[],"sources":["../../../src/admin/components/ActivityFeed.tsx"],"sourcesContent":["import { Link } from 'react-router-dom';\nimport {\n Plus,\n Ban,\n AlertTriangle,\n CheckCircle,\n UserPlus,\n Rocket,\n XCircle,\n Activity,\n CreditCard,\n Mail,\n Settings,\n Shield,\n} from 'lucide-react';\nimport { Card, CardHeader, CardTitle, CardContent, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { Activity as ActivityType } from '../types';\n\ninterface ActivityFeedProps {\n /** List of activities */\n activities: ActivityType[];\n /** Maximum items to display */\n maxItems?: number;\n /** Loading state */\n loading?: boolean;\n /** Link to view all activities */\n viewAllLink?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Activity feed component showing recent platform activities\n */\nexport function ActivityFeed({\n activities,\n maxItems = 10,\n loading,\n viewAllLink = '/activity',\n className,\n}: ActivityFeedProps) {\n const displayActivities = activities.slice(0, maxItems);\n\n return (\n <Card className={className}>\n <CardHeader className=\"flex flex-row items-center justify-between\">\n <CardTitle className=\"text-lg\">Recent Activity</CardTitle>\n <Link to={viewAllLink} className=\"text-sm text-primary hover:underline\">\n View All\n </Link>\n </CardHeader>\n <CardContent>\n {loading ? (\n <div className=\"space-y-4\">\n {Array.from({ length: 5 }).map((_, i) => (\n <ActivityItemSkeleton key={i} />\n ))}\n </div>\n ) : displayActivities.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-text-secondary\">No recent activity</p>\n ) : (\n <div className=\"space-y-4\">\n {displayActivities.map((activity) => (\n <ActivityItem key={activity.id} activity={activity} />\n ))}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n\ninterface ActivityItemProps {\n activity: ActivityType;\n}\n\nfunction ActivityItem({ activity }: ActivityItemProps) {\n const icon = getActivityIcon(activity.type);\n const severityColors = {\n info: 'bg-status-info-bg text-status-info-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n error: 'bg-status-error-bg text-status-error-text',\n success: 'bg-status-success-bg text-status-success-text',\n };\n const severity = activity.severity || 'info';\n\n return (\n <div className=\"flex items-start gap-3\">\n <div className={cn('rounded-full p-2', severityColors[severity])}>{icon}</div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{activity.description}</p>\n <div className=\"mt-1 flex items-center gap-2 text-xs text-text-secondary\">\n <span>{activity.actor}</span>\n <span>-</span>\n <span>{formatRelativeTime(activity.timestamp)}</span>\n </div>\n </div>\n </div>\n );\n}\n\nfunction ActivityItemSkeleton() {\n return (\n <div className=\"flex items-start gap-3\">\n <Skeleton className=\"h-9 w-9 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n </div>\n );\n}\n\nfunction getActivityIcon(type: string) {\n const iconMap: Record<string, React.ReactNode> = {\n 'tenant.created': <Plus className=\"h-4 w-4\" />,\n 'tenant.suspended': <Ban className=\"h-4 w-4\" />,\n 'tenant.activated': <CheckCircle className=\"h-4 w-4\" />,\n 'payment.failed': <AlertTriangle className=\"h-4 w-4\" />,\n 'payment.succeeded': <CreditCard className=\"h-4 w-4\" />,\n 'user.created': <UserPlus className=\"h-4 w-4\" />,\n 'user.blocked': <Ban className=\"h-4 w-4\" />,\n 'deployment.started': <Rocket className=\"h-4 w-4\" />,\n 'deployment.completed': <CheckCircle className=\"h-4 w-4\" />,\n 'deployment.failed': <XCircle className=\"h-4 w-4\" />,\n 'newsletter.sent': <Mail className=\"h-4 w-4\" />,\n 'settings.changed': <Settings className=\"h-4 w-4\" />,\n 'security.alert': <Shield className=\"h-4 w-4\" />,\n };\n\n return iconMap[type] || <Activity className=\"h-4 w-4\" />;\n}\n"],"mappings":";;;;;;;AAoCA,SAAgB,EAAa,EAC3B,eACA,cAAW,IACX,YACA,iBAAc,aACd,gBACoB;CACpB,IAAM,IAAoB,EAAW,MAAM,GAAG,EAAS;AAEvD,QACE,kBAAC,GAAD;EAAiB;YAAjB,CACE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,GAAD;IAAW,WAAU;cAAU;IAA2B,CAAA,EAC1D,kBAAC,GAAD;IAAM,IAAI;IAAa,WAAU;cAAuC;IAEjE,CAAA,CACI;MACb,kBAAC,GAAD,EAAA,UACG,IACC,kBAAC,OAAD;GAAK,WAAU;aACZ,MAAM,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,GAAD,EAAgC,EAAL,EAAK,CAChC;GACE,CAAA,GACJ,EAAkB,WAAW,IAC/B,kBAAC,KAAD;GAAG,WAAU;aAA+C;GAAsB,CAAA,GAElF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAkB,KAAK,MACtB,kBAAC,GAAD,EAA0C,aAAY,EAAnC,EAAS,GAA0B,CACtD;GACE,CAAA,EAEI,CAAA,CACT;;;AAQX,SAAS,EAAa,EAAE,eAA+B;CACrD,IAAM,IAAO,EAAgB,EAAS,KAAK;AAS3C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAW,EAAG,oBAVA;IACrB,MAAM;IACN,SAAS;IACT,OAAO;IACP,SAAS;IACV,CACgB,EAAS,YAAY,QAI8B;aAAG;GAAW,CAAA,EAC9E,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAyC,EAAS;IAAgB,CAAA,EAC/E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD,EAAA,UAAO,EAAS,OAAa,CAAA;KAC7B,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA;KACd,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAS,UAAU,EAAQ,CAAA;KACjD;MACF;KACF;;;AAIV,SAAS,IAAuB;AAC9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA,EAC7C,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,EAClC,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,CAC9B;KACF;;;AAIV,SAAS,EAAgB,GAAc;AAiBrC,QAhBiD;EAC/C,kBAAkB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAC9C,oBAAoB,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EAC/C,oBAAoB,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EACvD,kBAAkB,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;EACvD,qBAAqB,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACvD,gBAAgB,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EAChD,gBAAgB,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EAC3C,sBAAsB,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACpD,wBAAwB,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EAC3D,qBAAqB,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;EACpD,mBAAmB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAC/C,oBAAoB,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EACpD,kBAAkB,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACjD,CAEc,MAAS,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA"}
1
+ {"version":3,"file":"ActivityFeed.js","names":[],"sources":["../../../src/admin/components/ActivityFeed.tsx"],"sourcesContent":["import { Link } from 'react-router';\nimport {\n Plus,\n Ban,\n AlertTriangle,\n CheckCircle,\n UserPlus,\n Rocket,\n XCircle,\n Activity,\n CreditCard,\n Mail,\n Settings,\n Shield,\n} from 'lucide-react';\nimport { Card, CardHeader, CardTitle, CardContent, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { Activity as ActivityType } from '../types';\n\ninterface ActivityFeedProps {\n /** List of activities */\n activities: ActivityType[];\n /** Maximum items to display */\n maxItems?: number;\n /** Loading state */\n loading?: boolean;\n /** Link to view all activities */\n viewAllLink?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Activity feed component showing recent platform activities\n */\nexport function ActivityFeed({\n activities,\n maxItems = 10,\n loading,\n viewAllLink = '/activity',\n className,\n}: ActivityFeedProps) {\n const displayActivities = activities.slice(0, maxItems);\n\n return (\n <Card className={className}>\n <CardHeader className=\"flex flex-row items-center justify-between\">\n <CardTitle className=\"text-lg\">Recent Activity</CardTitle>\n <Link to={viewAllLink} className=\"text-sm text-primary hover:underline\">\n View All\n </Link>\n </CardHeader>\n <CardContent>\n {loading ? (\n <div className=\"space-y-4\">\n {Array.from({ length: 5 }).map((_, i) => (\n <ActivityItemSkeleton key={i} />\n ))}\n </div>\n ) : displayActivities.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-text-secondary\">No recent activity</p>\n ) : (\n <div className=\"space-y-4\">\n {displayActivities.map((activity) => (\n <ActivityItem key={activity.id} activity={activity} />\n ))}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n\ninterface ActivityItemProps {\n activity: ActivityType;\n}\n\nfunction ActivityItem({ activity }: ActivityItemProps) {\n const icon = getActivityIcon(activity.type);\n const severityColors = {\n info: 'bg-status-info-bg text-status-info-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n error: 'bg-status-error-bg text-status-error-text',\n success: 'bg-status-success-bg text-status-success-text',\n };\n const severity = activity.severity || 'info';\n\n return (\n <div className=\"flex items-start gap-3\">\n <div className={cn('rounded-full p-2', severityColors[severity])}>{icon}</div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{activity.description}</p>\n <div className=\"mt-1 flex items-center gap-2 text-xs text-text-secondary\">\n <span>{activity.actor}</span>\n <span>-</span>\n <span>{formatRelativeTime(activity.timestamp)}</span>\n </div>\n </div>\n </div>\n );\n}\n\nfunction ActivityItemSkeleton() {\n return (\n <div className=\"flex items-start gap-3\">\n <Skeleton className=\"h-9 w-9 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n </div>\n );\n}\n\nfunction getActivityIcon(type: string) {\n const iconMap: Record<string, React.ReactNode> = {\n 'tenant.created': <Plus className=\"h-4 w-4\" />,\n 'tenant.suspended': <Ban className=\"h-4 w-4\" />,\n 'tenant.activated': <CheckCircle className=\"h-4 w-4\" />,\n 'payment.failed': <AlertTriangle className=\"h-4 w-4\" />,\n 'payment.succeeded': <CreditCard className=\"h-4 w-4\" />,\n 'user.created': <UserPlus className=\"h-4 w-4\" />,\n 'user.blocked': <Ban className=\"h-4 w-4\" />,\n 'deployment.started': <Rocket className=\"h-4 w-4\" />,\n 'deployment.completed': <CheckCircle className=\"h-4 w-4\" />,\n 'deployment.failed': <XCircle className=\"h-4 w-4\" />,\n 'newsletter.sent': <Mail className=\"h-4 w-4\" />,\n 'settings.changed': <Settings className=\"h-4 w-4\" />,\n 'security.alert': <Shield className=\"h-4 w-4\" />,\n };\n\n return iconMap[type] || <Activity className=\"h-4 w-4\" />;\n}\n"],"mappings":";;;;;;;AAoCA,SAAgB,EAAa,EAC3B,eACA,cAAW,IACX,YACA,iBAAc,aACd,gBACoB;CACpB,IAAM,IAAoB,EAAW,MAAM,GAAG,EAAS;AAEvD,QACE,kBAAC,GAAD;EAAiB;YAAjB,CACE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,GAAD;IAAW,WAAU;cAAU;IAA2B,CAAA,EAC1D,kBAAC,GAAD;IAAM,IAAI;IAAa,WAAU;cAAuC;IAEjE,CAAA,CACI;MACb,kBAAC,GAAD,EAAA,UACG,IACC,kBAAC,OAAD;GAAK,WAAU;aACZ,MAAM,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,GAAD,EAAgC,EAAL,EAAK,CAChC;GACE,CAAA,GACJ,EAAkB,WAAW,IAC/B,kBAAC,KAAD;GAAG,WAAU;aAA+C;GAAsB,CAAA,GAElF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAkB,KAAK,MACtB,kBAAC,GAAD,EAA0C,aAAY,EAAnC,EAAS,GAA0B,CACtD;GACE,CAAA,EAEI,CAAA,CACT;;;AAQX,SAAS,EAAa,EAAE,eAA+B;CACrD,IAAM,IAAO,EAAgB,EAAS,KAAK;AAS3C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAW,EAAG,oBAVA;IACrB,MAAM;IACN,SAAS;IACT,OAAO;IACP,SAAS;IACV,CACgB,EAAS,YAAY,QAI8B;aAAG;GAAW,CAAA,EAC9E,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAyC,EAAS;IAAgB,CAAA,EAC/E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD,EAAA,UAAO,EAAS,OAAa,CAAA;KAC7B,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA;KACd,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAS,UAAU,EAAQ,CAAA;KACjD;MACF;KACF;;;AAIV,SAAS,IAAuB;AAC9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA,EAC7C,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,EAClC,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,CAC9B;KACF;;;AAIV,SAAS,EAAgB,GAAc;AAiBrC,QAhBiD;EAC/C,kBAAkB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAC9C,oBAAoB,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EAC/C,oBAAoB,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EACvD,kBAAkB,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;EACvD,qBAAqB,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACvD,gBAAgB,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EAChD,gBAAgB,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EAC3C,sBAAsB,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACpD,wBAAwB,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EAC3D,qBAAqB,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;EACpD,mBAAmB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAC/C,oBAAoB,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EACpD,kBAAkB,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACjD,CAEc,MAAS,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA"}
@@ -2,7 +2,7 @@ import { cn as e } from "../utils/cn.js";
2
2
  import { Badge as t, Skeleton as n } from "./ui.js";
3
3
  import { formatChange as r, formatCurrency as i, formatNumber as a } from "../utils/formatters.js";
4
4
  import { Minus as o, TrendingDown as s, TrendingUp as c } from "lucide-react";
5
- import { Link as l } from "react-router-dom";
5
+ import { Link as l } from "react-router";
6
6
  import { jsx as u, jsxs as d } from "react/jsx-runtime";
7
7
  import { GlassCard as f } from "@burdenoff/fe-libs/ui";
8
8
  //#region src/admin/components/MetricCard.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"MetricCard.js","names":[],"sources":["../../../src/admin/components/MetricCard.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Link } from 'react-router-dom';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Badge, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatNumber, formatCurrency, formatChange } from '../utils/formatters';\n\nexport interface MetricCardProps {\n /** Card title */\n title: string;\n /** Metric value (number or formatted string) */\n value: number | string;\n /** Change from previous period */\n change?: number;\n /** Type of change display */\n changeType?: 'number' | 'percentage' | 'currency';\n /** Trend direction */\n trend?: 'up' | 'down' | 'neutral';\n /** Whether change is positive or negative for styling */\n changeIsPositive?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n /** Subtitle text */\n subtitle?: string;\n /** Link to navigate to on click */\n linkTo?: string;\n /** Priority indicator */\n priority?: 'normal' | 'high' | 'critical';\n /** Loading state */\n loading?: boolean;\n /**\n * Hero metric — uses the glass treatment with an accent glow (Boff UI 2.0).\n * Reserve for the single most important metric per group.\n */\n hero?: boolean;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Metric card component for dashboard\n */\nexport function MetricCard({\n title,\n value,\n change,\n changeType = 'number',\n trend,\n changeIsPositive,\n icon,\n subtitle,\n linkTo,\n priority,\n loading,\n hero = false,\n className,\n}: MetricCardProps) {\n // Determine if change is positive based on trend or explicit prop\n const isPositive = changeIsPositive ?? trend === 'up';\n\n // Format the value\n const formattedValue =\n typeof value === 'number'\n ? changeType === 'currency'\n ? formatCurrency(value)\n : formatNumber(value)\n : value;\n\n // Format the change\n const formattedChange =\n change !== undefined ? formatChange(change, changeType === 'percentage') : null;\n\n // Get trend icon\n const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;\n\n // Priority accent (left rail) — semantic tokens only.\n const priorityClasses = {\n normal: '',\n high: 'border-l-4 border-l-status-warning-border',\n critical: 'border-l-4 border-l-status-error-border',\n };\n\n const cardContent = (\n <GlassCard\n treatment={hero ? 'glass' : 'metric'}\n glow={hero}\n className={cn('h-full p-6', priority && priorityClasses[priority], className)}\n >\n <div className=\"flex flex-row items-center justify-between pb-2\">\n <span className=\"text-sm font-medium text-text-secondary\">{title}</span>\n {icon && <div className=\"text-text-secondary\">{icon}</div>}\n </div>\n {loading ? (\n <div className=\"space-y-2\">\n <Skeleton className=\"h-8 w-24\" />\n <Skeleton className=\"h-4 w-16\" />\n </div>\n ) : (\n <div>\n <div className=\"flex items-baseline justify-between\">\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">\n {formattedValue}\n </div>\n {formattedChange !== null && (\n <Badge\n variant={isPositive ? 'success' : 'destructive'}\n className=\"flex items-center gap-1 tabular-nums\"\n >\n <TrendIcon className=\"h-3 w-3\" />\n {formattedChange}\n </Badge>\n )}\n </div>\n {subtitle && <p className=\"mt-1 text-xs text-text-secondary\">{subtitle}</p>}\n </div>\n )}\n </GlassCard>\n );\n\n if (linkTo) {\n return (\n <Link\n to={linkTo}\n className=\"block rounded-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2\"\n >\n {cardContent}\n </Link>\n );\n }\n\n return cardContent;\n}\n\n/**\n * Grid container for metric cards\n */\nexport function MetricsGrid({ children, className }: { children: ReactNode; className?: string }) {\n return (\n <div className={cn('grid gap-4', 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4', className)}>\n {children}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA2CA,SAAgB,EAAW,EACzB,UACA,UACA,WACA,gBAAa,UACb,UACA,qBACA,SACA,aACA,WACA,aACA,YACA,UAAO,IACP,gBACkB;CAElB,IAAM,IAAa,KAAoB,MAAU,MAG3C,IACJ,OAAO,KAAU,WACb,MAAe,aACb,EAAe,EAAM,GACrB,EAAa,EAAM,GACrB,GAGA,IACJ,MAAW,KAAA,IAAgE,OAApD,EAAa,GAAQ,MAAe,aAAa,EAGpE,IAAY,MAAU,OAAO,IAAa,MAAU,SAAS,IAAe,GAS5E,IACJ,kBAAC,GAAD;EACE,WAAW,IAAO,UAAU;EAC5B,MAAM;EACN,WAAW,EAAG,cAAc,KAVR;GACtB,QAAQ;GACR,MAAM;GACN,UAAU;GACX,CAM2D,IAAW,EAAU;YAH/E,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cAA2C;IAAa,CAAA,EACvE,KAAQ,kBAAC,OAAD;IAAK,WAAU;cAAuB;IAAW,CAAA,CACtD;MACL,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,EACjC,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,CAC7B;OAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA,EACL,MAAoB,QACnB,kBAAC,GAAD;IACE,SAAS,IAAa,YAAY;IAClC,WAAU;cAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAChC,EACK;MAEN;MACL,KAAY,kBAAC,KAAD;GAAG,WAAU;aAAoC;GAAa,CAAA,CACvE,EAAA,CAAA,CAEE;;AAcd,QAXI,IAEA,kBAAC,GAAD;EACE,IAAI;EACJ,WAAU;YAET;EACI,CAAA,GAIJ;;AAMT,SAAgB,EAAY,EAAE,aAAU,gBAA0D;AAChG,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,cAAc,6CAA6C,EAAU;EACrF;EACG,CAAA"}
1
+ {"version":3,"file":"MetricCard.js","names":[],"sources":["../../../src/admin/components/MetricCard.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Link } from 'react-router';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Badge, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatNumber, formatCurrency, formatChange } from '../utils/formatters';\n\nexport interface MetricCardProps {\n /** Card title */\n title: string;\n /** Metric value (number or formatted string) */\n value: number | string;\n /** Change from previous period */\n change?: number;\n /** Type of change display */\n changeType?: 'number' | 'percentage' | 'currency';\n /** Trend direction */\n trend?: 'up' | 'down' | 'neutral';\n /** Whether change is positive or negative for styling */\n changeIsPositive?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n /** Subtitle text */\n subtitle?: string;\n /** Link to navigate to on click */\n linkTo?: string;\n /** Priority indicator */\n priority?: 'normal' | 'high' | 'critical';\n /** Loading state */\n loading?: boolean;\n /**\n * Hero metric — uses the glass treatment with an accent glow (Boff UI 2.0).\n * Reserve for the single most important metric per group.\n */\n hero?: boolean;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Metric card component for dashboard\n */\nexport function MetricCard({\n title,\n value,\n change,\n changeType = 'number',\n trend,\n changeIsPositive,\n icon,\n subtitle,\n linkTo,\n priority,\n loading,\n hero = false,\n className,\n}: MetricCardProps) {\n // Determine if change is positive based on trend or explicit prop\n const isPositive = changeIsPositive ?? trend === 'up';\n\n // Format the value\n const formattedValue =\n typeof value === 'number'\n ? changeType === 'currency'\n ? formatCurrency(value)\n : formatNumber(value)\n : value;\n\n // Format the change\n const formattedChange =\n change !== undefined ? formatChange(change, changeType === 'percentage') : null;\n\n // Get trend icon\n const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;\n\n // Priority accent (left rail) — semantic tokens only.\n const priorityClasses = {\n normal: '',\n high: 'border-l-4 border-l-status-warning-border',\n critical: 'border-l-4 border-l-status-error-border',\n };\n\n const cardContent = (\n <GlassCard\n treatment={hero ? 'glass' : 'metric'}\n glow={hero}\n className={cn('h-full p-6', priority && priorityClasses[priority], className)}\n >\n <div className=\"flex flex-row items-center justify-between pb-2\">\n <span className=\"text-sm font-medium text-text-secondary\">{title}</span>\n {icon && <div className=\"text-text-secondary\">{icon}</div>}\n </div>\n {loading ? (\n <div className=\"space-y-2\">\n <Skeleton className=\"h-8 w-24\" />\n <Skeleton className=\"h-4 w-16\" />\n </div>\n ) : (\n <div>\n <div className=\"flex items-baseline justify-between\">\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">\n {formattedValue}\n </div>\n {formattedChange !== null && (\n <Badge\n variant={isPositive ? 'success' : 'destructive'}\n className=\"flex items-center gap-1 tabular-nums\"\n >\n <TrendIcon className=\"h-3 w-3\" />\n {formattedChange}\n </Badge>\n )}\n </div>\n {subtitle && <p className=\"mt-1 text-xs text-text-secondary\">{subtitle}</p>}\n </div>\n )}\n </GlassCard>\n );\n\n if (linkTo) {\n return (\n <Link\n to={linkTo}\n className=\"block rounded-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2\"\n >\n {cardContent}\n </Link>\n );\n }\n\n return cardContent;\n}\n\n/**\n * Grid container for metric cards\n */\nexport function MetricsGrid({ children, className }: { children: ReactNode; className?: string }) {\n return (\n <div className={cn('grid gap-4', 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4', className)}>\n {children}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA2CA,SAAgB,EAAW,EACzB,UACA,UACA,WACA,gBAAa,UACb,UACA,qBACA,SACA,aACA,WACA,aACA,YACA,UAAO,IACP,gBACkB;CAElB,IAAM,IAAa,KAAoB,MAAU,MAG3C,IACJ,OAAO,KAAU,WACb,MAAe,aACb,EAAe,EAAM,GACrB,EAAa,EAAM,GACrB,GAGA,IACJ,MAAW,KAAA,IAAgE,OAApD,EAAa,GAAQ,MAAe,aAAa,EAGpE,IAAY,MAAU,OAAO,IAAa,MAAU,SAAS,IAAe,GAS5E,IACJ,kBAAC,GAAD;EACE,WAAW,IAAO,UAAU;EAC5B,MAAM;EACN,WAAW,EAAG,cAAc,KAVR;GACtB,QAAQ;GACR,MAAM;GACN,UAAU;GACX,CAM2D,IAAW,EAAU;YAH/E,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cAA2C;IAAa,CAAA,EACvE,KAAQ,kBAAC,OAAD;IAAK,WAAU;cAAuB;IAAW,CAAA,CACtD;MACL,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,EACjC,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,CAC7B;OAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA,EACL,MAAoB,QACnB,kBAAC,GAAD;IACE,SAAS,IAAa,YAAY;IAClC,WAAU;cAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAChC,EACK;MAEN;MACL,KAAY,kBAAC,KAAD;GAAG,WAAU;aAAoC;GAAa,CAAA,CACvE,EAAA,CAAA,CAEE;;AAcd,QAXI,IAEA,kBAAC,GAAD;EACE,IAAI;EACJ,WAAU;YAET;EACI,CAAA,GAIJ;;AAMT,SAAgB,EAAY,EAAE,aAAU,gBAA0D;AAChG,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,cAAc,6CAA6C,EAAU;EACrF;EACG,CAAA"}
@@ -12,7 +12,7 @@ import { mapActivityLogToUi as u } from "../utils/mappers.js";
12
12
  import { useGetDashboardActiveWorkspaceCountQuery as d, useGetDashboardOrganizationCountQuery as f, useGetDashboardTenantCountQuery as p, useGetDashboardUserCountQuery as m, useGetRecentActivityLogsQuery as h } from "../../generated/global-operations.js";
13
13
  import { useMemo as g } from "react";
14
14
  import { Briefcase as _, Building2 as v, FileText as y, HelpCircle as b, Layout as x, Plus as S, UserPlus as C, Users as w } from "lucide-react";
15
- import { useNavigate as T } from "react-router-dom";
15
+ import { useNavigate as T } from "react-router";
16
16
  import { jsx as E, jsxs as D } from "react/jsx-runtime";
17
17
  import { useI18n as O } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
18
18
  import { NextSteps as k, PagePurpose as A } from "@burdenoff/fe-libs/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/admin/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Building2,\n Users,\n Layout,\n Briefcase,\n Plus,\n UserPlus,\n FileText,\n HelpCircle,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n ActivityFeed,\n QuickLinks,\n PermissionGuard,\n} from '../components';\nimport { useNavigate } from 'react-router-dom';\nimport { PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { mapActivityLogToUi } from '../utils/mappers';\nimport type { Activity } from '../types';\nimport {\n useGetDashboardTenantCountQuery,\n useGetDashboardOrganizationCountQuery,\n useGetDashboardActiveWorkspaceCountQuery,\n useGetDashboardUserCountQuery,\n useGetRecentActivityLogsQuery,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\n/**\n * Dashboard page — platform overview with real-time metrics\n */\nexport function DashboardPage() {\n const { canViewDashboard, isLoading: permissionsLoading } = useAdminPermissions();\n const { basePath = '' } = useAdmin();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const skipQueries = permissionsLoading || !canViewDashboard;\n\n const { data: tenantData, loading: tenantLoading } = useGetDashboardTenantCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: orgData, loading: orgLoading } = useGetDashboardOrganizationCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: workspaceData, loading: workspaceLoading } =\n useGetDashboardActiveWorkspaceCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: userData, loading: userLoading } = useGetDashboardUserCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: activityData, loading: activityLoading } = useGetRecentActivityLogsQuery({\n variables: { limit: 10 },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const metrics = {\n tenants: tenantData?.tenants?.total ?? 0,\n organizations: orgData?.allOrganizations?.total ?? 0,\n workspaces: workspaceData?.allWorkspaces?.total ?? 0,\n users: userData?.userCount ?? 0,\n };\n\n const recentActivity: Activity[] = useMemo(\n () => (activityData?.activityLogs?.nodes ?? []).map(mapActivityLogToUi),\n [activityData]\n );\n\n const quickLinks = [\n {\n to: joinPath(basePath, 'tenants'),\n label: tr('admin.dashboard.quickLinks.addTenant', 'Add Tenant'),\n icon: <Plus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.addTenantDescription', 'Create a new tenant'),\n },\n {\n to: joinPath(basePath, 'users'),\n label: tr('admin.dashboard.quickLinks.inviteUser', 'Invite User'),\n icon: <UserPlus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.inviteUserDescription', 'Invite a new user'),\n },\n {\n to: joinPath(basePath, 'audit'),\n label: tr('admin.dashboard.quickLinks.auditLogs', 'Audit Logs'),\n icon: <FileText className=\"h-4 w-4\" />,\n description: tr(\n 'admin.dashboard.quickLinks.auditLogsDescription',\n 'View security audit trail'\n ),\n },\n {\n to: joinPath(basePath, 'support'),\n label: tr('admin.dashboard.quickLinks.support', 'Support'),\n icon: <HelpCircle className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.supportDescription', 'Manage support tickets'),\n },\n ];\n\n const isLoading = tenantLoading || orgLoading || workspaceLoading || userLoading;\n\n return (\n <PermissionGuard\n allowed={canViewDashboard}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.dashboard.title', 'Dashboard')}\n denyMessage={tr(\n 'admin.dashboard.denyMessage',\n \"You don't have permission to view the admin dashboard.\"\n )}\n >\n <PageLayout\n title={tr('admin.dashboard.title', 'Dashboard')}\n description={tr('admin.dashboard.description', 'Platform overview and key metrics')}\n gradientWord={tr('admin.dashboard.title', 'Dashboard')}\n aurora\n >\n <div className=\"space-y-6\">\n <PagePurpose>\n {tr(\n 'admin.dashboard.purpose',\n 'Your control tower for the whole platform. See live tenant, organization, workspace and user counts at a glance, watch the most recent administrative activity, and jump straight into the day-to-day jobs — onboarding a tenant, inviting a user, or reviewing the audit trail.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"admin-dashboard\"\n steps={[\n {\n id: 'add-tenant',\n label: tr('admin.dashboard.nextSteps.addTenant', 'Onboard a tenant'),\n description: tr(\n 'admin.dashboard.nextSteps.addTenantDesc',\n 'Provision a new customer tenant to start serving them.'\n ),\n onClick: () => navigate(joinPath(basePath, 'tenants/new')),\n },\n {\n id: 'invite-user',\n label: tr('admin.dashboard.nextSteps.inviteUser', 'Invite a teammate'),\n description: tr(\n 'admin.dashboard.nextSteps.inviteUserDesc',\n 'Bring another operator into the admin console.'\n ),\n onClick: () => navigate(joinPath(basePath, 'users')),\n },\n {\n id: 'review-audit',\n label: tr('admin.dashboard.nextSteps.reviewAudit', 'Review the audit trail'),\n description: tr(\n 'admin.dashboard.nextSteps.reviewAuditDesc',\n 'Check who changed what across the platform.'\n ),\n onClick: () => navigate(joinPath(basePath, 'audit')),\n },\n ]}\n />\n\n {/* Metrics Grid */}\n <MetricsGrid>\n <MetricCard\n title={tr('admin.dashboard.metrics.totalTenants', 'Total Tenants')}\n value={metrics.tenants}\n loading={isLoading}\n hero\n icon={<Building2 className=\"h-5 w-5 text-action-primary-bg\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.organizations', 'Organizations')}\n value={metrics.organizations}\n loading={isLoading}\n icon={<Briefcase className=\"h-5 w-5 text-text-secondary\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.activeWorkspaces', 'Active Workspaces')}\n value={metrics.workspaces}\n loading={isLoading}\n icon={<Layout className=\"h-5 w-5 text-status-success-text\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.totalUsers', 'Total Users')}\n value={metrics.users}\n loading={isLoading}\n icon={<Users className=\"h-5 w-5 text-text-secondary\" />}\n />\n </MetricsGrid>\n\n {/* Content Area */}\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <ActivityFeed activities={recentActivity} loading={activityLoading} />\n </div>\n <div className=\"space-y-4\">\n <QuickLinks links={quickLinks} />\n </div>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/admin/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Building2,\n Users,\n Layout,\n Briefcase,\n Plus,\n UserPlus,\n FileText,\n HelpCircle,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n ActivityFeed,\n QuickLinks,\n PermissionGuard,\n} from '../components';\nimport { useNavigate } from 'react-router';\nimport { PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { mapActivityLogToUi } from '../utils/mappers';\nimport type { Activity } from '../types';\nimport {\n useGetDashboardTenantCountQuery,\n useGetDashboardOrganizationCountQuery,\n useGetDashboardActiveWorkspaceCountQuery,\n useGetDashboardUserCountQuery,\n useGetRecentActivityLogsQuery,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\n/**\n * Dashboard page — platform overview with real-time metrics\n */\nexport function DashboardPage() {\n const { canViewDashboard, isLoading: permissionsLoading } = useAdminPermissions();\n const { basePath = '' } = useAdmin();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const skipQueries = permissionsLoading || !canViewDashboard;\n\n const { data: tenantData, loading: tenantLoading } = useGetDashboardTenantCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: orgData, loading: orgLoading } = useGetDashboardOrganizationCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: workspaceData, loading: workspaceLoading } =\n useGetDashboardActiveWorkspaceCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: userData, loading: userLoading } = useGetDashboardUserCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: activityData, loading: activityLoading } = useGetRecentActivityLogsQuery({\n variables: { limit: 10 },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const metrics = {\n tenants: tenantData?.tenants?.total ?? 0,\n organizations: orgData?.allOrganizations?.total ?? 0,\n workspaces: workspaceData?.allWorkspaces?.total ?? 0,\n users: userData?.userCount ?? 0,\n };\n\n const recentActivity: Activity[] = useMemo(\n () => (activityData?.activityLogs?.nodes ?? []).map(mapActivityLogToUi),\n [activityData]\n );\n\n const quickLinks = [\n {\n to: joinPath(basePath, 'tenants'),\n label: tr('admin.dashboard.quickLinks.addTenant', 'Add Tenant'),\n icon: <Plus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.addTenantDescription', 'Create a new tenant'),\n },\n {\n to: joinPath(basePath, 'users'),\n label: tr('admin.dashboard.quickLinks.inviteUser', 'Invite User'),\n icon: <UserPlus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.inviteUserDescription', 'Invite a new user'),\n },\n {\n to: joinPath(basePath, 'audit'),\n label: tr('admin.dashboard.quickLinks.auditLogs', 'Audit Logs'),\n icon: <FileText className=\"h-4 w-4\" />,\n description: tr(\n 'admin.dashboard.quickLinks.auditLogsDescription',\n 'View security audit trail'\n ),\n },\n {\n to: joinPath(basePath, 'support'),\n label: tr('admin.dashboard.quickLinks.support', 'Support'),\n icon: <HelpCircle className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.supportDescription', 'Manage support tickets'),\n },\n ];\n\n const isLoading = tenantLoading || orgLoading || workspaceLoading || userLoading;\n\n return (\n <PermissionGuard\n allowed={canViewDashboard}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.dashboard.title', 'Dashboard')}\n denyMessage={tr(\n 'admin.dashboard.denyMessage',\n \"You don't have permission to view the admin dashboard.\"\n )}\n >\n <PageLayout\n title={tr('admin.dashboard.title', 'Dashboard')}\n description={tr('admin.dashboard.description', 'Platform overview and key metrics')}\n gradientWord={tr('admin.dashboard.title', 'Dashboard')}\n aurora\n >\n <div className=\"space-y-6\">\n <PagePurpose>\n {tr(\n 'admin.dashboard.purpose',\n 'Your control tower for the whole platform. See live tenant, organization, workspace and user counts at a glance, watch the most recent administrative activity, and jump straight into the day-to-day jobs — onboarding a tenant, inviting a user, or reviewing the audit trail.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"admin-dashboard\"\n steps={[\n {\n id: 'add-tenant',\n label: tr('admin.dashboard.nextSteps.addTenant', 'Onboard a tenant'),\n description: tr(\n 'admin.dashboard.nextSteps.addTenantDesc',\n 'Provision a new customer tenant to start serving them.'\n ),\n onClick: () => navigate(joinPath(basePath, 'tenants/new')),\n },\n {\n id: 'invite-user',\n label: tr('admin.dashboard.nextSteps.inviteUser', 'Invite a teammate'),\n description: tr(\n 'admin.dashboard.nextSteps.inviteUserDesc',\n 'Bring another operator into the admin console.'\n ),\n onClick: () => navigate(joinPath(basePath, 'users')),\n },\n {\n id: 'review-audit',\n label: tr('admin.dashboard.nextSteps.reviewAudit', 'Review the audit trail'),\n description: tr(\n 'admin.dashboard.nextSteps.reviewAuditDesc',\n 'Check who changed what across the platform.'\n ),\n onClick: () => navigate(joinPath(basePath, 'audit')),\n },\n ]}\n />\n\n {/* Metrics Grid */}\n <MetricsGrid>\n <MetricCard\n title={tr('admin.dashboard.metrics.totalTenants', 'Total Tenants')}\n value={metrics.tenants}\n loading={isLoading}\n hero\n icon={<Building2 className=\"h-5 w-5 text-action-primary-bg\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.organizations', 'Organizations')}\n value={metrics.organizations}\n loading={isLoading}\n icon={<Briefcase className=\"h-5 w-5 text-text-secondary\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.activeWorkspaces', 'Active Workspaces')}\n value={metrics.workspaces}\n loading={isLoading}\n icon={<Layout className=\"h-5 w-5 text-status-success-text\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.totalUsers', 'Total Users')}\n value={metrics.users}\n loading={isLoading}\n icon={<Users className=\"h-5 w-5 text-text-secondary\" />}\n />\n </MetricsGrid>\n\n {/* Content Area */}\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <ActivityFeed activities={recentActivity} loading={activityLoading} />\n </div>\n <div className=\"space-y-4\">\n <QuickLinks links={quickLinks} />\n </div>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -16,7 +16,7 @@ import { TENANT_FEATURE_FLAGS as v } from "../constants/featureFlags.js";
16
16
  import { TenantMembersPanel as re } from "../components/TenantMembersPanel.js";
17
17
  import { TenantSsoPanel as ie } from "../components/TenantSsoPanel.js";
18
18
  import { ArrowLeft as y, Ban as ae, Building2 as oe, Calendar as b, CheckCircle as se, Globe as x, Loader2 as S, Mail as C, Settings as w, Users as ce } from "lucide-react";
19
- import { useNavigate as le, useParams as ue } from "react-router-dom";
19
+ import { useNavigate as le, useParams as ue } from "react-router";
20
20
  import { jsx as T, jsxs as E } from "react/jsx-runtime";
21
21
  import { useFeatureFlag as D } from "@burdenoff/fe-libs/shared/feature-flags";
22
22
  import { useI18n as O } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
@@ -1 +1 @@
1
- {"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router-dom';\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 { 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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"}
@@ -14,7 +14,7 @@ import { emitTenantLifecycle as p } from "../utils/tenantEvents.js";
14
14
  import { TENANT_FEATURE_FLAGS as m } from "../constants/featureFlags.js";
15
15
  import { useCallback as h, useMemo as g } from "react";
16
16
  import { ArrowLeft as _, Loader2 as v } from "lucide-react";
17
- import { useNavigate as y, useParams as b } from "react-router-dom";
17
+ import { useNavigate as y, useParams as b } from "react-router";
18
18
  import { Fragment as x, jsx as S, jsxs as C } from "react/jsx-runtime";
19
19
  import { useFeatureFlag as w } from "@burdenoff/fe-libs/shared/feature-flags";
20
20
  import { useI18n as T } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
@@ -1 +1 @@
1
- {"version":3,"file":"TenantFormRoutePage.js","names":[],"sources":["../../../src/admin/pages/TenantFormRoutePage.tsx"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2 } from 'lucide-react';\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 { emitTenantLifecycle } from '../utils/tenantEvents';\nimport {\n useCreateTenantMutation,\n useGetTenantQuery,\n useUpdateTenantMutation,\n} from '../../generated/global-operations';\nimport { Button, PageLayout, PermissionGuard, TenantForm, useToast } from '../components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport type { CreateTenantInput, UpdateTenantInput } from '../schemas';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { joinPath } from '../utils/navigation';\nimport { tWithFallback } from '../utils/i18n';\nimport { mapTenantToUi } from '../utils/mappers';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\ntype TenantFormRoutePageProps = {\n mode: 'create' | 'edit';\n};\n\nexport function TenantFormRoutePage({ mode }: TenantFormRoutePageProps) {\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const { canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n const isEdit = mode === 'edit';\n // Page-level feature flags. Default-allow keeps prior behavior intact when\n // the flag is missing; turning the flag OFF in OpenBao/feature-flag-svc\n // disables the corresponding route without a code deploy.\n const flagKey = isEdit ? TENANT_FEATURE_FLAGS.edit : TENANT_FEATURE_FLAGS.create;\n const managementFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const flagEnabled = useFeatureFlag(flagKey, true);\n const pageFeatureEnabled = managementFlagEnabled && flagEnabled;\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 tenantsPath = joinPath(basePath, 'tenants');\n const closePath = isEdit && tenantId ? joinPath(basePath, `tenants/${tenantId}`) : tenantsPath;\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId ?? '' },\n // Also skip when the page is feature-flag-disabled: the early return\n // below renders the \"feature disabled\" panel instead of the form, so a\n // network round-trip would be wasted.\n skip: !isEdit || !tenantId || permissionsLoading || !canManageTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [createTenant] = useCreateTenantMutation();\n const [updateTenant] = useUpdateTenantMutation();\n\n // Memoize the mapped tenant so its object identity is stable across\n // renders. mapTenantToUi otherwise produces a new object on every render,\n // forcing downstream memoization (TenantForm.defaultValues, reset effects)\n // to fan out unnecessarily.\n const tenant = useMemo(\n () => (data?.tenant ? mapTenantToUi(data.tenant) : undefined),\n [data?.tenant]\n );\n\n const handleClose = useCallback(() => {\n navigate(closePath);\n }, [navigate, closePath]);\n\n const handleSave = useCallback(\n async (input: CreateTenantInput | UpdateTenantInput) => {\n if (isEdit) {\n if (!tenantId) return;\n try {\n const updated = await updateTenant({\n variables: {\n id: tenantId,\n input: {\n name: input.name,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n // The update mutation does not accept slug, so emit the slug from\n // the persisted record (mutation response, falling back to the\n // already-loaded tenant) rather than the user's form input — the\n // form's slug field is read-only for edits, but a defensive emit\n // protects listeners against any future change.\n emitTenantLifecycle(events, 'tenant.tenant.updated', {\n tenantId,\n tenantSlug: updated.data?.updateTenant?.slug ?? tenant?.slug,\n });\n toast.success(\n tr('admin.tenants.toast.updatedTitle', 'Tenant updated'),\n tr('admin.tenants.toast.updatedDescription', '{{name}} has been updated.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${tenantId}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.updateFailedTitle', 'Update failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so TenantForm.onSubmit's try/catch sees the rejection\n // and keeps the form open instead of calling onClose() — losing\n // the user's edits would be a worse outcome than a duplicate\n // surfacing of the same error.\n throw err;\n }\n return;\n }\n\n try {\n const result = await createTenant({\n variables: {\n input: {\n name: input.name!,\n slug: input.slug!,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const created = result.data?.createTenant;\n if (!created?.id) {\n // A resolved mutation without a createdTenant record is a\n // degenerate response (federation hiccup, partial schema, etc.).\n // Treat as failure so the user is not shown a misleading success\n // toast and the form stays open with their input intact.\n throw new Error(\n tr(\n 'admin.tenants.toast.createNoRecord',\n 'Tenant create returned no record. Please retry.'\n )\n );\n }\n // Slug from the server-confirmed record so listeners cannot see a\n // form value that was rejected (e.g. uniqueness collision).\n emitTenantLifecycle(events, 'tenant.tenant.created', {\n tenantId: created.id,\n tenantSlug: created.slug,\n });\n toast.success(\n tr('admin.tenants.toast.createdTitle', 'Tenant created'),\n tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${created.id}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.createFailedTitle', 'Create failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so the form stays open on failure (see edit branch).\n throw err;\n }\n },\n [\n basePath,\n createTenant,\n events,\n isEdit,\n navigate,\n tenant?.slug,\n tenantId,\n tenantsPath,\n toast,\n tr,\n updateTenant,\n ]\n );\n\n // Permission denials win over feature-flag denials: a user without\n // canManageTenants should see \"permission denied\" rather than learning\n // whether the feature flag is on/off (minor information disclosure).\n if (!permissionsLoading && !canManageTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.permissionDenied',\n 'You do not have permission to manage tenants.'\n )}\n >\n <></>\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={tr('admin.tenantForm.featureDisabled', 'This feature is currently disabled.')}\n >\n <Button variant=\"outline\" onClick={() => navigate(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canManageTenants}\n isLoading={permissionsLoading}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.denyMessage',\n \"You don't have permission to manage tenants.\"\n )}\n >\n {isEdit && loading ? (\n <PageLayout title={tr('admin.tenantForm.loadingTitle', 'Loading tenant...')}>\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 ) : isEdit && (error || !tenant) ? (\n <PageLayout\n title={tr('admin.tenantForm.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantForm.notFoundDescription',\n 'The tenant you want to edit 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(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={\n isEdit\n ? tr('admin.tenantForm.editDescription', 'Update tenant configuration and metadata.')\n : tr('admin.tenantForm.createDescription', 'Add a new tenant to the platform.')\n }\n >\n <PagePurpose className=\"mb-6\">\n {isEdit\n ? tr(\n 'admin.tenantForm.editPurpose',\n 'Update this tenant’s details — its display name, custom domain, owner and plan. Changes here affect how the customer account is identified and billed across the platform.'\n )\n : tr(\n 'admin.tenantForm.createPurpose',\n 'Provision a brand-new customer account. Give it a name and URL slug, set the owner and starting plan, and the platform spins up an isolated tenant ready for its users.'\n )}\n </PagePurpose>\n <TenantForm open={true} onClose={handleClose} tenant={tenant} onSave={handleSave} />\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TenantFormRoutePage.js","names":[],"sources":["../../../src/admin/pages/TenantFormRoutePage.tsx"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport { ArrowLeft, Loader2 } from 'lucide-react';\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 { emitTenantLifecycle } from '../utils/tenantEvents';\nimport {\n useCreateTenantMutation,\n useGetTenantQuery,\n useUpdateTenantMutation,\n} from '../../generated/global-operations';\nimport { Button, PageLayout, PermissionGuard, TenantForm, useToast } from '../components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport type { CreateTenantInput, UpdateTenantInput } from '../schemas';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { joinPath } from '../utils/navigation';\nimport { tWithFallback } from '../utils/i18n';\nimport { mapTenantToUi } from '../utils/mappers';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\ntype TenantFormRoutePageProps = {\n mode: 'create' | 'edit';\n};\n\nexport function TenantFormRoutePage({ mode }: TenantFormRoutePageProps) {\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const { canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n const isEdit = mode === 'edit';\n // Page-level feature flags. Default-allow keeps prior behavior intact when\n // the flag is missing; turning the flag OFF in OpenBao/feature-flag-svc\n // disables the corresponding route without a code deploy.\n const flagKey = isEdit ? TENANT_FEATURE_FLAGS.edit : TENANT_FEATURE_FLAGS.create;\n const managementFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const flagEnabled = useFeatureFlag(flagKey, true);\n const pageFeatureEnabled = managementFlagEnabled && flagEnabled;\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 tenantsPath = joinPath(basePath, 'tenants');\n const closePath = isEdit && tenantId ? joinPath(basePath, `tenants/${tenantId}`) : tenantsPath;\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId ?? '' },\n // Also skip when the page is feature-flag-disabled: the early return\n // below renders the \"feature disabled\" panel instead of the form, so a\n // network round-trip would be wasted.\n skip: !isEdit || !tenantId || permissionsLoading || !canManageTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [createTenant] = useCreateTenantMutation();\n const [updateTenant] = useUpdateTenantMutation();\n\n // Memoize the mapped tenant so its object identity is stable across\n // renders. mapTenantToUi otherwise produces a new object on every render,\n // forcing downstream memoization (TenantForm.defaultValues, reset effects)\n // to fan out unnecessarily.\n const tenant = useMemo(\n () => (data?.tenant ? mapTenantToUi(data.tenant) : undefined),\n [data?.tenant]\n );\n\n const handleClose = useCallback(() => {\n navigate(closePath);\n }, [navigate, closePath]);\n\n const handleSave = useCallback(\n async (input: CreateTenantInput | UpdateTenantInput) => {\n if (isEdit) {\n if (!tenantId) return;\n try {\n const updated = await updateTenant({\n variables: {\n id: tenantId,\n input: {\n name: input.name,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n // The update mutation does not accept slug, so emit the slug from\n // the persisted record (mutation response, falling back to the\n // already-loaded tenant) rather than the user's form input — the\n // form's slug field is read-only for edits, but a defensive emit\n // protects listeners against any future change.\n emitTenantLifecycle(events, 'tenant.tenant.updated', {\n tenantId,\n tenantSlug: updated.data?.updateTenant?.slug ?? tenant?.slug,\n });\n toast.success(\n tr('admin.tenants.toast.updatedTitle', 'Tenant updated'),\n tr('admin.tenants.toast.updatedDescription', '{{name}} has been updated.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${tenantId}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.updateFailedTitle', 'Update failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so TenantForm.onSubmit's try/catch sees the rejection\n // and keeps the form open instead of calling onClose() — losing\n // the user's edits would be a worse outcome than a duplicate\n // surfacing of the same error.\n throw err;\n }\n return;\n }\n\n try {\n const result = await createTenant({\n variables: {\n input: {\n name: input.name!,\n slug: input.slug!,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const created = result.data?.createTenant;\n if (!created?.id) {\n // A resolved mutation without a createdTenant record is a\n // degenerate response (federation hiccup, partial schema, etc.).\n // Treat as failure so the user is not shown a misleading success\n // toast and the form stays open with their input intact.\n throw new Error(\n tr(\n 'admin.tenants.toast.createNoRecord',\n 'Tenant create returned no record. Please retry.'\n )\n );\n }\n // Slug from the server-confirmed record so listeners cannot see a\n // form value that was rejected (e.g. uniqueness collision).\n emitTenantLifecycle(events, 'tenant.tenant.created', {\n tenantId: created.id,\n tenantSlug: created.slug,\n });\n toast.success(\n tr('admin.tenants.toast.createdTitle', 'Tenant created'),\n tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${created.id}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.createFailedTitle', 'Create failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so the form stays open on failure (see edit branch).\n throw err;\n }\n },\n [\n basePath,\n createTenant,\n events,\n isEdit,\n navigate,\n tenant?.slug,\n tenantId,\n tenantsPath,\n toast,\n tr,\n updateTenant,\n ]\n );\n\n // Permission denials win over feature-flag denials: a user without\n // canManageTenants should see \"permission denied\" rather than learning\n // whether the feature flag is on/off (minor information disclosure).\n if (!permissionsLoading && !canManageTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.permissionDenied',\n 'You do not have permission to manage tenants.'\n )}\n >\n <></>\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={tr('admin.tenantForm.featureDisabled', 'This feature is currently disabled.')}\n >\n <Button variant=\"outline\" onClick={() => navigate(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canManageTenants}\n isLoading={permissionsLoading}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.denyMessage',\n \"You don't have permission to manage tenants.\"\n )}\n >\n {isEdit && loading ? (\n <PageLayout title={tr('admin.tenantForm.loadingTitle', 'Loading tenant...')}>\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 ) : isEdit && (error || !tenant) ? (\n <PageLayout\n title={tr('admin.tenantForm.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantForm.notFoundDescription',\n 'The tenant you want to edit 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(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={\n isEdit\n ? tr('admin.tenantForm.editDescription', 'Update tenant configuration and metadata.')\n : tr('admin.tenantForm.createDescription', 'Add a new tenant to the platform.')\n }\n >\n <PagePurpose className=\"mb-6\">\n {isEdit\n ? tr(\n 'admin.tenantForm.editPurpose',\n 'Update this tenant’s details — its display name, custom domain, owner and plan. Changes here affect how the customer account is identified and billed across the platform.'\n )\n : tr(\n 'admin.tenantForm.createPurpose',\n 'Provision a brand-new customer account. Give it a name and URL slug, set the owner and starting plan, and the platform spins up an isolated tenant ready for its users.'\n )}\n </PagePurpose>\n <TenantForm open={true} onClose={handleClose} tenant={tenant} onSave={handleSave} />\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ import { emitTenantLifecycle as d } from "../utils/tenantEvents.js";
19
19
  import { TENANT_FEATURE_FLAGS as f } from "../constants/featureFlags.js";
20
20
  import { useCallback as p, useMemo as m, useState as h } from "react";
21
21
  import { Building2 as g, Plus as ge, Search as _ } from "lucide-react";
22
- import { useNavigate as v } from "react-router-dom";
22
+ import { useNavigate as v } 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
25
  import { useI18n as _e } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
@@ -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 { 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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"}
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';\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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"}
@@ -17,7 +17,7 @@ import { useAdminStore as v } from "../store/adminStore.js";
17
17
  import { UserStatus as y } from "../../generated/global-types.js";
18
18
  import { useCallback as b, useMemo as x, useState as S } from "react";
19
19
  import { Search as C, Users as w } from "lucide-react";
20
- import { useNavigate as T } from "react-router-dom";
20
+ import { useNavigate as T } from "react-router";
21
21
  import { jsx as E, jsxs as D } from "react/jsx-runtime";
22
22
  import { useI18n as O } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
23
23
  import { EmphasisPanel as k, PagePurpose as A } from "@burdenoff/fe-libs/ui";
@@ -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, 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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"}
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';\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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"}
@@ -1,6 +1,6 @@
1
1
  import { ToastProvider as e } from "../components/Toast.js";
2
2
  import { createContext as t, useContext as n, useMemo as r } from "react";
3
- import { MemoryRouter as i } from "react-router-dom";
3
+ import { MemoryRouter as i } from "react-router";
4
4
  import { QueryClient as a, QueryClientProvider as o } from "@tanstack/react-query";
5
5
  import { PermissionProvider as s } from "@burdenoff/fe-libs/shared/providers/shell";
6
6
  import { jsx as c } from "react/jsx-runtime";
@@ -1 +1 @@
1
- {"version":3,"file":"AdminProvider.js","names":[],"sources":["../../../src/admin/providers/AdminProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo } from 'react';\nimport { MemoryRouter } from 'react-router-dom';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';\nimport type { AdminRootProps } from '../types';\nimport { ToastProvider } from '../components/Toast';\n\n/**\n * Context value for Admin microfrontend\n */\ntype AdminContextValue = AdminRootProps;\n\nconst AdminContext = createContext<AdminContextValue | null>(null);\n\n/**\n * Hook to access Admin context\n */\nexport const useAdmin = () => {\n const context = useContext(AdminContext);\n if (!context) {\n throw new Error('useAdmin must be used within AdminProvider');\n }\n return context;\n};\n\n/**\n * Provider component for Admin microfrontend\n * Sets up routing, context, and React Query client\n */\nexport const AdminProvider: FC<PropsWithChildren<AdminRootProps>> = (props) => {\n const {\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n apiGatewayUrl,\n authToken,\n defaultView,\n onTenantSelect,\n onUserSelect,\n } = props;\n\n const contextValue: AdminContextValue = {\n basePath,\n navigate,\n currentUser,\n tenantId,\n apiGatewayUrl,\n authToken,\n defaultView,\n onTenantSelect,\n onUserSelect,\n };\n\n // Create QueryClient instance (memoized to avoid recreation on re-renders)\n const queryClient = useMemo(\n () =>\n new QueryClient({\n defaultOptions: {\n queries: {\n retry: 1,\n refetchOnWindowFocus: false,\n staleTime: 5 * 60 * 1000, // 5 minutes\n },\n },\n }),\n []\n );\n\n // Wrap content with QueryClientProvider, PermissionProvider and ToastProvider\n const content = (\n <QueryClientProvider client={queryClient}>\n <ToastProvider>\n <AdminContext.Provider value={contextValue}>\n {/* scopeId is the host shell's tenantId for context; gateway=\"global\" ensures\n permissions are fetched from the global RBAC service, not workspace-scoped */}\n <PermissionProvider scopeId={tenantId ?? null} gateway=\"global\">\n {children}\n </PermissionProvider>\n </AdminContext.Provider>\n </ToastProvider>\n </QueryClientProvider>\n );\n\n // When integrated into a shell (basePath provided), the shell already has a router\n // so we don't create another one. When standalone (no basePath), use MemoryRouter.\n if (basePath) {\n // Integrated mode: shell already has BrowserRouter, don't create nested router\n return content;\n }\n\n // Standalone mode: create MemoryRouter for independent operation\n return <MemoryRouter>{content}</MemoryRouter>;\n};\n"],"mappings":";;;;;;;AAaA,IAAM,IAAe,EAAwC,KAAK,EAKrD,UAAiB;CAC5B,IAAM,IAAU,EAAW,EAAa;AACxC,KAAI,CAAC,EACH,OAAU,MAAM,6CAA6C;AAE/D,QAAO;GAOI,KAAwD,MAAU;CAC7E,IAAM,EACJ,aACA,aACA,aACA,gBACA,aACA,kBACA,cACA,gBACA,mBACA,oBACE,GAEE,IAAkC;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,EAkBK,IACJ,kBAAC,GAAD;EAAqB,QAhBH,QAEhB,IAAI,EAAY,EACd,gBAAgB,EACd,SAAS;GACP,OAAO;GACP,sBAAsB;GACtB,WAAW,MAAS;GACrB,EACF,EACF,CAAC,EACJ,EAAE,CACH;YAKG,kBAAC,GAAD,EAAA,UACE,kBAAC,EAAa,UAAd;GAAuB,OAAO;aAG5B,kBAAC,GAAD;IAAoB,SAAS,KAAY;IAAM,SAAQ;IACpD;IACkB,CAAA;GACC,CAAA,EACV,CAAA;EACI,CAAA;AAWxB,QANI,IAEK,IAIF,kBAAC,GAAD,EAAA,UAAe,GAAuB,CAAA"}
1
+ {"version":3,"file":"AdminProvider.js","names":[],"sources":["../../../src/admin/providers/AdminProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo } from 'react';\nimport { MemoryRouter } from 'react-router';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';\nimport type { AdminRootProps } from '../types';\nimport { ToastProvider } from '../components/Toast';\n\n/**\n * Context value for Admin microfrontend\n */\ntype AdminContextValue = AdminRootProps;\n\nconst AdminContext = createContext<AdminContextValue | null>(null);\n\n/**\n * Hook to access Admin context\n */\nexport const useAdmin = () => {\n const context = useContext(AdminContext);\n if (!context) {\n throw new Error('useAdmin must be used within AdminProvider');\n }\n return context;\n};\n\n/**\n * Provider component for Admin microfrontend\n * Sets up routing, context, and React Query client\n */\nexport const AdminProvider: FC<PropsWithChildren<AdminRootProps>> = (props) => {\n const {\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n apiGatewayUrl,\n authToken,\n defaultView,\n onTenantSelect,\n onUserSelect,\n } = props;\n\n const contextValue: AdminContextValue = {\n basePath,\n navigate,\n currentUser,\n tenantId,\n apiGatewayUrl,\n authToken,\n defaultView,\n onTenantSelect,\n onUserSelect,\n };\n\n // Create QueryClient instance (memoized to avoid recreation on re-renders)\n const queryClient = useMemo(\n () =>\n new QueryClient({\n defaultOptions: {\n queries: {\n retry: 1,\n refetchOnWindowFocus: false,\n staleTime: 5 * 60 * 1000, // 5 minutes\n },\n },\n }),\n []\n );\n\n // Wrap content with QueryClientProvider, PermissionProvider and ToastProvider\n const content = (\n <QueryClientProvider client={queryClient}>\n <ToastProvider>\n <AdminContext.Provider value={contextValue}>\n {/* scopeId is the host shell's tenantId for context; gateway=\"global\" ensures\n permissions are fetched from the global RBAC service, not workspace-scoped */}\n <PermissionProvider scopeId={tenantId ?? null} gateway=\"global\">\n {children}\n </PermissionProvider>\n </AdminContext.Provider>\n </ToastProvider>\n </QueryClientProvider>\n );\n\n // When integrated into a shell (basePath provided), the shell already has a router\n // so we don't create another one. When standalone (no basePath), use MemoryRouter.\n if (basePath) {\n // Integrated mode: shell already has BrowserRouter, don't create nested router\n return content;\n }\n\n // Standalone mode: create MemoryRouter for independent operation\n return <MemoryRouter>{content}</MemoryRouter>;\n};\n"],"mappings":";;;;;;;AAaA,IAAM,IAAe,EAAwC,KAAK,EAKrD,UAAiB;CAC5B,IAAM,IAAU,EAAW,EAAa;AACxC,KAAI,CAAC,EACH,OAAU,MAAM,6CAA6C;AAE/D,QAAO;GAOI,KAAwD,MAAU;CAC7E,IAAM,EACJ,aACA,aACA,aACA,gBACA,aACA,kBACA,cACA,gBACA,mBACA,oBACE,GAEE,IAAkC;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,EAkBK,IACJ,kBAAC,GAAD;EAAqB,QAhBH,QAEhB,IAAI,EAAY,EACd,gBAAgB,EACd,SAAS;GACP,OAAO;GACP,sBAAsB;GACtB,WAAW,MAAS;GACrB,EACF,EACF,CAAC,EACJ,EAAE,CACH;YAKG,kBAAC,GAAD,EAAA,UACE,kBAAC,EAAa,UAAd;GAAuB,OAAO;aAG5B,kBAAC,GAAD;IAAoB,SAAS,KAAY;IAAM,SAAQ;IACpD;IACkB,CAAA;GACC,CAAA,EACV,CAAA;EACI,CAAA;AAWxB,QANI,IAEK,IAIF,kBAAC,GAAD,EAAA,UAAe,GAAuB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-admin",
3
- "version": "2026.720.1",
3
+ "version": "2026.801.1",
4
4
  "description": "Burdenoff Admin Console Microfrontend - Platform administration and management interface",
5
5
  "type": "module",
6
6
  "files": [
@@ -22,27 +22,27 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "scripts": {
24
24
  "dev": "vite",
25
- "build": "tsc && vite build",
25
+ "build": "tsc && bash scripts/build-limits.sh --gate vite build",
26
26
  "schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
27
27
  "codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
28
28
  "codegen:all": "bun run schema:pull && bun run codegen",
29
29
  "lint": "eslint .",
30
30
  "lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
31
31
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
32
- "type:check": "tsc --noEmit",
33
- "sanity": "bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build",
32
+ "type:check": "bash scripts/build-limits.sh tsc --noEmit",
33
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build'",
34
34
  "switch:status": "bun scripts/deps-mode.ts status",
35
35
  "switch:remote": "bun scripts/deps-mode.ts remote",
36
36
  "switch:local": "bun scripts/deps-mode.ts local",
37
37
  "switch:check": "bun scripts/deps-mode.ts check",
38
38
  "prepare": "lefthook install || true",
39
39
  "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
40
- "test": "vitest run",
41
- "test:coverage": "vitest run --coverage"
40
+ "test": "bash scripts/build-limits.sh vitest run",
41
+ "test:coverage": "bash scripts/build-limits.sh vitest run --coverage"
42
42
  },
43
43
  "dependencies": {
44
44
  "@apollo/client": "^4.0.11",
45
- "@burdenoff/fe-libs": "2026.720.3",
45
+ "@burdenoff/fe-libs": "2026.801.1",
46
46
  "@hookform/resolvers": "^5.1.1",
47
47
  "@tanstack/react-query": "^5.90.16",
48
48
  "clsx": "^2.1.1",
@@ -53,7 +53,7 @@
53
53
  "react": "^19.0.0",
54
54
  "react-dom": "^19.0.0",
55
55
  "react-hook-form": "^7.56.4",
56
- "react-router-dom": "^7.1.3",
56
+ "react-router": "^8.3.0",
57
57
  "recharts": "^2.15.3",
58
58
  "tailwind-merge": "^3.4.0",
59
59
  "zod": "^3.25.34",