@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.1 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,15 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState, useMemo } from "react";
2
+ import { useId, useEffect, useState, useRef, useMemo } from "react";
3
3
  import { useQuery, useQueryClient, useMutation, QueryClient, QueryClientProvider } from "react-query";
4
- import { Modal, Typography, Button, Portal, Flex, IconButton, Box, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Alert, Tabs, Grid, SearchForm, Searchbar, Loader, Badge, Pagination } from "@strapi/design-system";
5
- import styled, { keyframes } from "styled-components";
4
+ import { NavLink, Outlet, Routes, Route, Navigate, useOutletContext } from "react-router-dom";
5
+ import { Typography, ScrollArea, Box, Flex, Badge, SubNav as SubNav$1, Divider, Loader, Alert, Modal, Button, Portal, IconButton, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Tabs, Grid, SearchForm, Searchbar } from "@strapi/design-system";
6
+ import { useRBAC, Page, Layouts, BackButton, useNotification, useFetchClient } from "@strapi/strapi/admin";
6
7
  import { dashClient } from "@better-auth/infra/client";
7
8
  import { createAuthClient, InferPlugin } from "better-auth/client";
8
- import { Cross, Images, Plus, Trash, Pencil } from "@strapi/icons";
9
- import { useNotification, useFetchClient } from "@strapi/strapi/admin";
10
- import { g as getMediaLibraryComponent } from "./index-Cvcysa5M.mjs";
9
+ import { useIntl } from "react-intl";
10
+ import { P as PLUGIN_ID, a as PERMISSIONS, g as getMediaLibraryComponent, b as getEditViewSidePanels } from "./index-X2-Fh6sz.mjs";
11
+ import { Cross, Images, Trash, Plus, Pencil } from "@strapi/icons";
12
+ import styled, { keyframes } from "styled-components";
11
13
  const dashPathMethods = () => ({
12
14
  id: "dash-path-methods",
13
15
  pathMethods: {
@@ -99,6 +101,315 @@ const client = createAuthClient({
99
101
  headers: getAuthHeaders()
100
102
  }
101
103
  });
104
+ const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
105
+ const HEIGHT_TOP_NAVIGATION = "6.4rem";
106
+ const HEIGHT_TOP_NAVIGATION_MEDIUM = "5.6rem";
107
+ const WIDTH_SIDE_NAVIGATION = "23.2rem";
108
+ const MainSubNav = styled(SubNav$1)`
109
+ width: 100%;
110
+ height: 100%;
111
+ overflow: hidden;
112
+ background-color: ${({ theme }) => theme.colors.neutral0};
113
+ display: flex;
114
+ flex-direction: column;
115
+ border-right: 0;
116
+ box-shadow: none;
117
+ position: relative;
118
+
119
+ ${({ theme }) => theme.breakpoints.medium} {
120
+ width: ${WIDTH_SIDE_NAVIGATION};
121
+ position: sticky;
122
+ top: 0;
123
+ border-right: 1px solid ${({ theme }) => theme.colors.neutral150};
124
+ overscroll-behavior: contain;
125
+ }
126
+ `;
127
+ const Main = ({ children, ...props }) => /* @__PURE__ */ jsx(MainSubNav, { ...props, children });
128
+ const StyledLink = styled(NavLink)`
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: space-between;
132
+ text-decoration: none;
133
+ height: 32px;
134
+ color: ${({ theme }) => theme.colors.neutral800};
135
+
136
+ &.active > div {
137
+ background-color: ${({ theme }) => theme.colors.primary100};
138
+ color: ${({ theme }) => theme.colors.primary700};
139
+ font-weight: 500;
140
+ }
141
+
142
+ &:hover.active > div {
143
+ background-color: ${({ theme }) => theme.colors.primary100};
144
+ color: ${({ theme }) => theme.colors.primary700};
145
+ font-weight: 500;
146
+ }
147
+
148
+ &:hover > div {
149
+ background-color: ${({ theme }) => theme.colors.neutral100};
150
+ }
151
+
152
+ &:focus-visible {
153
+ outline-offset: -2px;
154
+ }
155
+ `;
156
+ const Link = (props) => {
157
+ const { to, label, endAction, handleClick, ...rest } = props;
158
+ return /* @__PURE__ */ jsx(StyledLink, { to, ...rest, onClick: handleClick, children: /* @__PURE__ */ jsx(
159
+ Box,
160
+ {
161
+ width: "100%",
162
+ paddingLeft: 3,
163
+ paddingRight: 3,
164
+ paddingTop: { initial: 1, large: 0 },
165
+ paddingBottom: { initial: 1, large: 0 },
166
+ borderRadius: 1,
167
+ children: /* @__PURE__ */ jsxs(
168
+ Flex,
169
+ {
170
+ justifyContent: "space-between",
171
+ width: "100%",
172
+ gap: { initial: 2, large: 1 },
173
+ children: [
174
+ /* @__PURE__ */ jsx(
175
+ Typography,
176
+ {
177
+ tag: "div",
178
+ lineHeight: "32px",
179
+ width: { initial: "80dvw", medium: "100%" },
180
+ overflow: "hidden",
181
+ style: { textOverflow: "ellipsis", whiteSpace: "nowrap" },
182
+ children: label
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsx(Flex, { gap: 2, children: endAction })
186
+ ]
187
+ }
188
+ )
189
+ }
190
+ ) });
191
+ };
192
+ const StyledHeader = styled(Flex)`
193
+ flex: 0 0 ${HEIGHT_TOP_NAVIGATION};
194
+ height: ${HEIGHT_TOP_NAVIGATION};
195
+
196
+ ${({ theme }) => theme.breakpoints.medium} {
197
+ flex: 0 0 ${HEIGHT_TOP_NAVIGATION_MEDIUM};
198
+ height: ${HEIGHT_TOP_NAVIGATION_MEDIUM};
199
+ }
200
+ `;
201
+ const Header = ({ label }) => {
202
+ return /* @__PURE__ */ jsx(
203
+ StyledHeader,
204
+ {
205
+ justifyContent: "space-between",
206
+ paddingLeft: 5,
207
+ paddingRight: 5,
208
+ children: /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", children: label })
209
+ }
210
+ );
211
+ };
212
+ const Sections = ({
213
+ children,
214
+ ...props
215
+ }) => {
216
+ return /* @__PURE__ */ jsx(
217
+ Box,
218
+ {
219
+ paddingTop: { initial: 5, large: 4 },
220
+ paddingBottom: { initial: 5, large: 4 },
221
+ paddingLeft: { initial: 3, large: 0 },
222
+ paddingRight: { initial: 3, large: 0 },
223
+ maxWidth: { initial: "100%", medium: WIDTH_SIDE_NAVIGATION },
224
+ children: /* @__PURE__ */ jsx(Flex, { tag: "ul", gap: 6, direction: "column", alignItems: "stretch", ...props, children: children.map((child, index) => {
225
+ return /* @__PURE__ */ jsx("li", { children: child }, index);
226
+ }) })
227
+ }
228
+ );
229
+ };
230
+ const Section = ({
231
+ label,
232
+ children,
233
+ badgeLabel
234
+ }) => {
235
+ const listId = useId();
236
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
237
+ /* @__PURE__ */ jsx(
238
+ Box,
239
+ {
240
+ paddingLeft: {
241
+ initial: 3,
242
+ large: 5
243
+ },
244
+ paddingRight: {
245
+ initial: 3,
246
+ large: 5
247
+ },
248
+ children: /* @__PURE__ */ jsxs(Flex, { position: "relative", justifyContent: "space-between", gap: 2, children: [
249
+ /* @__PURE__ */ jsx(Flex, { children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: label }) }) }),
250
+ /* @__PURE__ */ jsx(Flex, { gap: 1, children: badgeLabel && /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral600", children: badgeLabel }) })
251
+ ] })
252
+ }
253
+ ),
254
+ /* @__PURE__ */ jsx(
255
+ Flex,
256
+ {
257
+ tag: "ol",
258
+ id: listId,
259
+ direction: "column",
260
+ gap: { initial: 2, large: "2px" },
261
+ alignItems: "stretch",
262
+ marginLeft: {
263
+ initial: 0,
264
+ large: 2
265
+ },
266
+ marginRight: {
267
+ initial: 0,
268
+ large: 2
269
+ },
270
+ children: children.map((child, index) => {
271
+ return /* @__PURE__ */ jsx("li", { children: child }, index);
272
+ })
273
+ }
274
+ )
275
+ ] });
276
+ };
277
+ styled.button`
278
+ cursor: pointer;
279
+ width: 100%;
280
+ border: none;
281
+ padding: 0;
282
+ background: transparent;
283
+ display: flex;
284
+ align-items: center;
285
+ border-radius: ${({ theme }) => theme.borderRadius};
286
+ padding-left: ${({ theme }) => theme.spaces[3]};
287
+ padding-right: ${({ theme }) => theme.spaces[3]};
288
+ padding-top: ${({ theme }) => theme.spaces[2]};
289
+ padding-bottom: ${({ theme }) => theme.spaces[2]};
290
+
291
+ &:hover {
292
+ background-color: ${({ theme }) => theme.colors.neutral100};
293
+ }
294
+ `;
295
+ styled.li`
296
+ ${StyledLink} > div {
297
+ padding-left: 36px;
298
+ }
299
+ `;
300
+ styled(Box)`
301
+ width: 100%;
302
+
303
+ ${MainSubNav} {
304
+ background-color: transparent;
305
+ border-right: none;
306
+ }
307
+
308
+ ${({ theme }) => theme.breakpoints.medium} {
309
+ overflow: hidden;
310
+
311
+ ${MainSubNav} {
312
+ top: 0;
313
+ }
314
+ }
315
+ `;
316
+ const Content = ({ children }) => {
317
+ return /* @__PURE__ */ jsx(ScrollArea, { children });
318
+ };
319
+ const SubNav = {
320
+ Main,
321
+ Content,
322
+ Header,
323
+ Link,
324
+ Sections,
325
+ Section
326
+ };
327
+ const sections = [
328
+ {
329
+ id: "api-roles",
330
+ intlLabel: {
331
+ id: getTranslation("settings.general"),
332
+ defaultMessage: "General"
333
+ },
334
+ links: [
335
+ {
336
+ id: "overview",
337
+ intlLabel: {
338
+ id: getTranslation("settings.overview"),
339
+ defaultMessage: "Overview"
340
+ },
341
+ to: "/plugins/better-auth-dashboard/overview",
342
+ testid: "nav-overview"
343
+ },
344
+ {
345
+ id: "users",
346
+ intlLabel: {
347
+ id: getTranslation("settings.users"),
348
+ defaultMessage: "User Management"
349
+ },
350
+ to: "/plugins/better-auth-dashboard/users",
351
+ testid: "nav-users"
352
+ },
353
+ {
354
+ id: "organizations",
355
+ intlLabel: {
356
+ id: getTranslation("settings.organizations"),
357
+ defaultMessage: "Organization Management"
358
+ },
359
+ to: "/plugins/better-auth-dashboard/organizations",
360
+ testid: "nav-organizations"
361
+ }
362
+ ]
363
+ }
364
+ ];
365
+ const Nav = ({ isFullPage = false, orgEnabled = false }) => {
366
+ const { formatMessage } = useIntl();
367
+ const overviewRBAC = useRBAC(PERMISSIONS.overview);
368
+ const userRBAC = useRBAC(PERMISSIONS.user);
369
+ const organizationRBAC = useRBAC(PERMISSIONS.organization);
370
+ return /* @__PURE__ */ jsxs(SubNav.Main, { children: [
371
+ !isFullPage && /* @__PURE__ */ jsxs(Fragment, { children: [
372
+ /* @__PURE__ */ jsx(SubNav.Header, { label: "Authentication" }),
373
+ /* @__PURE__ */ jsx(Divider, {})
374
+ ] }),
375
+ /* @__PURE__ */ jsxs(SubNav.Content, { children: [
376
+ isFullPage && /* @__PURE__ */ jsxs(Fragment, { children: [
377
+ /* @__PURE__ */ jsx(SubNav.Header, { label: "Authentication" }),
378
+ /* @__PURE__ */ jsx(Divider, {})
379
+ ] }),
380
+ /* @__PURE__ */ jsx(SubNav.Sections, { "data-testid": "main-nav", children: sections.map((section) => /* @__PURE__ */ jsx(
381
+ SubNav.Section,
382
+ {
383
+ label: formatMessage(section.intlLabel),
384
+ children: section.links.filter((link) => orgEnabled || link.id !== "organizations").filter((link) => {
385
+ if (link.id === "overview") {
386
+ return overviewRBAC.allowedActions.canRead;
387
+ }
388
+ if (link.id === "users") {
389
+ return userRBAC.allowedActions.canRead;
390
+ }
391
+ if (link.id === "organizations") {
392
+ return organizationRBAC.allowedActions.canRead;
393
+ }
394
+ return true;
395
+ }).map((link) => {
396
+ return /* @__PURE__ */ jsx(
397
+ SubNav.Link,
398
+ {
399
+ to: link.to,
400
+ label: formatMessage(link.intlLabel),
401
+ "data-testid": link.testid,
402
+ children: formatMessage(link.intlLabel)
403
+ },
404
+ link.id
405
+ );
406
+ })
407
+ },
408
+ section.id
409
+ )) })
410
+ ] })
411
+ ] });
412
+ };
102
413
  function useDashConfig() {
103
414
  return useQuery({
104
415
  queryKey: ["dash-config"],
@@ -115,6 +426,74 @@ function useDashConfig() {
115
426
  function hasPlugin(config, id) {
116
427
  return config?.plugins.some((p) => p.id === id) ?? false;
117
428
  }
429
+ const RESPONSIVE_DEFAULT_SPACING = {
430
+ initial: 4,
431
+ medium: 6,
432
+ large: 10
433
+ };
434
+ const App = () => {
435
+ const { data: config, isLoading, isError, error } = useDashConfig();
436
+ const orgEnabled = hasPlugin(config, "organization");
437
+ const orgOptionsQuery = useQuery({
438
+ queryKey: ["dash-org-options"],
439
+ queryFn: async () => {
440
+ const result = await client.dash.organization.options();
441
+ if (result.error) return { teamsEnabled: false };
442
+ return result.data ?? { teamsEnabled: false };
443
+ },
444
+ enabled: orgEnabled
445
+ });
446
+ const teamsEnabled = orgEnabled ? orgOptionsQuery.data?.teamsEnabled ?? false : false;
447
+ if (isLoading) {
448
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading Better Auth dashboard…" }) });
449
+ }
450
+ if (isError) {
451
+ return /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(
452
+ Alert,
453
+ {
454
+ closeLabel: "Close",
455
+ title: "Error loading configuration",
456
+ variant: "danger",
457
+ children: error?.message
458
+ }
459
+ ) });
460
+ }
461
+ if (!config) return null;
462
+ return /* @__PURE__ */ jsx(
463
+ "div",
464
+ {
465
+ "data-testid": "dashboard-root",
466
+ style: { display: "flex", flex: "1 1 auto", minHeight: "100vh" },
467
+ children: /* @__PURE__ */ jsxs(Layouts.Root, { sideNav: /* @__PURE__ */ jsx(Nav, { orgEnabled }), children: [
468
+ /* @__PURE__ */ jsx(Page.Title, { children: "Authentication" }),
469
+ /* @__PURE__ */ jsx(
470
+ Box,
471
+ {
472
+ display: { initial: "block", medium: "none" },
473
+ paddingLeft: RESPONSIVE_DEFAULT_SPACING,
474
+ paddingRight: RESPONSIVE_DEFAULT_SPACING,
475
+ paddingTop: RESPONSIVE_DEFAULT_SPACING,
476
+ children: /* @__PURE__ */ jsx(BackButton, { fallback: "/plugins/better-auth-dashboard" })
477
+ }
478
+ ),
479
+ /* @__PURE__ */ jsx(Outlet, { context: { config, teamsEnabled } })
480
+ ] })
481
+ }
482
+ );
483
+ };
484
+ const ProtectedApp = () => {
485
+ return /* @__PURE__ */ jsx(
486
+ Page.Protect,
487
+ {
488
+ permissions: [
489
+ ...PERMISSIONS.overview,
490
+ ...PERMISSIONS.user,
491
+ ...PERMISSIONS.organization
492
+ ],
493
+ children: /* @__PURE__ */ jsx(App, {})
494
+ }
495
+ );
496
+ };
118
497
  function ConfirmDialog({
119
498
  title,
120
499
  message,
@@ -141,8 +520,9 @@ function ConfirmDialog({
141
520
  );
142
521
  }
143
522
  const DASH_CONTEXT_HEADER = "X-Dash-Context";
144
- const withContext = (ctx) => ({
523
+ const withContext = (ctx, headers) => ({
145
524
  headers: {
525
+ ...headers,
146
526
  [DASH_CONTEXT_HEADER]: btoa(JSON.stringify(ctx))
147
527
  }
148
528
  });
@@ -450,11 +830,22 @@ function UserCombobox({
450
830
  queryKey: ["user-combobox-search", search],
451
831
  queryFn: async () => {
452
832
  const whereClause = search ? JSON.stringify([
453
- { field: "email", operator: "contains", value: search }
833
+ {
834
+ field: "email",
835
+ operator: "contains",
836
+ value: search,
837
+ connector: "OR"
838
+ },
839
+ {
840
+ field: "name",
841
+ operator: "contains",
842
+ value: search,
843
+ connector: "OR"
844
+ }
454
845
  ]) : void 0;
455
846
  const result = await client.dash.listUsers({
456
847
  query: {
457
- limit: 20,
848
+ limit: search ? 100 : 20,
458
849
  offset: 0,
459
850
  sortBy: "createdAt",
460
851
  sortOrder: "desc",
@@ -506,7 +897,7 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
506
897
  ...logo ? { logo } : {},
507
898
  ...teamsEnabled && !skipDefaultTeam && defaultTeamName ? { defaultTeamName } : {}
508
899
  },
509
- withContext({ userId: ownerId, skipDefaultTeam })
900
+ withContext({ userId: ownerId, skipDefaultTeam }, getAuthHeaders())
510
901
  );
511
902
  if (result.error)
512
903
  throw new Error(result.error.message ?? "Create failed");
@@ -542,6 +933,7 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
542
933
  loading: createMutation.isLoading,
543
934
  disabled: !name || !slug || !ownerId,
544
935
  onClick: () => createMutation.mutate(),
936
+ "data-testid": "create-org-submit",
545
937
  children: "Create organization"
546
938
  }
547
939
  )
@@ -629,6 +1021,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
629
1021
  function slugify(str) {
630
1022
  return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
631
1023
  }
1024
+ const EntryRow = styled.div`
1025
+ display: flex;
1026
+ align-items: center;
1027
+ justify-content: space-between;
1028
+ gap: 8px;
1029
+ padding: 8px 12px;
1030
+ width: 100%;
1031
+ box-sizing: border-box;
1032
+ background: #ffffff;
1033
+ border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
1034
+ border-radius: 4px;
1035
+ min-width: 0;
1036
+ `;
1037
+ function getDisplayLabel(doc) {
1038
+ for (const key of ["name", "title", "email", "username", "label", "slug"]) {
1039
+ const v = doc[key];
1040
+ if (typeof v === "string" && v.trim()) return v;
1041
+ }
1042
+ return String(doc.documentId ?? doc.id ?? "");
1043
+ }
1044
+ function normalizeValue(val, cache) {
1045
+ if (!val) return [];
1046
+ if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
1047
+ const items2 = val.set ?? [];
1048
+ return items2.filter((item) => item?.documentId).map((item) => ({
1049
+ documentId: item.documentId,
1050
+ label: cache.get(item.documentId) ?? item.documentId
1051
+ }));
1052
+ }
1053
+ const items = Array.isArray(val) ? val : [val];
1054
+ return items.filter(
1055
+ (item) => typeof item === "object" && item !== null && "documentId" in item
1056
+ ).map((item) => ({
1057
+ documentId: String(item.documentId),
1058
+ label: getDisplayLabel(item)
1059
+ }));
1060
+ }
1061
+ function RelationField({
1062
+ name,
1063
+ label,
1064
+ attribute,
1065
+ value,
1066
+ onChange,
1067
+ readOnly = false
1068
+ }) {
1069
+ const { get } = useFetchClient();
1070
+ const cache = useRef(/* @__PURE__ */ new Map());
1071
+ const [search, setSearch] = useState("");
1072
+ const [addKey, setAddKey] = useState(0);
1073
+ const target = attribute.target ?? "";
1074
+ const relationType = attribute.relation ?? "manyToOne";
1075
+ const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
1076
+ const currentDocs = normalizeValue(value, cache.current);
1077
+ const selectedIds = new Set(currentDocs.map((d) => d.documentId));
1078
+ const { data: searchResults = [], isLoading } = useQuery({
1079
+ queryKey: ["relation-search", target, search],
1080
+ queryFn: async () => {
1081
+ const params = new URLSearchParams({
1082
+ uid: target,
1083
+ "pagination[pageSize]": "50"
1084
+ });
1085
+ if (search) params.set("filters[name][$containsi]", search);
1086
+ const { data } = await get(
1087
+ `/better-auth-dashboard/db?${params}`
1088
+ );
1089
+ const docs = data.results ?? [];
1090
+ for (const doc of docs) {
1091
+ if (doc.documentId) {
1092
+ cache.current.set(String(doc.documentId), getDisplayLabel(doc));
1093
+ }
1094
+ }
1095
+ return docs.map((doc) => ({
1096
+ documentId: String(doc.documentId ?? ""),
1097
+ label: getDisplayLabel(doc)
1098
+ })).filter((d) => d.documentId);
1099
+ },
1100
+ keepPreviousData: true,
1101
+ enabled: !readOnly
1102
+ });
1103
+ const availableOptions = searchResults.filter(
1104
+ (r) => !selectedIds.has(r.documentId)
1105
+ );
1106
+ const commit = (ids) => {
1107
+ onChange(name, { set: ids.map((id) => ({ documentId: id })) });
1108
+ };
1109
+ const handleSelect = (val) => {
1110
+ if (!val) return;
1111
+ const opt = searchResults.find((r) => r.documentId === val);
1112
+ if (opt) cache.current.set(val, opt.label);
1113
+ if (isMulti) {
1114
+ if (selectedIds.has(val)) return;
1115
+ commit([...currentDocs.map((d) => d.documentId), val]);
1116
+ } else {
1117
+ commit([val]);
1118
+ }
1119
+ setSearch("");
1120
+ setAddKey((k) => k + 1);
1121
+ };
1122
+ const handleRemove = (docId) => {
1123
+ commit(
1124
+ currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
1125
+ );
1126
+ };
1127
+ return /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
1128
+ /* @__PURE__ */ jsx(Field.Label, { children: label }),
1129
+ !readOnly && /* @__PURE__ */ jsx(
1130
+ Combobox,
1131
+ {
1132
+ value: "",
1133
+ onChange: (val) => handleSelect(val),
1134
+ onInputChange: (e) => setSearch(e.target.value),
1135
+ loading: isLoading,
1136
+ placeholder: "Search…",
1137
+ children: availableOptions.map((opt) => /* @__PURE__ */ jsx(ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
1138
+ },
1139
+ addKey
1140
+ ),
1141
+ currentDocs.length > 0 && /* @__PURE__ */ jsx(
1142
+ Flex,
1143
+ {
1144
+ direction: "column",
1145
+ gap: 1,
1146
+ style: { marginTop: 8, width: "100%" },
1147
+ children: currentDocs.map((doc) => /* @__PURE__ */ jsxs(EntryRow, { children: [
1148
+ /* @__PURE__ */ jsx(
1149
+ Typography,
1150
+ {
1151
+ variant: "omega",
1152
+ textColor: "neutral800",
1153
+ style: {
1154
+ overflow: "hidden",
1155
+ textOverflow: "ellipsis",
1156
+ whiteSpace: "nowrap",
1157
+ flex: 1,
1158
+ minWidth: 0
1159
+ },
1160
+ children: doc.label
1161
+ }
1162
+ ),
1163
+ !readOnly && /* @__PURE__ */ jsx(
1164
+ IconButton,
1165
+ {
1166
+ label: "Remove",
1167
+ size: "S",
1168
+ onClick: () => handleRemove(doc.documentId),
1169
+ children: /* @__PURE__ */ jsx(Trash, {})
1170
+ }
1171
+ )
1172
+ ] }, doc.documentId))
1173
+ }
1174
+ )
1175
+ ] });
1176
+ }
632
1177
  function makeLabel(name) {
633
1178
  return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
634
1179
  }
@@ -752,24 +1297,15 @@ function DynamicField({
752
1297
  ] });
753
1298
  }
754
1299
  if (type === "relation") {
755
- return /* @__PURE__ */ jsxs(
756
- Field.Root,
1300
+ return /* @__PURE__ */ jsx(
1301
+ RelationField,
757
1302
  {
758
- style: fieldStyle,
759
- hint: `Relation → ${attribute.target ?? "unknown"}`,
760
- children: [
761
- /* @__PURE__ */ jsx(Field.Label, { children: label }),
762
- /* @__PURE__ */ jsx(
763
- TextInput,
764
- {
765
- value: value != null ? String(value) : "",
766
- onChange: (e) => onChange(name, e.target.value),
767
- disabled: readOnly,
768
- placeholder: "ID…"
769
- }
770
- ),
771
- /* @__PURE__ */ jsx(Field.Hint, {})
772
- ]
1303
+ name,
1304
+ label,
1305
+ attribute,
1306
+ value,
1307
+ onChange,
1308
+ readOnly
773
1309
  }
774
1310
  );
775
1311
  }
@@ -809,6 +1345,21 @@ function CustomFieldsSection({
809
1345
  )) })
810
1346
  ] });
