@openmrs/esm-billing-app 1.0.2-pre.78 → 1.0.2-pre.786

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 (203) hide show
  1. package/.eslintrc +16 -2
  2. package/README.md +54 -9
  3. package/__mocks__/bills.mock.ts +12 -0
  4. package/__mocks__/react-i18next.js +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1146.js +1 -2
  7. package/dist/1146.js.map +1 -1
  8. package/dist/1197.js +1 -1
  9. package/dist/1856.js +1 -0
  10. package/dist/1856.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +2 -0
  13. package/dist/2177.js.LICENSE.txt +9 -0
  14. package/dist/2177.js.map +1 -0
  15. package/dist/2524.js +1 -0
  16. package/dist/2524.js.map +1 -0
  17. package/dist/2690.js +1 -1
  18. package/dist/3041.js +1 -0
  19. package/dist/3041.js.map +1 -0
  20. package/dist/3099.js +1 -1
  21. package/dist/3584.js +1 -1
  22. package/dist/4055.js +1 -1
  23. package/dist/4132.js +1 -1
  24. package/dist/4225.js +1 -0
  25. package/dist/4225.js.map +1 -0
  26. package/dist/4300.js +1 -1
  27. package/dist/4335.js +1 -1
  28. package/dist/4618.js +1 -1
  29. package/dist/4652.js +1 -1
  30. package/dist/4724.js +1 -0
  31. package/dist/4724.js.map +1 -0
  32. package/dist/4739.js +1 -1
  33. package/dist/4739.js.map +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 -0
  38. package/dist/5422.js.map +1 -0
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6468.js +1 -1
  43. package/dist/6540.js +1 -1
  44. package/dist/6540.js.map +1 -1
  45. package/dist/6606.js +1 -0
  46. package/dist/6606.js.map +1 -0
  47. package/dist/6679.js +1 -1
  48. package/dist/6840.js +1 -1
  49. package/dist/6859.js +1 -1
  50. package/dist/7097.js +1 -1
  51. package/dist/7159.js +1 -1
  52. package/dist/723.js +1 -1
  53. package/dist/7452.js +2 -0
  54. package/dist/7452.js.map +1 -0
  55. package/dist/7617.js +1 -1
  56. package/dist/795.js +1 -1
  57. package/dist/8163.js +1 -1
  58. package/dist/8349.js +1 -1
  59. package/dist/8618.js +1 -1
  60. package/dist/890.js +1 -1
  61. package/dist/8930.js +2 -0
  62. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  63. package/dist/8930.js.map +1 -0
  64. package/dist/9214.js +1 -1
  65. package/dist/942.js +1 -0
  66. package/dist/942.js.map +1 -0
  67. package/dist/9538.js +1 -1
  68. package/dist/9569.js +1 -1
  69. package/dist/961.js +1 -1
  70. package/dist/961.js.map +1 -1
  71. package/dist/986.js +1 -1
  72. package/dist/9879.js +1 -1
  73. package/dist/9895.js +1 -1
  74. package/dist/9900.js +1 -1
  75. package/dist/9913.js +1 -1
  76. package/dist/main.js +1 -1
  77. package/dist/main.js.map +1 -1
  78. package/dist/openmrs-esm-billing-app.js +1 -1
  79. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +368 -262
  80. package/dist/openmrs-esm-billing-app.js.map +1 -1
  81. package/dist/routes.json +1 -1
  82. package/e2e/README.md +19 -18
  83. package/e2e/core/test.ts +1 -1
  84. package/e2e/fixtures/api.ts +1 -1
  85. package/e2e/specs/sample-test.spec.ts +0 -1
  86. package/e2e/support/github/Dockerfile +1 -1
  87. package/package.json +13 -10
  88. package/src/bill-history/bill-history.component.tsx +17 -25
  89. package/src/bill-history/bill-history.scss +4 -94
  90. package/src/bill-history/bill-history.test.tsx +37 -78
  91. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  92. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +100 -78
  93. package/src/bill-item-actions/edit-bill-item.test.tsx +116 -31
  94. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  95. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  96. package/src/billable-services/billable-service.resource.ts +17 -9
  97. package/src/billable-services/billable-services-home.component.tsx +1 -1
  98. package/src/billable-services/billable-services.component.tsx +142 -145
  99. package/src/billable-services/billable-services.scss +3 -0
  100. package/src/billable-services/billable-services.test.tsx +2 -45
  101. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  102. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +18 -192
  103. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  104. package/src/billable-services/create-edit/add-billable-service.component.tsx +345 -298
  105. package/src/billable-services/create-edit/add-billable-service.scss +5 -6
  106. package/src/billable-services/create-edit/add-billable-service.test.tsx +37 -36
  107. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
  108. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  109. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  110. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  111. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  112. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  113. package/src/billing-form/billing-checkin-form.test.tsx +97 -24
  114. package/src/billing-form/billing-form.component.tsx +216 -269
  115. package/src/billing-form/billing-form.scss +143 -0
  116. package/src/billing.resource.ts +16 -19
  117. package/src/bills-table/bills-table.test.tsx +98 -54
  118. package/src/config-schema.ts +52 -24
  119. package/src/dashboard.meta.ts +4 -2
  120. package/src/helpers/functions.ts +5 -4
  121. package/src/index.ts +17 -6
  122. package/src/invoice/invoice-table.component.tsx +35 -69
  123. package/src/invoice/invoice-table.scss +1 -5
  124. package/src/invoice/invoice-table.test.tsx +273 -62
  125. package/src/invoice/invoice.component.tsx +36 -29
  126. package/src/invoice/invoice.scss +7 -4
  127. package/src/invoice/invoice.test.tsx +324 -120
  128. package/src/invoice/payments/payment-form/payment-form.component.tsx +31 -29
  129. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  130. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  131. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  132. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  133. package/src/invoice/payments/payments.component.tsx +53 -65
  134. package/src/invoice/payments/payments.test.tsx +282 -0
  135. package/src/invoice/payments/utils.ts +5 -23
  136. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  137. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  138. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  139. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  140. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  141. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  142. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  143. package/src/left-panel-link.test.tsx +1 -4
  144. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  145. package/src/modal/require-payment-modal.test.tsx +27 -22
  146. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  147. package/src/routes.json +22 -2
  148. package/src/types/index.ts +26 -17
  149. package/translations/am.json +70 -21
  150. package/translations/ar.json +70 -21
  151. package/translations/ar_SY.json +70 -21
  152. package/translations/bn.json +75 -26
  153. package/translations/de.json +70 -21
  154. package/translations/en.json +70 -21
  155. package/translations/en_US.json +70 -21
  156. package/translations/es.json +70 -21
  157. package/translations/es_MX.json +70 -21
  158. package/translations/fr.json +83 -34
  159. package/translations/he.json +70 -21
  160. package/translations/hi.json +70 -21
  161. package/translations/hi_IN.json +70 -21
  162. package/translations/id.json +70 -21
  163. package/translations/it.json +105 -56
  164. package/translations/ka.json +70 -21
  165. package/translations/km.json +70 -21
  166. package/translations/ku.json +70 -21
  167. package/translations/ky.json +70 -21
  168. package/translations/lg.json +70 -21
  169. package/translations/ne.json +70 -21
  170. package/translations/pl.json +70 -21
  171. package/translations/pt.json +70 -21
  172. package/translations/pt_BR.json +70 -21
  173. package/translations/qu.json +70 -21
  174. package/translations/ro_RO.json +214 -165
  175. package/translations/ru_RU.json +70 -21
  176. package/translations/si.json +70 -21
  177. package/translations/sw.json +70 -21
  178. package/translations/sw_KE.json +70 -21
  179. package/translations/tr.json +70 -21
  180. package/translations/tr_TR.json +70 -21
  181. package/translations/uk.json +70 -21
  182. package/translations/uz.json +70 -21
  183. package/translations/uz@Latn.json +70 -21
  184. package/translations/uz_UZ.json +70 -21
  185. package/translations/vi.json +70 -21
  186. package/translations/zh.json +70 -21
  187. package/translations/zh_CN.json +125 -76
  188. package/dist/1146.js.LICENSE.txt +0 -21
  189. package/dist/2352.js +0 -1
  190. package/dist/2352.js.map +0 -1
  191. package/dist/246.js +0 -1
  192. package/dist/246.js.map +0 -1
  193. package/dist/6525.js +0 -2
  194. package/dist/6525.js.map +0 -1
  195. package/dist/8556.js +0 -2
  196. package/dist/8556.js.map +0 -1
  197. package/dist/8638.js +0 -1
  198. package/dist/8638.js.map +0 -1
  199. package/dist/9968.js +0 -1
  200. package/dist/9968.js.map +0 -1
  201. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  202. package/src/invoice/payments/payments.component.test.tsx +0 -121
  203. /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(
@@ -97,27 +93,23 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
97
93
  status: item.paymentStatus,
98
94
  quantity: item.quantity,
99
95
  price: convertToCurrency(item.price, defaultCurrency),
100
- total: item.price * item.quantity,
96
+ total: convertToCurrency(item.price * item.quantity, defaultCurrency),
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,46 +1,20 @@
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, waitFor } 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>);
10
+ const mockShowModal = jest.mocked(showModal);
26
11
 
