@openmrs/esm-billing-app 1.0.1-pre.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +7 -0
  11. package/__mocks__/bills.mock.ts +394 -0
  12. package/__mocks__/delivery-summary.mock.ts +89 -0
  13. package/__mocks__/encounter-observation.mock.ts +10651 -0
  14. package/__mocks__/encounter-observations.mock.ts +6189 -0
  15. package/__mocks__/hiv-summary.mock.ts +22 -0
  16. package/__mocks__/patient-summary.mock.ts +32 -0
  17. package/__mocks__/patient.mock.ts +59 -0
  18. package/__mocks__/program-summary.mock.ts +43 -0
  19. package/__mocks__/react-i18next.js +57 -0
  20. package/dist/146.js +1 -0
  21. package/dist/146.js.map +1 -0
  22. package/dist/294.js +2 -0
  23. package/dist/294.js.LICENSE.txt +9 -0
  24. package/dist/294.js.map +1 -0
  25. package/dist/319.js +1 -0
  26. package/dist/384.js +1 -0
  27. package/dist/384.js.map +1 -0
  28. package/dist/421.js +1 -0
  29. package/dist/421.js.map +1 -0
  30. package/dist/533.js +1 -0
  31. package/dist/533.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/591.js +2 -0
  34. package/dist/591.js.LICENSE.txt +9 -0
  35. package/dist/591.js.map +1 -0
  36. package/dist/614.js +2 -0
  37. package/dist/614.js.LICENSE.txt +37 -0
  38. package/dist/614.js.map +1 -0
  39. package/dist/753.js +1 -0
  40. package/dist/753.js.map +1 -0
  41. package/dist/757.js +1 -0
  42. package/dist/770.js +1 -0
  43. package/dist/770.js.map +1 -0
  44. package/dist/783.js +1 -0
  45. package/dist/783.js.map +1 -0
  46. package/dist/788.js +1 -0
  47. package/dist/800.js +2 -0
  48. package/dist/800.js.LICENSE.txt +3 -0
  49. package/dist/800.js.map +1 -0
  50. package/dist/807.js +1 -0
  51. package/dist/833.js +1 -0
  52. package/dist/935.js +2 -0
  53. package/dist/935.js.LICENSE.txt +19 -0
  54. package/dist/935.js.map +1 -0
  55. package/dist/992.js +1 -0
  56. package/dist/992.js.map +1 -0
  57. package/dist/main.js +2 -0
  58. package/dist/main.js.LICENSE.txt +47 -0
  59. package/dist/main.js.map +1 -0
  60. package/dist/openmrs-esm-billing-app.js +1 -0
  61. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +609 -0
  62. package/dist/openmrs-esm-billing-app.js.map +1 -0
  63. package/dist/routes.json +1 -0
  64. package/e2e/README.md +115 -0
  65. package/e2e/core/global-setup.ts +32 -0
  66. package/e2e/core/index.ts +1 -0
  67. package/e2e/core/test.ts +20 -0
  68. package/e2e/fixtures/api.ts +27 -0
  69. package/e2e/fixtures/index.ts +1 -0
  70. package/e2e/pages/home-page.ts +9 -0
  71. package/e2e/pages/index.ts +1 -0
  72. package/e2e/specs/sample-test.spec.ts +11 -0
  73. package/e2e/support/github/Dockerfile +34 -0
  74. package/e2e/support/github/docker-compose.yml +24 -0
  75. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  76. package/example.env +6 -0
  77. package/i18next-parser.config.js +89 -0
  78. package/jest.config.js +34 -0
  79. package/package.json +124 -0
  80. package/playwright.config.ts +32 -0
  81. package/prettier.config.js +8 -0
  82. package/src/bill-history/bill-history.component.tsx +199 -0
  83. package/src/bill-history/bill-history.scss +151 -0
  84. package/src/bill-history/bill-history.test.tsx +122 -0
  85. package/src/billable-services/bill-waiver/bill-selection.component.tsx +76 -0
  86. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +110 -0
  87. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  88. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  89. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  90. package/src/billable-services/bill-waiver/patient-bills.component.tsx +137 -0
  91. package/src/billable-services/bill-waiver/utils.ts +41 -0
  92. package/src/billable-services/billable-service.resource.ts +72 -0
  93. package/src/billable-services/billable-services-home.component.tsx +51 -0
  94. package/src/billable-services/billable-services.component.tsx +255 -0
  95. package/src/billable-services/billable-services.scss +218 -0
  96. package/src/billable-services/billable-services.test.tsx +16 -0
  97. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  98. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  99. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  100. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  101. package/src/billable-services/dashboard/dashboard.scss +27 -0
  102. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  103. package/src/billable-services/dashboard/service-metrics.component.tsx +41 -0
  104. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  105. package/src/billable-services-admin-card-link.component.tsx +25 -0
  106. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  107. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  108. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  109. package/src/billing-form/billing-checkin-form.component.tsx +127 -0
  110. package/src/billing-form/billing-checkin-form.scss +13 -0
  111. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  112. package/src/billing-form/billing-form.component.tsx +347 -0
  113. package/src/billing-form/billing-form.resource.ts +32 -0
  114. package/src/billing-form/billing-form.scss +88 -0
  115. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  116. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  117. package/src/billing-header/billing-header.component.tsx +43 -0
  118. package/src/billing-header/billing-header.scss +83 -0
  119. package/src/billing-header/billing-illustration.component.tsx +30 -0
  120. package/src/billing.resource.ts +148 -0
  121. package/src/bills-table/bills-table.component.tsx +280 -0
  122. package/src/bills-table/bills-table.scss +181 -0
  123. package/src/bills-table/bills-table.test.tsx +154 -0
  124. package/src/config-schema.ts +50 -0
  125. package/src/constants.ts +3 -0
  126. package/src/dashboard.meta.ts +7 -0
  127. package/src/declarations.d.ts +4 -0
  128. package/src/helpers/functions.ts +66 -0
  129. package/src/helpers/index.ts +1 -0
  130. package/src/index.ts +72 -0
  131. package/src/invoice/invoice-table.component.tsx +189 -0
  132. package/src/invoice/invoice-table.scss +91 -0
  133. package/src/invoice/invoice.component.tsx +144 -0
  134. package/src/invoice/invoice.scss +93 -0
  135. package/src/invoice/invoice.test.tsx +242 -0
  136. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  137. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  138. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  139. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  140. package/src/invoice/payments/payment-history/payment-history.component.tsx +69 -0
  141. package/src/invoice/payments/payment.resource.ts +44 -0
  142. package/src/invoice/payments/payments.component.tsx +147 -0
  143. package/src/invoice/payments/payments.scss +46 -0
  144. package/src/invoice/payments/utils.ts +68 -0
  145. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  146. package/src/invoice/printable-invoice/print-receipt.component.tsx +29 -0
  147. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  148. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  149. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  150. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  151. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  152. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  153. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  154. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  155. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  156. package/src/left-panel-link.component.tsx +41 -0
  157. package/src/left-panel-link.test.tsx +38 -0
  158. package/src/metrics-cards/card.component.tsx +14 -0
  159. package/src/metrics-cards/card.scss +20 -0
  160. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  161. package/src/metrics-cards/metrics-cards.scss +12 -0
  162. package/src/metrics-cards/metrics-cards.test.tsx +44 -0
  163. package/src/metrics-cards/metrics.resource.ts +45 -0
  164. package/src/modal/require-payment-modal.component.tsx +85 -0
  165. package/src/modal/require-payment.scss +6 -0
  166. package/src/root.component.tsx +19 -0
  167. package/src/root.scss +30 -0
  168. package/src/routes.json +78 -0
  169. package/src/setup-tests.ts +13 -0
  170. package/src/types/index.ts +181 -0
  171. package/test-helpers.tsx +23 -0
  172. package/translations/am.json +117 -0
  173. package/translations/en.json +117 -0
  174. package/translations/es.json +117 -0
  175. package/translations/fr.json +117 -0
  176. package/translations/he.json +117 -0
  177. package/translations/km.json +117 -0
  178. package/tsconfig.json +16 -0
  179. package/webpack.config.js +1 -0