811
1347
  }
1348
+ function EditViewSidePanels({ model, documentId, document: document2 }) {
1349
+ const panels = getEditViewSidePanels(model);
1350
+ if (panels.length === 0) return null;
1351
+ return /* @__PURE__ */ jsx(Fragment, { children: panels.map(({ id, title, Component }) => /* @__PURE__ */ jsxs(FormSection, { children: [
1352
+ /* @__PURE__ */ jsx(SectionLabel, { children: title }),
1353
+ /* @__PURE__ */ jsx(
1354
+ Component,
1355
+ {
1356
+ model,
1357
+ documentId,
1358
+ document: document2
1359
+ }
1360
+ )
1361
+ ] }, id)) });
1362
+ }
812
1363
  const SYSTEM_FIELDS = /* @__PURE__ */ new Set([
813
1364
  "id",
814
1365
  "documentId",
@@ -840,6 +1391,8 @@ function useModelSchema(model) {
840
1391
  if (SYSTEM_FIELDS.has(name)) continue;
841
1392
  if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
842
1393
  continue;
1394
+ const baOptions = attr.pluginOptions?.["better-auth"];
1395
+ if (baOptions?.managed === true) continue;
843
1396
  attributes[name] = attr;
844
1397
  }
845
1398
  return attributes;
@@ -892,12 +1445,13 @@ function OrganizationDetail({
892
1445
  const schemaQuery = useModelSchema("organization");
893
1446
  const qc = useQueryClient();
894
1447
  const { toggleNotification } = useNotification();
1448
+ const { get, put } = useFetchClient();
895
1449
  const orgQuery = useQuery({
896
1450
  queryKey: ["dash-org", organizationId],
897
1451
  queryFn: async () => {
898
1452
  const result = await client.dash.organization[organizationId](
899
1453
  {},
900
- withContext({ organizationId })
1454
+ withContext({ organizationId }, getAuthHeaders())
901
1455
  );
902
1456
  if (result.error)
903
1457
  throw new Error(result.error.message ?? "Failed to load org");
@@ -907,7 +1461,7 @@ function OrganizationDetail({
907
1461
  const membersQuery = useQuery({
908
1462
  queryKey: ["dash-org-members", organizationId],
909
1463
  queryFn: async () => {
910
- const result = await client.dash.organization[organizationId].members({}, withContext({ organizationId }));
1464
+ const result = await client.dash.organization[organizationId].members({}, withContext({ organizationId }, getAuthHeaders()));
911
1465
  if (result.error) throw new Error(result.error.message ?? "Failed");
912
1466
  return result.data ?? [];
913
1467
  }
@@ -916,7 +1470,7 @@ function OrganizationDetail({
916
1470
  queryKey: ["dash-org-teams", organizationId],
917
1471
  queryFn: async () => {
918
1472
  if (!teamsEnabled) return [];
919
- const result = await client.dash.organization[organizationId].teams({}, withContext({ organizationId }));
1473
+ const result = await client.dash.organization[organizationId].teams({}, withContext({ organizationId }, getAuthHeaders()));
920
1474
  if (result.error) throw new Error(result.error.message ?? "Failed");
921
1475
  return result.data ?? [];
922
1476
  },
@@ -925,11 +1479,29 @@ function OrganizationDetail({
925
1479
  const ssoQuery = useQuery({
926
1480
  queryKey: ["dash-org-sso", organizationId],
927
1481
  queryFn: async () => {
928
- const result = await client.dash.organization[organizationId].ssoProviders({}, withContext({ organizationId }));
1482
+ const result = await client.dash.organization[organizationId].ssoProviders({}, withContext({ organizationId }, getAuthHeaders()));
1483
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1484
+ return result.data ?? [];
1485
+ }
1486
+ });
1487
+ const invitationsQuery = useQuery({
1488
+ queryKey: ["dash-org-invitations", organizationId],
1489
+ queryFn: async () => {
1490
+ const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }, getAuthHeaders()));
929
1491
  if (result.error) throw new Error(result.error.message ?? "Failed");
930
1492
  return result.data ?? [];
931
1493
  }
932
1494
  });
1495
+ const strapiOrgQuery = useQuery({
1496
+ queryKey: ["dash-strapi-org", organizationId],
1497
+ enabled: !!orgQuery.data,
1498
+ queryFn: async () => {
1499
+ const { data } = await get(
1500
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
1501
+ );
1502
+ return data.results?.[0] ?? null;
1503
+ }
1504
+ });
933
1505
  const [activeTab, setActiveTab] = useState("details");
934
1506
  const [editName, setEditName] = useState(void 0);
935
1507
  const [editSlug, setEditSlug] = useState(void 0);
@@ -942,12 +1514,15 @@ function OrganizationDetail({
942
1514
  const [confirmDeleteSsoId, setConfirmDeleteSsoId] = useState(
943
1515
  null
944
1516
  );
1517
+ const [inviteEmail, setInviteEmail] = useState("");
1518
+ const [inviteRole, setInviteRole] = useState("member");
1519
+ const [confirmCancelInvitationId, setConfirmCancelInvitationId] = useState(null);
945
1520
  const handleExtraChange = (name, value) => {
946
1521
  setEditExtra((prev) => ({ ...prev, [name]: value }));
947
1522
  };
948
1523
  const org = orgQuery.data;
949
1524
  const extraData = {
950
- ...org,
1525
+ ...strapiOrgQuery.data ?? {},
951
1526
  ...editExtra
952
1527
  };
953
1528
  const updateOrgMutation = useMutation({
@@ -956,17 +1531,21 @@ function OrganizationDetail({
956
1531
  if (editName !== void 0) body.name = editName;
957
1532
  if (editSlug !== void 0) body.slug = editSlug;
958
1533
  if (editLogo !== void 0) body.logo = editLogo;
959
- const result = await client.dash.organization.update(
960
- body,
961
- withContext({ organizationId })
1534
+ const documentId = strapiOrgQuery.data?.documentId;
1535
+ if (!documentId)
1536
+ throw new Error("Could not resolve documentId for organization");
1537
+ await put(
1538
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
1539
+ body
962
1540
  );
963
- if (result.error)
964
- throw new Error(result.error.message ?? "Update failed");
965
- return result.data;
966
1541
  },
967
- onSuccess: () => {
1542
+ onSuccess: async () => {
1543
+ await qc.invalidateQueries({
1544
+ queryKey: ["dash-strapi-org", organizationId]
1545
+ });
968
1546
  qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
969
1547
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
1548
+ qc.invalidateQueries({ queryKey: ["dash-strapi-org", organizationId] });
970
1549
  setEditName(void 0);
971
1550
  setEditSlug(void 0);
972
1551
  setEditLogo(void 0);
@@ -990,7 +1569,7 @@ function OrganizationDetail({
990
1569
  mutationFn: async () => {
991
1570
  const result = await client.dash.organization.addMember(
992
1571
  { userId: addUserId, role: addRole },
993
- withContext({ organizationId })
1572
+ withContext({ organizationId }, getAuthHeaders())
994
1573
  );
995
1574
  if (result.error)
996
1575
  throw new Error(result.error.message ?? "Add member failed");
@@ -1013,7 +1592,7 @@ function OrganizationDetail({
1013
1592
  mutationFn: async (memberId) => {
1014
1593
  const result = await client.dash.organization.removeMember(
1015
1594
  { memberId },
1016
- withContext({ organizationId })
1595
+ withContext({ organizationId }, getAuthHeaders())
1017
1596
  );
1018
1597
  if (result.error) throw new Error(result.error.message ?? "Failed");
1019
1598
  },
@@ -1036,7 +1615,7 @@ function OrganizationDetail({
1036
1615
  }) => {
1037
1616
  const result = await client.dash.organization.updateMemberRole(
1038
1617
  { memberId, role },
1039
- withContext({ organizationId })
1618
+ withContext({ organizationId }, getAuthHeaders())
1040
1619
  );
1041
1620
  if (result.error) throw new Error(result.error.message ?? "Failed");
1042
1621
  return result.data;
@@ -1057,7 +1636,7 @@ function OrganizationDetail({
1057
1636
  mutationFn: async () => {
1058
1637
  const result = await client.dash.organization.createTeam(
1059
1638
  { name: newTeamName },
1060
- withContext({ organizationId })
1639
+ withContext({ organizationId }, getAuthHeaders())
1061
1640
  );
1062
1641
  if (result.error)
1063
1642
  throw new Error(result.error.message ?? "Create team failed");
@@ -1079,7 +1658,7 @@ function OrganizationDetail({
1079
1658
  mutationFn: async (teamId) => {
1080
1659
  const result = await client.dash.organization.deleteTeam(
1081
1660
  { teamId },
1082
- withContext({ organizationId })
1661
+ withContext({ organizationId }, getAuthHeaders())
1083
1662
  );
1084
1663
  if (result.error) throw new Error(result.error.message ?? "Failed");
1085
1664
  },
@@ -1097,7 +1676,10 @@ function OrganizationDetail({
1097
1676
  });
1098
1677
  const deleteSsoMutation = useMutation({
1099
1678
  mutationFn: async (providerId) => {
1100
- const result = await client.dash.organization[organizationId].ssoProvider.delete({ providerId }, withContext({ organizationId }));
1679
+ const result = await client.dash.organization[organizationId].ssoProvider.delete(
1680
+ { providerId },
1681
+ withContext({ organizationId }, getAuthHeaders())
1682
+ );
1101
1683
  if (result.error) throw new Error(result.error.message ?? "Failed");
1102
1684
  },
1103
1685
  onSuccess: () => {
@@ -1112,9 +1694,74 @@ function OrganizationDetail({
1112
1694
  });
1113
1695
  }
1114
1696
  });
1115
- const members = membersQuery.data ?? [];
1116
- const teams = teamsQuery.data ?? [];
1697
+ const inviteMemberMutation = useMutation({
1698
+ mutationFn: async () => {
1699
+ const result = await client.dash.organization.inviteMember(
1700
+ { email: inviteEmail, role: inviteRole, invitedBy: "" },
1701
+ withContext({ organizationId }, getAuthHeaders())
1702
+ );
1703
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1704
+ return result.data;
1705
+ },
1706
+ onSuccess: () => {
1707
+ qc.invalidateQueries({
1708
+ queryKey: ["dash-org-invitations", organizationId]
1709
+ });
1710
+ setInviteEmail("");
1711
+ setInviteRole("member");
1712
+ toggleNotification({ type: "success", message: "Invitation sent" });
1713
+ },
1714
+ onError: (err) => {
1715
+ toggleNotification({
1716
+ type: "danger",
1717
+ message: err.message ?? "Failed to invite"
1718
+ });
1719
+ }
1720
+ });
1721
+ const cancelInvitationMutation = useMutation({
1722
+ mutationFn: async (invitationId) => {
1723
+ const result = await client.dash.organization.cancelInvitation(
1724
+ { invitationId },
1725
+ withContext({ organizationId }, getAuthHeaders())
1726
+ );
1727
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1728
+ },
1729
+ onSuccess: () => {
1730
+ setConfirmCancelInvitationId(null);
1731
+ qc.invalidateQueries({
1732
+ queryKey: ["dash-org-invitations", organizationId]
1733
+ });
1734
+ toggleNotification({ type: "success", message: "Invitation cancelled" });
1735
+ },
1736
+ onError: (err) => {
1737
+ toggleNotification({
1738
+ type: "danger",
1739
+ message: err.message ?? "Failed to cancel"
1740
+ });
1741
+ }
1742
+ });
1743
+ const resendInvitationMutation = useMutation({
1744
+ mutationFn: async (invitationId) => {
1745
+ const result = await client.dash.organization.resendInvitation(
1746
+ { invitationId },
1747
+ withContext({ organizationId }, getAuthHeaders())
1748
+ );
1749
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1750
+ },
1751
+ onSuccess: () => {
1752
+ toggleNotification({ type: "success", message: "Invitation resent" });
1753
+ },
1754
+ onError: (err) => {
1755
+ toggleNotification({
1756
+ type: "danger",
1757
+ message: err.message ?? "Failed to resend"
1758
+ });
1759
+ }
1760
+ });
1761
+ const members = membersQuery.data ?? [];
1762
+ const teams = teamsQuery.data ?? [];
1117
1763
  const ssoProviders = ssoQuery.data ?? [];
1764
+ const invitations = invitationsQuery.data ?? [];
1118
1765
  const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
1119
1766
  const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
1120
1767
  const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1171,6 +1818,11 @@ function OrganizationDetail({
1171
1818
  teams.length,
1172
1819
  ")"
1173
1820
  ] }),
1821
+ /* @__PURE__ */ jsxs(Tabs.Trigger, { value: "invitations", children: [
1822
+ "Invitations (",
1823
+ invitations.length,
1824
+ ")"
1825
+ ] }),
1174
1826
  /* @__PURE__ */ jsxs(Tabs.Trigger, { value: "sso", children: [
1175
1827
  "SSO (",
1176
1828
  ssoProviders.length,
@@ -1225,33 +1877,43 @@ function OrganizationDetail({
1225
1877
  }
1226
1878
  )
1227
1879
  ] }),
1228
- /* @__PURE__ */ jsx(EditSidebar, { children: /* @__PURE__ */ jsxs(FormSection, { children: [
1229
- /* @__PURE__ */ jsx(SectionLabel, { children: "Details" }),
1230
- /* @__PURE__ */ jsxs(
1231
- "div",
1880
+ /* @__PURE__ */ jsxs(EditSidebar, { children: [
1881
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1882
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Details" }),
1883
+ /* @__PURE__ */ jsxs(
1884
+ "div",
1885
+ {
1886
+ style: {
1887
+ display: "grid",
1888
+ gridTemplateColumns: "1fr 1fr",
1889
+ gap: 12
1890
+ },
1891
+ children: [
1892
+ /* @__PURE__ */ jsxs(MetaItem, { style: { gridColumn: "1 / -1" }, children: [
1893
+ /* @__PURE__ */ jsx(MetaKey, { children: "Organization ID" }),
1894
+ /* @__PURE__ */ jsx(MonoChip, { children: org?.id })
1895
+ ] }),
1896
+ /* @__PURE__ */ jsxs(MetaItem, { children: [
1897
+ /* @__PURE__ */ jsx(MetaKey, { children: "Members" }),
1898
+ /* @__PURE__ */ jsx(MetaVal, { children: org?.memberCount ?? 0 })
1899
+ ] }),
1900
+ /* @__PURE__ */ jsxs(MetaItem, { children: [
1901
+ /* @__PURE__ */ jsx(MetaKey, { children: "Created" }),
1902
+ /* @__PURE__ */ jsx(MetaVal, { children: org?.createdAt ? new Date(org.createdAt).toLocaleDateString() : "—" })
1903
+ ] })
1904
+ ]
1905
+ }
1906
+ )
1907
+ ] }),
1908
+ /* @__PURE__ */ jsx(
1909
+ EditViewSidePanels,
1232
1910
  {
1233
- style: {
1234
- display: "grid",
1235
- gridTemplateColumns: "1fr 1fr",
1236
- gap: 12
1237
- },
1238
- children: [
1239
- /* @__PURE__ */ jsxs(MetaItem, { style: { gridColumn: "1 / -1" }, children: [
1240
- /* @__PURE__ */ jsx(MetaKey, { children: "Organization ID" }),
1241
- /* @__PURE__ */ jsx(MonoChip, { children: org?.id })
1242
- ] }),
1243
- /* @__PURE__ */ jsxs(MetaItem, { children: [
1244
- /* @__PURE__ */ jsx(MetaKey, { children: "Members" }),
1245
- /* @__PURE__ */ jsx(MetaVal, { children: org?.memberCount ?? 0 })
1246
- ] }),
1247
- /* @__PURE__ */ jsxs(MetaItem, { children: [
1248
- /* @__PURE__ */ jsx(MetaKey, { children: "Created" }),
1249
- /* @__PURE__ */ jsx(MetaVal, { children: org?.createdAt ? new Date(org.createdAt).toLocaleDateString() : "—" })
1250
- ] })
1251
- ]
1911
+ model: "plugin::better-auth.organization",
1912
+ documentId: strapiOrgQuery.data?.documentId,
1913
+ document: strapiOrgQuery.data ?? void 0
1252
1914
  }
1253
1915
  )
1254
- ] }) })
1916
+ ] })
1255
1917
  ] }) }),
1256
1918
  /* @__PURE__ */ jsx(Tabs.Content, { value: "members", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1257
1919
  /* @__PURE__ */ jsxs(FormSection, { children: [
@@ -1426,7 +2088,127 @@ function OrganizationDetail({
1426
2088
  },
1427
2089
  provider.id
1428
2090
  )) })
1429
- ] }) }) })
2091
+ ] }) }) }),
2092
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
2093
+ /* @__PURE__ */ jsxs(FormSection, { children: [
2094
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Invite member by email" }),
2095
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [
2096
+ /* @__PURE__ */ jsx(Grid.Item, { col: 8, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
2097
+ /* @__PURE__ */ jsx(Field.Label, { children: "Email address" }),
2098
+ /* @__PURE__ */ jsx(
2099
+ TextInput,
2100
+ {
2101
+ type: "email",
2102
+ value: inviteEmail,
2103
+ onChange: (e) => setInviteEmail(e.target.value),
2104
+ placeholder: "user@example.com"
2105
+ }
2106
+ )
2107
+ ] }) }),
2108
+ /* @__PURE__ */ jsx(Grid.Item, { col: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
2109
+ /* @__PURE__ */ jsx(Field.Label, { children: "Role" }),
2110
+ /* @__PURE__ */ jsxs(
2111
+ SingleSelect,
2112
+ {
2113
+ value: inviteRole,
2114
+ onChange: (v) => setInviteRole(String(v)),
2115
+ "aria-label": "Invite role",
2116
+ children: [
2117
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "member", children: "Member" }),
2118
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "admin", children: "Admin" }),
2119
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "owner", children: "Owner" })
2120
+ ]
2121
+ }
2122
+ )
2123
+ ] }) })
2124
+ ] }),
2125
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
2126
+ Button,
2127
+ {
2128
+ size: "S",
2129
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
2130
+ disabled: !inviteEmail,
2131
+ loading: inviteMemberMutation.isLoading,
2132
+ onClick: () => inviteMemberMutation.mutate(),
2133
+ children: "Send invitation"
2134
+ }
2135
+ ) })
2136
+ ] }),
2137
+ /* @__PURE__ */ jsxs(FormSection, { children: [
2138
+ /* @__PURE__ */ jsxs(SectionLabel, { children: [
2139
+ "Invitations (",
2140
+ invitations.length,
2141
+ ")"
2142
+ ] }),
2143
+ invitationsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : invitations.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No invitations yet." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: invitations.map((inv) => {
2144
+ const statusColor = {
2145
+ pending: "#f59e0b",
2146
+ accepted: "#5cb176",
2147
+ rejected: "#d02b20",
2148
+ canceled: "#8e8ea9"
2149
+ };
2150
+ const color = statusColor[inv.status] ?? "#8e8ea9";
2151
+ const isPending = inv.status === "pending";
2152
+ return /* @__PURE__ */ jsxs(AccountRow, { children: [
2153
+ /* @__PURE__ */ jsxs(
2154
+ Flex,
2155
+ {
2156
+ direction: "column",
2157
+ gap: 1,
2158
+ alignItems: "flex-start",
2159
+ style: { flex: 1 },
2160
+ children: [
2161
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
2162
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
2163
+ inv.email,
2164
+ " · role: ",
2165
+ inv.role
2166
+ ] }),
2167
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
2168
+ "Expires",
2169
+ " ",
2170
+ new Date(inv.expiresAt).toLocaleDateString()
2171
+ ] })
2172
+ ]
2173
+ }
2174
+ ),
2175
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
2176
+ /* @__PURE__ */ jsx(
2177
+ MonoChip,
2178
+ {
2179
+ style: {
2180
+ color,
2181
+ borderColor: color,
2182
+ background: `${color}18`
2183
+ },
2184
+ children: inv.status
2185
+ }
2186
+ ),
2187
+ isPending && /* @__PURE__ */ jsxs(Fragment, { children: [
2188
+ /* @__PURE__ */ jsx(
2189
+ Button,
2190
+ {
2191
+ size: "S",
2192
+ variant: "secondary",
2193
+ loading: resendInvitationMutation.isLoading,
2194
+ onClick: () => resendInvitationMutation.mutate(inv.id),
2195
+ children: "Resend"
2196
+ }
2197
+ ),
2198
+ /* @__PURE__ */ jsx(
2199
+ IconButton,
2200
+ {
2201
+ label: "Cancel invitation",
2202
+ onClick: () => setConfirmCancelInvitationId(inv.id),
2203
+ children: /* @__PURE__ */ jsx(Trash, {})
2204
+ }
2205
+ )
2206
+ ] })
2207
+ ] })
2208
+ ] }, inv.id);
2209
+ }) })
2210
+ ] })
2211
+ ] }) })
1430
2212
  ] }),
1431
2213
  confirmRemoveMemberId && /* @__PURE__ */ jsx(
1432
2214
  ConfirmDialog,
@@ -1460,6 +2242,17 @@ function OrganizationDetail({
1460
2242
  onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
1461
2243
  onCancel: () => setConfirmDeleteSsoId(null)
1462
2244
  }
2245
+ ),
2246
+ confirmCancelInvitationId && /* @__PURE__ */ jsx(
2247
+ ConfirmDialog,
2248
+ {
2249
+ title: "Cancel invitation",
2250
+ message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
2251
+ confirmLabel: "Cancel invitation",
2252
+ loading: cancelInvitationMutation.isLoading,
2253
+ onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
2254
+ onCancel: () => setConfirmCancelInvitationId(null)
2255
+ }
1463
2256
  )
1464
2257
  ]
1465
2258
  }