27
12
  jest.mock('../helpers', () => ({
28
13
  convertToCurrency: jest.fn((price) => `USD ${price}`),
29
14
  }));
30
15
 
31
16
  describe('InvoiceTable', () => {
32
- const mockT = jest.fn((key) => key);
33
-
34
- beforeEach(() => {
35
- (useTranslation as jest.Mock).mockReturnValue({ t: mockT, i18n: { language: 'en' } });
36
- jest.useFakeTimers();
37
- });
38
-
39
- afterEach(() => {
40
- jest.useRealTimers();
41
- });
42
-
43
- const bill: MappedBill = {
17
+ const defaultBill: MappedBill = {
44
18
  uuid: 'bill-uuid',
45
19
  id: 123,
46
20
  patientUuid: 'patient-uuid',
@@ -95,57 +69,294 @@ describe('InvoiceTable', () => {
95
69
  tenderedAmount: 300,
96
70
  };
97
71
 
98
- it('renders the table and displays line items correctly', () => {
99
- render(<InvoiceTable bill={bill} />);
72
+ beforeEach(() => {
73
+ mockUseConfig.mockReturnValue({
74
+ ...getDefaultsFromConfigSchema(configSchema),
75
+ defaultCurrency: 'USD',
76
+ });
77
+ });
78
+
79
+ it('should render table headers correctly', () => {
80
+ render(<InvoiceTable bill={defaultBill} />);
81
+
82
+ expect(screen.getByText(/line items/i)).toBeInTheDocument();
83
+ expect(screen.getByText(/items to be billed/i)).toBeInTheDocument();
84
+ expect(screen.getByRole('columnheader', { name: /no/i })).toBeInTheDocument();
85
+ expect(screen.getByRole('columnheader', { name: /bill item/i })).toBeInTheDocument();
86
+ expect(screen.getByRole('columnheader', { name: /bill code/i })).toBeInTheDocument();
87
+ expect(screen.getByRole('columnheader', { name: /status/i })).toBeInTheDocument();
88
+ expect(screen.getByRole('columnheader', { name: /quantity/i })).toBeInTheDocument();
89
+ expect(screen.getByRole('columnheader', { name: /price/i })).toBeInTheDocument();
90
+ expect(screen.getByRole('columnheader', { name: /total/i })).toBeInTheDocument();
91
+ });
92
+
93
+ it('should render line items correctly', () => {
94
+ render(<InvoiceTable bill={defaultBill} />);
100
95
 
101
96
  expect(screen.getByText('Item 1')).toBeInTheDocument();
102
97
  expect(screen.getByText('Item 2')).toBeInTheDocument();
103
98
  expect(screen.getByTestId('receipt-number-0')).toHaveTextContent('12345');
99
+ expect(screen.getByTestId('receipt-number-1')).toHaveTextContent('12345');
100
+ });
101
+
102
+ it('should display loading skeleton when bill is loading', () => {
103
+ render(<InvoiceTable bill={defaultBill} isLoadingBill={true} />);
104
+
105
+ expect(screen.getByTestId('loader')).toBeInTheDocument();
106
+ expect(screen.queryByText(/line items/i)).not.toBeInTheDocument();
104
107
  });
105
108
 
106
- it('renders the edit button and calls showModal when clicked', () => {
107
- render(<InvoiceTable bill={bill} />);
109
+ it('should display payment status for each line item', () => {
110
+ render(<InvoiceTable bill={defaultBill} />);
111
+
112
+ expect(screen.getByText('PAID')).toBeInTheDocument();
113
+ expect(screen.getByText('PENDING')).toBeInTheDocument();
114
+ });
115
+
116
+ it('should display correct quantities', () => {
117
+ render(<InvoiceTable bill={defaultBill} />);
118
+
119
+ // Item 1 has quantity 1, Item 2 has quantity 2
120
+ const rows = screen.getAllByRole('row');
121
+ expect(rows).toHaveLength(3); // Header row + 2 data rows
122
+ });
123
+
124
+ it('should calculate and display line item totals correctly', () => {
125
+ const billWithCalculation: MappedBill = {
126
+ ...defaultBill,
127
+ lineItems: [
128
+ {
129
+ uuid: '1',
130
+ item: 'Service A',
131
+ paymentStatus: 'PENDING',
132
+ quantity: 3,
133
+ price: 100,
134
+ display: '',
135
+ voided: false,
136
+ voidReason: '',
137
+ billableService: 'Service A',
138
+ priceName: '',
139
+ priceUuid: '',
140
+ lineItemOrder: 0,
141
+ resourceVersion: '',
142
+ },
143
+ ],
144
+ };
145
+
146
+ render(<InvoiceTable bill={billWithCalculation} />);
147
+
148
+ // Total should be 3 * 100 = 300
149
+ expect(screen.getByText('USD 300')).toBeInTheDocument();
150
+ });
151
+
152
+ it('should render edit buttons for all line items', () => {
153
+ render(<InvoiceTable bill={defaultBill} />);
154
+
155
+ const editButton1 = screen.getByTestId('edit-button-1');
156
+ const editButton2 = screen.getByTestId('edit-button-2');
157
+
158
+ expect(editButton1).toBeInTheDocument();
159
+ expect(editButton2).toBeInTheDocument();
160
+ });
161
+
162
+ it('should open edit modal when edit button is clicked', async () => {
163
+ const user = userEvent.setup();
164
+ render(<InvoiceTable bill={defaultBill} />);
108
165
 
109
166
  const editButton = screen.getByTestId('edit-button-1');
110
- fireEvent.click(editButton);
111
- expect(showModal).toHaveBeenCalledWith('edit-bill-line-item-dialog', expect.anything());
167
+ await user.click(editButton);
168
+
169
+ expect(mockShowModal).toHaveBeenCalledTimes(1);
170
+ expect(mockShowModal).toHaveBeenCalledWith(
171
+ 'edit-bill-line-item-dialog',
172
+ expect.objectContaining({
173
+ bill: defaultBill,
174
+ item: expect.objectContaining({ uuid: '1' }),
175
+ }),
176
+ );
112
177
  });
113
178
 
114
- it('displays a skeleton loader when the bill is loading', () => {
115
- render(<InvoiceTable bill={bill} isLoadingBill={true} />);
179
+ it('should filter line items based on search term', async () => {
180
+ const user = userEvent.setup();
181
+ render(<InvoiceTable bill={defaultBill} />);
116
182
 
117
- expect(screen.getByTestId('loader')).toBeInTheDocument();
183
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
184
+ await user.type(searchInput, 'Item 2');
185
+
186
+ await waitFor(() => {
187
+ expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
188
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
189
+ });
118
190
  });
119
191
 
120
- it('filters line items based on the search term', () => {
121
- render(<InvoiceTable bill={bill} />);
122
- const searchInput = screen.getByPlaceholderText('searchThisTable'); //
192
+ it('should show all items when search is cleared', async () => {
193
+ const user = userEvent.setup();
194
+ render(<InvoiceTable bill={defaultBill} />);
123
195
 
124
- fireEvent.change(searchInput, { target: { value: 'Item 2' } });
196
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
125
197
 
126
- expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
127
- expect(screen.getByText('Item 2')).toBeInTheDocument();
198
+ // Search for Item 1
199
+ await user.type(searchInput, 'Item 1');
200
+
201
+ await waitFor(() => {
202
+ expect(screen.queryByText('Item 2')).not.toBeInTheDocument();
203
+ });
204
+
205
+ // Clear search
206
+ await user.clear(searchInput);
207
+
208
+ await waitFor(() => {
209
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
210
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
211
+ });
128
212
  });
129
213
 
130
- it('correctly handles row selection', () => {
131
- const onSelectItem = jest.fn();
132
- render(<InvoiceTable bill={bill} onSelectItem={onSelectItem} />);
214
+ it('should display empty state when no line items exist', () => {
215
+ const emptyBill: MappedBill = {
216
+ ...defaultBill,
217
+ lineItems: [],
218
+ };
133
219
 
134
- const checkboxes = screen.getAllByLabelText('Select row');
135
- fireEvent.click(checkboxes[0]);
220
+ render(<InvoiceTable bill={emptyBill} />);
136
221
 
137
- expect(onSelectItem).toHaveBeenCalledWith([bill.lineItems[0]]);
222
+ expect(screen.getByText(/no matching items to display/i)).toBeInTheDocument();
223
+ expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
138
224
  });
139
225
 
140
- it('resets isRedirecting to false after timeout', () => {
141
- render(<InvoiceTable bill={bill} />);
226
+ it('should show empty state when search has no results', async () => {
227
+ const user = userEvent.setup();
228
+ render(<InvoiceTable bill={defaultBill} />);
229
+
230
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
231
+ await user.type(searchInput, 'NonexistentItem');
142
232
 
143
- const button = screen.getByTestId('edit-button-1');
144
- fireEvent.click(button);
145
- act(() => {
146
- jest.advanceTimersByTime(1000);
233
+ await waitFor(() => {
234
+ expect(screen.getByText(/no matching items to display/i)).toBeInTheDocument();
235
+ expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
147
236
  });
237
+ });
238
+
239
+ it('should handle line items with zero price', () => {
240
+ const billWithZeroPrice: MappedBill = {
241
+ ...defaultBill,
242
+ lineItems: [
243
+ {
244
+ uuid: '1',
245
+ item: 'Free Service',
246
+ paymentStatus: 'PAID',
247
+ quantity: 1,
248
+ price: 0,
249
+ display: '',
250
+ voided: false,
251
+ voidReason: '',
252
+ billableService: 'Free Service',
253
+ priceName: '',
254
+ priceUuid: '',
255
+ lineItemOrder: 0,
256
+ resourceVersion: '',
257
+ },
258
+ ],
259
+ };
260
+
261
+ render(<InvoiceTable bill={billWithZeroPrice} />);
262
+
263
+ // USD 0 appears for both price and total
264
+ expect(screen.getAllByText('USD 0').length).toBeGreaterThan(0);
265
+ });
266
+
267
+ it('should handle line items with zero quantity', () => {
268
+ const billWithZeroQuantity: MappedBill = {
269
+ ...defaultBill,
270
+ lineItems: [
271
+ {
272
+ uuid: '1',
273
+ item: 'Service',
274
+ paymentStatus: 'PENDING',
275
+ quantity: 0,
276
+ price: 100,
277
+ display: '',
278
+ voided: false,
279
+ voidReason: '',
280
+ billableService: 'Service',
281
+ priceName: '',
282
+ priceUuid: '',
283
+ lineItemOrder: 0,
284
+ resourceVersion: '',
285
+ },
286
+ ],
287
+ };
288
+
289
+ render(<InvoiceTable bill={billWithZeroQuantity} />);
290
+
291
+ // Total should be 0 * 100 = 0
292
+ expect(screen.getByText('USD 0')).toBeInTheDocument();
293
+ });
294
+
295
+ it('should use billableService name when available, otherwise use item name', () => {
296
+ const billWithBillableService: MappedBill = {
297
+ ...defaultBill,
298
+ lineItems: [
299
+ {
300
+ uuid: '1',
301
+ item: 'Item Name',
302
+ billableService: 'Billable Service Name',
303
+ paymentStatus: 'PAID',
304
+ quantity: 1,
305
+ price: 100,
306
+ display: '',
307
+ voided: false,
308
+ voidReason: '',
309
+ priceName: '',
310
+ priceUuid: '',
311
+ lineItemOrder: 0,
312
+ resourceVersion: '',
313
+ },
314
+ {
315
+ uuid: '2',
316
+ item: 'Item Without Billable',
317
+ billableService: '',
318
+ paymentStatus: 'PENDING',
319
+ quantity: 1,
320
+ price: 200,
321
+ display: '',
322
+ voided: false,
323
+ voidReason: '',
324
+ priceName: '',
325
+ priceUuid: '',
326
+ lineItemOrder: 1,
327
+ resourceVersion: '',
328
+ },
329
+ ],
330
+ };
331
+
332
+ render(<InvoiceTable bill={billWithBillableService} />);
333
+
334
+ expect(screen.getByText('Billable Service Name')).toBeInTheDocument();
335
+ expect(screen.getByText('Item Without Billable')).toBeInTheDocument();
336
+ });
337
+
338
+ it('should display line item numbers starting from 1', () => {
339
+ render(<InvoiceTable bill={defaultBill} />);
340
+
341
+ // Check the table body for numbered rows
342
+ const rows = screen.getAllByRole('row');
343
+ // First row is header, so data rows start at index 1
344
+ expect(rows.length).toBeGreaterThan(2);
345
+ });
346
+
347
+ it('should pass correct currency to convertToCurrency helper', () => {
348
+ render(<InvoiceTable bill={defaultBill} />);
349
+
350
+ // Verify prices are formatted with USD - multiple occurrences expected
351
+ expect(screen.getAllByText('USD 100').length).toBeGreaterThan(0);
352
+ expect(screen.getAllByText('USD 200').length).toBeGreaterThan(0);
353
+ });
354
+
355
+ it('should render search input in expanded state', () => {
356
+ render(<InvoiceTable bill={defaultBill} />);
148
357
 
149
- expect(button).not.toBeDisabled();
358
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
359
+ expect(searchInput).toBeInTheDocument();
360
+ expect(searchInput).toBeVisible();
150
361
  });
151
362
  });