@openmrs/esm-billing-app 1.0.2-pre.76 → 1.0.2-pre.764

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 (198) hide show
  1. package/README.md +54 -9
  2. package/__mocks__/bills.mock.ts +12 -0
  3. package/__mocks__/react-i18next.js +6 -5
  4. package/dist/1119.js +1 -1
  5. package/dist/1146.js +1 -2
  6. package/dist/1146.js.map +1 -1
  7. package/dist/1197.js +1 -1
  8. package/dist/1856.js +1 -0
  9. package/dist/1856.js.map +1 -0
  10. package/dist/2146.js +1 -1
  11. package/dist/2177.js +2 -0
  12. package/dist/2177.js.LICENSE.txt +9 -0
  13. package/dist/2177.js.map +1 -0
  14. package/dist/2524.js +1 -0
  15. package/dist/2524.js.map +1 -0
  16. package/dist/2690.js +1 -1
  17. package/dist/3041.js +1 -0
  18. package/dist/3041.js.map +1 -0
  19. package/dist/3099.js +1 -1
  20. package/dist/3584.js +1 -1
  21. package/dist/4055.js +1 -1
  22. package/dist/4132.js +1 -1
  23. package/dist/4225.js +1 -0
  24. package/dist/4225.js.map +1 -0
  25. package/dist/4300.js +1 -1
  26. package/dist/4335.js +1 -1
  27. package/dist/4618.js +1 -1
  28. package/dist/4652.js +1 -1
  29. package/dist/4724.js +1 -0
  30. package/dist/4724.js.map +1 -0
  31. package/dist/4739.js +1 -1
  32. package/dist/4739.js.map +1 -1
  33. package/dist/4944.js +1 -1
  34. package/dist/5173.js +1 -1
  35. package/dist/5241.js +1 -1
  36. package/dist/5422.js +1 -0
  37. package/dist/5422.js.map +1 -0
  38. package/dist/5442.js +1 -1
  39. package/dist/5661.js +1 -1
  40. package/dist/6022.js +1 -1
  41. package/dist/6468.js +1 -1
  42. package/dist/6540.js +1 -1
  43. package/dist/6540.js.map +1 -1
  44. package/dist/6606.js +1 -0
  45. package/dist/6606.js.map +1 -0
  46. package/dist/6679.js +1 -1
  47. package/dist/6840.js +1 -1
  48. package/dist/6859.js +1 -1
  49. package/dist/7097.js +1 -1
  50. package/dist/7159.js +1 -1
  51. package/dist/723.js +1 -1
  52. package/dist/7452.js +2 -0
  53. package/dist/7452.js.map +1 -0
  54. package/dist/7617.js +1 -1
  55. package/dist/795.js +1 -1
  56. package/dist/8163.js +1 -1
  57. package/dist/8349.js +1 -1
  58. package/dist/8618.js +1 -1
  59. package/dist/890.js +1 -1
  60. package/dist/8930.js +2 -0
  61. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  62. package/dist/8930.js.map +1 -0
  63. package/dist/9214.js +1 -1
  64. package/dist/942.js +1 -0
  65. package/dist/942.js.map +1 -0
  66. package/dist/9538.js +1 -1
  67. package/dist/9569.js +1 -1
  68. package/dist/961.js +1 -1
  69. package/dist/961.js.map +1 -1
  70. package/dist/986.js +1 -1
  71. package/dist/9879.js +1 -1
  72. package/dist/9895.js +1 -1
  73. package/dist/9900.js +1 -1
  74. package/dist/9913.js +1 -1
  75. package/dist/main.js +1 -1
  76. package/dist/main.js.map +1 -1
  77. package/dist/openmrs-esm-billing-app.js +1 -1
  78. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +368 -262
  79. package/dist/openmrs-esm-billing-app.js.map +1 -1
  80. package/dist/routes.json +1 -1
  81. package/e2e/README.md +19 -18
  82. package/e2e/specs/sample-test.spec.ts +0 -1
  83. package/package.json +10 -10
  84. package/src/bill-history/bill-history.component.tsx +17 -25
  85. package/src/bill-history/bill-history.scss +4 -94
  86. package/src/bill-history/bill-history.test.tsx +37 -78
  87. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  88. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +100 -78
  89. package/src/bill-item-actions/edit-bill-item.test.tsx +116 -31
  90. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  91. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  92. package/src/billable-services/billable-service.resource.ts +17 -9
  93. package/src/billable-services/billable-services-home.component.tsx +1 -1
  94. package/src/billable-services/billable-services.component.tsx +142 -145
  95. package/src/billable-services/billable-services.scss +3 -0
  96. package/src/billable-services/billable-services.test.tsx +2 -45
  97. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  98. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +18 -192
  99. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  100. package/src/billable-services/create-edit/add-billable-service.component.tsx +345 -298
  101. package/src/billable-services/create-edit/add-billable-service.scss +5 -6
  102. package/src/billable-services/create-edit/add-billable-service.test.tsx +37 -36
  103. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
  104. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  105. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  106. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  107. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  108. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  109. package/src/billing-form/billing-checkin-form.test.tsx +97 -24
  110. package/src/billing-form/billing-form.component.tsx +216 -269
  111. package/src/billing-form/billing-form.scss +143 -0
  112. package/src/billing.resource.ts +16 -19
  113. package/src/bills-table/bills-table.test.tsx +97 -53
  114. package/src/config-schema.ts +52 -24
  115. package/src/dashboard.meta.ts +4 -2
  116. package/src/helpers/functions.ts +5 -4
  117. package/src/index.ts +17 -6
  118. package/src/invoice/invoice-table.component.tsx +34 -68
  119. package/src/invoice/invoice-table.scss +1 -5
  120. package/src/invoice/invoice-table.test.tsx +20 -47
  121. package/src/invoice/invoice.component.tsx +36 -29
  122. package/src/invoice/invoice.scss +7 -4
  123. package/src/invoice/invoice.test.tsx +22 -48
  124. package/src/invoice/payments/payment-form/payment-form.component.tsx +31 -29
  125. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  126. package/src/invoice/payments/payment-form/payment-form.test.tsx +215 -67
  127. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  128. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  129. package/src/invoice/payments/payments.component.tsx +53 -65
  130. package/src/invoice/payments/payments.test.tsx +282 -0
  131. package/src/invoice/payments/utils.ts +5 -23
  132. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  133. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  134. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  135. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  136. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  137. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  138. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  139. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  140. package/src/modal/require-payment-modal.test.tsx +27 -22
  141. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  142. package/src/routes.json +22 -2
  143. package/src/types/index.ts +26 -17
  144. package/translations/am.json +60 -18
  145. package/translations/ar.json +60 -18
  146. package/translations/ar_SY.json +60 -18
  147. package/translations/bn.json +65 -23
  148. package/translations/de.json +60 -18
  149. package/translations/en.json +70 -21
  150. package/translations/en_US.json +60 -18
  151. package/translations/es.json +60 -18
  152. package/translations/es_MX.json +60 -18
  153. package/translations/fr.json +73 -31
  154. package/translations/he.json +60 -18
  155. package/translations/hi.json +60 -18
  156. package/translations/hi_IN.json +60 -18
  157. package/translations/id.json +61 -19
  158. package/translations/it.json +96 -54
  159. package/translations/ka.json +61 -19
  160. package/translations/km.json +60 -18
  161. package/translations/ku.json +60 -18
  162. package/translations/ky.json +60 -18
  163. package/translations/lg.json +60 -18
  164. package/translations/ne.json +60 -18
  165. package/translations/pl.json +60 -18
  166. package/translations/pt.json +60 -18
  167. package/translations/pt_BR.json +60 -18
  168. package/translations/qu.json +60 -18
  169. package/translations/ro_RO.json +206 -164
  170. package/translations/ru_RU.json +60 -18
  171. package/translations/si.json +60 -18
  172. package/translations/sw.json +60 -18
  173. package/translations/sw_KE.json +60 -18
  174. package/translations/tr.json +60 -18
  175. package/translations/tr_TR.json +60 -18
  176. package/translations/uk.json +60 -18
  177. package/translations/uz.json +60 -18
  178. package/translations/uz@Latn.json +60 -18
  179. package/translations/uz_UZ.json +60 -18
  180. package/translations/vi.json +60 -18
  181. package/translations/zh.json +60 -18
  182. package/translations/zh_CN.json +117 -75
  183. package/dist/1146.js.LICENSE.txt +0 -21
  184. package/dist/2352.js +0 -1
  185. package/dist/2352.js.map +0 -1
  186. package/dist/246.js +0 -1
  187. package/dist/246.js.map +0 -1
  188. package/dist/6525.js +0 -2
  189. package/dist/6525.js.map +0 -1
  190. package/dist/8556.js +0 -2
  191. package/dist/8556.js.map +0 -1
  192. package/dist/8638.js +0 -1
  193. package/dist/8638.js.map +0 -1
  194. package/dist/9968.js +0 -1
  195. package/dist/9968.js.map +0 -1
  196. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  197. package/src/invoice/payments/payments.component.test.tsx +0 -121
  198. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState, useEffect, useCallback } from 'react';