@@ -1481,7 +2274,7 @@ function TeamRow({
1481
2274
  organizationId
1482
2275
  ].teams[team.id].members(
1483
2276
  { params: { orgId: organizationId, teamId: team.id } },
1484
- withContext({ organizationId })
2277
+ withContext({ organizationId }, getAuthHeaders())
1485
2278
  );
1486
2279
  if (result.error) throw new Error(result.error.message ?? "Failed");
1487
2280
  return result.data ?? [];
@@ -1493,7 +2286,7 @@ function TeamRow({
1493
2286
  mutationFn: async () => {
1494
2287
  const result = await client.dash.organization.addTeamMember(
1495
2288
  { teamId: team.id, userId: addUserId },
1496
- withContext({ organizationId })
2289
+ withContext({ organizationId }, getAuthHeaders())
1497
2290
  );
1498
2291
  if (result.error) throw new Error(result.error.message ?? "Failed");
1499
2292
  return result.data;
@@ -1509,7 +2302,7 @@ function TeamRow({
1509
2302
  mutationFn: async (userId) => {
1510
2303
  const result = await client.dash.organization.removeTeamMember(
1511
2304
  { teamId: team.id, userId },
1512
- withContext({ organizationId })
2305
+ withContext({ organizationId }, getAuthHeaders())
1513
2306
  );
1514
2307
  if (result.error) throw new Error(result.error.message ?? "Failed");
1515
2308
  },
@@ -1605,12 +2398,12 @@ function TeamRow({
1605
2398
  )
1606
2399
  ] });
1607
2400
  }
1608
- const PAGE_SIZE$2 = 25;
1609
- const fadeUp$3 = keyframes`
2401
+ const PAGE_SIZE$1 = 25;
2402
+ const fadeUp$2 = keyframes`
1610
2403
  from { opacity: 0; transform: translateY(6px); }
1611
2404
  to { opacity: 1; transform: translateY(0); }
1612
2405
  `;
1613
- const Wrap$3 = styled.div`
2406
+ const Wrap$2 = styled.div`
1614
2407
  padding: 28px 32px;
1615
2408
  background: #f6f6f9;
1616
2409
  min-height: 100%;
@@ -1618,39 +2411,39 @@ const Wrap$3 = styled.div`
1618
2411
  flex-direction: column;
1619
2412
  gap: 20px;
1620
2413
  `;
1621
- const PageHeader$2 = styled.div`
2414
+ const PageHeader$1 = styled.div`
1622
2415
  display: flex;
1623
2416
  justify-content: space-between;
1624
2417
  align-items: flex-start;
1625
2418
  `;
1626
- const TitleBlock$2 = styled.div`
2419
+ const TitleBlock$1 = styled.div`
1627
2420
  display: flex;
1628
2421
  flex-direction: column;
1629
2422
  gap: 4px;
1630
2423
  `;
1631
- const PageTitle$2 = styled.h1`
2424
+ const PageTitle$1 = styled.h1`
1632
2425
  margin: 0;
1633
2426
  font-size: 22px;
1634
2427
  font-weight: 800;
1635
2428
  color: #32324d;
1636
2429
  letter-spacing: -0.03em;
1637
2430
  `;
1638
- const PageSubtitle$2 = styled.p`
2431
+ const PageSubtitle$1 = styled.p`
1639
2432
  margin: 0;
1640
2433
  font-size: 12px;
1641
2434
  color: #8e8ea9;
1642
2435
  `;
1643
- const TableCard$2 = styled.div`
2436
+ const TableCard$1 = styled.div`
1644
2437
  background: #ffffff;
1645
2438
  border: 1px solid #eaeaef;
1646
2439
  border-radius: 10px;
1647
2440
  overflow: hidden;
1648
2441
  `;
1649
- const Table$2 = styled.table`
2442
+ const Table$1 = styled.table`
1650
2443
  width: 100%;
1651
2444
  border-collapse: collapse;
1652
2445
  `;
1653
- const TH$2 = styled.th`
2446
+ const TH$1 = styled.th`
1654
2447
  text-align: left;
1655
2448
  padding: 10px 14px;
1656
2449
  font-size: 10px;
@@ -1664,13 +2457,13 @@ const TH$2 = styled.th`
1664
2457
  &:first-child { padding-left: 20px; }
1665
2458
  &:last-child { padding-right: 20px; }
1666
2459
  `;
1667
- const THCheck$1 = styled(TH$2)`
2460
+ const THCheck$1 = styled(TH$1)`
1668
2461
  width: 44px;
1669
2462
  `;
1670
- const THActions$1 = styled(TH$2)`
2463
+ const THActions$1 = styled(TH$1)`
1671
2464
  width: 80px;
1672
2465
  `;
1673
- const TD$2 = styled.td`
2466
+ const TD$1 = styled.td`
1674
2467
  padding: 11px 14px;
1675
2468
  font-size: 12px;
1676
2469
  color: #32324d;
@@ -1679,14 +2472,14 @@ const TD$2 = styled.td`
1679
2472
  &:first-child { padding-left: 20px; }
1680
2473
  &:last-child { padding-right: 20px; }
1681
2474
  `;
1682
- const TDCheck$1 = styled(TD$2)`
2475
+ const TDCheck$1 = styled(TD$1)`
1683
2476
  width: 44px;
1684
2477
  `;
1685
- const TDActions$1 = styled(TD$2)`
2478
+ const TDActions$1 = styled(TD$1)`
1686
2479
  width: 80px;
1687
2480
  `;
1688
- const TR$2 = styled.tr`
1689
- animation: ${fadeUp$3} 280ms ease both;
2481
+ const TR$1 = styled.tr`
2482
+ animation: ${fadeUp$2} 280ms ease both;
1690
2483
  animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1691
2484
  background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1692
2485
  transition: background 120ms ease;
@@ -1773,7 +2566,9 @@ function OrgAvatar({ name, logo }) {
1773
2566
  return /* @__PURE__ */ jsx(OrgLogoFallback, { $bg: color.bg, $fg: color.fg, children: initials });
1774
2567
  }
1775
2568
  function OrganizationsPage({ teamsEnabled }) {
2569
+ const rbac = useRBAC(PERMISSIONS.organization);
1776
2570
  const qc = useQueryClient();
2571
+ const { toggleNotification } = useNotification();
1777
2572
  const [page, setPage] = useState(1);
1778
2573
  const [search, setSearch] = useState("");
1779
2574
  const [searchInput, setSearchInput] = useState("");
@@ -1782,13 +2577,13 @@ function OrganizationsPage({ teamsEnabled }) {
1782
2577
  const [detailOrgId, setDetailOrgId] = useState(null);
1783
2578
  const [confirmDelete, setConfirmDelete] = useState(null);
1784
2579
  const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
1785
- const offset = (page - 1) * PAGE_SIZE$2;
2580
+ const offset = (page - 1) * PAGE_SIZE$1;
1786
2581
  const orgsQuery = useQuery({
1787
2582
  queryKey: ["dash-organizations", page, search],
1788
2583
  queryFn: async () => {
1789
2584
  const result = await client.dash.listOrganizations({
1790
2585
  query: {
1791
- limit: PAGE_SIZE$2,
2586
+ limit: PAGE_SIZE$1,
1792
2587
  offset,
1793
2588
  sortBy: "createdAt",
1794
2589
  sortOrder: "desc",
@@ -1805,7 +2600,7 @@ function OrganizationsPage({ teamsEnabled }) {
1805
2600
  mutationFn: async (organizationId) => {
1806
2601
  const result = await client.dash.organization.delete(
1807
2602
  { organizationId },
1808
- withContext({ organizationId })
2603
+ withContext({ organizationId }, getAuthHeaders())
1809
2604
  );
1810
2605
  if (result.error)
1811
2606
  throw new Error(result.error.message ?? "Delete failed");
@@ -1813,27 +2608,44 @@ function OrganizationsPage({ teamsEnabled }) {
1813
2608
  onSuccess: () => {
1814
2609
  setConfirmDelete(null);
1815
2610
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2611
+ toggleNotification({ type: "success", message: "Organization deleted" });
2612
+ },
2613
+ onError: (err) => {
2614
+ toggleNotification({
2615
+ type: "danger",
2616
+ message: err.message ?? "Failed to delete organization"
2617
+ });
1816
2618
  }
1817
2619
  });
1818
2620
  const deleteManyMutation = useMutation({
1819
2621
  mutationFn: async (organizationIds) => {
1820
2622
  const result = await client.dash.organization.deleteMany(
1821
2623
  {},
1822
- withContext({ organizationIds })
2624
+ withContext({ organizationIds }, getAuthHeaders())
1823
2625
  );
1824
2626
  if (result.error)
1825
2627
  throw new Error(result.error.message ?? "Delete failed");
1826
2628
  return result.data;
1827
2629
  },
1828
- onSuccess: () => {
2630
+ onSuccess: (_data, organizationIds) => {
1829
2631
  setConfirmDeleteMany(false);
1830
2632
  setSelected(/* @__PURE__ */ new Set());
1831
2633
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2634
+ toggleNotification({
2635
+ type: "success",
2636
+ message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
2637
+ });
2638
+ },
2639
+ onError: (err) => {
2640
+ toggleNotification({
2641
+ type: "danger",
2642
+ message: err.message ?? "Failed to delete organizations"
2643
+ });
1832
2644
  }
1833
2645
  });
1834
2646
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
1835
2647
  const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
1836
- const pageCount = Math.ceil(total / PAGE_SIZE$2);
2648
+ const pageCount = Math.ceil(total / PAGE_SIZE$1);
1837
2649
  const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
1838
2650
  const someSelected = selected.size > 0;
1839
2651
  const toggleSelect = (id) => {
@@ -1853,210 +2665,218 @@ function OrganizationsPage({ teamsEnabled }) {
1853
2665
  setSearch(searchInput);
1854
2666
  setPage(1);
1855
2667
  };
1856
- return /* @__PURE__ */ jsxs(Wrap$3, { "data-testid": "organizations-page", children: [
1857
- /* @__PURE__ */ jsxs(PageHeader$2, { children: [
1858
- /* @__PURE__ */ jsxs(TitleBlock$2, { children: [
1859
- /* @__PURE__ */ jsx(PageTitle$2, { children: "Organizations" }),
1860
- /* @__PURE__ */ jsxs(PageSubtitle$2, { children: [
1861
- total.toLocaleString(),
1862
- " total"
1863
- ] })
1864
- ] }),
1865
- /* @__PURE__ */ jsx(
1866
- Button,
1867
- {
1868
- startIcon: /* @__PURE__ */ jsx(Plus, {}),
1869
- onClick: () => setShowCreate(true),
1870
- "data-testid": "create-org-btn",
1871
- children: "Create organization"
1872
- }
1873
- )
1874
- ] }),
1875
- /* @__PURE__ */ jsxs(Toolbar$1, { children: [
1876
- /* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
1877
- Searchbar,
1878
- {
1879
- clearLabel: "Clear",
1880
- name: "search",
1881
- placeholder: "Search organizations…",
1882
- value: searchInput,
1883
- onChange: (e) => setSearchInput(e.target.value),
1884
- onClear: () => {
1885
- setSearchInput("");
1886
- setSearch("");
1887
- setPage(1);
1888
- },
1889
- children: "Search organizations"
1890
- }
1891
- ) }),
1892
- someSelected && /* @__PURE__ */ jsxs(
1893
- Button,
1894
- {
1895
- variant: "danger-light",
1896
- size: "S",
1897
- onClick: () => setConfirmDeleteMany(true),
1898
- "data-testid": "delete-selected-orgs-btn",
1899
- children: [
1900
- "Delete ",
1901
- selected.size,
1902
- " selected"
1903
- ]
1904
- }
1905
- )
1906
- ] }),
1907
- orgsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
1908
- /* @__PURE__ */ jsx(TableCard$2, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxs(Table$2, { children: [
1909
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1910
- /* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
1911
- Checkbox,
1912
- {
1913
- checked: allSelected,
1914
- onCheckedChange: handleSelectAll,
1915
- "aria-label": "Select all"
1916
- }
1917
- ) }),
1918
- /* @__PURE__ */ jsx(TH$2, { children: "Name" }),
1919
- /* @__PURE__ */ jsx(TH$2, { children: "Slug" }),
1920
- /* @__PURE__ */ jsx(TH$2, { children: "Members" }),
1921
- /* @__PURE__ */ jsx(TH$2, { children: "Created" }),
1922
- /* @__PURE__ */ jsx(THActions$1, {})
1923
- ] }) }),
1924
- /* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
1925
- TD$2,
1926
- {
1927
- colSpan: 6,
1928
- style: {
1929
- textAlign: "center",
1930
- padding: "40px",
1931
- color: "#8e8ea9"
1932
- },
1933
- "data-testid": "orgs-empty",
1934
- children: search ? `No organizations matching "${search}"` : "No organizations found"
1935
- }
1936
- ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
1937
- TR$2,
1938
- {
1939
- $selected: selected.has(org.id),
1940
- $i: i,
1941
- "data-testid": "org-row",
1942
- children: [
1943
- /* @__PURE__ */ jsx(TDCheck$1, { children: /* @__PURE__ */ jsx(
2668
+ return /* @__PURE__ */ jsx(
2669
+ Page.Protect,
2670
+ {
2671
+ permissions: PERMISSIONS.organization.filter(
2672
+ (p) => p.action === `plugin::${PLUGIN_ID}.organization.read`
2673
+ ),
2674
+ children: /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
2675
+ /* @__PURE__ */ jsxs(PageHeader$1, { children: [
2676
+ /* @__PURE__ */ jsxs(TitleBlock$1, { children: [
2677
+ /* @__PURE__ */ jsx(PageTitle$1, { children: "Organizations" }),
2678
+ /* @__PURE__ */ jsxs(PageSubtitle$1, { children: [
2679
+ total.toLocaleString(),
2680
+ " total"
2681
+ ] })
2682
+ ] }),
2683
+ rbac.allowedActions.canCreate && /* @__PURE__ */ jsx(
2684
+ Button,
2685
+ {
2686
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
2687
+ onClick: () => setShowCreate(true),
2688
+ "data-testid": "create-org-btn",
2689
+ children: "Create organization"
2690
+ }
2691
+ )
2692
+ ] }),
2693
+ /* @__PURE__ */ jsxs(Toolbar$1, { children: [
2694
+ /* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
2695
+ Searchbar,
2696
+ {
2697
+ clearLabel: "Clear",
2698
+ name: "search",
2699
+ placeholder: "Search organizations…",
2700
+ value: searchInput,
2701
+ onChange: (e) => setSearchInput(e.target.value),
2702
+ onClear: () => {
2703
+ setSearchInput("");
2704
+ setSearch("");
2705
+ setPage(1);
2706
+ },
2707
+ children: "Search organizations"
2708
+ }
2709
+ ) }),
2710
+ someSelected && rbac.allowedActions.canDelete && /* @__PURE__ */ jsxs(
2711
+ Button,
2712
+ {
2713
+ variant: "danger-light",
2714
+ size: "S",
2715
+ onClick: () => setConfirmDeleteMany(true),
2716
+ "data-testid": "delete-selected-orgs-btn",
2717
+ children: [
2718
+ "Delete ",
2719
+ selected.size,
2720
+ " selected"
2721
+ ]
2722
+ }
2723
+ )
2724
+ ] }),
2725
+ orgsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
2726
+ /* @__PURE__ */ jsx(TableCard$1, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxs(Table$1, { children: [
2727
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2728
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
1944
2729
  Checkbox,
1945
2730
  {
1946
- checked: selected.has(org.id),
1947
- onCheckedChange: () => toggleSelect(org.id),
1948
- "aria-label": `Select ${org.name}`
2731
+ checked: allSelected,
2732
+ onCheckedChange: handleSelectAll,
2733
+ "aria-label": "Select all"
1949
2734
  }
1950
2735
  ) }),
1951
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1952
- /* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
1953
- /* @__PURE__ */ jsx(OrgName, { children: org.name })
1954
- ] }) }),
1955
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
1956
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
1957
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
1958
- /* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
1959
- /* @__PURE__ */ jsx(
1960
- IconButton,
1961
- {
1962
- label: "Edit organization",
1963
- onClick: () => setDetailOrgId(org.id),
1964
- "data-testid": "edit-org-btn",
1965
- children: /* @__PURE__ */ jsx(Pencil, {})
1966
- }
1967
- ),
1968
- /* @__PURE__ */ jsx(
1969
- IconButton,
1970
- {
1971
- label: "Delete organization",
1972
- onClick: () => setConfirmDelete(org.id),
1973
- "data-testid": "delete-org-btn",
1974
- children: /* @__PURE__ */ jsx(Trash, {})
1975
- }
1976
- )
1977
- ] }) })
1978
- ]
1979
- },
1980
- org.id
1981
- )) })
1982
- ] }) }),
1983
- pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1984
- /* @__PURE__ */ jsx(
1985
- Button,
1986
- {
1987
- variant: "tertiary",
1988
- size: "S",
1989
- disabled: page === 1,
1990
- onClick: () => setPage((p) => p - 1),
1991
- children: "Previous"
1992
- }
1993
- ),
1994
- /* @__PURE__ */ jsx(
1995
- Button,
1996
- {
1997
- variant: "tertiary",
1998
- size: "S",
1999
- disabled: page >= pageCount,
2000
- onClick: () => setPage((p) => p + 1),
2001
- children: "Next"
2002
- }
2003
- )
2004
- ] }) }),
2005
- showCreate && /* @__PURE__ */ jsx(
2006
- CreateOrganizationDialog,
2007
- {
2008
- teamsEnabled,
2009
- onClose: () => setShowCreate(false)
2010
- }
2011
- ),
2012
- detailOrgId && /* @__PURE__ */ jsx(
2013
- OrganizationDetail,
2014
- {
2015
- organizationId: detailOrgId,
2016
- teamsEnabled,
2017
- onClose: () => setDetailOrgId(null)
2018
- }
2019
- ),
2020
- confirmDelete && /* @__PURE__ */ jsx(
2021
- ConfirmDialog,
2022
- {
2023
- title: "Delete organization",
2024
- message: "Are you sure you want to delete this organization? All members and teams will be removed. This action cannot be undone.",
2025
- confirmLabel: "Delete",
2026
- loading: deleteMutation.isLoading,
2027
- onConfirm: () => deleteMutation.mutate(confirmDelete),
2028
- onCancel: () => setConfirmDelete(null)
2029
- }
2030
- ),
2031
- confirmDeleteMany && /* @__PURE__ */ jsx(
2032
- ConfirmDialog,
2033
- {
2034
- title: `Delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}`,
2035
- message: `Are you sure you want to delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
2036
- confirmLabel: "Delete all",
2037
- loading: deleteManyMutation.isLoading,
2038
- onConfirm: () => deleteManyMutation.mutate([...selected]),
2039
- onCancel: () => setConfirmDeleteMany(false)
2040
- }
2041
- )
2042
- ] });
2043
- }
2044
- const PALETTE = [
2045
- { bg: "#EAF5FF", fg: "#0C75AF" },
2046
- { bg: "#F0F0FF", fg: "#4945FF" },
2047
- { bg: "#EAFBE7", fg: "#328048" },
2048
- { bg: "#FFF3D3", fg: "#8E6A00" },
2049
- { bg: "#FCECEA", fg: "#D02B20" },
2050
- { bg: "#F6ECFC", fg: "#8312D1" },
2051
- { bg: "#E8F8F5", fg: "#1A7F64" }
2052
- ];
2053
- const Circle = styled.div`
2054
- width: ${(p) => p.$size}px;
2055
- height: ${(p) => p.$size}px;
2056
- border-radius: 50%;
2057
- background: ${(p) => p.$bg};
2058
- color: ${(p) => p.$fg};
2059
- display: inline-flex;
2736
+ /* @__PURE__ */ jsx(TH$1, { children: "Name" }),
2737
+ /* @__PURE__ */ jsx(TH$1, { children: "Slug" }),
2738
+ /* @__PURE__ */ jsx(TH$1, { children: "Members" }),
2739
+ /* @__PURE__ */ jsx(TH$1, { children: "Created" }),
2740
+ /* @__PURE__ */ jsx(THActions$1, {})
2741
+ ] }) }),
2742
+ /* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2743
+ TD$1,
2744
+ {
2745
+ colSpan: 6,
2746
+ style: {
2747
+ textAlign: "center",
2748
+ padding: "40px",
2749
+ color: "#8e8ea9"
2750
+ },
2751
+ "data-testid": "orgs-empty",
2752
+ children: search ? `No organizations matching "${search}"` : "No organizations found"
2753
+ }
2754
+ ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
2755
+ TR$1,
2756
+ {
2757
+ $selected: selected.has(org.id),
2758
+ $i: i,
2759
+ "data-testid": "org-row",
2760
+ children: [
2761
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(TDCheck$1, { children: /* @__PURE__ */ jsx(
2762
+ Checkbox,
2763
+ {
2764
+ checked: selected.has(org.id),
2765
+ onCheckedChange: () => toggleSelect(org.id),
2766
+ "aria-label": `Select ${org.name}`
2767
+ }
2768
+ ) }),
2769
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
2770
+ /* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
2771
+ /* @__PURE__ */ jsx(OrgName, { children: org.name })
2772
+ ] }) }),
2773
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
2774
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
2775
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
2776
+ /* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
2777
+ rbac.allowedActions.canUpdate && /* @__PURE__ */ jsx(
2778
+ IconButton,
2779
+ {
2780
+ label: "Edit organization",
2781
+ onClick: () => setDetailOrgId(org.id),
2782
+ "data-testid": "edit-org-btn",
2783
+ children: /* @__PURE__ */ jsx(Pencil, {})
2784
+ }
2785
+ ),
2786
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(
2787
+ IconButton,
2788
+ {
2789
+ label: "Delete organization",
2790
+ onClick: () => setConfirmDelete(org.id),
2791
+ "data-testid": "delete-org-btn",
2792
+ children: /* @__PURE__ */ jsx(Trash, {})
2793
+ }
2794
+ )
2795
+ ] }) })
2796
+ ]
2797
+ },
2798
+ org.id
2799
+ )) })
2800
+ ] }) }),
2801
+ pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2802
+ /* @__PURE__ */ jsx(
2803
+ Button,
2804
+ {
2805
+ variant: "tertiary",
2806
+ size: "S",
2807
+ disabled: page === 1,
2808
+ onClick: () => setPage((p) => p - 1),
2809
+ children: "Previous"
2810
+ }
2811
+ ),
2812
+ /* @__PURE__ */ jsx(
2813
+ Button,
2814
+ {
2815
+ variant: "tertiary",
2816
+ size: "S",
2817
+ disabled: page >= pageCount,
2818
+ onClick: () => setPage((p) => p + 1),
2819
+ children: "Next"
2820
+ }
2821
+ )
2822
+ ] }) }),
2823
+ rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsx(
2824
+ CreateOrganizationDialog,
2825
+ {
2826
+ teamsEnabled,
2827
+ onClose: () => setShowCreate(false)
2828
+ }
2829
+ ),
2830
+ rbac.allowedActions.canUpdate && detailOrgId && /* @__PURE__ */ jsx(
2831
+ OrganizationDetail,
2832
+ {
2833
+ organizationId: detailOrgId,
2834
+ teamsEnabled,
2835
+ onClose: () => setDetailOrgId(null)
2836
+ }
2837
+ ),
2838
+ rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsx(
2839
+ ConfirmDialog,
2840
+ {
2841
+ title: "Delete organization",
2842
+ message: "Are you sure you want to delete this organization? All members and teams will be removed. This action cannot be undone.",
2843
+ confirmLabel: "Delete",
2844
+ loading: deleteMutation.isLoading,
2845
+ onConfirm: () => deleteMutation.mutate(confirmDelete),
2846
+ onCancel: () => setConfirmDelete(null)
2847
+ }
2848
+ ),
2849
+ rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsx(
2850
+ ConfirmDialog,
2851
+ {
2852
+ title: `Delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}`,
2853
+ message: `Are you sure you want to delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
2854
+ confirmLabel: "Delete all",
2855
+ loading: deleteManyMutation.isLoading,
2856
+ onConfirm: () => deleteManyMutation.mutate([...selected]),
2857
+ onCancel: () => setConfirmDeleteMany(false)
2858
+ }
2859
+ )
2860
+ ] })
2861
+ }
2862
+ );
2863
+ }
2864
+ const PALETTE = [
2865
+ { bg: "#EAF5FF", fg: "#0C75AF" },
2866
+ { bg: "#F0F0FF", fg: "#4945FF" },
2867
+ { bg: "#EAFBE7", fg: "#328048" },
2868
+ { bg: "#FFF3D3", fg: "#8E6A00" },
2869
+ { bg: "#FCECEA", fg: "#D02B20" },
2870
+ { bg: "#F6ECFC", fg: "#8312D1" },
2871
+ { bg: "#E8F8F5", fg: "#1A7F64" }
2872
+ ];
2873
+ const Circle = styled.div`
2874
+ width: ${(p) => p.$size}px;
2875
+ height: ${(p) => p.$size}px;
2876
+ border-radius: 50%;
2877
+ background: ${(p) => p.$bg};
2878
+ color: ${(p) => p.$fg};
2879
+ display: inline-flex;
2060
2880
  align-items: center;
2061
2881
  justify-content: center;
2062
2882
  font-size: ${(p) => Math.round(p.$size * 0.38)}px;
@@ -2101,95 +2921,143 @@ function Avatar({
2101
2921
  const color = getColor(name || "?");
2102
2922
  return /* @__PURE__ */ jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
2103
2923
  }
2104
- const fadeUp$2 = keyframes`
2105
- from { opacity: 0; transform: translateY(8px); }
2924
+ const T = {
2925
+ bg: "#f6f6f9",
2926
+ bgCard: "#ffffff",
2927
+ border: "#eaeaef",
2928
+ borderHover: "rgba(73,69,255,0.35)",
2929
+ accent: "#4945ff",
2930
+ green: "#5cb176",
2931
+ greenDim: "rgba(92,177,118,0.12)",
2932
+ amber: "#d9822f",
2933
+ red: "#d02b20",
2934
+ redDim: "rgba(208,43,32,0.1)",
2935
+ purple: "#8460b8",
2936
+ textPrimary: "#32324d",
2937
+ textSecondary: "#666687",
2938
+ textMuted: "#b8b8c7",
2939
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
2940
+ };
2941
+ const fadeUp$1 = keyframes`
2942
+ from { opacity: 0; transform: translateY(10px); }
2106
2943
  to { opacity: 1; transform: translateY(0); }
2107
2944
  `;
