@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,242 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { useReactToPrint } from 'react-to-print';
5
+ import { showSnackbar } from '@openmrs/esm-framework';
6
+ import { mockPayments, mockBill } from '../../__mocks__/bills.mock';
7
+ import { useBill, processBillPayment } from '../billing.resource';
8
+ import { usePaymentModes } from './payments/payment.resource';
9
+ import Invoice from './invoice.component';
10
+
11
+ const mockedBill = jest.mocked(useBill);
12
+ const mockedProcessBillPayment = jest.mocked(processBillPayment);
13
+ const mockedUsePaymentModes = jest.mocked(usePaymentModes);
14
+ const mockedUseReactToPrint = jest.mocked(useReactToPrint);
15
+
16
+ jest.mock('./payments/payment.resource', () => ({
17
+ usePaymentModes: jest.fn(),
18
+ updateBillVisitAttribute: jest.fn(),
19
+ }));
20
+
21
+ jest.mock('../billing.resource', () => ({
22
+ useBill: jest.fn(),
23
+ processBillPayment: jest.fn(),
24
+ useDefaultFacility: jest.fn().mockReturnValue({ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590', display: 'MTRH' }),
25
+ }));
26
+
27
+ jest.mock('react-router-dom', () => {
28
+ const originalModule = jest.requireActual('react-router-dom');
29
+
30
+ return {
31
+ ...originalModule,
32
+ useParams: jest.fn().mockReturnValue({ patientUuid: 'patientUuid', billUuid: 'billUuid' }),
33
+ };
34
+ });
35
+
36
+ jest.mock('react-to-print', () => {
37
+ const originalModule = jest.requireActual('react-to-print');
38
+
39
+ return {
40
+ ...originalModule,
41
+ useReactToPrint: jest.fn(),
42
+ };
43
+ });
44
+
45
+ jest.mock('@openmrs/esm-framework', () => {
46
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
47
+
48
+ return {
49
+ ...originalModule,
50
+ usePatient: jest.fn().mockReturnValue({
51
+ patient: {
52
+ id: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
53
+ name: [{ given: ['John'], family: 'Doe' }],
54
+ },
55
+ patientUuid: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
56
+ isLoading: false,
57
+ error: null,
58
+ }),
59
+ };
60
+ });
61
+
62
+ xdescribe('Invoice', () => {
63
+ beforeEach(() => {
64
+ mockedBill.mockReturnValue({
65
+ bill: mockBill,
66
+ isLoading: false,
67
+ error: null,
68
+ isValidating: false,
69
+ mutate: jest.fn(),
70
+ });
71
+
72
+ mockedUsePaymentModes.mockReturnValue({
73
+ paymentModes: [
74
+ { uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
75
+ { uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
76
+ ],
77
+ isLoading: false,
78
+ error: null,
79
+ mutate: jest.fn(),
80
+ });
81
+ });
82
+
83
+ afterEach(() => jest.clearAllMocks());
84
+
85
+ test('should be able to search through the invoice table and settle a bill', async () => {
86
+ const user = userEvent.setup();
87
+
88
+ renderInvoice();
89
+
90
+ const expectedHeaders = [
91
+ /Total amount/i,
92
+ /Amount tendered/i,
93
+ /Date and time/i,
94
+ /Invoice status/i,
95
+ /Invoice number/i,
96
+ ];
97
+
98
+ expectedHeaders.forEach((header) => {
99
+ expect(screen.getByRole('heading', { name: header })).toBeInTheDocument();
100
+ });
101
+
102
+ const printButton = screen.getByRole('button', { name: /Print bill/i });
103
+ expect(printButton).toBeInTheDocument();
104
+
105
+ // Should show the line items table with the correct headers
106
+ const expectedColumnHeaders = [/No/i, /Bill item/i, /Bill code/i, /Status/i, /Quantity/i, /Price/i, /Total/i];
107
+
108
+ expectedColumnHeaders.forEach((columnHeader) => {
109
+ expect(screen.getByRole('columnheader', { name: columnHeader })).toBeInTheDocument();
110
+ });
111
+
112
+ expect(screen.getByRole('heading', { name: /Line items/i })).toBeInTheDocument();
113
+ expect(screen.getByText(/Items to be billed/i)).toBeInTheDocument();
114
+
115
+ // Should be able to search the line items table
116
+ const searchInput = screen.getByRole('searchbox');
117
+ expect(searchInput).toBeInTheDocument();
118
+ await user.type(searchInput, 'Hemoglobin');
119
+ expect(screen.getByText('Hemoglobin')).toBeInTheDocument();
120
+
121
+ await user.type(searchInput, 'Some random text');
122
+ expect(screen.queryByText('Hemoglobin')).not.toBeInTheDocument();
123
+ expect(screen.getByText(/No matching items to display/i)).toBeInTheDocument();
124
+ await user.clear(searchInput);
125
+
126
+ const row = mockBill.lineItems[0].item + ' ' + mockBill.receiptNumber + ' ' + mockBill.status.toUpperCase();
127
+
128
+ expect(screen.getByRole('row', { name: new RegExp(row, 'i') })).toBeInTheDocument();
129
+
130
+ // should be able to handle payments
131
+ const paymentSection = await screen.findByRole('heading', { name: /Payments/i });
132
+ expect(paymentSection).toBeInTheDocument();
133
+
134
+ const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
135
+ expect(addPaymentOptionButton).toBeInTheDocument();
136
+ await user.click(addPaymentOptionButton);
137
+ const paymentModeInput = screen.getByRole('combobox', { name: /Payment method/i });
138
+ expect(paymentModeInput).toBeInTheDocument();
139
+ await user.click(paymentModeInput);
140
+
141
+ // select cash payment mode
142
+ const cashPaymentMode = await screen.findByText('Cash');
143
+ expect(cashPaymentMode).toBeInTheDocument();
144
+ await user.click(cashPaymentMode);
145
+
146
+ // enter payment amount
147
+ const paymentAmountInput = screen.getByPlaceholderText('Enter amount');
148
+ expect(paymentAmountInput).toBeInTheDocument();
149
+ await user.type(paymentAmountInput, '100');
150
+
151
+ // enter payment reference number
152
+ const paymentReferenceNumberInput = screen.getByRole('textbox', { name: /Reference number/ });
153
+ expect(paymentReferenceNumberInput).toBeInTheDocument();
154
+ await user.type(paymentReferenceNumberInput, '123456');
155
+
156
+ expect(addPaymentOptionButton).toBeDisabled();
157
+
158
+ // should process payment
159
+ mockedProcessBillPayment.mockResolvedValueOnce(Promise.resolve({} as any));
160
+ const processPaymentButton = screen.getByRole('button', { name: /Process Payment/i });
161
+ expect(processPaymentButton).toBeInTheDocument();
162
+ await user.click(processPaymentButton);
163
+
164
+ expect(processBillPayment).toHaveBeenCalledTimes(1);
165
+ expect(processBillPayment).toHaveBeenCalledWith(
166
+ {
167
+ cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590',
168
+ cashier: 'fe00dd43-4c39-4ce9-9832-bc3620c80c6c',
169
+ patient: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
170
+ payments: [{ amount: 100, amountTendered: 100, attributes: [], instanceType: 'uuid' }],
171
+ status: 'PAID',
172
+ },
173
+ '6eb8d678-514d-46ad-9554-51e48d96d567',
174
+ );
175
+ expect(showSnackbar).toHaveBeenCalled();
176
+ expect(showSnackbar).toHaveBeenCalledWith({
177
+ kind: 'success',
178
+ subtitle: 'Bill payment processing has been successful',
179
+ timeoutInMs: 3000,
180
+ title: 'Bill payment',
181
+ });
182
+ });
183
+
184
+ test('should show print preview when print button is clicked', async () => {
185
+ const user = userEvent.setup();
186
+
187
+ renderInvoice();
188
+
189
+ const printButton = screen.getByRole('button', { name: /Print bill/i });
190
+ expect(printButton).toBeInTheDocument();
191
+ await user.click(printButton);
192
+ expect(mockedUseReactToPrint).toHaveBeenCalledTimes(1);
193
+ expect(mockedUseReactToPrint).toHaveBeenCalledWith(
194
+ expect.objectContaining({
195
+ documentTitle: 'Invoice 0035-6 - John Doe',
196
+ }),
197
+ );
198
+ });
199
+
200
+ test('should show payment history if bill is paid and disable adding more payments', async () => {
201
+ const user = userEvent.setup();
202
+ mockedBill.mockReturnValue({
203
+ bill: {
204
+ ...mockBill,
205
+ status: 'PAID',
206
+ payments: mockPayments,
207
+ tenderedAmount: 100,
208
+ },
209
+ isLoading: false,
210
+ error: null,
211
+ isValidating: false,
212
+ mutate: jest.fn(),
213
+ });
214
+
215
+ mockedUsePaymentModes.mockReturnValue({
216
+ paymentModes: [
217
+ { uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
218
+ { uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
219
+ ],
220
+ isLoading: false,
221
+ error: null,
222
+ mutate: jest.fn(),
223
+ });
224
+
225
+ renderInvoice();
226
+ const paymentHistorySection = screen.getByRole('heading', { name: /Payments/i });
227
+ expect(paymentHistorySection).toBeInTheDocument();
228
+
229
+ const expectedColumnHeaders = [/Date of payment/, /Bill amount/, /Amount tendered/, /Payment method/];
230
+ expectedColumnHeaders.forEach((header) => {
231
+ expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
232
+ });
233
+
234
+ const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
235
+ expect(addPaymentOptionButton).toBeInTheDocument();
236
+ expect(addPaymentOptionButton).toBeDisabled();
237
+ });
238
+ });
239
+
240
+ function renderInvoice() {
241
+ return render(<Invoice />);
242
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import styles from './invoice-breakdown.scss';
3
+
4
+ type InvoiceBreakDownProps = {
5
+ label: string;
6
+ value: string;
7
+ hasBalance?: Boolean;
8
+ };
9
+
10
+ export const InvoiceBreakDown: React.FC<InvoiceBreakDownProps> = ({ label, value, hasBalance }) => {
11
+ return (
12
+ <div className={styles.invoiceBreakdown}>
13
+ <span className={hasBalance ? styles.extendedLabel : styles.label}>{label}: </span>
14
+ <span className={styles.value}>{value}</span>
15
+ </div>
16
+ );
17
+ };
@@ -0,0 +1,29 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .invoiceBreakdown {
6
+ display: grid;
7
+ grid-template-columns: 1fr 1fr;
8
+ align-items: flex-end;
9
+ margin: layout.$spacing-02 0;
10
+ }
11
+
12
+ .label {
13
+ @include type.type-style('heading-03');
14
+ color: colors.$gray-100;
15
+ text-align: end;
16
+ }
17
+
18
+ .value {
19
+ @extend .label;
20
+ font-weight: bold;
21
+ margin-left: layout.$spacing-03;
22
+ text-align: start;
23
+ }
24
+
25
+ .extendedLabel {
26
+ @extend .label;
27
+ font-weight: bold;
28
+ color: crimson;
29
+ }
@@ -0,0 +1,105 @@
1
+ import React, { useCallback } from 'react';
2
+ import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { TrashCan, Add } from '@carbon/react/icons';
5
+ import { Button, Dropdown, NumberInputSkeleton, TextInput, NumberInput } from '@carbon/react';
6
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
7
+ import { type PaymentFormValue } from '../payments.component';
8
+ import { usePaymentModes } from '../payment.resource';
9
+ import styles from './payment-form.scss';
10
+
11
+ type PaymentFormProps = { disablePayment: boolean; amountDue: number };
12
+
13
+ const DEFAULT_PAYMENT = { method: '', amount: 0, referenceCode: '' };
14
+
15
+ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue }) => {
16
+ const { t } = useTranslation();
17
+ const {
18
+ control,
19
+ formState: { errors },
20
+ } = useFormContext<PaymentFormValue>();
21
+ const { paymentModes, isLoading, error } = usePaymentModes();
22
+ const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
23
+
24
+ const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT), [append]);
25
+ const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
26
+
27
+ if (isLoading) {
28
+ return <NumberInputSkeleton />;
29
+ }
30
+
31
+ if (error) {
32
+ return (
33
+ <div className={styles.errorPaymentContainer}>
34
+ <ErrorState headerTitle={t('errorLoadingPaymentModes', 'Payment modes error')} error={error} />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ return (
40
+ <div className={styles.container}>
41
+ {fields.map((field, index) => (
42
+ <div key={field.id} className={styles.paymentMethodContainer}>
43
+ <Controller
44
+ control={control}
45
+ name={`payment.${index}.method`}
46
+ render={({ field }) => (
47
+ <Dropdown
48
+ id="paymentMethod"
49
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
50
+ titleText={t('paymentMethod', 'Payment method')}
51
+ label={t('selectPaymentMethod', 'Select payment method')}
52
+ items={paymentModes}
53
+ itemToString={(item) => (item ? item.name : '')}
54
+ invalid={!!errors?.payment?.[index]?.method}
55
+ invalidText={errors?.payment?.[index]?.method?.message}
56
+ />
57
+ )}
58
+ />
59
+ <Controller
60
+ control={control}
61
+ name={`payment.${index}.amount`}
62
+ render={({ field }) => (
63
+ <NumberInput
64
+ id="paymentAmount"
65
+ {...field}
66
+ onChange={(e) => field.onChange(Number(e.target.value))}
67
+ invalid={!!errors?.payment?.[index]?.amount}
68
+ invalidText={errors?.payment?.[index]?.amount?.message}
69
+ label={t('amount', 'Amount')}
70
+ placeholder={t('enterAmount', 'Enter amount')}
71
+ />
72
+ )}
73
+ />
74
+ <Controller
75
+ name={`payment.${index}.referenceCode`}
76
+ control={control}
77
+ render={({ field }) => (
78
+ <TextInput
79
+ id="paymentReferenceCode"
80
+ {...field}
81
+ labelText={t('referenceNumber', 'Reference number')}
82
+ placeholder={t('enterReferenceNumber', 'Enter ref. number')}
83
+ type="text"
84
+ />
85
+ )}
86
+ />
87
+ <div className={styles.removeButtonContainer}>
88
+ <TrashCan onClick={handleRemovePaymentMode} className={styles.removeButton} size={20} />
89
+ </div>
90
+ </div>
91
+ ))}
92
+ <Button
93
+ disabled={disablePayment}
94
+ size="md"
95
+ onClick={handleAppendPaymentMode}
96
+ className={styles.paymentButtons}
97
+ renderIcon={(props) => <Add size={24} {...props} />}
98
+ iconDescription="Add">
99
+ {t('addPaymentOptions', 'Add payment option')}
100
+ </Button>
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default PaymentForm;
@@ -0,0 +1,54 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ margin: 1rem;
7
+ }
8
+
9
+ .paymentContainer {
10
+ margin: layout.$layout-01;
11
+ padding: layout.$layout-01;
12
+ width: 70%;
13
+ border-right: 1px solid colors.$cool-gray-40;
14
+ }
15
+
16
+ .paymentButtons {
17
+ margin: layout.$layout-01 0;
18
+ }
19
+
20
+ .paymentMethodContainer {
21
+ display: grid;
22
+ grid-template-columns: repeat(4, minmax(auto, 1fr));
23
+ align-items: flex-start;
24
+ column-gap: 1rem;
25
+ margin: 0.625rem 0;
26
+ width: 100%;
27
+ }
28
+
29
+ .paymentTotals {
30
+ margin-top: layout.$spacing-01;
31
+ }
32
+
33
+ .processPayments {
34
+ display: flex;
35
+ justify-content: flex-end;
36
+ margin: layout.$spacing-05;
37
+ column-gap: layout.$spacing-04;
38
+ }
39
+
40
+ .errorPaymentContainer {
41
+ margin: layout.$spacing-04;
42
+ min-height: layout.$spacing-09;
43
+ }
44
+
45
+ .removeButtonContainer {
46
+ display: flex;
47
+ align-self: center;
48
+ cursor: pointer;
49
+ margin-left: layout.$spacing-07;
50
+ }
51
+
52
+ .removeButton {
53
+ color: colors.$red-60;
54
+ }
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { DataTable, Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '@carbon/react';
3
+ import { type MappedBill } from '../../../types';
4
+ import { formatDate, useConfig } from '@openmrs/esm-framework';
5
+ import { convertToCurrency } from '../../../helpers';
6
+
7
+ type PaymentHistoryProps = {
8
+ bill: MappedBill;
9
+ };
10
+
11
+ const PaymentHistory: React.FC<PaymentHistoryProps> = ({ bill }) => {
12
+ const { defaultCurrency } = useConfig();
13
+ const headers = [
14
+ {
15
+ key: 'dateCreated',
16
+ header: 'Date of payment',
17
+ },
18
+ {
19
+ key: 'amount',
20
+ header: 'Bill amount',
21
+ },
22
+ {
23
+ key: 'amountTendered',
24
+ header: 'Amount tendered',
25
+ },
26
+ {
27
+ key: 'paymentMethod',
28
+ header: 'Payment method',
29
+ },
30
+ ];
31
+ const rows = bill?.payments?.map((payment) => ({
32
+ id: `${payment.uuid}`,
33
+ dateCreated: formatDate(new Date(payment.dateCreated)),
34
+ amountTendered: convertToCurrency(payment.amountTendered, defaultCurrency),
35
+ amount: convertToCurrency(payment.amount, defaultCurrency),
36
+ paymentMethod: payment.instanceType.name,
37
+ }));
38
+
39
+ if (Object.values(bill?.payments ?? {}).length === 0) {
40
+ return;
41
+ }
42
+
43
+ return (
44
+ <DataTable size="sm" rows={rows} headers={headers}>
45
+ {({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
46
+ <Table {...getTableProps()}>
47
+ <TableHead>
48
+ <TableRow>
49
+ {headers.map((header) => (
50
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
51
+ ))}
52
+ </TableRow>
53
+ </TableHead>
54
+ <TableBody>
55
+ {rows.map((row) => (
56
+ <TableRow {...getRowProps({ row })}>
57
+ {row.cells.map((cell) => (
58
+ <TableCell key={cell.id}>{cell.value}</TableCell>
59
+ ))}
60
+ </TableRow>
61
+ ))}
62
+ </TableBody>
63
+ </Table>
64
+ )}
65
+ </DataTable>
66
+ );
67
+ };
68
+
69
+ export default PaymentHistory;
@@ -0,0 +1,44 @@
1
+ import useSWR from 'swr';
2
+ import { type Visit, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
3
+ import { apiBasePath } from '../../constants';
4
+
5
+ type PaymentMethod = {
6
+ uuid: string;
7
+ description: string;
8
+ name: string;
9
+ retired: boolean;
10
+ };
11
+
12
+ const swrOption = {
13
+ errorRetryCount: 2,
14
+ };
15
+
16
+ export const usePaymentModes = () => {
17
+ const url = `${apiBasePath}paymentMode`;
18
+ const { data, isLoading, error, mutate } = useSWR<{ data: { results: Array<PaymentMethod> } }>(
19
+ url,
20
+ openmrsFetch,
21
+ swrOption,
22
+ );
23
+
24
+ return {
25
+ paymentModes: data?.data?.results ?? [],
26
+ isLoading,
27
+ mutate,
28
+ error,
29
+ };
30
+ };
31
+
32
+ export const updateBillVisitAttribute = async (visit: Visit) => {
33
+ const { uuid, attributes } = visit;
34
+ const pendingPaymentAtrributeUuid = attributes?.find(
35
+ (attribute) => attribute.attributeType.uuid === '919b51c9-8e2e-468f-8354-181bf3e55786',
36
+ )?.uuid;
37
+ return openmrsFetch(`${restBaseUrl}/visit/${uuid}/attribute/${pendingPaymentAtrributeUuid}`, {
38
+ body: { value: false },
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ method: 'POST',
43
+ });
44
+ };