@openmrs/esm-billing-app 1.0.2-pre.90 → 1.0.2-pre.905

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 (214) hide show
  1. package/.eslintrc +16 -2
  2. package/README.md +54 -9
  3. package/__mocks__/bills.mock.ts +12 -0
  4. package/__mocks__/react-i18next.js +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1146.js +1 -2
  7. package/dist/1146.js.map +1 -1
  8. package/dist/1197.js +1 -1
  9. package/dist/1537.js +1 -0
  10. package/dist/1537.js.map +1 -0
  11. package/dist/1856.js +1 -0
  12. package/dist/1856.js.map +1 -0
  13. package/dist/2146.js +1 -1
  14. package/dist/2524.js +1 -0
  15. package/dist/2524.js.map +1 -0
  16. package/dist/2690.js +1 -1
  17. package/dist/3099.js +1 -1
  18. package/dist/3584.js +1 -1
  19. package/dist/3717.js +2 -0
  20. package/dist/3717.js.map +1 -0
  21. package/dist/4055.js +1 -1
  22. package/dist/4132.js +1 -1
  23. package/dist/4300.js +1 -1
  24. package/dist/4335.js +1 -1
  25. package/dist/4618.js +1 -1
  26. package/dist/4652.js +1 -1
  27. package/dist/4692.js +1 -0
  28. package/dist/4692.js.map +1 -0
  29. package/dist/4724.js +1 -0
  30. package/dist/4724.js.map +1 -0
  31. package/dist/4739.js +1 -1
  32. package/dist/4739.js.map +1 -1
  33. package/dist/4944.js +1 -1
  34. package/dist/5173.js +1 -1
  35. package/dist/5241.js +1 -1
  36. package/dist/5442.js +1 -1
  37. package/dist/5661.js +1 -1
  38. package/dist/6022.js +1 -1
  39. package/dist/6468.js +1 -1
  40. package/dist/6540.js +1 -1
  41. package/dist/6540.js.map +1 -1
  42. package/dist/6679.js +1 -1
  43. package/dist/6840.js +1 -1
  44. package/dist/6859.js +1 -1
  45. package/dist/7097.js +1 -1
  46. package/dist/7159.js +1 -1
  47. package/dist/723.js +1 -1
  48. package/dist/7255.js +1 -1
  49. package/dist/7255.js.map +1 -1
  50. package/dist/7617.js +1 -1
  51. package/dist/795.js +1 -1
  52. package/dist/8163.js +1 -1
  53. package/dist/8349.js +1 -1
  54. package/dist/8618.js +1 -1
  55. package/dist/8708.js +2 -0
  56. package/dist/{6557.js.LICENSE.txt → 8708.js.LICENSE.txt} +22 -0
  57. package/dist/8708.js.map +1 -0
  58. package/dist/890.js +1 -1
  59. package/dist/9214.js +1 -1
  60. package/dist/9538.js +1 -1
  61. package/dist/9569.js +1 -1
  62. package/dist/961.js +1 -1
  63. package/dist/961.js.map +1 -1
  64. package/dist/986.js +1 -1
  65. package/dist/9879.js +1 -1
  66. package/dist/9895.js +1 -1
  67. package/dist/9900.js +1 -1
  68. package/dist/9913.js +1 -1
  69. package/dist/main.js +1 -1
  70. package/dist/main.js.map +1 -1
  71. package/dist/openmrs-esm-billing-app.js +1 -1
  72. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +282 -296
  73. package/dist/openmrs-esm-billing-app.js.map +1 -1
  74. package/dist/routes.json +1 -1
  75. package/e2e/README.md +19 -18
  76. package/e2e/core/test.ts +1 -1
  77. package/e2e/fixtures/api.ts +1 -1
  78. package/e2e/specs/sample-test.spec.ts +0 -1
  79. package/e2e/support/github/Dockerfile +1 -1
  80. package/package.json +18 -15
  81. package/src/bill-history/bill-history.component.tsx +20 -28
  82. package/src/bill-history/bill-history.scss +4 -94
  83. package/src/bill-history/bill-history.test.tsx +37 -78
  84. package/src/bill-item-actions/bill-item-actions.scss +21 -5
  85. package/src/bill-item-actions/edit-bill-item.modal.tsx +225 -0
  86. package/src/bill-item-actions/edit-bill-item.test.tsx +214 -40
  87. package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
  88. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +28 -32
  89. package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
  90. package/src/billable-services/bill-waiver/utils.ts +13 -3
  91. package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +32 -64
  92. package/src/billable-services/billable-service-form/billable-service-form.test.tsx +893 -0
  93. package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +504 -0
  94. package/src/billable-services/billable-service.resource.ts +42 -26
  95. package/src/billable-services/billable-services-home.component.tsx +13 -42
  96. package/src/billable-services/billable-services-left-panel-link.component.tsx +48 -0
  97. package/src/billable-services/billable-services-left-panel-menu.component.tsx +46 -0
  98. package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
  99. package/src/billable-services/billable-services.component.tsx +156 -152
  100. package/src/billable-services/billable-services.scss +29 -0
  101. package/src/billable-services/billable-services.test.tsx +6 -49
  102. package/src/billable-services/cash-point/add-cash-point.modal.tsx +170 -0
  103. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
  104. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  105. package/src/billable-services/dashboard/dashboard.component.tsx +0 -2
  106. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  107. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +74 -0
  108. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  109. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  110. package/src/billable-services-admin-card-link.component.test.tsx +2 -2
  111. package/src/billable-services-admin-card-link.component.tsx +1 -1
  112. package/src/billing-dashboard/billing-dashboard.scss +1 -1
  113. package/src/billing-form/billing-checkin-form.component.tsx +21 -17
  114. package/src/billing-form/billing-checkin-form.test.tsx +99 -26
  115. package/src/billing-form/billing-form.component.tsx +222 -292
  116. package/src/billing-form/billing-form.scss +143 -0
  117. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
  118. package/src/billing.resource.ts +69 -74
  119. package/src/bills-table/bills-table.component.tsx +3 -3
  120. package/src/bills-table/bills-table.test.tsx +98 -54
  121. package/src/config-schema.ts +52 -24
  122. package/src/dashboard.meta.ts +4 -2
  123. package/src/helpers/functions.ts +5 -4
  124. package/src/index.ts +71 -9
  125. package/src/invoice/invoice-table.component.tsx +36 -70
  126. package/src/invoice/invoice-table.scss +8 -5
  127. package/src/invoice/invoice-table.test.tsx +273 -62
  128. package/src/invoice/invoice.component.tsx +39 -32
  129. package/src/invoice/invoice.scss +11 -4
  130. package/src/invoice/invoice.test.tsx +324 -120
  131. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  132. package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
  133. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  134. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  135. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  136. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  137. package/src/invoice/payments/payments.component.tsx +55 -67
  138. package/src/invoice/payments/payments.scss +4 -3
  139. package/src/invoice/payments/payments.test.tsx +282 -0
  140. package/src/invoice/payments/utils.ts +15 -27
  141. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  142. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  143. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  144. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  145. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  146. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  147. package/src/invoice/printable-invoice/printable-invoice.component.tsx +20 -34
  148. package/src/left-panel-link.test.tsx +1 -4
  149. package/src/metrics-cards/metrics-cards.component.tsx +16 -6
  150. package/src/metrics-cards/metrics-cards.scss +4 -0
  151. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  152. package/src/modal/require-payment-modal.test.tsx +27 -22
  153. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
  154. package/src/routes.json +44 -20
  155. package/src/types/index.ts +81 -23
  156. package/translations/am.json +132 -75
  157. package/translations/ar.json +133 -76
  158. package/translations/ar_SY.json +133 -76
  159. package/translations/bn.json +135 -78
  160. package/translations/de.json +133 -76
  161. package/translations/en.json +133 -78
  162. package/translations/en_US.json +133 -76
  163. package/translations/es.json +132 -75
  164. package/translations/es_MX.json +133 -76
  165. package/translations/fr.json +138 -81
  166. package/translations/he.json +132 -75
  167. package/translations/hi.json +133 -76
  168. package/translations/hi_IN.json +133 -76
  169. package/translations/id.json +133 -76
  170. package/translations/it.json +159 -102
  171. package/translations/ka.json +133 -76
  172. package/translations/km.json +132 -75
  173. package/translations/ku.json +133 -76
  174. package/translations/ky.json +133 -76
  175. package/translations/lg.json +133 -76
  176. package/translations/ne.json +133 -76
  177. package/translations/pl.json +133 -76
  178. package/translations/pt.json +133 -76
  179. package/translations/pt_BR.json +133 -76
  180. package/translations/qu.json +133 -76
  181. package/translations/ro_RO.json +222 -165
  182. package/translations/ru_RU.json +133 -76
  183. package/translations/si.json +133 -76
  184. package/translations/sw.json +133 -76
  185. package/translations/sw_KE.json +133 -76
  186. package/translations/tr.json +133 -76
  187. package/translations/tr_TR.json +133 -76
  188. package/translations/uk.json +133 -76
  189. package/translations/uz.json +133 -76
  190. package/translations/uz@Latn.json +133 -76
  191. package/translations/uz_UZ.json +133 -76
  192. package/translations/vi.json +133 -76
  193. package/translations/zh.json +133 -76
  194. package/translations/zh_CN.json +164 -107
  195. package/dist/1146.js.LICENSE.txt +0 -21
  196. package/dist/2352.js +0 -1
  197. package/dist/2352.js.map +0 -1
  198. package/dist/246.js +0 -1
  199. package/dist/246.js.map +0 -1
  200. package/dist/4689.js +0 -2
  201. package/dist/4689.js.map +0 -1
  202. package/dist/6557.js +0 -2
  203. package/dist/6557.js.map +0 -1
  204. package/dist/8638.js +0 -1
  205. package/dist/8638.js.map +0 -1
  206. package/dist/9968.js +0 -1
  207. package/dist/9968.js.map +0 -1
  208. package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
  209. package/src/billable-services/create-edit/add-billable-service.component.tsx +0 -401
  210. package/src/billable-services/create-edit/add-billable-service.test.tsx +0 -154
  211. package/src/billable-services/dashboard/service-metrics.component.tsx +0 -41
  212. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  213. package/src/invoice/payments/payments.component.test.tsx +0 -121
  214. /package/dist/{4689.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
@@ -0,0 +1,504 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Button,
4
+ ButtonSet,
5
+ ComboBox,
6
+ Dropdown,
7
+ Form,
8
+ FormGroup,
9
+ FormLabel,
10
+ InlineLoading,
11
+ Layer,
12
+ NumberInput,
13
+ Search,
14
+ Stack,
15
+ TextInput,
16
+ Tile,
17
+ } from '@carbon/react';
18
+ import { type TFunction } from 'i18next';
19
+ import { Controller, useFieldArray, useForm, useWatch } from 'react-hook-form';
20
+ import { useTranslation } from 'react-i18next';
21
+ import { Add, TrashCan } from '@carbon/react/icons';
22
+ import { z } from 'zod';
23
+ import { zodResolver } from '@hookform/resolvers/zod';
24
+ import {
25
+ getCoreTranslation,
26
+ ResponsiveWrapper,
27
+ showSnackbar,
28
+ useDebounce,
29
+ useLayoutType,
30
+ } from '@openmrs/esm-framework';
31
+ import type { BillableService, ServicePrice } from '../../types';
32
+ import {
33
+ createBillableService,
34
+ updateBillableService,
35
+ useConceptsSearch,
36
+ usePaymentModes,
37
+ useServiceTypes,
38
+ } from '../billable-service.resource';
39
+ import styles from './billable-service-form.scss';
40
+
41
+ interface BillableServiceFormWorkspaceProps {
42
+ serviceToEdit?: BillableService;
43
+ closeWorkspace: () => void;
44
+ closeWorkspaceWithSavedChanges?: () => void;
45
+ promptBeforeClosing?: (testFcn: () => boolean) => void;
46
+ onWorkspaceClose?: () => void;
47
+ }
48
+
49
+ interface BillableServiceFormData {
50
+ name: string;
51
+ payment: PaymentModeForm[];
52
+ serviceType: ServiceType | null;
53
+ concept?: { uuid: string; display: string } | null;
54
+ shortName?: string;
55
+ }
56
+
57
+ interface PaymentModeForm {
58
+ paymentMode: string;
59
+ price: string | number | undefined;
60
+ }
61
+
62
+ interface ServiceType {
63
+ uuid: string;
64
+ display: string;
65
+ }
66
+
67
+ const DEFAULT_PAYMENT_OPTION: PaymentModeForm = { paymentMode: '', price: '' };
68
+ const MAX_NAME_LENGTH = 255;
69
+
70
+ /**
71
+ * Transforms a BillableService into form data structure
72
+ * Centralizes the mapping logic to avoid duplication between defaultValues and reset()
73
+ * Exported for testing
74
+ */
75
+ export const transformServiceToFormData = (service?: BillableService): BillableServiceFormData => {
76
+ if (!service) {
77
+ return {
78
+ name: '',
79
+ shortName: '',
80
+ serviceType: null,
81
+ concept: null,
82
+ payment: [DEFAULT_PAYMENT_OPTION],
83
+ };
84
+ }
85
+
86
+ return {
87
+ name: service.name || '',
88
+ shortName: service.shortName || '',
89
+ serviceType: service.serviceType || null,
90
+ concept: service.concept ? { uuid: service.concept.uuid, display: service.concept.display } : null,
91
+ payment: service.servicePrices?.map((servicePrice: ServicePrice) => ({
92
+ paymentMode: servicePrice.paymentMode?.uuid || '',
93
+ price: servicePrice.price ?? '',
94
+ })) || [DEFAULT_PAYMENT_OPTION],
95
+ };
96
+ };
97
+
98
+ /**
99
+ * Normalizes price value from form (string | number | undefined) to number
100
+ * Handles Carbon NumberInput which can return either type
101
+ * Exported for testing
102
+ */
103
+ export const normalizePrice = (price: string | number | undefined): number => {
104
+ if (typeof price === 'number') {
105
+ return price;
106
+ }
107
+ return parseFloat(String(price));
108
+ };
109
+
110
+ const createBillableServiceSchema = (t: TFunction) => {
111
+ const servicePriceSchema = z.object({
112
+ paymentMode: z
113
+ .string({
114
+ required_error: t('paymentModeRequired', 'Payment mode is required'),
115
+ })
116
+ .trim()
117
+ .min(1, t('paymentModeRequired', 'Payment mode is required')),
118
+ price: z.union([z.number(), z.string(), z.undefined()]).superRefine((val, ctx) => {
119
+ if (val === undefined || val === null || val === '') {
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ message: t('priceIsRequired', 'Price is required'),
123
+ });
124
+ return;
125
+ }
126
+
127
+ const numValue = typeof val === 'number' ? val : parseFloat(val);
128
+ if (isNaN(numValue) || numValue <= 0) {
129
+ ctx.addIssue({
130
+ code: z.ZodIssueCode.custom,
131
+ message: t('priceMustBePositive', 'Price must be greater than 0'),
132
+ });
133
+ }
134
+ }),
135
+ });
136
+
137
+ return z.object({
138
+ name: z
139
+ .string({
140
+ required_error: t('serviceNameRequired', 'Service name is required'),
141
+ })
142
+ .trim()
143
+ .min(1, t('serviceNameRequired', 'Service name is required'))
144
+ .max(
145
+ MAX_NAME_LENGTH,
146
+ t('serviceNameExceedsLimit', 'Service name cannot exceed {{MAX_NAME_LENGTH}} characters', {
147
+ MAX_NAME_LENGTH,
148
+ }),
149
+ ),
150
+ shortName: z
151
+ .string()
152
+ .max(
153
+ MAX_NAME_LENGTH,
154
+ t('shortNameExceedsLimit', 'Short name cannot exceed {{MAX_NAME_LENGTH}} characters', { MAX_NAME_LENGTH }),
155
+ )
156
+ .optional(),
157
+ serviceType: z
158
+ .object({
159
+ uuid: z.string(),
160
+ display: z.string(),
161
+ })
162
+ .nullable()
163
+ .refine((val) => val !== null, t('serviceTypeRequired', 'Service type is required')),
164
+ concept: z
165
+ .object({
166
+ uuid: z.string(),
167
+ display: z.string(),
168
+ })
169
+ .nullable()
170
+ .optional(),
171
+ payment: z.array(servicePriceSchema).min(1, t('paymentOptionRequired', 'At least one payment option is required')),
172
+ });
173
+ };
174
+
175
+ const BillableServiceFormWorkspace: React.FC<BillableServiceFormWorkspaceProps> = ({
176
+ serviceToEdit,
177
+ closeWorkspace,
178
+ closeWorkspaceWithSavedChanges,
179
+ onWorkspaceClose,
180
+ }) => {
181
+ const { t } = useTranslation();
182
+ const layout = useLayoutType();
183
+ const isTablet = layout === 'tablet';
184
+ const { paymentModes, isLoadingPaymentModes } = usePaymentModes();
185
+ const { serviceTypes, isLoadingServiceTypes } = useServiceTypes();
186
+
187
+ const billableServiceSchema = useMemo(() => createBillableServiceSchema(t), [t]);
188
+
189
+ const {
190
+ control,
191
+ handleSubmit,
192
+ formState: { errors },
193
+ setValue,
194
+ reset,
195
+ } = useForm<BillableServiceFormData>({
196
+ mode: 'all',
197
+ defaultValues: transformServiceToFormData(serviceToEdit),
198
+ resolver: zodResolver(billableServiceSchema),
199
+ });
200
+ const { fields, remove, append } = useFieldArray({ name: 'payment', control });
201
+
202
+ const handleAppendPaymentMode = () => append(DEFAULT_PAYMENT_OPTION);
203
+ const handleRemovePaymentMode = (index: number) => remove(index);
204
+
205
+ const searchInputRef = useRef(null);
206
+ const [isSubmitting, setIsSubmitting] = useState(false);
207
+
208
+ const selectedConcept = useWatch({ control, name: 'concept' });
209
+ const [searchTerm, setSearchTerm] = useState('');
210
+ const debouncedSearchTerm = useDebounce(searchTerm.trim());
211
+ const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm);
212
+
213
+ // Re-initialize form when editing and dependencies load
214
+ // Needed because serviceTypes/paymentModes may not be available during initial render
215
+ useEffect(() => {
216
+ if (serviceToEdit && !isLoadingPaymentModes && !isLoadingServiceTypes) {
217
+ reset(transformServiceToFormData(serviceToEdit));
218
+ }
219
+ }, [serviceToEdit, isLoadingPaymentModes, isLoadingServiceTypes, reset]);
220
+
221
+ const onSubmit = async (data: BillableServiceFormData) => {
222
+ setIsSubmitting(true);
223
+
224
+ const payload = {
225
+ name: data.name,
226
+ shortName: data.shortName || '',
227
+ serviceType: data.serviceType!.uuid,
228
+ servicePrices: data.payment.map((payment) => {
229
+ const mode = paymentModes.find((m) => m.uuid === payment.paymentMode);
230
+ return {
231
+ paymentMode: payment.paymentMode,
232
+ name: mode?.name || 'Unknown',
233
+ price: normalizePrice(payment.price),
234
+ };
235
+ }),
236
+ serviceStatus: 'ENABLED',
237
+ concept: data.concept?.uuid,
238
+ };
239
+
240
+ try {
241
+ if (serviceToEdit) {
242
+ await updateBillableService(serviceToEdit.uuid, payload);
243
+ } else {
244
+ await createBillableService(payload);
245
+ }
246
+
247
+ showSnackbar({
248
+ title: serviceToEdit
249
+ ? t('billableServiceUpdated', 'Billable service updated')
250
+ : t('billableServiceCreated', 'Billable service created'),
251
+ subtitle: serviceToEdit
252
+ ? t('billableServiceUpdatedSuccessfully', 'Billable service updated successfully')
253
+ : t('billableServiceCreatedSuccessfully', 'Billable service created successfully'),
254
+ kind: 'success',
255
+ });
256
+
257
+ // Call onWorkspaceClose callback to refresh data in parent component
258
+ if (onWorkspaceClose) {
259
+ onWorkspaceClose();
260
+ }
261
+
262
+ // Close the workspace
263
+ if (closeWorkspaceWithSavedChanges) {
264
+ closeWorkspaceWithSavedChanges();
265
+ } else {
266
+ closeWorkspace();
267
+ }
268
+ } catch (error) {
269
+ showSnackbar({
270
+ title: t('billPaymentError', 'Bill payment error'),
271
+ kind: 'error',
272
+ subtitle: error instanceof Error ? error.message : String(error),
273
+ });
274
+ } finally {
275
+ setIsSubmitting(false);
276
+ }
277
+ };
278
+
279
+ const getPaymentErrorMessage = () => {
280
+ const paymentError = errors.payment;
281
+ if (paymentError && typeof paymentError.message === 'string') {
282
+ return paymentError.message;
283
+ }
284
+ return null;
285
+ };
286
+
287
+ if (isLoadingPaymentModes || isLoadingServiceTypes) {
288
+ return (
289
+ <InlineLoading
290
+ status="active"
291
+ iconDescription={t('loadingDescription', 'Loading')}
292
+ description={t('loading', 'Loading data') + '...'}
293
+ />
294
+ );
295
+ }
296
+
297
+ return (
298
+ <Form
299
+ aria-label={t('billableServiceForm', 'Billable service form')}
300
+ className={styles.form}
301
+ id="billable-service-form"
302
+ onSubmit={handleSubmit(onSubmit)}>
303
+ <Stack className={styles.stack} gap={5}>
304
+ <FormGroup className={styles.formGroup}>
305
+ {serviceToEdit ? (
306
+ <FormLabel className={styles.serviceNameLabel}>{serviceToEdit.name}</FormLabel>
307
+ ) : (
308
+ <Controller
309
+ name="name"
310
+ control={control}
311
+ render={({ field }) => (
312
+ <Layer>
313
+ <TextInput
314
+ {...field}
315
+ enableCounter
316
+ id="serviceName"
317
+ invalid={!!errors.name}
318
+ invalidText={errors.name?.message}
319
+ labelText={t('serviceName', 'Service name')}
320
+ maxCount={MAX_NAME_LENGTH}
321
+ placeholder={t('enterServiceName', 'Enter service name')}
322
+ type="text"
323
+ />
324
+ </Layer>
325
+ )}
326
+ />
327
+ )}
328
+ </FormGroup>
329
+ <FormGroup>
330
+ <Controller
331
+ name="shortName"
332
+ control={control}
333
+ render={({ field }) => (
334
+ <Layer>
335
+ <TextInput
336
+ {...field}
337
+ enableCounter
338
+ id="serviceShortName"
339
+ invalid={!!errors.shortName}
340
+ invalidText={errors.shortName?.message}
341
+ labelText={t('shortName', 'Short name')}
342
+ maxCount={MAX_NAME_LENGTH}
343
+ placeholder={t('enterServiceShortName', 'Enter service short name')}
344
+ type="text"
345
+ value={field.value || ''}
346
+ />
347
+ </Layer>
348
+ )}
349
+ />
350
+ </FormGroup>
351
+ <FormGroup>
352
+ <FormLabel className={styles.conceptLabel}>{t('associatedConcept', 'Associated concept')}</FormLabel>
353
+ <ResponsiveWrapper>
354
+ <Search
355
+ id="conceptsSearch"
356
+ labelText={t('associatedConcept', 'Associated concept')}
357
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
358
+ onClear={() => {
359
+ setSearchTerm('');
360
+ setValue('concept', null);
361
+ }}
362
+ placeholder={t('searchConcepts', 'Search associated concept')}
363
+ ref={searchInputRef}
364
+ value={selectedConcept?.display || searchTerm}
365
+ />
366
+ </ResponsiveWrapper>
367
+
368
+ {(() => {
369
+ if (!debouncedSearchTerm || selectedConcept) {
370
+ return null;
371
+ }
372
+ if (isSearching) {
373
+ return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
374
+ }
375
+ if (searchResults && searchResults.length) {
376
+ return (
377
+ <ul className={styles.conceptsList}>
378
+ {searchResults?.map((searchResult) => (
379
+ <li
380
+ className={styles.service}
381
+ key={searchResult.concept.uuid}
382
+ onClick={() => {
383
+ setValue('concept', {
384
+ uuid: searchResult.concept.uuid,
385
+ display: searchResult.display,
386
+ });
387
+ setSearchTerm('');
388
+ }}
389
+ role="menuitem">
390
+ {searchResult.display}
391
+ </li>
392
+ ))}
393
+ </ul>
394
+ );
395
+ }
396
+ return (
397
+ <Layer>
398
+ <Tile className={styles.emptyResults}>
399
+ <span>{t('noResultsFor', 'No results for {{searchTerm}}', { searchTerm: debouncedSearchTerm })}</span>
400
+ </Tile>
401
+ </Layer>
402
+ );
403
+ })()}
404
+ </FormGroup>
405
+ <FormGroup>
406
+ <Controller
407
+ name="serviceType"
408
+ control={control}
409
+ render={({ field }) => (
410
+ <Layer>
411
+ <ComboBox
412
+ id="serviceType"
413
+ items={serviceTypes ?? []}
414
+ titleText={t('serviceType', 'Service type')}
415
+ itemToString={(item: ServiceType) => item?.display || ''}
416
+ selectedItem={field.value}
417
+ onChange={({ selectedItem }: { selectedItem: ServiceType | null }) => {
418
+ field.onChange(selectedItem);
419
+ }}
420
+ placeholder={t('selectServiceType', 'Select service type')}
421
+ invalid={!!errors.serviceType}
422
+ invalidText={errors.serviceType?.message}
423
+ />
424
+ </Layer>
425
+ )}
426
+ />
427
+ </FormGroup>
428
+ <section>
429
+ <div>
430
+ {fields.map((field, index) => (
431
+ <div key={field.id} className={styles.paymentMethodContainer}>
432
+ <Controller
433
+ control={control}
434
+ name={`payment.${index}.paymentMode`}
435
+ render={({ field }) => (
436
+ <Layer>
437
+ <Dropdown
438
+ id={`paymentMode-${index}`}
439
+ invalid={!!errors?.payment?.[index]?.paymentMode}
440
+ invalidText={errors?.payment?.[index]?.paymentMode?.message}
441
+ items={paymentModes ?? []}
442
+ itemToString={(item) => (item ? item.name : '')}
443
+ label={t('selectPaymentMode', 'Select payment mode')}
444
+ onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
445
+ selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
446
+ titleText={t('paymentMode', 'Payment mode')}
447
+ />
448
+ </Layer>
449
+ )}
450
+ />
451
+ <Controller
452
+ control={control}
453
+ name={`payment.${index}.price`}
454
+ render={({ field }) => (
455
+ <Layer>
456
+ <NumberInput
457
+ allowEmpty
458
+ disableWheel
459
+ id={`price-${index}`}
460
+ invalid={!!errors?.payment?.[index]?.price}
461
+ invalidText={errors?.payment?.[index]?.price?.message}
462
+ label={t('sellingPrice', 'Selling price')}
463
+ min={0}
464
+ onChange={(_, { value }) => {
465
+ field.onChange(value === '' || value === undefined ? '' : value);
466
+ }}
467
+ placeholder={t('enterSellingPrice', 'Enter selling price')}
468
+ step={0.01}
469
+ value={field.value === undefined || field.value === null ? '' : field.value}
470
+ />
471
+ </Layer>
472
+ )}
473
+ />
474
+ <div className={styles.removeButtonContainer}>
475
+ <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
476
+ </div>
477
+ </div>
478
+ ))}
479
+ <Button
480
+ className={styles.paymentButtons}
481
+ iconDescription={t('add', 'Add')}
482
+ kind="tertiary"
483
+ onClick={handleAppendPaymentMode}
484
+ renderIcon={(props) => <Add size={24} {...props} />}
485
+ type="button">
486
+ {t('addPaymentOption', 'Add payment option')}
487
+ </Button>
488
+ {getPaymentErrorMessage() && <div className={styles.errorMessage}>{getPaymentErrorMessage()}</div>}
489
+ </div>
490
+ </section>
491
+ </Stack>
492
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
493
+ <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
494
+ {getCoreTranslation('cancel')}
495
+ </Button>
496
+ <Button className={styles.button} kind="primary" disabled={isSubmitting} type="submit">
497
+ {isSubmitting ? <InlineLoading description={t('saving', 'Saving') + '...'} /> : getCoreTranslation('save')}
498
+ </Button>
499
+ </ButtonSet>
500
+ </Form>
501
+ );
502
+ };
503
+
504
+ export default BillableServiceFormWorkspace;
@@ -1,16 +1,21 @@
1
1
  import useSWR from 'swr';
