@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,296 @@
1
+ import React, { useState, useMemo, useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ ButtonSet,
5
+ Button,
6
+ Stack,
7
+ TextInput,
8
+ ComboBox,
9
+ Toggle,
10
+ InlineNotification,
11
+ InlineLoading,
12
+ } from '@carbon/react';
13
+ import { Add } from '@carbon/react/icons';
14
+ import { Controller, useFieldArray, useForm, FormProvider } from 'react-hook-form';
15
+ import { zodResolver } from '@hookform/resolvers/zod';
16
+
17
+ import { type DefaultWorkspaceProps, useLayoutType, useDebounce, ResponsiveWrapper, showSnackbar, restBaseUrl } from '@openmrs/esm-framework';
18
+
19
+ import classNames from 'classnames';
20
+ import styles from './global/billing-charge-items.scss';
21
+ import { billableFormSchema, BillableFormSchema } from './schemas/form-schemas';
22
+ import { handleMutation } from '../../billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes.resource';
23
+ import PriceField from './components/price.component';
24
+ import ConceptSearch from './components/concept-search.component';
25
+ import { createBillableService, useConceptsSearch, useServiceTypes } from './billing-charge-items.resource';
26
+ import { formatBillableServicePayloadForSubmission, mapInputToPayloadSchema } from '../utils';
27
+
28
+ interface AddServiceFormProps extends DefaultWorkspaceProps {
29
+ initialValues?: BillableFormSchema;
30
+ }
31
+
32
+ const AddServiceForm: React.FC<AddServiceFormProps> = ({
33
+ closeWorkspace,
34
+ promptBeforeClosing,
35
+ closeWorkspaceWithSavedChanges,
36
+ initialValues,
37
+ }) => {
38
+ const { t } = useTranslation();
39
+ const isTablet = useLayoutType() === 'tablet';
40
+ const [conceptToLookup, setConceptToLookup] = useState('');
41
+ const debouncedConceptToLookup = useDebounce(conceptToLookup, 500);
42
+ const [selectedConcept, setSelectedConcept] = useState<any>(null);
43
+ const inEditMode = !!initialValues;
44
+
45
+ const { isLoading: isLoadingServiceTypes, serviceTypes } = useServiceTypes();
46
+ const { isSearching, searchResults: concepts } = useConceptsSearch(debouncedConceptToLookup);
47
+ const formMethods = useForm<BillableFormSchema>({
48
+ resolver: zodResolver(billableFormSchema),
49
+ defaultValues: initialValues
50
+ ? mapInputToPayloadSchema(initialValues)
51
+ : {
52
+ name: '',
53
+ shortName: '',
54
+ concept: null,
55
+ serviceType: null,
56
+ serviceStatus: 'ENABLED',
57
+ servicePrices: [],
58
+ },
59
+ });
60
+
61
+ const {
62
+ setValue,
63
+ control,
64
+ handleSubmit,
65
+ trigger,
66
+ formState: { errors, isDirty, defaultValues, isSubmitting },
67
+ } = formMethods;
68
+
69
+ useEffect(() => {
70
+ if (initialValues) {
71
+ setConceptToLookup(initialValues.concept?.concept?.display);
72
+ trigger();
73
+ }
74
+ }, [initialValues, trigger]);
75
+
76
+ const {
77
+ fields: servicePriceFields,
78
+ append: appendServicePrice,
79
+ remove: removeServicePrice,
80
+ } = useFieldArray({
81
+ control,
82
+ name: 'servicePrices',
83
+ });
84
+
85
+ const handleSelectConcept = (concept) => {
86
+ setSelectedConcept(concept);
87
+ setValue('concept', concept);
88
+ setConceptToLookup('');
89
+ };
90
+
91
+ useEffect(() => {
92
+ promptBeforeClosing(() => isDirty);
93
+ }, [isDirty, promptBeforeClosing]);
94
+
95
+ const onSubmit = async (data: BillableFormSchema) => {
96
+ const formPayload = formatBillableServicePayloadForSubmission(data, initialValues?.['uuid']);
97
+ try {
98
+ const response = await createBillableService(formPayload, initialValues?.['uuid']);
99
+ if (response.ok) {
100
+ showSnackbar({
101
+ title: inEditMode
102
+ ? t('serviceUpdatedSuccessfully', 'Service updated successfully')
103
+ : t('serviceCreated', 'Service created successfully'),
104
+ kind: 'success',
105
+ subtitle: inEditMode
106
+ ? t('serviceUpdatedSuccessfully', 'Service updated successfully')
107
+ : t('serviceCreatedSuccessfully', 'Service created successfully'),
108
+ isLowContrast: true,
109
+ timeoutInMs: 5000,
110
+ });
111
+ handleMutation(`${restBaseUrl}/cashier/billableService?v`);
112
+
113
+ closeWorkspaceWithSavedChanges();
114
+ }
115
+ } catch (e) {
116
+ const formSchemaError = JSON.stringify(e, null, 2);
117
+ const errorMessage = e?.servicePrices?.root?.message || 'Unknown error occurred';
118
+ showSnackbar({
119
+ title: t('serviceCreationFailed', 'Service creation failed'),
120
+ subtitle: t('serviceCreationFailedSubtitle', 'The service creation failed: {{errorMessage}}', {
121
+ errorMessage,
122
+ }),
123
+ kind: 'error',
124
+ isLowContrast: true,
125
+ timeoutInMs: 5000,
126
+ });
127
+ }
128
+ };
129
+
130
+ const renderServicePriceFields = useMemo(
131
+ () =>
132
+ servicePriceFields.map((field, index) => (
133
+ <PriceField
134
+ key={field.id}
135
+ field={field}
136
+ index={index}
137
+ control={control}
138
+ removeServicePrice={removeServicePrice}
139
+ errors={errors}
140
+ />
141
+ )),
142
+ [servicePriceFields, control, removeServicePrice, errors],
143
+ );
144
+
145
+ const handleError = (err) => {
146
+ console.error(JSON.stringify(err, null, 2));
147
+ const errorMessage = Object.entries(err as Record<string, { message: string }>)
148
+ .map(([field, error]) => `${field}: ${error.message}`)
149
+ .join('; ');
150
+
151
+ showSnackbar({
152
+ title: t('serviceCreationFailed', 'Service creation failed'),
153
+ subtitle: t('serviceCreationFailedSubtitle', 'The service creation failed: {{errorMessage}}', {
154
+ errorMessage,
155
+ }),
156
+ kind: 'error',
157
+ isLowContrast: true,
158
+ timeoutInMs: 5000,
159
+ });
160
+ };
161
+
162
+ return (
163
+ <FormProvider {...formMethods}>
164
+ <form onSubmit={handleSubmit(onSubmit, handleError)} className={styles.form}>
165
+ <div className={styles.formContainer}>
166
+ <Stack className={styles.formStackControl} gap={7}>
167
+ {errors.concept && (
168
+ <InlineNotification
169
+ kind="error"
170
+ title={t('conceptMissing', 'Concept missing')}
171
+ subtitle={t('conceptMissingSubtitle', 'Please select a stock item')}
172
+ />
173
+ )}
174
+ <ResponsiveWrapper>
175
+ <Controller
176
+ name="name"
177
+ control={control}
178
+ render={({ field }) => (
179
+ <TextInput
180
+ id="serviceName"
181
+ {...field}
182
+ type="text"
183
+ labelText={t('serviceName', 'Service name')}
184
+ invalid={!!errors.name}
185
+ invalidText={errors?.name?.message}
186
+ />
187
+ )}
188
+ />
189
+ </ResponsiveWrapper>
190
+ <ResponsiveWrapper>
191
+ <Controller
192
+ name="shortName"
193
+ control={control}
194
+ render={({ field }) => (
195
+ <TextInput
196
+ id="serviceShortName"
197
+ {...field}
198
+ type="text"
199
+ labelText={t('serviceShortName', 'Service short name')}
200
+ invalid={!!errors.shortName}
201
+ invalidText={errors?.shortName?.message}
202
+ />
203
+ )}
204
+ />
205
+ </ResponsiveWrapper>
206
+
207
+ <ConceptSearch
208
+ selectedConcept={selectedConcept}
209
+ setConceptToLookup={setConceptToLookup}
210
+ conceptToLookup={conceptToLookup}
211
+ defaultValues={defaultValues}
212
+ errors={errors}
213
+ isSearching={isSearching}
214
+ concepts={concepts}
215
+ handleSelectConcept={handleSelectConcept}
216
+ />
217
+
218
+ <ResponsiveWrapper>
219
+ <Controller
220
+ name="serviceType"
221
+ control={control}
222
+ render={({ field }) => {
223
+ return (
224
+ <ComboBox
225
+ id="serviceType"
226
+ onChange={({ selectedItem }) => field.onChange(selectedItem)}
227
+ titleText={t('serviceType', 'Service type')}
228
+ items={serviceTypes ?? []}
229
+ itemToString={(item) => (item ? item.display : '')}
230
+ placeholder={t('selectServiceType', 'Select service type')}
231
+ disabled={isLoadingServiceTypes}
232
+ initialSelectedItem={field.value}
233
+ invalid={!!errors.serviceType}
234
+ invalidText={errors?.serviceType?.message}
235
+ itemToElement={(item) => (
236
+ <div role="option" aria-selected={field.value?.uuid === item?.uuid}>
237
+ {item?.display}
238
+ </div>
239
+ )}
240
+ />
241
+ );
242
+ }}
243
+ />
244
+ </ResponsiveWrapper>
245
+ <ResponsiveWrapper>
246
+ <Controller
247
+ control={control}
248
+ name="serviceStatus"
249
+ render={({ field }) => (
250
+ <Toggle
251
+ labelText={t('status', 'Status')}
252
+ labelA="Off"
253
+ labelB="On"
254
+ defaultToggled={field.value === 'ENABLED'}
255
+ id="serviceStatus"
256
+ onToggle={(value) => (value ? field.onChange('ENABLED') : field.onChange('DISABLED'))}
257
+ />
258
+ )}
259
+ />
260
+ </ResponsiveWrapper>
261
+ {renderServicePriceFields}
262
+ <Button size="sm" kind="tertiary" renderIcon={Add} onClick={() => appendServicePrice({})}>
263
+ {t('addPaymentMethod', 'Add payment method')}
264
+ </Button>
265
+ {!!errors.servicePrices && (
266
+ <InlineNotification
267
+ aria-label="closes notification"
268
+ kind="error"
269
+ lowContrast={true}
270
+ statusIconDescription="notification"
271
+ title={t('paymentMethodRequired', 'Payment method required')}
272
+ subtitle={t('atLeastOnePriceRequired', 'At least one price is required')}
273
+ />
274
+ )}
275
+ </Stack>
276
+ </div>
277
+ <ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
278
+ <Button style={{ maxWidth: '50%' }} kind="secondary" onClick={() => closeWorkspace()}>
279
+ {t('cancel', 'Cancel')}
280
+ </Button>
281
+ <Button disabled={isSubmitting || !isDirty} style={{ maxWidth: '50%' }} kind="primary" type="submit">
282
+ {isSubmitting ? (
283
+ <span style={{ display: 'flex', justifyItems: 'center' }}>
284
+ {t('submitting', 'Submitting...')} <InlineLoading status="active" iconDescription="Loading" />
285
+ </span>
286
+ ) : (
287
+ t('saveAndClose', 'Save & close')
288
+ )}
289
+ </Button>
290
+ </ButtonSet>
291
+ </form>
292
+ </FormProvider>
293
+ );
294
+ };
295
+
296
+ export default AddServiceForm;
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import styles from '../global/billing-charge-items.scss';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { FormGroup, Search, InlineLoading } from '@carbon/react';
5
+ import { ResponsiveWrapper } from '@openmrs/esm-framework';
6
+
7
+ type ConceptSearchProps = {
8
+ setConceptToLookup: (value: string) => void;
9
+ conceptToLookup: string;
10
+ selectedConcept: any;
11
+ handleSelectConcept: (concept: any) => void;
12
+ errors: any;
13
+ isSearching: boolean;
14
+ concepts: any;
15
+ defaultValues: any;
16
+ };
17
+
18
+ const ConceptSearch: React.FC<ConceptSearchProps> = ({
19
+ selectedConcept,
20
+ setConceptToLookup,
21
+ conceptToLookup,
22
+ defaultValues,
23
+ errors,
24
+ isSearching,
25
+ concepts,
26
+ handleSelectConcept,
27
+ }) => {
28
+ const { t } = useTranslation();
29
+ return (
30
+ <FormGroup className={styles.formGroupWithConcept} legendText={t('serviceConcept', 'Service Concept')}>
31
+ <ResponsiveWrapper>
32
+ <Search
33
+ placeholder={t('searchForConcept', 'Search for concept')}
34
+ labelText={t('search', 'Search')}
35
+ closeButtonLabelText={t('clear', 'Clear')}
36
+ onChange={(e) => setConceptToLookup(e.target.value)}
37
+ value={
38
+ selectedConcept
39
+ ? selectedConcept?.concept?.display
40
+ : (conceptToLookup ?? defaultValues?.concept?.concept?.display)
41
+ }
42
+ />
43
+ </ResponsiveWrapper>
44
+ {isSearching && (
45
+ <div className={styles.searchResults}>
46
+ <InlineLoading status="active" iconDescription="Loading" description="Loading data..." />
47
+ </div>
48
+ )}
49
+ {concepts && concepts.length > 0 && !isSearching && (
50
+ <div className={styles.searchResults}>
51
+ {concepts.map((concept) => (
52
+ <div
53
+ role="button"
54
+ tabIndex={0}
55
+ onClick={() => handleSelectConcept(concept)}
56
+ key={concept.concept.uuid}
57
+ className={styles.searchItem}>
58
+ {concept.concept.display}
59
+ </div>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </FormGroup>
64
+ );
65
+ };
66
+
67
+ export default ConceptSearch;
@@ -0,0 +1,81 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Controller, useFormContext, type Control } from 'react-hook-form';
3
+ import { useTranslation } from 'react-i18next';
4
+ import styles from '../global/billing-charge-items.scss';
5
+ import { ComboBox, NumberInput, IconButton } from '@carbon/react';
6
+ import { TrashCan } from '@carbon/react/icons';
7
+ import { ResponsiveWrapper } from '@openmrs/esm-framework';
8
+ import { usePaymentModes } from '../../../billing-payment-modes/billing-payment-modes.resource';
9
+ import { BillableFormSchema } from '../schemas/form-schemas';
10
+
11
+ interface PriceFieldProps {
12
+ field: Record<string, any>;
13
+ index: number;
14
+ control: Control<BillableFormSchema>;
15
+ removeServicePrice: (index: number) => void;
16
+ errors: Record<string, any>;
17
+ }
18
+
19
+ const PriceField: React.FC<PriceFieldProps> = ({ field, index, control, removeServicePrice, errors }) => {
20
+ const { t } = useTranslation();
21
+ const { paymentModes, isLoading } = usePaymentModes();
22
+ const { watch } = useFormContext();
23
+ const servicePrices = watch('servicePrices');
24
+ const availablePaymentModes = useMemo(
25
+ () =>
26
+ paymentModes?.filter(
27
+ (paymentMode) => !servicePrices?.some((servicePrice) => servicePrice.paymentMode?.uuid === paymentMode.uuid),
28
+ ),
29
+ [paymentModes, servicePrices],
30
+ );
31
+
32
+ return (
33
+ <div key={field.id} className={styles.paymentMethods}>
34
+ <ResponsiveWrapper>
35
+ <Controller
36
+ name={`servicePrices.${index}.paymentMode`}
37
+ control={control}
38
+ render={({ field }) => (
39
+ <ComboBox
40
+ id={`servicePrices.${index}.paymentMode`}
41
+ onChange={({ selectedItem }) => field.onChange(selectedItem)}
42
+ titleText={t('paymentMethodDescription', 'Payment method {{methodName}}', {
43
+ methodName: servicePrices[index]?.paymentMode?.name ?? '',
44
+ })}
45
+ items={availablePaymentModes ?? []}
46
+ itemToString={(item) => (item ? item.name : '')}
47
+ placeholder={t('selectPaymentMode', 'Select payment mode')}
48
+ disabled={isLoading}
49
+ initialSelectedItem={field.value}
50
+ invalid={!!errors?.servicePrices?.[index]?.paymentMode}
51
+ invalidText={errors?.servicePrices?.[index]?.paymentMode?.message}
52
+ />
53
+ )}
54
+ />
55
+ </ResponsiveWrapper>
56
+ <ResponsiveWrapper>
57
+ <Controller
58
+ name={`servicePrices.${index}.price`}
59
+ control={control}
60
+ render={({ field }) => (
61
+ <NumberInput
62
+ id={`servicePrices.${index}.price`}
63
+ onChange={(e, { value }) => field.onChange(parseFloat(value.toString()))}
64
+ type="number"
65
+ label={t('price', 'Price')}
66
+ placeholder={t('enterPrice', 'Enter price')}
67
+ defaultValue={field.value}
68
+ invalid={!!errors?.servicePrices?.[index]?.price}
69
+ invalidText={errors?.servicePrices?.[index]?.price?.message}
70
+ />
71
+ )}
72
+ />
73
+ </ResponsiveWrapper>
74
+ <IconButton kind="tertiary" size="md" label={t('delete', 'Delete')} onClick={() => removeServicePrice(index)}>
75
+ <TrashCan />
76
+ </IconButton>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default PriceField;
@@ -0,0 +1,107 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { FormGroup, Search, InlineLoading } from '@carbon/react';
4
+ import { ErrorState, ResponsiveWrapper, useDebounce } from '@openmrs/esm-framework';
5
+ import type { UseFormSetValue } from 'react-hook-form';
6
+ import styles from '../global/billing-charge-commodity.scss';
7
+ import { formatStockItemToPayload } from '../../utils';
8
+ import { BillableFormSchema } from '../schemas/form-schemas';
9
+ import { useCommodityItem } from '../billing-charge-items.resource';
10
+
11
+ interface StockItem {
12
+ uuid: string;
13
+ commonName: string;
14
+ }
15
+
16
+ type StockItemSearchProps = {
17
+ setValue: UseFormSetValue<BillableFormSchema>;
18
+ defaultStockItem?: string;
19
+ };
20
+
21
+ const StockItemSearch: React.FC<StockItemSearchProps> = ({ setValue, defaultStockItem }) => {
22
+ const { t } = useTranslation();
23
+ const [inputValue, setInputValue] = useState<string>(defaultStockItem ?? '');
24
+ const [searchTerm, setSearchTerm] = useState<string>(defaultStockItem ? '' : '');
25
+ const debouncedSearchTerm = useDebounce(searchTerm, 500);
26
+ const [selectedStockItem, setSelectedStockItem] = useState<StockItem | null>(null);
27
+
28
+ const { stockItems = [], isLoading, error } = useCommodityItem(debouncedSearchTerm);
29
+
30
+ useEffect(() => {
31
+ if (defaultStockItem && !selectedStockItem) {
32
+ setSelectedStockItem(stockItems.find((item) => item.uuid === defaultStockItem) ?? null);
33
+ }
34
+ }, [defaultStockItem, selectedStockItem, setValue, stockItems]);
35
+
36
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
37
+ const v = e?.currentTarget?.value ?? '';
38
+ setInputValue(v);
39
+ setSearchTerm(v);
40
+ setSelectedStockItem(null);
41
+ };
42
+
43
+ const handleClear = () => {
44
+ setInputValue('');
45
+ setSearchTerm('');
46
+ setSelectedStockItem(null);
47
+ };
48
+
49
+ const handleStockItemSelect = (item: StockItem) => {
50
+ setSelectedStockItem(item);
51
+ setInputValue(item.commonName);
52
+ setSearchTerm(''); // hide results
53
+ const payload = formatStockItemToPayload(item);
54
+ Object.entries(payload).forEach(([key, value]) => {
55
+ setValue(key as keyof BillableFormSchema, value);
56
+ });
57
+ };
58
+
59
+ if (error) {
60
+ return <ErrorState headerTitle={t('stockItemError', 'Stock Item')} error={error} />;
61
+ }
62
+
63
+ return (
64
+ <FormGroup className={styles.formGroupWithConcept} legendText={t('searchForCommodity', 'Search for commodity')}>
65
+ <ResponsiveWrapper>
66
+ <Search
67
+ placeholder={t('searchForCommodity', 'Search for commodity')}
68
+ labelText={t('search', 'Search')}
69
+ closeButtonLabelText={t('clear', 'Clear')}
70
+ value={inputValue}
71
+ onChange={handleInputChange}
72
+ onClear={handleClear}
73
+ disabled={Boolean(defaultStockItem)}
74
+ />
75
+ </ResponsiveWrapper>
76
+
77
+ {isLoading && (
78
+ <div className={styles.searchResults}>
79
+ <InlineLoading status="active" iconDescription="Loading" description={t('loadingData', 'Loading data...')} />
80
+ </div>
81
+ )}
82
+
83
+ {!isLoading && stockItems.length > 0 && searchTerm && (
84
+ <div className={styles.searchResults} role="listbox">
85
+ {stockItems.map((item) => (
86
+ <div
87
+ key={item.uuid}
88
+ className={styles.searchItem}
89
+ role="option"
90
+ aria-selected={selectedStockItem?.uuid === item.uuid}
91
+ tabIndex={0}
92
+ onClick={() => handleStockItemSelect(item)}
93
+ onKeyPress={(e) => {
94
+ if (e.key === 'Enter' || e.key === ' ') {
95
+ handleStockItemSelect(item);
96
+ }
97
+ }}>
98
+ {item.commonName}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ )}
103
+ </FormGroup>
104
+ );
105
+ };
106
+
107
+ export default StockItemSearch;
@@ -0,0 +1,61 @@
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
+ }
15
+
16
+ .tablet {
17
+ padding: layout.$spacing-06 layout.$spacing-05;
18
+ background-color: colors.$white;
19
+ }
20
+
21
+ .desktop {
22
+ padding: 0;
23
+ }
24
+
25
+ .paymentMethods {
26
+ display: grid;
27
+ grid-template-columns: 0.45fr 0.45fr 0.1fr;
28
+ column-gap: 0.25rem;
29
+ justify-content: space-between;
30
+ align-items: flex-end;
31
+ }
32
+
33
+ .searchResults {
34
+ position: absolute;
35
+ top: 2.625rem;
36
+ z-index: 99;
37
+ height: 16rem;
38
+ padding: 0.5rem;
39
+ overflow-y: scroll;
40
+ background-color: colors.$white;
41
+ border: 1px solid colors.$gray-30;
42
+ width: 100%;
43
+ }
44
+
45
+ .searchItem {
46
+ padding: 0.5rem;
47
+ color: colors.$gray-70;
48
+ cursor: pointer;
49
+ @include type.type-style('label-01');
50
+ border-bottom: 1px solid colors.$gray-30;
51
+ &:hover {
52
+ background-color: colors.$gray-20;
53
+ }
54
+ }
55
+
56
+ .formGroupWithConcept {
57
+ position: relative;
58
+ }
59
+ .formStackControl {
60
+ row-gap: layout.$layout-01;
61
+ }
@@ -0,0 +1,59 @@
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
+ }
15
+
16
+ .tablet {
17
+ padding: layout.$spacing-06 layout.$spacing-05;
18
+ background-color: colors.$white;
19
+ }
20
+
21
+ .desktop {
22
+ padding: 0;
23
+ }
24
+
25
+ .paymentMethods {
26
+ display: flex;
27
+ flex-direction: column;
28
+ row-gap: layout.$spacing-05;
29
+ }
30
+
31
+ .searchResults {
32
+ position: absolute;
33
+ top: 2.625rem;
34
+ z-index: 99;
35
+ height: 16rem;
36
+ padding: 0.5rem;
37
+ overflow-y: scroll;
38
+ background-color: colors.$white;
39
+ border: 1px solid colors.$gray-30;
40
+ width: 100%;
41
+ }
42
+
43
+ .searchItem {
44
+ padding: 0.5rem;
45
+ color: colors.$gray-70;
46
+ cursor: pointer;
47
+ @include type.type-style('label-01');
48
+ border-bottom: 1px solid colors.$gray-30;
49
+ &:hover {
50
+ background-color: colors.$gray-20;
51
+ }
52
+ }
53
+
54
+ .formGroupWithConcept {
55
+ position: relative;
56
+ }
57
+ .formStackControl {
58
+ row-gap: layout.$layout-01;
59
+ }