2108
- const Wrap$2 = styled.div`
2109
- padding: 28px 32px;
2110
- background: #f6f6f9;
2945
+ const Wrap$1 = styled.div`
2946
+ padding: 28px 32px 48px;
2947
+ background: ${T.bg};
2111
2948
  min-height: 100%;
2112
2949
  display: flex;
2113
2950
  flex-direction: column;
2114
- gap: 20px;
2951
+ gap: 22px;
2115
2952
  `;
2116
- const StatRow = styled.div`
2117
- display: grid;
2118
- grid-template-columns: repeat(5, 1fr);
2119
- gap: 12px;
2120
- @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2121
- @media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
2953
+ const SectionDivider = styled.div`
2954
+ display: flex;
2955
+ align-items: center;
2956
+ gap: 10px;
2957
+ margin-bottom: -6px;
2122
2958
  `;
2123
- const MainRow = styled.div`
2124
- display: grid;
2125
- grid-template-columns: 1fr 300px;
2126
- gap: 12px;
2127
- @media (max-width: 960px) { grid-template-columns: 1fr; }
2959
+ const DivLabel = styled.span`
2960
+ font-size: 9px;
2961
+ font-weight: 700;
2962
+ letter-spacing: 0.14em;
2963
+ text-transform: uppercase;
2964
+ color: ${T.textMuted};
2965
+ white-space: nowrap;
2128
2966
  `;
2129
- const ActiveRow = styled.div`
2130
- display: grid;
2131
- grid-template-columns: repeat(3, 1fr);
2132
- gap: 12px;
2967
+ const DivLine = styled.div`
2968
+ flex: 1;
2969
+ height: 1px;
2970
+ background: ${T.border};
2133
2971
  `;
2134
2972
  const Card = styled.div`
2135
- background: #ffffff;
2136
- border: 1px solid #eaeaef;
2137
- border-radius: 10px;
2973
+ background: ${T.bgCard};
2974
+ border: 1px solid ${T.border};
2975
+ border-radius: 12px;
2138
2976
  overflow: hidden;
2139
2977
  position: relative;
2140
- animation: ${fadeUp$2} 360ms ease both;
2978
+ animation: ${fadeUp$1} 380ms ease both;
2141
2979
  animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
2142
- transition: border-color 180ms ease, box-shadow 180ms ease;
2143
-
2144
- ${(p) => p.$accent && `
2145
- &::before {
2146
- content: '';
2147
- position: absolute;
2148
- top: 0; left: 0; right: 0;
2149
- height: 3px;
2150
- background: ${p.$accent};
2151
- z-index: 1;
2152
- }
2153
- `}
2154
-
2980
+ transition: border-color 200ms, box-shadow 200ms;
2981
+ &:hover {
2982
+ border-color: ${T.borderHover};
2983
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
2984
+ }
2985
+ `;
2986
+ const PillGroup = styled.div`
2987
+ display: flex;
2988
+ background: rgba(0,0,0,0.03);
2989
+ border: 1px solid ${T.border};
2990
+ border-radius: 9px;
2991
+ padding: 3px;
2992
+ gap: 2px;
2993
+ `;
2994
+ const Pill = styled.button`
2995
+ appearance: none;
2996
+ border: none;
2997
+ cursor: pointer;
2998
+ padding: 5px 16px;
2999
+ border-radius: 6px;
3000
+ font-size: 11px;
3001
+ font-weight: 600;
3002
+ letter-spacing: 0.03em;
3003
+ transition: background 180ms, color 180ms;
3004
+ background: ${(p) => p.$active ? T.accent : "transparent"};
3005
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
2155
3006
  &:hover {
2156
- border-color: #c0bfff;
2157
- box-shadow: 0 2px 16px rgba(73,69,255,0.08);
3007
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
3008
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
2158
3009
  }
2159
3010
  `;
2160
- const StatTop = styled.div`
2161
- padding: 18px 18px 10px;
3011
+ const StatGrid = styled.div`
3012
+ display: grid;
3013
+ grid-template-columns: repeat(5, 1fr);
3014
+ gap: 10px;
3015
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
3016
+ `;
3017
+ const StatCard = styled(Card)`
3018
+ padding: 18px 20px 0;
3019
+ &::after {
3020
+ content: '';
3021
+ position: absolute;
3022
+ top: 0; left: 0; right: 0;
3023
+ height: 2px;
3024
+ background: ${(p) => p.$accent};
3025
+ opacity: 0.75;
3026
+ }
2162
3027
  `;
2163
3028
  const StatLabel = styled.div`
2164
- font-size: 10px;
3029
+ font-size: 9px;
2165
3030
  font-weight: 700;
2166
- letter-spacing: 0.08em;
3031
+ letter-spacing: 0.1em;
2167
3032
  text-transform: uppercase;
2168
- color: #8e8ea9;
2169
- margin-bottom: 8px;
3033
+ color: ${T.textMuted};
3034
+ margin-bottom: 10px;
2170
3035
  `;
2171
3036
  const StatValue = styled.div`
2172
3037
  font-size: 30px;
2173
3038
  font-weight: 800;
2174
- color: #32324d;
3039
+ color: ${T.textPrimary};
2175
3040
  line-height: 1;
2176
- letter-spacing: -0.05em;
3041
+ letter-spacing: -0.045em;
2177
3042
  font-variant-numeric: tabular-nums;
2178
3043
  margin-bottom: 10px;
2179
3044
  `;
2180
3045
  const TrendBadge = styled.span`
2181
3046
  display: inline-flex;
2182
3047
  align-items: center;
2183
- gap: 2px;
2184
- padding: 2px 7px;
3048
+ gap: 3px;
3049
+ padding: 2px 8px;
2185
3050
  border-radius: 20px;
2186
- font-size: 10px;
3051
+ font-size: 9px;
2187
3052
  font-weight: 700;
2188
- background: ${(p) => p.$pos ? "#eafbe7" : "#fcecea"};
2189
- color: ${(p) => p.$pos ? "#5cb176" : "#d02b20"};
3053
+ letter-spacing: 0.03em;
3054
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
3055
+ color: ${(p) => p.$pos ? T.green : T.red};
3056
+ margin-bottom: 12px;
2190
3057
  `;
2191
3058
  const SparkWrap = styled.div`
2192
- height: 48px;
3059
+ height: 44px;
3060
+ margin: 0 -20px;
2193
3061
  `;
2194
3062
  const ChartCard = styled(Card)`
2195
3063
  padding: 20px 20px 12px;
@@ -2201,31 +3069,42 @@ const ChartHeader = styled.div`
2201
3069
  display: flex;
2202
3070
  align-items: center;
2203
3071
  justify-content: space-between;
3072
+ gap: 12px;
2204
3073
  flex-shrink: 0;
2205
3074
  `;
2206
3075
  const ChartTitle = styled.div`
2207
- font-size: 13px;
3076
+ font-size: 12px;
2208
3077
  font-weight: 700;
2209
- color: #32324d;
3078
+ color: ${T.textPrimary};
3079
+ letter-spacing: 0.01em;
2210
3080
  `;
2211
- const LegendRow = styled.div`
3081
+ const SeriesRow = styled.div`
2212
3082
  display: flex;
2213
3083
  align-items: center;
2214
- gap: 14px;
3084
+ gap: 6px;
2215
3085
  `;
2216
- const LegendItem = styled.div`
3086
+ const SeriesBtn = styled.button`
3087
+ appearance: none;
3088
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
3089
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
3090
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
3091
+ font-size: 10px;
3092
+ font-weight: 600;
3093
+ padding: 3px 10px 3px 8px;
3094
+ border-radius: 6px;
3095
+ cursor: pointer;
2217
3096
  display: flex;
2218
3097
  align-items: center;
2219
3098
  gap: 5px;
2220
- font-size: 10px;
2221
- color: #8e8ea9;
2222
- font-weight: 600;
2223
- text-transform: uppercase;
2224
- letter-spacing: 0.04em;
3099
+ transition: all 160ms;
3100
+ &:hover {
3101
+ border-color: ${(p) => `${p.$c}88`};
3102
+ color: ${(p) => p.$c};
3103
+ background: ${(p) => `${p.$c}22`};
3104
+ }
2225
3105
  `;
2226
- const LegendDot = styled.span`
2227
- width: 8px;
2228
- height: 8px;
3106
+ const SDot = styled.span`
3107
+ width: 6px; height: 6px;
2229
3108
  border-radius: 50%;
2230
3109
  background: ${(p) => p.$c};
2231
3110
  display: inline-block;
@@ -2233,173 +3112,241 @@ const LegendDot = styled.span`
2233
3112
  `;
2234
3113
  const HoverInfo = styled.div`
2235
3114
  font-size: 11px;
2236
- color: #8e8ea9;
3115
+ color: ${T.textSecondary};
2237
3116
  font-variant-numeric: tabular-nums;
2238
- min-height: 16px;
3117
+ font-family: ${T.mono};
3118
+ min-height: 15px;
3119
+ `;
3120
+ const TwoPanel = styled.div`
3121
+ display: grid;
3122
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
3123
+ gap: 10px;
3124
+ align-items: stretch;
3125
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
2239
3126
  `;
2240
3127
  const FeedCard = styled(Card)`
2241
3128
  display: flex;
2242
3129
  flex-direction: column;
2243
- max-height: 380px;
3130
+ overflow: hidden;
2244
3131
  `;
2245
- const FeedHeader = styled.div`
2246
- padding: 16px 18px 12px;
2247
- border-bottom: 1px solid #f0f0f7;
2248
- flex-shrink: 0;
2249
- font-size: 10px;
3132
+ const FeedHead = styled.div`
3133
+ padding: 13px 16px 9px;
3134
+ border-bottom: 1px solid ${T.border};
3135
+ font-size: 11px;
2250
3136
  font-weight: 700;
2251
- letter-spacing: 0.08em;
2252
- text-transform: uppercase;
2253
- color: #8e8ea9;
3137
+ color: ${T.textPrimary};
3138
+ flex-shrink: 0;
3139
+ display: flex;
3140
+ justify-content: space-between;
3141
+ align-items: center;
2254
3142
  `;
2255
- const FeedList = styled.div`
3143
+ const FeedSelect = styled.select`
3144
+ appearance: none;
3145
+ border: 1px solid ${T.border};
3146
+ border-radius: 6px;
3147
+ background: ${T.bgCard};
3148
+ color: ${T.textSecondary};
3149
+ font-size: 10px;
3150
+ font-weight: 600;
3151
+ padding: 3px 22px 3px 8px;
3152
+ cursor: pointer;
3153
+ outline: none;
3154
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666687'/%3E%3C/svg%3E");
3155
+ background-repeat: no-repeat;
3156
+ background-position: right 7px center;
3157
+ transition: border-color 160ms;
3158
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
3159
+ `;
3160
+ const FeedScroll = styled.div`
2256
3161
  flex: 1;
2257
3162
  overflow-y: auto;
2258
- padding: 6px 0;
2259
- &::-webkit-scrollbar { width: 3px; }
2260
- &::-webkit-scrollbar-track { background: transparent; }
2261
- &::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
3163
+ min-height: 0;
3164
+ &::-webkit-scrollbar { width: 4px; }
3165
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
2262
3166
  `;
2263
3167
  const FeedItem = styled.div`
3168
+ padding: 9px 14px;
3169
+ border-bottom: 1px solid #f0f0f5;
2264
3170
  display: flex;
2265
- align-items: flex-start;
2266
- gap: 10px;
2267
- padding: 9px 18px;
2268
- &:hover { background: #fafafe; }
3171
+ flex-direction: column;
3172
+ gap: 3px;
3173
+ transition: background 130ms;
3174
+ &:hover { background: #fafaff; }
3175
+ &:last-child { border-bottom: none; }
2269
3176
  `;
2270
- const FeedContent = styled.div`
2271
- flex: 1;
2272
- min-width: 0;
3177
+ const FeedTop = styled.div`
3178
+ display: flex;
3179
+ align-items: center;
3180
+ justify-content: space-between;
3181
+ gap: 6px;
2273
3182
  `;
2274
- const FeedName = styled.div`
2275
- font-size: 12px;
3183
+ const FeedName = styled.span`
3184
+ font-size: 11px;
2276
3185
  font-weight: 600;
2277
- color: #32324d;
2278
- white-space: nowrap;
3186
+ color: ${T.textPrimary};
2279
3187
  overflow: hidden;
2280
3188
  text-overflow: ellipsis;
3189
+ white-space: nowrap;
2281
3190
  `;
2282
- const FeedSub = styled.div`
3191
+ const FeedEmail = styled.span`
2283
3192
  font-size: 10px;
2284
- color: #8e8ea9;
2285
- white-space: nowrap;
3193
+ color: ${T.textSecondary};
2286
3194
  overflow: hidden;
2287
3195
  text-overflow: ellipsis;
2288
- margin-top: 1px;
3196
+ white-space: nowrap;
3197
+ display: block;
2289
3198
  `;
2290
- const FeedTime = styled.div`
2291
- font-size: 10px;
2292
- color: #b8b8c7;
3199
+ const FeedMeta = styled.span`
3200
+ font-size: 9px;
3201
+ color: ${T.textMuted};
2293
3202
  white-space: nowrap;
2294
3203
  font-variant-numeric: tabular-nums;
2295
- margin-top: 1px;
2296
3204
  `;
2297
- const UsersCard = styled(Card)``;
2298
- const TableWrap = styled.div`
2299
- overflow-x: auto;
3205
+ const RtnCard = styled(Card)`
3206
+ padding: 20px;
3207
+ display: flex;
3208
+ flex-direction: column;
3209
+ gap: 12px;
2300
3210
  `;
2301
- const Table$1 = styled.table`
2302
- width: 100%;
2303
- border-collapse: collapse;
3211
+ const RtnRow = styled.div`
3212
+ display: flex;
3213
+ align-items: center;
3214
+ gap: 10px;
3215
+ padding: 4px 6px;
3216
+ border-radius: 7px;
3217
+ transition: background 130ms;
3218
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
3219
+ cursor: default;
2304
3220
  `;
2305
- const TH$1 = styled.th`
2306
- text-align: left;
2307
- padding: 10px 14px;
3221
+ const RtnLabel = styled.span`
2308
3222
  font-size: 10px;
2309
- font-weight: 700;
2310
- color: #8e8ea9;
2311
- text-transform: uppercase;
2312
- letter-spacing: 0.06em;
2313
- border-bottom: 1px solid #eaeaef;
2314
- background: #fafafa;
3223
+ color: ${T.textSecondary};
3224
+ min-width: 80px;
3225
+ text-align: right;
2315
3226
  white-space: nowrap;
