@appcorp/fusion-storybook 0.5.3 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base-modules/user/context/use-user-module.d.ts +0 -1
- package/base-modules/user/context/use-user-module.js +57 -37
- package/base-modules/user/context.d.ts +0 -1
- package/base-modules/user/context.js +0 -1
- package/base-modules/user/form.js +12 -28
- package/base-modules/user/page.d.ts +5 -0
- package/base-modules/user/page.js +16 -10
- package/base-modules/user/validate.d.ts +10 -0
- package/base-modules/user/validate.js +32 -2
- package/base-modules/user/view.js +3 -3
- package/package.json +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/utils/resolve-rbac-permissions.js +82 -0
- package/base-modules/user/drawer.d.ts +0 -1
- package/base-modules/user/drawer.js +0 -25
- package/base-modules/user/more-actions.d.ts +0 -1
- package/base-modules/user/more-actions.js +0 -51
|
@@ -13,7 +13,6 @@ export declare const useUserModule: () => {
|
|
|
13
13
|
handleDelete: (row?: TableRow) => void;
|
|
14
14
|
handleEdit: (row?: TableRow) => void;
|
|
15
15
|
handleFilters: () => void;
|
|
16
|
-
handleMoreActions: () => void;
|
|
17
16
|
handlePageChange: (page: number | unknown) => void;
|
|
18
17
|
handlePageLimitChange: (k: string, value: object) => void;
|
|
19
18
|
handleSearch: (query: string) => void;
|
|
@@ -57,8 +57,10 @@ export const useUserModule = () => {
|
|
|
57
57
|
workspaceId: workspace === null || workspace === void 0 ? void 0 : workspace.id,
|
|
58
58
|
});
|
|
59
59
|
}, [state, workspace === null || workspace === void 0 ? void 0 : workspace.id, (_a = workspace === null || workspace === void 0 ? void 0 : workspace.school) === null || _a === void 0 ? void 0 : _a.country]);
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Browser callers must always name the tenant (the API/tenant guard needs it
|
|
61
|
+
// to scope every single-record operation).
|
|
62
|
+
const byIdParams = useMemo(() => ({ id: state.id, workspaceId: workspace === null || workspace === void 0 ? void 0 : workspace.id }), [state.id, workspace === null || workspace === void 0 ? void 0 : workspace.id]);
|
|
63
|
+
const deleteParams = useMemo(() => ({ id: state.id, workspaceId: workspace === null || workspace === void 0 ? void 0 : workspace.id }), [state.id, workspace === null || workspace === void 0 ? void 0 : workspace.id]);
|
|
62
64
|
// ============================================================================
|
|
63
65
|
// 1.4.3 UTILITIES
|
|
64
66
|
// ============================================================================
|
|
@@ -100,7 +102,6 @@ export const useUserModule = () => {
|
|
|
100
102
|
avatar: "",
|
|
101
103
|
email: "",
|
|
102
104
|
enabled: true,
|
|
103
|
-
filterEnabled: undefined,
|
|
104
105
|
id: "",
|
|
105
106
|
name: "",
|
|
106
107
|
password: "",
|
|
@@ -121,9 +122,16 @@ export const useUserModule = () => {
|
|
|
121
122
|
}
|
|
122
123
|
if (data) {
|
|
123
124
|
const response = data;
|
|
125
|
+
// The API scopes `workspaces` to the current tenant; surface that
|
|
126
|
+
// membership role so the table/view can render it.
|
|
127
|
+
const items = ((_a = response.items) !== null && _a !== void 0 ? _a : []).map((item) => {
|
|
128
|
+
var _a, _b, _c;
|
|
129
|
+
const membership = (_a = item.workspaces) === null || _a === void 0 ? void 0 : _a[0];
|
|
130
|
+
return Object.assign(Object.assign({}, item), { userRole: (_c = (_b = membership === null || membership === void 0 ? void 0 : membership.role) === null || _b === void 0 ? void 0 : _b.userRole) !== null && _c !== void 0 ? _c : "" });
|
|
131
|
+
});
|
|
124
132
|
dispatch({
|
|
125
133
|
type: USER_ACTION_TYPES.SET_ITEMS,
|
|
126
|
-
payload: { items
|
|
134
|
+
payload: { items, count: (_b = response.count) !== null && _b !== void 0 ? _b : 0 },
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
}, [dispatch, showToast, t]);
|
|
@@ -240,11 +248,12 @@ export const useUserModule = () => {
|
|
|
240
248
|
resetFormAndCloseDrawer();
|
|
241
249
|
}, [resetFormAndCloseDrawer]);
|
|
242
250
|
const handleCreate = useCallback(() => {
|
|
251
|
+
resetRecordFormState();
|
|
243
252
|
dispatch({
|
|
244
253
|
type: USER_ACTION_TYPES.SET_DRAWER,
|
|
245
254
|
payload: { drawer: USER_DRAWER.FORM_DRAWER },
|
|
246
255
|
});
|
|
247
|
-
}, [dispatch]);
|
|
256
|
+
}, [dispatch, resetRecordFormState]);
|
|
248
257
|
const handleView = useCallback((row) => {
|
|
249
258
|
resetRecordFormState();
|
|
250
259
|
byIdFetchNow === null || byIdFetchNow === void 0 ? void 0 : byIdFetchNow(undefined, { params: { id: row === null || row === void 0 ? void 0 : row.id } });
|
|
@@ -265,21 +274,15 @@ export const useUserModule = () => {
|
|
|
265
274
|
if (!confirm(t("messagesDeleteConfirmation")))
|
|
266
275
|
return;
|
|
267
276
|
deleteFetchNow === null || deleteFetchNow === void 0 ? void 0 : deleteFetchNow(undefined, {
|
|
268
|
-
body: JSON.stringify({ id: row === null || row === void 0 ? void 0 : row.id }),
|
|
277
|
+
body: JSON.stringify({ id: row === null || row === void 0 ? void 0 : row.id, workspaceId: workspace === null || workspace === void 0 ? void 0 : workspace.id }),
|
|
269
278
|
});
|
|
270
|
-
}, [deleteFetchNow, t]);
|
|
279
|
+
}, [deleteFetchNow, t, workspace === null || workspace === void 0 ? void 0 : workspace.id]);
|
|
271
280
|
const handleFilters = useCallback(() => {
|
|
272
281
|
dispatch({
|
|
273
282
|
type: USER_ACTION_TYPES.SET_DRAWER,
|
|
274
283
|
payload: { drawer: USER_DRAWER.FILTER_DRAWER },
|
|
275
284
|
});
|
|
276
285
|
}, [dispatch]);
|
|
277
|
-
const handleMoreActions = useCallback(() => {
|
|
278
|
-
dispatch({
|
|
279
|
-
type: USER_ACTION_TYPES.SET_DRAWER,
|
|
280
|
-
payload: { drawer: USER_DRAWER.MORE_ACTIONS_DRAWER },
|
|
281
|
-
});
|
|
282
|
-
}, [dispatch]);
|
|
283
286
|
const handlePageChange = useCallback((page) => {
|
|
284
287
|
const nextPage = typeof page === "number" ? page : state.currentPage + 1;
|
|
285
288
|
dispatch({
|
|
@@ -339,18 +342,33 @@ export const useUserModule = () => {
|
|
|
339
342
|
});
|
|
340
343
|
}, [dispatch]);
|
|
341
344
|
const toggleStatus = useCallback((row) => {
|
|
345
|
+
if (!(row === null || row === void 0 ? void 0 : row.id))
|
|
346
|
+
return;
|
|
347
|
+
if (!confirm(t("messagesToggleConfirmation")))
|
|
348
|
+
return;
|
|
342
349
|
updateFetchNow(undefined, {
|
|
343
|
-
body: JSON.stringify(
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
id: row.id,
|
|
352
|
+
workspaceId: workspace === null || workspace === void 0 ? void 0 : workspace.id,
|
|
353
|
+
enabled: !row.enabled,
|
|
354
|
+
}),
|
|
344
355
|
});
|
|
345
|
-
}, [updateFetchNow, workspace === null || workspace === void 0 ? void 0 : workspace.id]);
|
|
356
|
+
}, [updateFetchNow, t, workspace === null || workspace === void 0 ? void 0 : workspace.id]);
|
|
346
357
|
// ============================================================================
|
|
347
358
|
// 1.4.7 NETWORK ACTIONS
|
|
348
359
|
// ============================================================================
|
|
349
360
|
const handleUploadImage = useCallback(async () => {
|
|
350
361
|
var _a;
|
|
351
362
|
if (!((_a = state.avatar) === null || _a === void 0 ? void 0 : _a.startsWith("blob:")))
|
|
352
|
-
return
|
|
353
|
-
|
|
363
|
+
return {};
|
|
364
|
+
if (!(workspace === null || workspace === void 0 ? void 0 : workspace.id)) {
|
|
365
|
+
toastNetworkError();
|
|
366
|
+
return { failed: true };
|
|
367
|
+
}
|
|
368
|
+
// The Supabase user id does not exist yet on create — use a random path so
|
|
369
|
+
// the upload never lands on an empty/overwritten key.
|
|
370
|
+
const userKey = updateParams.id || crypto.randomUUID();
|
|
371
|
+
const fileRef = `${workspace.id}/users/${userKey}/${crypto.randomUUID()}.webp`;
|
|
354
372
|
try {
|
|
355
373
|
const blob = await fetch(state.avatar).then((r) => r.blob());
|
|
356
374
|
const webpBlob = await blobToWebP(blob, { quality: 0.8 });
|
|
@@ -383,36 +401,44 @@ export const useUserModule = () => {
|
|
|
383
401
|
type: USER_ACTION_TYPES.SET_INPUT_FIELD,
|
|
384
402
|
payload: { key: "avatar", value: url },
|
|
385
403
|
});
|
|
386
|
-
return url;
|
|
404
|
+
return { url };
|
|
387
405
|
}
|
|
388
406
|
}
|
|
389
407
|
catch (_b) {
|
|
390
408
|
toastNetworkError();
|
|
409
|
+
return { failed: true };
|
|
391
410
|
}
|
|
392
|
-
return
|
|
411
|
+
return {};
|
|
393
412
|
}, [state.avatar, dispatch, workspace === null || workspace === void 0 ? void 0 : workspace.id, updateParams.id]);
|
|
394
413
|
const checkUserEmailExists = useCallback(async (emailParam) => {
|
|
395
|
-
var _a;
|
|
414
|
+
var _a, _b;
|
|
396
415
|
const targetEmail = (typeof emailParam === "string" && emailParam) || state.email;
|
|
397
416
|
if (!targetEmail)
|
|
398
417
|
return null;
|
|
399
418
|
const result = await fetchData({
|
|
400
|
-
url: `${USER_API_ROUTES.UNIT_BY_EMAIL}?email=${encodeURIComponent(String(targetEmail))}`,
|
|
419
|
+
url: `${USER_API_ROUTES.UNIT_BY_EMAIL}?email=${encodeURIComponent(String(targetEmail))}&workspaceId=${encodeURIComponent((_a = workspace === null || workspace === void 0 ? void 0 : workspace.id) !== null && _a !== void 0 ? _a : "")}`,
|
|
401
420
|
method: API_METHODS.GET,
|
|
402
421
|
headers: {
|
|
403
422
|
// "x-api-token": process.env.NEXT_PUBLIC_API_KEY!,
|
|
404
423
|
},
|
|
405
424
|
});
|
|
406
|
-
return (
|
|
407
|
-
}, [state.email]);
|
|
425
|
+
return (_b = result === null || result === void 0 ? void 0 : result.data) !== null && _b !== void 0 ? _b : null;
|
|
426
|
+
}, [state.email, workspace === null || workspace === void 0 ? void 0 : workspace.id]);
|
|
408
427
|
const handleSubmit = useCallback(async () => {
|
|
409
428
|
dispatch({
|
|
410
429
|
type: USER_ACTION_TYPES.SET_DISABLE_SAVE_BUTTON,
|
|
411
430
|
payload: { disabled: true },
|
|
412
431
|
});
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
432
|
+
const upload = await handleUploadImage();
|
|
433
|
+
if (upload.failed) {
|
|
434
|
+
dispatch({
|
|
435
|
+
type: USER_ACTION_TYPES.SET_DISABLE_SAVE_BUTTON,
|
|
436
|
+
payload: { disabled: false },
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const finalParams = upload.url
|
|
441
|
+
? Object.assign(Object.assign({}, updateParams), { avatar: upload.url }) : updateParams;
|
|
416
442
|
validateForm({
|
|
417
443
|
params: finalParams,
|
|
418
444
|
schema: userFormValidation,
|
|
@@ -470,13 +496,7 @@ export const useUserModule = () => {
|
|
|
470
496
|
// ============================================================================
|
|
471
497
|
const headerActions = useMemo(() => [
|
|
472
498
|
{
|
|
473
|
-
enabled:
|
|
474
|
-
handleOnClick: handleMoreActions,
|
|
475
|
-
label: t("actionsButtonMoreActions"),
|
|
476
|
-
order: 0,
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
enabled: false,
|
|
499
|
+
enabled: true,
|
|
480
500
|
handleOnClick: handleFilters,
|
|
481
501
|
label: t("actionsButtonFilters"),
|
|
482
502
|
order: 1,
|
|
@@ -487,7 +507,7 @@ export const useUserModule = () => {
|
|
|
487
507
|
label: t("actionsButtonAdd"),
|
|
488
508
|
order: 2,
|
|
489
509
|
},
|
|
490
|
-
], [handleCreate, handleFilters,
|
|
510
|
+
], [handleCreate, handleFilters, t]);
|
|
491
511
|
const rowActions = useMemo(() => [
|
|
492
512
|
{
|
|
493
513
|
enabled: true,
|
|
@@ -502,13 +522,14 @@ export const useUserModule = () => {
|
|
|
502
522
|
order: 2,
|
|
503
523
|
},
|
|
504
524
|
{
|
|
505
|
-
enabled:
|
|
525
|
+
enabled: true,
|
|
506
526
|
handleOnClick: handleDelete,
|
|
507
527
|
label: t("actionsButtonDelete"),
|
|
508
528
|
order: 3,
|
|
529
|
+
variant: "destructive",
|
|
509
530
|
},
|
|
510
531
|
{
|
|
511
|
-
enabled:
|
|
532
|
+
enabled: true,
|
|
512
533
|
handleOnClick: toggleStatus,
|
|
513
534
|
label: t("actionsButtonToggleStatus"),
|
|
514
535
|
order: 4,
|
|
@@ -541,7 +562,6 @@ export const useUserModule = () => {
|
|
|
541
562
|
handleDelete,
|
|
542
563
|
handleEdit,
|
|
543
564
|
handleFilters,
|
|
544
|
-
handleMoreActions,
|
|
545
565
|
handlePageChange,
|
|
546
566
|
handlePageLimitChange,
|
|
547
567
|
handleSearch,
|
|
@@ -2,7 +2,6 @@ import { UserBE, USER_ROLE } from "../../type";
|
|
|
2
2
|
export declare const USER_DRAWER: {
|
|
3
3
|
readonly FILTER_DRAWER: string;
|
|
4
4
|
readonly FORM_DRAWER: string;
|
|
5
|
-
readonly MORE_ACTIONS_DRAWER: string;
|
|
6
5
|
readonly VIEW_DRAWER: string;
|
|
7
6
|
};
|
|
8
7
|
export declare const USER_ACTION_TYPES: {
|
|
@@ -26,7 +26,6 @@ import { pageLimit } from "./constants";
|
|
|
26
26
|
export const USER_DRAWER = {
|
|
27
27
|
FILTER_DRAWER: DRAWER_TYPES.FILTER_DRAWER,
|
|
28
28
|
FORM_DRAWER: DRAWER_TYPES.FORM_DRAWER,
|
|
29
|
-
MORE_ACTIONS_DRAWER: DRAWER_TYPES.MORE_ACTIONS_DRAWER,
|
|
30
29
|
VIEW_DRAWER: DRAWER_TYPES.VIEW_DRAWER,
|
|
31
30
|
};
|
|
32
31
|
// ============================================================================
|
|
@@ -3,19 +3,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
4
|
import { EnhancedInput } from "@appcorp/shadcn/components/enhanced-input";
|
|
5
5
|
import { EnhancedCheckbox } from "@appcorp/shadcn/components/enhanced-checkbox";
|
|
6
|
-
import {
|
|
6
|
+
import { EnhancedCombobox } from "@appcorp/shadcn/components/enhanced-combobox";
|
|
7
7
|
import { EnhancedDropzone } from "@appcorp/shadcn/components/enhanced-dropzone";
|
|
8
8
|
import { useUserModule } from "./context";
|
|
9
9
|
import { USER_ROLE } from "../../type";
|
|
10
|
-
import { USER_API_ROUTES } from "../../constants";
|
|
11
10
|
import { formatPhone } from "@react-pakistan/util-functions/general/format-phone";
|
|
12
11
|
import { useState } from "react";
|
|
13
12
|
import { Eye, EyeOff } from "lucide-react";
|
|
14
13
|
import { getTranslatedError } from "../../utils/get-translated-error";
|
|
14
|
+
import { MIN_PASSWORD_LENGTH } from "./validate";
|
|
15
15
|
export const UserForm = () => {
|
|
16
16
|
const t = useTranslations("user");
|
|
17
17
|
const { handleAvatar, handleChange, state } = useUserModule();
|
|
18
18
|
const { avatar, email, enabled, errors, name, password, phone, userRole } = state;
|
|
19
|
+
const isEdit = Boolean(state.id);
|
|
19
20
|
const roleLabelMap = {
|
|
20
21
|
SCHOOL_ADMIN: t("formOptionAdmin"),
|
|
21
22
|
TEACHER: t("formOptionTeacher"),
|
|
@@ -25,36 +26,19 @@ export const UserForm = () => {
|
|
|
25
26
|
const roleOptions = Object.values(USER_ROLE)
|
|
26
27
|
.filter((roleOption) => roleOption !== USER_ROLE.SUPER_ADMIN &&
|
|
27
28
|
roleOption !== USER_ROLE.STUDENT &&
|
|
28
|
-
roleOption !== USER_ROLE.PARENT
|
|
29
|
+
roleOption !== USER_ROLE.PARENT &&
|
|
30
|
+
roleOption !== USER_ROLE.SCHOOL_ADMIN)
|
|
29
31
|
.map((roleOption) => ({
|
|
30
32
|
id: roleOption,
|
|
31
33
|
name: roleLabelMap[roleOption] || roleOption.replace(/_/g, " "),
|
|
32
34
|
}));
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
info: t("formRoleIdInfo"),
|
|
37
|
-
label: t("formRoleIdLabel"),
|
|
38
|
-
onValueChange: (value) => handleChange("userRole", value),
|
|
39
|
-
options: roleOptions.map((opt) => ({
|
|
40
|
-
value: opt.id,
|
|
41
|
-
label: opt.name,
|
|
42
|
-
})),
|
|
43
|
-
placeholder: t("formRoleIdPlaceholder"),
|
|
44
|
-
required: true,
|
|
45
|
-
searchEndpoint: USER_API_ROUTES.ROLE,
|
|
46
|
-
searchPlaceholder: t("formSearchRolePlaceholder"),
|
|
47
|
-
formatSearchResult: (item) => {
|
|
48
|
-
var _a, _b;
|
|
49
|
-
return ({
|
|
50
|
-
label: String((_a = item.label) !== null && _a !== void 0 ? _a : ""),
|
|
51
|
-
value: String((_b = item.value) !== null && _b !== void 0 ? _b : ""),
|
|
52
|
-
});
|
|
53
|
-
},
|
|
54
|
-
value: userRole !== null && userRole !== void 0 ? userRole : "",
|
|
55
|
-
});
|
|
56
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "name", t }), id: "name", info: t("formNameInfo"), label: t("formNameLabel"), onChange: (e) => handleChange("name", e.target.value), placeholder: t("formNamePlaceholder"), required: true, value: name || "" }), _jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "email", t }), id: "email", info: t("formEmailInfo"), label: t("formEmailLabel"), onChange: (e) => handleChange("email", e.target.value), placeholder: t("formEmailPlaceholder"), required: true, type: "email", value: email }), _jsxs("div", { className: "relative", children: [_jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "password", t }), id: "password", info: t("formPasswordInfo"), label: t("formPasswordLabel"), onChange: (e) => handleChange("password", e.target.value), placeholder: t("formPasswordPlaceholder"), type: passwordVisible ? "text" : "password", value: password || "" }), _jsx("button", { type: "button", "aria-label": passwordVisible ? t("actionHidePassword") : t("actionShowPassword"), onClick: () => setPasswordVisible(!passwordVisible), className: "text-muted-foreground hover:text-foreground absolute inset-y-0 top-1 right-3 flex items-center justify-center p-1 text-sm", children: passwordVisible ? (_jsx(EyeOff, { className: "h-4 w-4" })) : (_jsx(Eye, { className: "h-4 w-4" })) })] }), _jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "phone", t }), id: "phone", info: t("formPhoneInfo"), label: t("formPhoneLabel"), onChange: (e) => {
|
|
35
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "name", t }), id: "name", info: t("formNameInfo"), label: t("formNameLabel"), onChange: (e) => handleChange("name", e.target.value), placeholder: t("formNamePlaceholder"), required: true, value: name || "" }), _jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "email", t }), id: "email", info: isEdit ? t("formEmailImmutableInfo") : t("formEmailInfo"), label: t("formEmailLabel"), onChange: (e) => handleChange("email", e.target.value), placeholder: t("formEmailPlaceholder"), readOnly: isEdit, required: true, type: "email", value: email }), _jsxs("div", { className: "relative", children: [_jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "password", t }), id: "password", info: isEdit
|
|
36
|
+
? t("formPasswordEditInfo", { length: MIN_PASSWORD_LENGTH })
|
|
37
|
+
: t("formPasswordCreateInfo", { length: MIN_PASSWORD_LENGTH }), label: t("formPasswordLabel"), onChange: (e) => handleChange("password", e.target.value), placeholder: t("formPasswordPlaceholder"), required: !isEdit, type: passwordVisible ? "text" : "password", value: password || "" }), _jsx("button", { type: "button", "aria-label": passwordVisible ? t("actionHidePassword") : t("actionShowPassword"), onClick: () => setPasswordVisible(!passwordVisible), className: "text-muted-foreground hover:text-foreground absolute inset-y-0 top-1 right-3 flex items-center justify-center p-1 text-sm", children: passwordVisible ? (_jsx(EyeOff, { className: "h-4 w-4" })) : (_jsx(Eye, { className: "h-4 w-4" })) })] }), _jsx(EnhancedInput, { error: getTranslatedError({ errors, key: "phone", t }), id: "phone", info: t("formPhoneInfo"), label: t("formPhoneLabel"), onChange: (e) => {
|
|
57
38
|
var _a;
|
|
58
39
|
return handleChange("phone", ((_a = formatPhone(e.target.value)) === null || _a === void 0 ? void 0 : _a.international) || e.target.value);
|
|
59
|
-
}, placeholder: t("formPhonePlaceholder"), type: "tel", value: phone || "" }), _jsx(EnhancedDropzone, { accept: ["image/*"], error: getTranslatedError({ errors, key: "avatar", t }), id: "avatar", info: t("formAvatarInfo"), label: t("formAvatarLabel"), maxFiles: 1, maxSize: 0.25 * 1024 * 1024, onChange: handleAvatar, onRemoveRemote: () => handleChange("avatar", ""), resize: true, value: avatar ? [avatar] : [] }), _jsx(EnhancedCheckbox, { checked: enabled, error: getTranslatedError({ errors, key: "enabled", t }), id: "enabled", info: t("formEnabledInfo"), label: t("formEnabledLabel"), onCheckedChange: (checked) => handleChange("enabled", checked) }),
|
|
40
|
+
}, placeholder: t("formPhonePlaceholder"), type: "tel", value: phone || "" }), _jsx(EnhancedDropzone, { accept: ["image/*"], error: getTranslatedError({ errors, key: "avatar", t }), id: "avatar", info: t("formAvatarInfo"), label: t("formAvatarLabel"), maxFiles: 1, maxSize: 0.25 * 1024 * 1024, onChange: handleAvatar, onRemoveRemote: () => handleChange("avatar", ""), resize: true, value: avatar ? [avatar] : [] }), _jsx(EnhancedCheckbox, { checked: enabled, error: getTranslatedError({ errors, key: "enabled", t }), id: "enabled", info: t("formEnabledInfo"), label: t("formEnabledLabel"), onCheckedChange: (checked) => handleChange("enabled", checked) }), _jsx(EnhancedCombobox, { emptyText: t("formNoRoleEmpty"), error: getTranslatedError({ errors, key: "userRole", t }), id: "userRole", info: t("formRoleIdInfo"), label: t("formRoleIdLabel"), onValueChange: (value) => handleChange("userRole", value), options: roleOptions.map((opt) => ({
|
|
41
|
+
value: opt.id,
|
|
42
|
+
label: opt.name,
|
|
43
|
+
})), placeholder: t("formRoleIdPlaceholder"), required: true, searchPlaceholder: t("formSearchRolePlaceholder"), value: userRole !== null && userRole !== void 0 ? userRole : "" })] }));
|
|
60
44
|
};
|
|
@@ -34,6 +34,11 @@ interface Props {
|
|
|
34
34
|
tableColumnHeaderId: string;
|
|
35
35
|
tableColumnHeaderName: string;
|
|
36
36
|
tableColumnHeaderPhone: string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional so consumers on an older package release still compile; defaults
|
|
39
|
+
* to the `user` namespace translation.
|
|
40
|
+
*/
|
|
41
|
+
tableColumnHeaderRole?: string;
|
|
37
42
|
tableDescription: string;
|
|
38
43
|
tableSearchPlaceholder: string;
|
|
39
44
|
tableTitle: string;
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"use client";
|
|
19
19
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
20
20
|
import { useMemo } from "react";
|
|
21
|
+
import { useTranslations } from "next-intl";
|
|
21
22
|
import { COMPONENT_TYPE } from "@appcorp/shadcn/components/enhanced-table";
|
|
22
23
|
import { createGenericModulePage, } from "@react-pakistan/util-functions/factory/generic-component-factory";
|
|
23
24
|
import { useUserModule, UserProvider, USER_ACTION_TYPES } from "./context";
|
|
24
25
|
import { UserFilter } from "./filter";
|
|
25
26
|
import { UserForm } from "./form";
|
|
26
27
|
import { UserView } from "./view";
|
|
27
|
-
import { UserMoreActions } from "./more-actions";
|
|
28
28
|
import { resolveRbacPermissions } from "../../utils/resolve-rbac-permissions";
|
|
29
29
|
import { RbacNoAccess } from "../../components/rbac-no-access";
|
|
30
30
|
// ============================================================================
|
|
@@ -34,6 +34,7 @@ const tableBodyCols = [
|
|
|
34
34
|
{ componentType: COMPONENT_TYPE.ID, key: "id" },
|
|
35
35
|
{ componentType: COMPONENT_TYPE.TEXT, key: "name" },
|
|
36
36
|
{ componentType: COMPONENT_TYPE.TEXT, key: "email" },
|
|
37
|
+
{ componentType: COMPONENT_TYPE.TEXT, key: "userRole" },
|
|
37
38
|
{ componentType: COMPONENT_TYPE.TEXT, key: "phone" },
|
|
38
39
|
{ componentType: COMPONENT_TYPE.BOOLEAN, key: "enabled" },
|
|
39
40
|
{ componentType: COMPONENT_TYPE.ACTIONS },
|
|
@@ -44,20 +45,20 @@ const tableBodyCols = [
|
|
|
44
45
|
const createComponentInstances = () => ({
|
|
45
46
|
filter: _jsx(UserFilter, {}),
|
|
46
47
|
form: _jsx(UserForm, {}),
|
|
47
|
-
moreActions: _jsx(UserMoreActions, {}),
|
|
48
48
|
view: _jsx(UserView, {}),
|
|
49
49
|
});
|
|
50
|
-
const createUserConfig = ({ dispatch, drawerButtonCancel, drawerButtonSave, drawerFilterDescription, drawerFilterTitle, drawerFormDescription, drawerFormTitle, drawerMoreActionsDescription, drawerMoreActionsTitle, drawerViewDescription, drawerViewTitle, tableColumnHeaderActions, tableColumnHeaderEmail, tableColumnHeaderEnabled, tableColumnHeaderId, tableColumnHeaderName, tableColumnHeaderPhone, tableDescription, tableSearchPlaceholder, tableTitle, }) => {
|
|
50
|
+
const createUserConfig = ({ dispatch, drawerButtonCancel, drawerButtonSave, drawerFilterDescription, drawerFilterTitle, drawerFormDescription, drawerFormTitle, drawerMoreActionsDescription, drawerMoreActionsTitle, drawerViewDescription, drawerViewTitle, tableColumnHeaderActions, tableColumnHeaderEmail, tableColumnHeaderEnabled, tableColumnHeaderId, tableColumnHeaderName, tableColumnHeaderPhone, tableColumnHeaderRole, tableDescription, tableSearchPlaceholder, tableTitle, }) => {
|
|
51
51
|
const components = createComponentInstances();
|
|
52
52
|
return {
|
|
53
53
|
moduleName: "user",
|
|
54
54
|
tableColumns: [
|
|
55
55
|
{ label: tableColumnHeaderId, width: "5%" },
|
|
56
|
-
{ label: tableColumnHeaderName, width: "
|
|
57
|
-
{ label: tableColumnHeaderEmail, width: "
|
|
58
|
-
{ label:
|
|
56
|
+
{ label: tableColumnHeaderName, width: "22%" },
|
|
57
|
+
{ label: tableColumnHeaderEmail, width: "24%" },
|
|
58
|
+
{ label: tableColumnHeaderRole, width: "13%" },
|
|
59
|
+
{ label: tableColumnHeaderPhone, width: "17%" },
|
|
59
60
|
{ label: tableColumnHeaderEnabled, width: "10%" },
|
|
60
|
-
{ label: tableColumnHeaderActions, width: "
|
|
61
|
+
{ label: tableColumnHeaderActions, width: "9%" },
|
|
61
62
|
],
|
|
62
63
|
cancelLabel: drawerButtonCancel,
|
|
63
64
|
drawerFilterDescription,
|
|
@@ -70,7 +71,7 @@ const createUserConfig = ({ dispatch, drawerButtonCancel, drawerButtonSave, draw
|
|
|
70
71
|
drawerViewTitle,
|
|
71
72
|
filterContent: components.filter,
|
|
72
73
|
formContent: components.form,
|
|
73
|
-
moreActionsContent:
|
|
74
|
+
moreActionsContent: null,
|
|
74
75
|
saveLabel: drawerButtonSave,
|
|
75
76
|
searchPlaceholder: tableSearchPlaceholder,
|
|
76
77
|
tableDescription,
|
|
@@ -86,7 +87,10 @@ const createUserConfig = ({ dispatch, drawerButtonCancel, drawerButtonSave, draw
|
|
|
86
87
|
// ============================================================================
|
|
87
88
|
const GenericUserPage = createGenericModulePage();
|
|
88
89
|
const UserPageInner = (props) => {
|
|
90
|
+
var _a;
|
|
89
91
|
const context = useUserModule();
|
|
92
|
+
const t = useTranslations("user");
|
|
93
|
+
const roleHeader = (_a = props.tableColumnHeaderRole) !== null && _a !== void 0 ? _a : t("tableColumnHeaderRole");
|
|
90
94
|
const userConfig = useMemo(() => createUserConfig({
|
|
91
95
|
dispatch: context.dispatch,
|
|
92
96
|
drawerButtonCancel: props.drawerButtonCancel,
|
|
@@ -105,6 +109,7 @@ const UserPageInner = (props) => {
|
|
|
105
109
|
tableColumnHeaderId: props.tableColumnHeaderId,
|
|
106
110
|
tableColumnHeaderName: props.tableColumnHeaderName,
|
|
107
111
|
tableColumnHeaderPhone: props.tableColumnHeaderPhone,
|
|
112
|
+
tableColumnHeaderRole: roleHeader,
|
|
108
113
|
tableDescription: props.tableDescription,
|
|
109
114
|
tableSearchPlaceholder: props.tableSearchPlaceholder,
|
|
110
115
|
tableTitle: props.tableTitle,
|
|
@@ -126,16 +131,17 @@ const UserPageInner = (props) => {
|
|
|
126
131
|
props.tableColumnHeaderId,
|
|
127
132
|
props.tableColumnHeaderName,
|
|
128
133
|
props.tableColumnHeaderPhone,
|
|
134
|
+
roleHeader,
|
|
129
135
|
props.tableDescription,
|
|
130
136
|
props.tableSearchPlaceholder,
|
|
131
137
|
props.tableTitle,
|
|
132
138
|
]);
|
|
133
139
|
const hasPermission = resolveRbacPermissions({
|
|
134
140
|
userRole: props.userRole,
|
|
135
|
-
moduleName: "
|
|
141
|
+
moduleName: "User",
|
|
136
142
|
});
|
|
137
143
|
if (!hasPermission) {
|
|
138
|
-
return _jsx(RbacNoAccess, { moduleName: "
|
|
144
|
+
return _jsx(RbacNoAccess, { moduleName: "User" });
|
|
139
145
|
}
|
|
140
146
|
return (_jsx("div", { className: "p-4", children: _jsx(GenericUserPage, { overrideConfig: userConfig, context: context, tableBodyCols: tableBodyCols }) }));
|
|
141
147
|
};
|
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
* User Validation Schema
|
|
3
3
|
*
|
|
4
4
|
* Zod validation schemas for user form data with i18n-ready error keys.
|
|
5
|
+
*
|
|
6
|
+
* Password rules:
|
|
7
|
+
* - required on create (the onboarding email delivers it),
|
|
8
|
+
* - optional on edit (an empty value keeps the current password),
|
|
9
|
+
* - at least `MIN_PASSWORD_LENGTH` characters whenever provided.
|
|
5
10
|
*/
|
|
6
11
|
import { z } from "zod";
|
|
7
12
|
import { USER_ROLE } from "../../type";
|
|
13
|
+
export declare const MIN_PASSWORD_LENGTH = 8;
|
|
8
14
|
export declare const VALIDATION_KEYS: {
|
|
9
15
|
readonly name: "validationUserNameRequired";
|
|
10
16
|
readonly emailInvalid: "validationUserEmailInvalid";
|
|
11
17
|
readonly phoneInvalid: "validationUserPhoneInvalid";
|
|
12
18
|
readonly roleInvalid: "validationUserRoleInvalid";
|
|
13
19
|
readonly roleSchoolAdminNotAllowed: "validationUserRoleSchoolAdminNotAllowed";
|
|
20
|
+
readonly passwordRequired: "validationUserPasswordRequired";
|
|
21
|
+
readonly passwordMinLength: "validationUserPasswordMinLength";
|
|
14
22
|
};
|
|
15
23
|
export declare const userFormValidation: z.ZodObject<{
|
|
24
|
+
id: z.ZodOptional<z.ZodString>;
|
|
16
25
|
name: z.ZodString;
|
|
17
26
|
email: z.ZodEmail;
|
|
18
27
|
password: z.ZodOptional<z.ZodString>;
|
|
@@ -21,3 +30,4 @@ export declare const userFormValidation: z.ZodObject<{
|
|
|
21
30
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
22
31
|
userRole: z.ZodEnum<typeof USER_ROLE> & z.ZodType<USER_ROLE.SUPER_ADMIN | USER_ROLE.TEACHER | USER_ROLE.STUDENT | USER_ROLE.PARENT | USER_ROLE.STAFF, USER_ROLE, z.core.$ZodTypeInternals<USER_ROLE.SUPER_ADMIN | USER_ROLE.TEACHER | USER_ROLE.STUDENT | USER_ROLE.PARENT | USER_ROLE.STAFF, USER_ROLE>>;
|
|
23
32
|
}, z.core.$strip>;
|
|
33
|
+
export type UserForm = z.infer<typeof userFormValidation>;
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* User Validation Schema
|
|
3
3
|
*
|
|
4
4
|
* Zod validation schemas for user form data with i18n-ready error keys.
|
|
5
|
+
*
|
|
6
|
+
* Password rules:
|
|
7
|
+
* - required on create (the onboarding email delivers it),
|
|
8
|
+
* - optional on edit (an empty value keeps the current password),
|
|
9
|
+
* - at least `MIN_PASSWORD_LENGTH` characters whenever provided.
|
|
5
10
|
*/
|
|
6
11
|
import { z } from "zod";
|
|
7
12
|
import { USER_ROLE } from "../../type";
|
|
@@ -9,24 +14,29 @@ import { toE164Phone } from "../../utils/to-e164-phone";
|
|
|
9
14
|
// ============================================================================
|
|
10
15
|
// VALIDATION KEYS
|
|
11
16
|
// ============================================================================
|
|
17
|
+
export const MIN_PASSWORD_LENGTH = 8;
|
|
12
18
|
export const VALIDATION_KEYS = {
|
|
13
19
|
name: "validationUserNameRequired",
|
|
14
20
|
emailInvalid: "validationUserEmailInvalid",
|
|
15
21
|
phoneInvalid: "validationUserPhoneInvalid",
|
|
16
22
|
roleInvalid: "validationUserRoleInvalid",
|
|
17
23
|
roleSchoolAdminNotAllowed: "validationUserRoleSchoolAdminNotAllowed",
|
|
24
|
+
passwordRequired: "validationUserPasswordRequired",
|
|
25
|
+
passwordMinLength: "validationUserPasswordMinLength",
|
|
18
26
|
};
|
|
19
27
|
// ============================================================================
|
|
20
28
|
// VALIDATION SCHEMA
|
|
21
29
|
// ============================================================================
|
|
22
|
-
export const userFormValidation = z
|
|
30
|
+
export const userFormValidation = z
|
|
31
|
+
.object({
|
|
32
|
+
id: z.string().optional(),
|
|
23
33
|
name: z.string().min(1, VALIDATION_KEYS.name),
|
|
24
34
|
email: z.email(VALIDATION_KEYS.emailInvalid),
|
|
25
35
|
password: z.string().optional(),
|
|
26
36
|
phone: z
|
|
27
37
|
.string()
|
|
28
38
|
.optional()
|
|
29
|
-
.transform((val) => { var _a; return (val ? (_a = toE164Phone(val)) !== null && _a !== void 0 ? _a : val : val); }),
|
|
39
|
+
.transform((val) => { var _a; return (val ? ((_a = toE164Phone(val)) !== null && _a !== void 0 ? _a : val) : val); }),
|
|
30
40
|
avatar: z.string().optional(),
|
|
31
41
|
enabled: z.boolean().optional(),
|
|
32
42
|
userRole: z
|
|
@@ -36,4 +46,24 @@ export const userFormValidation = z.object({
|
|
|
36
46
|
.refine((v) => v !== USER_ROLE.SCHOOL_ADMIN, {
|
|
37
47
|
message: VALIDATION_KEYS.roleSchoolAdminNotAllowed,
|
|
38
48
|
}),
|
|
49
|
+
})
|
|
50
|
+
.superRefine((value, ctx) => {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
const password = (_b = (_a = value.password) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : "";
|
|
53
|
+
const isCreate = !value.id;
|
|
54
|
+
if (isCreate && !password) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: "custom",
|
|
57
|
+
message: VALIDATION_KEYS.passwordRequired,
|
|
58
|
+
path: ["password"],
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (password && password.length < MIN_PASSWORD_LENGTH) {
|
|
63
|
+
ctx.addIssue({
|
|
64
|
+
code: "custom",
|
|
65
|
+
message: VALIDATION_KEYS.passwordMinLength,
|
|
66
|
+
path: ["password"],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
39
69
|
});
|
|
@@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@app
|
|
|
6
6
|
import { Badge } from "@appcorp/shadcn/components/ui/badge";
|
|
7
7
|
import { Separator } from "@appcorp/shadcn/components/ui/separator";
|
|
8
8
|
import { Avatar, AvatarFallback, AvatarImage, } from "@appcorp/shadcn/components/ui/avatar";
|
|
9
|
-
import { User, Phone, Mail, CheckCircle2, XCircle } from "lucide-react";
|
|
9
|
+
import { User, Phone, Mail, Shield, CheckCircle2, XCircle } from "lucide-react";
|
|
10
10
|
import { formatValue } from "@react-pakistan/util-functions/general/format-value";
|
|
11
11
|
import { formatPhoneDisplay } from "@react-pakistan/util-functions/general/format-phone-display";
|
|
12
12
|
const getInitials = (fullName) => {
|
|
@@ -22,6 +22,6 @@ const getInitials = (fullName) => {
|
|
|
22
22
|
export const UserView = () => {
|
|
23
23
|
const t = useTranslations("user");
|
|
24
24
|
const { state } = useUserModule();
|
|
25
|
-
const { avatar, email, enabled, name, phone } = state;
|
|
26
|
-
return (_jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(User, { className: "text-primary h-5 w-5" }), _jsx(CardTitle, { className: "text-lg", children: t("viewFieldUserInformation") })] }), _jsx(CardDescription, { children: t("viewUserAccountDetails") })] }), _jsx(Separator, {}), _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "grid grid-cols-1 gap-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldProfilePicture") }), _jsxs(Avatar, { className: "h-20 w-20", children: [_jsx(AvatarImage, { src: avatar || undefined, alt: name || t("viewFieldName") }), _jsx(AvatarFallback, { className: "text-lg", children: getInitials(name) })] })] }), _jsxs("div", { className: "space-y-1", children: [
|
|
25
|
+
const { avatar, email, enabled, name, phone, userRole } = state;
|
|
26
|
+
return (_jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(User, { className: "text-primary h-5 w-5" }), _jsx(CardTitle, { className: "text-lg", children: t("viewFieldUserInformation") })] }), _jsx(CardDescription, { children: t("viewUserAccountDetails") })] }), _jsx(Separator, {}), _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "grid grid-cols-1 gap-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldProfilePicture") }), _jsxs(Avatar, { className: "h-20 w-20", children: [_jsx(AvatarImage, { src: avatar || undefined, alt: name || t("viewFieldName") }), _jsx(AvatarFallback, { className: "text-lg", children: getInitials(name) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("formNameLabel") }), _jsx("p", { className: "text-base", children: formatValue(name) })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "text-muted-foreground h-4 w-4" }), _jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldRole") })] }), _jsx("p", { className: "text-base", children: formatValue(userRole) })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "text-muted-foreground h-4 w-4" }), _jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("formEmailLabel") })] }), _jsx("p", { className: "text-base", children: formatValue(email) })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Phone, { className: "text-muted-foreground h-4 w-4" }), _jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("formPhoneLabel") })] }), _jsx("p", { className: "text-base", children: formatPhoneDisplay(phone) || formatValue(phone) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldStatus") }), _jsxs(Badge, { variant: enabled ? "default" : "destructive", className: "gap-1", children: [enabled ? (_jsx(CheckCircle2, { className: "h-3 w-3" })) : (_jsx(XCircle, { className: "h-3 w-3" })), enabled ? t("badgeActive") : t("badgeInactive")] })] })] }) })] }) }));
|
|
27
27
|
};
|