2
2
  import { type OpenmrsResource, openmrsFetch, restBaseUrl, useOpenmrsFetchAll, useConfig } from '@openmrs/esm-framework';
3
- import { type ServiceConcept } from '../types';
4
3
  import { apiBasePath } from '../constants';
5
- import { type BillableService } from '../types/index';
4
+ import type {
5
+ BillableService,
6
+ ConceptSearchResult,
7
+ CreateBillableServicePayload,
8
+ UpdateBillableServicePayload,
9
+ } from '../types';
10
+ import type { BillingConfig } from '../config-schema';
6
11
 
7
12
  type ResponseObject = {
8
13
  results: Array<OpenmrsResource>;
9
14
  };
10
15
 
11
16
  export const useBillableServices = () => {
12
- const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,concept:(uuid,display,name:(name)),serviceType:(display),servicePrices:(uuid,name,price,paymentMode:(uuid,name)))`;
13
- const { data, isLoading, isValidating, error, mutate } = useOpenmrsFetchAll<BillableService[]>(url);
17
+ const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,concept:(uuid,display,name:(name)),serviceType:(display,uuid),servicePrices:(uuid,name,price,paymentMode:(uuid,name)))`;
18
+ const { data, isLoading, isValidating, error, mutate } = useOpenmrsFetchAll<BillableService>(url);
14
19
 
