@digihmis/esm-administration-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +4 -0
  2. package/dist/101.js +1 -0
  3. package/dist/101.js.map +1 -0
  4. package/dist/117.js +1 -0
  5. package/dist/117.js.map +1 -0
  6. package/dist/132.js +1 -0
  7. package/dist/132.js.map +1 -0
  8. package/dist/15.js +1 -0
  9. package/dist/15.js.map +1 -0
  10. package/dist/152.js +1 -0
  11. package/dist/152.js.map +1 -0
  12. package/dist/153.js +1 -0
  13. package/dist/153.js.map +1 -0
  14. package/dist/190.js +1 -0
  15. package/dist/190.js.map +1 -0
  16. package/dist/209.js +1 -0
  17. package/dist/209.js.map +1 -0
  18. package/dist/266.js +1 -0
  19. package/dist/266.js.map +1 -0
  20. package/dist/317.js +1 -0
  21. package/dist/317.js.map +1 -0
  22. package/dist/349.js +1 -0
  23. package/dist/349.js.map +1 -0
  24. package/dist/371.js +1 -0
  25. package/dist/371.js.map +1 -0
  26. package/dist/378.js +1 -0
  27. package/dist/378.js.map +1 -0
  28. package/dist/394.js +1 -0
  29. package/dist/394.js.map +1 -0
  30. package/dist/442.js +1 -0
  31. package/dist/442.js.map +1 -0
  32. package/dist/45.js +1 -0
  33. package/dist/45.js.map +1 -0
  34. package/dist/454.js +1 -0
  35. package/dist/454.js.map +1 -0
  36. package/dist/466.js +1 -0
  37. package/dist/466.js.map +1 -0
  38. package/dist/482.js +1 -0
  39. package/dist/482.js.map +1 -0
  40. package/dist/501.js +1 -0
  41. package/dist/501.js.map +1 -0
  42. package/dist/508.js +1 -0
  43. package/dist/508.js.map +1 -0
  44. package/dist/578.js +1 -0
  45. package/dist/578.js.map +1 -0
  46. package/dist/598.js +1 -0
  47. package/dist/598.js.map +1 -0
  48. package/dist/606.js +1 -0
  49. package/dist/606.js.map +1 -0
  50. package/dist/61.js +1 -0
  51. package/dist/61.js.map +1 -0
  52. package/dist/640.js +1 -0
  53. package/dist/640.js.map +1 -0
  54. package/dist/685.js +1 -0
  55. package/dist/685.js.map +1 -0
  56. package/dist/689.js +1 -0
  57. package/dist/689.js.map +1 -0
  58. package/dist/709.js +1 -0
  59. package/dist/709.js.map +1 -0
  60. package/dist/712.js +1 -0
  61. package/dist/712.js.map +1 -0
  62. package/dist/720.js +1 -0
  63. package/dist/720.js.map +1 -0
  64. package/dist/737.js +12 -0
  65. package/dist/737.js.map +1 -0
  66. package/dist/742.js +1 -0
  67. package/dist/742.js.map +1 -0
  68. package/dist/747.js +1 -0
  69. package/dist/747.js.map +1 -0
  70. package/dist/771.js +1 -0
  71. package/dist/771.js.map +1 -0
  72. package/dist/806.js +1 -0
  73. package/dist/806.js.map +1 -0
  74. package/dist/912.js +1 -0
  75. package/dist/912.js.map +1 -0
  76. package/dist/913.js +1 -0
  77. package/dist/913.js.map +1 -0
  78. package/dist/916.js +15 -0
  79. package/dist/916.js.map +1 -0
  80. package/dist/940.js +1 -0
  81. package/dist/940.js.map +1 -0
  82. package/dist/digihmis-esm-administration-app.js +6 -0
  83. package/dist/digihmis-esm-administration-app.js.buildmanifest.json +1100 -0
  84. package/dist/digihmis-esm-administration-app.js.map +1 -0
  85. package/dist/main.js +17 -0
  86. package/dist/main.js.map +1 -0
  87. package/dist/routes.json +1 -0
  88. package/increment-version.js +17 -0
  89. package/jest.config.js +26 -0
  90. package/package.json +51 -0
  91. package/release-version.js +16 -0
  92. package/rspack.config.js +1 -0
  93. package/src/bed-management/bed-management.component.tsx +19 -0
  94. package/src/bed-management/bed-management.scss +0 -0
  95. package/src/billing-management/billing-charge-items/billing-charge-items-modal/billing-charge-items.modal.tsx +79 -0
  96. package/src/billing-management/billing-charge-items/billing-charge-items-modal/billing-charge-items.resource.ts +11 -0
  97. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-item.workspace.tsx +212 -0
  98. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-items.resource.ts +51 -0
  99. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-service.workspace.tsx +296 -0
  100. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/concept-search.component.tsx +67 -0
  101. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.tsx +81 -0
  102. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/stock-search.component.tsx +107 -0
  103. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/global/billing-charge-commodity.scss +61 -0
  104. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/global/billing-charge-items.scss +59 -0
  105. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/schemas/form-schemas.ts +33 -0
  106. package/src/billing-management/billing-charge-items/billing-charge-items.component.tsx +222 -0
  107. package/src/billing-management/billing-charge-items/billing-charge-items.resource.ts +20 -0
  108. package/src/billing-management/billing-charge-items/billing-charge-items.scss +84 -0
  109. package/src/billing-management/billing-charge-items/type/index.ts +88 -0
  110. package/src/billing-management/billing-charge-items/utils/index.ts +96 -0
  111. package/src/billing-management/billing-management.component.tsx +21 -0
  112. package/src/billing-management/billing-management.scss +0 -0
  113. package/src/billing-management/billing-payment-modes/billing-payment-modes-modal/billing-delete-payment-modes.modal.scss +28 -0
  114. package/src/billing-management/billing-payment-modes/billing-payment-modes-modal/billing-delete-payment-modes.modal.tsx +67 -0
  115. package/src/billing-management/billing-payment-modes/billing-payment-modes-modal/delete-payment-mode.test.tsx +56 -0
  116. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes-form-schema.ts +52 -0
  117. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes.resource.ts +35 -0
  118. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes.workspace.scss +33 -0
  119. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes.workspace.test.tsx +239 -0
  120. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes.workspace.tsx +218 -0
  121. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/payment-attributes/payment-mode-attributes.component.tsx +156 -0
  122. package/src/billing-management/billing-payment-modes/billing-payment-modes-workspace/payment-attributes/payment-mode-attributes.scss +10 -0
  123. package/src/billing-management/billing-payment-modes/billing-payment-modes.component.tsx +243 -0
  124. package/src/billing-management/billing-payment-modes/billing-payment-modes.resource.ts +23 -0
  125. package/src/billing-management/billing-payment-modes/billing-payment-modes.scss +140 -0
  126. package/src/billing-management/billing-payment-modes/type/index.ts +42 -0
  127. package/src/billing-management/billing-payment-points/billing-payment-point-modal/billing-delete-payment-modes.modal.scss +28 -0
  128. package/src/billing-management/billing-payment-points/billing-payment-point-modal/billing-delete-payment-point.modal.tsx +72 -0
  129. package/src/billing-management/billing-payment-points/billing-payment-point-modal/delete-payment-point.test.tsx +56 -0
  130. package/src/billing-management/billing-payment-points/billing-payment-point.component.tsx +202 -0
  131. package/src/billing-management/billing-payment-points/billing-payment-point.resource.ts +16 -0
  132. package/src/billing-management/billing-payment-points/billing-payment-point.scss +0 -0
  133. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-point.resource.ts +39 -0
  134. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-point.workspace.scss +33 -0
  135. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-points.workspace.tsx +194 -0
  136. package/src/billing-management/billing-payment-points/type/index.ts +35 -0
  137. package/src/billing-management/billing-tabs/billing-tabs.component.scss +15 -0
  138. package/src/billing-management/billing-tabs/billing-tabs.component.tsx +36 -0
  139. package/src/components/custom-icon-wrapper/custom-icon-wrapper.component.tsx +17 -0
  140. package/src/components/custom-page-header/custom-page-header.component.tsx +38 -0
  141. package/src/components/custom-page-header/custom-page-header.scss +76 -0
  142. package/src/config-schema.ts +142 -0
  143. package/src/constant/index.ts +1 -0
  144. package/src/dashboard.meta.ts +47 -0
  145. package/src/declarations.d.ts +6 -0
  146. package/src/form-builder/form-builder.component.tsx +19 -0
  147. package/src/form-builder/form-builder.scss +0 -0
  148. package/src/hooks/useConcept.ts +13 -0
  149. package/src/hooks/useQueues.ts +22 -0
  150. package/src/hooks/useSystemSetting.ts +18 -0
  151. package/src/index.ts +133 -0
  152. package/src/left-panel/left-panel.component.tsx +49 -0
  153. package/src/left-panel/left-panel.scss +13 -0
  154. package/src/location-management/components/auto-suggest/autosuggest.component.tsx +149 -0
  155. package/src/location-management/components/auto-suggest/autosuggest.scss +61 -0
  156. package/src/location-management/components/auto-suggest/location-autosuggest.component.tsx +97 -0
  157. package/src/location-management/components/auto-suggest/location-autosuggest.scss +48 -0
  158. package/src/location-management/components/custom-results-tile/results-tile.component.tsx +43 -0
  159. package/src/location-management/components/custom-results-tile/results-tile.scss +86 -0
  160. package/src/location-management/forms/add-location/add-location.workspace.scss +71 -0
  161. package/src/location-management/forms/add-location/add-location.workspace.tsx +199 -0
  162. package/src/location-management/forms/search-location/search-location.workspace.scss +71 -0
  163. package/src/location-management/forms/search-location/search-location.workspace.tsx +214 -0
  164. package/src/location-management/helpers/index.ts +33 -0
  165. package/src/location-management/hooks/UseFacilityLocations.ts +12 -0
  166. package/src/location-management/hooks/useLocation.ts +18 -0
  167. package/src/location-management/hooks/useLocationTags.ts +15 -0
  168. package/src/location-management/location-management-dashboard.component.tsx +21 -0
  169. package/src/location-management/location-management-dashboard.scss +3 -0
  170. package/src/location-management/tables/locations-table.component.tsx +242 -0
  171. package/src/location-management/tables/locations-table.resource.ts +26 -0
  172. package/src/location-management/tables/locations-table.scss +114 -0
  173. package/src/location-management/types/index.ts +120 -0
  174. package/src/queue-management/queue-management-dashboard.component.tsx +23 -0
  175. package/src/queue-management/queue-management-dashboard.scss +8 -0
  176. package/src/queue-management/queue-management-table/queue-management-table.component.tsx +296 -0
  177. package/src/queue-management/queue-management-table/queue-management-table.scss +78 -0
  178. package/src/queue-management/queue-management-table/queue-management.resource.ts +39 -0
  179. package/src/queue-management/queue-management-workspace/queue-room/queue-room-form.scss +56 -0
  180. package/src/queue-management/queue-management-workspace/queue-room/queue-room-form.test.tsx +82 -0
  181. package/src/queue-management/queue-management-workspace/queue-room/queue-room-form.workspace.tsx +169 -0
  182. package/src/queue-management/queue-management-workspace/queue-room/queue-room.resource.ts +20 -0
  183. package/src/queue-management/queue-management-workspace/queue-service/queue-service-form.scss +61 -0
  184. package/src/queue-management/queue-management-workspace/queue-service/queue-service-form.test.tsx +127 -0
  185. package/src/queue-management/queue-management-workspace/queue-service/queue-service-form.workspace.tsx +178 -0
  186. package/src/queue-management/queue-management-workspace/queue-service/queue-service.resource.ts +55 -0
  187. package/src/queue-management/queue-types/index.ts +33 -0
  188. package/src/routes.json +157 -0
  189. package/src/setup-tests.ts +1 -0
  190. package/src/system-info/system-info.component.tsx +338 -0
  191. package/src/system-info/system-info.resource.ts +40 -0
  192. package/src/system-info/system-info.scss +622 -0
  193. package/src/type/index.ts +6 -0
  194. package/translations/am.json +111 -0
  195. package/translations/ar.json +115 -0
  196. package/translations/en.json +111 -0
  197. package/translations/es.json +112 -0
  198. package/translations/fr.json +112 -0
  199. package/translations/he.json +112 -0
  200. package/translations/km.json +110 -0
  201. package/translations/pt.json +112 -0
  202. package/translations/sw.json +111 -0
  203. package/translations/vi.json +110 -0
  204. package/translations/zh.json +110 -0
  205. package/tsconfig.json +5 -0
  206. package/version.js +27 -0
