@openmrs/esm-billing-app 1.0.1-pre.98 → 1.0.2-pre.58

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 (224) hide show
  1. package/.eslintignore +0 -1
  2. package/.eslintrc +33 -24
  3. package/.husky/pre-commit +1 -1
  4. package/.turbo.json +1 -1
  5. package/.tx/config +11 -0
  6. package/README.md +111 -1
  7. package/dist/1119.js +1 -0
  8. package/dist/1197.js +1 -0
  9. package/dist/1362.js +1 -0
  10. package/dist/1362.js.map +1 -0
  11. package/dist/2146.js +1 -0
  12. package/dist/2690.js +1 -0
  13. package/dist/3029.js +2 -0
  14. package/dist/3029.js.LICENSE.txt +7 -0
  15. package/dist/3029.js.map +1 -0
  16. package/dist/3099.js +1 -0
  17. package/dist/3511.js +1 -0
  18. package/dist/3511.js.map +1 -0
  19. package/dist/3584.js +1 -0
  20. package/dist/4055.js +1 -0
  21. package/dist/4132.js +1 -0
  22. package/dist/4225.js +1 -0
  23. package/dist/4225.js.map +1 -0
  24. package/dist/4300.js +1 -0
  25. package/dist/4335.js +1 -0
  26. package/dist/4618.js +1 -0
  27. package/dist/4652.js +1 -0
  28. package/dist/4817.js +2 -0
  29. package/dist/4817.js.LICENSE.txt +77 -0
  30. package/dist/4817.js.map +1 -0
  31. package/dist/4944.js +1 -0
  32. package/dist/4993.js +1 -0
  33. package/dist/4993.js.map +1 -0
  34. package/dist/5173.js +1 -0
  35. package/dist/5241.js +1 -0
  36. package/dist/5442.js +1 -0
  37. package/dist/5661.js +1 -0
  38. package/dist/6022.js +1 -0
  39. package/dist/6468.js +1 -0
  40. package/dist/6540.js +2 -0
  41. package/dist/6540.js.map +1 -0
  42. package/dist/6606.js +2 -0
  43. package/dist/{591.js.LICENSE.txt → 6606.js.LICENSE.txt} +2 -2
  44. package/dist/6606.js.map +1 -0
  45. package/dist/6679.js +1 -0
  46. package/dist/6840.js +1 -0
  47. package/dist/6859.js +1 -0
  48. package/dist/6941.js +1 -0
  49. package/dist/6941.js.map +1 -0
  50. package/dist/7097.js +1 -0
  51. package/dist/7159.js +1 -0
  52. package/dist/723.js +1 -0
  53. package/dist/7255.js +1 -0
  54. package/dist/7255.js.map +1 -0
  55. package/dist/7617.js +1 -0
  56. package/dist/763.js +1 -0
  57. package/dist/763.js.map +1 -0
  58. package/dist/8163.js +1 -0
  59. package/dist/8349.js +1 -0
  60. package/dist/8618.js +1 -0
  61. package/dist/890.js +1 -0
  62. package/dist/9055.js +1 -0
  63. package/dist/9055.js.map +1 -0
  64. package/dist/9214.js +1 -0
  65. package/dist/9538.js +1 -0
  66. package/dist/{935.js → 961.js} +2 -2
  67. package/dist/{935.js.map → 961.js.map} +1 -1
  68. package/dist/986.js +1 -0
  69. package/dist/9879.js +1 -0
  70. package/dist/9895.js +1 -0
  71. package/dist/9900.js +1 -0
  72. package/dist/9913.js +1 -0
  73. package/dist/main.js +1 -1
  74. package/dist/main.js.LICENSE.txt +31 -1
  75. package/dist/main.js.map +1 -1
  76. package/dist/openmrs-esm-billing-app.js +1 -1
  77. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +844 -165
  78. package/dist/openmrs-esm-billing-app.js.map +1 -1
  79. package/dist/routes.json +1 -1
  80. package/jest.config.js +4 -1
  81. package/package.json +19 -21
  82. package/src/bill-history/bill-history.component.tsx +5 -3
  83. package/src/bill-history/bill-history.scss +24 -9
  84. package/src/bill-history/bill-history.test.tsx +58 -16
  85. package/src/bill-item-actions/bill-item-actions.scss +26 -0
  86. package/src/bill-item-actions/edit-bill-item.component.tsx +221 -0
  87. package/src/bill-item-actions/edit-bill-item.test.tsx +137 -0
  88. package/src/billable-services/bill-waiver/bill-selection.component.tsx +1 -1
  89. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +2 -2
  90. package/src/billable-services/bill-waiver/bill-waiver-form.scss +4 -4
  91. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +4 -4
  92. package/src/billable-services/bill-waiver/patient-bills.component.tsx +1 -1
  93. package/src/billable-services/billable-service.resource.ts +19 -6
  94. package/src/billable-services/billable-services-home.component.tsx +19 -3
  95. package/src/billable-services/billable-services-menu-item/item.component.tsx +17 -0
  96. package/src/billable-services/billable-services-menu-item/item.scss +14 -0
  97. package/src/billable-services/billable-services.component.tsx +48 -9
  98. package/src/billable-services/billable-services.scss +10 -9
  99. package/src/billable-services/billable-services.test.tsx +172 -8
  100. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +276 -0
  101. package/src/billable-services/cash-point/cash-point-configuration.scss +23 -0
  102. package/src/billable-services/create-edit/add-billable-service.component.tsx +126 -47
  103. package/src/billable-services/create-edit/add-billable-service.scss +14 -8
  104. package/src/billable-services/create-edit/add-billable-service.test.tsx +12 -10
  105. package/src/billable-services/dashboard/dashboard.scss +3 -3
  106. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +280 -0
  107. package/src/billable-services/payyment-modes/payment-modes-config.scss +23 -0
  108. package/src/billing-dashboard/billing-dashboard.component.tsx +17 -4
  109. package/src/billing-dashboard/billing-dashboard.scss +3 -3
  110. package/src/billing-form/billing-form.component.tsx +31 -25
  111. package/src/billing-form/billing-form.scss +9 -10
  112. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +38 -14
  113. package/src/billing-header/billing-header.component.tsx +21 -5
  114. package/src/billing-header/billing-header.scss +1 -1
  115. package/src/billing.resource.ts +21 -4
  116. package/src/bills-table/bills-table.component.tsx +46 -36
  117. package/src/bills-table/bills-table.scss +6 -6
  118. package/src/bills-table/bills-table.test.tsx +108 -68
  119. package/src/config-schema.ts +36 -1
  120. package/src/constants.ts +2 -0
  121. package/src/dashboard.meta.ts +2 -1
  122. package/src/helpers/functions.ts +0 -2
  123. package/src/hooks/selectedDateContext.ts +10 -0
  124. package/src/index.ts +22 -27
  125. package/src/invoice/invoice-table.component.tsx +95 -56
  126. package/src/invoice/invoice-table.scss +7 -8
  127. package/src/invoice/invoice-table.test.tsx +151 -0
  128. package/src/invoice/invoice.component.tsx +7 -9
  129. package/src/invoice/invoice.scss +2 -2
  130. package/src/invoice/invoice.test.tsx +199 -169
  131. package/src/invoice/payments/payment-form/payment-form.component.tsx +84 -55
  132. package/src/invoice/payments/payment-form/payment-form.test.tsx +174 -0
  133. package/src/invoice/payments/payment-history/payment-history.component.tsx +9 -7
  134. package/src/invoice/payments/payment-history/payment-history.test.tsx +160 -0
  135. package/src/invoice/payments/payments.component.test.tsx +121 -0
  136. package/src/invoice/payments/payments.component.tsx +57 -48
  137. package/src/invoice/payments/utils.ts +17 -13
  138. package/src/invoice/printable-invoice/print-receipt.component.tsx +23 -8
  139. package/src/invoice/printable-invoice/print-receipt.test.tsx +50 -0
  140. package/src/metrics-cards/card.component.tsx +4 -2
  141. package/src/metrics-cards/metrics-cards.test.tsx +1 -1
  142. package/src/modal/require-payment-modal.component.tsx +2 -2
  143. package/src/modal/require-payment-modal.test.tsx +66 -0
  144. package/src/modal/require-payment.scss +2 -1
  145. package/src/routes.json +40 -8
  146. package/src/types/index.ts +15 -0
  147. package/{i18next-parser.config.js → tools/i18next-parser.config.js} +19 -19
  148. package/tools/update-openmrs-deps.mjs +42 -0
  149. package/translations/am.json +53 -0
  150. package/translations/ar.json +170 -0
  151. package/translations/ar_SY.json +170 -0
  152. package/translations/bn.json +170 -0
  153. package/translations/de.json +170 -0
  154. package/translations/en.json +53 -0
  155. package/translations/es.json +53 -0
  156. package/translations/es_MX.json +170 -0
  157. package/translations/fr.json +53 -0
  158. package/translations/he.json +53 -0
  159. package/translations/hi.json +170 -0
  160. package/translations/hi_IN.json +170 -0
  161. package/translations/id.json +170 -0
  162. package/translations/it.json +170 -0
  163. package/translations/km.json +53 -0
  164. package/translations/ku.json +170 -0
  165. package/translations/ky.json +170 -0
  166. package/translations/lg.json +170 -0
  167. package/translations/ne.json +170 -0
  168. package/translations/pl.json +170 -0
  169. package/translations/pt.json +170 -0
  170. package/translations/pt_BR.json +170 -0
  171. package/translations/qu.json +170 -0
  172. package/translations/ro_RO.json +170 -0
  173. package/translations/ru_RU.json +170 -0
  174. package/translations/si.json +170 -0
  175. package/translations/sw.json +170 -0
  176. package/translations/sw_KE.json +170 -0
  177. package/translations/tr.json +170 -0
  178. package/translations/tr_TR.json +170 -0
  179. package/translations/uk.json +170 -0
  180. package/translations/uz.json +170 -0
  181. package/translations/uz@Latn.json +170 -0
  182. package/translations/uz_UZ.json +170 -0
  183. package/translations/vi.json +170 -0
  184. package/translations/zh.json +170 -0
  185. package/translations/zh_CN.json +170 -0
  186. package/tsconfig.json +10 -8
  187. package/webpack.config.js +1 -1
  188. package/dist/146.js +0 -1
  189. package/dist/146.js.map +0 -1
  190. package/dist/294.js +0 -2
  191. package/dist/294.js.map +0 -1
  192. package/dist/319.js +0 -1
  193. package/dist/384.js +0 -1
  194. package/dist/384.js.map +0 -1
  195. package/dist/421.js +0 -1
  196. package/dist/421.js.map +0 -1
  197. package/dist/533.js +0 -1
  198. package/dist/533.js.map +0 -1
  199. package/dist/574.js +0 -1
  200. package/dist/591.js +0 -2
  201. package/dist/591.js.map +0 -1
  202. package/dist/614.js +0 -2
  203. package/dist/614.js.LICENSE.txt +0 -37
  204. package/dist/614.js.map +0 -1
  205. package/dist/753.js +0 -1
  206. package/dist/753.js.map +0 -1
  207. package/dist/757.js +0 -1
  208. package/dist/770.js +0 -1
  209. package/dist/770.js.map +0 -1
  210. package/dist/783.js +0 -1
  211. package/dist/783.js.map +0 -1
  212. package/dist/788.js +0 -1
  213. package/dist/800.js +0 -2
  214. package/dist/800.js.LICENSE.txt +0 -3
  215. package/dist/800.js.map +0 -1
  216. package/dist/807.js +0 -1
  217. package/dist/833.js +0 -1
  218. package/dist/992.js +0 -1
  219. package/dist/992.js.map +0 -1
  220. package/src/root.scss +0 -30
  221. /package/dist/{294.js.LICENSE.txt → 6540.js.LICENSE.txt} +0 -0
  222. /package/dist/{935.js.LICENSE.txt → 961.js.LICENSE.txt} +0 -0
  223. /package/{src → tools}/setup-tests.ts +0 -0
  224. /package/{test-helpers.tsx → tools/test-helpers.tsx} +0 -0
