@burdenoff/microfe-admin 2026.624.1 → 2026.626.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.
- package/dist/admin/components/AccessDenied.js +2 -2
- package/dist/admin/components/AccessDenied.js.map +1 -1
- package/dist/admin/components/ActivityFeed.js +4 -4
- package/dist/admin/components/ActivityFeed.js.map +1 -1
- package/dist/admin/components/Charts.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +1 -1
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/components/DataTable.js +5 -5
- package/dist/admin/components/DataTable.js.map +1 -1
- package/dist/admin/components/FormSheet.js +1 -1
- package/dist/admin/components/FormSheet.js.map +1 -1
- package/dist/admin/components/MetricCard.js +40 -37
- package/dist/admin/components/MetricCard.js.map +1 -1
- package/dist/admin/components/PageLayout.js +28 -22
- package/dist/admin/components/PageLayout.js.map +1 -1
- package/dist/admin/components/TenantSsoPanel.js +1 -1
- package/dist/admin/components/TenantSsoPanel.js.map +1 -1
- package/dist/admin/components/Toast.js +11 -11
- package/dist/admin/components/Toast.js.map +1 -1
- package/dist/admin/components/forms/SubscriptionForm.js +2 -2
- package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
- package/dist/admin/components/forms/TenantForm.js +1 -1
- package/dist/admin/components/forms/UserForm.js +1 -1
- package/dist/admin/components/ui.js +48 -41
- package/dist/admin/components/ui.js.map +1 -1
- package/dist/admin/constants/index.js +33 -24
- package/dist/admin/constants/index.js.map +1 -1
- package/dist/admin/pages/AuditLogsPage.js +94 -77
- package/dist/admin/pages/AuditLogsPage.js.map +1 -1
- package/dist/admin/pages/BillingPage.js +70 -18
- package/dist/admin/pages/BillingPage.js.map +1 -1
- package/dist/admin/pages/CommunicationsPage.js +98 -90
- package/dist/admin/pages/CommunicationsPage.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +276 -259
- package/dist/admin/pages/ContentPagesPage.js.map +1 -1
- package/dist/admin/pages/DashboardPage.js +114 -82
- package/dist/admin/pages/DashboardPage.js.map +1 -1
- package/dist/admin/pages/FeatureFlagsPage.js +94 -74
- package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
- package/dist/admin/pages/OrganizationsListPage.js +106 -86
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/ProductsPage.js +139 -128
- package/dist/admin/pages/ProductsPage.js.map +1 -1
- package/dist/admin/pages/ReferralsPage.js +74 -52
- package/dist/admin/pages/ReferralsPage.js.map +1 -1
- package/dist/admin/pages/RolesPermissionsPage.js +137 -116
- package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
- package/dist/admin/pages/SREPage.js +38 -32
- package/dist/admin/pages/SREPage.js.map +1 -1
- package/dist/admin/pages/SandboxSettingsPage.js +361 -322
- package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.js +145 -130
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.js +123 -118
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +261 -218
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantFormRoutePage.js +65 -61
- package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +88 -71
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +129 -112
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/WaitlistPage.js +80 -63
- package/dist/admin/pages/WaitlistPage.js.map +1 -1
- package/dist/admin/pages/WorkflowsPage.js +108 -100
- package/dist/admin/pages/WorkflowsPage.js.map +1 -1
- package/dist/admin/pages/WorkspacesListPage.js +62 -49
- package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
- package/dist/node_modules/@hookform/resolvers/dist/resolvers.js +2 -2
- package/dist/node_modules/@hookform/resolvers/dist/resolvers.js.map +1 -1
- package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js +35 -26
- package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js.map +1 -1
- package/dist/node_modules/react-hook-form/dist/index.esm.js +609 -516
- package/dist/node_modules/react-hook-form/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,89 +4,90 @@ import { useAdminPermissions as n } from "../hooks/useAdminPermissions.js";
|
|
|
4
4
|
import { Avatar as r, Button as i, EmptyState as a, Input as o } from "../components/ui.js";
|
|
5
5
|
import { PageLayout as s } from "../components/PageLayout.js";
|
|
6
6
|
import { UserStatusBadge as c } from "../components/StatusBadge.js";
|
|
7
|
-
import { formatDate as
|
|
8
|
-
import { DataTable as
|
|
7
|
+
import { formatDate as l, formatNumber as u, formatRelativeTime as ee } from "../utils/formatters.js";
|
|
8
|
+
import { DataTable as te, Pagination as d } from "../components/DataTable.js";
|
|
9
9
|
import { tWithFallback as f } from "../utils/i18n.js";
|
|
10
10
|
import { RowActions as p } from "../components/RowActions.js";
|
|
11
11
|
import { PermissionGuard as m } from "../components/PermissionGuard.js";
|
|
12
12
|
import "../components/index.js";
|
|
13
13
|
import { joinPath as h } from "../utils/navigation.js";
|
|
14
14
|
import { mapUserToUi as g } from "../utils/mappers.js";
|
|
15
|
-
import { useListUsersQuery as
|
|
16
|
-
import { useAdminStore as
|
|
17
|
-
import { UserStatus as
|
|
18
|
-
import { useCallback as
|
|
19
|
-
import { Search as
|
|
20
|
-
import { useNavigate as
|
|
21
|
-
import { jsx as
|
|
22
|
-
import {
|
|
15
|
+
import { useListUsersQuery as ne, useUpdateUserMutation as _ } from "../../generated/global-operations.js";
|
|
16
|
+
import { useAdminStore as v } from "../store/adminStore.js";
|
|
17
|
+
import { UserStatus as y } from "../../generated/global-types.js";
|
|
18
|
+
import { useCallback as b, useMemo as x, useState as S } from "react";
|
|
19
|
+
import { Search as C, Users as w } from "lucide-react";
|
|
20
|
+
import { useNavigate as T } from "react-router-dom";
|
|
21
|
+
import { jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
22
|
+
import { EmphasisPanel as O, PagePurpose as k } from "@burdenoff/fe-libs/ui";
|
|
23
|
+
import { useI18n as A } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
23
24
|
//#region src/admin/pages/UsersListPage.tsx
|
|
24
|
-
var
|
|
25
|
-
function
|
|
26
|
-
let { canViewUsers:
|
|
25
|
+
var j = 20;
|
|
26
|
+
function M() {
|
|
27
|
+
let { canViewUsers: M, canUpdateUsers: N, isLoading: P } = n(), F = T(), { basePath: I = "" } = t(), { userFilter: L, updateUserFilter: R } = v(), z = e(), { t: B } = A(), V = b((e, t, n) => f(B, e, t, n), [B]), [H, U] = S(1), W = x(() => {
|
|
27
28
|
let e = {};
|
|
28
|
-
|
|
29
|
+
L.search && (e.search = L.search);
|
|
29
30
|
let t = {
|
|
30
|
-
active:
|
|
31
|
-
inactive:
|
|
32
|
-
blocked:
|
|
31
|
+
active: y.Active,
|
|
32
|
+
inactive: y.Inactive,
|
|
33
|
+
blocked: y.Suspended
|
|
33
34
|
};
|
|
34
|
-
return
|
|
35
|
-
}, [
|
|
35
|
+
return L.status !== "all" && t[L.status] && (e.status = t[L.status]), e;
|
|
36
|
+
}, [L.search, L.status]), G = P || !M, { data: K, loading: q, error: J, refetch: Y } = ne({
|
|
36
37
|
variables: {
|
|
37
|
-
filter:
|
|
38
|
+
filter: W,
|
|
38
39
|
pagination: {
|
|
39
|
-
limit:
|
|
40
|
-
offset: (
|
|
40
|
+
limit: j,
|
|
41
|
+
offset: (H - 1) * j
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
fetchPolicy: "cache-and-network",
|
|
44
|
-
skip:
|
|
45
|
-
}), [
|
|
45
|
+
skip: G
|
|
46
|
+
}), [X, { loading: Z }] = _({
|
|
46
47
|
onCompleted: () => {
|
|
47
|
-
|
|
48
|
+
z.success(V("admin.users.toast.updatedTitle", "User updated"), V("admin.users.toast.updatedDescription", "User status has been successfully updated.")), Y();
|
|
48
49
|
},
|
|
49
50
|
onError: (e) => {
|
|
50
|
-
|
|
51
|
+
z.error(V("admin.users.toast.updateFailedTitle", "Update failed"), e.message);
|
|
51
52
|
}
|
|
52
|
-
}),
|
|
53
|
-
let t = e.status === "active" ?
|
|
54
|
-
await
|
|
53
|
+
}), Q = b(async (e) => {
|
|
54
|
+
let t = e.status === "active" ? y.Inactive : y.Active;
|
|
55
|
+
await X({ variables: {
|
|
55
56
|
id: e.id,
|
|
56
57
|
input: { status: t }
|
|
57
58
|
} });
|
|
58
|
-
}, [
|
|
59
|
+
}, [X]), re = x(() => (K?.users?.users ?? []).map(g), [K]), $ = K?.users?.totalCount ?? 0, ie = Math.ceil($ / j), ae = b((e) => {
|
|
59
60
|
let t = [{
|
|
60
61
|
key: "view",
|
|
61
|
-
label:
|
|
62
|
-
onClick: () =>
|
|
62
|
+
label: V("admin.users.actions.viewDetails", "View Details"),
|
|
63
|
+
onClick: () => F(h(I, `users/${e.id}`))
|
|
63
64
|
}];
|
|
64
|
-
return
|
|
65
|
+
return N && t.push({
|
|
65
66
|
key: "toggle-status",
|
|
66
|
-
label: e.status === "active" ?
|
|
67
|
-
onClick: () =>
|
|
67
|
+
label: e.status === "active" ? V("admin.users.actions.disableUser", "Disable User") : V("admin.users.actions.enableUser", "Enable User"),
|
|
68
|
+
onClick: () => Q(e)
|
|
68
69
|
}), t;
|
|
69
70
|
}, [
|
|
70
|
-
P,
|
|
71
71
|
F,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
I,
|
|
73
|
+
N,
|
|
74
|
+
Q,
|
|
75
|
+
V
|
|
76
|
+
]), oe = [
|
|
76
77
|
{
|
|
77
78
|
key: "name",
|
|
78
|
-
header:
|
|
79
|
-
render: (e) => /* @__PURE__ */
|
|
79
|
+
header: V("admin.users.table.user", "User"),
|
|
80
|
+
render: (e) => /* @__PURE__ */ D("div", {
|
|
80
81
|
className: "flex items-center gap-3",
|
|
81
|
-
children: [/* @__PURE__ */
|
|
82
|
+
children: [/* @__PURE__ */ E(r, {
|
|
82
83
|
fallback: (e.firstName?.[0] ?? e.email[0]).toUpperCase(),
|
|
83
84
|
size: "sm"
|
|
84
|
-
}), /* @__PURE__ */
|
|
85
|
+
}), /* @__PURE__ */ D("div", {
|
|
85
86
|
className: "min-w-0",
|
|
86
|
-
children: [/* @__PURE__ */
|
|
87
|
+
children: [/* @__PURE__ */ E("div", {
|
|
87
88
|
className: "truncate font-medium text-text-primary",
|
|
88
89
|
children: e.name || e.email
|
|
89
|
-
}), /* @__PURE__ */
|
|
90
|
+
}), /* @__PURE__ */ E("div", {
|
|
90
91
|
className: "truncate text-xs text-text-secondary",
|
|
91
92
|
children: e.email
|
|
92
93
|
})]
|
|
@@ -95,149 +96,165 @@ function j() {
|
|
|
95
96
|
},
|
|
96
97
|
{
|
|
97
98
|
key: "status",
|
|
98
|
-
header:
|
|
99
|
+
header: V("admin.users.table.status", "Status"),
|
|
99
100
|
width: "120px",
|
|
100
|
-
render: (e) => /* @__PURE__ */
|
|
101
|
+
render: (e) => /* @__PURE__ */ E(c, { status: e.status })
|
|
101
102
|
},
|
|
102
103
|
{
|
|
103
104
|
key: "role",
|
|
104
|
-
header:
|
|
105
|
+
header: V("admin.users.table.role", "Role"),
|
|
105
106
|
width: "120px"
|
|
106
107
|
},
|
|
107
108
|
{
|
|
108
109
|
key: "tenantId",
|
|
109
|
-
header:
|
|
110
|
+
header: V("admin.users.table.tenant", "Tenant"),
|
|
110
111
|
width: "150px",
|
|
111
|
-
render: (e) => /* @__PURE__ */
|
|
112
|
+
render: (e) => /* @__PURE__ */ E("span", {
|
|
112
113
|
className: "text-xs text-text-secondary",
|
|
113
114
|
children: e.tenantId || "—"
|
|
114
115
|
})
|
|
115
116
|
},
|
|
116
117
|
{
|
|
117
118
|
key: "createdAt",
|
|
118
|
-
header:
|
|
119
|
+
header: V("admin.users.table.joined", "Joined"),
|
|
119
120
|
width: "120px",
|
|
120
121
|
sortable: !0,
|
|
121
|
-
render: (e) =>
|
|
122
|
+
render: (e) => l(e.createdAt)
|
|
122
123
|
},
|
|
123
124
|
{
|
|
124
125
|
key: "lastLoginAt",
|
|
125
|
-
header:
|
|
126
|
+
header: V("admin.users.table.lastLogin", "Last Login"),
|
|
126
127
|
width: "130px",
|
|
127
|
-
render: (e) => e.lastLoginAt ?
|
|
128
|
+
render: (e) => e.lastLoginAt ? ee(e.lastLoginAt) : V("admin.users.table.never", "Never")
|
|
128
129
|
},
|
|
129
130
|
{
|
|
130
131
|
key: "actions",
|
|
131
132
|
header: "",
|
|
132
133
|
width: "50px",
|
|
133
134
|
align: "right",
|
|
134
|
-
render: (e) => /* @__PURE__ */
|
|
135
|
-
actions:
|
|
136
|
-
ariaLabel:
|
|
135
|
+
render: (e) => /* @__PURE__ */ E(p, {
|
|
136
|
+
actions: ae(e),
|
|
137
|
+
ariaLabel: V("admin.users.table.actionsFor", "Actions for {{email}}", { email: e.email })
|
|
137
138
|
})
|
|
138
139
|
}
|
|
139
140
|
];
|
|
140
|
-
return /* @__PURE__ */
|
|
141
|
-
allowed:
|
|
142
|
-
isLoading:
|
|
143
|
-
pageTitle:
|
|
144
|
-
denyMessage:
|
|
145
|
-
children: /* @__PURE__ */
|
|
146
|
-
title:
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
return /* @__PURE__ */ E(m, {
|
|
142
|
+
allowed: M,
|
|
143
|
+
isLoading: P,
|
|
144
|
+
pageTitle: V("admin.users.title", "Users"),
|
|
145
|
+
denyMessage: V("admin.users.denyMessage", "You don't have permission to view users."),
|
|
146
|
+
children: /* @__PURE__ */ E(s, {
|
|
147
|
+
title: V("admin.users.title", "Users"),
|
|
148
|
+
gradientWord: V("admin.users.title", "Users"),
|
|
149
|
+
description: $ ? V("admin.users.descriptionWithTotal", "Manage all users on the platform · {{total}} total", { total: $ }) : V("admin.users.description", "Manage all users on the platform"),
|
|
150
|
+
children: /* @__PURE__ */ D("div", {
|
|
149
151
|
className: "space-y-4",
|
|
150
152
|
children: [
|
|
151
|
-
/* @__PURE__ */
|
|
153
|
+
/* @__PURE__ */ E(k, { children: V("admin.users.purpose", "Every person with an account across all tenants lives here. Use this page to find someone by name or email, check when they last signed in, and enable or disable their access when they join, leave, or need to be locked out.") }),
|
|
154
|
+
/* @__PURE__ */ D(O, {
|
|
155
|
+
className: "flex items-center gap-4 p-5",
|
|
156
|
+
children: [/* @__PURE__ */ E("div", {
|
|
157
|
+
className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
|
|
158
|
+
children: /* @__PURE__ */ E(w, { className: "h-6 w-6" })
|
|
159
|
+
}), /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("div", {
|
|
160
|
+
className: "text-3xl font-bold tabular-nums text-text-primary",
|
|
161
|
+
children: u($)
|
|
162
|
+
}), /* @__PURE__ */ E("p", {
|
|
163
|
+
className: "text-sm text-text-secondary",
|
|
164
|
+
children: V("admin.users.emphasis.label", "users across all tenants")
|
|
165
|
+
})] })]
|
|
166
|
+
}),
|
|
167
|
+
/* @__PURE__ */ D("div", {
|
|
152
168
|
className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
|
|
153
|
-
children: [/* @__PURE__ */
|
|
169
|
+
children: [/* @__PURE__ */ D("div", {
|
|
154
170
|
className: "relative max-w-sm flex-1",
|
|
155
|
-
children: [/* @__PURE__ */
|
|
156
|
-
placeholder:
|
|
157
|
-
value:
|
|
171
|
+
children: [/* @__PURE__ */ E(C, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ E(o, {
|
|
172
|
+
placeholder: V("admin.users.searchPlaceholder", "Search by email or name..."),
|
|
173
|
+
value: L.search,
|
|
158
174
|
onChange: (e) => {
|
|
159
|
-
|
|
175
|
+
U(1), R({ search: e.target.value });
|
|
160
176
|
},
|
|
161
177
|
className: "pl-9"
|
|
162
178
|
})]
|
|
163
|
-
}), /* @__PURE__ */
|
|
179
|
+
}), /* @__PURE__ */ D("div", {
|
|
164
180
|
className: "flex items-center gap-2",
|
|
165
|
-
children: [/* @__PURE__ */
|
|
166
|
-
value:
|
|
181
|
+
children: [/* @__PURE__ */ D("select", {
|
|
182
|
+
value: L.status,
|
|
167
183
|
onChange: (e) => {
|
|
168
|
-
|
|
184
|
+
U(1), R({ status: e.target.value });
|
|
169
185
|
},
|
|
170
186
|
className: "h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2",
|
|
171
187
|
children: [
|
|
172
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ E("option", {
|
|
173
189
|
value: "all",
|
|
174
|
-
children:
|
|
190
|
+
children: V("admin.users.filters.status.all", "All Status")
|
|
175
191
|
}),
|
|
176
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ E("option", {
|
|
177
193
|
value: "active",
|
|
178
|
-
children:
|
|
194
|
+
children: V("admin.users.filters.status.active", "Active")
|
|
179
195
|
}),
|
|
180
|
-
/* @__PURE__ */
|
|
196
|
+
/* @__PURE__ */ E("option", {
|
|
181
197
|
value: "inactive",
|
|
182
|
-
children:
|
|
198
|
+
children: V("admin.users.filters.status.inactive", "Inactive")
|
|
183
199
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
200
|
+
/* @__PURE__ */ E("option", {
|
|
185
201
|
value: "blocked",
|
|
186
|
-
children:
|
|
202
|
+
children: V("admin.users.filters.status.suspended", "Suspended")
|
|
187
203
|
})
|
|
188
204
|
]
|
|
189
|
-
}), (
|
|
205
|
+
}), (L.search || L.status !== "all") && /* @__PURE__ */ E(i, {
|
|
190
206
|
variant: "ghost",
|
|
191
207
|
onClick: () => {
|
|
192
|
-
|
|
208
|
+
U(1), R({
|
|
193
209
|
search: "",
|
|
194
210
|
status: "all"
|
|
195
211
|
});
|
|
196
212
|
},
|
|
197
|
-
children:
|
|
213
|
+
children: V("admin.users.filters.clear", "Clear")
|
|
198
214
|
})]
|
|
199
215
|
})]
|
|
200
216
|
}),
|
|
201
|
-
|
|
217
|
+
J && /* @__PURE__ */ D("div", {
|
|
202
218
|
className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
|
|
203
219
|
children: [
|
|
204
|
-
|
|
220
|
+
V("admin.users.error.loadFailed", "Failed to load users"),
|
|
205
221
|
": ",
|
|
206
|
-
|
|
222
|
+
J.message
|
|
207
223
|
]
|
|
208
224
|
}),
|
|
209
|
-
/* @__PURE__ */
|
|
210
|
-
columns:
|
|
211
|
-
data:
|
|
225
|
+
/* @__PURE__ */ E(te, {
|
|
226
|
+
columns: oe,
|
|
227
|
+
data: re,
|
|
212
228
|
keyExtractor: (e) => e.id,
|
|
213
|
-
onRowClick: (e) =>
|
|
214
|
-
sortBy:
|
|
215
|
-
sortOrder:
|
|
216
|
-
onSort: (e, t) =>
|
|
229
|
+
onRowClick: (e) => F(h(I, `users/${e.id}`)),
|
|
230
|
+
sortBy: L.sortBy,
|
|
231
|
+
sortOrder: L.sortOrder,
|
|
232
|
+
onSort: (e, t) => R({
|
|
217
233
|
sortBy: e,
|
|
218
234
|
sortOrder: t
|
|
219
235
|
}),
|
|
220
|
-
loading:
|
|
221
|
-
emptyState: /* @__PURE__ */
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
236
|
+
loading: q || Z,
|
|
237
|
+
emptyState: /* @__PURE__ */ E(a, {
|
|
238
|
+
illustration: "empty-people",
|
|
239
|
+
icon: /* @__PURE__ */ E(w, { className: "h-8 w-8" }),
|
|
240
|
+
title: V("admin.users.empty.title", "No users found"),
|
|
241
|
+
description: V("admin.users.empty.description", "Try adjusting your search or filters"),
|
|
242
|
+
action: /* @__PURE__ */ E(i, {
|
|
226
243
|
variant: "outline",
|
|
227
|
-
onClick: () =>
|
|
244
|
+
onClick: () => R({
|
|
228
245
|
search: "",
|
|
229
246
|
status: "all"
|
|
230
247
|
}),
|
|
231
|
-
children:
|
|
248
|
+
children: V("admin.users.empty.clearFilters", "Clear filters")
|
|
232
249
|
})
|
|
233
250
|
})
|
|
234
251
|
}),
|
|
235
|
-
$ >
|
|
236
|
-
page:
|
|
237
|
-
totalPages:
|
|
252
|
+
$ > j && /* @__PURE__ */ E(d, {
|
|
253
|
+
page: H,
|
|
254
|
+
totalPages: ie,
|
|
238
255
|
totalItems: $,
|
|
239
|
-
pageSize:
|
|
240
|
-
onPageChange:
|
|
256
|
+
pageSize: j,
|
|
257
|
+
onPageChange: U
|
|
241
258
|
})
|
|
242
259
|
]
|
|
243
260
|
})
|
|
@@ -245,6 +262,6 @@ function j() {
|
|
|
245
262
|
});
|
|
246
263
|
}
|
|
247
264
|
//#endregion
|
|
248
|
-
export {
|
|
265
|
+
export { M as UsersListPage };
|
|
249
266
|
|
|
250
267
|
//# sourceMappingURL=UsersListPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsersListPage.js","names":[],"sources":["../../../src/admin/pages/UsersListPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Search, Users as UsersIcon } from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminStore } from '../store/adminStore';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n UserStatusBadge,\n EmptyState,\n Avatar,\n Button,\n RowActions,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate, formatRelativeTime } from '../utils/formatters';\nimport { mapUserToUi } from '../utils/mappers';\nimport type { User, UserFilter } from '../types';\nimport { useListUsersQuery, useUpdateUserMutation } from '../../generated/global-operations';\nimport { UserStatus as GqlUserStatus } from '../../generated/global-types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\nconst PAGE_SIZE = 20;\n\n/**\n * Users list page — connected to real backend with enable/disable actions\n */\nexport function UsersListPage() {\n const { canViewUsers, canUpdateUsers, isLoading: permissionsLoading } = useAdminPermissions();\n\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const { userFilter, updateUserFilter } = useAdminStore();\n const toast = useToast();\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const [page, setPage] = useState(1);\n\n // Build GraphQL filter\n const gqlFilter = useMemo(() => {\n const f: {\n search?: string;\n status?: GqlUserStatus;\n } = {};\n if (userFilter.search) f.search = userFilter.search;\n const statusMap: Record<string, GqlUserStatus> = {\n active: GqlUserStatus.Active,\n inactive: GqlUserStatus.Inactive,\n blocked: GqlUserStatus.Suspended,\n };\n if (userFilter.status !== 'all' && statusMap[userFilter.status]) {\n f.status = statusMap[userFilter.status];\n }\n return f;\n }, [userFilter.search, userFilter.status]);\n\n const skipQueries = permissionsLoading || !canViewUsers;\n\n const { data, loading, error, refetch } = useListUsersQuery({\n variables: {\n filter: gqlFilter,\n pagination: {\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [updateUser, { loading: isUpdating }] = useUpdateUserMutation({\n onCompleted: () => {\n toast.success(\n tr('admin.users.toast.updatedTitle', 'User updated'),\n tr('admin.users.toast.updatedDescription', 'User status has been successfully updated.')\n );\n void refetch();\n },\n onError: (err) => {\n toast.error(tr('admin.users.toast.updateFailedTitle', 'Update failed'), err.message);\n },\n });\n\n const handleToggleStatus = useCallback(\n async (user: User) => {\n const newStatus = user.status === 'active' ? GqlUserStatus.Inactive : GqlUserStatus.Active;\n await updateUser({\n variables: {\n id: user.id,\n input: {\n status: newStatus,\n },\n },\n });\n },\n [updateUser]\n );\n\n const users: User[] = useMemo(() => (data?.users?.users ?? []).map(mapUserToUi), [data]);\n\n const total = data?.users?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const getRowActions = useCallback(\n (user: User) => {\n const actions = [\n {\n key: 'view',\n label: tr('admin.users.actions.viewDetails', 'View Details'),\n onClick: () => navigate(joinPath(basePath, `users/${user.id}`)),\n },\n ];\n\n if (canUpdateUsers) {\n actions.push({\n key: 'toggle-status',\n label:\n user.status === 'active'\n ? tr('admin.users.actions.disableUser', 'Disable User')\n : tr('admin.users.actions.enableUser', 'Enable User'),\n onClick: () => handleToggleStatus(user),\n });\n }\n\n return actions;\n },\n [navigate, basePath, canUpdateUsers, handleToggleStatus, tr]\n );\n\n const columns = [\n {\n key: 'name',\n header: tr('admin.users.table.user', 'User'),\n render: (user: User) => (\n <div className=\"flex items-center gap-3\">\n <Avatar fallback={(user.firstName?.[0] ?? user.email[0]).toUpperCase()} size=\"sm\" />\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-text-primary\">{user.name || user.email}</div>\n <div className=\"truncate text-xs text-text-secondary\">{user.email}</div>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('admin.users.table.status', 'Status'),\n width: '120px',\n render: (user: User) => <UserStatusBadge status={user.status} />,\n },\n {\n key: 'role',\n header: tr('admin.users.table.role', 'Role'),\n width: '120px',\n },\n {\n key: 'tenantId',\n header: tr('admin.users.table.tenant', 'Tenant'),\n width: '150px',\n render: (user: User) => (\n <span className=\"text-xs text-text-secondary\">{user.tenantId || '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: tr('admin.users.table.joined', 'Joined'),\n width: '120px',\n sortable: true,\n render: (user: User) => formatDate(user.createdAt),\n },\n {\n key: 'lastLoginAt',\n header: tr('admin.users.table.lastLogin', 'Last Login'),\n width: '130px',\n render: (user: User) =>\n user.lastLoginAt\n ? formatRelativeTime(user.lastLoginAt)\n : tr('admin.users.table.never', 'Never'),\n },\n {\n key: 'actions',\n header: '',\n width: '50px',\n align: 'right' as const,\n render: (user: User) => (\n <RowActions\n actions={getRowActions(user)}\n ariaLabel={tr('admin.users.table.actionsFor', 'Actions for {{email}}', {\n email: user.email,\n })}\n />\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewUsers}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.users.title', 'Users')}\n denyMessage={tr('admin.users.denyMessage', \"You don't have permission to view users.\")}\n >\n <PageLayout\n title={tr('admin.users.title', 'Users')}\n description={\n total\n ? tr(\n 'admin.users.descriptionWithTotal',\n 'Manage all users on the platform · {{total}} total',\n { total }\n )\n : tr('admin.users.description', 'Manage all users on the platform')\n }\n >\n <div className=\"space-y-4\">\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder={tr('admin.users.searchPlaceholder', 'Search by email or name...')}\n value={userFilter.search}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ search: e.target.value });\n }}\n className=\"pl-9\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={userFilter.status}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ status: e.target.value as UserFilter['status'] });\n }}\n className=\"h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\"\n >\n <option value=\"all\">{tr('admin.users.filters.status.all', 'All Status')}</option>\n <option value=\"active\">{tr('admin.users.filters.status.active', 'Active')}</option>\n <option value=\"inactive\">\n {tr('admin.users.filters.status.inactive', 'Inactive')}\n </option>\n <option value=\"blocked\">\n {tr('admin.users.filters.status.suspended', 'Suspended')}\n </option>\n </select>\n {(userFilter.search || userFilter.status !== 'all') && (\n <Button\n variant=\"ghost\"\n onClick={() => {\n setPage(1);\n updateUserFilter({ search: '', status: 'all' });\n }}\n >\n {tr('admin.users.filters.clear', 'Clear')}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {tr('admin.users.error.loadFailed', 'Failed to load users')}: {error.message}\n </div>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={users}\n keyExtractor={(u) => u.id}\n onRowClick={(u) => navigate(joinPath(basePath, `users/${u.id}`))}\n sortBy={userFilter.sortBy}\n sortOrder={userFilter.sortOrder}\n onSort={(column, order) =>\n updateUserFilter({\n sortBy: column as UserFilter['sortBy'],\n sortOrder: order,\n })\n }\n loading={loading || isUpdating}\n emptyState={\n <EmptyState\n icon={<UsersIcon className=\"h-8 w-8\" />}\n title={tr('admin.users.empty.title', 'No users found')}\n description={tr(\n 'admin.users.empty.description',\n 'Try adjusting your search or filters'\n )}\n action={\n <Button\n variant=\"outline\"\n onClick={() => updateUserFilter({ search: '', status: 'all' })}\n >\n {tr('admin.users.empty.clearFilters', 'Clear filters')}\n </Button>\n }\n />\n }\n />\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UsersListPage.js","names":[],"sources":["../../../src/admin/pages/UsersListPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Search, Users as UsersIcon } from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminStore } from '../store/adminStore';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n UserStatusBadge,\n EmptyState,\n Avatar,\n Button,\n RowActions,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate, formatRelativeTime, formatNumber } from '../utils/formatters';\nimport { mapUserToUi } from '../utils/mappers';\nimport type { User, UserFilter } from '../types';\nimport { useListUsersQuery, useUpdateUserMutation } from '../../generated/global-operations';\nimport { UserStatus as GqlUserStatus } from '../../generated/global-types';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\nconst PAGE_SIZE = 20;\n\n/**\n * Users list page — connected to real backend with enable/disable actions\n */\nexport function UsersListPage() {\n const { canViewUsers, canUpdateUsers, isLoading: permissionsLoading } = useAdminPermissions();\n\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const { userFilter, updateUserFilter } = useAdminStore();\n const toast = useToast();\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const [page, setPage] = useState(1);\n\n // Build GraphQL filter\n const gqlFilter = useMemo(() => {\n const f: {\n search?: string;\n status?: GqlUserStatus;\n } = {};\n if (userFilter.search) f.search = userFilter.search;\n const statusMap: Record<string, GqlUserStatus> = {\n active: GqlUserStatus.Active,\n inactive: GqlUserStatus.Inactive,\n blocked: GqlUserStatus.Suspended,\n };\n if (userFilter.status !== 'all' && statusMap[userFilter.status]) {\n f.status = statusMap[userFilter.status];\n }\n return f;\n }, [userFilter.search, userFilter.status]);\n\n const skipQueries = permissionsLoading || !canViewUsers;\n\n const { data, loading, error, refetch } = useListUsersQuery({\n variables: {\n filter: gqlFilter,\n pagination: {\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [updateUser, { loading: isUpdating }] = useUpdateUserMutation({\n onCompleted: () => {\n toast.success(\n tr('admin.users.toast.updatedTitle', 'User updated'),\n tr('admin.users.toast.updatedDescription', 'User status has been successfully updated.')\n );\n void refetch();\n },\n onError: (err) => {\n toast.error(tr('admin.users.toast.updateFailedTitle', 'Update failed'), err.message);\n },\n });\n\n const handleToggleStatus = useCallback(\n async (user: User) => {\n const newStatus = user.status === 'active' ? GqlUserStatus.Inactive : GqlUserStatus.Active;\n await updateUser({\n variables: {\n id: user.id,\n input: {\n status: newStatus,\n },\n },\n });\n },\n [updateUser]\n );\n\n const users: User[] = useMemo(() => (data?.users?.users ?? []).map(mapUserToUi), [data]);\n\n const total = data?.users?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const getRowActions = useCallback(\n (user: User) => {\n const actions = [\n {\n key: 'view',\n label: tr('admin.users.actions.viewDetails', 'View Details'),\n onClick: () => navigate(joinPath(basePath, `users/${user.id}`)),\n },\n ];\n\n if (canUpdateUsers) {\n actions.push({\n key: 'toggle-status',\n label:\n user.status === 'active'\n ? tr('admin.users.actions.disableUser', 'Disable User')\n : tr('admin.users.actions.enableUser', 'Enable User'),\n onClick: () => handleToggleStatus(user),\n });\n }\n\n return actions;\n },\n [navigate, basePath, canUpdateUsers, handleToggleStatus, tr]\n );\n\n const columns = [\n {\n key: 'name',\n header: tr('admin.users.table.user', 'User'),\n render: (user: User) => (\n <div className=\"flex items-center gap-3\">\n <Avatar fallback={(user.firstName?.[0] ?? user.email[0]).toUpperCase()} size=\"sm\" />\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-text-primary\">{user.name || user.email}</div>\n <div className=\"truncate text-xs text-text-secondary\">{user.email}</div>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('admin.users.table.status', 'Status'),\n width: '120px',\n render: (user: User) => <UserStatusBadge status={user.status} />,\n },\n {\n key: 'role',\n header: tr('admin.users.table.role', 'Role'),\n width: '120px',\n },\n {\n key: 'tenantId',\n header: tr('admin.users.table.tenant', 'Tenant'),\n width: '150px',\n render: (user: User) => (\n <span className=\"text-xs text-text-secondary\">{user.tenantId || '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: tr('admin.users.table.joined', 'Joined'),\n width: '120px',\n sortable: true,\n render: (user: User) => formatDate(user.createdAt),\n },\n {\n key: 'lastLoginAt',\n header: tr('admin.users.table.lastLogin', 'Last Login'),\n width: '130px',\n render: (user: User) =>\n user.lastLoginAt\n ? formatRelativeTime(user.lastLoginAt)\n : tr('admin.users.table.never', 'Never'),\n },\n {\n key: 'actions',\n header: '',\n width: '50px',\n align: 'right' as const,\n render: (user: User) => (\n <RowActions\n actions={getRowActions(user)}\n ariaLabel={tr('admin.users.table.actionsFor', 'Actions for {{email}}', {\n email: user.email,\n })}\n />\n ),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewUsers}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.users.title', 'Users')}\n denyMessage={tr('admin.users.denyMessage', \"You don't have permission to view users.\")}\n >\n <PageLayout\n title={tr('admin.users.title', 'Users')}\n gradientWord={tr('admin.users.title', 'Users')}\n description={\n total\n ? tr(\n 'admin.users.descriptionWithTotal',\n 'Manage all users on the platform · {{total}} total',\n { total }\n )\n : tr('admin.users.description', 'Manage all users on the platform')\n }\n >\n <div className=\"space-y-4\">\n <PagePurpose>\n {tr(\n 'admin.users.purpose',\n 'Every person with an account across all tenants lives here. Use this page to find someone by name or email, check when they last signed in, and enable or disable their access when they join, leave, or need to be locked out.'\n )}\n </PagePurpose>\n\n {/* Emphasis: total user count — the headline stat for this page. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <UsersIcon className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">\n {formatNumber(total)}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr('admin.users.emphasis.label', 'users across all tenants')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder={tr('admin.users.searchPlaceholder', 'Search by email or name...')}\n value={userFilter.search}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ search: e.target.value });\n }}\n className=\"pl-9\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={userFilter.status}\n onChange={(e) => {\n setPage(1);\n updateUserFilter({ status: e.target.value as UserFilter['status'] });\n }}\n className=\"h-10 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\"\n >\n <option value=\"all\">{tr('admin.users.filters.status.all', 'All Status')}</option>\n <option value=\"active\">{tr('admin.users.filters.status.active', 'Active')}</option>\n <option value=\"inactive\">\n {tr('admin.users.filters.status.inactive', 'Inactive')}\n </option>\n <option value=\"blocked\">\n {tr('admin.users.filters.status.suspended', 'Suspended')}\n </option>\n </select>\n {(userFilter.search || userFilter.status !== 'all') && (\n <Button\n variant=\"ghost\"\n onClick={() => {\n setPage(1);\n updateUserFilter({ search: '', status: 'all' });\n }}\n >\n {tr('admin.users.filters.clear', 'Clear')}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {tr('admin.users.error.loadFailed', 'Failed to load users')}: {error.message}\n </div>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={users}\n keyExtractor={(u) => u.id}\n onRowClick={(u) => navigate(joinPath(basePath, `users/${u.id}`))}\n sortBy={userFilter.sortBy}\n sortOrder={userFilter.sortOrder}\n onSort={(column, order) =>\n updateUserFilter({\n sortBy: column as UserFilter['sortBy'],\n sortOrder: order,\n })\n }\n loading={loading || isUpdating}\n emptyState={\n <EmptyState\n illustration=\"empty-people\"\n icon={<UsersIcon className=\"h-8 w-8\" />}\n title={tr('admin.users.empty.title', 'No users found')}\n description={tr(\n 'admin.users.empty.description',\n 'Try adjusting your search or filters'\n )}\n action={\n <Button\n variant=\"outline\"\n onClick={() => updateUserFilter({ search: '', status: 'all' })}\n >\n {tr('admin.users.empty.clearFilters', 'Clear filters')}\n </Button>\n }\n />\n }\n />\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|