@openmrs/esm-billing-app 1.1.1 → 1.1.2-pre.1

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 (193) hide show
  1. package/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
  2. package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
  3. package/.turbo/turbo-build.log +44 -0
  4. package/__mocks__/bills.mock.ts +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1197.js +1 -1
  7. package/dist/1435.js +1 -0
  8. package/dist/1435.js.map +1 -0
  9. package/dist/1807.js +1 -0
  10. package/dist/1807.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +1 -1
  13. package/dist/2177.js.map +1 -1
  14. package/dist/2690.js +1 -1
  15. package/dist/2704.js +1 -0
  16. package/dist/2704.js.map +1 -0
  17. package/dist/3002.js +1 -0
  18. package/dist/3002.js.map +1 -0
  19. package/dist/3041.js +1 -1
  20. package/dist/3041.js.map +1 -1
  21. package/dist/3099.js +1 -1
  22. package/dist/3184.js +1 -1
  23. package/dist/3184.js.map +1 -1
  24. package/dist/3584.js +1 -1
  25. package/dist/4055.js +1 -1
  26. package/dist/4132.js +1 -1
  27. package/dist/4225.js +1 -1
  28. package/dist/4225.js.map +1 -1
  29. package/dist/4300.js +1 -1
  30. package/dist/4335.js +1 -1
  31. package/dist/439.js +1 -1
  32. package/dist/4618.js +1 -1
  33. package/dist/4652.js +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5422.js +1 -1
  38. package/dist/5422.js.map +1 -1
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6404.js +1 -0
  43. package/dist/6404.js.map +1 -0
  44. package/dist/6468.js +1 -1
  45. package/dist/6540.js +1 -1
  46. package/dist/6540.js.map +1 -1
  47. package/dist/6589.js +1 -1
  48. package/dist/6606.js +1 -1
  49. package/dist/6606.js.map +1 -1
  50. package/dist/6679.js +1 -1
  51. package/dist/6792.js +1 -0
  52. package/dist/6792.js.map +1 -0
  53. package/dist/6840.js +1 -1
  54. package/dist/6859.js +1 -1
  55. package/dist/7097.js +1 -1
  56. package/dist/7159.js +1 -1
  57. package/dist/723.js +1 -1
  58. package/dist/7255.js +1 -1
  59. package/dist/7255.js.map +1 -1
  60. package/dist/7617.js +1 -1
  61. package/dist/795.js +1 -1
  62. package/dist/8163.js +1 -1
  63. package/dist/8341.js +2 -0
  64. package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
  65. package/dist/8341.js.map +1 -0
  66. package/dist/8349.js +1 -1
  67. package/dist/8371.js +1 -1
  68. package/dist/8421.js +1 -0
  69. package/dist/8421.js.map +1 -0
  70. package/dist/8618.js +1 -1
  71. package/dist/890.js +1 -1
  72. package/dist/9214.js +1 -1
  73. package/dist/9538.js +1 -1
  74. package/dist/9569.js +1 -1
  75. package/dist/961.js +1 -1
  76. package/dist/961.js.map +1 -1
  77. package/dist/986.js +1 -1
  78. package/dist/9879.js +1 -1
  79. package/dist/9895.js +1 -1
  80. package/dist/9900.js +1 -1
  81. package/dist/9913.js +1 -1
  82. package/dist/main.js +1 -1
  83. package/dist/main.js.LICENSE.txt +0 -15
  84. package/dist/main.js.map +1 -1
  85. package/dist/openmrs-esm-billing-app.js +1 -1
  86. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +284 -259
  87. package/dist/openmrs-esm-billing-app.js.map +1 -1
  88. package/dist/routes.json +1 -1
  89. package/e2e/commands/patient-operations.ts +1 -1
  90. package/e2e/pages/billing-dashboard-page.ts +3 -1
  91. package/e2e/pages/billing-form-page.ts +5 -0
  92. package/e2e/pages/invoice-page.ts +10 -0
  93. package/e2e/specs/billing-dashboard.spec.ts +126 -3
  94. package/e2e/specs/billing-patient-chart.spec.ts +95 -9
  95. package/package.json +3 -6
  96. package/src/bill-history/bill-action-menu.component.tsx +41 -0
  97. package/src/bill-history/bill-action-menu.scss +3 -0
  98. package/src/bill-history/bill-history.component.tsx +15 -5
  99. package/src/bill-history/bill-history.scss +0 -1
  100. package/src/bill-history/bill-history.test.tsx +78 -1
  101. package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
  102. package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
  103. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
  104. package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
  105. package/src/billing-form/billing-checkin-form.component.tsx +116 -57
  106. package/src/billing-form/billing-checkin-form.scss +26 -2
  107. package/src/billing-form/billing-checkin-form.test.tsx +51 -1
  108. package/src/billing-form/billing-form.resource.test.ts +87 -0
  109. package/src/billing-form/billing-form.resource.ts +33 -0
  110. package/src/billing-form/billing-form.scss +54 -7
  111. package/src/billing-form/billing-form.test.tsx +547 -0
  112. package/src/billing-form/billing-form.workspace.tsx +150 -45
  113. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
  114. package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
  115. package/src/billing-header/billing-header.component.tsx +1 -34
  116. package/src/billing-header/billing-header.scss +0 -50
  117. package/src/billing.resource.test.ts +11 -11
  118. package/src/billing.resource.ts +42 -12
  119. package/src/bills-table/bills-table.component.tsx +16 -12
  120. package/src/bills-table/bills-table.test.tsx +84 -7
  121. package/src/index.ts +5 -0
  122. package/src/invoice/invoice.component.tsx +46 -16
  123. package/src/invoice/invoice.scss +9 -8
  124. package/src/invoice/invoice.test.tsx +128 -7
  125. package/src/invoice/line-item-action-menu.component.tsx +2 -2
  126. package/src/invoice/payments/payments.component.tsx +2 -2
  127. package/src/invoice/payments/payments.test.tsx +31 -2
  128. package/src/metrics-cards/metrics.resource.ts +3 -4
  129. package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
  130. package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
  131. package/src/modal/require-payment.modal.tsx +2 -1
  132. package/src/routes.json +4 -0
  133. package/src/types/index.ts +10 -1
  134. package/tools/setup-tests.ts +7 -6
  135. package/translations/am.json +28 -0
  136. package/translations/ar.json +28 -0
  137. package/translations/ar_SY.json +28 -0
  138. package/translations/bn.json +28 -0
  139. package/translations/cs.json +28 -0
  140. package/translations/de.json +266 -238
  141. package/translations/en.json +29 -0
  142. package/translations/en_US.json +28 -0
  143. package/translations/es.json +28 -0
  144. package/translations/es_MX.json +28 -0
  145. package/translations/fr.json +28 -0
  146. package/translations/he.json +28 -0
  147. package/translations/hi.json +28 -0
  148. package/translations/hi_IN.json +28 -0
  149. package/translations/id.json +28 -0
  150. package/translations/it.json +28 -0
  151. package/translations/ka.json +28 -0
  152. package/translations/km.json +28 -0
  153. package/translations/ku.json +28 -0
  154. package/translations/ky.json +28 -0
  155. package/translations/lg.json +28 -0
  156. package/translations/ne.json +28 -0
  157. package/translations/pl.json +28 -0
  158. package/translations/pt.json +28 -0
  159. package/translations/pt_BR.json +28 -0
  160. package/translations/qu.json +28 -0
  161. package/translations/ro_RO.json +28 -0
  162. package/translations/ru_RU.json +28 -0
  163. package/translations/si.json +28 -0
  164. package/translations/sq.json +28 -0
  165. package/translations/sw.json +28 -0
  166. package/translations/sw_KE.json +28 -0
  167. package/translations/tr.json +28 -0
  168. package/translations/tr_TR.json +28 -0
  169. package/translations/uk.json +28 -0
  170. package/translations/uz.json +28 -0
  171. package/translations/uz@Latn.json +28 -0
  172. package/translations/uz_UZ.json +28 -0
  173. package/translations/vi.json +28 -0
  174. package/translations/zh.json +268 -240
  175. package/translations/zh_CN.json +30 -2
  176. package/translations/zh_TW.json +28 -0
  177. package/turbo.json +29 -0
  178. package/dist/1537.js +0 -1
  179. package/dist/1537.js.map +0 -1
  180. package/dist/1907.js +0 -2
  181. package/dist/1907.js.map +0 -1
  182. package/dist/1981.js +0 -1
  183. package/dist/1981.js.map +0 -1
  184. package/dist/2820.js +0 -1
  185. package/dist/2820.js.map +0 -1
  186. package/dist/8025.js +0 -1
  187. package/dist/8025.js.map +0 -1
  188. package/dist/9727.js +0 -2
  189. package/dist/9727.js.LICENSE.txt +0 -14
  190. package/dist/9727.js.map +0 -1
  191. package/dist/9756.js +0 -1
  192. package/dist/9756.js.map +0 -1
  193. package/src/hooks/selectedDateContext.ts +0 -10
