@mesob/auth-react 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/components/auth/auth-layout.d.ts +1 -1
  2. package/dist/components/auth/auth-layout.js +10 -2
  3. package/dist/components/auth/auth-layout.js.map +1 -1
  4. package/dist/components/auth/countdown.js +8 -6
  5. package/dist/components/auth/countdown.js.map +1 -1
  6. package/dist/components/auth/forgot-password.js +21 -19
  7. package/dist/components/auth/forgot-password.js.map +1 -1
  8. package/dist/components/auth/reset-password-form.js +22 -21
  9. package/dist/components/auth/reset-password-form.js.map +1 -1
  10. package/dist/components/auth/set-password.d.ts +9 -0
  11. package/dist/components/auth/set-password.js +527 -0
  12. package/dist/components/auth/set-password.js.map +1 -0
  13. package/dist/components/auth/sign-in.js +45 -26
  14. package/dist/components/auth/sign-in.js.map +1 -1
  15. package/dist/components/auth/sign-up.js +25 -29
  16. package/dist/components/auth/sign-up.js.map +1 -1
  17. package/dist/components/auth/verification-form.js +24 -27
  18. package/dist/components/auth/verification-form.js.map +1 -1
  19. package/dist/components/auth/verify-email.js +40 -31
  20. package/dist/components/auth/verify-email.js.map +1 -1
  21. package/dist/components/auth/verify-phone.js +40 -31
  22. package/dist/components/auth/verify-phone.js.map +1 -1
  23. package/dist/components/authorization/deny.d.ts +11 -0
  24. package/dist/components/authorization/deny.js +52 -0
  25. package/dist/components/authorization/deny.js.map +1 -0
  26. package/dist/components/authorization/grant.d.ts +12 -0
  27. package/dist/components/authorization/grant.js +57 -0
  28. package/dist/components/authorization/grant.js.map +1 -0
  29. package/dist/components/iam/permission-selector.d.ts +19 -0
  30. package/dist/components/iam/permission-selector.js +122 -0
  31. package/dist/components/iam/permission-selector.js.map +1 -0
  32. package/dist/components/iam/permissions.js +21 -33
  33. package/dist/components/iam/permissions.js.map +1 -1
  34. package/dist/components/iam/role-detail-layout.d.ts +11 -0
  35. package/dist/components/iam/role-detail-layout.js +137 -0
  36. package/dist/components/iam/role-detail-layout.js.map +1 -0
  37. package/dist/components/iam/role-detail-page.d.ts +9 -0
  38. package/dist/components/iam/role-detail-page.js +229 -0
  39. package/dist/components/iam/role-detail-page.js.map +1 -0
  40. package/dist/components/iam/role-permissions-page.d.ts +8 -0
  41. package/dist/components/iam/role-permissions-page.js +397 -0
  42. package/dist/components/iam/role-permissions-page.js.map +1 -0
  43. package/dist/components/iam/roles.js +20 -10
  44. package/dist/components/iam/roles.js.map +1 -1
  45. package/dist/components/iam/tenants.js +9 -2
  46. package/dist/components/iam/tenants.js.map +1 -1
  47. package/dist/components/iam/users.js +10 -9
  48. package/dist/components/iam/users.js.map +1 -1
  49. package/dist/components/profile/account.js +110 -19
  50. package/dist/components/profile/account.js.map +1 -1
  51. package/dist/components/profile/change-email-form.js +26 -29
  52. package/dist/components/profile/change-email-form.js.map +1 -1
  53. package/dist/components/profile/change-phone-form.js +26 -29
  54. package/dist/components/profile/change-phone-form.js.map +1 -1
  55. package/dist/components/profile/change-profile.d.ts +2 -1
  56. package/dist/components/profile/change-profile.js +16 -8
  57. package/dist/components/profile/change-profile.js.map +1 -1
  58. package/dist/components/profile/otp-verification-modal.js +24 -27
  59. package/dist/components/profile/otp-verification-modal.js.map +1 -1
  60. package/dist/components/profile/security.js +88 -57
  61. package/dist/components/profile/security.js.map +1 -1
  62. package/dist/components/profile/verify-change-email-form.js +24 -27
  63. package/dist/components/profile/verify-change-email-form.js.map +1 -1
  64. package/dist/components/profile/verify-change-phone-form.js +24 -27
  65. package/dist/components/profile/verify-change-phone-form.js.map +1 -1
  66. package/dist/index.d.ts +9 -1
  67. package/dist/index.js +1897 -821
  68. package/dist/index.js.map +1 -1
  69. package/dist/pages/auth/forgot-password.d.ts +7 -0
  70. package/dist/pages/auth/forgot-password.js +784 -0
  71. package/dist/pages/auth/forgot-password.js.map +1 -0
  72. package/dist/pages/auth/layout.d.ts +8 -0
  73. package/dist/pages/auth/layout.js +562 -0
  74. package/dist/pages/auth/layout.js.map +1 -0
  75. package/dist/pages/auth/reset-password.d.ts +10 -0
  76. package/dist/pages/auth/reset-password.js +913 -0
  77. package/dist/pages/auth/reset-password.js.map +1 -0
  78. package/dist/pages/auth/set-password.d.ts +10 -0
  79. package/dist/pages/auth/set-password.js +946 -0
  80. package/dist/pages/auth/set-password.js.map +1 -0
  81. package/dist/pages/auth/sign-in.d.ts +10 -0
  82. package/dist/pages/auth/sign-in.js +984 -0
  83. package/dist/pages/auth/sign-in.js.map +1 -0
  84. package/dist/pages/auth/sign-up.d.ts +10 -0
  85. package/dist/pages/auth/sign-up.js +940 -0
  86. package/dist/pages/auth/sign-up.js.map +1 -0
  87. package/dist/pages/auth/verify-email.d.ts +10 -0
  88. package/dist/pages/auth/verify-email.js +950 -0
  89. package/dist/pages/auth/verify-email.js.map +1 -0
  90. package/dist/pages/auth/verify-phone.d.ts +10 -0
  91. package/dist/pages/auth/verify-phone.js +964 -0
  92. package/dist/pages/auth/verify-phone.js.map +1 -0
  93. package/dist/pages/iam/permissions.d.ts +5 -0
  94. package/dist/pages/iam/permissions.js +308 -0
  95. package/dist/pages/iam/permissions.js.map +1 -0
  96. package/dist/pages/iam/role-detail-layout.d.ts +12 -0
  97. package/dist/pages/iam/role-detail-layout.js +145 -0
  98. package/dist/pages/iam/role-detail-layout.js.map +1 -0
  99. package/dist/pages/iam/role-detail.d.ts +12 -0
  100. package/dist/pages/iam/role-detail.js +241 -0
  101. package/dist/pages/iam/role-detail.js.map +1 -0
  102. package/dist/pages/iam/role-permissions.d.ts +12 -0
  103. package/dist/pages/iam/role-permissions.js +409 -0
  104. package/dist/pages/iam/role-permissions.js.map +1 -0
  105. package/dist/pages/iam/role-users.d.ts +12 -0
  106. package/dist/pages/iam/role-users.js +825 -0
  107. package/dist/pages/iam/role-users.js.map +1 -0
  108. package/dist/pages/iam/roles.d.ts +5 -0
  109. package/dist/pages/iam/roles.js +684 -0
  110. package/dist/pages/iam/roles.js.map +1 -0
  111. package/dist/pages/iam/sessions.d.ts +5 -0
  112. package/dist/pages/iam/sessions.js +315 -0
  113. package/dist/pages/iam/sessions.js.map +1 -0
  114. package/dist/pages/iam/tenant-detail.d.ts +10 -0
  115. package/dist/pages/iam/tenant-detail.js +186 -0
  116. package/dist/pages/iam/tenant-detail.js.map +1 -0
  117. package/dist/pages/iam/tenants.d.ts +5 -0
  118. package/dist/pages/iam/tenants.js +610 -0
  119. package/dist/pages/iam/tenants.js.map +1 -0
  120. package/dist/pages/iam/user-activity.d.ts +10 -0
  121. package/dist/pages/iam/user-activity.js +850 -0
  122. package/dist/pages/iam/user-activity.js.map +1 -0
  123. package/dist/pages/iam/user-detail-layout.d.ts +12 -0
  124. package/dist/pages/iam/user-detail-layout.js +106 -0
  125. package/dist/pages/iam/user-detail-layout.js.map +1 -0
  126. package/dist/pages/iam/user-detail.d.ts +10 -0
  127. package/dist/pages/iam/user-detail.js +102 -0
  128. package/dist/pages/iam/user-detail.js.map +1 -0
  129. package/dist/pages/iam/users.d.ts +5 -0
  130. package/dist/pages/iam/users.js +1275 -0
  131. package/dist/pages/iam/users.js.map +1 -0
  132. package/dist/pages/profile/account.d.ts +5 -0
  133. package/dist/pages/profile/account.js +182 -0
  134. package/dist/pages/profile/account.js.map +1 -0
  135. package/dist/pages/profile/layout.d.ts +8 -0
  136. package/dist/pages/profile/layout.js +133 -0
  137. package/dist/pages/profile/layout.js.map +1 -0
  138. package/dist/pages/profile/security.d.ts +5 -0
  139. package/dist/pages/profile/security.js +1539 -0
  140. package/dist/pages/profile/security.js.map +1 -0
  141. package/dist/{types-vcfvnAzQ.d.ts → types-g9QcNRxT.d.ts} +13 -7
  142. package/package.json +102 -3