@@ -0,0 +1,347 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import {
3
+ ButtonSet,
4
+ Button,
5
+ Form,
6
+ InlineLoading,
7
+ RadioButtonGroup,
8
+ RadioButton,
9
+ Search,
10
+ Stack,
11
+ Table,
12
+ TableHead,
13
+ TableBody,
14
+ TableHeader,
15
+ TableRow,
16
+ TableCell,
17
+ } from '@carbon/react';
18
+ import styles from './billing-form.scss';
19
+ import { useTranslation } from 'react-i18next';
20
+ import { restBaseUrl, showSnackbar, showToast, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
21
+ import { useFetchSearchResults, processBillItems } from '../billing.resource';
22
+ import { mutate } from 'swr';
23
+ import { convertToCurrency } from '../helpers';
24
+ import { z } from 'zod';
25
+ import { TrashCan } from '@carbon/react/icons';
26
+ import fuzzy from 'fuzzy';
27
+ import { type BillabeItem } from '../types';
28
+ import { apiBasePath } from '../constants';
29
+
30
+ type BillingFormProps = {
31
+ patientUuid: string;
32
+ closeWorkspace: () => void;
33
+ };
34
+
35
+ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }) => {
36
+ const { t } = useTranslation();
37
+ const { defaultCurrency } = useConfig();
38
+ const isTablet = useLayoutType() === 'tablet';
39
+
40
+ const [grandTotal, setGrandTotal] = useState(0);
41
+ const [searchOptions, setSearchOptions] = useState([]);
42
+ const [billItems, setBillItems] = useState([]);
43
+ const [searchVal, setSearchVal] = useState('');
44
+ const [category, setCategory] = useState('');
45
+ const [saveDisabled, setSaveDisabled] = useState<boolean>(false);
46
+ const [isSubmitting, setIsSubmitting] = useState(false);
47
+ const [addedItems, setAddedItems] = useState([]);
48
+ const [searchTerm, setSearchTerm] = useState('');
49
+ const debouncedSearchTerm = useDebounce(searchTerm);
50
+
51
+ const toggleSearch = (choiceSelected) => {
52
+ (document.getElementById('searchField') as HTMLInputElement).disabled = false;
53
+ setCategory(choiceSelected === 'Stock Item' ? 'Stock Item' : 'Service');
54
+ };
55
+
56
+ const billItemSchema = z.object({
57
+ Qnty: z.number().min(1, t('quantityGreaterThanZero', 'Quantity must be at least one for all items.')), // zod logic
58
+ });
59
+
60
+ const calculateTotal = (event, itemName) => {
61
+ const quantity = parseInt(event.target.value);
62
+ let isValid = true;
63
+
64
+ try {
65
+ billItemSchema.parse({ Qnty: quantity });
66
+ } catch (error) {
67
+ isValid = false;
68
+ const parsedErrorMessage = JSON.parse(error.message);
69
+ showToast({
70
+ title: t('billItems', 'Save Bill'),
71
+ kind: 'error',
72
+ description: parsedErrorMessage[0].message,
73
+ });
74
+ }
75
+
76
+ const updatedItems = billItems.map((item) => {
77
+ if (item.Item.toLowerCase().includes(itemName.toLowerCase())) {
78
+ return { ...item, Qnty: quantity, Total: quantity > 0 ? item.Price * quantity : 0 };
79
+ }
80
+ return item;
81
+ });
82
+
83
+ const anyInvalidQuantity = updatedItems.some((item) => item.Qnty <= 0);
84
+
85
+ setBillItems(updatedItems);
86
+ setSaveDisabled(!isValid || anyInvalidQuantity);
87
+
88
+ const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
89
+ setGrandTotal(updatedGrandTotal);
90
+ };
91
+
92
+ const calculateTotalAfterAddBillItem = (items) => {
93
+ const sum = items.reduce((acc, item) => acc + item.Price * item.Qnty, 0);
94
+ setGrandTotal(sum);
95
+ };
96
+
97
+ const addItemToBill = (event, itemid, itemname, itemcategory, itemPrice) => {
98
+ const existingItemIndex = billItems.findIndex((item) => item.uuid === itemid);
99
+
100
+ let updatedItems = [];
101
+ if (existingItemIndex >= 0) {
102
+ updatedItems = billItems.map((item, index) => {
103
+ if (index === existingItemIndex) {
104
+ const updatedQuantity = item.Qnty + 1;
105
+ return { ...item, Qnty: updatedQuantity, Total: updatedQuantity * item.Price };
106
+ }
107
+ return item;
108
+ });
109
+ } else {
110
+ const newItem = {
111
+ uuid: itemid,
112
+ Item: itemname,
113
+ Qnty: 1,
114
+ Price: itemPrice,
115
+ Total: itemPrice,
116
+ category: itemcategory,
117
+ };
118
+ updatedItems = [...billItems, newItem];
119
+ setAddedItems([...addedItems, newItem]);
120
+ }
121
+
122
+ setBillItems(updatedItems);
123
+ setSearchOptions([]);
124
+ calculateTotalAfterAddBillItem(updatedItems);
125
+ (document.getElementById('searchField') as HTMLInputElement).value = '';
126
+ };
127
+
128
+ const removeItemFromBill = (uuid) => {
129
+ const updatedItems = billItems.filter((item) => item.uuid !== uuid);
130
+ setBillItems(updatedItems);
131
+
132
+ // Update the list of added items
133
+ setAddedItems(addedItems.filter((item) => item.uuid !== uuid));
134
+
135
+ const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
136
+ setGrandTotal(updatedGrandTotal);
137
+ };
138
+
139
+ const { data, error, isLoading, isValidating } = useFetchSearchResults(debouncedSearchTerm, category);
140
+
141
+ const handleSearchTermChange = (e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value);
142
+
143
+ const filterItems = useMemo(() => {
144
+ if (!debouncedSearchTerm || isLoading || error) {
145
+ return [];
146
+ }
147
+
148
+ const res = data as { results: BillabeItem[] };
149
+ const existingItemUuids = new Set(billItems.map((item) => item.uuid));
150
+
151
+ const preprocessedData = res?.results
152
+ ?.map((item) => {
153
+ return {
154
+ uuid: item.uuid || '',
155
+ Item: item.commonName ? item.commonName : item.name,
156
+ Qnty: 1,
157
+ Price: item.commonName ? 10 : item.servicePrices[0]?.price,
158
+ Total: item.commonName ? 10 : item.servicePrices[0]?.price,
159
+ category: item.commonName ? 'StockItem' : 'Service',
160
+ };
161
+ })
162
+ .filter((item) => !existingItemUuids.has(item.uuid));
163
+
164
+ return debouncedSearchTerm
165
+ ? fuzzy
166
+ .filter(debouncedSearchTerm, preprocessedData, {
167
+ extract: (o) => `${o.Item}`,
168
+ })
169
+ .sort((r1, r2) => r1.score - r2.score)
170
+ .map((result) => result.original)
171
+ : searchOptions;
172
+ }, [debouncedSearchTerm, data, billItems]);
173
+
174
+ useEffect(() => {
175
+ setSearchOptions(filterItems);
176
+ }, [filterItems]);
177
+
178
+ const postBillItems = () => {
179
+ setIsSubmitting(true);
180
+ const bill = {
181
+ cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590',
182
+ cashier: 'f9badd80-ab76-11e2-9e96-0800200c9a66',
183
+ lineItems: [],
184
+ payments: [],
185
+ patient: patientUuid,
186
+ status: 'PENDING',
187
+ };
188
+
189
+ billItems.forEach((item) => {
190
+ let lineItem: any = {
191
+ quantity: parseInt(item.Qnty),
192
+ price: item.Price,
193
+ priceName: 'Default',
194
+ priceUuid: '7b9171ac-d3c1-49b4-beff-c9902aee5245',
195
+ lineItemOrder: 0,
196
+ paymentStatus: 'PENDING',
197
+ };
198
+
199
+ if (item.category === 'StockItem') {
200
+ lineItem.item = item.uuid;
201
+ } else {
202
+ lineItem.billableService = item.uuid;
203
+ }
204
+
205
+ bill?.lineItems.push(lineItem);
206
+ });
207
+
208
+ const url = `${apiBasePath}bill`;
209
+ processBillItems(bill).then(
210
+ () => {
211
+ setIsSubmitting(false);
212
+
213
+ closeWorkspace();
214
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
215
+ showSnackbar({
216
+ title: t('billItems', 'Save Bill'),
217
+ subtitle: 'Bill processing has been successful',
218
+ kind: 'success',
219
+ timeoutInMs: 3000,
220
+ });
221
+ },
222
+ (error) => {
223
+ setIsSubmitting(false);
224
+ showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: error?.message });
225
+ },
226
+ );
227
+ };
228
+
229
+ return (
230
+ <Form className={styles.form}>
231
+ <div className={styles.grid}>
232
+ <Stack>
233
+ <RadioButtonGroup
234
+ legendText={t('selectCategory', 'Select category')}
235
+ name="radio-button-group"
236
+ defaultSelected="radio-1"
237
+ className={styles.mt2}
238
+ onChange={toggleSearch}>
239
+ <RadioButton labelText={t('stockItem', 'Stock Item')} value="Stock Item" id="stockItem" />
240
+ <RadioButton labelText={t('service', 'Service')} value="Service" id="service" />
241
+ </RadioButtonGroup>
242
+ </Stack>
243
+ <Stack>
244
+ <Search
245
+ size="lg"
246
+ id="searchField"
247
+ disabled
248
+ closeButtonLabelText={t('clearSearchInput', 'Clear search input')}
249
+ className={styles.mt2}
250
+ placeholder={t('searchItems', 'Search items and services')}
251
+ labelText={t('searchItems', 'Search items and services')}
252
+ onKeyUp={handleSearchTermChange}
253
+ />
254
+ </Stack>
255
+ <Stack>
256
+ <ul className={styles.searchContent}>
257
+ {searchOptions?.length > 0 &&
258
+ searchOptions?.map((row) => (
259
+ <li key={row.uuid} className={styles.searchItem}>
260
+ <Button
261
+ id={row.uuid}
262
+ onClick={(e) => addItemToBill(e, row.uuid, row.Item, row.category, row.Price)}
263
+ style={{ background: 'inherit', color: 'black' }}>
264
+ {row.Item} Qnty.{row.Qnty} Ksh.{row.Price}
265
+ </Button>
266
+ </li>
267
+ ))}
268
+
269
+ {searchOptions?.length === 0 && !isLoading && !!debouncedSearchTerm && (
270
+ <p>{t('noResultsFound', 'No results found')}</p>
271
+ )}
272
+ </ul>
273
+ </Stack>
274
+ <Stack>
275
+ <Table aria-label="sample table" className={styles.mt2}>
276
+ <TableHead>
277
+ <TableRow>
278
+ <TableHeader>Item</TableHeader>
279
+ <TableHeader>Quantity</TableHeader>
280
+ <TableHeader>Price</TableHeader>
281
+ <TableHeader>Total</TableHeader>
282
+ <TableHeader>Action</TableHeader>
283
+ </TableRow>
284
+ </TableHead>
285
+ <TableBody>
286
+ {billItems && Array.isArray(billItems) ? (
287
+ billItems.map((row) => (
288
+ <TableRow>
289
+ <TableCell>{row.Item}</TableCell>
290
+ <TableCell>
291
+ <input
292
+ type="number"
293
+ className={`form-control ${row.Qnty <= 0 ? styles.invalidInput : ''}`}
294
+ id={row.Item}
295
+ min={0}
296
+ max={100}
297
+ value={row.Qnty}
298
+ onChange={(e) => {
299
+ calculateTotal(e, row.Item);
300
+ row.Qnty = e.target.value;
301
+ }}
302
+ />
303
+ </TableCell>
304
+ <TableCell id={row.Item + 'Price'}>{row.Price}</TableCell>
305
+ <TableCell id={row.Item + 'Total'} className="totalValue">
306
+ {row.Total}
307
+ </TableCell>
308
+ <TableCell>
309
+ <TrashCan onClick={() => removeItemFromBill(row.uuid)} className={styles.removeButton} />
310
+ </TableCell>
311
+ </TableRow>
312
+ ))
313
+ ) : (
314
+ <p>Loading...</p>
315
+ )}
316
+ <TableRow>
317
+ <TableCell colSpan={3}></TableCell>
318
+ <TableCell style={{ fontWeight: 'bold' }}>{t('grandTotal', 'Grand total')}:</TableCell>
319
+ <TableCell id="GrandTotalSum">{convertToCurrency(grandTotal, defaultCurrency)}</TableCell>
320
+ </TableRow>
321
+ </TableBody>
322
+ </Table>
323
+ </Stack>
324
+
325
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
326
+ <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
327
+ {t('discard', 'Discard')}
328
+ </Button>
329
+ <Button
330
+ className={styles.button}
331
+ kind="primary"
332
+ onClick={postBillItems}
333
+ disabled={isSubmitting || saveDisabled}
334
+ type="submit">
335
+ {isSubmitting ? (
336
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
337
+ ) : (
338
+ t('saveAndClose', 'Save and close')
339
+ )}
340
+ </Button>
341
+ </ButtonSet>
342
+ </div>
343
+ </Form>
344
+ );
345
+ };
346
+
347
+ export default BillingForm;
@@ -0,0 +1,32 @@
1
+ import useSWR from 'swr';
2
+ import { type OpenmrsResource, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
3
+ import { apiBasePath } from '../constants';
4
+
5
+ export const useBillableItems = () => {
6
+ const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price,paymentMode))`;
7
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
8
+ return {
9
+ lineItems: data?.data?.results ?? [],
10
+ isLoading,
11
+ error,
12
+ };
13
+ };
14
+
15
+ export const useCashPoint = () => {
16
+ const url = `${apiBasePath}cashPoint`;
17
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
18
+
19
+ return { isLoading, error, cashPoints: data?.data?.results ?? [] };
20
+ };
21
+
22
+ export const createPatientBill = (payload) => {
23
+ const postUrl = `${apiBasePath}bill`;
24
+ return openmrsFetch(postUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
25
+ };
26
+
27
+ export const usePaymentMethods = () => {
28
+ const url = `${apiBasePath}paymentMode`;
29
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
30
+
31
+ return { isLoading, error, paymentModes: data?.data?.results ?? [] };
32
+ };
@@ -0,0 +1,88 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .tablet {
6
+ padding: 1.5rem spacing.$spacing-05;
7
+ background-color: $ui-02;
8
+ }
9
+
10
+ .desktop {
11
+ padding: 0rem;
12
+ }
13
+
14
+ .button {
15
+ height: 4rem;
16
+ display: flex;
17
+ align-content: flex-start;
18
+ align-items: baseline;
19
+ min-width: 50%;
20
+
21
+ :global(.cds--inline-loading) {
22
+ min-height: 1rem !important;
23
+ }
24
+
25
+ :global(.cds--inline-loading__text) {
26
+ font-size: unset !important;
27
+ }
28
+ }
29
+
30
+ .mt2 {
31
+ margin-top: spacing.$spacing-07;
32
+ }
33
+
34
+ .searchContent {
35
+ position: absolute;
36
+ background-color: #fff;
37
+ min-width: 230px;
38
+ overflow: auto;
39
+ border: 1px solid #ddd;
40
+ z-index: 1;
41
+ width: 92%;
42
+ }
43
+
44
+ .searchItem {
45
+ border-bottom: 0.1rem solid;
46
+ border-bottom-color: silver;
47
+ cursor: pointer;
48
+ }
49
+
50
+ .invalidInput {
51
+ border: 2px solid red;
52
+ }
53
+
54
+ .removeButton {
55
+ color: #ee0909;
56
+ right: 20px;
57
+ cursor: pointer;
58
+ }
59
+
60
+ .form {
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: space-between;
64
+ height: calc(100vh - 6rem);
65
+ }
66
+
67
+ :global(.omrs-breakpoint-lt-desktop) .form {
68
+ background-color: #ededed;
69
+ }
70
+
71
+ :global(.omrs-breakpoint-gt-tablet) .form {
72
+ background-color: $ui-02;
73
+ }
74
+
75
+ .grid {
76
+ margin: spacing.$spacing-05;
77
+ }
78
+
79
+ .row {
80
+ margin: spacing.$spacing-03 0rem 0rem;
81
+ display: flex;
82
+ flex-flow: row wrap;
83
+ gap: spacing.$spacing-05;
84
+ }
85
+
86
+ .spacer {
87
+ margin-top: spacing.$spacing-05;
88
+ }
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import { z } from 'zod';
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import { Controller, useForm } from 'react-hook-form';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { TextInput, InlineLoading, ComboBox, RadioButtonGroup, RadioButton } from '@carbon/react';
7
+ import { usePaymentMethods } from '../billing-form.resource';
8
+ import styles from './visit-attributes-form.scss';
9
+ import { useConfig } from '@openmrs/esm-framework';
10
+
11
+ type VisitAttributesFormProps = {
12
+ setAttributes: (state) => void;
13
+ setPaymentMethod?: (value: any) => void;
14
+ };
15
+
16
+ type VisitAttributesFormValue = {
17
+ paymentDetails: string;
18
+ paymentMethods: string;
19
+ insuranceScheme: string;
20
+ policyNumber: string;
21
+ patientCategory: string;
22
+ };
23
+
24
+ const visitAttributesFormSchema = z.object({
25
+ paymentDetails: z.string(),
26
+ paymentMethods: z.string(),
27
+ insuranceSchema: z.string(),
28
+ policyNumber: z.string(),
29
+ patientCategory: z.string(),
30
+ });
31
+
32
+ const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes, setPaymentMethod }) => {
33
+ const { t } = useTranslation();
34
+ const { control, getValues, watch } = useForm<VisitAttributesFormValue>({
35
+ mode: 'all',
36
+ defaultValues: {},
37
+ resolver: zodResolver(visitAttributesFormSchema),
38
+ });
39
+ const { patientCatergory, catergoryConcepts } = useConfig();
40
+ const [paymentDetails, paymentMethods, insuranceSchema, policyNumber, patientCategory] = watch([
41
+ 'paymentDetails',
42
+ 'paymentMethods',
43
+ 'insuranceScheme',
44
+ 'policyNumber',
45
+ 'patientCategory',
46
+ ]);
47
+
48
+ const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentMethods();
49
+ React.useEffect(() => {
50
+ setAttributes(createVisitAttributesPayload());
51
+ }, [paymentDetails, paymentMethods, insuranceSchema, policyNumber, patientCategory]);
52
+
53
+ const createVisitAttributesPayload = () => {
54
+ const { paymentDetails, paymentMethods, insuranceScheme, policyNumber, patientCategory } = getValues();
55
+ setPaymentMethod(paymentMethods);
56
+ const formPayload = [
57
+ { uuid: patientCatergory.paymentDetails, value: paymentDetails },
58
+ { uuid: patientCatergory.paymentMethods, value: paymentMethods },
59
+ { uuid: patientCatergory.insuranceScheme, value: insuranceScheme },
60
+ { uuid: patientCatergory.policyNumber, value: policyNumber },
61
+ { uuid: patientCatergory.patientCategory, value: patientCategory },
62
+ ];
63
+ const visitAttributesPayload = formPayload.filter(
64
+ (item) => item.value !== undefined && item.value !== null && item.value !== '',
65
+ );
66
+ return Object.entries(visitAttributesPayload).map(([key, value]) => ({
67
+ attributeType: value.uuid,
68
+ value: value.value,
69
+ }));
70
+ };
71
+
72
+ if (isLoadingPaymentModes) {
73
+ return (
74
+ <InlineLoading
75
+ status="active"
76
+ iconDescription={t('loadingDescription', 'Loading')}
77
+ description={t('loading', 'Loading data...')}
78
+ />
79
+ );
80
+ }
81
+
82
+ return (
83
+ <section>
84
+ <div className={styles.sectionTitle}>{t('paymentDetails', 'Payment Details')}</div>
85
+ <Controller
86
+ name="paymentDetails"
87
+ control={control}
88
+ render={({ field }) => (
89
+ <RadioButtonGroup
90
+ onChange={(selected) => field.onChange(selected)}
91
+ orientation="vertical"
92
+ legendText={t('paymentDetails', 'Payment Details')}
93
+ name="payment-details">
94
+ <RadioButton labelText="Paying" value={catergoryConcepts.payingDetails} id="radio-1" />
95
+ <RadioButton labelText="Non paying" value={catergoryConcepts.nonPayingDetails} id="radio-2" />
96
+ </RadioButtonGroup>
97
+ )}
98
+ />
99
+
100
+ {paymentDetails === catergoryConcepts.payingDetails && (
101
+ <Controller
102
+ control={control}
103
+ name="paymentMethods"
104
+ render={({ field }) => (
105
+ <ComboBox
106
+ className={styles.sectionField}
107
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
108
+ id="paymentMethods"
109
+ items={paymentModes}
110
+ itemToString={(item) => (item ? item.name : '')}
111
+ titleText={t('paymentMethods', 'Payment methods')}
112
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
113
+ />
114
+ )}
115
+ />
116
+ )}
117
+
118
+ {paymentMethods === catergoryConcepts.insuranceDetails && paymentDetails === catergoryConcepts.payingDetails && (
119
+ <>
120
+ <Controller
121
+ control={control}
122
+ name="insuranceScheme"
123
+ render={({ field }) => (
124
+ <TextInput
125
+ className={styles.sectionField}
126
+ onChange={(e) => field.onChange(e.target.value)}
127
+ id="insurance-scheme"
128
+ type="text"
129
+ labelText={t('insuranceScheme', 'Insurance scheme')}
130
+ />
131
+ )}
132
+ />
133
+ <Controller
134
+ control={control}
135
+ name="policyNumber"
136
+ render={({ field }) => (
137
+ <TextInput
138
+ className={styles.sectionField}
139
+ onChange={(e) => field.onChange(e.target.value)}
140
+ {...field}
141
+ id="policy-number"
142
+ type="text"
143
+ labelText={t('policyNumber', 'Policy number')}
144
+ />
145
+ )}
146
+ />
147
+ </>
148
+ )}
149
+
150
+ {paymentDetails === catergoryConcepts.nonPayingDetails && (
151
+ <Controller
152
+ control={control}
153
+ name="patientCategory"
154
+ render={({ field }) => (
155
+ <ComboBox
156
+ className={styles.sectionField}
157
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
158
+ id="patientCategory"
159
+ items={[
160
+ { text: 'Child under 5', uuid: catergoryConcepts.childUnder5 },
161
+ { text: 'Student', uuid: catergoryConcepts.student },
162
+ ]}
163
+ itemToString={(item) => (item ? item.text : '')}
164
+ titleText={t('patientCategory', 'Patient category')}
165
+ />
166
+ )}
167
+ />
168
+ )}
169
+ </section>
170
+ );
171
+ };
172
+
173
+ export default VisitAttributesForm;
@@ -0,0 +1,22 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .sectionContainer {
6
+ margin: 0 layout.$spacing-03;
7
+ }
8
+
9
+ .sectionTitle {
10
+ @include type.type-style('heading-compact-02');
11
+ color: colors.$gray-70;
12
+ margin: 0 0 layout.$spacing-03 0;
13
+ }
14
+ .visitAttributesContainer {
15
+ row-gap: layout.$spacing-03;
16
+ display: flex;
17
+ flex-direction: column;
18
+ }
19
+
20
+ .sectionField {
21
+ margin-top: layout.$spacing-03;
22
+ }