@@ -0,0 +1,199 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import {
3
+ type DefaultWorkspaceProps,
4
+ ResponsiveWrapper,
5
+ useLayoutType,
6
+ showSnackbar,
7
+ restBaseUrl,
8
+ } from '@openmrs/esm-framework';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Controller, useForm } from 'react-hook-form';
11
+ import {
12
+ ButtonSet,
13
+ Button,
14
+ InlineLoading,
15
+ TextInput,
16
+ FormGroup,
17
+ Stack,
18
+ Form,
19
+ FilterableMultiSelect,
20
+ } from '@carbon/react';
21
+ import classNames from 'classnames';
22
+ import { z } from 'zod';
23
+ import { zodResolver } from '@hookform/resolvers/zod';
24
+ import styles from './add-location.workspace.scss';
25
+ import { type LocationResponse } from '../../types';
26
+ import { extractErrorMessagesFromResponse } from '../../helpers';
27
+ import { useLocationTags } from '../../hooks/useLocationTags';
28
+ import { mutate } from 'swr';
29
+ import { saveOrUpdateLocation } from '../../hooks/useLocation';
30
+
31
+ type AddLocationWorkspaceProps = DefaultWorkspaceProps & {
32
+ location?: LocationResponse;
33
+ };
34
+
35
+ const locationFormSchema = z.object({
36
+ name: z.string().min(1, { message: 'Location name is required' }),
37
+ tags: z
38
+ .object({
39
+ uuid: z.string().uuid(),
40
+ display: z.string(),
41
+ })
42
+ .array()
43
+ .nonempty('At least one tag is required'),
44
+ });
45
+
46
+ type LocationFormType = z.infer<typeof locationFormSchema>;
47
+
48
+ const AddLocationWorkspace: React.FC<AddLocationWorkspaceProps> = ({
49
+ closeWorkspace,
50
+ closeWorkspaceWithSavedChanges,
51
+ promptBeforeClosing,
52
+ location,
53
+ }) => {
54
+ const { t } = useTranslation();
55
+ const isTablet = useLayoutType() === 'tablet';
56
+ const { locationTagList: Tags } = useLocationTags();
57
+
58
+ const hasLocationAttributes = useMemo(() => {
59
+ return location?.attributes && location.attributes.length > 0;
60
+ }, [location?.attributes]);
61
+
62
+ const handleMutation = () => {
63
+ const url = `${restBaseUrl}/location`;
64
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
65
+ };
66
+
67
+ const {
68
+ handleSubmit,
69
+ control,
70
+ getValues,
71
+ formState: { isSubmitting, isDirty, errors },
72
+ } = useForm<LocationFormType>({
73
+ resolver: zodResolver(locationFormSchema),
74
+ defaultValues: {
75
+ name: location?.name || '',
76
+ tags: location?.tags || [],
77
+ },
78
+ });
79
+
80
+ const onSubmit = async (data: LocationFormType) => {
81
+ const formDataFormSubmission = getValues();
82
+
83
+ const locationTagsUuid = formDataFormSubmission?.tags?.map((tag) => tag.uuid) || [];
84
+
85
+ const locationPayload = {
86
+ name: formDataFormSubmission.name,
87
+ tags: locationTagsUuid,
88
+ };
89
+
90
+ try {
91
+ const locationUuid = location?.uuid && typeof location.uuid === 'string' ? location.uuid : undefined;
92
+
93
+ await saveOrUpdateLocation(locationPayload, locationUuid);
94
+
95
+ showSnackbar({
96
+ title: t('success', 'Success'),
97
+ kind: 'success',
98
+ subtitle: locationUuid
99
+ ? t('locationUpdated', 'Location {{locationName}} was updated successfully.', {
100
+ locationName: data.name,
101
+ })
102
+ : t('locationCreated', 'Location {{locationName}} was created successfully.', {
103
+ locationName: data.name,
104
+ }),
105
+ });
106
+ handleMutation();
107
+ closeWorkspaceWithSavedChanges();
108
+ } catch (error: any) {
109
+ const errorMessages = extractErrorMessagesFromResponse(error);
110
+ showSnackbar({
111
+ title: t('error', 'Error'),
112
+ kind: 'error',
113
+ subtitle: errorMessages.join(', ') || t('locationSaveError', 'Error saving location'),
114
+ });
115
+ }
116
+ };
117
+
118
+ useEffect(() => {
119
+ promptBeforeClosing(() => isDirty);
120
+ }, [isDirty, promptBeforeClosing]);
121
+
122
+ return (
123
+ <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
124
+ <div className={styles.formContainer}>
125
+ <Stack gap={3}>
126
+ <ResponsiveWrapper>
127
+ <FormGroup legendText="">
128
+ <Controller
129
+ control={control}
130
+ name="name"
131
+ render={({ field }) => (
132
+ <TextInput
133
+ id="locationName"
134
+ placeholder={t('locationPlaceholder', 'Add a location')}
135
+ labelText={t('locationName', 'Location Name')}
136
+ value={field.value}
137
+ onChange={field.onChange}
138
+ invalid={!!errors.name?.message}
139
+ invalidText={errors.name?.message}
140
+ disabled={hasLocationAttributes}
141
+ />
142
+ )}
143
+ />
144
+ </FormGroup>
145
+ </ResponsiveWrapper>
146
+
147
+ <ResponsiveWrapper>
148
+ <FormGroup legendText="">
149
+ <Controller
150
+ control={control}
151
+ name="tags"
152
+ render={({ field: { onChange, value, ref } }) => (
153
+ <FilterableMultiSelect
154
+ id="locationTags"
155
+ titleText={t('selectTags', 'Select tag(s)')}
156
+ placeholder={t('selectTagPlaceholder', 'Select a tag')}
157
+ items={Tags || []}
158
+ selectedItems={(value || []).map(
159
+ (selected) => Tags?.find((tag) => tag.uuid === selected.uuid) || selected,
160
+ )}
161
+ onChange={({ selectedItems }) => onChange(selectedItems || [])}
162
+ itemToString={(item) => (item && typeof item === 'object' ? item.display : '')}
163
+ selectionFeedback="top-after-reopen"
164
+ invalid={!!errors.tags?.message}
165
+ invalidText={errors.tags?.message}
166
+ disabled={!Tags?.length}
167
+ ref={ref}
168
+ />
169
+ )}
170
+ />
171
+ </FormGroup>
172
+ </ResponsiveWrapper>
173
+ </Stack>
174
+ </div>
175
+
176
+ <ButtonSet
177
+ className={classNames({
178
+ [styles.tablet]: isTablet,
179
+ [styles.desktop]: !isTablet,
180
+ })}>
181
+ <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
182
+ {t('cancel', 'Cancel')}
183
+ </Button>
184
+ <Button className={styles.buttonContainer} disabled={isSubmitting || !isDirty} kind="primary" type="submit">
185
+ {isSubmitting ? (
186
+ <span className={styles.inlineLoading}>
187
+ {t('submitting', 'Submitting...')}
188
+ <InlineLoading status="active" iconDescription="Loading" />
189
+ </span>
190
+ ) : (
191
+ t('save', 'Save')
192
+ )}
193
+ </Button>
194
+ </ButtonSet>
195
+ </Form>
196
+ );
197
+ };
198
+
199
+ export default AddLocationWorkspace;
@@ -0,0 +1,71 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .form {
6
+ display: flex;
7
+ flex-direction: column;
8
+ justify-content: space-between;
9
+ height: 100%;
10
+ }
11
+
12
+ .formContainer {
13
+ margin: layout.$spacing-05;
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: layout.$spacing-05;
17
+
18
+ :global(.omrs-breakpoint-lt-tablet) & {
19
+ margin: layout.$spacing-03;
20
+ gap: layout.$spacing-03;
21
+ }
22
+ }
23
+
24
+ .tablet {
25
+ padding: layout.$spacing-06 layout.$spacing-05;
26
+ background-color: colors.$white;
27
+
28
+ :global(.omrs-breakpoint-lt-tablet) & {
29
+ padding: layout.$spacing-05 layout.$spacing-03;
30
+ }
31
+ }
32
+
33
+ .desktop {
34
+ padding: 0;
35
+ margin: 0 layout.$spacing-05 layout.$spacing-05;
36
+
37
+ :global(.omrs-breakpoint-lt-desktop) & {
38
+ margin: 0 layout.$spacing-03 layout.$spacing-03;
39
+ }
40
+ }
41
+
42
+ .buttonContainer {
43
+ height: layout.$spacing-10;
44
+ display: flex;
45
+ align-content: flex-start;
46
+ align-items: baseline;
47
+ min-width: 50%;
48
+
49
+ :global(.omrs-breakpoint-lt-tablet) & {
50
+ max-width: none;
51
+ flex: 1;
52
+ }
53
+ }
54
+
55
+ .inlineLoading {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: layout.$spacing-03;
59
+
60
+ :global(.omrs-breakpoint-lt-tablet) & {
61
+ gap: layout.$spacing-02;
62
+ }
63
+ }
64
+
65
+ .button {
66
+ height: layout.$spacing-10;
67
+ display: flex;
68
+ align-content: flex-start;
69
+ align-items: baseline;
70
+ min-width: 50%;
71
+ }
@@ -0,0 +1,214 @@
1
+ import { Button, ButtonSet, FilterableMultiSelect, Form, FormGroup, InlineLoading, Stack } from '@carbon/react';
2
+ import { zodResolver } from '@hookform/resolvers/zod';
3
+ import {
4
+ type DefaultWorkspaceProps,
5
+ ResponsiveWrapper,
6
+ restBaseUrl,
7
+ showSnackbar,
8
+ useLayoutType,
9
+ } from '@openmrs/esm-framework';
10
+ import classNames from 'classnames';
11
+ import React, { useEffect, useState } from 'react';
12
+ import { Controller, useForm } from 'react-hook-form';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { mutate } from 'swr';
15
+ import { z } from 'zod';
16
+ import { saveOrUpdateLocation } from '../../hooks/useLocation';
17
+ import { useLocationTags } from '../../hooks/useLocationTags';
18
+ import { LocationResponse } from '../../types';
19
+ import styles from './search-location.workspace.scss';
20
+ import { extractErrorMessagesFromResponse } from '../../helpers';
21
+ import ResultsTile from '../../components/custom-results-tile/results-tile.component';
22
+ import { LocationAutosuggest } from '../../components/auto-suggest/location-autosuggest.component';
23
+
24
+ type AddLocationWorkspaceProps = DefaultWorkspaceProps & {
25
+ location?: LocationResponse;
26
+ };
27
+
28
+ const locationFormSchema = z.object({
29
+ name: z.string().min(1, { message: 'Location name is required' }),
30
+ tags: z
31
+ .object({
32
+ uuid: z.string().uuid(),
33
+ display: z.string(),
34
+ })
35
+ .array(),
36
+ });
37
+
38
+ type LocationFormType = z.infer<typeof locationFormSchema>;
39
+
40
+ const SearchLocationWorkspace: React.FC<AddLocationWorkspaceProps> = ({
41
+ closeWorkspace,
42
+ closeWorkspaceWithSavedChanges,
43
+ promptBeforeClosing,
44
+ location,
45
+ }) => {
46
+ const { t } = useTranslation();
47
+ const isTablet = useLayoutType() === 'tablet';
48
+ const { locationTagList: Tags } = useLocationTags();
49
+ const [selectedLocation, setSelectedLocation] = useState<LocationResponse | null>(null);
50
+
51
+ const handleMutation = () => {
52
+ const url = `${restBaseUrl}/location`;
53
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
54
+ };
55
+
56
+ const {
57
+ handleSubmit,
58
+ control,
59
+ setValue,
60
+ reset,
61
+ formState: { isSubmitting, isDirty, errors },
62
+ } = useForm<LocationFormType>({
63
+ resolver: zodResolver(locationFormSchema),
64
+ defaultValues: {
65
+ name: location?.name || '',
66
+ tags: location?.tags || [],
67
+ },
68
+ });
69
+
70
+ const onSubmit = async (data: LocationFormType) => {
71
+ try {
72
+ const locationUuid = selectedLocation?.uuid || location?.uuid;
73
+
74
+ const locationTagsUuid = data.tags.map((tag) => tag.uuid);
75
+
76
+ const locationPayload = {
77
+ tags: locationTagsUuid,
78
+ };
79
+
80
+ await saveOrUpdateLocation(locationUuid, locationPayload);
81
+
82
+ showSnackbar({
83
+ title: t('success', 'Success'),
84
+ kind: 'success',
85
+ subtitle: t('locationUpdated', 'Location {{locationName}} was updated successfully.', {
86
+ locationName: selectedLocation?.name || location?.name || data.name,
87
+ }),
88
+ });
89
+
90
+ handleMutation();
91
+ closeWorkspaceWithSavedChanges();
92
+ } catch (error: any) {
93
+ console.error('Error saving location:', error);
94
+ const errorMessages = extractErrorMessagesFromResponse(error);
95
+ showSnackbar({
96
+ title: t('error', 'Error'),
97
+ kind: 'error',
98
+ subtitle: errorMessages.join(', ') || t('locationSaveError', 'Error saving location'),
99
+ });
100
+ }
101
+ };
102
+
103
+ const handleLocationSelected = (locationUuid: string, locationData: LocationResponse) => {
104
+ setSelectedLocation(locationData);
105
+
106
+ setValue('name', locationData.name || '', { shouldDirty: true });
107
+
108
+ if (locationData?.tags && locationData.tags.length > 0) {
109
+ const formattedTags = locationData.tags.map((tag) => ({
110
+ uuid: tag.uuid,
111
+ display: tag.display || tag.name || '',
112
+ }));
113
+ if (formattedTags.length > 0) {
114
+ setValue('tags', formattedTags as [(typeof formattedTags)[0], ...typeof formattedTags], { shouldDirty: true });
115
+ }
116
+ } else {
117
+ setValue('tags', [], { shouldDirty: true });
118
+ }
119
+ };
120
+
121
+ const handleClearSelection = () => {
122
+ setSelectedLocation(null);
123
+ reset();
124
+ };
125
+
126
+ const renderSelectedLocationTile = () => {
127
+ if (!selectedLocation) {
128
+ return null;
129
+ }
130
+
131
+ return (
132
+ <div>
133
+ <ResultsTile location={selectedLocation} onClose={handleClearSelection} />
134
+ </div>
135
+ );
136
+ };
137
+
138
+ useEffect(() => {
139
+ promptBeforeClosing(() => isDirty);
140
+ }, [isDirty, promptBeforeClosing]);
141
+
142
+ const isFormReady = (selectedLocation || location) && isDirty;
143
+
144
+ return (
145
+ <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
146
+ <div className={styles.formContainer}>
147
+ <Stack gap={3}>
148
+ <ResponsiveWrapper>
149
+ <FormGroup legendText="">
150
+ {!selectedLocation ? (
151
+ <LocationAutosuggest
152
+ onLocationSelected={handleLocationSelected}
153
+ labelText={t('searchForLocation', 'Search for location')}
154
+ placeholder={t('searchParentLocation', 'Search for location...')}
155
+ />
156
+ ) : (
157
+ renderSelectedLocationTile()
158
+ )}
159
+ </FormGroup>
160
+ </ResponsiveWrapper>
161
+
162
+ <ResponsiveWrapper>
163
+ <FormGroup legendText="">
164
+ <Controller
165
+ control={control}
166
+ name="tags"
167
+ render={({ field: { onChange, value, ref } }) => (
168
+ <FilterableMultiSelect
169
+ id="locationTags"
170
+ titleText={t('selectTags', 'Select tag(s)')}
171
+ placeholder={t('selectTagPlaceholder', 'Select a tag')}
172
+ items={Tags || []}
173
+ selectedItems={(value || []).map(
174
+ (selected) => Tags?.find((tag) => tag.uuid === selected.uuid) || selected,
175
+ )}
176
+ onChange={({ selectedItems }) => onChange(selectedItems || [])}
177
+ itemToString={(item) => (item && typeof item === 'object' ? item.display : '')}
178
+ selectionFeedback="top-after-reopen"
179
+ invalid={!!errors.tags?.message}
180
+ invalidText={errors.tags?.message}
181
+ disabled={!Tags?.length}
182
+ ref={ref}
183
+ />
184
+ )}
185
+ />
186
+ </FormGroup>
187
+ </ResponsiveWrapper>
188
+ </Stack>
189
+ </div>
190
+
191
+ <ButtonSet
192
+ className={classNames({
193
+ [styles.tablet]: isTablet,
194
+ [styles.desktop]: !isTablet,
195
+ })}>
196
+ <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
197
+ {t('cancel', 'Cancel')}
198
+ </Button>
199
+ <Button className={styles.buttonContainer} disabled={isSubmitting || !isFormReady} kind="primary" type="submit">
200
+ {isSubmitting ? (
201
+ <span className={styles.inlineLoading}>
202
+ {t('submitting', 'Submitting...')}
203
+ <InlineLoading status="active" iconDescription="Loading" />
204
+ </span>
205
+ ) : (
206
+ t('saveAndClose', 'Save & close')
207
+ )}
208
+ </Button>
209
+ </ButtonSet>
210
+ </Form>
211
+ );
212
+ };
213
+
214
+ export default SearchLocationWorkspace;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Extracts a list of error messages from an error object returned from an API call.
3
+ * Checks if the error object contains a fieldErrors property, and if so, extracts all the error messages from the field errors.
4
+ * If the error object does not contain a fieldErrors property, it returns the error message from the error object.
5
+ * @param errorObject an object containing an error response from an API call.
6
+ * @returns a list of error messages.
7
+ */
8
+
9
+ interface FieldError {
10
+ message: string;
11
+ }
12
+
13
+ interface ErrorResponseBody {
14
+ error?: {
15
+ message?: string;
16
+ fieldErrors?: Record<string, FieldError[]>;
17
+ };
18
+ }
19
+
20
+ interface APIErrorObject {
21
+ responseBody?: ErrorResponseBody;
22
+ message?: string;
23
+ }
24
+
25
+ export function extractErrorMessagesFromResponse(errorObject: APIErrorObject) {
26
+ const fieldErrors = errorObject?.responseBody?.error?.fieldErrors;
27
+
28
+ if (!fieldErrors) {
29
+ return [errorObject?.responseBody?.error?.message ?? errorObject?.message];
30
+ }
31
+
32
+ return Object.values(fieldErrors).flatMap((errors: Array<Error>) => errors.map((error) => error.message));
33
+ }
@@ -0,0 +1,12 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { type LocationResponse } from '../types';
3
+
4
+ export const searchLocation = async (searchTerm: string) => {
5
+ const customPresentation =
6
+ 'custom:(uuid,display,name,description,stateProvince,country,countyDistrict,address5,address6,tags,attributes:(uuid,attributeType:(uuid,display),value))';
7
+
8
+ const url = `${restBaseUrl}/location?v=${customPresentation}&q=${searchTerm}`;
9
+
10
+ const response = await openmrsFetch<{ results: Array<LocationResponse> }>(url);
11
+ return response?.data?.results ?? [];
12
+ };
@@ -0,0 +1,18 @@
1
+ import { restBaseUrl, openmrsFetch } from '@openmrs/esm-framework';
2
+
3
+ export const saveOrUpdateLocation = async (locationPayload, locationUuid) => {
4
+ if (typeof locationPayload === 'string' && typeof locationUuid === 'object') {
5
+ const temp = locationPayload;
6
+ locationPayload = locationUuid;
7
+ locationUuid = temp;
8
+ }
9
+
10
+ const url = locationUuid ? `${restBaseUrl}/location/${locationUuid}` : `${restBaseUrl}/location`;
11
+ return await openmrsFetch(url, {
12
+ method: 'POST',
13
+ body: locationPayload,
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ });
18
+ };
@@ -0,0 +1,15 @@
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import { type LocationTagsResponse } from '../types/index';
4
+
5
+ export const useLocationTags = () => {
6
+ const customPresentation = 'custom:(uuid,display,name,description)';
7
+ const url = `${restBaseUrl}/locationtag?v=${customPresentation}`;
8
+ const { isLoading, error, data } = useSWR<FetchResponse<LocationTagsResponse>>(url, openmrsFetch);
9
+
10
+ return {
11
+ isLoading,
12
+ error,
13
+ locationTagList: data?.data?.results || [],
14
+ };
15
+ };
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { ExtensionSlot, useConfig } from '@openmrs/esm-framework';
3
+ import styles from './location-management-dashboard.scss';
4
+ import { CustomPageHeader } from '../components/custom-page-header/custom-page-header.component';
5
+ import { ConfigObject } from '../config-schema';
6
+ import { useTranslation } from 'react-i18next';
7
+ import LocationsTable from './tables/locations-table.component';
8
+
9
+ const LocationManagementDashboard: React.FC = () => {
10
+ const { t } = useTranslation();
11
+ const { locationIcon } = useConfig<ConfigObject>();
12
+
13
+ return (
14
+ <div className={styles.dashboardContainer}>
15
+ <CustomPageHeader subTitle={t('manageLocations', 'Manage locations and tags')} iconSrc={locationIcon.src} />
16
+ <LocationsTable />
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default LocationManagementDashboard;
@@ -0,0 +1,3 @@
1
+ .dashboardContainer {
2
+ background: var(--cds-field);
3
+ }