@@ -31,6 +31,7 @@ import {
31
31
  type LayoutType,
32
32
  } from '@openmrs/esm-framework';
33
33
  import { usePaginatedBills } from '../billing.resource';
34
+ import { BillStatus } from '../types';
34
35
  import type { MappedBill } from '../types';
35
36
  import type { BillingConfig } from '../config-schema';
36
37
  import styles from './bills-table.scss';
@@ -42,9 +43,9 @@ interface BillDisplayItem extends Omit<MappedBill, 'id'> {
42
43
  }
43
44
 
44
45
  interface BillPaymentStatusFilterItem {
45
- id: string;
46
+ id: BillStatus | '';
46
47
  text: string;
47
- status: string;
48
+ status: BillStatus | '';
48
49
  }
49
50
 
50
51
  const mapLineItems = (bill: MappedBill) =>
@@ -64,13 +65,20 @@ const BillsTable: React.FC = () => {
64
65
  const billPaymentStatusFilterItems: BillPaymentStatusFilterItem[] = useMemo(
65
66
  () => [
66
67
  { id: '', text: t('allBills', 'All bills'), status: '' },
67
- { id: 'PENDING', text: t('pendingBills', 'Pending bills'), status: 'PENDING,POSTED' },
68
- { id: 'PAID', text: t('paidBills', 'Paid bills'), status: 'PAID' },
68
+ {
69
+ id: BillStatus.PENDING,
70
+ text: t('pendingConfirmationBills', 'Pending confirmation'),
71
+ status: BillStatus.PENDING,
72
+ },
73
+ { id: BillStatus.POSTED, text: t('pendingPaymentBills', 'Pending payment'), status: BillStatus.POSTED },
74
+ { id: BillStatus.PAID, text: t('paidBills', 'Paid bills'), status: BillStatus.PAID },
69
75
  ],
70
76
  [t],
71
77
  );
78
+
79
+ // Default to 'POSTED' (pending payment) so cashiers see bills awaiting payment on load
72
80
  const [billPaymentStatus, setBillPaymentStatus] = useState<BillPaymentStatusFilterItem>(
73
- billPaymentStatusFilterItems[1],
81
+ () => billPaymentStatusFilterItems.find((item) => item.id === BillStatus.POSTED) ?? billPaymentStatusFilterItems[0],
74
82
  );
75
83
  const [searchString, setSearchString] = useState('');
76
84
  const debouncedSearchString = useDebounce(searchString, 500);
@@ -127,9 +135,6 @@ const BillsTable: React.FC = () => {
127
135
  return mappedBills;
128
136
  }, [bills]);
