@palladium-ethiopia/esm-admin-app 5.4.2-pre.100

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 (149) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/README.md +12 -0
  3. package/dist/117.js +1 -0
  4. package/dist/152.js +1 -0
  5. package/dist/152.js.map +1 -0
  6. package/dist/209.js +1 -0
  7. package/dist/209.js.map +1 -0
  8. package/dist/41.js +1 -0
  9. package/dist/41.js.map +1 -0
  10. package/dist/442.js +1 -0
  11. package/dist/442.js.map +1 -0
  12. package/dist/466.js +1 -0
  13. package/dist/466.js.map +1 -0
  14. package/dist/555.js +1 -0
  15. package/dist/555.js.map +1 -0
  16. package/dist/61.js +1 -0
  17. package/dist/61.js.map +1 -0
  18. package/dist/672.js +15 -0
  19. package/dist/672.js.map +1 -0
  20. package/dist/689.js +1 -0
  21. package/dist/689.js.map +1 -0
  22. package/dist/710.js +1 -0
  23. package/dist/710.js.map +1 -0
  24. package/dist/712.js +1 -0
  25. package/dist/712.js.map +1 -0
  26. package/dist/771.js +1 -0
  27. package/dist/771.js.map +1 -0
  28. package/dist/789.js +1 -0
  29. package/dist/789.js.map +1 -0
  30. package/dist/806.js +1 -0
  31. package/dist/826.js +1 -0
  32. package/dist/826.js.map +1 -0
  33. package/dist/914.js +27 -0
  34. package/dist/914.js.map +1 -0
  35. package/dist/926.js +17 -0
  36. package/dist/926.js.map +1 -0
  37. package/dist/ethiopia-esm-admin-app.js +6 -0
  38. package/dist/ethiopia-esm-admin-app.js.buildmanifest.json +556 -0
  39. package/dist/ethiopia-esm-admin-app.js.map +1 -0
  40. package/dist/main.js +32 -0
  41. package/dist/main.js.map +1 -0
  42. package/dist/routes.json +1 -0
  43. package/jest.config.js +8 -0
  44. package/package.json +52 -0
  45. package/rspack.config.js +1 -0
  46. package/src/components/confirm-modal/confirmation-operation-modal.component.tsx +43 -0
  47. package/src/components/confirm-modal/confirmation-operation.test.tsx +69 -0
  48. package/src/components/dashboard/dashboard.component.tsx +131 -0
  49. package/src/components/dashboard/dashboard.scss +38 -0
  50. package/src/components/dashboard/etl-dashboard.component.tsx +11 -0
  51. package/src/components/empty-state/empty-state-log.components.tsx +20 -0
  52. package/src/components/empty-state/empty-state-log.scss +28 -0
  53. package/src/components/empty-state/empty-state-log.test.tsx +24 -0
  54. package/src/components/facility-setup/card.component.tsx +16 -0
  55. package/src/components/facility-setup/facility-info.component.tsx +142 -0
  56. package/src/components/facility-setup/facility-info.scss +87 -0
  57. package/src/components/facility-setup/facility-setup.component.tsx +21 -0
  58. package/src/components/facility-setup/facility-setup.resource.tsx +7 -0
  59. package/src/components/facility-setup/facility-setup.scss +38 -0
  60. package/src/components/facility-setup/header/header.component.tsx +23 -0
  61. package/src/components/facility-setup/header/header.scss +19 -0
  62. package/src/components/header/header-illustration.component.tsx +13 -0
  63. package/src/components/header/header.component.tsx +28 -0
  64. package/src/components/header/header.scss +19 -0
  65. package/src/components/hook/healthWorkerAdapter.ts +213 -0
  66. package/src/components/hook/useFacilityInfo.tsx +37 -0
  67. package/src/components/hook/useSystemRoleSetting.tsx +33 -0
  68. package/src/components/locations/auto-suggest/autosuggest.component.tsx +149 -0
  69. package/src/components/locations/auto-suggest/autosuggest.scss +61 -0
  70. package/src/components/locations/auto-suggest/location-autosuggest.component.tsx +94 -0
  71. package/src/components/locations/auto-suggest/location-autosuggest.scss +48 -0
  72. package/src/components/locations/common/results-tile.component.tsx +45 -0
  73. package/src/components/locations/common/results-tile.scss +86 -0
  74. package/src/components/locations/forms/add-location/add-location.workspace.scss +34 -0
  75. package/src/components/locations/forms/add-location/add-location.workspace.tsx +200 -0
  76. package/src/components/locations/forms/search-location/search-location.workspace.scss +79 -0
  77. package/src/components/locations/forms/search-location/search-location.workspace.tsx +215 -0
  78. package/src/components/locations/header/header.component.tsx +48 -0
  79. package/src/components/locations/header/header.scss +58 -0
  80. package/src/components/locations/helpers/index.ts +16 -0
  81. package/src/components/locations/home/home-locations.component.tsx +18 -0
  82. package/src/components/locations/home/home-locations.scss +8 -0
  83. package/src/components/locations/hooks/UseFacilityLocations.ts +12 -0
  84. package/src/components/locations/hooks/useLocation.ts +18 -0
  85. package/src/components/locations/hooks/useLocationTags.ts +15 -0
  86. package/src/components/locations/tables/locations-table.component.tsx +243 -0
  87. package/src/components/locations/tables/locations-table.resource.ts +26 -0
  88. package/src/components/locations/tables/locations-table.scss +115 -0
  89. package/src/components/locations/types/index.ts +120 -0
  90. package/src/components/locations/utils/index.ts +5 -0
  91. package/src/components/logs-table/operation-log-resource.ts +41 -0
  92. package/src/components/logs-table/operation-log-table.component.tsx +120 -0
  93. package/src/components/logs-table/operation-log.scss +10 -0
  94. package/src/components/logs-table/operation-log.test.tsx +47 -0
  95. package/src/components/modal/hwr-confirmation.modal.scss +21 -0
  96. package/src/components/modal/hwr-confirmation.modal.tsx +170 -0
  97. package/src/components/modal/hwr-empty.modal.component.tsx +54 -0
  98. package/src/components/modal/hwr-sync.modal.scss +30 -0
  99. package/src/components/modal/hwr-sync.modal.tsx +209 -0
  100. package/src/components/modal/hwr-sync.resource.ts +23 -0
  101. package/src/components/provider-banner/provider-banner.component.tsx +106 -0
  102. package/src/components/provider-banner/provider-banner.module.scss +51 -0
  103. package/src/components/provider-banner/provider-banner.resource.ts +29 -0
  104. package/src/components/side-menu/left-panel.scss +42 -0
  105. package/src/components/side-menu/left-pannel.component.tsx +22 -0
  106. package/src/components/users/header/header.scss +90 -0
  107. package/src/components/users/header/user-management-header.component.tsx +42 -0
  108. package/src/components/users/manage-users/hooks/useProviderAttributeMapping.ts +110 -0
  109. package/src/components/users/manage-users/hooks/useUserFormSteps.ts +119 -0
  110. package/src/components/users/manage-users/hooks/useUserFormSubmission.ts +264 -0
  111. package/src/components/users/manage-users/hooks/useUserManagementForm.ts +122 -0
  112. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-list/user-role-scope-list.component.tsx +177 -0
  113. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-fields.scss +117 -0
  114. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope-fields.component.tsx +290 -0
  115. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope.workspace.tsx +316 -0
  116. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/userRoleScopeFormSchema.tsx +43 -0
  117. package/src/components/users/manage-users/manage-user.component.tsx +19 -0
  118. package/src/components/users/manage-users/manage-user.scss +31 -0
  119. package/src/components/users/manage-users/provider-autosuggest.component.tsx +117 -0
  120. package/src/components/users/manage-users/provider-search.resource.ts +34 -0
  121. package/src/components/users/manage-users/sections/demographic-section.component.tsx +156 -0
  122. package/src/components/users/manage-users/sections/login-section.component.tsx +88 -0
  123. package/src/components/users/manage-users/sections/provider-section.component.tsx +270 -0
  124. package/src/components/users/manage-users/sections/roles-section.component.tsx +88 -0
  125. package/src/components/users/manage-users/user-details/user-detail.scss +75 -0
  126. package/src/components/users/manage-users/user-details/user-details.component.tsx +182 -0
  127. package/src/components/users/manage-users/user-list/user-list.component.tsx +378 -0
  128. package/src/components/users/manage-users/user-list/user-list.resource.ts +30 -0
  129. package/src/components/users/manage-users/user-list/user-list.scss +37 -0
  130. package/src/components/users/manage-users/user-management.constants.ts +20 -0
  131. package/src/components/users/manage-users/user-management.utils.ts +100 -0
  132. package/src/components/users/manage-users/user-management.workspace.scss +172 -0
  133. package/src/components/users/manage-users/user-management.workspace.tsx +334 -0
  134. package/src/components/users/userManagementFormSchema.tsx +179 -0
  135. package/src/config-schema.ts +142 -0
  136. package/src/constants.ts +50 -0
  137. package/src/declarations.d.ts +2 -0
  138. package/src/index.ts +55 -0
  139. package/src/left-pannel-link.component.tsx +40 -0
  140. package/src/root.component.tsx +39 -0
  141. package/src/root.scss +12 -0
  142. package/src/routes.json +100 -0
  143. package/src/setup-tests.ts +1 -0
  144. package/src/types/index.ts +385 -0
  145. package/src/user-management.resources.ts +232 -0
  146. package/src/utils/utils.ts +20 -0
  147. package/translations/am.json +159 -0
  148. package/translations/en.json +159 -0
  149. package/tsconfig.json +5 -0
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar, Location, UserFollow } from '@carbon/react/icons';
4
+ import { formatDate, useSession, ServiceQueuesPictogram } from '@openmrs/esm-framework';
5
+ import styles from './header.scss';
6
+
7
+ interface HeaderProps {
8
+ title: string;
9
+ }
10
+
11
+ const Header: React.FC<HeaderProps> = ({ title }) => {
12
+ const { t } = useTranslation();
13
+ const session = useSession();
14
+ const location = session?.sessionLocation?.display;
15
+
16
+ return (
17
+ <div className={styles.header} id="admin-header">
18
+ <div className={styles.leftJustifiedItems}>
19
+ <ServiceQueuesPictogram />
20
+ <div className={styles.pageLabels}>
21
+ <p>{t('users', 'Users')}</p>
22
+ <p className={styles.pageName}>{title}</p>
23
+ </div>
24
+ </div>
25
+ <div className={styles.rightJustifiedItems}>
26
+ <div className={styles.userContainer}>
27
+ <p>{session?.user?.person?.display}</p>
28
+ <UserFollow size={16} className={styles.userIcon} />
29
+ </div>
30
+ <div className={styles.dateAndLocation}>
31
+ <Location size={16} />
32
+ <span className={styles.value}>{location}</span>
33
+ <span className={styles.middot}>&middot;</span>
34
+ <Calendar size={16} />
35
+ <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ );
40
+ };
41
+
42
+ export default Header;
@@ -0,0 +1,110 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import type { AttributeType } from '../../../../types';
3
+ import type { ConfigObject } from '../../../../config-schema';
4
+ import { extractProviderFormValues } from '../user-management.utils';
5
+
6
+ export interface AttributeTypeMapping {
7
+ licenseNumber: string;
8
+ licenseExpiry: string;
9
+ providerNationalId: string;
10
+ qualification: string;
11
+ licenseBody: string;
12
+ phoneNumber: string;
13
+ providerAddress: string;
14
+ passportNumber: string;
15
+ providerUniqueIdentifier: string;
16
+ }
17
+
18
+ export interface ProviderValues {
19
+ providerLicenseNumber: string;
20
+ licenseExpiryDate: Date | string;
21
+ qualification: string;
22
+ nationalId: string;
23
+ passportNumber: string;
24
+ registrationNumber: string;
25
+ phoneNumber: string;
26
+ email: string;
27
+ providerUniqueIdentifier: string;
28
+ }
29
+
30
+ interface UseProviderAttributeMappingParams {
31
+ provider: Array<{ attributes?: Array<{ attributeType?: { uuid: string }; value?: string | { name?: string } }> }>;
32
+ providerAttributeType: AttributeType[];
33
+ config: Pick<
34
+ ConfigObject,
35
+ | 'licenseNumberUuid'
36
+ | 'licenseExpiryDateUuid'
37
+ | 'providerNationalIdUuid'
38
+ | 'qualificationUuid'
39
+ | 'licenseBodyUuid'
40
+ | 'phoneNumberUuid'
41
+ | 'providerAddressUuid'
42
+ | 'passportNumberUuid'
43
+ | 'providerUniqueIdentifierAttributeTypeUuid'
44
+ >;
45
+ }
46
+
47
+ export function useProviderAttributeMapping({
48
+ provider,
49
+ providerAttributeType,
50
+ config,
51
+ }: UseProviderAttributeMappingParams) {
52
+ const attributeTypeMapping = useMemo<AttributeTypeMapping>(() => {
53
+ return {
54
+ licenseNumber: providerAttributeType.find((type) => type.uuid === config.licenseNumberUuid)?.uuid || '',
55
+ licenseExpiry: providerAttributeType.find((type) => type.uuid === config.licenseExpiryDateUuid)?.uuid || '',
56
+ providerNationalId: providerAttributeType.find((type) => type.uuid === config.providerNationalIdUuid)?.uuid || '',
57
+ qualification: providerAttributeType.find((type) => type.uuid === config.qualificationUuid)?.uuid || '',
58
+ licenseBody: providerAttributeType.find((type) => type.uuid === config.licenseBodyUuid)?.uuid || '',
59
+ phoneNumber: providerAttributeType.find((type) => type.uuid === config.phoneNumberUuid)?.uuid || '',
60
+ providerAddress: providerAttributeType.find((type) => type.uuid === config.providerAddressUuid)?.uuid || '',
61
+ passportNumber: providerAttributeType.find((type) => type.uuid === config.passportNumberUuid)?.uuid || '',
62
+ providerUniqueIdentifier:
63
+ providerAttributeType.find((type) => type.uuid === config.providerUniqueIdentifierAttributeTypeUuid)?.uuid ||
64
+ '',
65
+ };
66
+ }, [
67
+ providerAttributeType,
68
+ config.licenseNumberUuid,
69
+ config.licenseExpiryDateUuid,
70
+ config.providerNationalIdUuid,
71
+ config.qualificationUuid,
72
+ config.licenseBodyUuid,
73
+ config.phoneNumberUuid,
74
+ config.providerAddressUuid,
75
+ config.passportNumberUuid,
76
+ config.providerUniqueIdentifierAttributeTypeUuid,
77
+ ]);
78
+
79
+ const providerAttributes = useMemo(() => provider.flatMap((item) => item.attributes || []), [provider]);
80
+
81
+ const getProviderAttributeValue = useCallback(
82
+ (uuid: string, key: 'value' | 'display' = 'value') =>
83
+ providerAttributes.find((attr) => attr.attributeType?.uuid === uuid)?.[key],
84
+ [providerAttributes],
85
+ );
86
+
87
+ const providerValues = useMemo<ProviderValues>(() => {
88
+ const firstProvider = provider[0];
89
+ if (!firstProvider) {
90
+ return {
91
+ providerLicenseNumber: '',
92
+ licenseExpiryDate: '',
93
+ qualification: '',
94
+ nationalId: '',
95
+ passportNumber: '',
96
+ registrationNumber: '',
97
+ phoneNumber: '',
98
+ email: '',
99
+ providerUniqueIdentifier: '',
100
+ };
101
+ }
102
+ return extractProviderFormValues(firstProvider, attributeTypeMapping);
103
+ }, [provider, attributeTypeMapping]);
104
+
105
+ return {
106
+ attributeTypeMapping,
107
+ getProviderAttributeValue,
108
+ providerValues,
109
+ };
110
+ }
@@ -0,0 +1,119 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { ChevronLeft, ChevronRight } from '@carbon/react/icons';
3
+ import type { TFunction } from 'i18next';
4
+ import { SECTIONS } from '../../../../constants';
5
+ import { FORM_STEP_IDS, FORM_STEP_KEYS } from '../user-management.constants';
6
+
7
+ export interface FormStep {
8
+ id: string;
9
+ label: string;
10
+ }
11
+
12
+ interface UseUserFormStepsParams {
13
+ t: TFunction;
14
+ closeWorkspace: () => void;
15
+ isInitialValuesEmpty: boolean;
16
+ isStepValid: (stepIndex: number) => boolean;
17
+ }
18
+
19
+ export function useUserFormSteps({ t, closeWorkspace, isInitialValuesEmpty, isStepValid }: UseUserFormStepsParams) {
20
+ const [activeSection, setActiveSection] = useState(SECTIONS.DEMOGRAPHIC);
21
+ const [currentIndex, setCurrentIndex] = useState(0);
22
+ const [completedStepIndex, setCompletedStepIndex] = useState(-1);
23
+
24
+ const steps = useMemo<FormStep[]>(
25
+ () =>
26
+ FORM_STEP_IDS.map((id) => ({
27
+ id,
28
+ label: t(FORM_STEP_KEYS[id].key, FORM_STEP_KEYS[id].fallback),
29
+ })),
30
+ [t],
31
+ );
32
+
33
+ const toggleSection = useCallback((section: string) => {
34
+ setActiveSection((prev) => (prev !== section ? section : prev));
35
+ }, []);
36
+
37
+ const hasLoginInfo = activeSection === SECTIONS.LOGIN;
38
+ const hasRoles = activeSection === SECTIONS.ROLES;
39
+ const hasDemographicInfo = activeSection === SECTIONS.DEMOGRAPHIC;
40
+ const hasProviderAccount = activeSection === SECTIONS.PROVIDER;
41
+
42
+ const isSaveAndClose = !(hasDemographicInfo || hasLoginInfo || hasProviderAccount);
43
+
44
+ const getSubmitButtonText = () =>
45
+ t(isSaveAndClose ? 'saveAndClose' : 'next', isSaveAndClose ? 'Save & close' : 'Next');
46
+
47
+ const getSubmitButtonType = () => (isSaveAndClose ? 'submit' : 'button');
48
+
49
+ const getSubmitButtonIcon = () => (isSaveAndClose ? ChevronLeft : ChevronRight);
50
+
51
+ const handleBackClick = useCallback(() => {
52
+ if (hasDemographicInfo) {
53
+ closeWorkspace();
54
+ } else {
55
+ toggleSection(steps[currentIndex - 1].id);
56
+ setCurrentIndex((i) => i - 1);
57
+ }
58
+ }, [hasDemographicInfo, closeWorkspace, currentIndex, steps, toggleSection]);
59
+
60
+ const handleNextClick = useCallback(
61
+ (e: React.MouseEvent) => {
62
+ if (!isSaveAndClose) {
63
+ e.preventDefault();
64
+ toggleSection(steps[currentIndex + 1].id);
65
+ setCurrentIndex((i) => i + 1);
66
+ }
67
+ },
68
+ [isSaveAndClose, currentIndex, steps, toggleSection],
69
+ );
70
+
71
+ const markStepComplete = useCallback((index: number) => {
72
+ setCompletedStepIndex((prev) => Math.max(prev, index));
73
+ }, []);
74
+
75
+ const isStepEnabled = useCallback(
76
+ (index: number) => {
77
+ if (!isInitialValuesEmpty) {
78
+ return true;
79
+ }
80
+ if (index === 0) {
81
+ return true;
82
+ }
83
+ return completedStepIndex >= index - 1 && isStepValid(index - 1);
84
+ },
85
+ [isInitialValuesEmpty, completedStepIndex, isStepValid],
86
+ );
87
+
88
+ const handleStepChange = useCallback(
89
+ (newIndex: number) => {
90
+ if (!isStepEnabled(newIndex)) {
91
+ return;
92
+ }
93
+ toggleSection(steps[newIndex].id);
94
+ setCurrentIndex(newIndex);
95
+ },
96
+ [steps, toggleSection, isStepEnabled],
97
+ );
98
+
99
+ return {
100
+ steps,
101
+ currentIndex,
102
+ activeSection,
103
+ hasDemographicInfo,
104
+ hasLoginInfo,
105
+ hasProviderAccount,
106
+ hasRoles,
107
+ isSaveAndClose,
108
+ toggleSection,
109
+ setCurrentIndex,
110
+ markStepComplete,
111
+ isStepEnabled,
112
+ handleBackClick,
113
+ handleNextClick,
114
+ handleStepChange,
115
+ getSubmitButtonText,
116
+ getSubmitButtonType,
117
+ getSubmitButtonIcon,
118
+ };
119
+ }
@@ -0,0 +1,264 @@
1
+ import { restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
2
+ import { mutate } from 'swr';
3
+ import type { TFunction } from 'i18next';
4
+ import {
5
+ createUser,
6
+ handleMutation,
7
+ createProvider,
8
+ type ProviderWithAttributes,
9
+ } from '../../../../user-management.resources';
10
+ import { createProviderAttribute, updateProviderAttributes } from '../../../modal/hwr-sync.resource';
11
+ import { Provider, User } from '../../../../types';
12
+ import type { ProviderSearchResult } from '../provider-search.resource';
13
+ import type { AttributeTypeMapping, ProviderValues } from './useProviderAttributeMapping';
14
+ import type { FieldErrors } from 'react-hook-form';
15
+ import type { UserFormSchema } from './useUserManagementForm';
16
+ import { extractNameParts } from '../user-management.utils';
17
+ interface UseUserFormSubmissionParams {
18
+ attributeTypeMapping: AttributeTypeMapping;
19
+ selectedProvider: ProviderSearchResult | null;
20
+ initialUserValue: User;
21
+ provider: ProviderWithAttributes[];
22
+ providerValues: ProviderValues;
23
+ isProviderReadOnly: boolean;
24
+ isInitialValuesEmpty: boolean;
25
+ personPhonenumberAttributeUuid: string;
26
+ personEmailAttributeUuid: string;
27
+ licenseBodyUuid: string;
28
+ providerNationalIdUuid: string;
29
+ licenseNumberUuid: string;
30
+ passportNumberUuid: string;
31
+ providerUniqueIdentifierAttributeTypeUuid: string;
32
+ closeWorkspaceWithSavedChanges: () => void;
33
+ t: TFunction;
34
+ }
35
+
36
+ export function useUserFormSubmission({
37
+ attributeTypeMapping,
38
+ selectedProvider,
39
+ initialUserValue,
40
+ provider,
41
+ providerValues,
42
+ isProviderReadOnly,
43
+ isInitialValuesEmpty,
44
+ personPhonenumberAttributeUuid,
45
+ personEmailAttributeUuid,
46
+ licenseBodyUuid,
47
+ providerNationalIdUuid,
48
+ licenseNumberUuid,
49
+ passportNumberUuid,
50
+ providerUniqueIdentifierAttributeTypeUuid,
51
+ closeWorkspaceWithSavedChanges,
52
+ t,
53
+ }: UseUserFormSubmissionParams) {
54
+ const showSnackbarMessage = (title: string, subtitle: string, kind: 'success' | 'error') => {
55
+ showSnackbar({ title, subtitle, kind, isLowContrast: true });
56
+ };
57
+
58
+ const sanitizeFormData = (data: UserFormSchema): UserFormSchema => {
59
+ const sanitized = { ...data };
60
+
61
+ if (isProviderReadOnly) {
62
+ const display = selectedProvider?.person?.display ?? initialUserValue?.person?.display ?? '';
63
+ const { givenName, middleName, familyName } = extractNameParts(display);
64
+ const gender = (selectedProvider?.person?.gender ?? initialUserValue?.person?.gender) as 'M' | 'F' | undefined;
65
+
66
+ Object.assign(sanitized, {
67
+ givenName,
68
+ middleName,
69
+ familyName,
70
+ gender,
71
+ phoneNumber: providerValues.phoneNumber,
72
+ email: providerValues.email,
73
+ providerUniqueIdentifier: providerValues.providerUniqueIdentifier,
74
+ nationalId: providerValues.nationalId,
75
+ passportNumber: providerValues.passportNumber,
76
+ providerLicense: providerValues.providerLicenseNumber,
77
+ registrationNumber: providerValues.registrationNumber,
78
+ qualification: providerValues.qualification,
79
+ licenseExpiryDate: providerValues.licenseExpiryDate
80
+ ? providerValues.licenseExpiryDate instanceof Date
81
+ ? providerValues.licenseExpiryDate
82
+ : new Date(providerValues.licenseExpiryDate)
83
+ : undefined,
84
+ systemId: provider[0]?.identifier ?? '',
85
+ });
86
+ } else {
87
+ if (isInitialValuesEmpty && provider.length > 0) {
88
+ sanitized.phoneNumber = providerValues.phoneNumber;
89
+ sanitized.email = providerValues.email;
90
+ } else if (isInitialValuesEmpty && provider.length === 0) {
91
+ sanitized.phoneNumber = '';
92
+ sanitized.email = '';
93
+ } else if (!isInitialValuesEmpty && !data.isEditProvider && provider.length > 0) {
94
+ sanitized.nationalId = providerValues.nationalId;
95
+ sanitized.passportNumber = providerValues.passportNumber;
96
+ sanitized.providerLicense = providerValues.providerLicenseNumber;
97
+ sanitized.registrationNumber = providerValues.registrationNumber;
98
+ sanitized.providerUniqueIdentifier = providerValues.providerUniqueIdentifier;
99
+ }
100
+ }
101
+
102
+ return sanitized;
103
+ };
104
+
105
+ const onSubmit = async (data: UserFormSchema) => {
106
+ const sanitizedData = sanitizeFormData(data);
107
+ const setProvider = sanitizedData.providerIdentifiers;
108
+ const editProvider = sanitizedData.isEditProvider;
109
+ const providerUUID = provider[0]?.uuid || '';
110
+
111
+ const providerPayload: Partial<Provider> = {
112
+ attributes: [
113
+ { attributeType: attributeTypeMapping.licenseNumber, value: sanitizedData.providerLicense },
114
+ {
115
+ attributeType: attributeTypeMapping.licenseExpiry,
116
+ value: sanitizedData.licenseExpiryDate ? sanitizedData.licenseExpiryDate.toISOString() : '',
117
+ },
118
+ { attributeType: attributeTypeMapping.licenseBody, value: sanitizedData.registrationNumber },
119
+ {
120
+ attributeType: attributeTypeMapping.providerUniqueIdentifier,
121
+ value: sanitizedData.providerUniqueIdentifier,
122
+ },
123
+ {
124
+ attributeType: attributeTypeMapping.providerNationalId,
125
+ value: sanitizedData.nationalId,
126
+ },
127
+ {
128
+ attributeType: attributeTypeMapping.qualification,
129
+ value: sanitizedData.qualification,
130
+ },
131
+ {
132
+ attributeType: attributeTypeMapping.passportNumber,
133
+ value: sanitizedData.passportNumber,
134
+ },
135
+ {
136
+ attributeType: attributeTypeMapping.phoneNumber,
137
+ value: sanitizedData.phoneNumber,
138
+ },
139
+ {
140
+ attributeType: attributeTypeMapping.providerAddress,
141
+ value: sanitizedData.email,
142
+ },
143
+ ].filter((attr) => attr.value),
144
+ };
145
+
146
+ const includePassword =
147
+ isInitialValuesEmpty || (sanitizedData.password && sanitizedData.password.trim().length > 0);
148
+
149
+ const payload: Partial<User> = {
150
+ username: sanitizedData.username,
151
+ ...(includePassword ? { password: sanitizedData.password } : {}),
152
+ person: selectedProvider?.person?.uuid
153
+ ? { uuid: selectedProvider.person.uuid, gender: selectedProvider.person.gender ?? '' }
154
+ : {
155
+ uuid: initialUserValue?.person?.uuid,
156
+ names: [
157
+ {
158
+ givenName: sanitizedData.givenName,
159
+ familyName: sanitizedData.familyName,
160
+ middleName: sanitizedData.middleName,
161
+ },
162
+ ],
163
+ gender: sanitizedData.gender,
164
+ attributes: [
165
+ { attributeType: personPhonenumberAttributeUuid, value: sanitizedData.phoneNumber },
166
+ { attributeType: personEmailAttributeUuid, value: sanitizedData.email },
167
+ ],
168
+ },
169
+ roles: sanitizedData.roles?.map((role) => ({
170
+ uuid: role.uuid,
171
+ name: role.display,
172
+ description: role.description ?? '',
173
+ })),
174
+ };
175
+
176
+ try {
177
+ const response = await createUser(payload, initialUserValue?.uuid || '');
178
+ if (response.uuid) {
179
+ showSnackbarMessage(t('userSaved', 'User saved successfully'), '', 'success');
180
+
181
+ handleMutation(
182
+ `${restBaseUrl}/user?v=custom:(uuid,username,display,systemId,retired,person:(uuid,display,gender,names:(givenName,familyName,middleName),attributes:(uuid,display)),roles:(uuid,description,display,name))`,
183
+ );
184
+
185
+ if (setProvider) {
186
+ try {
187
+ const providerUrl = providerUUID ? `${restBaseUrl}/provider/${providerUUID}` : `${restBaseUrl}/provider`;
188
+ const personUUID = response.person.uuid;
189
+ const identifier = response.systemId;
190
+ const providerResponse = await createProvider(personUUID, identifier, providerPayload, providerUrl);
191
+ if (providerResponse.ok) {
192
+ showSnackbarMessage(t('providerSaved', 'Provider saved successfully'), '', 'success');
193
+ }
194
+ } catch (error) {
195
+ showSnackbarMessage(
196
+ t('providerFail', 'Failed to save provider'),
197
+ t('providerFailedSubtitle', 'An error occurred while creating provider'),
198
+ 'error',
199
+ );
200
+ }
201
+ }
202
+ if (editProvider) {
203
+ const updatableAttributes = [
204
+ { attributeType: licenseBodyUuid, value: sanitizedData?.registrationNumber },
205
+ { attributeType: providerNationalIdUuid, value: sanitizedData?.nationalId },
206
+ { attributeType: licenseNumberUuid, value: sanitizedData?.providerLicense },
207
+ { attributeType: passportNumberUuid, value: sanitizedData?.passportNumber },
208
+ {
209
+ attributeType: providerUniqueIdentifierAttributeTypeUuid,
210
+ value: sanitizedData?.providerUniqueIdentifier,
211
+ },
212
+ ].filter((attr) => attr?.value !== undefined && attr?.value !== null && attr?.value !== '');
213
+
214
+ await Promise.all(
215
+ updatableAttributes?.map((attr) => {
216
+ const existingAttributes = provider[0]?.attributes?.find(
217
+ (at) => at?.attributeType?.uuid === attr?.attributeType,
218
+ )?.uuid;
219
+
220
+ const attrPayload = {
221
+ attributeType: attr?.attributeType,
222
+ value: attr?.value,
223
+ };
224
+
225
+ if (!existingAttributes) {
226
+ return createProviderAttribute(attrPayload, providerUUID);
227
+ }
228
+ return updateProviderAttributes(attrPayload, providerUUID, existingAttributes);
229
+ }),
230
+ );
231
+ showSnackbar({
232
+ title: 'Success',
233
+ kind: 'success',
234
+ subtitle: t('updateMessage', 'Provider updated successfully'),
235
+ });
236
+ }
237
+ } else {
238
+ throw new Error('User creation failed');
239
+ }
240
+ handleMutation(`${restBaseUrl}/user`);
241
+ mutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/provider`));
242
+ closeWorkspaceWithSavedChanges();
243
+ } catch (error) {
244
+ showSnackbarMessage(
245
+ t('userSaveFailed', 'Failed to save user'),
246
+ t('userCreationFailedSubtitle', 'An error occurred while saving user form '),
247
+ 'error',
248
+ );
249
+ }
250
+ };
251
+
252
+ const handleError = (errors: FieldErrors<UserFormSchema>) => {
253
+ showSnackbar({
254
+ title: t('userSaveFailed', 'Fail to save'),
255
+ subtitle: t('userCreationFailedSubtitle', 'An error occurred while saving user form', {
256
+ errorMessage: JSON.stringify(errors, null, 2),
257
+ }),
258
+ kind: 'error',
259
+ isLowContrast: true,
260
+ });
261
+ };
262
+
263
+ return { onSubmit, handleError };
264
+ }
@@ -0,0 +1,122 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import UserManagementFormSchema from '../../userManagementFormSchema';
5
+ import { extractNameParts } from '../user-management.utils';
6
+ import type { User } from '../../../../types';
7
+ import type { ProviderValues } from './useProviderAttributeMapping';
8
+
9
+ export type UserFormSchema = {
10
+ givenName: string;
11
+ middleName?: string;
12
+ familyName: string;
13
+ gender?: 'M' | 'F';
14
+ phoneNumber?: string;
15
+ email?: string;
16
+ providerIdentifiers?: boolean;
17
+ username: string;
18
+ password?: string;
19
+ confirmPassword?: string;
20
+ roles?: Array<{ uuid: string; display: string; description?: string | null }>;
21
+ primaryRole?: string;
22
+ systemId?: string;
23
+ providerLicense?: string;
24
+ licenseExpiryDate?: Date;
25
+ registrationNumber?: string;
26
+ qualification?: string;
27
+ nationalId?: string;
28
+ passportNumber?: string;
29
+ isEditProvider?: boolean;
30
+ providerUniqueIdentifier?: string;
31
+ };
32
+
33
+ interface UseUserManagementFormParams {
34
+ initialUserValue: User;
35
+ usernames: string[];
36
+ providerValues: ProviderValues;
37
+ loadingProvider: boolean;
38
+ isInitialValuesEmpty: boolean;
39
+ isProviderReadOnly?: boolean;
40
+ }
41
+
42
+ export function useUserManagementForm({
43
+ initialUserValue,
44
+ usernames,
45
+ providerValues,
46
+ loadingProvider,
47
+ isInitialValuesEmpty,
48
+ isProviderReadOnly = false,
49
+ }: UseUserManagementFormParams) {
50
+ const { userManagementFormSchema } = UserManagementFormSchema(
51
+ usernames,
52
+ undefined,
53
+ !isProviderReadOnly,
54
+ isInitialValuesEmpty,
55
+ );
56
+
57
+ const formDefaultValues = useMemo(() => {
58
+ if (isInitialValuesEmpty) {
59
+ return {};
60
+ }
61
+ const { givenName, middleName, familyName } = extractNameParts(initialUserValue.person?.display || '');
62
+ return {
63
+ ...initialUserValue,
64
+ givenName,
65
+ middleName,
66
+ familyName,
67
+ phoneNumber: providerValues.phoneNumber,
68
+ email: providerValues.email,
69
+ roles:
70
+ initialUserValue.roles?.map((role) => ({
71
+ uuid: role.uuid,
72
+ display: role.display,
73
+ description: role.description,
74
+ })) || [],
75
+ gender: initialUserValue.person?.gender,
76
+ providerLicense: providerValues.providerLicenseNumber,
77
+ licenseExpiryDate: providerValues.licenseExpiryDate
78
+ ? new Date(providerValues.licenseExpiryDate as Date)
79
+ : undefined,
80
+ qualification: providerValues.qualification,
81
+ nationalId: providerValues.nationalId,
82
+ passportNumber: providerValues.passportNumber,
83
+ registrationNumber: providerValues.registrationNumber,
84
+ providerUniqueIdentifier: providerValues.providerUniqueIdentifier,
85
+ };
86
+ }, [
87
+ isInitialValuesEmpty,
88
+ initialUserValue,
89
+ providerValues.phoneNumber,
90
+ providerValues.email,
91
+ providerValues.providerLicenseNumber,
92
+ providerValues.licenseExpiryDate,
93
+ providerValues.qualification,
94
+ providerValues.nationalId,
95
+ providerValues.passportNumber,
96
+ providerValues.registrationNumber,
97
+ providerValues.providerUniqueIdentifier,
98
+ ]);
99
+
100
+ const userFormMethods = useForm<UserFormSchema>({
101
+ resolver: zodResolver(userManagementFormSchema),
102
+ mode: 'all',
103
+ defaultValues: formDefaultValues,
104
+ });
105
+
106
+ const { reset } = userFormMethods;
107
+ const { errors, isSubmitting, isDirty } = userFormMethods.formState;
108
+
109
+ useEffect(() => {
110
+ if (!loadingProvider && !isInitialValuesEmpty) {
111
+ reset(formDefaultValues);
112
+ }
113
+ }, [loadingProvider, formDefaultValues, isInitialValuesEmpty, reset]);
114
+
115
+ return {
116
+ userFormMethods,
117
+ formDefaultValues,
118
+ errors,
119
+ isSubmitting,
120
+ isDirty,
121
+ };
122
+ }