2316
- &:first-child { padding-left: 20px; }
2317
- &:last-child { padding-right: 20px; }
2318
- `;
2319
- const TR$1 = styled.tr`
2320
- &:hover td { background: #fafafe; }
2321
- &:last-child td { border-bottom: none; }
2322
3227
  `;
2323
- const TD$1 = styled.td`
2324
- padding: 10px 14px;
2325
- font-size: 12px;
2326
- color: #32324d;
2327
- border-bottom: 1px solid #f5f5f9;
2328
- vertical-align: middle;
2329
- &:first-child { padding-left: 20px; }
2330
- &:last-child { padding-right: 20px; }
3228
+ const RtnSize = styled.span`
3229
+ font-size: 9px;
3230
+ color: ${T.textMuted};
3231
+ min-width: 52px;
3232
+ text-align: right;
3233
+ white-space: nowrap;
3234
+ font-variant-numeric: tabular-nums;
2331
3235
  `;
2332
- const StatusChip$1 = styled.span`
2333
- display: inline-flex;
2334
- align-items: center;
2335
- gap: 4px;
2336
- padding: 2px 8px;
2337
- border-radius: 20px;
2338
- font-size: 10px;
2339
- font-weight: 700;
2340
- background: ${(p) => p.$banned ? "#fcecea" : p.$verified ? "#eafbe7" : "#f0f0ff"};
2341
- color: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#8e8ea9"};
2342
-
2343
- &::before {
2344
- content: '';
2345
- display: inline-block;
2346
- width: 5px;
2347
- height: 5px;
2348
- border-radius: 50%;
2349
- background: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#b8b8c7"};
2350
- }
3236
+ const RtnTrack = styled.div`
3237
+ flex: 1;
3238
+ height: 10px;
3239
+ background: #eaeaef;
3240
+ border-radius: 4px;
3241
+ overflow: hidden;
2351
3242
  `;
2352
- const Divider = styled.div`
2353
- display: flex;
2354
- align-items: center;
2355
- gap: 10px;
2356
- margin-bottom: -8px;
3243
+ const RtnBar = styled.div`
3244
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
3245
+ height: 100%;
3246
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
3247
+ border-radius: 4px;
3248
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2357
3249
  `;
2358
- const DivLabel = styled.span`
3250
+ const RtnPct = styled.span`
2359
3251
  font-size: 10px;
2360
3252
  font-weight: 700;
2361
- text-transform: uppercase;
2362
- letter-spacing: 0.08em;
2363
- color: #b8b8c7;
2364
- white-space: nowrap;
3253
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
3254
+ min-width: 38px;
3255
+ text-align: right;
3256
+ font-variant-numeric: tabular-nums;
3257
+ font-family: ${T.mono};
2365
3258
  `;
2366
- const DivLine = styled.div`
2367
- flex: 1;
2368
- height: 1px;
2369
- background: #eaeaef;
3259
+ const RtnTip = styled.div`
3260
+ font-size: 10px;
3261
+ color: ${T.textSecondary};
3262
+ min-height: 14px;
3263
+ font-variant-numeric: tabular-nums;
3264
+ padding: 0 6px;
2370
3265
  `;
2371
- const Empty = styled.div`
3266
+ const RingGrid = styled.div`
2372
3267
  display: flex;
2373
3268
  flex-direction: column;
2374
- align-items: center;
3269
+ gap: 10px;
3270
+ `;
3271
+ const RingCard = styled(Card)`
3272
+ padding: 18px 20px;
3273
+ display: flex;
3274
+ align-items: center;
3275
+ gap: 16px;
3276
+ flex: 1;
3277
+ `;
3278
+ const RingInfo = styled.div`
3279
+ display: flex;
3280
+ flex-direction: column;
3281
+ gap: 4px;
3282
+ `;
3283
+ const RingVal = styled.div`
3284
+ font-size: 24px;
3285
+ font-weight: 800;
3286
+ color: ${T.textPrimary};
3287
+ letter-spacing: -0.04em;
3288
+ font-variant-numeric: tabular-nums;
3289
+ line-height: 1;
3290
+ `;
3291
+ const RingLabel = styled.div`
3292
+ font-size: 9px;
3293
+ font-weight: 700;
3294
+ text-transform: uppercase;
3295
+ letter-spacing: 0.1em;
3296
+ color: ${T.textMuted};
3297
+ `;
3298
+ const Empty = styled.div`
3299
+ display: flex;
3300
+ flex-direction: column;
3301
+ align-items: center;
2375
3302
  justify-content: center;
2376
3303
  padding: 32px;
2377
- color: #8e8ea9;
3304
+ color: ${T.textMuted};
2378
3305
  font-size: 12px;
2379
- gap: 6px;
3306
+ gap: 8px;
2380
3307
  `;
3308
+ function useCountUp(target, duration = 850) {
3309
+ const [count, setCount] = useState(0);
3310
+ const prevRef = useRef(0);
3311
+ useEffect(() => {
3312
+ let raf;
3313
+ let start = 0;
3314
+ const from = prevRef.current;
3315
+ const diff = target - from;
3316
+ const step = (ts) => {
3317
+ if (!start) start = ts;
3318
+ const progress = Math.min((ts - start) / duration, 1);
3319
+ const eased = 1 - (1 - progress) ** 3;
3320
+ setCount(Math.round(from + eased * diff));
3321
+ if (progress < 1) raf = requestAnimationFrame(step);
3322
+ else prevRef.current = target;
3323
+ };
3324
+ raf = requestAnimationFrame(step);
3325
+ return () => cancelAnimationFrame(raf);
3326
+ }, [target, duration]);
3327
+ return count;
3328
+ }
2381
3329
  function Sparkline({
2382
3330
  data,
2383
3331
  color,
2384
3332
  id
2385
3333
  }) {
2386
3334
  const W = 300;
2387
- const H = 48;
3335
+ const H = 44;
2388
3336
  if (data.length < 2) return /* @__PURE__ */ jsx("svg", { width: "100%", height: H });
2389
3337
  const min = Math.min(...data);
2390
3338
  const max = Math.max(...data, min + 1);
2391
- const range = max - min;
2392
3339
  const pts = data.map((v, i) => ({
2393
3340
  x: i / (data.length - 1) * W,
2394
- y: H - 4 - (v - min) / range * (H - 10)
3341
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
2395
3342
  }));
2396
- const line = pts.reduce((acc, p, i) => {
3343
+ const line = pts.reduce((a, p, i) => {
2397
3344
  if (i === 0) return `M ${p.x} ${p.y}`;
2398
3345
  const pr = pts[i - 1];
2399
3346
  const cx = (pr.x + p.x) / 2;
2400
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
3347
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2401
3348
  }, "");
2402
- const area = `${line} L ${pts[pts.length - 1].x} ${H} L 0 ${H} Z`;
3349
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
2403
3350
  const gid = `spk-${id}`;
2404
3351
  return /* @__PURE__ */ jsxs(
2405
3352
  "svg",
@@ -2408,10 +3355,10 @@ function Sparkline({
2408
3355
  height: H,
2409
3356
  viewBox: `0 0 ${W} ${H}`,
2410
3357
  preserveAspectRatio: "none",
2411
- "aria-hidden": "true",
3358
+ "aria-hidden": true,
2412
3359
  children: [
2413
3360
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2414
- /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.25" }),
3361
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
2415
3362
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
2416
3363
  ] }) }),
2417
3364
  /* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
@@ -2420,57 +3367,58 @@ function Sparkline({
2420
3367
  {
2421
3368
  d: line,
2422
3369
  stroke: color,
2423
- strokeWidth: "2",
3370
+ strokeWidth: "1.6",
2424
3371
  fill: "none",
2425
- strokeLinecap: "round",
2426
- strokeLinejoin: "round"
3372
+ strokeLinecap: "round"
2427
3373
  }
2428
3374
  )
2429
3375
  ]
2430
3376
  }
2431
3377
  );
2432
3378
  }
2433
- const SERIES = [
2434
- { key: "totalUsers", color: "#4945FF", label: "Total" },
2435
- { key: "newUsers", color: "#5CB176", label: "New" },
2436
- { key: "activeUsers", color: "#E57553", label: "Active" }
3379
+ const ALL_SERIES = [
3380
+ { key: "totalUsers", color: T.accent, label: "Total" },
3381
+ { key: "newUsers", color: T.green, label: "New" },
3382
+ { key: "activeUsers", color: T.amber, label: "Active" }
2437
3383
  ];
2438
- function AreaChart({
3384
+ function GrowthChart({
2439
3385
  data,
2440
3386
  hovered,
2441
- onHover
3387
+ onHover,
3388
+ activeSeries
2442
3389
  }) {
2443
3390
  const W = 600;
2444
- const H = 220;
2445
- const PL = 44;
3391
+ const H = 200;
3392
+ const PL = 40;
2446
3393
  const PR = 12;
2447
3394
  const PT = 12;
2448
3395
  const PB = 28;
2449
3396
  const CW = W - PL - PR;
2450
3397
  const CH = H - PT - PB;
2451
- if (data.length === 0) {
3398
+ if (!data.length) {
2452
3399
  return /* @__PURE__ */ jsxs(Empty, { children: [
2453
3400
  /* @__PURE__ */ jsx("div", { style: { fontSize: 24 }, children: "📊" }),
2454
3401
  /* @__PURE__ */ jsx("div", { children: "No growth data for this period" })
2455
3402
  ] });
2456
3403
  }
2457
- const allVals = data.flatMap((d) => SERIES.map((s) => d[s.key]));
3404
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
3405
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
2458
3406
  const maxV = Math.max(...allVals, 10);
2459
3407
  const yMax = Math.ceil(maxV * 1.15);
2460
3408
  const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
2461
3409
  const yp = (v) => PT + (1 - v / yMax) * CH;
2462
3410
  const smooth = (vals) => {
2463
3411
  const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
2464
- const path = pts.reduce((acc, p, i) => {
3412
+ const path = pts.reduce((a, p, i) => {
2465
3413
  if (i === 0) return `M ${p.x} ${p.y}`;
2466
3414
  const pr = pts[i - 1];
2467
3415
  const cx = (pr.x + p.x) / 2;
2468
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
3416
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2469
3417
  }, "");
2470
- const area = `${path} L ${pts[pts.length - 1].x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
3418
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2471
3419
  return { pts, path, area };
2472
3420
  };
2473
- const lines = SERIES.map((s) => ({
3421
+ const lines = active.map((s) => ({
2474
3422
  ...s,
2475
3423
  ...smooth(data.map((d) => d[s.key]))
2476
3424
  }));
@@ -2493,17 +3441,17 @@ function AreaChart({
2493
3441
  /* @__PURE__ */ jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxs(
2494
3442
  "linearGradient",
2495
3443
  {
2496
- id: `ag-${s.color.replace("#", "")}`,
3444
+ id: `ag-${s.key}`,
2497
3445
  x1: "0",
2498
3446
  y1: "0",
2499
3447
  x2: "0",
2500
3448
  y2: "1",
2501
3449
  children: [
2502
- /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.13" }),
3450
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
2503
3451
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
2504
3452
  ]
2505
3453
  },
2506
- s.color
3454
+ s.key
2507
3455
  )) }),
2508
3456
  yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
2509
3457
  /* @__PURE__ */ jsx(
@@ -2523,7 +3471,7 @@ function AreaChart({
2523
3471
  x: PL - 6,
2524
3472
  y: t.y + 4,
2525
3473
  textAnchor: "end",
2526
- fill: "#b8b8c7",
3474
+ fill: T.textMuted,
2527
3475
  fontSize: "9",
2528
3476
  children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
2529
3477
  }
@@ -2540,25 +3488,18 @@ function AreaChart({
2540
3488
  strokeWidth: "1"
2541
3489
  }
2542
3490
  ),
2543
- lines.map((s) => /* @__PURE__ */ jsx(
2544
- "path",
2545
- {
2546
- d: s.area,
2547
- fill: `url(#ag-${s.color.replace("#", "")})`
2548
- },
2549
- `a-${s.color}`
2550
- )),
3491
+ lines.map((s) => /* @__PURE__ */ jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
2551
3492
  lines.map((s) => /* @__PURE__ */ jsx(
2552
3493
  "path",
2553
3494
  {
2554
3495
  d: s.path,
2555
3496
  stroke: s.color,
2556
- strokeWidth: "1.8",
3497
+ strokeWidth: "2",
2557
3498
  fill: "none",
2558
3499
  strokeLinecap: "round",
2559
3500
  strokeLinejoin: "round"
2560
3501
  },
2561
- `l-${s.color}`
3502
+ `line-${s.key}`
2562
3503
  )),
2563
3504
  data.map((d, i) => {
2564
3505
  if (i % step !== 0 && i !== data.length - 1) return null;
@@ -2568,7 +3509,7 @@ function AreaChart({
2568
3509
  x: xp(i),
2569
3510
  y: H - 6,
2570
3511
  textAnchor: "middle",
2571
- fill: "#b8b8c7",
3512
+ fill: T.textMuted,
2572
3513
  fontSize: "9",
2573
3514
  children: d.label
2574
3515
  },
@@ -2578,7 +3519,7 @@ function AreaChart({
2578
3519
  data.map((d, i) => {
2579
3520
  const isHov = hovered === i;
2580
3521
  return (
2581
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover hit area
3522
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
2582
3523
  /* @__PURE__ */ jsxs(
2583
3524
  "g",
2584
3525
  {
@@ -2603,10 +3544,9 @@ function AreaChart({
2603
3544
  y1: PT,
2604
3545
  x2: xp(i),
2605
3546
  y2: PT + CH,
2606
- stroke: "#32324d",
3547
+ stroke: "rgba(50,50,77,0.2)",
2607
3548
  strokeWidth: "1",
2608
- strokeDasharray: "3 3",
2609
- opacity: "0.35"
3549
+ strokeDasharray: "3 3"
2610
3550
  }
2611
3551
  ),
2612
3552
  lines.map((s) => /* @__PURE__ */ jsx(
@@ -2616,11 +3556,11 @@ function AreaChart({
2616
3556
  cy: yp(d[s.key]),
2617
3557
  r: isHov ? 4 : 2.5,
2618
3558
  fill: s.color,
2619
- stroke: "white",
3559
+ stroke: T.bg,
2620
3560
  strokeWidth: isHov ? 2 : 1,
2621
- opacity: isHov ? 1 : 0.45
3561
+ opacity: isHov ? 1 : 0.5
2622
3562
  },
2623
- s.color
3563
+ s.key
2624
3564
  ))
2625
3565
  ]
2626
3566
  },
@@ -2632,23 +3572,70 @@ function AreaChart({
2632
3572
  }
2633
3573
  );
2634
3574
  }
3575
+ function ProgressRing({
3576
+ value,
3577
+ max,
3578
+ color,
3579
+ size = 56
3580
+ }) {
3581
+ const r = (size - 9) / 2;
3582
+ const circ = 2 * Math.PI * r;
3583
+ const pct = Math.min(value / Math.max(max, 1), 1);
3584
+ const offset = circ * (1 - pct);
3585
+ return /* @__PURE__ */ jsxs(
3586
+ "svg",
3587
+ {
3588
+ width: size,
3589
+ height: size,
3590
+ style: { flexShrink: 0 },
3591
+ "aria-hidden": true,
3592
+ children: [
3593
+ /* @__PURE__ */ jsx(
3594
+ "circle",
3595
+ {
3596
+ cx: size / 2,
3597
+ cy: size / 2,
3598
+ r,
3599
+ fill: "none",
3600
+ stroke: "#e5e7eb",
3601
+ strokeWidth: "7"
3602
+ }
3603
+ ),
3604
+ /* @__PURE__ */ jsx(
3605
+ "circle",
3606
+ {
3607
+ cx: size / 2,
3608
+ cy: size / 2,
3609
+ r,
3610
+ fill: "none",
3611
+ stroke: color,
3612
+ strokeWidth: "7",
3613
+ strokeDasharray: `${circ} ${circ}`,
3614
+ strokeDashoffset: offset,
3615
+ strokeLinecap: "round",
3616
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
3617
+ style: {
3618
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
3619
+ }
3620
+ }
3621
+ )
3622
+ ]
3623
+ }
3624
+ );
3625
+ }
2635
3626
  function relTime(date) {
2636
- const d = typeof date === "string" ? new Date(date) : date;
2637
- const m = Math.floor((Date.now() - d.getTime()) / 6e4);
2638
- if (m < 1) return "just now";
2639
- if (m < 60) return `${m}m ago`;
2640
- const h = Math.floor(m / 60);
2641
- if (h < 24) return `${h}h ago`;
2642
- return `${Math.floor(h / 24)}d ago`;
3627
+ const diff = Date.now() - new Date(date).getTime();
3628
+ const mins = Math.floor(diff / 6e4);
3629
+ if (mins < 1) return "just now";
3630
+ if (mins < 60) return `${mins}m`;
3631
+ const hrs = Math.floor(mins / 60);
3632
+ if (hrs < 24) return `${hrs}h`;
3633
+ return `${Math.floor(hrs / 24)}d`;
2643
3634
  }
2644
- function fmtDate(date) {
2645
- return new Date(date).toLocaleDateString(void 0, {
2646
- month: "short",
2647
- day: "numeric",
2648
- year: "numeric"
2649
- });
3635
+ function rateHue(r) {
3636
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
2650
3637
  }
2651
- function StatCardItem({
3638
+ function StatItem({
2652
3639
  id,
2653
3640
  label,
2654
3641
  value,
@@ -2657,26 +3644,39 @@ function StatCardItem({
2657
3644
  color,
2658
3645
  delay = 0
2659
3646
  }) {
3647
+ const animated = useCountUp(value);
2660
3648
  const isPos = pct === void 0 || pct >= 0;
2661
- return /* @__PURE__ */ jsxs(Card, { $delay: delay, $accent: color, children: [
2662
- /* @__PURE__ */ jsxs(StatTop, { children: [
2663
- /* @__PURE__ */ jsx(StatLabel, { children: label }),
2664
- /* @__PURE__ */ jsx(StatValue, { children: typeof value === "number" ? value.toLocaleString() : value }),
2665
- pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
2666
- isPos ? "↑" : "↓",
2667
- " ",
2668
- Math.abs(pct).toFixed(1),
2669
- "%"
2670
- ] })
3649
+ return /* @__PURE__ */ jsxs(StatCard, { $delay: delay, $accent: color, children: [
3650
+ /* @__PURE__ */ jsx(StatLabel, { children: label }),
3651
+ /* @__PURE__ */ jsx(StatValue, { children: animated.toLocaleString() }),
3652
+ pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
3653
+ isPos ? "↑" : "↓",
3654
+ " ",
3655
+ Math.abs(pct).toFixed(1),
3656
+ "%"
2671
3657
  ] }),
2672
3658
  sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(SparkWrap, { children: /* @__PURE__ */ jsx(Sparkline, { data: sparkline, color, id }) })
2673
3659
  ] });
2674
3660
  }
2675
3661
  function OverviewPage() {
3662
+ const { get } = useFetchClient();
2676
3663
  const [period, setPeriod] = useState(
2677
3664
  "weekly"
2678
3665
  );
3666
+ const [feedMode, setFeedMode] = useState("signups");
2679
3667
  const [hovIdx, setHovIdx] = useState(null);
3668
+ const [rtnHov, setRtnHov] = useState(null);
3669
+ const [activeSeries, setActiveSeries] = useState(
3670
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
3671
+ );
3672
+ const toggleSeries = (key) => {
3673
+ setActiveSeries((prev) => {
3674
+ const next = new Set(prev);
3675
+ if (next.has(key) && next.size > 1) next.delete(key);
3676
+ else next.add(key);
3677
+ return next;
3678
+ });
3679
+ };
2680
3680
  const statsQuery = useQuery({
2681
3681
  queryKey: ["dash-user-stats"],
2682
3682
  queryFn: async () => {
@@ -2693,12 +3693,10 @@ function OverviewPage() {
2693
3693
  return r.data;
2694
3694
  }
2695
3695
  });
2696
- const sessionsQuery = useQuery({
2697
- queryKey: ["dash-recent-sessions"],
3696
+ const retentionQuery = useQuery({
3697
+ queryKey: ["dash-user-retention", period],
2698
3698
  queryFn: async () => {
2699
- const r = await client.dash.listAllSessions({
2700
- query: { page: 1, limit: 10 }
2701
- });
3699
+ const r = await client.dash.userRetentionData({ query: { period } });
2702
3700
  if (r.error) throw new Error(r.error.message ?? "Failed");
2703
3701
  return r.data;
2704
3702
  }
@@ -2707,12 +3705,22 @@ function OverviewPage() {
2707
3705
  queryKey: ["dash-recent-users"],
2708
3706
  queryFn: async () => {
2709
3707
  const r = await client.dash.listUsers({
2710
- query: { page: 1, limit: 8, search: "" }
3708
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
2711
3709
  });
2712
3710
  if (r.error) throw new Error(r.error.message ?? "Failed");
2713
3711
  return r.data;
2714
3712
  }
2715
3713
  });
3714
+ const sessionsQuery = useQuery({
3715
+ queryKey: ["dash-recent-sessions"],
3716
+ queryFn: async () => {
3717
+ const { data } = await get(
3718
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=updatedAt:desc"
3719
+ );
3720
+ return data.results ?? [];
3721
+ },
3722
+ refetchInterval: 3e4
3723
+ });
2716
3724
  const orgsQuery = useQuery({
2717
3725
  queryKey: ["dash-orgs-count"],
2718
3726
  queryFn: async () => {
@@ -2723,596 +3731,403 @@ function OverviewPage() {
2723
3731
  return r.data;
2724
3732
  }
2725
3733
  });
3734
+ const sessionsRaw = sessionsQuery.data ?? [];
3735
+ const activeUserIds = [];
3736
+ const lastActiveByUserId = /* @__PURE__ */ new Map();
3737
+ for (const s of sessionsRaw) {
3738
+ const uid = String(s.userId);
3739
+ if (!lastActiveByUserId.has(uid)) {
3740
+ lastActiveByUserId.set(uid, s.updatedAt ?? s.createdAt);
3741
+ activeUserIds.push(uid);
3742
+ }
3743
+ }
3744
+ const activeUsersQuery = useQuery({
3745
+ queryKey: ["dash-active-users", activeUserIds],
3746
+ queryFn: async () => {
3747
+ if (activeUserIds.length === 0) return [];
3748
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
3749
+ for (let i = 0; i < activeUserIds.length; i++) {
3750
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
3751
+ }
3752
+ params.set("pagination[pageSize]", "12");
3753
+ const { data } = await get(
3754
+ `/better-auth-dashboard/db?${params}`
3755
+ );
3756
+ return data.results ?? [];
3757
+ },
3758
+ enabled: feedMode === "active" && activeUserIds.length > 0
3759
+ });
3760
+ const chartRef = useRef(null);
3761
+ const [chartHeight, setChartHeight] = useState(0);
3762
+ useEffect(() => {
3763
+ const el = chartRef.current;
3764
+ if (!el) return;
3765
+ const ro = new ResizeObserver((entries) => {
3766
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
3767
+ });
3768
+ ro.observe(el);
3769
+ return () => ro.disconnect();
3770
+ }, []);
2726
3771
  if (statsQuery.isLoading) {
2727
- return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) });
3772
+ return /* @__PURE__ */ jsx(
3773
+ Flex,
3774
+ {
3775
+ justifyContent: "center",
3776
+ alignItems: "center",
3777
+ padding: 12,
3778
+ style: { background: T.bg, minHeight: "100%" },
3779
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
3780
+ }
3781
+ );
2728
3782
  }
2729
3783
  const stats = statsQuery.data;
2730
3784
  if (!stats) return null;
2731
3785
  const graphData = graphQuery.data?.data ?? [];
2732
- const sessions = sessionsQuery.data?.sessions ?? [];
2733
- const recentUsers = usersQuery.data?.users ?? [];
3786
+ const rtnData = retentionQuery.data?.data ?? [];
3787
+ const users = usersQuery.data?.users ?? [];
2734
3788
  const orgCount = orgsQuery.data?.total ?? 0;
3789
+ const activeUserMap = /* @__PURE__ */ new Map();
3790
+ for (const u of activeUsersQuery.data ?? []) {
3791
+ activeUserMap.set(String(u.id), u);
3792
+ }
3793
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
2735
3794
  const totalSpark = graphData.map((d) => d.totalUsers);
2736
3795
  const newSpark = graphData.map((d) => d.newUsers);
2737
3796
  const activeSpark = graphData.map((d) => d.activeUsers);
2738
3797
  const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
2739
- return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "overview-page", children: [
2740
- /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
2741
- /* @__PURE__ */ jsxs(Box, { children: [
2742
- /* @__PURE__ */ jsx(Typography, { variant: "alpha", textColor: "neutral800", children: "Overview" }),
2743
- /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
2744
- weekday: "long",
2745
- year: "numeric",
2746
- month: "long",
2747
- day: "numeric"
2748
- }) }) })
2749
- ] }),
2750
- /* @__PURE__ */ jsx(Box, { width: "150px", children: /* @__PURE__ */ jsxs(
2751
- SingleSelect,
2752
- {
2753
- value: period,
2754
- onChange: (v) => setPeriod(v),
2755
- size: "S",
2756
- "aria-label": "Select period",
2757
- children: [
2758
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "daily", children: "Daily" }),
2759
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "weekly", children: "Weekly" }),
2760
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "monthly", children: "Monthly" })
2761
- ]
2762
- }
2763
- ) })
2764
- ] }),
2765
- /* @__PURE__ */ jsxs(Divider, { children: [
2766
- /* @__PURE__ */ jsx(DivLabel, { children: "Sign-ups" }),
2767
- /* @__PURE__ */ jsx(DivLine, {})
2768
- ] }),
2769
- /* @__PURE__ */ jsxs(StatRow, { children: [
2770
- /* @__PURE__ */ jsx(
2771
- StatCardItem,
2772
- {
2773
- id: "total",
2774
- label: "Total Users",
2775
- value: stats.total ?? 0,
2776
- sparkline: totalSpark,
2777
- color: "#4945FF",
2778
- delay: 0
2779
- }
2780
- ),
2781
- /* @__PURE__ */ jsx(
2782
- StatCardItem,
2783
- {
2784
- id: "d-sig",
2785
- label: "Daily Sign-ups",
2786
- value: stats.daily.signUps ?? 0,
2787
- pct: stats.daily.percentage ?? void 0,
2788
- sparkline: newSpark,
2789
- color: "#5CB176",
2790
- delay: 1
2791
- }
2792
- ),
2793
- /* @__PURE__ */ jsx(
2794
- StatCardItem,
2795
- {
2796
- id: "w-sig",
2797
- label: "Weekly Sign-ups",
2798
- value: stats.weekly.signUps ?? 0,
2799
- pct: stats.weekly.percentage ?? void 0,
2800
- sparkline: newSpark,
2801
- color: "#5CB176",
2802
- delay: 2
2803
- }
2804
- ),
2805
- /* @__PURE__ */ jsx(
2806
- StatCardItem,
2807
- {
2808
- id: "m-sig",
2809
- label: "Monthly Sign-ups",
2810
- value: stats.monthly.signUps ?? 0,
2811
- pct: stats.monthly.percentage ?? void 0,
2812
- sparkline: newSpark,
2813
- color: "#5CB176",
2814
- delay: 3
2815
- }
3798
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
3799
+ const activeMax = Math.max(
3800
+ stats.activeUsers.daily.active ?? 0,
3801
+ stats.activeUsers.weekly.active ?? 0,
3802
+ stats.activeUsers.monthly.active ?? 0,
3803
+ 1
3804
+ );
3805
+ return /* @__PURE__ */ jsx(
3806
+ Page.Protect,
3807
+ {
3808
+ permissions: PERMISSIONS.overview.filter(
3809
+ (p) => p.action === `plugin::${PLUGIN_ID}.overview.read`
2816
3810
  ),
2817
- /* @__PURE__ */ jsx(
2818
- StatCardItem,
2819
- {
2820
- id: "orgs",
2821
- label: "Organizations",
2822
- value: orgCount,
2823
- color: "#9E6BF9",
2824
- delay: 4
2825
- }
2826
- )
2827
- ] }),
2828
- /* @__PURE__ */ jsxs(MainRow, { children: [
2829
- /* @__PURE__ */ jsxs(ChartCard, { $delay: 5, children: [
2830
- /* @__PURE__ */ jsxs(ChartHeader, { children: [
2831
- /* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
2832
- /* @__PURE__ */ jsx(LegendRow, { children: SERIES.map((s) => /* @__PURE__ */ jsxs(LegendItem, { children: [
2833
- /* @__PURE__ */ jsx(LegendDot, { $c: s.color }),
2834
- s.label
2835
- ] }, s.color)) })
3811
+ children: /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "overview-page", children: [
3812
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
3813
+ /* @__PURE__ */ jsxs("div", { children: [
3814
+ /* @__PURE__ */ jsx(
3815
+ "div",
3816
+ {
3817
+ style: {
3818
+ fontSize: 22,
3819
+ fontWeight: 800,
3820
+ color: T.textPrimary,
3821
+ letterSpacing: "-0.03em"
3822
+ },
3823
+ children: "Overview"
3824
+ }
3825
+ ),
3826
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
3827
+ weekday: "long",
3828
+ year: "numeric",
3829
+ month: "long",
3830
+ day: "numeric"
3831
+ }) })
3832
+ ] }),
3833
+ /* @__PURE__ */ jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
2836
3834
  ] }),
2837
- /* @__PURE__ */ jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
2838
- graphQuery.isLoading ? /* @__PURE__ */ jsx(
2839
- Flex,
2840
- {
2841
- justifyContent: "center",
2842
- alignItems: "center",
2843
- style: { flex: 1, minHeight: 180 },
2844
- children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
2845
- }
2846
- ) : /* @__PURE__ */ jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
2847
- ] }),
2848
- /* @__PURE__ */ jsxs(FeedCard, { $delay: 6, children: [
2849
- /* @__PURE__ */ jsx(FeedHeader, { children: "Recent Sessions" }),
2850
- /* @__PURE__ */ jsx(FeedList, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent sessions" }) : sessions.map((s) => /* @__PURE__ */ jsxs(FeedItem, { children: [
3835
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3836
+ /* @__PURE__ */ jsx(DivLabel, { children: "Metrics" }),
3837
+ /* @__PURE__ */ jsx(DivLine, {})
3838
+ ] }),
3839
+ /* @__PURE__ */ jsxs(StatGrid, { children: [
2851
3840
  /* @__PURE__ */ jsx(
2852
- Avatar,
3841
+ StatItem,
2853
3842
  {
2854
- name: s.user?.name ?? "?",
2855
- src: s.user?.image,
2856
- size: 28
3843
+ id: "total",
3844
+ label: "Total Users",
3845
+ value: stats.total ?? 0,
3846
+ sparkline: totalSpark,
3847
+ color: T.accent,
3848
+ delay: 0
2857
3849
  }
2858
- ),
2859
- /* @__PURE__ */ jsxs(FeedContent, { children: [
2860
- /* @__PURE__ */ jsx(FeedName, { children: s.user?.name ?? "Unknown" }),
2861
- /* @__PURE__ */ jsx(FeedSub, { children: s.user?.email ?? "" }),
2862
- /* @__PURE__ */ jsx(FeedTime, { children: relTime(s.createdAt) })
2863
- ] })
2864
- ] }, s.id)) })
2865
- ] })
2866
- ] }),
2867
- /* @__PURE__ */ jsxs(Divider, { children: [
2868
- /* @__PURE__ */ jsx(DivLabel, { children: "Active Users" }),
2869
- /* @__PURE__ */ jsx(DivLine, {})
2870
- ] }),
2871
- /* @__PURE__ */ jsxs(ActiveRow, { children: [
2872
- /* @__PURE__ */ jsx(
2873
- StatCardItem,
2874
- {
2875
- id: "d-act",
2876
- label: "Daily Active",
2877
- value: stats.activeUsers.daily.active ?? 0,
2878
- pct: stats.activeUsers.daily.percentage ?? void 0,
2879
- sparkline: activeSpark,
2880
- color: "#E57553",
2881
- delay: 7
2882
- }
2883
- ),
2884
- /* @__PURE__ */ jsx(
2885
- StatCardItem,
2886
- {
2887
- id: "w-act",
2888
- label: "Weekly Active",
2889
- value: stats.activeUsers.weekly.active ?? 0,
2890
- pct: stats.activeUsers.weekly.percentage ?? void 0,
2891
- sparkline: activeSpark,
2892
- color: "#E57553",
2893
- delay: 8
2894
- }
2895
- ),
2896
- /* @__PURE__ */ jsx(
2897
- StatCardItem,
2898
- {
2899
- id: "m-act",
2900
- label: "Monthly Active",
2901
- value: stats.activeUsers.monthly.active ?? 0,
2902
- pct: stats.activeUsers.monthly.percentage ?? void 0,
2903
- sparkline: activeSpark,
2904
- color: "#E57553",
2905
- delay: 9
2906
- }
2907
- )
2908
- ] }),
2909
- /* @__PURE__ */ jsxs(Divider, { children: [
2910
- /* @__PURE__ */ jsx(DivLabel, { children: "Recent Users" }),
2911
- /* @__PURE__ */ jsx(DivLine, {})
2912
- ] }),
2913
- /* @__PURE__ */ jsx(UsersCard, { $delay: 10, children: /* @__PURE__ */ jsx(TableWrap, { children: /* @__PURE__ */ jsxs(Table$1, { children: [
2914
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: ["User", "Email", "Joined", "Status"].map((h) => /* @__PURE__ */ jsx(TH$1, { children: h }, h)) }) }),
2915
- /* @__PURE__ */ jsx("tbody", { children: usersQuery.isLoading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(TD$1, { colSpan: 4, style: { textAlign: "center", padding: 28 }, children: /* @__PURE__ */ jsx(Loader, { children: "Loading users…" }) }) }) : recentUsers.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2916
- TD$1,
2917
- {
2918
- colSpan: 4,
2919
- style: {
2920
- textAlign: "center",
2921
- padding: 28,
2922
- color: "#8e8ea9"
2923
- },
2924
- children: "No users yet"
2925
- }
2926
- ) }) : recentUsers.map((u) => /* @__PURE__ */ jsxs(TR$1, { children: [
2927
- /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
2928
- /* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 26 }),
2929
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 12 }, children: u.name })
2930
- ] }) }),
2931
- /* @__PURE__ */ jsx(
2932
- TD$1,
2933
- {
2934
- style: {
2935
- color: "#8e8ea9",
2936
- fontFamily: "monospace",
2937
- fontSize: 11
2938
- },
2939
- children: u.email
2940
- }
2941
- ),
2942
- /* @__PURE__ */ jsx(
2943
- TD$1,
2944
- {
2945
- style: {
2946
- color: "#8e8ea9",
2947
- fontSize: 11,
2948
- whiteSpace: "nowrap"
2949
- },
2950
- children: fmtDate(u.createdAt)
2951
- }
2952
- ),
2953
- /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(
2954
- StatusChip$1,
2955
- {
2956
- $verified: u.emailVerified,
2957
- $banned: u.banned,
2958
- children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
2959
- }
2960
- ) })
2961
- ] }, u.id)) })
2962
- ] }) }) })
2963
- ] });
2964
- }
2965
- const PAGE_SIZE$1 = 25;
2966
- const fadeUp$1 = keyframes`
2967
- from { opacity: 0; transform: translateY(6px); }
2968
- to { opacity: 1; transform: translateY(0); }
2969
- `;
2970
- const Wrap$1 = styled.div`
2971
- padding: 28px 32px;
2972
- background: #f6f6f9;
2973
- min-height: 100%;
2974
- display: flex;
2975
- flex-direction: column;
2976
- gap: 20px;
2977
- `;
2978
- const PageHeader$1 = styled.div`
2979
- display: flex;
2980
- justify-content: space-between;
2981
- align-items: flex-start;
2982
- `;
2983
- const TitleBlock$1 = styled.div`
2984
- display: flex;
2985
- flex-direction: column;
2986
- gap: 4px;
2987
- `;
2988
- const PageTitle$1 = styled.h1`
2989
- margin: 0;
2990
- font-size: 22px;
2991
- font-weight: 800;
2992
- color: #32324d;
2993
- letter-spacing: -0.03em;
2994
- `;
2995
- const PageSubtitle$1 = styled.p`
2996
- margin: 0;
2997
- font-size: 12px;
2998
- color: #8e8ea9;
2999
- `;
3000
- const TableCard$1 = styled.div`
3001
- background: #ffffff;
3002
- border: 1px solid #eaeaef;
3003
- border-radius: 10px;
3004
- overflow: hidden;
3005
- `;
3006
- const ColumnHeader = styled.div`
3007
- padding: 10px 20px;
3008
- display: flex;
3009
- align-items: center;
3010
- gap: 12px;
3011
- background: #fafafa;
3012
- border-bottom: 1px solid #eaeaef;
3013
- font-size: 10px;
3014
- font-weight: 700;
3015
- text-transform: uppercase;
3016
- letter-spacing: 0.08em;
3017
- color: #8e8ea9;
3018
- `;
3019
- const ColumnHeaderRight = styled.div`
3020
- margin-left: auto;
3021
- font-size: 10px;
3022
- font-weight: 700;
3023
- text-transform: uppercase;
3024
- letter-spacing: 0.08em;
3025
- color: #8e8ea9;
3026
- `;
3027
- const UserGroup = styled.div`
3028
- border-bottom: 1px solid #eaeaef;
3029
- animation: ${fadeUp$1} 280ms ease both;
3030
- animation-delay: ${(p) => (p.$i ?? 0) * 30}ms;
3031
- &:last-child { border-bottom: none; }
3032
- `;
3033
- const UserRowHeader = styled.div`
3034
- padding: 14px 20px;
3035
- display: flex;
3036
- align-items: center;
3037
- gap: 12px;
3038
- border-bottom: 1px solid #f5f5f9;
3039
- cursor: default;
3040
- &:hover { background: #fafafe; }
3041
- `;
3042
- const UserInfo = styled.div`
3043
- flex: 1;
3044
- display: flex;
3045
- flex-direction: column;
3046
- gap: 2px;
3047
- `;
3048
- const UserName$1 = styled.span`
3049
- font-size: 12px;
3050
- font-weight: 600;
3051
- color: #32324d;
3052
- `;
3053
- const UserEmail = styled.span`
3054
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3055
- font-size: 11px;
3056
- color: #8e8ea9;
3057
- `;
3058
- const SessionCountChip = styled.span`
3059
- display: inline-flex;
3060
- align-items: center;
3061
- padding: 2px 8px;
3062
- border-radius: 20px;
3063
- font-size: 10px;
3064
- font-weight: 700;
3065
- background: #f0f0ff;
3066
- color: #4945ff;
3067
- `;
3068
- const SessionCard = styled.div`
3069
- padding: 10px 20px 10px 64px;
3070
- border-top: 1px solid #f5f5f9;
3071
- background: #fafafa;
3072
- display: flex;
3073
- align-items: flex-start;
3074
- justify-content: space-between;
3075
- gap: 16px;
3076
- &:hover { background: #f5f5ff; }
3077
- `;
3078
- const SessionMeta = styled.div`
3079
- display: flex;
3080
- flex-direction: column;
3081
- gap: 3px;
3082
- flex: 1;
3083
- min-width: 0;
3084
- `;
3085
- const IpChip = styled.span`
3086
- display: inline-block;
3087
- background: #f0f0ff;
3088
- color: #4945ff;
3089
- border-radius: 5px;
3090
- padding: 2px 6px;
3091
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3092
- font-size: 11px;
3093
- font-weight: 600;
3094
- `;
3095
- const TimestampText = styled.span`
3096
- font-size: 11px;
3097
- color: #8e8ea9;
3098
- font-variant-numeric: tabular-nums;
3099
- `;
3100
- const AgentText = styled.span`
3101
- font-size: 11px;
3102
- color: #b8b8c7;
3103
- overflow: hidden;
3104
- text-overflow: ellipsis;
3105
- white-space: nowrap;
3106
- display: block;
3107
- max-width: 400px;
3108
- `;
3109
- const EmptyState = styled.div`
3110
- display: flex;
3111
- justify-content: center;
3112
- align-items: center;
3113
- padding: 48px;
3114
- font-size: 12px;
3115
- color: #8e8ea9;
3116
- `;
3117
- function SessionsPage() {
3118
- const qc = useQueryClient();
3119
- const [page, setPage] = useState(1);
3120
- const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
3121
- const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
3122
- const [confirmRevokeMany, setConfirmRevokeMany] = useState(false);
3123
- const sessionsQuery = useQuery({
3124
- queryKey: ["dash-all-sessions", page],
3125
- queryFn: async () => {
3126
- const result = await client.dash.listAllSessions({});
3127
- if (result.error)
3128
- throw new Error(result.error.message ?? "Failed to load sessions");
3129
- return result.data ?? [];
3130
- },
3131
- keepPreviousData: true
3132
- });
3133
- const revokeSessionMutation = useMutation({
3134
- mutationFn: async (sessionId) => {
3135
- const result = await client.dash.sessions.revoke(
3136
- {},
3137
- withContext({ sessionId })
3138
- );
3139
- if (result.error)
3140
- throw new Error(result.error.message ?? "Revoke failed");
3141
- },
3142
- onSuccess: () => {
3143
- setConfirmRevokeSessionId(null);
3144
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3145
- }
3146
- });
3147
- const revokeManyMutation = useMutation({
3148
- mutationFn: async (userIds) => {
3149
- const result = await client.dash.sessions.revokeMany(
3150
- {},
3151
- withContext({ userIds })
3152
- );
3153
- if (result.error)
3154
- throw new Error(result.error.message ?? "Revoke failed");
3155
- return result.data;
3156
- },
3157
- onSuccess: () => {
3158
- setConfirmRevokeMany(false);
3159
- setSelected(/* @__PURE__ */ new Set());
3160
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3161
- }
3162
- });
3163
- const usersWithSessions = sessionsQuery.data ?? [];
3164
- const allUserIds = usersWithSessions.map((u) => u.id);
3165
- const allSelected = allUserIds.length > 0 && allUserIds.every((id) => selected.has(id));
3166
- const someSelected = selected.size > 0;
3167
- const toggleSelect = (id) => {
3168
- setSelected((prev) => {
3169
- const next = new Set(prev);
3170
- if (next.has(id)) next.delete(id);
3171
- else next.add(id);
3172
- return next;
3173
- });
3174
- };
3175
- const handleSelectAll = () => {
3176
- if (allSelected) setSelected(/* @__PURE__ */ new Set());
3177
- else setSelected(new Set(allUserIds));
3178
- };
3179
- return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "sessions-page", children: [
3180
- /* @__PURE__ */ jsxs(PageHeader$1, { children: [
3181
- /* @__PURE__ */ jsxs(TitleBlock$1, { children: [
3182
- /* @__PURE__ */ jsx(PageTitle$1, { children: "Sessions" }),
3183
- /* @__PURE__ */ jsx(PageSubtitle$1, { children: usersWithSessions.length > 0 ? `${usersWithSessions.length} user${usersWithSessions.length !== 1 ? "s" : ""} with active sessions` : "Active sessions across all users" })
3184
- ] }),
3185
- someSelected && /* @__PURE__ */ jsxs(
3186
- Button,
3187
- {
3188
- variant: "danger-light",
3189
- size: "S",
3190
- onClick: () => setConfirmRevokeMany(true),
3191
- "data-testid": "revoke-selected-btn",
3192
- children: [
3193
- "Revoke sessions for ",
3194
- selected.size,
3195
- " user",
3196
- selected.size !== 1 ? "s" : ""
3197
- ]
3198
- }
3199
- )
3200
- ] }),
3201
- sessionsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: sessionsQuery.error instanceof Error ? sessionsQuery.error.message : "An error occurred" }),
3202
- /* @__PURE__ */ jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3203
- /* @__PURE__ */ jsxs(ColumnHeader, { children: [
3204
- /* @__PURE__ */ jsx(
3205
- Checkbox,
3206
- {
3207
- checked: allSelected,
3208
- onCheckedChange: handleSelectAll,
3209
- "aria-label": "Select all users"
3210
- }
3211
- ),
3212
- /* @__PURE__ */ jsx("span", { children: "User / Sessions" }),
3213
- /* @__PURE__ */ jsx(ColumnHeaderRight, { children: "Sessions" })
3214
- ] }),
3215
- usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
3216
- /* @__PURE__ */ jsxs(UserRowHeader, { children: [
3850
+ ),
3217
3851
  /* @__PURE__ */ jsx(
3218
- Checkbox,
3852
+ StatItem,
3219
3853
  {
3220
- checked: selected.has(userRow.id),
3221
- onCheckedChange: () => toggleSelect(userRow.id),
3222
- "aria-label": `Select ${userRow.name}`
3854
+ id: "d-sig",
3855
+ label: "Daily Sign-ups",
3856
+ value: stats.daily.signUps ?? 0,
3857
+ pct: stats.daily.percentage ?? void 0,
3858
+ sparkline: newSpark,
3859
+ color: T.green,
3860
+ delay: 1
3223
3861
  }
3224
3862
  ),
3225
- /* @__PURE__ */ jsx(Avatar, { name: userRow.name ?? "", src: null, size: 30 }),
3226
- /* @__PURE__ */ jsxs(UserInfo, { children: [
3227
- /* @__PURE__ */ jsx(UserName$1, { children: userRow.name }),
3228
- /* @__PURE__ */ jsx(UserEmail, { children: userRow.email })
3229
- ] }),
3230
- /* @__PURE__ */ jsxs(SessionCountChip, { children: [
3231
- userRow.sessions.length,
3232
- " session",
3233
- userRow.sessions.length !== 1 ? "s" : ""
3234
- ] })
3863
+ /* @__PURE__ */ jsx(
3864
+ StatItem,
3865
+ {
3866
+ id: "w-sig",
3867
+ label: "Weekly Sign-ups",
3868
+ value: stats.weekly.signUps ?? 0,
3869
+ pct: stats.weekly.percentage ?? void 0,
3870
+ sparkline: newSpark,
3871
+ color: T.green,
3872
+ delay: 2
3873
+ }
3874
+ ),
3875
+ /* @__PURE__ */ jsx(
3876
+ StatItem,
3877
+ {
3878
+ id: "m-sig",
3879
+ label: "Monthly Sign-ups",
3880
+ value: stats.monthly.signUps ?? 0,
3881
+ pct: stats.monthly.percentage ?? void 0,
3882
+ sparkline: newSpark,
3883
+ color: T.green,
3884
+ delay: 3
3885
+ }
3886
+ ),
3887
+ /* @__PURE__ */ jsx(
3888
+ StatItem,
3889
+ {
3890
+ id: "orgs",
3891
+ label: "Organizations",
3892
+ value: orgCount,
3893
+ color: T.purple,
3894
+ delay: 4
3895
+ }
3896
+ )
3235
3897
  ] }),