@@ -0,0 +1,1275 @@
1
+ "use client";
2
+
3
+ // src/pages/iam/users.tsx
4
+ import {
5
+ EntityDrawerTrigger,
6
+ EntityFilter,
7
+ EntityHeader,
8
+ EntitySearch,
9
+ EntitySort,
10
+ EntityViewToggle,
11
+ PageBody,
12
+ PageContainer,
13
+ useBreadcrumbs,
14
+ useEntityPagination,
15
+ useEntityParams
16
+ } from "@mesob/ui/components";
17
+ import { IconUsers as IconUsers3 } from "@tabler/icons-react";
18
+ import { useState as useState6 } from "react";
19
+
20
+ // src/lib/query-options.ts
21
+ import { keepPreviousData } from "@tanstack/react-query";
22
+ var defaultEntityQueryOptions = {
23
+ refetchOnMount: false,
24
+ refetchOnWindowFocus: false,
25
+ refetchOnReconnect: false,
26
+ staleTime: 60 * 1e3,
27
+ gcTime: 5 * 60 * 1e3,
28
+ placeholderData: keepPreviousData,
29
+ retry: 1
30
+ };
31
+
32
+ // src/provider.tsx
33
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
34
+ import { deepmerge } from "deepmerge-ts";
35
+ import createFetchClient from "openapi-fetch";
36
+ import createClient from "openapi-react-query";
37
+ import { createContext, useContext, useMemo, useState } from "react";
38
+
39
+ // src/utils/cookie.ts
40
+ var isProduction = typeof process !== "undefined" && process.env.NODE_ENV === "production";
41
+
42
+ // src/provider.tsx
43
+ import { jsx } from "react/jsx-runtime";
44
+ var SessionContext = createContext(null);
45
+ var ApiContext = createContext(null);
46
+ var ConfigContext = createContext(null);
47
+ var queryClient = new QueryClient({
48
+ defaultOptions: {
49
+ queries: {
50
+ refetchOnWindowFocus: false
51
+ }
52
+ }
53
+ });
54
+ function useApi() {
55
+ const context = useContext(ApiContext);
56
+ if (!context) {
57
+ throw new Error("useApi must be used within MesobAuthProvider");
58
+ }
59
+ return context;
60
+ }
61
+
62
+ // src/pages/iam/users/_components/bulk-invite-user-form.tsx
63
+ import { zodResolver } from "@hookform/resolvers/zod";
64
+ import {
65
+ Badge as Badge2,
66
+ Button,
67
+ Card as Card2,
68
+ CardContent as CardContent2,
69
+ CardHeader as CardHeader2,
70
+ CardTitle as CardTitle2,
71
+ EntityDrawer,
72
+ EntityFormActions,
73
+ Form,
74
+ FormControl,
75
+ FormField,
76
+ FormItem,
77
+ FormLabel,
78
+ FormMessage,
79
+ Input,
80
+ Textarea
81
+ } from "@mesob/ui/components";
82
+ import { IconDownload, IconUsers } from "@tabler/icons-react";
83
+ import { useQueryClient } from "@tanstack/react-query";
84
+ import { useEffect, useState as useState2 } from "react";
85
+ import { useForm } from "react-hook-form";
86
+ import { toast } from "sonner";
87
+ import { z } from "zod";
88
+
89
+ // src/pages/iam/shared/navigation.tsx
90
+ import { useMesob } from "@mesob/ui/providers";
91
+ import { jsx as jsx2 } from "react/jsx-runtime";
92
+ function AppLink({ href, children, ...props }) {
93
+ const mesob = useMesob();
94
+ const Link2 = mesob?.linkComponent ?? mesob?.navigation?.Link;
95
+ const locale = mesob?.locale;
96
+ if (Link2) {
97
+ return /* @__PURE__ */ jsx2(Link2, { href, ...locale ? { locale } : {}, ...props, children });
98
+ }
99
+ return /* @__PURE__ */ jsx2("a", { href, ...props, children });
100
+ }
101
+
102
+ // src/pages/iam/shared/page-helpers.tsx
103
+ import { jsx as jsx3 } from "react/jsx-runtime";
104
+ var authApi$ = {
105
+ useQuery(...args) {
106
+ const { hooks } = useApi();
107
+ return hooks.useQuery(...args);
108
+ },
109
+ useMutation(...args) {
110
+ const { hooks } = useApi();
111
+ return hooks.useMutation(...args);
112
+ }
113
+ };
114
+ function Link(props) {
115
+ return /* @__PURE__ */ jsx3(AppLink, { ...props });
116
+ }
117
+
118
+ // src/pages/iam/users/_components/invite-user-shared.tsx
119
+ import {
120
+ Badge,
121
+ Card,
122
+ CardContent,
123
+ CardDescription,
124
+ CardHeader,
125
+ CardTitle
126
+ } from "@mesob/ui/components";
127
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
128
+ var inviteTemplateCsv = `fullName,email,phone,emailVerified,phoneVerified,sendVia,password
129
+ Lulit Demo,lulit@example.com,,true,false,email,
130
+ Abel Demo,,+251911223344,false,true,sms|email,TempPass123!
131
+ Marta Demo,marta@example.com,+251922334455,true,true,email|sms,`;
132
+ var defaultInviteUserValues = {
133
+ fullName: "",
134
+ email: "",
135
+ phone: "",
136
+ password: "",
137
+ emailVerified: false,
138
+ phoneVerified: false,
139
+ viaEmail: false,
140
+ viaSms: false
141
+ };
142
+ function buildInviteUserPayload(values) {
143
+ return {
144
+ fullName: values.fullName.trim(),
145
+ email: values.email.trim() || void 0,
146
+ phone: values.phone.trim() || void 0,
147
+ password: values.password.trim() || void 0,
148
+ emailVerified: values.emailVerified,
149
+ phoneVerified: values.phoneVerified,
150
+ sendVia: [
151
+ values.viaEmail ? "email" : null,
152
+ values.viaSms ? "sms" : null
153
+ ].filter(Boolean)
154
+ };
155
+ }
156
+ function parseCsvLine(line) {
157
+ const cells = [];
158
+ let current = "";
159
+ let quoted = false;
160
+ for (let i = 0; i < line.length; i += 1) {
161
+ const char = line[i];
162
+ const next = line[i + 1];
163
+ if (char === '"' && quoted && next === '"') {
164
+ current += '"';
165
+ i += 1;
166
+ continue;
167
+ }
168
+ if (char === '"') {
169
+ quoted = !quoted;
170
+ continue;
171
+ }
172
+ if (char === "," && !quoted) {
173
+ cells.push(current.trim());
174
+ current = "";
175
+ continue;
176
+ }
177
+ current += char;
178
+ }
179
+ cells.push(current.trim());
180
+ return cells;
181
+ }
182
+ function parseBoolean(value) {
183
+ if (!value) {
184
+ return false;
185
+ }
186
+ return ["true", "1", "yes", "y"].includes(value.trim().toLowerCase());
187
+ }
188
+ function parseSendVia(value) {
189
+ if (!value) {
190
+ return [];
191
+ }
192
+ return value.split("|").map((item) => item.trim().toLowerCase()).filter(
193
+ (item) => item === "email" || item === "sms"
194
+ );
195
+ }
196
+ function parseBulkInviteCsv(text) {
197
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
198
+ if (!lines.length) {
199
+ throw new Error("Paste at least one CSV row.");
200
+ }
201
+ const header = parseCsvLine(lines[0]).map((cell) => cell.toLowerCase());
202
+ const hasHeader = header.includes("fullname");
203
+ const rows = hasHeader ? lines.slice(1) : lines;
204
+ if (!rows.length) {
205
+ throw new Error("CSV only has a header row.");
206
+ }
207
+ const keys = hasHeader ? header : [
208
+ "fullName",
209
+ "email",
210
+ "phone",
211
+ "emailVerified",
212
+ "phoneVerified",
213
+ "sendVia",
214
+ "password"
215
+ ];
216
+ return rows.map((line, index) => {
217
+ const values = parseCsvLine(line);
218
+ const record = Object.fromEntries(
219
+ keys.map((key, valueIndex) => [key, values[valueIndex] ?? ""])
220
+ );
221
+ const fullName = String(
222
+ record.fullname || record.fullName || record.name || ""
223
+ ).trim();
224
+ const email = String(record.email || "").trim();
225
+ const phone = String(record.phone || "").trim();
226
+ if (!fullName) {
227
+ throw new Error(`Row ${index + 1}: fullName is required.`);
228
+ }
229
+ if (!(email || phone)) {
230
+ throw new Error(`Row ${index + 1}: email or phone is required.`);
231
+ }
232
+ return {
233
+ fullName,
234
+ email: email || void 0,
235
+ phone: phone || void 0,
236
+ password: String(record.password || "").trim() || void 0,
237
+ emailVerified: parseBoolean(
238
+ String(record.emailverified || record.emailVerified || "")
239
+ ),
240
+ phoneVerified: parseBoolean(
241
+ String(record.phoneverified || record.phoneVerified || "")
242
+ ),
243
+ sendVia: parseSendVia(String(record.sendvia || record.sendVia || ""))
244
+ };
245
+ });
246
+ }
247
+ function downloadInviteTemplate() {
248
+ const blob = new Blob([inviteTemplateCsv], {
249
+ type: "text/csv;charset=utf-8"
250
+ });
251
+ const url = URL.createObjectURL(blob);
252
+ const link = document.createElement("a");
253
+ link.href = url;
254
+ link.download = "users-bulk-invite-template.csv";
255
+ link.click();
256
+ URL.revokeObjectURL(url);
257
+ }
258
+ function DeliveryBadges({ delivery }) {
259
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
260
+ delivery.email ? /* @__PURE__ */ jsxs(
261
+ Badge,
262
+ {
263
+ variant: delivery.email === "failed" ? "destructive" : "outline",
264
+ children: [
265
+ "email: ",
266
+ delivery.email
267
+ ]
268
+ }
269
+ ) : null,
270
+ delivery.sms ? /* @__PURE__ */ jsxs(Badge, { variant: delivery.sms === "failed" ? "destructive" : "outline", children: [
271
+ "sms: ",
272
+ delivery.sms
273
+ ] }) : null,
274
+ delivery.email || delivery.sms ? null : /* @__PURE__ */ jsx4(Badge, { variant: "secondary", children: "no delivery" })
275
+ ] });
276
+ }
277
+ function InviteResultCard({
278
+ result,
279
+ index
280
+ }) {
281
+ return /* @__PURE__ */ jsxs(Card, { className: "border-border/60 bg-background/70", children: [
282
+ /* @__PURE__ */ jsx4(CardHeader, { className: "pb-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
283
+ /* @__PURE__ */ jsxs("div", { children: [
284
+ /* @__PURE__ */ jsx4(CardTitle, { className: "text-base", children: result.user.fullName }),
285
+ /* @__PURE__ */ jsx4(CardDescription, { children: result.user.email || result.user.phone || "No identifier" })
286
+ ] }),
287
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
288
+ typeof index === "number" ? /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
289
+ "row ",
290
+ index + 1
291
+ ] }) : null,
292
+ /* @__PURE__ */ jsx4(Badge, { variant: result.hasPassword ? "secondary" : "default", children: result.hasPassword ? "password set" : "set-password flow" })
293
+ ] })
294
+ ] }) }),
295
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-3 text-sm", children: [
296
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
297
+ /* @__PURE__ */ jsxs(Badge, { variant: result.user.emailVerified ? "default" : "secondary", children: [
298
+ "email ",
299
+ result.user.emailVerified ? "verified" : "pending"
300
+ ] }),
301
+ /* @__PURE__ */ jsxs(Badge, { variant: result.user.phoneVerified ? "default" : "secondary", children: [
302
+ "phone ",
303
+ result.user.phoneVerified ? "verified" : "pending"
304
+ ] })
305
+ ] }),
306
+ /* @__PURE__ */ jsx4(DeliveryBadges, { delivery: result.delivery }),
307
+ result.inviteUrl ? /* @__PURE__ */ jsx4("p", { className: "rounded-md border border-dashed border-border/70 bg-muted/40 px-3 py-2 font-mono text-xs break-all", children: result.inviteUrl }) : null
308
+ ] })
309
+ ] });
310
+ }
311
+
312
+ // src/pages/iam/users/_components/bulk-invite-user-form.tsx
313
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
314
+ var schema = z.object({
315
+ sourceFile: z.string().optional(),
316
+ csvText: z.string().trim().min(1, "CSV text is required")
317
+ });
318
+ var defaults = {
319
+ sourceFile: "",
320
+ csvText: ""
321
+ };
322
+ function BulkInviteUserForm({ open, onClose }) {
323
+ const qc = useQueryClient();
324
+ const [invited, setInvited] = useState2([]);
325
+ const [failed, setFailed] = useState2([]);
326
+ const form = useForm({
327
+ resolver: zodResolver(schema),
328
+ defaultValues: defaults
329
+ });
330
+ const { control, formState, getValues, reset, setValue } = form;
331
+ const inviteBulk = authApi$.useMutation("post", "/users/invite/bulk", {
332
+ onSuccess: async () => {
333
+ await qc.invalidateQueries({ queryKey: ["get", "/users"] });
334
+ }
335
+ });
336
+ useEffect(() => {
337
+ if (open) {
338
+ return;
339
+ }
340
+ reset(defaults);
341
+ setInvited([]);
342
+ setFailed([]);
343
+ }, [open, reset]);
344
+ const handleFileChange = async (event, onChange) => {
345
+ const file = event.target.files?.[0];
346
+ if (!file) {
347
+ return;
348
+ }
349
+ try {
350
+ const text = await file.text();
351
+ onChange(file.name);
352
+ setValue("csvText", text, {
353
+ shouldDirty: true,
354
+ shouldValidate: true
355
+ });
356
+ toast.success(`Loaded ${file.name}`);
357
+ } catch (error) {
358
+ toast.error(
359
+ error instanceof Error ? error.message : "Failed to read CSV file"
360
+ );
361
+ } finally {
362
+ event.target.value = "";
363
+ }
364
+ };
365
+ const onSubmit = form.handleSubmit(async (values) => {
366
+ try {
367
+ const result = await inviteBulk.mutateAsync({
368
+ body: {
369
+ users: parseBulkInviteCsv(values.csvText)
370
+ }
371
+ });
372
+ setInvited(result.invited);
373
+ setFailed(result.failed);
374
+ toast.success(
375
+ `Bulk invite finished: ${result.invited.length} invited, ${result.failed.length} failed`
376
+ );
377
+ } catch (error) {
378
+ toast.error(
379
+ error instanceof Error ? error.message : "Failed to bulk invite users"
380
+ );
381
+ }
382
+ });
383
+ return /* @__PURE__ */ jsx5(
384
+ EntityDrawer,
385
+ {
386
+ title: "Bulk invite users",
387
+ open,
388
+ onClose,
389
+ isDirty: formState.isDirty,
390
+ size: "xl",
391
+ form: /* @__PURE__ */ jsx5(Form, { ...form, children: /* @__PURE__ */ jsxs2("form", { onSubmit, className: "space-y-4", children: [
392
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
393
+ /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: "Download the CSV template, fill it with your users, then upload or paste the CSV below." }),
394
+ /* @__PURE__ */ jsx5(
395
+ Button,
396
+ {
397
+ variant: "outline",
398
+ onClick: downloadInviteTemplate,
399
+ leftIcon: /* @__PURE__ */ jsx5(IconDownload, { className: "size-4" }),
400
+ children: "Download template"
401
+ }
402
+ )
403
+ ] }),
404
+ /* @__PURE__ */ jsx5(
405
+ FormField,
406
+ {
407
+ control,
408
+ name: "sourceFile",
409
+ render: ({ field }) => /* @__PURE__ */ jsxs2(FormItem, { children: [
410
+ /* @__PURE__ */ jsx5(FormLabel, { children: "Upload CSV" }),
411
+ /* @__PURE__ */ jsx5(FormControl, { children: /* @__PURE__ */ jsx5(
412
+ Input,
413
+ {
414
+ type: "file",
415
+ accept: ".csv,text/csv",
416
+ onChange: (event) => handleFileChange(event, field.onChange)
417
+ }
418
+ ) }),
419
+ field.value ? /* @__PURE__ */ jsxs2("p", { className: "text-sm text-muted-foreground", children: [
420
+ "Loaded file: ",
421
+ field.value
422
+ ] }) : null,
423
+ /* @__PURE__ */ jsx5(FormMessage, {})
424
+ ] })
425
+ }
426
+ ),
427
+ /* @__PURE__ */ jsx5(
428
+ FormField,
429
+ {
430
+ control,
431
+ name: "csvText",
432
+ render: ({ field }) => /* @__PURE__ */ jsxs2(FormItem, { children: [
433
+ /* @__PURE__ */ jsx5(FormLabel, { children: "CSV textarea" }),
434
+ /* @__PURE__ */ jsx5(FormControl, { children: /* @__PURE__ */ jsx5(
435
+ Textarea,
436
+ {
437
+ className: "min-h-[240px] font-mono text-xs",
438
+ placeholder: inviteTemplateCsv,
439
+ ...field
440
+ }
441
+ ) }),
442
+ /* @__PURE__ */ jsx5(FormMessage, {})
443
+ ] })
444
+ }
445
+ ),
446
+ (invited.length > 0 || failed.length > 0) && /* @__PURE__ */ jsxs2(Fragment, { children: [
447
+ /* @__PURE__ */ jsxs2(Card2, { className: "border-border/60", children: [
448
+ /* @__PURE__ */ jsx5(CardHeader2, { children: /* @__PURE__ */ jsxs2(CardTitle2, { className: "flex items-center gap-2 text-base", children: [
449
+ /* @__PURE__ */ jsx5(IconUsers, { className: "size-4" }),
450
+ "Invited",
451
+ /* @__PURE__ */ jsx5(Badge2, { children: invited.length })
452
+ ] }) }),
453
+ /* @__PURE__ */ jsx5(CardContent2, { className: "space-y-3", children: invited.length ? invited.map((result, index) => /* @__PURE__ */ jsx5(
454
+ InviteResultCard,
455
+ {
456
+ result,
457
+ index
458
+ },
459
+ `${result.user.id}-${index}`
460
+ )) : /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: "Successful invites will show here." }) })
461
+ ] }),
462
+ /* @__PURE__ */ jsxs2(Card2, { className: "border-border/60", children: [
463
+ /* @__PURE__ */ jsx5(CardHeader2, { children: /* @__PURE__ */ jsxs2(CardTitle2, { className: "text-base", children: [
464
+ "Failed rows",
465
+ /* @__PURE__ */ jsx5(Badge2, { className: "ml-2", variant: "destructive", children: failed.length })
466
+ ] }) }),
467
+ /* @__PURE__ */ jsx5(CardContent2, { className: "space-y-3", children: failed.length ? failed.map((failure) => /* @__PURE__ */ jsxs2(
468
+ "div",
469
+ {
470
+ className: "rounded-lg border border-destructive/30 bg-destructive/5 p-4",
471
+ children: [
472
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-3", children: [
473
+ /* @__PURE__ */ jsxs2("span", { className: "font-medium", children: [
474
+ "row ",
475
+ failure.index + 1
476
+ ] }),
477
+ failure.identifier ? /* @__PURE__ */ jsx5(Badge2, { variant: "outline", children: failure.identifier }) : null
478
+ ] }),
479
+ /* @__PURE__ */ jsx5("p", { className: "mt-2 text-sm text-muted-foreground", children: failure.error })
480
+ ]
481
+ },
482
+ `${failure.index}-${failure.identifier}`
483
+ )) : /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: "Failed invites will show here." }) })
484
+ ] })
485
+ ] })
486
+ ] }) }),
487
+ actions: /* @__PURE__ */ jsx5(
488
+ EntityFormActions,
489
+ {
490
+ mode: "new",
491
+ onSubmit,
492
+ onReset: () => {
493
+ reset(defaults);
494
+ setInvited([]);
495
+ setFailed([]);
496
+ },
497
+ isSubmitting: inviteBulk.isPending,
498
+ submitLabel: "Invite",
499
+ disabled: !getValues("csvText").trim()
500
+ }
501
+ )
502
+ }
503
+ );
504
+ }
505
+
506
+ // src/pages/iam/users/_components/invite-user-form.tsx
507
+ import { zodResolver as zodResolver2 } from "@hookform/resolvers/zod";
508
+ import {
509
+ Checkbox,
510
+ EntityDrawer as EntityDrawer2,
511
+ EntityFormActions as EntityFormActions2,
512
+ Form as Form2,
513
+ FormControl as FormControl2,
514
+ FormField as FormField2,
515
+ FormItem as FormItem2,
516
+ FormLabel as FormLabel2,
517
+ FormMessage as FormMessage2,
518
+ Input as Input2,
519
+ Stack
520
+ } from "@mesob/ui/components";
521
+ import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
522
+ import { useEffect as useEffect2, useState as useState3 } from "react";
523
+ import { useForm as useForm2, useWatch } from "react-hook-form";
524
+ import { toast as toast2 } from "sonner";
525
+ import { z as z2 } from "zod";
526
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
527
+ var schema2 = z2.object({
528
+ fullName: z2.string().trim().min(1, "Full name is required"),
529
+ email: z2.string().email("Invalid email").or(z2.literal("")),
530
+ phone: z2.string(),
531
+ password: z2.string(),
532
+ emailVerified: z2.boolean(),
533
+ phoneVerified: z2.boolean(),
534
+ viaEmail: z2.boolean(),
535
+ viaSms: z2.boolean()
536
+ }).superRefine((value, ctx) => {
537
+ if (!(value.email.trim() || value.phone.trim())) {
538
+ ctx.addIssue({
539
+ code: z2.ZodIssueCode.custom,
540
+ path: ["email"],
541
+ message: "Email or phone is required"
542
+ });
543
+ }
544
+ });
545
+ function InviteUserForm({ open, onClose }) {
546
+ const qc = useQueryClient2();
547
+ const [result, setResult] = useState3(null);
548
+ const form = useForm2({
549
+ resolver: zodResolver2(schema2),
550
+ defaultValues: defaultInviteUserValues
551
+ });
552
+ const { control, formState, reset, setValue } = form;
553
+ const email = useWatch({ control, name: "email" }) ?? "";
554
+ const phone = useWatch({ control, name: "phone" }) ?? "";
555
+ const hasEmail = email.trim().length > 0;
556
+ const hasPhone = phone.trim().length > 0;
557
+ const inviteUser = authApi$.useMutation("post", "/users/invite", {
558
+ onSuccess: async () => {
559
+ await qc.invalidateQueries({ queryKey: ["get", "/users"] });
560
+ }
561
+ });
562
+ useEffect2(() => {
563
+ if (!hasEmail) {
564
+ setValue("emailVerified", false, { shouldDirty: false });
565
+ setValue("viaEmail", false, { shouldDirty: false });
566
+ }
567
+ }, [hasEmail, setValue]);
568
+ useEffect2(() => {
569
+ if (!hasPhone) {
570
+ setValue("phoneVerified", false, { shouldDirty: false });
571
+ setValue("viaSms", false, { shouldDirty: false });
572
+ }
573
+ }, [hasPhone, setValue]);
574
+ useEffect2(() => {
575
+ if (open) {
576
+ return;
577
+ }
578
+ reset(defaultInviteUserValues);
579
+ setResult(null);
580
+ }, [open, reset]);
581
+ const onSubmit = form.handleSubmit(async (values) => {
582
+ try {
583
+ const inviteResult = await inviteUser.mutateAsync({
584
+ body: buildInviteUserPayload(values)
585
+ });
586
+ setResult(inviteResult);
587
+ reset(defaultInviteUserValues);
588
+ toast2.success("User invited");
589
+ } catch (error) {
590
+ toast2.error(
591
+ error instanceof Error ? error.message : "Failed to invite user"
592
+ );
593
+ }
594
+ });
595
+ return /* @__PURE__ */ jsx6(
596
+ EntityDrawer2,
597
+ {
598
+ title: "Invite user",
599
+ open,
600
+ onClose,
601
+ isDirty: formState.isDirty,
602
+ size: "lg",
603
+ form: /* @__PURE__ */ jsx6(Form2, { ...form, children: /* @__PURE__ */ jsx6("form", { onSubmit, className: "space-y-4", children: /* @__PURE__ */ jsxs3(Stack, { children: [
604
+ /* @__PURE__ */ jsx6(
605
+ FormField2,
606
+ {
607
+ control,
608
+ name: "fullName",
609
+ render: ({ field }) => /* @__PURE__ */ jsxs3(FormItem2, { children: [
610
+ /* @__PURE__ */ jsxs3(FormLabel2, { children: [
611
+ "Full name ",
612
+ /* @__PURE__ */ jsx6("span", { className: "text-destructive", children: "*" })
613
+ ] }),
614
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(Input2, { placeholder: "Aster Bekele", ...field }) }),
615
+ /* @__PURE__ */ jsx6(FormMessage2, {})
616
+ ] })
617
+ }
618
+ ),
619
+ /* @__PURE__ */ jsx6(
620
+ FormField2,
621
+ {
622
+ control,
623
+ name: "email",
624
+ render: ({ field }) => /* @__PURE__ */ jsxs3(FormItem2, { children: [
625
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Email" }),
626
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(
627
+ Input2,
628
+ {
629
+ type: "email",
630
+ placeholder: "aster@example.com",
631
+ ...field
632
+ }
633
+ ) }),
634
+ /* @__PURE__ */ jsx6(FormMessage2, {})
635
+ ] })
636
+ }
637
+ ),
638
+ /* @__PURE__ */ jsx6(
639
+ FormField2,
640
+ {
641
+ control,
642
+ name: "phone",
643
+ render: ({ field }) => /* @__PURE__ */ jsxs3(FormItem2, { children: [
644
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Phone" }),
645
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(Input2, { placeholder: "+251911223344", ...field }) }),
646
+ /* @__PURE__ */ jsx6(FormMessage2, {})
647
+ ] })
648
+ }
649
+ ),
650
+ /* @__PURE__ */ jsx6(
651
+ FormField2,
652
+ {
653
+ control,
654
+ name: "password",
655
+ render: ({ field }) => /* @__PURE__ */ jsxs3(FormItem2, { children: [
656
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Password" }),
657
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(
658
+ Input2,
659
+ {
660
+ type: "password",
661
+ placeholder: "Optional",
662
+ ...field
663
+ }
664
+ ) }),
665
+ /* @__PURE__ */ jsx6(FormMessage2, {})
666
+ ] })
667
+ }
668
+ ),
669
+ /* @__PURE__ */ jsx6(
670
+ FormField2,
671
+ {
672
+ control,
673
+ name: "emailVerified",
674
+ render: ({ field }) => /* @__PURE__ */ jsx6(FormItem2, { children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-3", children: [
675
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(
676
+ Checkbox,
677
+ {
678
+ checked: field.value,
679
+ disabled: !hasEmail,
680
+ onCheckedChange: (checked) => field.onChange(checked === true)
681
+ }
682
+ ) }),
683
+ /* @__PURE__ */ jsxs3("div", { className: "grid gap-1", children: [
684
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Email verified" }),
685
+ /* @__PURE__ */ jsx6(FormMessage2, {})
686
+ ] })
687
+ ] }) })
688
+ }
689
+ ),
690
+ /* @__PURE__ */ jsx6(
691
+ FormField2,
692
+ {
693
+ control,
694
+ name: "phoneVerified",
695
+ render: ({ field }) => /* @__PURE__ */ jsx6(FormItem2, { children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-3", children: [
696
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(
697
+ Checkbox,
698
+ {
699
+ checked: field.value,
700
+ disabled: !hasPhone,
701
+ onCheckedChange: (checked) => field.onChange(checked === true)
702
+ }
703
+ ) }),
704
+ /* @__PURE__ */ jsxs3("div", { className: "grid gap-1", children: [
705
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Phone verified" }),
706
+ /* @__PURE__ */ jsx6(FormMessage2, {})
707
+ ] })
708
+ ] }) })
709
+ }
710
+ ),
711
+ /* @__PURE__ */ jsx6(
712
+ FormField2,
713
+ {
714
+ control,
715
+ name: "viaEmail",
716
+ render: ({ field }) => /* @__PURE__ */ jsx6(FormItem2, { children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-3", children: [
717
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(
718
+ Checkbox,
719
+ {
720
+ checked: field.value,
721
+ disabled: !hasEmail,
722
+ onCheckedChange: (checked) => field.onChange(checked === true)
723
+ }
724
+ ) }),
725
+ /* @__PURE__ */ jsxs3("div", { className: "grid gap-1", children: [
726
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Send email invite" }),
727
+ /* @__PURE__ */ jsx6(FormMessage2, {})
728
+ ] })
729
+ ] }) })
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx6(
733
+ FormField2,
734
+ {
735
+ control,
736
+ name: "viaSms",
737
+ render: ({ field }) => /* @__PURE__ */ jsx6(FormItem2, { children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-3", children: [
738
+ /* @__PURE__ */ jsx6(FormControl2, { children: /* @__PURE__ */ jsx6(
739
+ Checkbox,
740
+ {
741
+ checked: field.value,
742
+ disabled: !hasPhone,
743
+ onCheckedChange: (checked) => field.onChange(checked === true)
744
+ }
745
+ ) }),
746
+ /* @__PURE__ */ jsxs3("div", { className: "grid gap-1", children: [
747
+ /* @__PURE__ */ jsx6(FormLabel2, { children: "Send SMS invite" }),
748
+ /* @__PURE__ */ jsx6(FormMessage2, {})
749
+ ] })
750
+ ] }) })
751
+ }
752
+ ),
753
+ result ? /* @__PURE__ */ jsx6(InviteResultCard, { result }) : null
754
+ ] }) }) }),
755
+ actions: /* @__PURE__ */ jsx6(
756
+ EntityFormActions2,
757
+ {
758
+ mode: "new",
759
+ onSubmit,
760
+ onReset: () => {
761
+ reset(defaultInviteUserValues);
762
+ setResult(null);
763
+ },
764
+ isSubmitting: inviteUser.isPending,
765
+ submitLabel: "Invite"
766
+ }
767
+ )
768
+ }
769
+ );
770
+ }
771
+
772
+ // src/pages/iam/users/_components/users-list.tsx
773
+ import {
774
+ Badge as Badge4,
775
+ DataTableAction,
776
+ DataTablePagination,
777
+ DisplayTable,
778
+ EntityEmptyState,
779
+ EntityLoadingState,
780
+ Tbody,
781
+ Td,
782
+ Th,
783
+ Thead,
784
+ Tr
785
+ } from "@mesob/ui/components";
786
+ import { IconCalendar, IconUsers as IconUsers2 } from "@tabler/icons-react";
787
+ import { useState as useState5 } from "react";
788
+
789
+ // src/pages/iam/users/_components/user-card.tsx
790
+ import {
791
+ Badge as Badge3,
792
+ Button as Button2,
793
+ Card as Card3,
794
+ CardContent as CardContent3,
795
+ CardHeader as CardHeader3,
796
+ DropdownMenu,
797
+ DropdownMenuContent,
798
+ DropdownMenuItem,
799
+ DropdownMenuPortal,
800
+ DropdownMenuTrigger
801
+ } from "@mesob/ui/components";
802
+ import { IconDots, IconPencil } from "@tabler/icons-react";
803
+ import { useState as useState4 } from "react";
804
+
805
+ // src/pages/iam/users/_components/user-form.tsx
806
+ import { zodResolver as zodResolver3 } from "@hookform/resolvers/zod";
807
+ import {
808
+ EntityDrawer as EntityDrawer3,
809
+ EntityFormActions as EntityFormActions3,
810
+ Input as Input3,
811
+ Label,
812
+ Skeleton
813
+ } from "@mesob/ui/components";
814
+ import { useQueryClient as useQueryClient3 } from "@tanstack/react-query";
815
+ import { useEffect as useEffect3 } from "react";
816
+ import { useForm as useForm3 } from "react-hook-form";
817
+ import { z as z3 } from "zod";
818
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
819
+ var schema3 = z3.object({
820
+ fullName: z3.string().min(1, "Name is required"),
821
+ email: z3.string().email().optional().or(z3.literal("")),
822
+ phone: z3.string().optional()
823
+ });
824
+ var defaults2 = {
825
+ fullName: "",
826
+ email: "",
827
+ phone: ""
828
+ };
829
+ function UserForm({
830
+ mode,
831
+ userId,
832
+ open,
833
+ onClose,
834
+ onSuccess
835
+ }) {
836
+ const qc = useQueryClient3();
837
+ const { data, isLoading } = authApi$.useQuery(
838
+ "get",
839
+ "/users/{id}",
840
+ { params: { path: { id: userId ?? "" } } },
841
+ { enabled: mode === "edit" && !!userId }
842
+ );
843
+ const create = authApi$.useMutation("post", "/users", {
844
+ onSuccess: () => {
845
+ qc.invalidateQueries({ queryKey: ["get", "/users"] });
846
+ }
847
+ });
848
+ const update = authApi$.useMutation("put", "/users/{id}", {
849
+ onSuccess: () => {
850
+ qc.invalidateQueries({ queryKey: ["get", "/users"] });
851
+ if (userId) {
852
+ qc.invalidateQueries({ queryKey: ["get", "/users/{id}"] });
853
+ }
854
+ }
855
+ });
856
+ const del = authApi$.useMutation("delete", "/users/{id}", {
857
+ onSuccess: () => {
858
+ qc.invalidateQueries({ queryKey: ["get", "/users"] });
859
+ }
860
+ });
861
+ const form = useForm3({
862
+ resolver: zodResolver3(schema3),
863
+ defaultValues: defaults2
864
+ });
865
+ const { reset, formState } = form;
866
+ useEffect3(() => {
867
+ if (!open) {
868
+ return;
869
+ }
870
+ if (mode === "edit" && data?.user && !isLoading) {
871
+ const u = data.user;
872
+ reset({
873
+ fullName: u.fullName,
874
+ email: u.email ?? "",
875
+ phone: u.phone ?? ""
876
+ });
877
+ } else {
878
+ reset(defaults2);
879
+ }
880
+ }, [mode, data, open, isLoading, reset]);
881
+ const onSubmit = form.handleSubmit(async (d) => {
882
+ const payload = {
883
+ fullName: d.fullName,
884
+ email: d.email || void 0,
885
+ phone: d.phone || void 0
886
+ };
887
+ if (mode === "new") {
888
+ await create.mutateAsync({ body: payload });
889
+ } else if (userId) {
890
+ await update.mutateAsync({
891
+ params: { path: { id: userId } },
892
+ body: payload
893
+ });
894
+ }
895
+ onSuccess?.();
896
+ onClose();
897
+ });
898
+ const onDelete = async () => {
899
+ if (!userId) {
900
+ return;
901
+ }
902
+ await del.mutateAsync({ params: { path: { id: userId } } });
903
+ onSuccess?.();
904
+ onClose();
905
+ };
906
+ const isSubmitting = create.isPending || update.isPending;
907
+ return /* @__PURE__ */ jsx7(
908
+ EntityDrawer3,
909
+ {
910
+ title: mode === "new" ? "New user" : "Edit user",
911
+ open,
912
+ onClose,
913
+ isDirty: formState.isDirty,
914
+ form: isLoading ? /* @__PURE__ */ jsx7(FormSkeleton, {}) : /* @__PURE__ */ jsxs4("form", { onSubmit, className: "space-y-4", children: [
915
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
916
+ /* @__PURE__ */ jsxs4(Label, { htmlFor: "fullName", children: [
917
+ "Full name ",
918
+ /* @__PURE__ */ jsx7("span", { className: "text-destructive", children: "*" })
919
+ ] }),
920
+ /* @__PURE__ */ jsx7(
921
+ Input3,
922
+ {
923
+ id: "fullName",
924
+ placeholder: "Full name",
925
+ ...form.register("fullName")
926
+ }
927
+ ),
928
+ formState.errors.fullName && /* @__PURE__ */ jsx7("p", { className: "text-sm text-destructive", children: formState.errors.fullName.message })
929
+ ] }),
930
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
931
+ /* @__PURE__ */ jsx7(Label, { htmlFor: "email", children: "Email" }),
932
+ /* @__PURE__ */ jsx7(
933
+ Input3,
934
+ {
935
+ id: "email",
936
+ type: "email",
937
+ placeholder: "Email",
938
+ ...form.register("email")
939
+ }
940
+ ),
941
+ formState.errors.email && /* @__PURE__ */ jsx7("p", { className: "text-sm text-destructive", children: formState.errors.email.message })
942
+ ] }),
943
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
944
+ /* @__PURE__ */ jsx7(Label, { htmlFor: "phone", children: "Phone" }),
945
+ /* @__PURE__ */ jsx7(
946
+ Input3,
947
+ {
948
+ id: "phone",
949
+ placeholder: "Phone",
950
+ ...form.register("phone")
951
+ }
952
+ )
953
+ ] })
954
+ ] }),
955
+ actions: /* @__PURE__ */ jsx7(
956
+ EntityFormActions3,
957
+ {
958
+ mode,
959
+ onSubmit,
960
+ onReset: mode === "new" ? () => reset(defaults2) : void 0,
961
+ onDelete: mode === "edit" ? onDelete : void 0,
962
+ isSubmitting,
963
+ isDeleting: del.isPending,
964
+ disabled: isLoading,
965
+ itemName: "user"
966
+ }
967
+ )
968
+ }
969
+ );
970
+ }
971
+ function FormSkeleton() {
972
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
973
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
974
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-4 w-20" }),
975
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-10 w-full" })
976
+ ] }),
977
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
978
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-4 w-16" }),
979
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-10 w-full" })
980
+ ] }),
981
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
982
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-4 w-14" }),
983
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-10 w-full" })
984
+ ] }),
985
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
986
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-4 w-16" }),
987
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-10 w-full" })
988
+ ] })
989
+ ] });
990
+ }
991
+
992
+ // src/pages/iam/users/_components/user-card.tsx
993
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
994
+ function UserCard({ user }) {
995
+ const [editOpen, setEditOpen] = useState4(false);
996
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
997
+ /* @__PURE__ */ jsxs5(Card3, { className: "group hover:shadow-md transition-shadow", children: [
998
+ /* @__PURE__ */ jsx8(CardHeader3, { className: "pb-2", children: /* @__PURE__ */ jsxs5("div", { className: "flex items-start justify-between", children: [
999
+ /* @__PURE__ */ jsx8(
1000
+ Link,
1001
+ {
1002
+ href: `/iam/users/${user.id}`,
1003
+ className: "text-left font-semibold hover:text-primary hover:underline",
1004
+ children: user.fullName
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ jsxs5(DropdownMenu, { children: [
1008
+ /* @__PURE__ */ jsx8(
1009
+ DropdownMenuTrigger,
1010
+ {
1011
+ render: /* @__PURE__ */ jsx8(
1012
+ Button2,
1013
+ {
1014
+ variant: "ghost",
1015
+ size: "icon",
1016
+ className: "h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
1017
+ }
1018
+ ),
1019
+ children: /* @__PURE__ */ jsx8(IconDots, { className: "h-4 w-4" })
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsx8(DropdownMenuPortal, { children: /* @__PURE__ */ jsx8(DropdownMenuContent, { children: /* @__PURE__ */ jsxs5(DropdownMenuItem, { onClick: () => setEditOpen(true), children: [
1023
+ /* @__PURE__ */ jsx8(IconPencil, { className: "mr-2 h-4 w-4" }),
1024
+ "Edit"
1025
+ ] }) }) })
1026
+ ] })
1027
+ ] }) }),
1028
+ /* @__PURE__ */ jsxs5(CardContent3, { className: "space-y-2", children: [
1029
+ user.email && /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
1030
+ /* @__PURE__ */ jsx8("span", { className: "text-sm", children: user.email }),
1031
+ user.emailVerified && /* @__PURE__ */ jsx8(Badge3, { variant: "outline", className: "text-xs", children: "Verified" })
1032
+ ] }),
1033
+ /* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted-foreground", children: [
1034
+ "Last sign in",
1035
+ " ",
1036
+ user.lastSignInAt ? new Date(user.lastSignInAt).toLocaleDateString() : "never"
1037
+ ] })
1038
+ ] })
1039
+ ] }),
1040
+ editOpen && /* @__PURE__ */ jsx8(
1041
+ UserForm,
1042
+ {
1043
+ mode: "edit",
1044
+ userId: user.id,
1045
+ open: editOpen,
1046
+ onClose: () => setEditOpen(false)
1047
+ }
1048
+ )
1049
+ ] });
1050
+ }
1051
+
1052
+ // src/pages/iam/users/_components/users-list.tsx
1053
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1054
+ var TABLE_COLUMN_COUNT = 4;
1055
+ function UsersList({
1056
+ data,
1057
+ isLoading,
1058
+ view,
1059
+ pageIndex,
1060
+ pageSize,
1061
+ pageCount,
1062
+ totalRows,
1063
+ onPageChange,
1064
+ onPageSizeChange,
1065
+ onCreateNew
1066
+ }) {
1067
+ const [editingUserId, setEditingUserId] = useState5(null);
1068
+ if (isLoading) {
1069
+ return /* @__PURE__ */ jsx9(
1070
+ EntityLoadingState,
1071
+ {
1072
+ view,
1073
+ rowCount: pageSize,
1074
+ columnCount: TABLE_COLUMN_COUNT,
1075
+ cardCount: pageSize
1076
+ }
1077
+ );
1078
+ }
1079
+ if (totalRows === 0) {
1080
+ return /* @__PURE__ */ jsx9(
1081
+ EntityEmptyState,
1082
+ {
1083
+ icon: IconUsers2,
1084
+ entityName: "user",
1085
+ title: "No users yet",
1086
+ description: "Invite your first user to get started.",
1087
+ actionLabel: "Invite user",
1088
+ onAction: onCreateNew
1089
+ }
1090
+ );
1091
+ }
1092
+ if (view === "table") {
1093
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
1094
+ editingUserId && /* @__PURE__ */ jsx9(
1095
+ UserForm,
1096
+ {
1097
+ mode: "edit",
1098
+ userId: editingUserId,
1099
+ open: true,
1100
+ onClose: () => setEditingUserId(null)
1101
+ }
1102
+ ),
1103
+ /* @__PURE__ */ jsxs6(DisplayTable, { withTableBorder: true, children: [
1104
+ /* @__PURE__ */ jsx9(Thead, { children: /* @__PURE__ */ jsxs6(Tr, { children: [
1105
+ /* @__PURE__ */ jsx9(Th, { className: "w-1/4", children: "Name" }),
1106
+ /* @__PURE__ */ jsx9(Th, { className: "w-1/4", children: "Contact" }),
1107
+ /* @__PURE__ */ jsx9(Th, { className: "w-1/4", children: "Last sign in" }),
1108
+ /* @__PURE__ */ jsx9(Th, { className: "w-[50px]" })
1109
+ ] }) }),
1110
+ /* @__PURE__ */ jsx9(Tbody, { children: data.map((user) => /* @__PURE__ */ jsxs6(Tr, { className: "group", children: [
1111
+ /* @__PURE__ */ jsx9(Td, { children: /* @__PURE__ */ jsx9(
1112
+ Link,
1113
+ {
1114
+ href: `/iam/users/${user.id}`,
1115
+ className: "block text-left font-medium hover:text-primary hover:underline cursor-pointer",
1116
+ children: /* @__PURE__ */ jsx9("p", { children: user.fullName })
1117
+ }
1118
+ ) }),
1119
+ /* @__PURE__ */ jsx9(Td, { children: /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
1120
+ user.email && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
1121
+ /* @__PURE__ */ jsx9("span", { className: "text-sm", children: user.email }),
1122
+ user.emailVerified && /* @__PURE__ */ jsx9(Badge4, { variant: "outline", className: "text-xs", children: "Verified" })
1123
+ ] }),
1124
+ user.phone && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
1125
+ /* @__PURE__ */ jsx9("span", { className: "text-sm", children: user.phone }),
1126
+ user.phoneVerified && /* @__PURE__ */ jsx9(Badge4, { variant: "outline", className: "text-xs", children: "Verified" })
1127
+ ] }),
1128
+ !(user.email || user.phone) && /* @__PURE__ */ jsx9("span", { className: "text-muted-foreground", children: "\u2014" })
1129
+ ] }) }),
1130
+ /* @__PURE__ */ jsx9(Td, { children: /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1 text-muted-foreground", children: [
1131
+ /* @__PURE__ */ jsx9(IconCalendar, { className: "h-4 w-4" }),
1132
+ user.lastSignInAt ? new Date(user.lastSignInAt).toLocaleDateString() : "Never"
1133
+ ] }) }),
1134
+ /* @__PURE__ */ jsx9(Td, { children: /* @__PURE__ */ jsx9(DataTableAction, { onClick: () => setEditingUserId(user.id) }) })
1135
+ ] }, user.id)) })
1136
+ ] }),
1137
+ /* @__PURE__ */ jsx9(
1138
+ DataTablePagination,
1139
+ {
1140
+ pageIndex,
1141
+ pageSize,
1142
+ pageCount,
1143
+ totalRows,
1144
+ onPageChange,
1145
+ onPageSizeChange
1146
+ }
1147
+ )
1148
+ ] });
1149
+ }
1150
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
1151
+ /* @__PURE__ */ jsx9("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: data.map((user) => /* @__PURE__ */ jsx9(UserCard, { user }, user.id)) }),
1152
+ /* @__PURE__ */ jsx9(
1153
+ DataTablePagination,
1154
+ {
1155
+ pageIndex,
1156
+ pageSize,
1157
+ pageCount,
1158
+ totalRows,
1159
+ onPageChange,
1160
+ onPageSizeChange
1161
+ }
1162
+ )
1163
+ ] });
1164
+ }
1165
+
1166
+ // src/pages/iam/users.tsx
1167
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1168
+ function UsersPage() {
1169
+ const { hooks } = useApi();
1170
+ useBreadcrumbs({
1171
+ items: [
1172
+ { label: "Home", href: "/dashboard" },
1173
+ { label: "IAM", href: "/iam/users" },
1174
+ { label: "Users" }
1175
+ ]
1176
+ });
1177
+ const [inviteOpen, setInviteOpen] = useState6(false);
1178
+ const [bulkInviteOpen, setBulkInviteOpen] = useState6(false);
1179
+ const { queryConfig, params, setParams } = useEntityParams({
1180
+ searchKey: "search",
1181
+ searchParamName: "handle"
1182
+ });
1183
+ const usersQuery = queryConfig;
1184
+ const { data, isPending, isFetching } = hooks.useQuery(
1185
+ "get",
1186
+ "/users",
1187
+ usersQuery,
1188
+ defaultEntityQueryOptions
1189
+ );
1190
+ const isLoading = isPending || isFetching;
1191
+ const users = data?.users ?? [];
1192
+ const { total, pageCount } = useEntityPagination({
1193
+ items: users,
1194
+ total: data?.total,
1195
+ pageSize: params.pageSize
1196
+ });
1197
+ return /* @__PURE__ */ jsx10(PageContainer, { className: "flex flex-1 flex-col gap-4 p-4 pt-0", children: /* @__PURE__ */ jsxs7(PageBody, { className: "px-0", children: [
1198
+ /* @__PURE__ */ jsx10(
1199
+ EntityHeader,
1200
+ {
1201
+ icon: /* @__PURE__ */ jsx10(IconUsers3, { className: "h-5 w-5" }),
1202
+ title: "Users",
1203
+ actions: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1204
+ /* @__PURE__ */ jsx10(
1205
+ EntityDrawerTrigger,
1206
+ {
1207
+ mode: "new",
1208
+ entity: "User",
1209
+ label: "Invite user",
1210
+ open: inviteOpen,
1211
+ onOpenChange: setInviteOpen,
1212
+ children: (open, onClose) => /* @__PURE__ */ jsx10(InviteUserForm, { open, onClose })
1213
+ }
1214
+ ),
1215
+ /* @__PURE__ */ jsx10(
1216
+ EntityDrawerTrigger,
1217
+ {
1218
+ mode: "new",
1219
+ entity: "User",
1220
+ label: "Bulk invite",
1221
+ variant: "outline",
1222
+ open: bulkInviteOpen,
1223
+ onOpenChange: setBulkInviteOpen,
1224
+ children: (open, onClose) => /* @__PURE__ */ jsx10(BulkInviteUserForm, { open, onClose })
1225
+ }
1226
+ )
1227
+ ] }),
1228
+ search: /* @__PURE__ */ jsx10(EntitySearch, { paramKey: "search", placeholder: "Search users..." }),
1229
+ filter: /* @__PURE__ */ jsx10(
1230
+ EntityFilter,
1231
+ {
1232
+ options: [
1233
+ { label: "All", value: "" },
1234
+ { label: "Email Verified", value: "emailVerified" },
1235
+ { label: "Phone Verified", value: "phoneVerified" },
1236
+ { label: "Not Verified", value: "notVerified" }
1237
+ ],
1238
+ placeholder: "Filter"
1239
+ }
1240
+ ),
1241
+ sort: /* @__PURE__ */ jsx10(
1242
+ EntitySort,
1243
+ {
1244
+ options: [
1245
+ { label: "Created", value: "createdAt" },
1246
+ { label: "Updated", value: "updatedAt" },
1247
+ { label: "Name", value: "fullName" },
1248
+ { label: "Last Sign In", value: "lastSignInAt" }
1249
+ ]
1250
+ }
1251
+ ),
1252
+ view: /* @__PURE__ */ jsx10(EntityViewToggle, { views: ["table", "card"] })
1253
+ }
1254
+ ),
1255
+ /* @__PURE__ */ jsx10(
1256
+ UsersList,
1257
+ {
1258
+ data: users,
1259
+ isLoading,
1260
+ view: params.view || "table",
1261
+ pageIndex: params.page - 1,
1262
+ pageSize: params.pageSize,
1263
+ pageCount,
1264
+ totalRows: total,
1265
+ onCreateNew: () => setInviteOpen(true),
1266
+ onPageChange: (p) => setParams({ page: p + 1 }),
1267
+ onPageSizeChange: (size) => setParams({ pageSize: size, page: 1 })
1268
+ }
1269
+ )
1270
+ ] }) });
1271
+ }
1272
+ export {
1273
+ UsersPage as default
1274
+ };
1275
+ //# sourceMappingURL=users.js.map