@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,169 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ Column,
5
+ Form,
6
+ Layer,
7
+ Stack,
8
+ TextInput,
9
+ Select,
10
+ SelectItem,
11
+ ButtonSet,
12
+ Button,
13
+ InlineNotification,
14
+ } from '@carbon/react';
15
+ import { type DefaultWorkspaceProps, restBaseUrl, showSnackbar, useLayoutType } from '@openmrs/esm-framework';
16
+ import { mutate } from 'swr';
17
+ import { saveQueueRoom } from './queue-room.resource';
18
+ import styles from './queue-room-form.scss';
19
+ import { useQueues } from '../../../hooks/useQueues';
20
+ import { useQueueLocations } from '../queue-service/queue-service.resource';
21
+
22
+ const QueueRoomForm: React.FC<DefaultWorkspaceProps> = ({ closeWorkspace }) => {
23
+ const { t } = useTranslation();
24
+ const isTablet = useLayoutType() === 'tablet';
25
+ const [queueRoomName, setQueueRoomName] = useState('');
26
+ const [queueRoomService, setQueueRoomService] = useState('');
27
+ const [isMissingRoomName, setMissingRoomName] = useState(false);
28
+ const [isMissingQueueRoomService, setMissingQueueRoomService] = useState(false);
29
+ const [selectedQueueLocation, setSelectedQueueLocation] = useState('');
30
+ const { queues } = useQueues(selectedQueueLocation);
31
+ const { queueLocations } = useQueueLocations();
32
+
33
+ const createQueueRoom = useCallback(
34
+ (event) => {
35
+ event.preventDefault();
36
+
37
+ if (!queueRoomName) {
38
+ setMissingRoomName(true);
39
+ return;
40
+ }
41
+ if (!queueRoomService) {
42
+ setMissingQueueRoomService(true);
43
+ return;
44
+ }
45
+
46
+ setMissingRoomName(false);
47
+ setMissingQueueRoomService(false);
48
+
49
+ saveQueueRoom(queueRoomName, queueRoomName, queueRoomService).then(
50
+ ({ status }) => {
51
+ if (status === 201) {
52
+ showSnackbar({
53
+ title: t('addQueueRoom', 'Add queue room'),
54
+ kind: 'success',
55
+ subtitle: t('queueRoomAddedSuccessfully', 'Queue room added successfully'),
56
+ });
57
+ closeWorkspace();
58
+ mutate(`${restBaseUrl}/queueroom`);
59
+ }
60
+ },
61
+ (error) => {
62
+ showSnackbar({
63
+ title: t('errorAddingQueueRoom', 'Error adding queue room'),
64
+ kind: 'error',
65
+ isLowContrast: false,
66
+ subtitle: error?.message,
67
+ });
68
+ },
69
+ );
70
+ },
71
+ [queueRoomName, queueRoomService, t, closeWorkspace],
72
+ );
73
+
74
+ return (
75
+ <Form onSubmit={createQueueRoom} className={styles.form}>
76
+ <Stack gap={4} className={styles.grid}>
77
+ <Column>
78
+ <Layer className={styles.input}>
79
+ <TextInput
80
+ id="queueRoomName"
81
+ invalidText="Required"
82
+ labelText={t('queueRoomName', 'Queue room name')}
83
+ onChange={(event) => setQueueRoomName(event.target.value)}
84
+ value={queueRoomName}
85
+ />
86
+ {isMissingRoomName && (
87
+ <section>
88
+ <InlineNotification
89
+ style={{ margin: '0', minWidth: '100%' }}
90
+ kind="error"
91
+ lowContrast={true}
92
+ title={t('missingQueueRoomName', 'Missing queue room name')}
93
+ subtitle={t('addQueueRoomName', 'Please add a queue room name')}
94
+ />
95
+ </section>
96
+ )}
97
+ </Layer>
98
+ </Column>
99
+
100
+ <Column>
101
+ <Layer className={styles.input}>
102
+ <section className={styles.section}>
103
+ <Select
104
+ labelText={t('selectQueueLocation', 'Select a queue location')}
105
+ id="location"
106
+ invalidText="Required"
107
+ value={selectedQueueLocation}
108
+ onChange={(event) => {
109
+ setSelectedQueueLocation(event.target.value);
110
+ }}>
111
+ {!selectedQueueLocation ? (
112
+ <SelectItem text={t('selectQueueLocation', 'Select a queue location')} value="" />
113
+ ) : null}
114
+ {queueLocations?.length > 0 &&
115
+ queueLocations.map((location) => (
116
+ <SelectItem key={location.id} text={location.name} value={location.id}>
117
+ {location.name}
118
+ </SelectItem>
119
+ ))}
120
+ </Select>
121
+ </section>
122
+ </Layer>
123
+ </Column>
124
+
125
+ <Column>
126
+ <Layer className={styles.input}>
127
+ <section className={styles.section}>
128
+ <Select
129
+ labelText={t('selectService', 'Select a service')}
130
+ id="service"
131
+ invalidText="Required"
132
+ value={queueRoomService}
133
+ onChange={(event) => setQueueRoomService(event.target.value)}>
134
+ {!queueRoomService ? <SelectItem text={t('selectService', 'Select a service')} value="" /> : null}
135
+ {queues?.length > 0 &&
136
+ queues.map((service) => (
137
+ <SelectItem key={service.uuid} text={service.display} value={service.uuid}>
138
+ {service.display}
139
+ </SelectItem>
140
+ ))}
141
+ </Select>
142
+ </section>
143
+ {isMissingQueueRoomService && (
144
+ <section>
145
+ <InlineNotification
146
+ style={{ margin: '0', minWidth: '100%' }}
147
+ kind="error"
148
+ lowContrast={true}
149
+ title={t('missingQueueRoomService', 'Missing queue room service')}
150
+ subtitle={t('addQueueRoomService', 'Please add a queue room service')}
151
+ />
152
+ </section>
153
+ )}
154
+ </Layer>
155
+ </Column>
156
+ </Stack>
157
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
158
+ <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
159
+ {t('cancel', 'Cancel')}
160
+ </Button>
161
+ <Button className={styles.button} kind="primary" type="submit">
162
+ {t('save', 'Save')}
163
+ </Button>
164
+ </ButtonSet>
165
+ </Form>
166
+ );
167
+ };
168
+
169
+ export default QueueRoomForm;
@@ -0,0 +1,20 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ export function saveQueueRoom(name: string, description: string, queueUuid: string) {
4
+ const abortController = new AbortController();
5
+
6
+ return openmrsFetch(`${restBaseUrl}/queue-room`, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ signal: abortController.signal,
12
+ body: {
13
+ name,
14
+ description,
15
+ queue: {
16
+ uuid: queueUuid,
17
+ },
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,61 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .heading {
6
+ @include type.type-style('heading-compact-01');
7
+ margin: layout.$spacing-05 0 layout.$spacing-05;
8
+ }
9
+
10
+ .warningContainer {
11
+ background-color: $carbon--red-50;
12
+ padding: layout.$spacing-04;
13
+ margin: layout.$spacing-03 0 layout.$spacing-03;
14
+ display: flex;
15
+ justify-content: space-between;
16
+ .warning {
17
+ @include type.type-style('heading-compact-01');
18
+ color: $ui-05;
19
+ }
20
+ }
21
+
22
+ .form {
23
+ display: flex;
24
+ flex-direction: column;
25
+ justify-content: space-between;
26
+ height: 100%;
27
+ }
28
+
29
+ .grid {
30
+ margin: 0 layout.$spacing-05;
31
+ padding: 0;
32
+ }
33
+
34
+ .input {
35
+ margin-top: layout.$spacing-04;
36
+ }
37
+
38
+ .button {
39
+ height: layout.$spacing-10;
40
+ display: flex;
41
+ align-content: flex-start;
42
+ align-items: baseline;
43
+ min-width: 50%;
44
+ }
45
+
46
+ .buttonSet {
47
+ padding: 0;
48
+ margin-top: layout.$spacing-05;
49
+ }
50
+
51
+ /* Tablet */
52
+ :global(.omrs-breakpoint-lt-desktop) {
53
+ .form {
54
+ height: 100%;
55
+ }
56
+
57
+ .buttonSet {
58
+ padding: layout.$spacing-06 layout.$spacing-05;
59
+ background-color: $ui-02;
60
+ }
61
+ }
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { showSnackbar, useLayoutType } from '@openmrs/esm-framework';
5
+ import { saveQueue } from './queue-service.resource';
6
+ import QueueServiceForm from './queue-service-form.workspace';
7
+
8
+ const defaultProps = {
9
+ closeWorkspace: jest.fn(),
10
+ closeWorkspaceWithSavedChanges: jest.fn(),
11
+ promptBeforeClosing: jest.fn(),
12
+ setTitle: jest.fn(),
13
+ };
14
+
15
+ const mockSaveQueue = jest.mocked(saveQueue);
16
+ const mockShowSnackbar = jest.mocked(showSnackbar);
17
+ const mockUseLayoutType = jest.mocked(useLayoutType);
18
+
19
+ jest.mock('./queue-service.resource', () => ({
20
+ useServiceConcepts: () => ({
21
+ queueConcepts: [
22
+ { uuid: '6f017eb0-b035-4acd-b284-da45f5067502', display: 'Concept 1' },
23
+ { uuid: '5f017eb0-b035-4acd-b284-da45f5067502', display: 'Concept 2' },
24
+ ],
25
+ }),
26
+ saveQueue: jest.fn(() => Promise.resolve({ status: 201 })),
27
+ }));
28
+
29
+ jest.mock('../../queue-management-workspace/queue-service/queue-service.resource.ts', () => ({
30
+ useQueueLocations: () => ({
31
+ queueLocations: [
32
+ { id: '34567eb0-b035-4acd-b284-da45f5067502', name: 'Location 1' },
33
+ { id: '12wi7eb0-b035-4acd-b284-da45f5067502', name: 'Location 2' },
34
+ ],
35
+ }),
36
+ }));
37
+
38
+ describe('QueueServiceForm', () => {
39
+ beforeEach(() => {
40
+ mockUseLayoutType.mockReturnValue('tablet');
41
+ });
42
+
43
+ it('renders validation errors when form is submitted with missing fields', async () => {
44
+ const user = userEvent.setup();
45
+ render(<QueueServiceForm {...defaultProps} />);
46
+
47
+ const queueNameInput = screen.getByRole('textbox', { name: /queue name/i });
48
+ const serviceTypeSelect = screen.getByRole('combobox', { name: /select a service type/i });
49
+ const locationSelect = screen.getByRole('combobox', { name: /select a location/i });
50
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
51
+ const saveButton = screen.getByRole('button', { name: /save/i });
52
+ expect(cancelButton).toBeInTheDocument();
53
+ expect(saveButton).toBeInTheDocument();
54
+ expect(queueNameInput).toBeInTheDocument();
55
+ expect(queueNameInput).not.toBeInvalid();
56
+ expect(serviceTypeSelect).toBeInTheDocument();
57
+ expect(serviceTypeSelect).not.toBeInvalid();
58
+
59
+ await user.click(saveButton);
60
+ expect(queueNameInput).toBeInvalid();
61
+
62
+ await user.type(queueNameInput, 'Test Queue');
63
+ expect(queueNameInput).not.toBeInvalid();
64
+ expect(serviceTypeSelect).toBeInvalid();
65
+
66
+ await user.selectOptions(serviceTypeSelect, '6f017eb0-b035-4acd-b284-da45f5067502');
67
+ await user.selectOptions(locationSelect, '34567eb0-b035-4acd-b284-da45f5067502');
68
+ await user.click(saveButton);
69
+
70
+ expect(serviceTypeSelect).not.toBeInvalid();
71
+ expect(queueNameInput).not.toBeInvalid();
72
+ expect(locationSelect).not.toBeInvalid();
73
+ });
74
+
75
+ it('submits the form when all required fields are filled', async () => {
76
+ const user = userEvent.setup();
77
+ render(<QueueServiceForm {...defaultProps} />);
78
+
79
+ const queueNameInput = screen.getByRole('textbox', { name: /queue name/i });
80
+ const serviceTypeSelect = screen.getByRole('combobox', { name: /select a service type/i });
81
+ const locationSelect = screen.getByRole('combobox', { name: /select a location/i });
82
+ const saveButton = screen.getByRole('button', { name: /save/i });
83
+
84
+ await user.type(queueNameInput, 'Test Queue');
85
+ await user.selectOptions(serviceTypeSelect, '6f017eb0-b035-4acd-b284-da45f5067502');
86
+ await user.selectOptions(locationSelect, '34567eb0-b035-4acd-b284-da45f5067502');
87
+ await user.click(saveButton);
88
+
89
+ expect(mockSaveQueue).toHaveBeenCalledTimes(1);
90
+ expect(mockSaveQueue).toHaveBeenCalledWith(
91
+ 'Test Queue',
92
+ '6f017eb0-b035-4acd-b284-da45f5067502',
93
+ '',
94
+ '34567eb0-b035-4acd-b284-da45f5067502',
95
+ );
96
+ expect(mockShowSnackbar).toHaveBeenCalledTimes(1);
97
+ expect(mockShowSnackbar).toHaveBeenCalledWith({
98
+ kind: 'success',
99
+ title: expect.stringMatching(/queue service created/i),
100
+ subtitle: expect.stringMatching(/queue service created successfully/i),
101
+ });
102
+ });
103
+
104
+ it('renders an error message when the queue service creation fails', async () => {
105
+ const user = userEvent.setup();
106
+ mockSaveQueue.mockRejectedValueOnce(new Error('Internal server error'));
107
+ render(<QueueServiceForm {...defaultProps} />);
108
+
109
+ const queueNameInput = screen.getByRole('textbox', { name: /queue name/i });
110
+ const serviceTypeSelect = screen.getByRole('combobox', { name: /select a service type/i });
111
+ const locationSelect = screen.getByRole('combobox', { name: /select a location/i });
112
+ const saveButton = screen.getByRole('button', { name: /save/i });
113
+
114
+ await user.type(queueNameInput, 'Test Queue');
115
+ await user.selectOptions(serviceTypeSelect, '6f017eb0-b035-4acd-b284-da45f5067502');
116
+ await user.selectOptions(locationSelect, '34567eb0-b035-4acd-b284-da45f5067502');
117
+ await user.click(saveButton);
118
+
119
+ expect(mockShowSnackbar).toHaveBeenCalledTimes(1);
120
+ expect(mockShowSnackbar).toHaveBeenCalledWith({
121
+ isLowContrast: false,
122
+ kind: 'error',
123
+ title: expect.stringMatching(/error creating queue service/i),
124
+ subtitle: expect.stringMatching(/internal server error/i),
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,178 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { TFunction } from 'i18next';
4
+ import { useForm, Controller } from 'react-hook-form';
5
+ import { z } from 'zod';
6
+ import { zodResolver } from '@hookform/resolvers/zod';
7
+ import {
8
+ Button,
9
+ ButtonSet,
10
+ Column,
11
+ Form,
12
+ InlineLoading,
13
+ Layer,
14
+ Select,
15
+ SelectItem,
16
+ Stack,
17
+ TextInput,
18
+ } from '@carbon/react';
19
+ import { mutate } from 'swr';
20
+ import { type DefaultWorkspaceProps, restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
21
+ import styles from './queue-service-form.scss';
22
+ import { saveQueue, useQueueLocations, useServiceConcepts } from './queue-service.resource';
23
+
24
+ const createQueueServiceSchema = (t: TFunction) =>
25
+ z.object({
26
+ queueName: z
27
+ .string({
28
+ required_error: t('queueNameRequired', 'Queue name is required'),
29
+ })
30
+ .trim()
31
+ .min(1, t('queueNameRequired', 'Queue name is required')),
32
+ queueServiceType: z
33
+ .string({
34
+ required_error: t('queueConceptRequired', 'Queue concept is required'),
35
+ })
36
+ .trim()
37
+ .min(1, t('queueConceptRequired', 'Queue concept is required')),
38
+ userLocation: z
39
+ .string({
40
+ required_error: t('queueLocationRequired', 'Queue location is required'),
41
+ })
42
+ .trim()
43
+ .min(1, t('queueLocationRequired', 'Queue location is required')),
44
+ });
45
+
46
+ type QueueServiceFormData = z.infer<ReturnType<typeof createQueueServiceSchema>>;
47
+
48
+ const QueueServiceForm: React.FC<DefaultWorkspaceProps> = ({ closeWorkspace }) => {
49
+ const { t } = useTranslation();
50
+ const { queueConcepts } = useServiceConcepts();
51
+ const { queueLocations } = useQueueLocations();
52
+
53
+ const QueueServiceSchema = createQueueServiceSchema(t);
54
+
55
+ const {
56
+ control,
57
+ handleSubmit,
58
+ formState: { errors, isSubmitting },
59
+ } = useForm<QueueServiceFormData>({
60
+ resolver: zodResolver(QueueServiceSchema),
61
+ defaultValues: {
62
+ queueName: '',
63
+ queueServiceType: '',
64
+ userLocation: '',
65
+ },
66
+ });
67
+
68
+ const createQueue = async (data: QueueServiceFormData) => {
69
+ try {
70
+ await saveQueue(data.queueName, data.queueServiceType, '', data.userLocation);
71
+
72
+ showSnackbar({
73
+ title: t('queueServiceCreated', 'Queue service created'),
74
+ kind: 'success',
75
+ subtitle: t('queueServiceCreatedSuccessfully', 'Queue service created successfully'),
76
+ });
77
+
78
+ closeWorkspace();
79
+ await Promise.all([
80
+ mutate(`${restBaseUrl}/queue?${data.userLocation}`),
81
+ mutate(`${restBaseUrl}/queue?location=${data.userLocation}`),
82
+ ]);
83
+ } catch (error) {
84
+ showSnackbar({
85
+ title: t('errorCreatingQueueService', 'Error creating queue service'),
86
+ kind: 'error',
87
+ isLowContrast: false,
88
+ subtitle: error?.responseBody?.message || error?.message,
89
+ });
90
+ }
91
+ };
92
+
93
+ return (
94
+ <Form onSubmit={handleSubmit(createQueue)} className={styles.form}>
95
+ <Stack gap={5} className={styles.grid}>
96
+ <Column>
97
+ <Layer className={styles.input}>
98
+ <Controller
99
+ name="queueName"
100
+ control={control}
101
+ render={({ field }) => (
102
+ <TextInput
103
+ {...field}
104
+ id="queueName"
105
+ invalidText={errors.queueName?.message}
106
+ invalid={!!errors.queueName}
107
+ labelText={t('queueName', 'Queue name')}
108
+ />
109
+ )}
110
+ />
111
+ </Layer>
112
+ </Column>
113
+ <Column>
114
+ <Layer className={styles.input}>
115
+ <Controller
116
+ name="queueServiceType"
117
+ control={control}
118
+ render={({ field }) => (
119
+ <Select
120
+ {...field}
121
+ labelText={t('selectServiceType', 'Select a service type')}
122
+ id="queueServiceType"
123
+ invalid={!!errors?.queueServiceType}
124
+ invalidText={errors?.queueServiceType?.message}>
125
+ <SelectItem text={t('selectServiceType', 'Select a service type')} value="" />
126
+ {queueConcepts?.length > 0 &&
127
+ queueConcepts.map((concept) => (
128
+ <SelectItem key={concept.uuid} text={concept.display} value={concept.uuid}>
129
+ {concept.display}
130
+ </SelectItem>
131
+ ))}
132
+ </Select>
133
+ )}
134
+ />
135
+ </Layer>
136
+ </Column>
137
+ <Column>
138
+ <Layer className={styles.input}>
139
+ <Controller
140
+ name="userLocation"
141
+ control={control}
142
+ render={({ field }) => (
143
+ <Select
144
+ {...field}
145
+ id="location"
146
+ invalid={!!errors?.userLocation}
147
+ invalidText={errors?.userLocation?.message}
148
+ labelText={t('selectALocation', 'Select a location')}>
149
+ <SelectItem text={t('selectALocation', 'Select a location')} value="" />
150
+ {queueLocations?.length > 0 &&
151
+ queueLocations.map((location) => (
152
+ <SelectItem key={location.id} text={location.name} value={location.id}>
153
+ {location.name}
154
+ </SelectItem>
155
+ ))}
156
+ </Select>
157
+ )}
158
+ />
159
+ </Layer>
160
+ </Column>
161
+ </Stack>
162
+ <ButtonSet className={styles.buttonSet}>
163
+ <Button className={styles.button} kind="secondary" onClick={closeWorkspace}>
164
+ {t('cancel', 'Cancel')}
165
+ </Button>
166
+ <Button className={styles.button} disabled={isSubmitting} kind="primary" type="submit">
167
+ {isSubmitting ? (
168
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
169
+ ) : (
170
+ <span>{t('save', 'Save')}</span>
171
+ )}
172
+ </Button>
173
+ </ButtonSet>
174
+ </Form>
175
+ );
176
+ };
177
+
178
+ export default QueueServiceForm;
@@ -0,0 +1,55 @@
1
+ import { fhirBaseUrl, getLocale, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { useConcept } from '../../../hooks/useConcept';
3
+ import { useSystemSetting } from '../../../hooks/useSystemSetting';
4
+ import useSWRImmutable from 'swr/immutable';
5
+ import { useMemo } from 'react';
6
+ import { type FHIRResponse } from '../../queue-types';
7
+
8
+ export function useServiceConcepts() {
9
+ const { systemSetting: serviceConceptSetting } = useSystemSetting('queue.serviceConceptSetName');
10
+ const { concept: serviceConceptSet, error, isLoading } = useConcept(serviceConceptSetting?.value);
11
+ return {
12
+ queueConcepts: serviceConceptSet?.setMembers?.sort((c1, c2) => c1.display.localeCompare(c2.display)) || [],
13
+ error,
14
+ isLoading,
15
+ };
16
+ }
17
+
18
+ export function saveQueue(
19
+ queueName: string,
20
+ queueServiceType: string,
21
+ queueDescription?: string,
22
+ queueLocation?: string,
23
+ ) {
24
+ const abortController = new AbortController();
25
+
26
+ return openmrsFetch(`${restBaseUrl}/queue`, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ },
31
+ signal: abortController.signal,
32
+ body: {
33
+ name: queueName,
34
+ description: queueDescription,
35
+ service: { uuid: queueServiceType },
36
+ location: {
37
+ uuid: queueLocation,
38
+ },
39
+ },
40
+ });
41
+ }
42
+
43
+ export function useQueueLocations() {
44
+ const apiUrl = `${fhirBaseUrl}/Location?_summary=data&_tag=queue location`;
45
+ const { data, error, isLoading } = useSWRImmutable<{ data: FHIRResponse }>(apiUrl, openmrsFetch);
46
+
47
+ const queueLocations = useMemo(
48
+ () =>
49
+ data?.data?.entry
50
+ ?.map((response) => response.resource)
51
+ .sort((a, b) => a.name.localeCompare(b.name, getLocale())) ?? [],
52
+ [data?.data?.entry],
53
+ );
54
+ return { queueLocations, isLoading, error };
55
+ }
@@ -0,0 +1,33 @@
1
+ import { type OpenmrsResource, type Location } from '@openmrs/esm-framework';
2
+
3
+ export interface QueueRoom {
4
+ uuid: string;
5
+ display: string;
6
+ name: string;
7
+ description: string;
8
+ }
9
+
10
+ export interface Queue {
11
+ uuid: string;
12
+ display: string;
13
+ name: string;
14
+ description: string;
15
+ location: Location;
16
+ service: Concept;
17
+ allowedPriorities: Array<Concept>;
18
+ allowedStatuses: Array<Concept>;
19
+ }
20
+
21
+ // TODO: The following types match the types from backend.
22
+ // They should be common enough to move to esm-core
23
+
24
+ export interface Concept extends OpenmrsResource {
25
+ setMembers?: Array<Concept>;
26
+ }
27
+
28
+ export interface FHIRResponse {
29
+ entry: Array<{ resource: fhir.Location }>;
30
+ total: number;
31
+ type: string;
32
+ resourceType: string;
33
+ }