3236
- userRow.sessions.map((session) => /* @__PURE__ */ jsxs(SessionCard, { "data-testid": "session-row", children: [
3237
- /* @__PURE__ */ jsxs(SessionMeta, { children: [
3238
- /* @__PURE__ */ jsxs(
3898
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3899
+ /* @__PURE__ */ jsx(DivLabel, { children: "Growth" }),
3900
+ /* @__PURE__ */ jsx(DivLine, {})
3901
+ ] }),
3902
+ /* @__PURE__ */ jsxs(TwoPanel, { children: [
3903
+ /* @__PURE__ */ jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
3904
+ /* @__PURE__ */ jsxs(ChartHeader, { children: [
3905
+ /* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
3906
+ /* @__PURE__ */ jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxs(
3907
+ SeriesBtn,
3908
+ {
3909
+ $on: activeSeries.has(s.key),
3910
+ $c: s.color,
3911
+ onClick: () => toggleSeries(s.key),
3912
+ children: [
3913
+ /* @__PURE__ */ jsx(SDot, { $c: s.color }),
3914
+ s.label
3915
+ ]
3916
+ },
3917
+ s.key
3918
+ )) })
3919
+ ] }),
3920
+ /* @__PURE__ */ jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
3921
+ graphQuery.isLoading ? /* @__PURE__ */ jsx(
3239
3922
  Flex,
3240
3923
  {
3241
- gap: 2,
3924
+ justifyContent: "center",
3242
3925
  alignItems: "center",
3243
- style: { flexWrap: "wrap" },
3244
- children: [
3245
- session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
3246
- /* @__PURE__ */ jsxs(TimestampText, { children: [
3247
- "Created ",
3248
- new Date(session.createdAt).toLocaleString(),
3249
- " ",
3250
- "· Expires",
3251
- " ",
3252
- new Date(session.expiresAt).toLocaleString()
3253
- ] })
3254
- ]
3926
+ style: { flex: 1, minHeight: 170 },
3927
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
3255
3928
  }
3256
- ),
3257
- session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
3929
+ ) : /* @__PURE__ */ jsx(
3930
+ GrowthChart,
3931
+ {
3932
+ data: graphData,
3933
+ hovered: hovIdx,
3934
+ onHover: setHovIdx,
3935
+ activeSeries
3936
+ }
3937
+ )
3258
3938
  ] }),
3259
- /* @__PURE__ */ jsx(
3260
- IconButton,
3939
+ /* @__PURE__ */ jsxs(
3940
+ FeedCard,
3261
3941
  {
3262
- label: "Revoke session",
3263
- onClick: () => setConfirmRevokeSessionId(session.id),
3264
- style: { flexShrink: 0 },
3265
- "data-testid": "revoke-session-btn",
3266
- children: /* @__PURE__ */ jsx(Trash, {})
3942
+ $delay: 6,
3943
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
3944
+ children: [
3945
+ /* @__PURE__ */ jsxs(FeedHead, { children: [
3946
+ /* @__PURE__ */ jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
3947
+ /* @__PURE__ */ jsxs(
3948
+ FeedSelect,
3949
+ {
3950
+ value: feedMode,
3951
+ onChange: (e) => setFeedMode(e.target.value),
3952
+ children: [
3953
+ /* @__PURE__ */ jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3954
+ /* @__PURE__ */ jsx("option", { value: "active", children: "Recently Active" })
3955
+ ]
3956
+ }
3957
+ )
3958
+ ] }),
3959
+ /* @__PURE__ */ jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
3960
+ /* @__PURE__ */ jsxs(FeedTop, { children: [
3961
+ /* @__PURE__ */ jsxs(
3962
+ Flex,
3963
+ {
3964
+ alignItems: "center",
3965
+ gap: 1,
3966
+ style: { minWidth: 0 },
3967
+ children: [
3968
+ /* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3969
+ /* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
3970
+ ]
3971
+ }
3972
+ ),
3973
+ /* @__PURE__ */ jsx(FeedMeta, { children: relTime(u.createdAt) })
3974
+ ] }),
3975
+ /* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
3976
+ ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
3977
+ /* @__PURE__ */ jsxs(FeedTop, { children: [
3978
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3979
+ /* @__PURE__ */ jsx(
3980
+ Avatar,
3981
+ {
3982
+ name: u.name,
3983
+ src: u.image ?? void 0,
3984
+ size: 20
3985
+ }
3986
+ ),
3987
+ /* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
3988
+ ] }),
3989
+ /* @__PURE__ */ jsx(FeedMeta, { children: relTime(
3990
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
3991
+ ) })
3992
+ ] }),
3993
+ /* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
3994
+ ] }, u.documentId)) })
3995
+ ]
3267
3996
  }
3268
3997
  )
3269
- ] }, session.id))
3270
- ] }, userRow.id))
3271
- ] }) }),
3272
- usersWithSessions.length === PAGE_SIZE$1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
3273
- /* @__PURE__ */ jsx(
3274
- Button,
3275
- {
3276
- variant: "tertiary",
3277
- size: "S",
3278
- disabled: page === 1,
3279
- onClick: () => setPage((p) => p - 1),
3280
- children: "Previous"
3281
- }
3282
- ),
3283
- /* @__PURE__ */ jsx(
3284
- Button,
3285
- {
3286
- variant: "tertiary",
3287
- size: "S",
3288
- onClick: () => setPage((p) => p + 1),
3289
- children: "Next"
3290
- }
3291
- )
3292
- ] }) }),
3293
- confirmRevokeSessionId && /* @__PURE__ */ jsx(
3294
- ConfirmDialog,
3295
- {
3296
- title: "Revoke session",
3297
- message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
3298
- confirmLabel: "Revoke",
3299
- loading: revokeSessionMutation.isLoading,
3300
- onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
3301
- onCancel: () => setConfirmRevokeSessionId(null)
3302
- }
3303
- ),
3304
- confirmRevokeMany && /* @__PURE__ */ jsx(
3305
- ConfirmDialog,
3306
- {
3307
- title: `Revoke sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
3308
- message: `Are you sure you want to revoke all sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be signed out on all their devices.`,
3309
- confirmLabel: "Revoke all",
3310
- loading: revokeManyMutation.isLoading,
3311
- onConfirm: () => revokeManyMutation.mutate([...selected]),
3312
- onCancel: () => setConfirmRevokeMany(false)
3313
- }
3314
- )
3315
- ] });
3998
+ ] }),
3999
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
4000
+ /* @__PURE__ */ jsx(DivLabel, { children: "Retention & Activity" }),
4001
+ /* @__PURE__ */ jsx(DivLine, {})
4002
+ ] }),
4003
+ /* @__PURE__ */ jsxs(TwoPanel, { children: [
4004
+ /* @__PURE__ */ jsxs(RtnCard, { $delay: 7, children: [
4005
+ /* @__PURE__ */ jsxs(ChartHeader, { children: [
4006
+ /* @__PURE__ */ jsx(ChartTitle, { children: "Cohort Retention" }),
4007
+ /* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 2, children: [
4008
+ { hue: 142, label: "≥70%" },
4009
+ { hue: 38, label: "40–70%" },
4010
+ { hue: 4, label: "<40%" }
4011
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxs(
4012
+ Flex,
4013
+ {
4014
+ alignItems: "center",
4015
+ gap: 1,
4016
+ style: { marginLeft: 8 },
4017
+ children: [
4018
+ /* @__PURE__ */ jsx(
4019
+ "span",
4020
+ {
4021
+ style: {
4022
+ width: 8,
4023
+ height: 8,
4024
+ borderRadius: "50%",
4025
+ background: `hsl(${hue},60%,50%)`,
4026
+ display: "inline-block",
4027
+ flexShrink: 0
4028
+ }
4029
+ }
4030
+ ),
4031
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
4032
+ ]
4033
+ },
4034
+ hue
4035
+ )) })
4036
+ ] }),
4037
+ /* @__PURE__ */ jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
4038
+ retentionQuery.isLoading ? /* @__PURE__ */ jsx(
4039
+ Flex,
4040
+ {
4041
+ justifyContent: "center",
4042
+ alignItems: "center",
4043
+ style: { minHeight: 80 },
4044
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
4045
+ }
4046
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxs(Empty, { children: [
4047
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 22 }, children: "📉" }),
4048
+ /* @__PURE__ */ jsx("div", { children: "No retention data for this period" })
4049
+ ] }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
4050
+ const hue = rateHue(row.retentionRate);
4051
+ return /* @__PURE__ */ jsxs(
4052
+ RtnRow,
4053
+ {
4054
+ $hov: rtnHov === i,
4055
+ onMouseEnter: () => setRtnHov(i),
4056
+ onMouseLeave: () => setRtnHov(null),
4057
+ children: [
4058
+ /* @__PURE__ */ jsx(RtnLabel, { children: row.label }),
4059
+ /* @__PURE__ */ jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
4060
+ /* @__PURE__ */ jsx(RtnTrack, { children: /* @__PURE__ */ jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
4061
+ /* @__PURE__ */ jsxs(RtnPct, { $hue: hue, children: [
4062
+ row.retentionRate.toFixed(1),
4063
+ "%"
4064
+ ] })
4065
+ ]
4066
+ },
4067
+ row.n
4068
+ );
4069
+ }) })
4070
+ ] }),
4071
+ /* @__PURE__ */ jsx(RingGrid, { children: [
4072
+ {
4073
+ label: "Daily Active",
4074
+ value: stats.activeUsers.daily.active,
4075
+ pct: stats.activeUsers.daily.percentage,
4076
+ color: T.amber,
4077
+ sparkline: activeSpark,
4078
+ delay: 8
4079
+ },
4080
+ {
4081
+ label: "Weekly Active",
4082
+ value: stats.activeUsers.weekly.active,
4083
+ pct: stats.activeUsers.weekly.percentage,
4084
+ color: T.green,
4085
+ sparkline: activeSpark,
4086
+ delay: 9
4087
+ },
4088
+ {
4089
+ label: "Monthly Active",
4090
+ value: stats.activeUsers.monthly.active,
4091
+ pct: stats.activeUsers.monthly.percentage,
4092
+ color: T.accent,
4093
+ sparkline: activeSpark,
4094
+ delay: 10
4095
+ }
4096
+ ].map(({ label, value, pct, color, delay }) => {
4097
+ const isPos = pct == null || pct >= 0;
4098
+ return /* @__PURE__ */ jsxs(RingCard, { $delay: delay, children: [
4099
+ /* @__PURE__ */ jsx(
4100
+ ProgressRing,
4101
+ {
4102
+ value: value ?? 0,
4103
+ max: activeMax,
4104
+ color,
4105
+ size: 56
4106
+ }
4107
+ ),
4108
+ /* @__PURE__ */ jsxs(RingInfo, { children: [
4109
+ /* @__PURE__ */ jsx(RingLabel, { children: label }),
4110
+ /* @__PURE__ */ jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
4111
+ pct != null && /* @__PURE__ */ jsxs(
4112
+ TrendBadge,
4113
+ {
4114
+ $pos: isPos,
4115
+ style: { marginTop: 4, marginBottom: 0 },
4116
+ children: [
4117
+ isPos ? "↑" : "↓",
4118
+ " ",
4119
+ Math.abs(pct).toFixed(1),
4120
+ "%"
4121
+ ]
4122
+ }
4123
+ )
4124
+ ] })
4125
+ ] }, label);
4126
+ }) })
4127
+ ] })
4128
+ ] })
4129
+ }
4130
+ );
3316
4131
  }
3317
4132
  function useUsers(options = {}) {
3318
4133
  const { limit = 25, offset = 0, search } = options;
@@ -3323,7 +4138,14 @@ function useUsers(options = {}) {
3323
4138
  {
3324
4139
  field: "email",
3325
4140
  operator: "contains",
3326
- value: search
4141
+ value: search,
4142
+ connector: "OR"
4143
+ },
4144
+ {
4145
+ field: "name",
4146
+ operator: "contains",
4147
+ value: search,
4148
+ connector: "OR"
3327
4149
  }
3328
4150
  ]) : void 0;
3329
4151
  const result = await client.dash.listUsers({
@@ -3552,6 +4374,45 @@ const ReadOnlyCodeInput = styled.div`
3552
4374
  word-break: break-all;
3553
4375
  overflow-wrap: anywhere;
3554
4376
  `;
4377
+ const SessionCard = styled.div`
4378
+ display: flex;
4379
+ align-items: flex-start;
4380
+ justify-content: space-between;
4381
+ gap: 12px;
4382
+ padding: 10px 14px;
4383
+ background: white;
4384
+ border: 1px solid #eaeaef;
4385
+ border-radius: 8px;
4386
+ `;
4387
+ const SessionMeta = styled.div`
4388
+ display: flex;
4389
+ flex-direction: column;
4390
+ gap: 3px;
4391
+ flex: 1;
4392
+ min-width: 0;
4393
+ `;
4394
+ const IpChip = styled.span`
4395
+ display: inline-block;
4396
+ background: #f0f0ff;
4397
+ color: #4945ff;
4398
+ border-radius: 5px;
4399
+ padding: 2px 6px;
4400
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
4401
+ font-size: 11px;
4402
+ font-weight: 600;
4403
+ `;
4404
+ const TimestampText = styled.span`
4405
+ font-size: 11px;
4406
+ color: #8e8ea9;
4407
+ `;
4408
+ const AgentText = styled.span`
4409
+ font-size: 11px;
4410
+ color: #b8b8c7;
4411
+ overflow: hidden;
4412
+ text-overflow: ellipsis;
4413
+ white-space: nowrap;
4414
+ display: block;
4415
+ `;
3555
4416
  const STANDARD_FIELDS = /* @__PURE__ */ new Set([
3556
4417
  "id",
3557
4418
  "name",
@@ -3573,11 +4434,15 @@ function UserDetailDrawer({
3573
4434
  }) {
3574
4435
  const qc = useQueryClient();
3575
4436
  const { toggleNotification } = useNotification();
4437
+ const { get, put } = useFetchClient();
3576
4438
  const schemaQuery = useModelSchema("user");
3577
4439
  const userQuery = useQuery({
3578
4440
  queryKey: ["dash-user", userId],
3579
4441
  queryFn: async () => {
3580
- const result = await client.dash.user({}, withContext({ userId }));
4442
+ const result = await client.dash.user(
4443
+ {},
4444
+ withContext({ userId }, getAuthHeaders())
4445
+ );
3581
4446
  if (result.error)
3582
4447
  throw new Error(result.error.message ?? "Failed to load user");
3583
4448
  return result.data;
@@ -3588,12 +4453,33 @@ function UserDetailDrawer({
3588
4453
  queryFn: async () => {
3589
4454
  const result = await client.dash.userOrganizations(
3590
4455
  {},
3591
- withContext({ userId })
4456
+ withContext({ userId }, getAuthHeaders())
3592
4457
  );
3593
4458
  if (result.error) throw new Error(result.error.message ?? "Failed");
3594
4459
  return result.data?.organizations ?? [];
3595
4460
  }
3596
4461
  });
4462
+ const sessionsQuery = useQuery({
4463
+ queryKey: ["dash-user-sessions", userId],
4464
+ queryFn: async () => {
4465
+ const { data } = await get(
4466
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
4467
+ );
4468
+ return data.results ?? [];
4469
+ }
4470
+ });
4471
+ const strapiUserQuery = useQuery({
4472
+ queryKey: ["dash-strapi-user", userId],
4473
+ enabled: !!schemaQuery.data,
4474
+ queryFn: async () => {
4475
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
4476
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
4477
+ const { data } = await get(
4478
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
4479
+ );
4480
+ return data.results?.[0] ?? null;
4481
+ }
4482
+ });
3597
4483
  const [activeTab, setActiveTab] = useState("profile");
3598
4484
  const [editName, setEditName] = useState(void 0);
3599
4485
  const [editEmail, setEditEmail] = useState(void 0);
@@ -3604,6 +4490,8 @@ function UserDetailDrawer({
3604
4490
  const [banReason, setBanReason] = useState("");
3605
4491
  const [banExpiresDays, setBanExpiresDays] = useState("");
3606
4492
  const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
4493
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
4494
+ const [confirmBan, setConfirmBan] = useState(false);
3607
4495
  const [confirmUnban, setConfirmUnban] = useState(false);
3608
4496
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
3609
4497
  const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
@@ -3622,26 +4510,33 @@ function UserDetailDrawer({
3622
4510
  setEditExtra((prev) => ({ ...prev, [name]: value }));
3623
4511
  };
3624
4512
  const extraData = {
3625
- ...user,
4513
+ ...strapiUserQuery.data ?? {},
3626
4514
  ...editExtra
3627
4515
  };
3628
4516
  const updateMutation = useMutation({
3629
4517
  mutationFn: async () => {
3630
- const body = { ...editExtra };
4518
+ const body = {};
3631
4519
  if (editName !== void 0) body.name = editName;
3632
4520
  if (editEmail !== void 0) body.email = editEmail;
3633
4521
  if (editEmailVerified !== void 0)
3634
4522
  body.emailVerified = editEmailVerified;
3635
4523
  if (editImage !== void 0) body.image = editImage;
3636
- const result = await client.dash.updateUser(
3637
- body,
3638
- withContext({ userId })
4524
+ const documentId = strapiUserQuery.data?.documentId;
4525
+ if (!documentId) throw new Error("Could not resolve documentId for user");
4526
+ const updateUser = await client.dash.updateUser(
4527
+ { userId, ...body },
4528
+ withContext({ userId }, getAuthHeaders())
3639
4529
  );
3640
- if (result.error)
3641
- throw new Error(result.error.message ?? "Update failed");
3642
- return result.data;
4530
+ if (!updateUser.data) throw new Error("Update failed");
4531
+ if (Object.keys(editExtra).length > 0) {
4532
+ await put(
4533
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4534
+ editExtra
4535
+ );
4536
+ }
3643
4537
  },
3644
- onSuccess: () => {
4538
+ onSuccess: async () => {
4539
+ await qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
3645
4540
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3646
4541
  qc.invalidateQueries({ queryKey: ["dash-users"] });
3647
4542
  setEditName(void 0);
@@ -3662,11 +4557,32 @@ function UserDetailDrawer({
3662
4557
  });
3663
4558
  }
3664
4559
  });
4560
+ const revokeSessionMutation = useMutation({
4561
+ mutationFn: async (sessionId) => {
4562
+ const result = await client.dash.sessions.revoke(
4563
+ {},
4564
+ withContext({ sessionId, userId }, getAuthHeaders())
4565
+ );
4566
+ if (result.error)
4567
+ throw new Error(result.error.message ?? "Revoke failed");
4568
+ },
4569
+ onSuccess: () => {
4570
+ setConfirmRevokeSessionId(null);
4571
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
4572
+ toggleNotification({ type: "success", message: "Session revoked" });
4573
+ },
4574
+ onError: (err) => {
4575
+ toggleNotification({
4576
+ type: "danger",
4577
+ message: err.message ?? "Failed to revoke session"
4578
+ });
4579
+ }
4580
+ });
3665
4581
  const passwordMutation = useMutation({
3666
4582
  mutationFn: async () => {
3667
4583
  const result = await client.dash.setPassword(
3668
4584
  { password: newPassword },
3669
- withContext({ userId })
4585
+ withContext({ userId }, getAuthHeaders())
3670
4586
  );
3671
4587
  if (result.error)
3672
4588
  throw new Error(result.error.message ?? "Password update failed");
@@ -3694,11 +4610,15 @@ function UserDetailDrawer({
3694
4610
  const days = parseInt(banExpiresDays, 10);
3695
4611
  if (!Number.isNaN(days)) body.banExpires = Date.now() + days * 864e5;
3696
4612
  }
3697
- const result = await client.dash.banUser(body, withContext({ userId }));
4613
+ const result = await client.dash.banUser(
4614
+ body,
4615
+ withContext({ userId }, getAuthHeaders())
4616
+ );
3698
4617
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
3699
4618
  return result.data;
3700
4619
  },
3701
4620
  onSuccess: () => {
4621
+ setConfirmBan(false);
3702
4622
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3703
4623
  qc.invalidateQueries({ queryKey: ["dash-users"] });
3704
4624
  setBanReason("");
@@ -3714,7 +4634,10 @@ function UserDetailDrawer({
3714
4634
  });
3715
4635
  const unbanMutation = useMutation({
3716
4636
  mutationFn: async () => {
3717
- const result = await client.dash.unbanUser({}, withContext({ userId }));
4637
+ const result = await client.dash.unbanUser(
4638
+ {},
4639
+ withContext({ userId }, getAuthHeaders())
4640
+ );
3718
4641
  if (result.error) throw new Error(result.error.message ?? "Unban failed");
3719
4642
  return result.data;
3720
4643
  },
@@ -3735,7 +4658,7 @@ function UserDetailDrawer({
3735
4658
  mutationFn: async (providerId) => {
3736
4659
  const result = await client.dash.unlinkAccount(
3737
4660
  { providerId },
3738
- withContext({ userId })
4661
+ withContext({ userId }, getAuthHeaders())
3739
4662
  );
3740
4663
  if (result.error)
3741
4664
  throw new Error(result.error.message ?? "Failed to unlink account");
@@ -3756,7 +4679,7 @@ function UserDetailDrawer({
3756
4679
  mutationFn: async () => {
3757
4680
  const result = await client.dash.sessions.revokeAll(
3758
4681
  { userId },
3759
- withContext({})
4682
+ withContext({}, getAuthHeaders())
3760
4683
  );
3761
4684
  if (result.error)
3762
4685
  throw new Error(result.error.message ?? "Revoke failed");
@@ -3778,7 +4701,7 @@ function UserDetailDrawer({
3778
4701
  const callbackUrl = new URL(window.location.href, window.location.origin);
3779
4702
  const result = await client.dash.sendVerificationEmail(
3780
4703
  { callbackUrl: callbackUrl.toString() },
3781
- withContext({ userId })
4704
+ withContext({ userId }, getAuthHeaders())
3782
4705
  );
3783
4706
  if (result.error) throw new Error(result.error.message ?? "Failed");
3784
4707
  return result.data;
@@ -3801,7 +4724,7 @@ function UserDetailDrawer({
3801
4724
  const callbackUrl = new URL(window.location.href, window.location.origin);
3802
4725
  const result = await client.dash.sendResetPasswordEmail(
3803
4726
  { callbackUrl: callbackUrl.toString() },
3804
- withContext({ userId })
4727
+ withContext({ userId }, getAuthHeaders())
3805
4728
  );
3806
4729
  if (result.error) throw new Error(result.error.message ?? "Failed");
3807
4730
  return result.data;
@@ -3823,7 +4746,7 @@ function UserDetailDrawer({
3823
4746
  mutationFn: async () => {
3824
4747
  const result = await client.dash.enableTwoFactor(
3825
4748
  {},
3826
- withContext({ userId })
4749
+ withContext({ userId }, getAuthHeaders())
3827
4750
  );
3828
4751
  if (result.error)
3829
4752
  throw new Error(result.error.message ?? "Failed to enable 2FA");
@@ -3848,7 +4771,7 @@ function UserDetailDrawer({
3848
4771
  mutationFn: async () => {
3849
4772
  const result = await client.dash.disableTwoFactor(
3850
4773
  {},
3851
- withContext({ userId })
4774
+ withContext({ userId }, getAuthHeaders())
3852
4775
  );
3853
4776
  if (result.error)
3854
4777
  throw new Error(result.error.message ?? "Failed to disable 2FA");
@@ -3876,7 +4799,7 @@ function UserDetailDrawer({
3876
4799
  mutationFn: async () => {
3877
4800
  const result = await client.dash.viewTwoFactorTotpUri(
3878
4801
  {},
3879
- withContext({ userId })
4802
+ withContext({ userId }, getAuthHeaders())
3880
4803
  );
3881
4804
  if (result.error)
3882
4805
  throw new Error(result.error.message ?? "Failed to load TOTP URI");
@@ -3899,7 +4822,7 @@ function UserDetailDrawer({
3899
4822
  mutationFn: async () => {
3900
4823
  const result = await client.dash.viewBackupCodes(
3901
4824
  {},
3902
- withContext({ userId })
4825
+ withContext({ userId }, getAuthHeaders())
3903
4826
  );
3904
4827
  if (result.error)
3905
4828
  throw new Error(result.error.message ?? "Failed to load backup codes");
@@ -3922,7 +4845,7 @@ function UserDetailDrawer({
3922
4845
  mutationFn: async () => {
3923
4846
  const result = await client.dash.generateBackupCodes(
3924
4847
  {},
3925
- withContext({ userId })
4848
+ withContext({ userId }, getAuthHeaders())
3926
4849
  );
3927
4850
  if (result.error)
3928
4851
  throw new Error(
@@ -4118,7 +5041,15 @@ function UserDetailDrawer({
4118
5041
  }
4119
5042
  )
4120
5043
  ] })
4121
- ] })
5044
+ ] }),
5045
+ /* @__PURE__ */ jsx(
5046
+ EditViewSidePanels,
5047
+ {
5048
+ model: "plugin::better-auth.user",
5049
+ documentId: strapiUserQuery.data?.documentId,
5050
+ document: strapiUserQuery.data ?? void 0
5051
+ }
5052
+ )
4122
5053
  ] })
4123
5054
  ] }) }),
4124
5055
  /* @__PURE__ */ jsx(Tabs.Content, { value: "security", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
@@ -4314,7 +5245,8 @@ function UserDetailDrawer({
4314
5245
  ] })
4315
5246
  ] })
4316
5247
  ] }) }),
4317
- banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 4, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(Fragment, { children: [
5248
+ banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(FormSection, { children: [
5249
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Ban status" }),
4318
5250
  /* @__PURE__ */ jsxs(WarnCard, { children: [
4319
5251
  /* @__PURE__ */ jsx(
4320
5252
  Typography,
@@ -4329,7 +5261,7 @@ function UserDetailDrawer({
4329
5261
  "Reason: ",
4330
5262
  user.banReason
4331
5263
  ] }),
4332
- user.banExpires && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
5264
+ user.banExpires ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
4333
5265
  "Expires:",
4334
5266
  " ",
4335
5267
  new Date(user.banExpires).toLocaleDateString(
@@ -4341,8 +5273,7 @@ function UserDetailDrawer({
4341
5273
  day: "numeric"
4342
5274
  }
4343
5275
  )
4344
- ] }),
4345
- !user.banExpires && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
5276
+ ] }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4346
5277
  ] }),
4347
5278
  /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
4348
5279
  Button,
@@ -4352,27 +5283,28 @@ function UserDetailDrawer({
4352
5283
  children: "Lift ban"
4353
5284
  }
4354
5285
  ) })
4355
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5286
+ ] }) : /* @__PURE__ */ jsxs(FormSection, { children: [
5287
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Apply ban" }),
5288
+ /* @__PURE__ */ jsxs(
5289
+ Field.Root,
5290
+ {
5291
+ hint: "Optional — will be shown to the user",
5292
+ style: { width: "100%" },
5293
+ children: [
5294
+ /* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
5295
+ /* @__PURE__ */ jsx(
5296
+ TextInput,
5297
+ {
5298
+ value: banReason,
5299
+ onChange: (e) => setBanReason(e.target.value),
5300
+ placeholder: "e.g. Violated terms of service"
5301
+ }
5302
+ ),
5303
+ /* @__PURE__ */ jsx(Field.Hint, {})
5304
+ ]
5305
+ }
5306
+ ),
4356
5307
  /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
4357
- /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(
4358
- Field.Root,
4359
- {
4360
- hint: "Optional — will be shown to the user",
4361
- style: { width: "100%" },
4362
- children: [
4363
- /* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
4364
- /* @__PURE__ */ jsx(
4365
- TextInput,
4366
- {
4367
- value: banReason,
4368
- onChange: (e) => setBanReason(e.target.value),
4369
- placeholder: "e.g. Violated terms of service"
4370
- }
4371
- ),
4372
- /* @__PURE__ */ jsx(Field.Hint, {})
4373
- ]
4374
- }
4375
- ) }),
4376
5308
  /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsxs(
4377
5309
  Field.Root,
4378
5310
  {
@@ -4393,24 +5325,13 @@ function UserDetailDrawer({
4393
5325
  ]
4394
5326
  }
4395
5327
  ) }),
4396
- /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: banExpiryPreview ? /* @__PURE__ */ jsx(
4397
- Flex,
4398
- {
4399
- direction: "column",
4400
- justifyContent: "flex-end",
4401
- style: { height: "100%", paddingBottom: 4 },
4402
- children: /* @__PURE__ */ jsxs(PreviewPill, { children: [
4403
- "⏱ Expires ",
4404
- banExpiryPreview
4405
- ] })
4406
- }
4407
- ) : /* @__PURE__ */ jsx(
5328
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsx(
4408
5329
  Flex,
4409
5330
  {
4410
5331
  direction: "column",
4411
5332
  justifyContent: "flex-end",
4412
5333
  style: { height: "100%", paddingBottom: 4 },
4413
- children: /* @__PURE__ */ jsx(PreviewPill, { children: "♾ Permanent ban" })
5334
+ children: /* @__PURE__ */ jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
4414
5335
  }
4415
5336
  ) })
4416
5337
  ] }),
@@ -4418,31 +5339,68 @@ function UserDetailDrawer({
4418
5339
  Button,
4419
5340
  {
4420
5341
  variant: "danger",
4421
- loading: banMutation.isLoading,
4422
- onClick: () => banMutation.mutate(),
5342
+ onClick: () => setConfirmBan(true),
4423
5343
  children: "Ban user"
4424
5344
  }
4425
5345
  ) })
4426
5346
  ] }) }) }),
4427
- /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
4428
- /* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
4429
- /* @__PURE__ */ jsxs(AccountRow, { children: [
4430
- /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4431
- /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4432
- /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4433
- ] }),
4434
- /* @__PURE__ */ jsx(
4435
- Button,
4436
- {
4437
- variant: "danger-light",
4438
- size: "S",
4439
- onClick: () => setConfirmRevokeAll(true),
4440
- style: { flexShrink: 0 },
4441
- children: "Revoke all"
4442
- }
4443
- )
5347
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
5348
+ /* @__PURE__ */ jsxs(FormSection, { children: [
5349
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
5350
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
5351
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
5352
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
5353
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
5354
+ ] }),
5355
+ /* @__PURE__ */ jsx(
5356
+ Button,
5357
+ {
5358
+ variant: "danger-light",
5359
+ size: "S",
5360
+ onClick: () => setConfirmRevokeAll(true),
5361
+ style: { flexShrink: 0 },
5362
+ children: "Revoke all"
5363
+ }
5364
+ )
5365
+ ] })
5366
+ ] }),
5367
+ /* @__PURE__ */ jsxs(FormSection, { children: [
5368
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Active sessions" }),
5369
+ sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(SessionCard, { children: [
5370
+ /* @__PURE__ */ jsxs(SessionMeta, { children: [
5371
+ /* @__PURE__ */ jsxs(
5372
+ Flex,
5373
+ {
5374
+ gap: 2,
5375
+ alignItems: "center",
5376
+ style: { flexWrap: "wrap" },
5377
+ children: [
5378
+ session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
5379
+ /* @__PURE__ */ jsxs(TimestampText, { children: [
5380
+ "Created",
5381
+ " ",
5382
+ new Date(session.createdAt).toLocaleString(),
5383
+ " · Expires",
5384
+ " ",
5385
+ new Date(session.expiresAt).toLocaleString()
5386
+ ] })
5387
+ ]
5388
+ }
5389
+ ),
5390
+ session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
5391
+ ] }),
5392
+ /* @__PURE__ */ jsx(
5393
+ IconButton,
5394
+ {
5395
+ label: "Revoke session",
5396
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
5397
+ style: { flexShrink: 0 },
5398
+ children: /* @__PURE__ */ jsx(Trash, {})
5399
+ }
5400
+ )
5401
+ ] }, session.documentId)) })
4444
5402
  ] })
4445
- ] }) }) }),
5403
+ ] }) }),
4446
5404
  /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
4447
5405
  /* @__PURE__ */ jsx(SectionLabel, { children: "Memberships" }),
4448
5406
  orgsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
@@ -4479,6 +5437,29 @@ function UserDetailDrawer({
4479
5437
  onCancel: () => setConfirmRevokeAll(false)
4480
5438
  }
4481
5439
  ),
5440
+ confirmRevokeSessionId && /* @__PURE__ */ jsx(
5441
+ ConfirmDialog,
5442
+ {
5443
+ title: "Revoke session",
5444
+ message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
5445
+ confirmLabel: "Revoke",
5446
+ loading: revokeSessionMutation.isLoading,
5447
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
5448
+ onCancel: () => setConfirmRevokeSessionId(null)
5449
+ }
5450
+ ),
5451
+ confirmBan && /* @__PURE__ */ jsx(
5452
+ ConfirmDialog,
5453
+ {
5454
+ title: "Ban user",
5455
+ message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
5456
+ confirmLabel: "Ban user",
5457
+ variant: "danger",
5458
+ loading: banMutation.isLoading,
5459
+ onConfirm: () => banMutation.mutate(),
5460
+ onCancel: () => setConfirmBan(false)
5461
+ }
5462
+ ),
4482
5463
  confirmUnban && /* @__PURE__ */ jsx(
4483
5464
  ConfirmDialog,
4484
5465
  {
@@ -4708,7 +5689,9 @@ const Toolbar = styled.div`
4708
5689
  gap: 12px;
4709
5690
  `;
4710
5691
  function UsersPage({ config }) {
5692
+ const rbac = useRBAC(PERMISSIONS.user);
4711
5693
  const qc = useQueryClient();
5694
+ const { toggleNotification } = useNotification();
4712
5695
  const banEnabled = hasPlugin(config, "admin");
4713
5696
  const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
4714
5697
  const twoFactorEnabled = hasPlugin(config, "two-factor");
@@ -4732,7 +5715,10 @@ function UsersPage({ config }) {
4732
5715
  const pageCount = Math.ceil(total / PAGE_SIZE);
4733
5716
  const deleteMutation = useMutation({
4734
5717
  mutationFn: async (userId) => {
4735
- const result = await client.dash.deleteUser({}, withContext({ userId }));
5718
+ const result = await client.dash.deleteUser(
5719
+ {},
5720
+ withContext({ userId }, getAuthHeaders())
5721
+ );
4736
5722
  if (result.error)
4737
5723
  throw new Error(result.error.message ?? "Delete failed");
4738
5724
  },
@@ -4740,38 +5726,65 @@ function UsersPage({ config }) {
4740
5726
  setConfirmDelete(null);
4741
5727
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4742
5728
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5729
+ toggleNotification({ type: "success", message: "User deleted" });
5730
+ },
5731
+ onError: (err) => {
5732
+ toggleNotification({
5733
+ type: "danger",
5734
+ message: err.message ?? "Failed to delete user"
5735
+ });
4743
5736
  }
4744
5737
  });
4745
5738
  const deleteManyMutation = useMutation({
4746
5739
  mutationFn: async (userIds) => {
4747
5740
  const result = await client.dash.deleteManyUsers(
4748
5741
  {},
4749
- withContext({ userIds })
5742
+ withContext({ userIds }, getAuthHeaders())
4750
5743
  );
4751
5744
  if (result.error)
4752
5745
  throw new Error(result.error.message ?? "Delete failed");
4753
5746
  return result.data;
4754
5747
  },
4755
- onSuccess: () => {
5748
+ onSuccess: (_data, userIds) => {
4756
5749
  setConfirmDeleteMany(false);
4757
5750
  setSelected(/* @__PURE__ */ new Set());
4758
5751
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4759
5752
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5753
+ toggleNotification({
5754
+ type: "success",
5755
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
5756
+ });
5757
+ },
5758
+ onError: (err) => {
5759
+ toggleNotification({
5760
+ type: "danger",
5761
+ message: err.message ?? "Failed to delete users"
5762
+ });
4760
5763
  }
4761
5764
  });
4762
5765
  const banManyMutation = useMutation({
4763
5766
  mutationFn: async (userIds) => {
4764
5767
  const result = await client.dash.banManyUsers(
4765
5768
  {},
4766
- withContext({ userIds })
5769
+ withContext({ userIds }, getAuthHeaders())
4767
5770
  );
4768
5771
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
4769
5772
  return result.data;
4770
5773
  },
4771
- onSuccess: () => {
5774
+ onSuccess: (_data, userIds) => {
4772
5775
  setConfirmBanMany(false);
4773
5776
  setSelected(/* @__PURE__ */ new Set());
4774
5777
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5778
+ toggleNotification({
5779
+ type: "success",
5780
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
5781
+ });
5782
+ },
5783
+ onError: (err) => {
5784
+ toggleNotification({
5785
+ type: "danger",
5786
+ message: err.message ?? "Failed to ban users"
5787
+ });
4775
5788
  }
4776
5789
  });
4777
5790
  const toggleSelect = (id) => {
@@ -4796,334 +5809,244 @@ function UsersPage({ config }) {
4796
5809
  setSearch(searchInput);
4797
5810
  setPage(1);
4798
5811
  };
4799
- return /* @__PURE__ */ jsxs(Wrap, { "data-testid": "users-page", children: [
4800
- /* @__PURE__ */ jsxs(PageHeader, { children: [
4801
- /* @__PURE__ */ jsxs(TitleBlock, { children: [
4802
- /* @__PURE__ */ jsx(PageTitle, { children: "Users" }),
4803
- /* @__PURE__ */ jsxs(PageSubtitle, { children: [
4804
- total.toLocaleString(),
4805
- " total",
4806
- data?.onlineUsers ? ` · ${data.onlineUsers} online` : ""
4807
- ] })
4808
- ] }),
4809
- /* @__PURE__ */ jsx(
4810
- Button,
4811
- {
4812
- startIcon: /* @__PURE__ */ jsx(Plus, {}),
4813
- onClick: () => setShowCreate(true),
4814
- "data-testid": "create-user-btn",
4815
- children: "Create user"
4816
- }
4817
- )
4818
- ] }),
4819
- /* @__PURE__ */ jsxs(Toolbar, { children: [
4820
- /* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
4821
- Searchbar,
4822
- {
4823
- clearLabel: "Clear",
4824
- name: "search",
4825
- placeholder: "Search by email…",
4826
- value: searchInput,
4827
- onChange: (e) => setSearchInput(e.target.value),
4828
- onClear: () => {
4829
- setSearchInput("");
4830
- setSearch("");
4831
- setPage(1);
4832
- },
4833
- "data-testid": "user-search",
4834
- children: "Search users"
4835
- }
4836
- ) }),
4837
- someSelected && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
4838
- /* @__PURE__ */ jsxs(
4839
- Button,
4840
- {
4841
- variant: "danger-light",
4842
- size: "S",
4843
- onClick: () => setConfirmDeleteMany(true),
4844
- "data-testid": "delete-selected-btn",
4845
- children: [
4846
- "Delete ",
4847
- selected.size,
4848
- " selected"
4849
- ]
4850
- }
4851
- ),
4852
- banEnabled && /* @__PURE__ */ jsxs(
4853
- Button,
4854
- {
4855
- variant: "secondary",
4856
- size: "S",
4857
- onClick: () => setConfirmBanMany(true),
4858
- children: [
4859
- "Ban ",
4860
- selected.size,
4861
- " selected"
4862
- ]
4863
- }
4864
- )
4865
- ] })
4866
- ] }),
4867
- isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: error?.message }),
4868
- /* @__PURE__ */ jsx(TableCard, { children: isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading users…" }) }) : /* @__PURE__ */ jsxs(Table, { children: [
4869
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
4870
- /* @__PURE__ */ jsx(THCheck, { children: /* @__PURE__ */ jsx(
4871
- Checkbox,
4872
- {
4873
- checked: allSelected,
4874
- onCheckedChange: handleSelectAll,
4875
- "aria-label": "Select all"
4876
- }
4877
- ) }),
4878
- /* @__PURE__ */ jsx(TH, { children: "Name" }),
4879
- /* @__PURE__ */ jsx(TH, { children: "Email" }),
4880
- /* @__PURE__ */ jsx(TH, { children: "Status" }),
4881
- /* @__PURE__ */ jsx(TH, { children: "Created" }),
4882
- /* @__PURE__ */ jsx(THActions, {})
4883
- ] }) }),
4884
- /* @__PURE__ */ jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
4885
- TD,
4886
- {
4887
- colSpan: 6,
4888
- style: {
4889
- textAlign: "center",
4890
- padding: "40px",
4891
- color: "#8e8ea9"
4892
- },
4893
- "data-testid": "users-empty",
4894
- children: search ? `No users matching "${search}"` : "No users found"
4895
- }
4896
- ) }) : users.map((user, i) => /* @__PURE__ */ jsxs(
4897
- TR,
4898
- {
4899
- $selected: selected.has(user.id),
4900
- $i: i,
4901
- "data-testid": "user-row",
4902
- children: [
4903
- /* @__PURE__ */ jsx(TDCheck, { children: /* @__PURE__ */ jsx(
5812
+ return /* @__PURE__ */ jsx(
5813
+ Page.Protect,
5814
+ {
5815
+ permissions: PERMISSIONS.user.filter(
5816
+ (p) => p.action === `plugin::${PLUGIN_ID}.user.read`
5817
+ ),
5818
+ children: /* @__PURE__ */ jsxs(Wrap, { "data-testid": "users-page", children: [
5819
+ /* @__PURE__ */ jsxs(PageHeader, { children: [
5820
+ /* @__PURE__ */ jsxs(TitleBlock, { children: [
5821
+ /* @__PURE__ */ jsx(PageTitle, { children: "Users" }),
5822
+ /* @__PURE__ */ jsxs(PageSubtitle, { children: [
5823
+ total.toLocaleString(),
5824
+ " total",
5825
+ data?.onlineUsers ? ` · ${data.onlineUsers} online` : ""
5826
+ ] })
5827
+ ] }),
5828
+ rbac.allowedActions.canCreate && /* @__PURE__ */ jsx(
5829
+ Button,
5830
+ {
5831
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
5832
+ onClick: () => setShowCreate(true),
5833
+ "data-testid": "create-user-btn",
5834
+ children: "Create user"
5835
+ }
5836
+ )
5837
+ ] }),
5838
+ /* @__PURE__ */ jsxs(Toolbar, { children: [
5839
+ /* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
5840
+ Searchbar,
5841
+ {
5842
+ clearLabel: "Clear",
5843
+ name: "search",
5844
+ placeholder: "Search by email…",
5845
+ value: searchInput,
5846
+ onChange: (e) => setSearchInput(e.target.value),
5847
+ onClear: () => {
5848
+ setSearchInput("");
5849
+ setSearch("");
5850
+ setPage(1);
5851
+ },
5852
+ "data-testid": "user-search",
5853
+ children: "Search users"
5854
+ }
5855
+ ) }),
5856
+ someSelected && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
5857
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsxs(
5858
+ Button,
5859
+ {
5860
+ variant: "danger-light",
5861
+ size: "S",
5862
+ onClick: () => setConfirmDeleteMany(true),
5863
+ "data-testid": "delete-selected-btn",
5864
+ children: [
5865
+ "Delete ",
5866
+ selected.size,
5867
+ " selected"
5868
+ ]
5869
+ }
5870
+ ),
5871
+ banEnabled && rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxs(
5872
+ Button,
5873
+ {
5874
+ variant: "secondary",
5875
+ size: "S",
5876
+ onClick: () => setConfirmBanMany(true),
5877
+ children: [
5878
+ "Ban ",
5879
+ selected.size,
5880
+ " selected"
5881
+ ]
5882
+ }
5883
+ )
5884
+ ] })
5885
+ ] }),
5886
+ isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: error?.message }),
5887
+ /* @__PURE__ */ jsx(TableCard, { children: isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading users…" }) }) : /* @__PURE__ */ jsxs(Table, { children: [
5888
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
5889
+ (rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsx(THCheck, { children: /* @__PURE__ */ jsx(
4904
5890
  Checkbox,
4905
5891
  {
4906
- checked: selected.has(user.id),
4907
- onCheckedChange: () => toggleSelect(user.id),
4908
- "aria-label": `Select ${user.name}`
5892
+ checked: allSelected,
5893
+ onCheckedChange: handleSelectAll,
5894
+ "aria-label": "Select all"
4909
5895
  }
4910
5896
  ) }),
4911
- /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
4912
- /* @__PURE__ */ jsx(
4913
- Avatar,
4914
- {
4915
- name: user.name ?? "",
4916
- src: user.image,
4917
- size: 28
4918
- }
4919
- ),
4920
- /* @__PURE__ */ jsx(UserName, { children: user.name })
4921
- ] }) }),
4922
- /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(MonoText, { children: user.email }) }),
4923
- /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
4924
- user.emailVerified ? /* @__PURE__ */ jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
4925
- user.banned && /* @__PURE__ */ jsx(StatusChip, { $variant: "banned", children: "Banned" })
4926
- ] }) }),
4927
- /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
4928
- /* @__PURE__ */ jsx(TDActions, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
4929
- /* @__PURE__ */ jsx(
4930
- IconButton,
4931
- {
4932
- label: "Edit user",
4933
- onClick: () => setDetailUserId(user.id),
4934
- "data-testid": "edit-user-btn",
4935
- children: /* @__PURE__ */ jsx(Pencil, {})
4936
- }
4937
- ),
4938
- /* @__PURE__ */ jsx(
4939
- IconButton,
4940
- {
4941
- label: "Delete user",
4942
- onClick: () => setConfirmDelete(user.id),
4943
- "data-testid": "delete-user-btn",
4944
- children: /* @__PURE__ */ jsx(Trash, {})
4945
- }
4946
- )
4947
- ] }) })
4948
- ]
4949
- },
4950
- user.id
4951
- )) })
4952
- ] }) }),
4953
- pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx(
4954
- Pagination,
4955
- {
4956
- activePage: page,
4957
- pageCount,
4958
- onChangePage: setPage
4959
- }
4960
- ) }),
4961
- showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
4962
- detailUserId && /* @__PURE__ */ jsx(
4963
- UserDetailDrawer,
4964
- {
4965
- userId: detailUserId,
4966
- banEnabled,
4967
- emailVerificationEnabled,
4968
- twoFactorEnabled,
4969
- onClose: () => setDetailUserId(null)
4970
- }
4971
- ),
4972
- confirmDelete && /* @__PURE__ */ jsx(
4973
- ConfirmDialog,
4974
- {
4975
- title: "Delete user",
4976
- message: "Are you sure you want to delete this user? This action cannot be undone.",
4977
- confirmLabel: "Delete",
4978
- loading: deleteMutation.isLoading,
4979
- onConfirm: () => deleteMutation.mutate(confirmDelete),
4980
- onCancel: () => setConfirmDelete(null)
4981
- }
4982
- ),
4983
- confirmDeleteMany && /* @__PURE__ */ jsx(
4984
- ConfirmDialog,
4985
- {
4986
- title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
4987
- message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
4988
- confirmLabel: "Delete all",
4989
- loading: deleteManyMutation.isLoading,
4990
- onConfirm: () => deleteManyMutation.mutate([...selected]),
4991
- onCancel: () => setConfirmDeleteMany(false)
4992
- }
4993
- ),
4994
- confirmBanMany && /* @__PURE__ */ jsx(
4995
- ConfirmDialog,
4996
- {
4997
- title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
4998
- message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
4999
- confirmLabel: "Ban all",
5000
- variant: "danger",
5001
- loading: banManyMutation.isLoading,
5002
- onConfirm: () => banManyMutation.mutate([...selected]),
5003
- onCancel: () => setConfirmBanMany(false)
5004
- }
5005
- )
5006
- ] });
5007
- }
5008
- const Accent = styled.div`
5009
- height: 3px;
5010
- background: linear-gradient(90deg, #4945ff 0%, #7b79ff 55%, #9593ff 100%);
5011
- flex-shrink: 0;
5012
- `;
5013
- const BrandIcon = styled.div`
5014
- width: 30px;
5015
- height: 30px;
5016
- border-radius: 7px;
5017
- background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
5018
- display: flex;
5019
- align-items: center;
5020
- justify-content: center;
5021
- color: white;
5022
- font-size: 12px;
5023
- font-weight: 900;
5024
- flex-shrink: 0;
5025
- letter-spacing: -0.06em;
5026
- user-select: none;
5027
- `;
5028
- const PathTag = styled.code`
5029
- padding: 2px 8px;
5030
- background: #f0f0ff;
5031
- border: 1px solid #d9d8ff;
5032
- border-radius: 4px;
5033
- font-size: 11px;
5034
- color: #4945ff;
5035
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
5036
- font-weight: 500;
5037
- `;
5038
- function App() {
5039
- const { data: config, isLoading, isError, error } = useDashConfig();
5040
- const orgEnabled = hasPlugin(config, "organization");
5041
- const orgOptionsQuery = useQuery({
5042
- queryKey: ["dash-org-options"],
5043
- queryFn: async () => {
5044
- const result = await client.dash.organization.options();
5045
- if (result.error) return { teamsEnabled: false };
5046
- return result.data ?? { teamsEnabled: false };
5047
- },
5048
- enabled: orgEnabled
5049
- });
5050
- const teamsEnabled = orgEnabled ? orgOptionsQuery.data?.teamsEnabled ?? false : false;
5051
- if (isLoading) {
5052
- return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading Better Auth dashboard…" }) });
5053
- }
5054
- if (isError) {
5055
- return /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(
5056
- Alert,
5057
- {
5058
- closeLabel: "Close",
5059
- title: "Error loading configuration",
5060
- variant: "danger",
5061
- children: error?.message
5062
- }
5063
- ) });
5064
- }
5065
- if (!config) return null;
5066
- return /* @__PURE__ */ jsx(Box, { background: "neutral100", minHeight: "100vh", "data-testid": "dashboard-root", children: /* @__PURE__ */ jsxs(Tabs.Root, { defaultValue: "overview", children: [
5067
- /* @__PURE__ */ jsxs(
5068
- Box,
5069
- {
5070
- background: "neutral0",
5071
- borderColor: "neutral150",
5072
- borderStyle: "solid",
5073
- borderWidth: "1px",
5074
- style: { overflow: "hidden" },
5075
- children: [
5076
- /* @__PURE__ */ jsx(Accent, {}),
5077
- /* @__PURE__ */ jsxs(
5078
- Box,
5897
+ /* @__PURE__ */ jsx(TH, { children: "Name" }),
5898
+ /* @__PURE__ */ jsx(TH, { children: "Email" }),
5899
+ /* @__PURE__ */ jsx(TH, { children: "Status" }),
5900
+ /* @__PURE__ */ jsx(TH, { children: "Created" }),
5901
+ /* @__PURE__ */ jsx(THActions, {})
5902
+ ] }) }),
5903
+ /* @__PURE__ */ jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
5904
+ TD,
5079
5905
  {
5080
- paddingLeft: 6,
5081
- paddingRight: 6,
5082
- paddingTop: 4,
5083
- paddingBottom: 0,
5906
+ colSpan: 6,
5907
+ style: {
5908
+ textAlign: "center",
5909
+ padding: "40px",
5910
+ color: "#8e8ea9"
5911
+ },
5912
+ "data-testid": "users-empty",
5913
+ children: search ? `No users matching "${search}"` : "No users found"
5914
+ }
5915
+ ) }) : users.map((user, i) => /* @__PURE__ */ jsxs(
5916
+ TR,
5917
+ {
5918
+ $selected: selected.has(user.id),
5919
+ $i: i,
5920
+ "data-testid": "user-row",
5084
5921
  children: [
5085
- /* @__PURE__ */ jsxs(
5086
- Flex,
5922
+ (rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsx(TDCheck, { children: /* @__PURE__ */ jsx(
5923
+ Checkbox,
5087
5924
  {
5088
- justifyContent: "space-between",
5089
- alignItems: "center",
5090
- paddingBottom: 4,
5091
- children: [
5092
- /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
5093
- /* @__PURE__ */ jsx(BrandIcon, { children: "BA" }),
5094
- /* @__PURE__ */ jsxs(Box, { children: [
5095
- /* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
5096
- /* @__PURE__ */ jsx(Box, { paddingTop: "2px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
5097
- ] })
5098
- ] }),
5099
- /* @__PURE__ */ jsx(PathTag, { children: config.basePath })
5100
- ]
5925
+ checked: selected.has(user.id),
5926
+ onCheckedChange: () => toggleSelect(user.id),
5927
+ "aria-label": `Select ${user.name}`
5101
5928
  }
5102
- ),
5103
- /* @__PURE__ */ jsxs(Tabs.List, { "aria-label": "Dashboard navigation", "data-testid": "main-nav", children: [
5104
- /* @__PURE__ */ jsx(Tabs.Trigger, { value: "overview", "data-testid": "nav-overview", children: "Overview" }),
5105
- /* @__PURE__ */ jsx(Tabs.Trigger, { value: "users", "data-testid": "nav-users", children: "Users" }),
5106
- orgEnabled && /* @__PURE__ */ jsx(
5107
- Tabs.Trigger,
5929
+ ) }),
5930
+ /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
5931
+ /* @__PURE__ */ jsx(
5932
+ Avatar,
5108
5933
  {
5109
- value: "organizations",
5110
- "data-testid": "nav-organizations",
5111
- children: "Organizations"
5934
+ name: user.name ?? "",
5935
+ src: user.image,
5936
+ size: 28
5112
5937
  }
5113
5938
  ),
5114
- /* @__PURE__ */ jsx(Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
5115
- ] })
5939
+ /* @__PURE__ */ jsx(UserName, { children: user.name })
5940
+ ] }) }),
5941
+ /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(MonoText, { children: user.email }) }),
5942
+ /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
5943
+ user.emailVerified ? /* @__PURE__ */ jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
5944
+ user.banned && /* @__PURE__ */ jsx(StatusChip, { $variant: "banned", children: "Banned" })
5945
+ ] }) }),
5946
+ /* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
5947
+ /* @__PURE__ */ jsx(TDActions, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
5948
+ rbac.allowedActions.canUpdate && /* @__PURE__ */ jsx(
5949
+ IconButton,
5950
+ {
5951
+ label: "Edit user",
5952
+ onClick: () => setDetailUserId(user.id),
5953
+ "data-testid": "edit-user-btn",
5954
+ children: /* @__PURE__ */ jsx(Pencil, {})
5955
+ }
5956
+ ),
5957
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(
5958
+ IconButton,
5959
+ {
5960
+ label: "Delete user",
5961
+ onClick: () => setConfirmDelete(user.id),
5962
+ "data-testid": "delete-user-btn",
5963
+ children: /* @__PURE__ */ jsx(Trash, {})
5964
+ }
5965
+ )
5966
+ ] }) })
5116
5967
  ]
5968
+ },
5969
+ user.id
5970
+ )) })
5971
+ ] }) }),
5972
+ pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
5973
+ /* @__PURE__ */ jsx(
5974
+ Button,
5975
+ {
5976
+ variant: "tertiary",
5977
+ size: "S",
5978
+ disabled: page === 1,
5979
+ onClick: () => setPage((p) => p - 1),
5980
+ children: "Previous"
5981
+ }
5982
+ ),
5983
+ /* @__PURE__ */ jsx(
5984
+ Button,
5985
+ {
5986
+ variant: "tertiary",
5987
+ size: "S",
5988
+ disabled: page >= pageCount,
5989
+ onClick: () => setPage((p) => p + 1),
5990
+ children: "Next"
5117
5991
  }
5118
5992
  )
5119
- ]
5120
- }
5121
- ),
5122
- /* @__PURE__ */ jsx(Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsx(OverviewPage, {}) }),
5123
- /* @__PURE__ */ jsx(Tabs.Content, { value: "users", "data-testid": "tab-users", children: /* @__PURE__ */ jsx(UsersPage, { config }) }),
5124
- orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) }),
5125
- /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", "data-testid": "tab-sessions", children: /* @__PURE__ */ jsx(SessionsPage, {}) })
5126
- ] }) });
5993
+ ] }) }),
5994
+ rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
5995
+ rbac.allowedActions.canUpdate && detailUserId && /* @__PURE__ */ jsx(
5996
+ UserDetailDrawer,
5997
+ {
5998
+ userId: detailUserId,
5999
+ banEnabled,
6000
+ emailVerificationEnabled,
6001
+ twoFactorEnabled,
6002
+ onClose: () => setDetailUserId(null)
6003
+ }
6004
+ ),
6005
+ rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsx(
6006
+ ConfirmDialog,
6007
+ {
6008
+ title: "Delete user",
6009
+ message: "Are you sure you want to delete this user? This action cannot be undone.",
6010
+ confirmLabel: "Delete",
6011
+ loading: deleteMutation.isLoading,
6012
+ onConfirm: () => deleteMutation.mutate(confirmDelete),
6013
+ onCancel: () => setConfirmDelete(null)
6014
+ }
6015
+ ),
6016
+ rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsx(
6017
+ ConfirmDialog,
6018
+ {
6019
+ title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
6020
+ message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
6021
+ confirmLabel: "Delete all",
6022
+ loading: deleteManyMutation.isLoading,
6023
+ onConfirm: () => deleteManyMutation.mutate([...selected]),
6024
+ onCancel: () => setConfirmDeleteMany(false)
6025
+ }
6026
+ ),
6027
+ rbac.allowedActions.canUpdate && confirmBanMany && /* @__PURE__ */ jsx(
6028
+ ConfirmDialog,
6029
+ {
6030
+ title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
6031
+ message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
6032
+ confirmLabel: "Ban all",
6033
+ variant: "danger",
6034
+ loading: banManyMutation.isLoading,
6035
+ onConfirm: () => banManyMutation.mutate([...selected]),
6036
+ onCancel: () => setConfirmBanMany(false)
6037
+ }
6038
+ )
6039
+ ] })
6040
+ }
6041
+ );
6042
+ }
6043
+ function UsersRoute() {
6044
+ const { config } = useOutletContext();
6045
+ return /* @__PURE__ */ jsx(UsersPage, { config });
6046
+ }
6047
+ function OrganizationsRoute() {
6048
+ const { teamsEnabled } = useOutletContext();
6049
+ return /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled });
5127
6050
  }
5128
6051
  function Root() {
5129
6052
  const queryClient = useMemo(
@@ -5138,7 +6061,12 @@ function Root() {
5138
6061
  }),
5139
6062
  []
5140
6063
  );
5141
- return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(App, {}) });
6064
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(Routes, { children: /* @__PURE__ */ jsxs(Route, { path: "/", element: /* @__PURE__ */ jsx(ProtectedApp, {}), children: [
6065
+ /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(Navigate, { to: "overview", replace: true }) }),
6066
+ /* @__PURE__ */ jsx(Route, { path: "overview", element: /* @__PURE__ */ jsx(OverviewPage, {}) }),
6067
+ /* @__PURE__ */ jsx(Route, { path: "users", element: /* @__PURE__ */ jsx(UsersRoute, {}) }),
6068
+ /* @__PURE__ */ jsx(Route, { path: "organizations", element: /* @__PURE__ */ jsx(OrganizationsRoute, {}) })
6069
+ ] }) }) });
5142
6070
  }
5143
6071
  export {
5144
6072
  Root,