@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,316 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ DefaultWorkspaceProps,
5
+ ResponsiveWrapper,
6
+ restBaseUrl,
7
+ showSnackbar,
8
+ useLayoutType,
9
+ } from '@openmrs/esm-framework';
10
+ import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
11
+ import styles from '../../../manage-users/user-management.workspace.scss';
12
+ import { TextInput, ButtonSet, Button, InlineLoading, Stack } from '@carbon/react';
13
+ import { z } from 'zod';
14
+ import { zodResolver } from '@hookform/resolvers/zod';
15
+ import classNames from 'classnames';
16
+ import { User, UserRoleScope } from '../../../../../types';
17
+ import UserRoleScopeFormSchema from './userRoleScopeFormSchema';
18
+ import {
19
+ createOrUpdateUserRoleScope,
20
+ handleMutation,
21
+ useStockOperationTypes,
22
+ useStockTagLocations,
23
+ useUserRoleScopes,
24
+ } from '../../../../../user-management.resources';
25
+ import { formatNewDate, ROLE_CATEGORIES, today } from '../../../../../constants';
26
+ import { CardHeader, EmptyState } from '@openmrs/esm-patient-common-lib/src';
27
+ import { Add, ChevronSortUp } from '@carbon/react/icons';
28
+ import { useSystemUserRoleConfigSetting } from '../../../../hook/useSystemRoleSetting';
29
+ import UserRoleScopeFormFields from './user-role-scope-fields.component';
30
+ import StockUserRoleScopesList from '../user-role-scope-list/user-role-scope-list.component';
31
+
32
+ type UserRoleScopeWorkspaceProps = DefaultWorkspaceProps & {
33
+ user?: User;
34
+ };
35
+
36
+ const UserRoleScopeWorkspace: React.FC<UserRoleScopeWorkspaceProps> = ({
37
+ closeWorkspace,
38
+ promptBeforeClosing,
39
+ closeWorkspaceWithSavedChanges,
40
+ user = {} as User,
41
+ }) => {
42
+ const { t } = useTranslation();
43
+ const isTablet = useLayoutType() === 'tablet';
44
+ const { userRoleScopeFormSchema } = UserRoleScopeFormSchema();
45
+ const { stockOperations, loadingStock } = useStockOperationTypes();
46
+ const { stockLocations } = useStockTagLocations();
47
+ const { rolesConfig, error } = useSystemUserRoleConfigSetting();
48
+ const { items, loadingRoleScope } = useUserRoleScopes();
49
+
50
+ const [userRoleScopeInitialValues, setUserRoleScopeInitialValues] = useState<UserRoleScope | null>(null);
51
+ const handleEditUserRoleScope = useCallback((userRoleScope: UserRoleScope) => {
52
+ setUserRoleScopeInitialValues(userRoleScope);
53
+ }, []);
54
+
55
+ type UserRoleScopeFormSchema = z.infer<typeof userRoleScopeFormSchema>;
56
+
57
+ const userRoleScopedefaultValues = useMemo(
58
+ () => ({
59
+ forms: userRoleScopeInitialValues
60
+ ? [
61
+ {
62
+ ...userRoleScopeInitialValues,
63
+ userName: user?.username || userRoleScopeInitialValues?.userName,
64
+ dateRange: {
65
+ activeTo: formatNewDate(userRoleScopeInitialValues?.activeTo),
66
+ activeFrom: formatNewDate(userRoleScopeInitialValues?.activeFrom),
67
+ },
68
+ locations:
69
+ userRoleScopeInitialValues?.locations?.map(({ locationName, locationUuid }) => ({
70
+ locationName,
71
+ locationUuid,
72
+ })) || [],
73
+ operationTypes:
74
+ userRoleScopeInitialValues?.operationTypes?.map(({ operationTypeName, operationTypeUuid }) => ({
75
+ operationTypeName,
76
+ operationTypeUuid,
77
+ })) || [],
78
+ },
79
+ ]
80
+ : [],
81
+ }),
82
+ [userRoleScopeInitialValues, user],
83
+ );
84
+
85
+ const roleScopeformMethods = useForm<UserRoleScopeFormSchema>({
86
+ resolver: zodResolver(userRoleScopeFormSchema),
87
+ mode: 'all',
88
+ defaultValues: userRoleScopedefaultValues as UserRoleScopeFormSchema,
89
+ });
90
+
91
+ const { reset } = roleScopeformMethods;
92
+
93
+ const { errors, isSubmitting, isDirty } = roleScopeformMethods.formState;
94
+ useEffect(() => {
95
+ if (isDirty) {
96
+ promptBeforeClosing(() => isDirty);
97
+ }
98
+ }, [isDirty, promptBeforeClosing]);
99
+
100
+ useEffect(() => {
101
+ if (userRoleScopeInitialValues && !loadingStock) {
102
+ reset(userRoleScopedefaultValues as UserRoleScopeFormSchema);
103
+ }
104
+ }, [userRoleScopedefaultValues, loadingStock, userRoleScopeInitialValues, user, reset]);
105
+ const {
106
+ fields: forms,
107
+ append: appendForm,
108
+ remove: removeForm,
109
+ } = useFieldArray({
110
+ control: roleScopeformMethods.control,
111
+ name: 'forms',
112
+ });
113
+
114
+ const mappedRoleScopeForms = (form) => ({
115
+ uuid: userRoleScopeInitialValues?.uuid,
116
+ userUuid: userRoleScopeInitialValues?.userUuid || user?.uuid,
117
+ userName: userRoleScopeInitialValues?.userName,
118
+ userGivenName: userRoleScopeInitialValues?.userGivenName,
119
+ userFamilyName: userRoleScopeInitialValues?.userFamilyName,
120
+ permanent: form?.permanent,
121
+ enabled: form?.enabled,
122
+ operationTypes: form?.operationTypes?.map(({ operationTypeUuid, operationTypeName }) => ({
123
+ operationTypeUuid,
124
+ operationTypeName,
125
+ })),
126
+ locations: form?.locations?.map(({ locationUuid, locationName }) => ({
127
+ locationUuid,
128
+ locationName,
129
+ enableDescendants: false,
130
+ })),
131
+ role: form?.role,
132
+ activeFrom: form?.dateRange?.activeFrom || null,
133
+ activeTo: form?.dateRange?.activeTo || null,
134
+ });
135
+
136
+ const showNotification = (titleKey, subtitleKey, kind, params = {}) => {
137
+ showSnackbar({
138
+ title: t(titleKey),
139
+ subtitle: t(subtitleKey, params),
140
+ kind,
141
+ isLowContrast: true,
142
+ });
143
+ };
144
+
145
+ const onSubmit = async (data: UserRoleScopeFormSchema) => {
146
+ try {
147
+ const userRoleScopeUrl = userRoleScopeInitialValues?.uuid
148
+ ? `${restBaseUrl}/stockmanagement/userrolescope/${userRoleScopeInitialValues.uuid}`
149
+ : `${restBaseUrl}/stockmanagement/userrolescope`;
150
+
151
+ await Promise.all(
152
+ data.forms.map(async (form) => {
153
+ const roleScope = mappedRoleScopeForms(form);
154
+ const response = await createOrUpdateUserRoleScope(userRoleScopeUrl, roleScope, user?.uuid ?? '');
155
+ if (response.ok) {
156
+ showNotification('userRoleScopeSaved', 'User role scope saved successfully', 'success');
157
+ closeWorkspaceWithSavedChanges();
158
+ }
159
+ }),
160
+ );
161
+
162
+ handleMutation(`${restBaseUrl}/stockmanagement/userrolescope`);
163
+ } catch (error) {
164
+ const errorMessage =
165
+ error?.responseBody?.error?.message ?? 'An error occurred while creating the User role scope';
166
+ showNotification('userRoleScopeCreationFailed', 'User role scope failed to save successfully', 'error', {
167
+ errorMessage,
168
+ });
169
+ }
170
+ };
171
+
172
+ const handleError = (error) => {
173
+ console.error('Error:', error);
174
+ showSnackbar({
175
+ title: t('userRoleScopeCreationFailed', 'User Role Scope creation failed'),
176
+ subtitle: t(
177
+ 'userRoleScopeCreationFailedSubtitle',
178
+ 'An error occurred while creating the user role scope mode. Please try again.',
179
+ ),
180
+ kind: 'error',
181
+ isLowContrast: true,
182
+ });
183
+ };
184
+
185
+ useEffect(() => {
186
+ if (isDirty) {
187
+ promptBeforeClosing(() => isDirty);
188
+ }
189
+ }, [isDirty, promptBeforeClosing]);
190
+
191
+ function extractInventoryRoleNames(rolesConfig) {
192
+ return rolesConfig.find((category) => category.category === ROLE_CATEGORIES.CORE_INVENTORY)?.roles || [];
193
+ }
194
+
195
+ const inventoryRoleNames = useMemo(() => extractInventoryRoleNames(rolesConfig || []), [rolesConfig]);
196
+
197
+ const inventoryRoles = useMemo(() => {
198
+ if (!user?.roles || inventoryRoleNames.length === 0) {
199
+ return [];
200
+ }
201
+ return user.roles.filter((role) => inventoryRoleNames.includes(role.display));
202
+ }, [user?.roles, inventoryRoleNames]);
203
+
204
+ const scopeRoles = useMemo(
205
+ () =>
206
+ items?.results?.reduce((acc, role) => {
207
+ if (role.userUuid === user.uuid) {
208
+ acc.push(role.role);
209
+ }
210
+ return acc;
211
+ }, []) || [],
212
+ [items, user.uuid],
213
+ );
214
+
215
+ const filteredInventoryRoles = useMemo(() => {
216
+ if (!user?.roles) {
217
+ return [];
218
+ }
219
+ return user.uuid ? inventoryRoles.filter((role) => !scopeRoles.includes(role.display)) : inventoryRoles;
220
+ }, [user?.roles, user.uuid, inventoryRoles, scopeRoles]);
221
+
222
+ const hasInventoryRole = useMemo(
223
+ () => filteredInventoryRoles.length > 0 && user.roles.some((role) => filteredInventoryRoles.includes(role)),
224
+ [user.roles, filteredInventoryRoles],
225
+ );
226
+
227
+ return (
228
+ <>
229
+ <div>
230
+ <StockUserRoleScopesList onEditUserRoleScope={handleEditUserRoleScope} user={user} />
231
+ </div>
232
+ {userRoleScopedefaultValues && (
233
+ <FormProvider {...roleScopeformMethods}>
234
+ <form onSubmit={roleScopeformMethods.handleSubmit(onSubmit, handleError)} className={styles.form}>
235
+ <div className={styles.formContainer}>
236
+ <Stack className={styles.formStackControl} gap={7}>
237
+ <ResponsiveWrapper>
238
+ <CardHeader
239
+ title={
240
+ userRoleScopeInitialValues
241
+ ? t('editRoleScope', 'Edit User Role Scope')
242
+ : t('addRoleScope', 'Add a new user role scope')
243
+ }>
244
+ <ChevronSortUp />
245
+ </CardHeader>
246
+ </ResponsiveWrapper>
247
+ <div className={styles.roleStockFields}>
248
+ <ResponsiveWrapper>
249
+ <TextInput
250
+ readOnly={true}
251
+ id="userName"
252
+ labelText={t('username', 'Username')}
253
+ value={user?.username || userRoleScopeInitialValues?.userName}
254
+ />
255
+ </ResponsiveWrapper>
256
+ </div>
257
+
258
+ {forms.map((field, index) => (
259
+ <UserRoleScopeFormFields
260
+ key={field.id}
261
+ field={field}
262
+ index={index}
263
+ control={roleScopeformMethods.control}
264
+ removeForm={removeForm}
265
+ filteredInventoryRoles={filteredInventoryRoles}
266
+ hasInventoryRole={hasInventoryRole}
267
+ stockOperations={stockOperations}
268
+ stockLocations={stockLocations}
269
+ loadingStock={loadingStock}
270
+ roleScopeformMethods={roleScopeformMethods}
271
+ userRoleScopeInitialValues={userRoleScopeInitialValues}
272
+ />
273
+ ))}
274
+ {!userRoleScopeInitialValues && (
275
+ <div>
276
+ <div className={styles.roleStockFields}>
277
+ {!hasInventoryRole && <EmptyState displayText={t('noUserRole', 'No user role')} headerTitle="" />}
278
+ <Button
279
+ size="sm"
280
+ kind="tertiary"
281
+ renderIcon={Add}
282
+ onClick={() => appendForm({})}
283
+ disabled={!hasInventoryRole}>
284
+ {t('addAnotherRoleScope', 'Add user role scope')}
285
+ </Button>
286
+ </div>
287
+ </div>
288
+ )}
289
+ </Stack>
290
+ </div>
291
+ <ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
292
+ <Button style={{ maxWidth: '50%' }} kind="secondary" onClick={() => closeWorkspace()}>
293
+ {t('cancel', 'Cancel')}
294
+ </Button>
295
+ <Button
296
+ disabled={isSubmitting || Object.keys(errors).length > 0}
297
+ style={{ maxWidth: '50%' }}
298
+ kind="primary"
299
+ type="submit">
300
+ {isSubmitting ? (
301
+ <span style={{ display: 'flex', justifyItems: 'center' }}>
302
+ {t('submitting', 'Submitting...')} <InlineLoading status="active" iconDescription="Loading" />
303
+ </span>
304
+ ) : (
305
+ t('saveAndClose', 'Save & close')
306
+ )}
307
+ </Button>
308
+ </ButtonSet>
309
+ </form>
310
+ </FormProvider>
311
+ )}
312
+ </>
313
+ );
314
+ };
315
+
316
+ export default UserRoleScopeWorkspace;
@@ -0,0 +1,43 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { z } from 'zod';
3
+
4
+ const UserRoleScopeFormSchema = () => {
5
+ const { t } = useTranslation();
6
+
7
+ const userRoleScopeFormSchema = z.object({
8
+ forms: z.array(
9
+ z.object({
10
+ userName: z.string().optional(),
11
+ permanent: z.boolean().optional(),
12
+ enabled: z.boolean().optional(),
13
+ operationTypes: z
14
+ .array(
15
+ z.object({
16
+ operationTypeName: z.string().optional(),
17
+ operationTypeUuid: z.string().optional(),
18
+ }),
19
+ )
20
+ .optional(),
21
+ locations: z
22
+ .array(
23
+ z.object({
24
+ locationName: z.string().optional(),
25
+ locationUuid: z.string().optional(),
26
+ }),
27
+ )
28
+ .optional(),
29
+ dateRange: z
30
+ .object({
31
+ activeTo: z.date().optional(),
32
+ activeFrom: z.date().optional(),
33
+ })
34
+ .optional(),
35
+ role: z.string().optional(),
36
+ }),
37
+ ),
38
+ });
39
+
40
+ return { userRoleScopeFormSchema };
41
+ };
42
+
43
+ export default UserRoleScopeFormSchema;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import styles from './manage-user.scss';
3
+ import UserList from './user-list/user-list.component';
4
+ import { useTranslation } from 'react-i18next';
5
+ import Header from '../header/user-management-header.component';
6
+
7
+ const UserManagentLandingPage: React.FC = () => {
8
+ const { t } = useTranslation();
9
+ return (
10
+ <section className={styles.section}>
11
+ <div className={`omrs-main-content`}>
12
+ <Header title={t('userManagement', 'User Management')} />
13
+ </div>
14
+ <UserList />
15
+ </section>
16
+ );
17
+ };
18
+
19
+ export default UserManagentLandingPage;
@@ -0,0 +1,31 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ height: calc(100vh - 3rem);
7
+ }
8
+
9
+ .servicesTableContainer {
10
+ margin: layout.$spacing-05;
11
+ }
12
+
13
+ .illo {
14
+ margin-top: layout.$spacing-05;
15
+ }
16
+
17
+ .content {
18
+ @include type.type-style('heading-compact-01');
19
+ color: colors.$gray-70;
20
+ margin-top: layout.$spacing-05;
21
+ }
22
+
23
+ .tile {
24
+ border: 1px solid colors.$gray-20;
25
+ padding: layout.$spacing-05 0;
26
+ text-align: center;
27
+ }
28
+
29
+ .section {
30
+ border-right: 1px solid colors.$gray-20;
31
+ }
@@ -0,0 +1,117 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Autosuggest } from '../../locations/auto-suggest/autosuggest.component';
4
+ import { fetchProvidersByName, type ProviderSearchResult } from './provider-search.resource';
5
+
6
+ const SEARCH_DEBOUNCE_MS = 300;
7
+
8
+ interface ProviderAutosuggestProps {
9
+ onProviderSelected: (providerData: ProviderSearchResult) => void;
10
+ labelText?: string;
11
+ placeholder?: string;
12
+ invalid?: boolean;
13
+ invalidText?: string;
14
+ }
15
+
16
+ export const ProviderAutosuggest: React.FC<ProviderAutosuggestProps> = ({
17
+ onProviderSelected,
18
+ labelText,
19
+ placeholder,
20
+ invalid = false,
21
+ invalidText,
22
+ }) => {
23
+ const [searchResults, setSearchResults] = useState<ProviderSearchResult[]>([]);
24
+ const [hasSelectedProvider, setHasSelectedProvider] = useState(false);
25
+ const { t } = useTranslation();
26
+
27
+ const getDisplayValue = useCallback((item: ProviderSearchResult) => {
28
+ return item.person?.display ?? item.identifier ?? '';
29
+ }, []);
30
+
31
+ const getFieldValue = useCallback((item: ProviderSearchResult) => {
32
+ return item.uuid ?? '';
33
+ }, []);
34
+
35
+ const handleSuggestionSelected = useCallback(
36
+ (field: string, value: string) => {
37
+ if (value) {
38
+ const selected = searchResults.find((item) => item.uuid === value);
39
+ if (selected) {
40
+ setHasSelectedProvider(true);
41
+ onProviderSelected(selected);
42
+ }
43
+ } else {
44
+ setHasSelectedProvider(false);
45
+ }
46
+ },
47
+ [onProviderSelected, searchResults],
48
+ );
49
+
50
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
51
+ const previousResolveRef = useRef<((results: ProviderSearchResult[]) => void) | null>(null);
52
+
53
+ const handleSearchResults = useCallback((query: string) => {
54
+ return new Promise<ProviderSearchResult[]>((resolve, reject) => {
55
+ if (timeoutRef.current) {
56
+ clearTimeout(timeoutRef.current);
57
+ previousResolveRef.current?.([]);
58
+ }
59
+ previousResolveRef.current = resolve;
60
+ timeoutRef.current = setTimeout(async () => {
61
+ try {
62
+ const abortController = new AbortController();
63
+ const results = await fetchProvidersByName(query, abortController);
64
+ setSearchResults(results);
65
+ previousResolveRef.current?.(results);
66
+ } catch (err) {
67
+ reject(err);
68
+ } finally {
69
+ timeoutRef.current = null;
70
+ previousResolveRef.current = null;
71
+ }
72
+ }, SEARCH_DEBOUNCE_MS);
73
+ });
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ return () => {
78
+ if (timeoutRef.current) {
79
+ clearTimeout(timeoutRef.current);
80
+ }
81
+ };
82
+ }, []);
83
+
84
+ const renderSuggestionItem = useCallback((item: ProviderSearchResult) => {
85
+ return <div>{item.person?.display ?? item.identifier ?? ''}</div>;
86
+ }, []);
87
+
88
+ const renderEmptyState = useCallback(
89
+ (value: string) => {
90
+ if (!value || hasSelectedProvider) {
91
+ return null;
92
+ }
93
+ return (
94
+ <div style={{ padding: '1rem' }}>
95
+ <p>{t('providerNotFound', 'No matching provider found')}</p>
96
+ </div>
97
+ );
98
+ },
99
+ [t, hasSelectedProvider],
100
+ );
101
+
102
+ return (
103
+ <Autosuggest
104
+ id="provider-autosuggest"
105
+ labelText={labelText ?? t('searchForProvider', 'Search for existing provider')}
106
+ placeholder={placeholder ?? t('searchProviderPlaceholder', 'Search by provider name')}
107
+ getDisplayValue={getDisplayValue}
108
+ getFieldValue={getFieldValue}
109
+ getSearchResults={handleSearchResults}
110
+ onSuggestionSelected={handleSuggestionSelected}
111
+ renderSuggestionItem={renderSuggestionItem}
112
+ renderEmptyState={renderEmptyState}
113
+ invalid={invalid}
114
+ invalidText={invalidText}
115
+ />
116
+ );
117
+ };
@@ -0,0 +1,34 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ const providerCustomRep =
4
+ 'custom:(uuid,identifier,retired,person:(uuid,display,gender),attributes:(uuid,display,value,attributeType:(uuid,name)))';
5
+
6
+ export interface ProviderSearchResult {
7
+ uuid: string;
8
+ identifier?: string;
9
+ retired?: boolean;
10
+ person?: {
11
+ uuid?: string;
12
+ display?: string;
13
+ gender?: string;
14
+ };
15
+ attributes?: Array<{
16
+ uuid?: string;
17
+ display?: string;
18
+ value?: string | { name?: string };
19
+ attributeType?: { uuid: string; name?: string };
20
+ }>;
21
+ }
22
+
23
+ export async function fetchProvidersByName(
24
+ query: string,
25
+ abortController: AbortController,
26
+ ): Promise<ProviderSearchResult[]> {
27
+ const response = await openmrsFetch<{ results: ProviderSearchResult[] }>(
28
+ `${restBaseUrl}/provider?q=${encodeURIComponent(query)}&includeAll=false&v=${providerCustomRep}`,
29
+ {
30
+ signal: abortController.signal,
31
+ },
32
+ );
33
+ return response?.data?.results ?? [];
34
+ }