@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.
@@ -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
- const byIdParams = useMemo(() => ({ id: state.id }), [state.id]);
61
- const deleteParams = useMemo(() => ({ id: state.id }), [state.id]);
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: (_a = response.items) !== null && _a !== void 0 ? _a : [], count: (_b = response.count) !== null && _b !== void 0 ? _b : 0 },
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(Object.assign(Object.assign({}, row), { workspaceId: workspace === null || workspace === void 0 ? void 0 : workspace.id, enabled: !(row === null || row === void 0 ? void 0 : row.enabled) })),
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 undefined;
353
- const fileRef = `${workspace === null || workspace === void 0 ? void 0 : workspace.id}/${updateParams.id}/avatar/${updateParams.id}.webp`;
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 undefined;
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 (_a = result === null || result === void 0 ? void 0 : result.data) !== null && _a !== void 0 ? _a : null;
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 uploadedUrl = await handleUploadImage();
414
- const finalParams = uploadedUrl
415
- ? Object.assign(Object.assign({}, updateParams), { avatar: uploadedUrl }) : updateParams;
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: false,
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, handleMoreActions, t]);
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: false,
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: false,
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 { useEnhancedCombobox } from "@appcorp/shadcn/hooks/use-enhanced-combobox";
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
- const { enhancedComboboxElement: userRoleCombo } = useEnhancedCombobox({
34
- emptyText: t("formNoRoleEmpty"),
35
- id: "userRole",
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) }), userRoleCombo] }));
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: "30%" },
57
- { label: tableColumnHeaderEmail, width: "30%" },
58
- { label: tableColumnHeaderPhone, width: "20%" },
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: "5%" },
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: components.moreActions,
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: "People Management",
141
+ moduleName: "User",
136
142
  });
137
143
  if (!hasPermission) {
138
- return _jsx(RbacNoAccess, { moduleName: "People Management" });
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.object({
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: [_jsxs("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(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")] })] })] }) })] }) }));
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appcorp/fusion-storybook",
3
- "version": "0.5.3",
3
+ "version": "0.5.6",
4
4
  "scripts": {
5
5
  "build-storybook": "storybook build",
6
6
  "build:next": "next build",