1
+ import React, { useMemo, useState, useCallback } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import fuzzy from 'fuzzy';
4
4
  import {
@@ -13,42 +13,38 @@ import {
13
13
  TableHead,
14
14
  TableHeader,
15
15
  TableRow,
16
- TableSelectRow,
17
16
  TableToolbarSearch,
18
17
  Tile,
19
18
  type DataTableRow,
20
19
  } from '@carbon/react';
21
20
  import { Edit } from '@carbon/react/icons';
22
- import { isDesktop, showModal, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
21
+ import {
22
+ isDesktop,
23
+ showModal,
24
+ useConfig,
25
+ useDebounce,
26
+ useLayoutType,
27
+ getCoreTranslation,
28
+ } from '@openmrs/esm-framework';
23
29
  import { type LineItem, type MappedBill } from '../types';
24
30
  import { convertToCurrency } from '../helpers';
31
+ import type { BillingConfig } from '../config-schema';
25
32
  import styles from './invoice-table.scss';
26
33
 
27
34
  type InvoiceTableProps = {
28
35
  bill: MappedBill;
29
- isSelectable?: boolean;
30
36
  isLoadingBill?: boolean;
31
- onSelectItem?: (selectedLineItems: LineItem[]) => void;
32
37
  };
33
38
 
34
- const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
39
+ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isLoadingBill }) => {
35
40
  const { t } = useTranslation();
36
- const { defaultCurrency, showEditBillButton } = useConfig();
41
+ const { defaultCurrency } = useConfig<BillingConfig>();
37
42
  const layout = useLayoutType();
38
43
  const lineItems = useMemo(() => bill?.lineItems ?? [], [bill?.lineItems]);
39
- const paidLineItems = useMemo(() => lineItems?.filter((item) => item.paymentStatus === 'PAID') ?? [], [lineItems]);
40
44
  const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
41
-
42
- const [selectedLineItems, setSelectedLineItems] = useState(paidLineItems ?? []);
43
45
  const [searchTerm, setSearchTerm] = useState('');
44
46
  const debouncedSearchTerm = useDebounce(searchTerm);
45
47
 
46
- useEffect(() => {
47
- if (onSelectItem) {
48
- onSelectItem(selectedLineItems);
49
- }
50
- }, [selectedLineItems, onSelectItem]);
51
-
52
48
  const filteredLineItems = useMemo(() => {
53
49
  if (!debouncedSearchTerm) {
54
50
  return lineItems;
@@ -65,14 +61,14 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
65
61
  }, [debouncedSearchTerm, lineItems]);
66
62
 
67
63
  const tableHeaders = [
68
- { header: 'No', key: 'no', width: 7 }, // Width as a percentage
69
- { header: 'Bill item', key: 'billItem', width: 25 },
70
- { header: 'Bill code', key: 'billCode', width: 20 },
71
- { header: 'Status', key: 'status', width: 25 },
72
- { header: 'Quantity', key: 'quantity', width: 15 },
73
- { header: 'Price', key: 'price', width: 24 },
74
- { header: 'Total', key: 'total', width: 15 },
75
- { header: t('actions', 'Actions'), key: 'actionButton' },
64
+ { header: t('number', 'No'), key: 'no', width: 7 }, // Width as a percentage
65
+ { header: t('billItem', 'Bill item'), key: 'billItem', width: 25 },
66
+ { header: t('billCode', 'Bill code'), key: 'billCode', width: 20 },
67
+ { header: t('status', 'Status'), key: 'status', width: 25 },
68
+ { header: t('quantity', 'Quantity'), key: 'quantity', width: 15 },
69
+ { header: t('price', 'Price'), key: 'price', width: 24 },
70
+ { header: t('total', 'Total'), key: 'total', width: 15 },
71
+ { header: getCoreTranslation('actions'), key: 'actionButton' },
76
72
  ];
77
73
 
78
74
  const handleSelectBillItem = useCallback(
@@ -100,24 +96,20 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
100
96
  total: item.price * item.quantity,
101
97
  actionButton: (
102
98
  <span>
103
- {showEditBillButton ? (
104
- <Button
105
- data-testid={`edit-button-${item.uuid}`}
106
- renderIcon={Edit}
107
- hasIconOnly
108
- kind="ghost"
109
- iconDescription={t('editThisBillItem', 'Edit this bill item')}
110
- tooltipPosition="left"
111
- onClick={() => handleSelectBillItem(item)}
112
- />
113
- ) : (
114
- '--'
115
- )}
99
+ <Button
100
+ data-testid={`edit-button-${item.uuid}`}
101
+ renderIcon={Edit}
102
+ hasIconOnly
103
+ kind="ghost"
104
+ iconDescription={t('editThisBillItem', 'Edit this bill item')}
105
+ tooltipPosition="left"
106
+ onClick={() => handleSelectBillItem(item)}
107
+ />
116
108
  </span>
117
109
  ),
118
110
  };
119
111
  }) ?? [],