package/src/index.ts CHANGED
@@ -1,23 +1,17 @@
1
1
  import { configSchema } from './config-schema';
2
- import { createDashboardLink, registerWorkspace } from '@openmrs/esm-patient-common-lib';
2
+ import { createDashboardLink } from '@openmrs/esm-patient-common-lib';
3
3
  import { createLeftPanelLink } from './left-panel-link.component';
4
4
  import { dashboardMeta } from './dashboard.meta';
5
- import {
6
- defineConfigSchema,
7
- getAsyncLifecycle,
8
- getSyncLifecycle,
9
- registerFeatureFlag,
10
- translateFrom,
11
- } from '@openmrs/esm-framework';
5
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
6
+ import appMenu from './billable-services/billable-services-menu-item/item.component';
12
7
  import BillableServiceHome from './billable-services/billable-services-home.component';
13
8
  import BillableServicesCardLink from './billable-services-admin-card-link.component';
14
9
  import BillHistory from './bill-history/bill-history.component';
15
10
  import BillingCheckInForm from './billing-form/billing-checkin-form.component';
16
11
  import RequirePaymentModal from './modal/require-payment-modal.component';
17
12
  import RootComponent from './root.component';
18
- import VisitAttributeTags from './invoice/payments/visit-tags/visit-attribute.component';
19
- import BillableServicesDashboard from './billable-services/dashboard/dashboard.component';
20
13
  import ServiceMetrics from './billable-services/dashboard/service-metrics.component';