129
137
 
130
- // Server-side search is now handled by the API, so we just use the bills directly
131
- const searchResults = billList;
132
-
133
138
  // Check if user has applied any filters (not "All bills") or search
134
139
  const hasActiveFiltersOrSearch = searchString.trim() !== '' || billPaymentStatus.id !== '';
135
140
 
@@ -156,7 +161,6 @@ const BillsTable: React.FC = () => {
156
161
  className={styles.filterDropdown}
157
162
  direction="bottom"
158
163
  id="bill-payment-status-filter"
159
- initialSelectedItem={billPaymentStatusFilterItems[1]}
160
164
  selectedItem={billPaymentStatus}
161
165
  items={billPaymentStatusFilterItems}
162
166
  itemToString={(item: BillPaymentStatusFilterItem) => (item ? item.text : '')}
@@ -196,10 +200,10 @@ const BillsTable: React.FC = () => {
196
200
  />
197
201
  <DataTable
198
202
  isSortable
199
- rows={searchResults}
203
+ rows={billList}
200
204
  headers={headerData}
201
205
  size={responsiveSize}
202
- useZebraStyles={searchResults?.length > 1 ? true : false}>
206
+ useZebraStyles={billList?.length > 1 ? true : false}>
203
207
  {({ rows, headers, getRowProps, getTableProps }) => (
204
208
  <TableContainer>
205
209
  <Table {...getTableProps()} aria-label={t('billList', 'Bill list')}>
@@ -227,7 +231,7 @@ const BillsTable: React.FC = () => {
227
231
  </TableContainer>
228
232
  )}
229
233
  </DataTable>
230
- {searchResults?.length === 0 && (
234
+ {billList?.length === 0 && (
231
235
  <div className={styles.filterEmptyState}>
232
236
  <Layer level={0}>
233
237
  <Tile className={styles.filterEmptyStateTile}>
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { render, screen, waitFor } from '@testing-library/react';
4
4
  import { usePaginatedBills } from '../billing.resource';
5
+ import { BillStatus } from '../types';
5
6
  import BillsTable from './bills-table.component';
6
7
 
7
8
  jest.mock('../billing.resource', () => ({
@@ -42,7 +43,7 @@ const mockBillsData = [
42
43
  paymentStatus: 'PENDING',
43
44
  },
44
45
  ],
45
- status: 'PENDING',
46
+ status: BillStatus.PENDING,
46
47
  cashPointUuid: 'cash-point-1',
47
48
  cashPointName: 'Main Cash Point',
48
49
  cashPointLocation: 'Main Hospital',
@@ -78,7 +79,7 @@ const mockBillsData = [
78
79
  paymentStatus: 'PENDING',
79
80
  },
80
81
  ],
81
- status: 'PENDING',
82
+ status: BillStatus.PENDING,
82
83
  cashPointUuid: 'cash-point-1',
83
84
  cashPointName: 'Main Cash Point',
84
85
  cashPointLocation: 'Main Hospital',
@@ -151,7 +152,7 @@ describe('BillsTable', () => {
151
152
 
152
153
  expect(screen.getByRole('progressbar')).toBeInTheDocument();
153
154
  expect(screen.getByText(/filter by/i)).toBeInTheDocument();
154
- expect(screen.getByText(/pending bills/i)).toBeInTheDocument();
155
+ expect(screen.getByText(/pending payment/i)).toBeInTheDocument();
155
156
  });
156
157
 
157
158
  test('should display an error state if there is a problem loading bill data', () => {
@@ -171,7 +172,7 @@ describe('BillsTable', () => {
171
172
  expect(screen.getByText(/error state/i)).toBeInTheDocument();
172
173
  expect(screen.queryByRole('table')).not.toBeInTheDocument();
173
174
  expect(screen.getByText(/filter by/i)).toBeInTheDocument();
174
- expect(screen.getByText(/pending bills/i)).toBeInTheDocument();
175
+ expect(screen.getByText(/pending payment/i)).toBeInTheDocument();
175
176
  });
176
177
 
177
178
  test('should pass search term to backend API', async () => {
@@ -200,7 +201,7 @@ describe('BillsTable', () => {
200
201
  await user.type(searchInput, 'John');
201
202
 
202
203
  await waitFor(() => {
203
- expect(mockBills).toHaveBeenCalledWith(10, 'PENDING,POSTED', 'John');
204
+ expect(mockBills).toHaveBeenCalledWith(10, 'POSTED', 'John');
204
205
  });
205
206
 
206
207
  expect(mockGoTo).toHaveBeenCalledWith(1);
@@ -220,7 +221,7 @@ describe('BillsTable', () => {
220
221
 
221
222
  // First call: initial render with PENDING filter (default)
222
223
  mockBills.mockImplementationOnce(() => ({
223
- bills: mockBillsData.map((bill) => ({ ...bill, status: 'PENDING' })),
224
+ bills: mockBillsData.map((bill) => ({ ...bill, status: BillStatus.PENDING })),
224
225
  isLoading: false,
225
226
  isValidating: false,
226
227
  error: null,
@@ -244,7 +245,7 @@ describe('BillsTable', () => {
244
245
 
245
246
  render(<BillsTable />);
246
247
 
247
- const filterDropdown = screen.getByText('Pending bills');
248
+ const filterDropdown = screen.getByText('Pending payment');
248
249
  await user.click(filterDropdown);
249
250
 
250
251
  const paidBillsOption = screen.getAllByText('Paid bills')[0];
@@ -325,6 +326,82 @@ describe('BillsTable', () => {
325
326
  });
326
327
  });
327
328
 
329
+ test('should default to "Pending payment" filter showing POSTED bills', () => {
330
+ render(<BillsTable />);
331
+
332
+ expect(screen.getByText('Pending payment')).toBeInTheDocument();
333
+ expect(mockBills).toHaveBeenCalledWith(expect.any(Number), 'POSTED', undefined);
334
+ });
335
+
336
+ test('should show "Pending confirmation" option in filter dropdown', async () => {
337
+ const user = userEvent.setup();
338
+ render(<BillsTable />);
339
+
340
+ const filterDropdown = screen.getByText('Pending payment');
341
+ await user.click(filterDropdown);
342
+
343
+ expect(screen.getByRole('option', { name: /pending confirmation/i })).toBeInTheDocument();
344
+ });
345
+
346
+ test('should filter by PENDING status when "Pending confirmation" is selected', async () => {
347
+ const user = userEvent.setup();
348
+ const mockGoTo = jest.fn();
349
+
350
+ mockBills.mockImplementation((_pageSize, status) => ({
351
+ bills: status === 'PENDING' ? [mockBillsData[0]] : mockBillsData,
352
+ isLoading: false,
353
+ isValidating: false,
354
+ error: null,
355
+ mutate: jest.fn(),
356
+ currentPage: 1,
357
+ totalCount: status === 'PENDING' ? 1 : 2,
358
+ goTo: mockGoTo,
359
+ }));
360
+
361
+ render(<BillsTable />);
362
+
363
+ const filterDropdown = screen.getByText('Pending payment');
364
+ await user.click(filterDropdown);
365
+
366
+ await user.click(screen.getByRole('option', { name: /pending confirmation/i }));
367
+
368
+ await waitFor(() => {
369
+ expect(mockBills).toHaveBeenCalledWith(expect.any(Number), 'PENDING', undefined);
370
+ });
371
+ });
372
+
373
+ test('should filter by POSTED status when "Pending payment" is selected', async () => {
374
+ const user = userEvent.setup();
375
+ const mockGoTo = jest.fn();
376
+
377
+ mockBills.mockImplementation((_pageSize, status) => ({
378
+ bills: status === 'POSTED' ? [mockBillsData[1]] : mockBillsData,
379
+ isLoading: false,
380
+ isValidating: false,
381
+ error: null,
382
+ mutate: jest.fn(),
383
+ currentPage: 1,
384
+ totalCount: 1,
385
+ goTo: mockGoTo,
386
+ }));
387
+
388
+ render(<BillsTable />);
389
+
390
+ // Navigate away from the default POSTED filter first, then select it again
391
+ const filterDropdown = screen.getByText('Pending payment');
392
+ await user.click(filterDropdown);
393
+ await user.click(screen.getByRole('option', { name: /all bills/i }));
394
+
395
+ mockBills.mockClear();
396
+
397
+ await user.click(screen.getByText('All bills'));
398
+ await user.click(screen.getByRole('option', { name: /pending payment/i }));
399
+
400
+ await waitFor(() => {
401
+ expect(mockBills).toHaveBeenCalledWith(expect.any(Number), 'POSTED', undefined);
402
+ });
403
+ });
404
+
328
405
  test('should keep data visible during subsequent loads', () => {
329
406
  mockBills.mockImplementationOnce(() => ({
330
407
  bills: mockBillsData,
package/src/index.ts CHANGED
@@ -130,3 +130,8 @@ export const deleteLineItemConfirmationModal = getAsyncLifecycle(
130
130
  () => import('./modal/delete-line-item-confirmation.modal'),
131
131
  options,
132
132
  );
133
+
134
+ export const finalizeBillConfirmationModal = getAsyncLifecycle(
135
+ () => import('./modal/finalize-bill-confirmation.modal'),
136
+ options,
137
+ );
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { Button, InlineLoading } from '@carbon/react';
3
- import { Printer } from '@carbon/react/icons';
3
+ import { Add, Printer } from '@carbon/react/icons';
4
4
  import { useParams } from 'react-router-dom';
5
5
  import { useReactToPrint } from 'react-to-print';
6
6
  import { useTranslation } from 'react-i18next';
@@ -8,7 +8,9 @@ import {
8
8
  ErrorState,
9
9
  ExtensionSlot,
10
10
  formatDate,
11
+ launchWorkspace2,
11
12
  parseDate,
13
+ showModal,
12
14
  showSnackbar,
13
15
  useConfig,
14
16
  usePatient,
@@ -20,6 +22,7 @@ import InvoiceTable from './invoice-table.component';
20
22
  import Payments from './payments/payments.component';
21
23
  import PrintReceipt from './printable-invoice/print-receipt.component';
22
24
  import PrintableInvoice from './printable-invoice/printable-invoice.component';
25
+ import { BillStatus } from '../types';
23
26
  import styles from './invoice.scss';
24
27
 
25
28
  interface InvoiceDetailsProps {
@@ -52,6 +55,14 @@ const Invoice: React.FC = () => {
52
55
  });
53
56
  }, [bill, patient]);
54
57
 
58
+ const handleFinalizeBill = () => {
59
+ const dispose = showModal('finalize-bill-confirmation-modal', {
60
+ bill,
61
+ onMutate: mutate,
62
+ closeModal: () => dispose(),
63
+ });
64
+ };
65
+
55
66
  const handlePrint = useReactToPrint({
56
67
  contentRef: componentRef,
57
68
  documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
@@ -112,27 +123,46 @@ const Invoice: React.FC = () => {
112
123
  return (
113
124
  <div className={styles.invoiceContainer}>
114
125
  {patient && patientUuid && <ExtensionSlot name="patient-header-slot" state={{ patient, patientUuid }} />}
126
+ <div className={styles.actionsContainer}>
127
+ {isValidating && (
128
+ <span>
129
+ <InlineLoading status="active" />
130
+ </span>
131
+ )}
132
+ {bill?.status === BillStatus.PENDING && (
133
+ <>
134
+ <Button
135
+ kind="ghost"
136
+ renderIcon={Add}
137
+ onClick={() =>
138
+ launchWorkspace2('billing-form-workspace', {
139
+ patientUuid,
140
+ billUuid: bill.uuid,
141
+ onMutate: mutate,
142
+ })
143
+ }>
144
+ {t('addItemsToBill', 'Add items to bill')}
145
+ </Button>
146
+ <Button kind="primary" onClick={handleFinalizeBill}>
147
+ {t('finalizeBill', 'Finalize bill')}
148
+ </Button>
149
+ </>
150
+ )}
151
+ <Button
152
+ disabled={isPrinting || isLoadingPatient || isLoadingBill}
153
+ onClick={handlePrint}
154
+ renderIcon={(props) => <Printer size={24} {...props} />}
155
+ iconDescription={t('printBill', 'Print bill')}>
156
+ {t('printBill', 'Print bill')}
157
+ </Button>
158
+ {bill && (bill.status === BillStatus.PAID || bill.tenderedAmount > 0) && <PrintReceipt billUuid={bill.uuid} />}
159
+ </div>
115
160
  <div className={styles.detailsContainer}>
116
161
  <section className={styles.details}>
117
162
  {Object.entries(invoiceDetails).map(([key, val]) => (
118
163
  <InvoiceDetails key={key} label={key} value={val} />
119
164
  ))}
120
165
  </section>
121
- <div className={styles.actionsContainer}>
122
- {isValidating && (
123
- <span>
124
- <InlineLoading status="active" />
125
- </span>
126
- )}
127
- <Button
128
- disabled={isPrinting || isLoadingPatient || isLoadingBill}
129
- onClick={handlePrint}
130
- renderIcon={(props) => <Printer size={24} {...props} />}
131
- iconDescription={t('printBill', 'Print bill')}>
132
- {t('printBill', 'Print bill')}
133
- </Button>
134
- {(bill?.status === 'PAID' || bill?.tenderedAmount > 0) && <PrintReceipt billUuid={bill?.uuid} />}
135
- </div>
136
166
  </div>
137
167
 
138
168
  <div className={styles.invoiceContent}>
@@ -17,6 +17,14 @@
17
17
  justify-content: center;
18
18
  }
19
19
 
20
+ .actionsContainer {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: flex-end;
24
+ gap: layout.$spacing-03;
25
+ padding: layout.$spacing-04 layout.$spacing-05;
26
+ }
27
+
20
28
  .detailsContainer {
21
29
  display: flex;
22
30
  }
@@ -27,17 +35,10 @@
27
35
  flex-flow: row wrap;
28
36
  align-items: center;
29
37
  justify-content: space-between;
30
- margin: layout.$spacing-05;
38
+ margin: layout.$spacing-05 layout.$spacing-06;
31
39
  row-gap: layout.$spacing-06;
32
40
  }
33
41
 
34
- .actionsContainer {
35
- display: flex;
36
- align-items: center;
37
- gap: layout.$spacing-03;
38
- margin: layout.$spacing-05;
39
- }
40
-
41
42
  .label {
42
43
  @include type.type-style('body-compact-02');
43
44
  color: colors.$gray-70;
@@ -2,8 +2,15 @@ import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { render, screen, waitFor } from '@testing-library/react';
4
4
  import { useReactToPrint } from 'react-to-print';
5
- import { getDefaultsFromConfigSchema, useConfig, usePatient } from '@openmrs/esm-framework';
5
+ import {
6
+ getDefaultsFromConfigSchema,
7
+ launchWorkspace2,
8
+ showModal,
9
+ useConfig,
10
+ usePatient,
11
+ } from '@openmrs/esm-framework';
6
12
  import { configSchema, type BillingConfig } from '../config-schema';
13
+ import { BillStatus } from '../types';
7
14
  import { mockBill, mockPatient } from 'mocks/bills.mock';
8
15
  import { useBill } from '../billing.resource';
9
16
  import { usePaymentModes } from './payments/payment.resource';
@@ -15,6 +22,7 @@ const mockUseBill = jest.mocked(useBill);
15
22
  const mockUsePatient = jest.mocked(usePatient);
16
23
  const mockUsePaymentModes = jest.mocked(usePaymentModes);
17
24
  const mockUseReactToPrint = jest.mocked(useReactToPrint);
25
+ const mockShowModal = jest.mocked(showModal);
18
26
 
19
27
  jest.mock('../helpers/functions', () => ({
20
28
  convertToCurrency: jest.fn((amount) => `USD ${amount}`),
@@ -62,7 +70,7 @@ describe('Invoice', () => {
62
70
  const defaultBillData = {
63
71
  ...mockBill,
64
72
  uuid: 'test-uuid',
65
- status: 'PENDING',
73
+ status: BillStatus.PENDING,
66
74
  totalAmount: 1000,
67
75
  tenderedAmount: 0,
68
76
  receiptNumber: 'RCPT-001',
@@ -188,7 +196,7 @@ describe('Invoice', () => {
188
196
  mockUseBill.mockReturnValue({
189
197
  bill: {
190
198
  ...defaultBillData,
191
- status: 'PAID',
199
+ status: BillStatus.PAID,
192
200
  tenderedAmount: 1000,
193
201
  },
194
202
  isLoading: false,
@@ -207,7 +215,7 @@ describe('Invoice', () => {
207
215
  mockUseBill.mockReturnValue({
208
216
  bill: {
209
217
  ...defaultBillData,
210
- status: 'PENDING',
218
+ status: BillStatus.PENDING,
211
219
  tenderedAmount: 500,
212
220
  },
213
221
  isLoading: false,
@@ -336,7 +344,7 @@ describe('Invoice', () => {
336
344
 
337
345
  const updatedBill = {
338
346
  ...defaultBillData,
339
- status: 'PAID',
347
+ status: BillStatus.PAID,
340
348
  tenderedAmount: 1000,
341
349
  };
342
350
 
@@ -456,7 +464,7 @@ describe('Invoice', () => {
456
464
  mockUseBill.mockReturnValue({
457
465
  bill: {
458
466
  ...defaultBillData,
459
- status: 'PENDING',
467
+ status: BillStatus.PENDING,
460
468
  totalAmount: 1000,
461
469
  tenderedAmount: 500, // Partial payment
462
470
  },
@@ -485,7 +493,7 @@ describe('Invoice', () => {
485
493
  mockUseBill.mockReturnValue({
486
494
  bill: {
487
495
  ...defaultBillData,
488
- status: 'PENDING',
496
+ status: BillStatus.PENDING,
489
497
  tenderedAmount: 0,
490
498
  },
491
499
  isLoading: false,
@@ -499,4 +507,117 @@ describe('Invoice', () => {
499
507
 
500
508
  expect(screen.queryByTestId('mock-print-receipt')).not.toBeInTheDocument();
501
509
  });
510
+
511
+ it('should show "Add items to bill" button for PENDING bills', async () => {
512
+ render(<Invoice />);
513
+ await waitForLoadingToFinish();
514
+
515
+ expect(screen.getByRole('button', { name: /add items to bill/i })).toBeInTheDocument();
516
+ });
517
+
518
+ it('should not show "Add items to bill" button for PAID bills', async () => {
519
+ mockUseBill.mockReturnValue({
520
+ bill: {
521
+ ...defaultBillData,
522
+ status: BillStatus.PAID,
523
+ tenderedAmount: 1000,
524
+ },
525
+ isLoading: false,
526
+ error: null,
527
+ isValidating: false,
528
+ mutate: jest.fn(),
529
+ });
530
+
531
+ render(<Invoice />);
532
+ await waitForLoadingToFinish();
533
+
534
+ expect(screen.queryByRole('button', { name: /add items to bill/i })).not.toBeInTheDocument();
535
+ });
536
+
537
+ it('should show "Finalize bill" button for PENDING bills', async () => {
538
+ render(<Invoice />);
539
+ await waitForLoadingToFinish();
540
+
541
+ expect(screen.getByRole('button', { name: /finalize bill/i })).toBeInTheDocument();
542
+ });
543
+
544
+ it('should not show "Finalize bill" button for POSTED bills', async () => {
545
+ mockUseBill.mockReturnValue({
546
+ bill: { ...defaultBillData, status: BillStatus.POSTED },
547
+ isLoading: false,
548
+ error: null,
549
+ isValidating: false,
550
+ mutate: jest.fn(),
551
+ });
552
+
553
+ render(<Invoice />);
554
+ await waitForLoadingToFinish();
555
+
556
+ expect(screen.queryByRole('button', { name: /finalize bill/i })).not.toBeInTheDocument();
557
+ });
558
+
559
+ it('should not show "Finalize bill" button for PAID bills', async () => {
560
+ mockUseBill.mockReturnValue({
561
+ bill: { ...defaultBillData, status: BillStatus.PAID, tenderedAmount: 1000 },
562
+ isLoading: false,
563
+ error: null,
564
+ isValidating: false,
565
+ mutate: jest.fn(),
566
+ });
567
+
568
+ render(<Invoice />);
569
+ await waitForLoadingToFinish();
570
+
571
+ expect(screen.queryByRole('button', { name: /finalize bill/i })).not.toBeInTheDocument();
572
+ });
573
+
574
+ it('should open finalize confirmation modal when "Finalize bill" button is clicked', async () => {
575
+ const mockMutate = jest.fn();
576
+ const user = userEvent.setup();
577
+
578
+ mockUseBill.mockReturnValue({
579
+ bill: defaultBillData,
580
+ isLoading: false,
581
+ error: null,
582
+ isValidating: false,
583
+ mutate: mockMutate,
584
+ });
585
+
586
+ render(<Invoice />);
587
+ await waitForLoadingToFinish();
588
+
589
+ await user.click(screen.getByRole('button', { name: /finalize bill/i }));
590
+
591
+ expect(mockShowModal).toHaveBeenCalledWith('finalize-bill-confirmation-modal', {
592
+ bill: defaultBillData,
593
+ onMutate: mockMutate,
594
+ closeModal: expect.any(Function),
595
+ });
596
+ });
597
+
598
+ it('should launch workspace with billUuid when "Add items to bill" is clicked', async () => {
599
+ const mockMutate = jest.fn();
600
+ const mockLaunchWorkspace2 = jest.mocked(launchWorkspace2);
601
+
602
+ mockUseBill.mockReturnValue({
603
+ bill: defaultBillData,
604
+ isLoading: false,
605
+ error: null,
606
+ isValidating: false,
607
+ mutate: mockMutate,
608
+ });
609
+
610
+ const user = userEvent.setup();
611
+ render(<Invoice />);
612
+ await waitForLoadingToFinish();
613
+
614
+ const addItemsButton = screen.getByRole('button', { name: /add items to bill/i });
615
+ await user.click(addItemsButton);
616
+
617
+ expect(mockLaunchWorkspace2).toHaveBeenCalledWith('billing-form-workspace', {
618
+ patientUuid: 'patientUuid',
619
+ billUuid: 'test-uuid',
620
+ onMutate: mockMutate,
621
+ });
622
+ });
502
623
  });
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
3
3
  import { getCoreTranslation, isDesktop, showModal, useLayoutType } from '@openmrs/esm-framework';
4
- import { type LineItem, type MappedBill } from '../types';
4
+ import { BillStatus, type LineItem, type MappedBill } from '../types';
5
5
  import styles from './line-item-action-menu.scss';
6
6
 
7
7
  type LineItemActionMenuProps = {
@@ -30,7 +30,7 @@ const LineItemActionMenu: React.FC<LineItemActionMenuProps> = ({ bill, item, onM
30
30
  });
31
31
  }, [item, onMutate]);
32
32
 
33
- const isPending = bill?.status === 'PENDING';
33
+ const isPending = bill?.status === BillStatus.PENDING;
34
34
 
35
35
  return (
36
36
  <Layer>
@@ -11,7 +11,7 @@ import PaymentHistory from './payment-history/payment-history.component';
11
11
  import { processBillPayment } from '../../billing.resource';
12
12
  import { updateBillVisitAttribute } from './payment.resource';
13
13
  import { convertToCurrency } from '../../helpers';
14
- import type { MappedBill } from '../../types';
14
+ import { BillStatus, type MappedBill } from '../../types';
15
15
  import styles from './payments.scss';
16
16
 
17
17
  type PaymentProps = {
@@ -113,7 +113,7 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate }) => {
113
113
  </CardHeader>
114
114
  <div>
115
115
  <PaymentHistory bill={bill} />
116
- <PaymentForm disablePayment={amountDue <= 0} />
116
+ <PaymentForm disablePayment={amountDue <= 0 || bill.status === BillStatus.PENDING} />
117
117
  </div>
118
118
  </div>
119
119
  <div className={styles.divider} />
@@ -9,7 +9,7 @@ import {
9
9
  type VisitReturnType,
10
10
  } from '@openmrs/esm-framework';
11
11
  import { useBillableServices } from '../../billable-services/billable-service.resource';
12
- import { type MappedBill } from '../../types';
12
+ import { BillStatus, type MappedBill } from '../../types';
13
13
  import { configSchema, type BillingConfig } from '../../config-schema';
14
14
  import { usePaymentModes } from './payment.resource';
15
15
  import Payments from './payments.component';
@@ -102,7 +102,7 @@ describe('Payments', () => {
102
102
  },
103
103
  ],
104
104
  receiptNumber: '12345',
105
- status: 'PAID',
105
+ status: BillStatus.PAID,
106
106
  identifier: 'invoice-123',
107
107
  dateCreated: '2023-09-01T12:00:00Z',
108
108
  lineItems: [],
@@ -224,6 +224,34 @@ describe('Payments', () => {
224
224
  expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
225
225
  });
226
226
 
227
+ it('should not show payment form when bill is in PENDING state', () => {
228
+ const pendingBill: MappedBill = {
229
+ ...mockBill,
230
+ status: BillStatus.PENDING,
231
+ totalAmount: 100,
232
+ tenderedAmount: 0,
233
+ };
234
+
235
+ render(<Payments bill={pendingBill} mutate={mockMutate} />);
236
+
237
+ expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
238
+ expect(screen.queryByText(/select payment method/i)).not.toBeInTheDocument();
239
+ });
240
+
241
+ it('should show payment form when bill is in POSTED state', () => {
242
+ const postedBill: MappedBill = {
243
+ ...mockBill,
244
+ status: BillStatus.POSTED,
245
+ totalAmount: 100,
246
+ tenderedAmount: 0,
247
+ };
248
+
249
+ render(<Payments bill={postedBill} mutate={mockMutate} />);
250
+
251
+ expect(screen.getByPlaceholderText(/enter amount/i)).toBeInTheDocument();
252
+ expect(screen.getByText(/select payment method/i)).toBeInTheDocument();
253
+ });
254
+
227
255
  it('should return null when bill is not provided', () => {
228
256
  const { container } = render(<Payments bill={null} mutate={mockMutate} />);
229
257
  expect(container).toBeEmptyDOMElement();
@@ -232,6 +260,7 @@ describe('Payments', () => {
232
260
  it('should show payment form when there is amount due', () => {
233
261
  const billWithAmountDue: MappedBill = {
234
262
  ...mockBill,
263
+ status: BillStatus.POSTED,
235
264
  totalAmount: 100,
236
265
  tenderedAmount: 0,
237
266
  lineItems: [],