120
- [filteredLineItems, bill?.receiptNumber, defaultCurrency, showEditBillButton, t, handleSelectBillItem],
112
+ [filteredLineItems, bill?.receiptNumber, defaultCurrency, t, handleSelectBillItem],
121
113
  );
122
114
 
123
115
  if (isLoadingBill) {
@@ -135,23 +127,10 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
135
127
  );
136
128
  }
137
129
 
138
- const handleRowSelection = (row: typeof DataTableRow, checked: boolean) => {
139
- const matchingRow = filteredLineItems.find((item) => item.uuid === row.id);
140
- let newSelectedLineItems;
141
-
142
- if (checked) {
143
- newSelectedLineItems = [...selectedLineItems, matchingRow];
144
- } else {
145
- newSelectedLineItems = selectedLineItems.filter((item) => item.uuid !== row.id);
146
- }
147
- setSelectedLineItems(newSelectedLineItems);
148
- onSelectItem(newSelectedLineItems);
149
- };
150
-
151
130
  return (
152
- <div className={styles.invoiceContainer}>
153
- <DataTable headers={tableHeaders} isSortable rows={tableRows} size={responsiveSize} useZebraStyles>
154
- {({ rows, headers, getRowProps, getSelectionProps, getTableProps, getToolbarProps }) => (
131
+ <>
132
+ <DataTable headers={tableHeaders} rows={tableRows} size={responsiveSize} useZebraStyles>
133
+ {({ rows, headers, getRowProps, getTableProps }) => (
155
134
  <TableContainer
156
135
  description={
157
136
  <span className={styles.tableDescription}>
@@ -172,7 +151,6 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
172
151
  className={`${styles.invoiceTable} billingTable`}>
173
152
  <TableHead>
174
153
  <TableRow>
175
- {rows.length > 1 && isSelectable ? <TableHeader /> : null}
176
154
  {headers.map((header) => (
177
155
  <TableHeader key={header.key}>{header.header}</TableHeader>
178
156
  ))}
@@ -186,18 +164,6 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
186
164
  {...getRowProps({
187
165
  row,
188
166
  })}>
189
- {rows.length > 1 && isSelectable && (
190
- <TableSelectRow
191
- aria-label="Select row"
192
- {...getSelectionProps({ row })}
193
- disabled={tableRows[index].status === 'PAID'}
194
- onChange={(checked: boolean) => handleRowSelection(row, checked)}
195
- checked={
196
- tableRows[index].status === 'PAID' ||
197
- Boolean(selectedLineItems?.find((item) => item?.uuid === row?.id))
198
- }
199
- />
200
- )}
201
167
  {row.cells.map((cell) => (
202
168
  <TableCell key={cell.id}>{cell.value}</TableCell>
203
169
  ))}
@@ -221,7 +187,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
221
187
  </Layer>
222
188
  </div>
223
189
  )}
224
- </div>
190
+ </>
225
191
  );
226
192
  };
227
193
 
@@ -29,11 +29,7 @@
29
29
 
30
30
  .headerContainer {
31
31
  background-color: colors.$gray-10;
32
- }
33
-
34
- .invoiceContainer {
35
- border: 1px solid $ui-03;
36
- }
32
+ }c
37
33
 
38
34
  .searchbox {
39
35
  input:focus {
@@ -1,43 +1,23 @@
1
1
  import React from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { render, screen, fireEvent, act } from '@testing-library/react';
4
- import { showModal } from '@openmrs/esm-framework';
5
- import InvoiceTable from './invoice-table.component';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen, act } from '@testing-library/react';
4
+ import { getDefaultsFromConfigSchema, showModal, useConfig } from '@openmrs/esm-framework';
6
5
  import { type MappedBill } from '../types';
6
+ import { configSchema, type BillingConfig } from '../config-schema';
7
+ import InvoiceTable from './invoice-table.component';
7
8
 
8
- // Mocking dependencies
9
- jest.mock('react-i18next', () => ({
10
- useTranslation: jest.fn(() => ({
11
- t: jest.fn((key, fallback) => fallback || key),
12
- i18n: { language: 'en' },
13
- })),
14
- }));
15
-
16
- jest.mock('@openmrs/esm-framework', () => ({
17
- showModal: jest.fn(),
18
- useConfig: jest.fn(() => ({
19
- defaultCurrency: 'USD',
20
- showEditBillButton: true,
21
- })),
22
- useDebounce: jest.fn((value) => value),
23
- useLayoutType: jest.fn(() => 'desktop'),
24
- isDesktop: jest.fn(() => true),
25
- }));
9
+ const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
26
10
 
27
11
  jest.mock('../helpers', () => ({
28
12
  convertToCurrency: jest.fn((price) => `USD ${price}`),
29
13
  }));
30
14
 
31
15
  describe('InvoiceTable', () => {
32
- const mockT = jest.fn((key) => key);
33
-
34
16
  beforeEach(() => {
35
- (useTranslation as jest.Mock).mockReturnValue({ t: mockT, i18n: { language: 'en' } });
36
- jest.useFakeTimers();
37
- });
38
-
39
- afterEach(() => {
40
- jest.useRealTimers();
17
+ mockUseConfig.mockReturnValue({
18
+ ...getDefaultsFromConfigSchema(configSchema),
19
+ defaultCurrency: 'USD',
20
+ });
41
21
  });
42
22
 
43
23
  const bill: MappedBill = {
@@ -103,11 +83,12 @@ describe('InvoiceTable', () => {
103
83
  expect(screen.getByTestId('receipt-number-0')).toHaveTextContent('12345');
104
84
  });
105
85
 
106
- it('renders the edit button and calls showModal when clicked', () => {
86
+ it('renders the edit button and calls showModal when clicked', async () => {
87
+ const user = userEvent.setup();
107
88
  render(<InvoiceTable bill={bill} />);
108
89
 
109
90
  const editButton = screen.getByTestId('edit-button-1');
110
- fireEvent.click(editButton);
91
+ await user.click(editButton);
111
92
  expect(showModal).toHaveBeenCalledWith('edit-bill-line-item-dialog', expect.anything());
112
93
  });
113
94
 
@@ -117,31 +98,23 @@ describe('InvoiceTable', () => {
117
98
  expect(screen.getByTestId('loader')).toBeInTheDocument();
118
99
  });
119
100
 
120
- it('filters line items based on the search term', () => {
101
+ it('filters line items based on the search term', async () => {
102
+ const user = userEvent.setup();
121
103
  render(<InvoiceTable bill={bill} />);
122
- const searchInput = screen.getByPlaceholderText('searchThisTable'); //
104
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
123
105
 
124
- fireEvent.change(searchInput, { target: { value: 'Item 2' } });
106
+ await user.type(searchInput, 'Item 2');
125
107
 
126
108
  expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
127
109
  expect(screen.getByText('Item 2')).toBeInTheDocument();
128
110
  });
129
111
 
130
- it('correctly handles row selection', () => {
131
- const onSelectItem = jest.fn();
132
- render(<InvoiceTable bill={bill} onSelectItem={onSelectItem} />);
133
-
134
- const checkboxes = screen.getAllByLabelText('Select row');
135
- fireEvent.click(checkboxes[0]);
136
-
137
- expect(onSelectItem).toHaveBeenCalledWith([bill.lineItems[0]]);
138
- });
139
-
140
- it('resets isRedirecting to false after timeout', () => {
112
+ it('resets isRedirecting to false after timeout', async () => {
113
+ const user = userEvent.setup();
141
114
  render(<InvoiceTable bill={bill} />);
142
115
 
143
116
  const button = screen.getByTestId('edit-button-1');
144
- fireEvent.click(button);
117
+ await user.click(button);
145
118
  act(() => {
146
119
  jest.advanceTimersByTime(1000);
147
120
  });
@@ -4,15 +4,15 @@ import { 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';
7
- import { ExtensionSlot, useConfig, usePatient } from '@openmrs/esm-framework';
8
- import { ErrorState } from '@openmrs/esm-patient-common-lib';
9
- import { convertToCurrency } from '../helpers';
10
- import { type LineItem } from '../types';
11
- import { useBill } from '../billing.resource';
7
+ import { ExtensionSlot, showSnackbar, useConfig, usePatient } from '@openmrs/esm-framework';
12
8
  import InvoiceTable from './invoice-table.component';
13
9
  import Payments from './payments/payments.component';
14
10
  import PrintReceipt from './printable-invoice/print-receipt.component';
15
11
  import PrintableInvoice from './printable-invoice/printable-invoice.component';
12
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
13
+ import { convertToCurrency } from '../helpers';
14
+ import { useBill, useDefaultFacility } from '../billing.resource';
15
+ import type { BillingConfig } from '../config-schema';
16
16
  import styles from './invoice.scss';
17
17
 
18
18
  interface InvoiceDetailsProps {
@@ -22,25 +22,20 @@ interface InvoiceDetailsProps {
22
22
 
23
23
  const Invoice: React.FC = () => {
24
24
  const { t } = useTranslation();
25
+ const { data } = useDefaultFacility();
25
26
  const { billUuid, patientUuid } = useParams();
26
27
  const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid);
27
28
  const { bill, isLoading: isLoadingBill, error, mutate } = useBill(billUuid);
28
29
  const [isPrinting, setIsPrinting] = useState(false);
29
- const [selectedLineItems, setSelectedLineItems] = useState<LineItem[]>([]);
30
30
  const componentRef = useRef<HTMLDivElement>(null);
31
31
  const onBeforeGetContentResolve = useRef<(() => void) | null>(null);
32
- const { defaultCurrency } = useConfig();
33
- const handleSelectItem = (lineItems: LineItem[]) => {
34
- setSelectedLineItems(lineItems);
35
- };
32
+ const { defaultCurrency } = useConfig<BillingConfig>();
36
33
 
37
34
  const handleAfterPrint = useCallback(() => {
38
35
  onBeforeGetContentResolve.current = null;
39
36
  setIsPrinting(false);
40
37
  }, []);
41
38
 
42
- const reactToPrintContent = useCallback(() => componentRef.current, []);
43
-
44
39
  const handleOnBeforeGetContent = useCallback(() => {
45
40
  return new Promise<void>((resolve) => {
46
41
  if (patient && bill) {
@@ -51,11 +46,17 @@ const Invoice: React.FC = () => {
51
46
  }, [bill, patient]);
52
47
 
53
48
  const handlePrint = useReactToPrint({
54
- content: reactToPrintContent,
49
+ contentRef: componentRef,
55
50
  documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
56
- onBeforeGetContent: handleOnBeforeGetContent,
51
+ onBeforePrint: handleOnBeforeGetContent,
57
52
  onAfterPrint: handleAfterPrint,
58
- removeAfterPrint: true,
53
+ preserveAfterPrint: false,
54
+ onPrintError: (_, error) =>
55
+ showSnackbar({
56
+ title: t('errorPrintingInvoice', 'Error printing invoice'),
57
+ kind: 'error',
58
+ subtitle: error.message,
59
+ }),
59
60
  });
60
61
 
61
62
  useEffect(() => {
@@ -64,11 +65,14 @@ const Invoice: React.FC = () => {
64
65
  }
65
66
  }, [isPrinting]);
66
67
 
67
- useEffect(() => {
68
- const unPaidLineItems = bill?.lineItems?.filter((item) => item.paymentStatus === 'PENDING') ?? [];
69
- setSelectedLineItems(unPaidLineItems);
70
- }, [bill?.lineItems]);
71
-
68
+ // Do not remove this comment. Adds the translation keys for the invoice details
69
+ /**
70
+ * t('totalAmount', 'Total Amount')
71
+ * t('amountTendered', 'Amount Tendered')
72
+ * t('invoiceNumber', 'Invoice Number')
73
+ * t('dateAndTime', 'Date And Time')
74
+ * t('invoiceStatus', 'Invoice Status')
75
+ */
72
76
  const invoiceDetails = {
73
77
  'Total Amount': convertToCurrency(bill?.totalAmount, defaultCurrency),
74
78
  'Amount Tendered': convertToCurrency(bill?.tenderedAmount, defaultCurrency),
@@ -77,14 +81,14 @@ const Invoice: React.FC = () => {
77
81
  'Invoice Status': bill?.status,
78
82
  };
79
83
 
80
- if (isLoadingPatient && isLoadingBill) {
84
+ if (isLoadingPatient || isLoadingBill) {
81
85
  return (
82
86
  <div className={styles.invoiceContainer}>
83
87
  <InlineLoading
84
88
  className={styles.loader}
85
89
  status="active"
86
90
  iconDescription="Loading"
87
- description="Loading patient header..."
91
+ description={t('loadingBillInfo', 'Loading bill information...')}
88
92
  />
89
93
  </div>
90
94
  );
@@ -109,7 +113,7 @@ const Invoice: React.FC = () => {
109
113
  </section>
110
114
  <div>
111
115
  <Button
112
- disabled={isPrinting}
116
+ disabled={isPrinting || isLoadingPatient || isLoadingBill}
113
117
  onClick={handlePrint}
114
118
  renderIcon={(props) => <Printer size={24} {...props} />}
115
119
  iconDescription="Print bill"
@@ -120,20 +124,23 @@ const Invoice: React.FC = () => {
120
124
  </div>
121
125
  </div>
122
126
 
123
- <InvoiceTable bill={bill} isLoadingBill={isLoadingBill} onSelectItem={handleSelectItem} />
124
- <Payments bill={bill} mutate={mutate} selectedLineItems={selectedLineItems} />
127
+ <InvoiceTable bill={bill} isLoadingBill={isLoadingBill} />
128
+ <Payments bill={bill} mutate={mutate} />
125
129
 
126
- <div className={styles.printContainer} ref={componentRef}>
127
- {isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoadingPatient} />}
128
- </div>
130
+ {bill && patient && (
131
+ <div className={styles.printContainer}>
132
+ <PrintableInvoice bill={bill} patient={patient} defaultFacility={data} componentRef={componentRef} />
133
+ </div>
134
+ )}
129
135
  </div>
130
136
  );
131
137
  };
132
138
 
133
139
  function InvoiceDetails({ label, value }: InvoiceDetailsProps) {
140
+ const { t } = useTranslation();
134
141
  return (
135
142
  <div>
136
- <h1 className={styles.label}>{label}</h1>
143
+ <h1 className={styles.label}>{t(label)}</h1>
137
144
  <span className={styles.value}>{value}</span>
138
145
  </div>
139
146
  );
@@ -78,16 +78,19 @@
78
78
  color: colors.$cool-gray-90;
79
79
  }
80
80
 
81
- @media screen {
82
- .printContainer {
83
- background-color: colors.$white;
84
- display: none;
81
+ .printContainer {
82
+ background-color: colors.$white;
83
+ display: none;
84
+
85
+ @media print {
86
+ display: block !important;
85
87
  }
86
88
  }
87
89
 
88
90
  @media print {
89
91
  html,
90
92
  body {
93
+ display: block !important;
91
94
  background-color: colors.$white !important;
92
95
  }
93
96
  }
@@ -1,24 +1,21 @@
1
1
  import React from 'react';
2
- import { screen, render } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
3
+ import { screen, render } from '@testing-library/react';
4
4
  import { useReactToPrint } from 'react-to-print';
5
- import { mockBill } from '../../__mocks__/bills.mock';
5
+ import { getDefaultsFromConfigSchema, useConfig, usePatient } from '@openmrs/esm-framework';
6
+ import { configSchema, type BillingConfig } from '../config-schema';
7
+ import { mockBill, mockPatient } from '../../__mocks__/bills.mock';
6
8
  import { useBill, processBillPayment } from '../billing.resource';
7
9
  import { usePaymentModes } from './payments/payment.resource';
8
10
  import Invoice from './invoice.component';
9
11
 
12
+ const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
13
+
10
14
  // Mock convertToCurrency
11
15
  jest.mock('../helpers/functions', () => ({
12
16
  convertToCurrency: jest.fn((amount) => `USD ${amount}`),
13
17
  }));
14
18
 
15
- // Mock i18next
16
- jest.mock('react-i18next', () => ({
17
- useTranslation: () => ({
18
- t: (key: string) => key,
19
- }),
20
- }));
21
-
22
19
  // Set window.i18next
23
20
  window.i18next = {
24
21
  language: 'en',
@@ -78,40 +75,9 @@ jest.mock('react-to-print', () => ({
78
75
  useReactToPrint: jest.fn(),
79
76
  }));
80
77
 
81
- // Mock OpenMRS framework
82
- jest.mock('@openmrs/esm-framework', () => ({
83
- showSnackbar: jest.fn(),
84
- useLayoutType: jest.fn(() => 'desktop'),
85
- isDesktop: jest.fn(() => true),
86
- useConfig: jest.fn(() => ({
87
- defaultCurrency: 'USD',
88
- })),
89
- formatDate: jest.fn((date) => date?.toString() ?? ''),
90
- ExtensionSlot: jest.fn(({ children }) => <div data-testid="extension-slot">{children}</div>),
91
- usePatient: jest.fn().mockReturnValue({
92
- patient: {
93
- id: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
94
- name: [{ given: ['John'], family: 'Doe' }],
95
- },
96
- patientUuid: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
97
- isLoading: false,
98
- error: null,
99
- }),
100
- createGlobalStore: jest.fn(),
101
- getGlobalStore: jest.fn(() => ({
102
- subscribe: jest.fn(),
103
- getState: jest.fn(),
104
- setState: jest.fn(),
105
- })),
106
- }));
107
-
108
- // Mock patient common lib
109
- jest.mock('@openmrs/esm-patient-common-lib', () => ({
110
- ErrorState: jest.fn(({ error }) => <div data-testid="error-state">Error: {error?.message || error}</div>),
111
- }));
112
-
113
78
  describe('Invoice', () => {
114
79
  const mockedBill = useBill as jest.Mock;
80
+ const mockedPatient = usePatient as jest.Mock;
115
81
  const mockedProcessBillPayment = processBillPayment as jest.Mock;
116
82
  const mockedUsePaymentModes = usePaymentModes as jest.Mock;
117
83
  const mockedUseReactToPrint = useReactToPrint as jest.Mock;
@@ -144,6 +110,14 @@ describe('Invoice', () => {
144
110
  mutate: jest.fn(),
145
111
  });
146
112
 
113
+ mockedPatient.mockReturnValue({
114
+ patient: mockPatient,
115
+ isLoading: false,
116
+ error: null,
117
+ isValidating: false,
118
+ mutate: jest.fn(),
119
+ });
120
+
147
121
  mockedUsePaymentModes.mockReturnValue({
148
122
  paymentModes: [
149
123
  { uuid: 'cash-uuid', name: 'Cash', description: 'Cash Method', retired: false },
@@ -154,15 +128,13 @@ describe('Invoice', () => {
154
128
  mutate: jest.fn(),
155
129
  });
156
130
 
131
+ mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
132
+
157
133
  // Setup print handler mock
158
134
  const printHandler = jest.fn();
159
135
  mockedUseReactToPrint.mockReturnValue(printHandler);
160
136
  });
161
137
 
162
- afterEach(() => {
163
- jest.clearAllMocks();
164
- });
165
-
166
138
  it('should render error state correctly', () => {
167
139
  mockedBill.mockReturnValue({
168
140
  bill: null,
@@ -173,8 +145,8 @@ describe('Invoice', () => {
173
145
  });
174
146
 
175
147
  render(<Invoice />);
176
- expect(screen.getByTestId('error-state')).toBeInTheDocument();
177
- expect(screen.getByText(/Test error/i)).toBeInTheDocument();
148
+ expect(screen.getByText(/Invoice error/i)).toBeInTheDocument();
149
+ expect(screen.getByText(/Error/)).toBeInTheDocument();
178
150
  });
179
151
 
180
152
  it('should render invoice details correctly', () => {
@@ -265,7 +237,9 @@ describe('Invoice', () => {
265
237
 
266
238
  it('should show patient information correctly', () => {
267
239
  render(<Invoice />);
268
- expect(screen.getByTestId('extension-slot')).toBeInTheDocument();
240
+ // Check that the invoice details are rendered
241
+ expect(screen.getByText('Invoice Number')).toBeInTheDocument();
242
+ expect(screen.getByText('RCPT-001')).toBeInTheDocument();
269
243
  });
270
244
 
271
245
  // Add more test cases as needed for specific features or edge cases