14
+ import VisitAttributeTags from './invoice/payments/visit-tags/visit-attribute.component';
21
15
 
22
16
  const moduleName = '@openmrs/esm-billing-app';
23
17
 
@@ -26,12 +20,6 @@ const options = {
26
20
  moduleName,
27
21
  };
28
22
 
29
- registerFeatureFlag(
30
- 'billing',
31
- 'Billing module',
32
- 'This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features',
33
- );
34
-
35
23
  // t('billing', 'Billing')
36
24
  export const billingDashboardLink = getSyncLifecycle(
37
25
  createLeftPanelLink({
@@ -45,16 +33,6 @@ export const importTranslation = require.context('../translations', false, /.jso
45
33
 
46
34
  export function startupApp() {
47
35
  defineConfigSchema(moduleName, configSchema);
48
-
49
- // t('billingForm', 'Billing form')
50
- registerWorkspace({
51
- name: 'billing-form-workspace',
52
- title: translateFrom(moduleName, 'billingForm', 'Billing form'),
53
- load: getAsyncLifecycle(() => import('./billing-form/billing-form.component'), options),
54
- type: 'billing',
55
- canHide: false,
56
- width: 'wider',
57
- });
58
36
  }
59
37
 
60
38
  export const billingSummaryDashboardLink = getSyncLifecycle(
@@ -62,11 +40,28 @@ export const billingSummaryDashboardLink = getSyncLifecycle(
62
40
  options,
63
41
  );
64
42
 
43
+ export const billableServicesAppMenuItem = getSyncLifecycle(appMenu, options);
44
+
65
45
  export const billableServicesCardLink = getSyncLifecycle(BillableServicesCardLink, options);
46
+
66
47
  export const billableServicesHome = getSyncLifecycle(BillableServiceHome, options);
48
+
67
49
  export const billingCheckInForm = getSyncLifecycle(BillingCheckInForm, options);
68
- export const serviceMetrics = getSyncLifecycle(ServiceMetrics, options);
50
+
69
51
  export const billingPatientSummary = getSyncLifecycle(BillHistory, options);
52
+
70
53
  export const requirePaymentModal = getSyncLifecycle(RequirePaymentModal, options);
54
+
71
55
  export const root = getSyncLifecycle(RootComponent, options);
56
+
57
+ export const serviceMetrics = getSyncLifecycle(ServiceMetrics, options);
58
+
72
59
  export const visitAttributeTags = getSyncLifecycle(VisitAttributeTags, options);
60
+
61
+ export const editBillLineItemDialog = getAsyncLifecycle(() => import('./bill-item-actions/edit-bill-item.component'), {
62
+ featureName: 'edit bill line item',
63
+ moduleName,
64
+ });
65
+
66
+ // t('billingForm', 'Billing form')
67
+ export const billingFormWorkspace = getAsyncLifecycle(() => import('./billing-form/billing-form.component'), options);
@@ -1,7 +1,8 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useMemo, useState, useEffect, useCallback } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import fuzzy from 'fuzzy';
4
4
  import {
5
+ Button,
5
6
  DataTable,
6
7
  DataTableSkeleton,
7
8
  Layer,
@@ -12,18 +13,16 @@ import {
12
13
  TableHead,
13
14
  TableHeader,
14
15
  TableRow,
15
- TableToolbar,
16
- TableToolbarContent,
17
- TableToolbarSearch,
18
16
  TableSelectRow,
17
+ TableToolbarSearch,
19
18
  Tile,
20
- type DataTableHeader,
21
19
  type DataTableRow,
22
20
  } from '@carbon/react';
23
- import { isDesktop, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
21
+ import { Edit } from '@carbon/react/icons';
22
+ import { isDesktop, showModal, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
24
23
  import { type LineItem, type MappedBill } from '../types';
25
- import styles from './invoice-table.scss';
26
24
  import { convertToCurrency } from '../helpers';
25
+ import styles from './invoice-table.scss';
27
26
 
28
27
  type InvoiceTableProps = {
29
28
  bill: MappedBill;
@@ -34,14 +33,21 @@ type InvoiceTableProps = {
34
33
 
35
34
  const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
36
35
  const { t } = useTranslation();
37
- const lineItems = bill?.lineItems ?? [];
36
+ const { defaultCurrency, showEditBillButton } = useConfig();
38
37
  const layout = useLayoutType();
38
+ const lineItems = useMemo(() => bill?.lineItems ?? [], [bill?.lineItems]);
39
+ const paidLineItems = useMemo(() => lineItems?.filter((item) => item.paymentStatus === 'PAID') ?? [], [lineItems]);
39
40
  const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
40
- const pendingLineItems = lineItems?.filter((item) => item.paymentStatus === 'PENDING') ?? [];
41
- const [selectedLineItems, setSelectedLineItems] = useState(pendingLineItems ?? []);
41
+
42
+ const [selectedLineItems, setSelectedLineItems] = useState(paidLineItems ?? []);
42
43
  const [searchTerm, setSearchTerm] = useState('');
43
44
  const debouncedSearchTerm = useDebounce(searchTerm);
44
- const { defaultCurrency } = useConfig();
45
+
46
+ useEffect(() => {
47
+ if (onSelectItem) {
48
+ onSelectItem(selectedLineItems);
49
+ }
50
+ }, [selectedLineItems, onSelectItem]);
45
51
 
46
52
  const filteredLineItems = useMemo(() => {
47
53
  if (!debouncedSearchTerm) {
@@ -58,16 +64,28 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
58
64
  : lineItems;
59
65
  }, [debouncedSearchTerm, lineItems]);
60
66
 
61
- const tableHeaders: Array<typeof DataTableHeader> = [
62
- { header: 'No', key: 'no' },
63
- { header: 'Bill item', key: 'billItem' },
64
- { header: 'Bill code', key: 'billCode' },
65
- { header: 'Status', key: 'status' },
66
- { header: 'Quantity', key: 'quantity' },
67
- { header: 'Price', key: 'price' },
68
- { header: 'Total', key: 'total' },
67
+ 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' },
69
76
  ];
70
77
 
78
+ const handleSelectBillItem = useCallback(
79
+ (row: LineItem) => {
80
+ const dispose = showModal('edit-bill-line-item-dialog', {
81
+ bill,
82
+ item: row,
83
+ closeModal: () => dispose(),
84
+ });
85
+ },
86
+ [bill],
87
+ );
88
+
71
89
  const tableRows: Array<typeof DataTableRow> = useMemo(
72
90
  () =>
73
91
  filteredLineItems?.map((item, index) => {
@@ -75,20 +93,38 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
75
93
  no: `${index + 1}`,
76
94
  id: `${item.uuid}`,
77
95
  billItem: item.billableService ? item.billableService : item?.item,
78
- billCode: bill?.receiptNumber,
79
- status: item?.paymentStatus,
96
+ billCode: <span data-testid={`receipt-number-${index}`}>{bill?.receiptNumber}</span>,
97
+ status: item.paymentStatus,
80
98
  quantity: item.quantity,
81
99
  price: convertToCurrency(item.price, defaultCurrency),
82
- total: convertToCurrency(item.price * item.quantity, defaultCurrency),
100
+ total: item.price * item.quantity,
101
+ actionButton: (
102
+ <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
+ )}
116
+ </span>
117
+ ),
83
118
  };
84
119
  }) ?? [],
85
- [bill?.receiptNumber, filteredLineItems],
120
+ [filteredLineItems, bill?.receiptNumber, defaultCurrency, showEditBillButton, t, handleSelectBillItem],
86
121
  );
87
122
 
88
123
  if (isLoadingBill) {
89
124
  return (
90
125
  <div className={styles.loaderContainer}>
91
126
  <DataTableSkeleton
127
+ data-testid="loader"
92
128
  columnCount={tableHeaders.length}
93
129
  showHeader={false}
94
130
  showToolbar={false}
@@ -123,20 +159,17 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
123
159
  </span>
124
160
  }
125
161
  title={t('lineItems', 'Line items')}>
126
- <div className={styles.toolbarWrapper}>
127
- <TableToolbar {...getToolbarProps()} className={styles.tableToolbar} size={responsiveSize}>
128
- <TableToolbarContent className={styles.headerContainer}>
129
- <TableToolbarSearch
130
- className={styles.searchbox}
131
- expanded
132
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
133
- placeholder={t('searchThisTable', 'Search this table')}
134
- size={responsiveSize}
135
- />
136
- </TableToolbarContent>
137
- </TableToolbar>
138
- </div>
139
- <Table {...getTableProps()} aria-label="Invoice line items" className={styles.table}>
162
+ <TableToolbarSearch
163
+ className={styles.searchbox}
164
+ expanded
165
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
166
+ placeholder={t('searchThisTable', 'Search this table')}
167
+ size={responsiveSize}
168
+ />
169
+ <Table
170
+ {...getTableProps()}
171
+ aria-label="Invoice line items"
172
+ className={`${styles.invoiceTable} billingTable`}>
140
173
  <TableHead>
141
174
  <TableRow>
142
175
  {rows.length > 1 && isSelectable ? <TableHeader /> : null}
@@ -146,25 +179,31 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
146
179
  </TableRow>
147
180
  </TableHead>
148
181
  <TableBody>
149
- {rows.map((row, index) => (
150
- <TableRow
151
- key={row.id}
152
- {...getRowProps({
153
- row,
154
- })}>
155
- {rows.length > 1 && isSelectable && (
156
- <TableSelectRow
157
- aria-label="Select row"
158
- {...getSelectionProps({ row })}
159
- onChange={(checked: boolean) => handleRowSelection(row, checked)}
160
- checked={Boolean(selectedLineItems?.find((item) => item?.uuid === row?.id))}
161
- />
162
- )}
163
- {row.cells.map((cell) => (
164
- <TableCell key={cell.id}>{cell.value}</TableCell>
165
- ))}
166
- </TableRow>
167
- ))}
182
+ {rows.map((row, index) => {
183
+ return (
184
+ <TableRow
185
+ key={row.id}
186
+ {...getRowProps({
187
+ row,
188
+ })}>
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
+ {row.cells.map((cell) => (
202
+ <TableCell key={cell.id}>{cell.value}</TableCell>
203
+ ))}
204
+ </TableRow>
205
+ );
206
+ })}
168
207
  </TableBody>
169
208
  </Table>
170
209
  </TableContainer>
@@ -1,7 +1,7 @@
1
1
  @use '@carbon/colors';
2
2
  @use '@carbon/layout';
3
3
  @use '@carbon/type';
4
- @import '~@openmrs/esm-styleguide/src/vars';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
5
 
6
6
  .filterEmptyState {
7
7
  align-items: center;
@@ -19,7 +19,7 @@
19
19
  .filterEmptyStateContent {
20
20
  @include type.type-style('heading-compact-02');
21
21
  color: $text-02;
22
- margin-bottom: 0.5rem;
22
+ margin-bottom: layout.$spacing-03;
23
23
  }
24
24
 
25
25
  .filterEmptyStateHelper {
@@ -32,7 +32,6 @@
32
32
  }
33
33
 
34
34
  .invoiceContainer {
35
- margin: layout.$spacing-09 layout.$spacing-05 0;
36
35
  border: 1px solid $ui-03;
37
36
  }
38
37
 
@@ -42,10 +41,10 @@
42
41
  }
43
42
  }
44
43
 
45
- .table {
46
- td {
47
- border-bottom: none !important;
48
- }
44
+ .invoiceTable {
45
+ width: 100%;
46
+ table-layout: fixed;
47
+ border-collapse: collapse;
49
48
  }
50
49
 
51
50
  .tableDescription {
@@ -57,7 +56,7 @@
57
56
  ::after {
58
57
  content: '';
59
58
  display: block;
60
- width: 2rem;
59
+ width: layout.$spacing-07;
61
60
  padding-top: 0.188rem;
62
61
  border-bottom: 0.375rem solid var(--brand-03);
63
62
  }
@@ -0,0 +1,151 @@
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';
6
+ import { type MappedBill } from '../types';
7
+
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
+ }));
26
+
27
+ jest.mock('../helpers', () => ({
28
+ convertToCurrency: jest.fn((price) => `USD ${price}`),
29
+ }));
30
+
31
+ 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 = {
44
+ uuid: 'bill-uuid',
45
+ id: 123,
46
+ patientUuid: 'patient-uuid',
47
+ patientName: 'John Doe',
48
+ lineItems: [
49
+ {
50
+ uuid: '1',
51
+ item: 'Item 1',
52
+ paymentStatus: 'PAID',
53
+ quantity: 1,
54
+ price: 100,
55
+ display: '',
56
+ voided: false,
57
+ voidReason: '',
58
+ billableService: '',
59
+ priceName: '',
60
+ priceUuid: '',
61
+ lineItemOrder: 0,
62
+ resourceVersion: '',
63
+ },
64
+ {
65
+ uuid: '2',
66
+ item: 'Item 2',
67
+ paymentStatus: 'PENDING',
68
+ quantity: 2,
69
+ price: 200,
70
+ display: '',
71
+ voided: false,
72
+ voidReason: '',
73
+ billableService: '',
74
+ priceName: '',
75
+ priceUuid: '',
76
+ lineItemOrder: 0,
77
+ resourceVersion: '',
78
+ },
79
+ ],
80
+ receiptNumber: '12345',
81
+ cashPointUuid: 'cash-point-uuid',
82
+ cashPointName: 'Main Cash Point',
83
+ cashPointLocation: 'Front Desk',
84
+ cashier: {
85
+ uuid: 'cashier-uuid',
86
+ display: 'John Doe',
87
+ links: [],
88
+ },
89
+ status: 'PAID',
90
+ identifier: 'receipt-identifier',
91
+ dateCreated: new Date().toISOString(),
92
+ billingService: 'billing-service-uuid',
93
+ payments: [],
94
+ totalAmount: 300,
95
+ tenderedAmount: 300,
96
+ };
97
+
98
+ it('renders the table and displays line items correctly', () => {
99
+ render(<InvoiceTable bill={bill} />);
100
+
101
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
102
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
103
+ expect(screen.getByTestId('receipt-number-0')).toHaveTextContent('12345');
104
+ });
105
+
106
+ it('renders the edit button and calls showModal when clicked', () => {
107
+ render(<InvoiceTable bill={bill} />);
108
+
109
+ const editButton = screen.getByTestId('edit-button-1');
110
+ fireEvent.click(editButton);
111
+ expect(showModal).toHaveBeenCalledWith('edit-bill-line-item-dialog', expect.anything());
112
+ });
113
+
114
+ it('displays a skeleton loader when the bill is loading', () => {
115
+ render(<InvoiceTable bill={bill} isLoadingBill={true} />);
116
+
117
+ expect(screen.getByTestId('loader')).toBeInTheDocument();
118
+ });
119
+
120
+ it('filters line items based on the search term', () => {
121
+ render(<InvoiceTable bill={bill} />);
122
+ const searchInput = screen.getByPlaceholderText('searchThisTable'); //
123
+
124
+ fireEvent.change(searchInput, { target: { value: 'Item 2' } });
125
+
126
+ expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
127
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
128
+ });
129
+
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', () => {
141
+ render(<InvoiceTable bill={bill} />);
142
+
143
+ const button = screen.getByTestId('edit-button-1');
144
+ fireEvent.click(button);
145
+ act(() => {
146
+ jest.advanceTimersByTime(1000);
147
+ });
148
+
149
+ expect(button).not.toBeDisabled();
150
+ });
151
+ });
@@ -24,15 +24,15 @@ const Invoice: React.FC = () => {
24
24
  const { t } = useTranslation();
25
25
  const { billUuid, patientUuid } = useParams();
26
26
  const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid);
27
- const { bill, isLoading: isLoadingBill, error } = useBill(billUuid);
27
+ const { bill, isLoading: isLoadingBill, error, mutate } = useBill(billUuid);
28
28
  const [isPrinting, setIsPrinting] = useState(false);
29
- const [selectedLineItems, setSelectedLineItems] = useState([]);
29
+ const [selectedLineItems, setSelectedLineItems] = useState<LineItem[]>([]);
30
30
  const componentRef = useRef<HTMLDivElement>(null);
31
31
  const onBeforeGetContentResolve = useRef<(() => void) | null>(null);
32
- const handleSelectItem = (lineItems: Array<LineItem>) => {
32
+ const { defaultCurrency } = useConfig();
33
+ const handleSelectItem = (lineItems: LineItem[]) => {
33
34
  setSelectedLineItems(lineItems);
34
35
  };
35
- const { defaultCurrency } = useConfig();
36
36
 
37
37
  const handleAfterPrint = useCallback(() => {
38
38
  onBeforeGetContentResolve.current = null;
@@ -52,9 +52,7 @@ const Invoice: React.FC = () => {
52
52
 
53
53
  const handlePrint = useReactToPrint({
54
54
  content: reactToPrintContent,
55
- documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${
56
- patient?.name?.[0].family
57
- }`,
55
+ documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
58
56
  onBeforeGetContent: handleOnBeforeGetContent,
59
57
  onAfterPrint: handleAfterPrint,
60
58
  removeAfterPrint: true,
@@ -118,12 +116,12 @@ const Invoice: React.FC = () => {
118
116
  size="md">
119
117
  {t('printBill', 'Print bill')}
120
118
  </Button>
121
- {bill?.status === 'PAID' ? <PrintReceipt billId={bill?.id} /> : null}
119
+ {(bill?.status === 'PAID' || bill?.tenderedAmount > 0) && <PrintReceipt billId={bill?.id} />}
122
120
  </div>
123
121
  </div>
124
122
 
125
123
  <InvoiceTable bill={bill} isLoadingBill={isLoadingBill} onSelectItem={handleSelectItem} />
126
- <Payments bill={bill} selectedLineItems={selectedLineItems} />
124
+ <Payments bill={bill} mutate={mutate} selectedLineItems={selectedLineItems} />
127
125
 
128
126
  <div className={styles.printContainer} ref={componentRef}>
129
127
  {isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoadingPatient} />}
@@ -4,7 +4,7 @@
4
4
 
5
5
  .invoiceContainer {
6
6
  background-color: colors.$gray-10;
7
- height: calc(100vh - 3rem);
7
+ height: calc(100vh - layout.$spacing-09);
8
8
  }
9
9
 
10
10
  .errorContainer {
@@ -28,7 +28,7 @@
28
28
  align-items: center;
29
29
  justify-content: space-between;
30
30
  margin: layout.$spacing-05;
31
- row-gap: 1.5rem;
31
+ row-gap: layout.$spacing-06;
32
32
  }
33
33
 
34
34
  .label {