15
20
  return {
16
21
  billableServices: data ?? [],
@@ -22,16 +27,23 @@ export const useBillableServices = () => {
22
27
  };
23
28
 
24
29
  export function useServiceTypes() {
25
- const config = useConfig();
26
- const serviceConceptUuid = config.serviceTypes.billableService;
30
+ const { serviceTypes } = useConfig<BillingConfig>();
31
+ const serviceConceptUuid = serviceTypes.billableService;
27
32
  const url = `${restBaseUrl}/concept/${serviceConceptUuid}?v=custom:(setMembers:(uuid,display))`;
28
33
 
29
- const { data, error, isLoading } = useSWR<{ data }>(url, openmrsFetch);
34
+ const { data, error, isLoading } = useSWR<{ data: { setMembers: Array<{ uuid: string; display: string }> } }>(
35
+ url,
36
+ openmrsFetch,
37
+ );
38
+
39
+ const sortedServiceTypes = data?.data.setMembers
40
+ ? [...data.data.setMembers].sort((a, b) => a.display.localeCompare(b.display))
41
+ : [];
30
42
 
31
43
  return {
32
- serviceTypes: data?.data.setMembers ?? [],
44
+ serviceTypes: sortedServiceTypes,
33
45
  error,
34
- isLoading,
46
+ isLoadingServiceTypes: isLoading,
35
47
  };
36
48
  }
37
49
 
@@ -40,28 +52,21 @@ export const usePaymentModes = () => {
40
52
 
41
53
  const { data, error, isLoading } = useSWR<{ data: ResponseObject }>(url, openmrsFetch);
42
54
 
55
+ const sortedPaymentModes = data?.data.results
56
+ ? [...data.data.results].sort((a, b) => a.name.localeCompare(b.name))
57
+ : [];
58
+
43
59
  return {
44
- paymentModes: data?.data.results ?? [],
60
+ paymentModes: sortedPaymentModes,
45
61
  error,
46
- isLoading,
62
+ isLoadingPaymentModes: isLoading,
47
63
  };
48
64
  };
49
65
 
50
- export const createBillableSerice = (payload: any) => {
51
- const url = `${apiBasePath}api/billable-service`;
52
- return openmrsFetch(url, {
53
- method: 'POST',
54
- body: payload,
55
- headers: {
56
- 'Content-Type': 'application/json',
57
- },
58
- });
59
- };
60
-
61
66
  export function useConceptsSearch(conceptToLookup: string) {
62
67
  const conditionsSearchUrl = `${restBaseUrl}/conceptsearch?q=${conceptToLookup}`;
63
68
 
64
- const { data, error, isLoading } = useSWR<{ data: { results: Array<ServiceConcept> } }, Error>(
69
+ const { data, error, isLoading } = useSWR<{ data: { results: Array<ConceptSearchResult> } }, Error>(
65
70
  conceptToLookup ? conditionsSearchUrl : null,
66
71
  openmrsFetch,
67
72
  );
@@ -73,11 +78,22 @@ export function useConceptsSearch(conceptToLookup: string) {
73
78
  };
74
79
  }
75
80
 
76
- export const updateBillableService = (uuid: string, payload: any) => {
77
- const url = `${apiBasePath}/billableService/${uuid}`;
81
+ export const createBillableService = (payload: CreateBillableServicePayload) => {
82
+ const url = `${apiBasePath}api/billable-service`;
78
83
  return openmrsFetch(url, {
79
84
  method: 'POST',
80
- body: JSON.stringify(payload),
85
+ body: payload,
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ });
90
+ };
91
+
92
+ export const updateBillableService = (uuid: string, payload: UpdateBillableServicePayload) => {
93
+ const url = `${apiBasePath}billableService/${uuid}`;
94
+ return openmrsFetch(url, {
95
+ method: 'POST',
96
+ body: payload,
81
97
  headers: {
82
98
  'Content-Type': 'application/json',
83
99
  },