@openmrs/esm-billing-app 1.0.1-pre.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +7 -0
  11. package/__mocks__/bills.mock.ts +394 -0
  12. package/__mocks__/delivery-summary.mock.ts +89 -0
  13. package/__mocks__/encounter-observation.mock.ts +10651 -0
  14. package/__mocks__/encounter-observations.mock.ts +6189 -0
  15. package/__mocks__/hiv-summary.mock.ts +22 -0
  16. package/__mocks__/patient-summary.mock.ts +32 -0
  17. package/__mocks__/patient.mock.ts +59 -0
  18. package/__mocks__/program-summary.mock.ts +43 -0
  19. package/__mocks__/react-i18next.js +57 -0
  20. package/dist/146.js +1 -0
  21. package/dist/146.js.map +1 -0
  22. package/dist/294.js +2 -0
  23. package/dist/294.js.LICENSE.txt +9 -0
  24. package/dist/294.js.map +1 -0
  25. package/dist/319.js +1 -0
  26. package/dist/384.js +1 -0
  27. package/dist/384.js.map +1 -0
  28. package/dist/421.js +1 -0
  29. package/dist/421.js.map +1 -0
  30. package/dist/533.js +1 -0
  31. package/dist/533.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/591.js +2 -0
  34. package/dist/591.js.LICENSE.txt +9 -0
  35. package/dist/591.js.map +1 -0
  36. package/dist/614.js +2 -0
  37. package/dist/614.js.LICENSE.txt +37 -0
  38. package/dist/614.js.map +1 -0
  39. package/dist/753.js +1 -0
  40. package/dist/753.js.map +1 -0
  41. package/dist/757.js +1 -0
  42. package/dist/770.js +1 -0
  43. package/dist/770.js.map +1 -0
  44. package/dist/783.js +1 -0
  45. package/dist/783.js.map +1 -0
  46. package/dist/788.js +1 -0
  47. package/dist/800.js +2 -0
  48. package/dist/800.js.LICENSE.txt +3 -0
  49. package/dist/800.js.map +1 -0
  50. package/dist/807.js +1 -0
  51. package/dist/833.js +1 -0
  52. package/dist/935.js +2 -0
  53. package/dist/935.js.LICENSE.txt +19 -0
  54. package/dist/935.js.map +1 -0
  55. package/dist/992.js +1 -0
  56. package/dist/992.js.map +1 -0
  57. package/dist/main.js +2 -0
  58. package/dist/main.js.LICENSE.txt +47 -0
  59. package/dist/main.js.map +1 -0
  60. package/dist/openmrs-esm-billing-app.js +1 -0
  61. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +609 -0
  62. package/dist/openmrs-esm-billing-app.js.map +1 -0
  63. package/dist/routes.json +1 -0
  64. package/e2e/README.md +115 -0
  65. package/e2e/core/global-setup.ts +32 -0
  66. package/e2e/core/index.ts +1 -0
  67. package/e2e/core/test.ts +20 -0
  68. package/e2e/fixtures/api.ts +27 -0
  69. package/e2e/fixtures/index.ts +1 -0
  70. package/e2e/pages/home-page.ts +9 -0
  71. package/e2e/pages/index.ts +1 -0
  72. package/e2e/specs/sample-test.spec.ts +11 -0
  73. package/e2e/support/github/Dockerfile +34 -0
  74. package/e2e/support/github/docker-compose.yml +24 -0
  75. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  76. package/example.env +6 -0
  77. package/i18next-parser.config.js +89 -0
  78. package/jest.config.js +34 -0
  79. package/package.json +124 -0
  80. package/playwright.config.ts +32 -0
  81. package/prettier.config.js +8 -0
  82. package/src/bill-history/bill-history.component.tsx +199 -0
  83. package/src/bill-history/bill-history.scss +151 -0
  84. package/src/bill-history/bill-history.test.tsx +122 -0
  85. package/src/billable-services/bill-waiver/bill-selection.component.tsx +76 -0
  86. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +110 -0
  87. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  88. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  89. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  90. package/src/billable-services/bill-waiver/patient-bills.component.tsx +137 -0
  91. package/src/billable-services/bill-waiver/utils.ts +41 -0
  92. package/src/billable-services/billable-service.resource.ts +72 -0
  93. package/src/billable-services/billable-services-home.component.tsx +51 -0
  94. package/src/billable-services/billable-services.component.tsx +255 -0
  95. package/src/billable-services/billable-services.scss +218 -0
  96. package/src/billable-services/billable-services.test.tsx +16 -0
  97. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  98. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  99. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  100. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  101. package/src/billable-services/dashboard/dashboard.scss +27 -0
  102. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  103. package/src/billable-services/dashboard/service-metrics.component.tsx +41 -0
  104. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  105. package/src/billable-services-admin-card-link.component.tsx +25 -0
  106. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  107. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  108. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  109. package/src/billing-form/billing-checkin-form.component.tsx +127 -0
  110. package/src/billing-form/billing-checkin-form.scss +13 -0
  111. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  112. package/src/billing-form/billing-form.component.tsx +347 -0
  113. package/src/billing-form/billing-form.resource.ts +32 -0
  114. package/src/billing-form/billing-form.scss +88 -0
  115. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  116. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  117. package/src/billing-header/billing-header.component.tsx +43 -0
  118. package/src/billing-header/billing-header.scss +83 -0
  119. package/src/billing-header/billing-illustration.component.tsx +30 -0
  120. package/src/billing.resource.ts +148 -0
  121. package/src/bills-table/bills-table.component.tsx +280 -0
  122. package/src/bills-table/bills-table.scss +181 -0
  123. package/src/bills-table/bills-table.test.tsx +154 -0
  124. package/src/config-schema.ts +50 -0
  125. package/src/constants.ts +3 -0
  126. package/src/dashboard.meta.ts +7 -0
  127. package/src/declarations.d.ts +4 -0
  128. package/src/helpers/functions.ts +66 -0
  129. package/src/helpers/index.ts +1 -0
  130. package/src/index.ts +72 -0
  131. package/src/invoice/invoice-table.component.tsx +189 -0
  132. package/src/invoice/invoice-table.scss +91 -0
  133. package/src/invoice/invoice.component.tsx +144 -0
  134. package/src/invoice/invoice.scss +93 -0
  135. package/src/invoice/invoice.test.tsx +242 -0
  136. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  137. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  138. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  139. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  140. package/src/invoice/payments/payment-history/payment-history.component.tsx +69 -0
  141. package/src/invoice/payments/payment.resource.ts +44 -0
  142. package/src/invoice/payments/payments.component.tsx +147 -0
  143. package/src/invoice/payments/payments.scss +46 -0
  144. package/src/invoice/payments/utils.ts +68 -0
  145. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  146. package/src/invoice/printable-invoice/print-receipt.component.tsx +29 -0
  147. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  148. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  149. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  150. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  151. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  152. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  153. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  154. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  155. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  156. package/src/left-panel-link.component.tsx +41 -0
  157. package/src/left-panel-link.test.tsx +38 -0
  158. package/src/metrics-cards/card.component.tsx +14 -0
  159. package/src/metrics-cards/card.scss +20 -0
  160. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  161. package/src/metrics-cards/metrics-cards.scss +12 -0
  162. package/src/metrics-cards/metrics-cards.test.tsx +44 -0
  163. package/src/metrics-cards/metrics.resource.ts +45 -0
  164. package/src/modal/require-payment-modal.component.tsx +85 -0
  165. package/src/modal/require-payment.scss +6 -0
  166. package/src/root.component.tsx +19 -0
  167. package/src/root.scss +30 -0
  168. package/src/routes.json +78 -0
  169. package/src/setup-tests.ts +13 -0
  170. package/src/types/index.ts +181 -0
  171. package/test-helpers.tsx +23 -0
  172. package/translations/am.json +117 -0
  173. package/translations/en.json +117 -0
  174. package/translations/es.json +117 -0
  175. package/translations/fr.json +117 -0
  176. package/translations/he.json +117 -0
  177. package/translations/km.json +117 -0
  178. package/tsconfig.json +16 -0
  179. package/webpack.config.js +1 -0
@@ -0,0 +1,199 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ Button,
5
+ DataTable,
6
+ DataTableSkeleton,
7
+ Layer,
8
+ Pagination,
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableContainer,
13
+ TableExpandedRow,
14
+ TableExpandHeader,
15
+ TableExpandRow,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ Tile,
20
+ } from '@carbon/react';
21
+ import { isDesktop, useConfig, useLayoutType, usePagination } from '@openmrs/esm-framework';
22
+ import {
23
+ CardHeader,
24
+ EmptyDataIllustration,
25
+ ErrorState,
26
+ launchPatientWorkspace,
27
+ usePaginationInfo,
28
+ } from '@openmrs/esm-patient-common-lib';
29
+ import { useBills } from '../billing.resource';
30
+ import InvoiceTable from '../invoice/invoice-table.component';
31
+ import styles from './bill-history.scss';
32
+ import { Add } from '@carbon/react/icons';
33
+ import { convertToCurrency } from '../helpers';
34
+
35
+ interface BillHistoryProps {
36
+ patientUuid: string;
37
+ }
38
+
39
+ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
40
+ const { t } = useTranslation();
41
+ const { bills, isLoading, error } = useBills(patientUuid);
42
+ const layout = useLayoutType();
43
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
44
+ const { paginated, goTo, results, currentPage } = usePagination(bills);
45
+ const { pageSize, defaultCurrency } = useConfig();
46
+ const [currentPageSize, setCurrentPageSize] = React.useState(pageSize);
47
+ const { pageSizes } = usePaginationInfo(pageSize, bills?.length, currentPage, results?.length);
48
+
49
+ const headerData = [
50
+ {
51
+ header: t('visitTime', 'Visit time'),
52
+ key: 'visitTime',
53
+ },
54
+ {
55
+ header: t('identifier', 'Identifier'),
56
+ key: 'identifier',
57
+ },
58
+ {
59
+ header: t('billedItems', 'Billed Items'),
60
+ key: 'billedItems',
61
+ },
62
+ {
63
+ header: t('billTotal', 'Bill total'),
64
+ key: 'billTotal',
65
+ },
66
+ ];
67
+
68
+ const setBilledItems = (bill) =>
69
+ bill?.lineItems?.reduce((acc, item) => acc + (acc ? ' & ' : '') + (item?.billableService || item?.item || ''), '');
70
+
71
+ const rowData = results?.map((bill) => ({
72
+ id: bill.uuid,
73
+ uuid: bill.uuid,
74
+ billTotal: convertToCurrency(bill?.totalAmount, defaultCurrency),
75
+ visitTime: bill.dateCreated,
76
+ identifier: bill.identifier,
77
+ billedItems: setBilledItems(bill),
78
+ }));
79
+
80
+ if (isLoading) {
81
+ return (
82
+ <div className={styles.loaderContainer}>
83
+ <DataTableSkeleton showHeader={false} showToolbar={false} zebra size={responsiveSize} />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ if (error) {
89
+ return (
90
+ <div className={styles.errorContainer}>
91
+ <Layer>
92
+ <ErrorState error={error} headerTitle={t('billsList', 'Bill list')} />
93
+ </Layer>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ if (bills.length === 0) {
99
+ return (
100
+ <Layer className={styles.emptyStateContainer}>
101
+ <Tile className={styles.tile}>
102
+ <div className={styles.illo}>
103
+ <EmptyDataIllustration />
104
+ </div>
105
+ <p className={styles.content}>There are no bills to display.</p>
106
+ <Button onClick={() => launchPatientWorkspace('billing-form-workspace')} kind="ghost">
107
+ {t('launchBillForm', 'Launch bill form')}
108
+ </Button>
109
+ </Tile>
110
+ </Layer>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <>
116
+ <CardHeader title={t('billingHistory', 'Billing History')}>
117
+ <Button renderIcon={Add} onClick={() => launchPatientWorkspace('billing-form-workspace', {})} kind="ghost">
118
+ {t('addBill', 'Add bill item(s)')}
119
+ </Button>
120
+ </CardHeader>
121
+ <div className={styles.billHistoryContainer}>
122
+ <DataTable isSortable rows={rowData} headers={headerData} size={responsiveSize} useZebraStyles>
123
+ {({
124
+ rows,
125
+ headers,
126
+ getExpandHeaderProps,
127
+ getTableProps,
128
+ getTableContainerProps,
129
+ getHeaderProps,
130
+ getRowProps,
131
+ }) => (
132
+ <TableContainer {...getTableContainerProps}>
133
+ <Table className={styles.table} {...getTableProps()} aria-label="Bill list">
134
+ <TableHead>
135
+ <TableRow>
136
+ <TableExpandHeader enableToggle {...getExpandHeaderProps()} />
137
+ {headers.map((header, i) => (
138
+ <TableHeader
139
+ key={i}
140
+ {...getHeaderProps({
141
+ header,
142
+ })}>
143
+ {header.header}
144
+ </TableHeader>
145
+ ))}
146
+ </TableRow>
147
+ </TableHead>
148
+ <TableBody>
149
+ {rows.map((row, i) => {
150
+ const currentBill = bills?.find((bill) => bill.uuid === row.id);
151
+
152
+ return (
153
+ <React.Fragment key={row.id}>
154
+ <TableExpandRow {...getRowProps({ row })}>
155
+ {row.cells.map((cell) => (
156
+ <TableCell key={cell.id}>{cell.value}</TableCell>
157
+ ))}
158
+ </TableExpandRow>
159
+ {row.isExpanded ? (
160
+ <TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 1}>
161
+ <div className={styles.container} key={i}>
162
+ <InvoiceTable bill={currentBill} isSelectable={false} />
163
+ </div>
164
+ </TableExpandedRow>
165
+ ) : (
166
+ <TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
167
+ )}
168
+ </React.Fragment>
169
+ );
170
+ })}
171
+ </TableBody>
172
+ </Table>
173
+ </TableContainer>
174
+ )}
175
+ </DataTable>
176
+ {paginated && (
177
+ <Pagination
178
+ forwardText={t('nextPage', 'Next page')}
179
+ backwardText={t('previousPage', 'Previous page')}
180
+ page={currentPage}
181
+ pageSize={currentPageSize}
182
+ pageSizes={pageSizes}
183
+ totalItems={bills.length}
184
+ className={styles.pagination}
185
+ size={responsiveSize}
186
+ onChange={({ page: newPage, pageSize }) => {
187
+ if (newPage !== currentPage) {
188
+ goTo(newPage);
189
+ }
190
+ setCurrentPageSize(pageSize);
191
+ }}
192
+ />
193
+ )}
194
+ </div>
195
+ </>
196
+ );
197
+ };
198
+
199
+ export default BillHistory;
@@ -0,0 +1,151 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .container {
6
+ margin: 2rem 0;
7
+ }
8
+
9
+ .emptyStateContainer,
10
+ .loaderContainer {
11
+ @extend .container;
12
+ }
13
+
14
+ .billHistoryContainer {
15
+ background-color: $ui-02;
16
+ border: 1px solid $ui-03;
17
+ border-bottom: none;
18
+ width: 100%;
19
+ margin: 0 auto;
20
+ max-width: 95vw;
21
+ padding-bottom: 0;
22
+ }
23
+
24
+ .headerContainer {
25
+ display: flex;
26
+ justify-content: space-between;
27
+ align-items: center;
28
+ padding: layout.$spacing-04 0 layout.$spacing-04 layout.$spacing-05;
29
+ background-color: $ui-02;
30
+ }
31
+
32
+ .backgroundDataFetchingIndicator {
33
+ align-items: center;
34
+ display: flex;
35
+ flex: 1 1 0%;
36
+ justify-content: center;
37
+ }
38
+
39
+ .tableContainer section {
40
+ position: relative;
41
+ }
42
+
43
+ .tableContainer a {
44
+ text-decoration: none;
45
+ }
46
+
47
+ .pagination {
48
+ overflow: hidden;
49
+
50
+ &:global(.cds--pagination) {
51
+ border-top: none;
52
+ }
53
+ }
54
+
55
+ .hiddenRow {
56
+ display: none;
57
+ }
58
+
59
+ .emptyRow {
60
+ padding: 0 1rem;
61
+ display: flex;
62
+ align-items: center;
63
+ }
64
+
65
+ .visitSummaryContainer {
66
+ width: 100%;
67
+ max-width: 768px;
68
+ margin: 1rem auto;
69
+ }
70
+
71
+ .expandedActiveVisitRow > td > div {
72
+ max-height: max-content !important;
73
+ }
74
+
75
+ .expandedActiveVisitRow td {
76
+ padding: 0 2rem;
77
+ }
78
+
79
+ .expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
80
+ padding: 0 1rem;
81
+ }
82
+
83
+ .action {
84
+ margin-bottom: layout.$spacing-03;
85
+ }
86
+
87
+ .illo {
88
+ margin-top: layout.$spacing-05;
89
+ }
90
+
91
+ .content {
92
+ @include type.type-style('heading-compact-01');
93
+ color: $text-02;
94
+ margin-top: layout.$spacing-05;
95
+ margin-bottom: layout.$spacing-03;
96
+ }
97
+
98
+ .desktopHeading,
99
+ .tabletHeading {
100
+ text-align: left;
101
+ text-transform: capitalize;
102
+
103
+ h4 {
104
+ @include type.type-style('heading-compact-02');
105
+ color: $text-02;
106
+
107
+ &:after {
108
+ content: '';
109
+ display: block;
110
+ width: 2rem;
111
+ padding-top: 3px;
112
+ border-bottom: 0.375rem solid;
113
+ @include brand-03(border-bottom-color);
114
+ }
115
+ }
116
+ }
117
+
118
+ .tile {
119
+ text-align: center;
120
+ border: 1px solid $ui-03;
121
+ }
122
+
123
+ .filterEmptyState {
124
+ display: flex;
125
+ justify-content: center;
126
+ align-items: center;
127
+ padding: layout.$spacing-05;
128
+ margin: layout.$spacing-09;
129
+ text-align: center;
130
+ }
131
+
132
+ .filterEmptyStateTile {
133
+ margin: auto;
134
+ }
135
+
136
+ .filterEmptyStateContent {
137
+ @include type.type-style('heading-compact-02');
138
+ color: $text-02;
139
+ margin-bottom: 0.5rem;
140
+ }
141
+
142
+ .filterEmptyStateHelper {
143
+ @include type.type-style('body-compact-01');
144
+ color: $text-02;
145
+ }
146
+
147
+ .table {
148
+ tr[data-child-row] td {
149
+ padding-left: 2rem !important;
150
+ }
151
+ }
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { useBills } from '../billing.resource';
5
+ import BillHistory from './bill-history.component';
6
+
7
+ const testProps = {
8
+ patientUuid: 'some-uuid',
9
+ };
10
+
11
+ const mockbills = useBills as jest.MockedFunction<typeof useBills>;
12
+
13
+ const mockBillsData = [
14
+ { uuid: '1', patientName: 'John Doe', identifier: '12345678', billingService: 'Checkup', totalAmount: 500 },
15
+ { uuid: '2', patientName: 'John Doe', identifier: '12345678', billingService: 'Consulatation', totalAmount: 600 },
16
+ { uuid: '3', patientName: 'John Doe', identifier: '12345678', billingService: 'Child services', totalAmount: 700 },
17
+ { uuid: '4', patientName: 'John Doe', identifier: '12345678', billingService: 'Medication', totalAmount: 800 },
18
+ { uuid: '5', patientName: 'John Doe', identifier: '12345678', billingService: 'Lab', totalAmount: 900 },
19
+ { uuid: '6', patientName: 'John Doe', identifier: '12345678', billingService: 'Pharmacy', totalAmount: 400 },
20
+ { uuid: '7', patientName: 'John Doe', identifier: '12345678', billingService: 'Nutrition', totalAmount: 300 },
21
+ { uuid: '8', patientName: 'John Doe', identifier: '12345678', billingService: 'Physiotherapy', totalAmount: 200 },
22
+ { uuid: '9', patientName: 'John Doe', identifier: '12345678', billingService: 'Dentist', totalAmount: 1100 },
23
+ { uuid: '10', patientName: 'John Doe', identifier: '12345678', billingService: 'Neuro', totalAmount: 1200 },
24
+ { uuid: '11', patientName: 'John Doe', identifier: '12345678', billingService: 'Outpatient', totalAmount: 1050 },
25
+ { uuid: '12', patientName: 'John Doe', identifier: '12345678', billingService: 'MCH', totalAmount: 1300 },
26
+ ];
27
+
28
+ jest.mock('../invoice/invoice-table.component', () => jest.fn(() => <div>Invoice table</div>));
29
+
30
+ jest.mock('../billing.resource', () => ({
31
+ ...jest.requireActual('../billing.resource'),
32
+ useBills: jest.fn(() => ({
33
+ bills: mockBillsData,
34
+ isLoading: false,
35
+ isValidating: false,
36
+ error: null,
37
+ })),
38
+ }));
39
+
40
+ jest.mock('@openmrs/esm-framework', () => ({
41
+ ...jest.requireActual('@openmrs/esm-framework'),
42
+ useLayoutType: jest.fn(() => 'small-desktop'),
43
+ usePagination: jest.fn().mockImplementation((data) => ({
44
+ currentPage: 1,
45
+ goTo: () => {},
46
+ results: data,
47
+ paginated: true,
48
+ })),
49
+ }));
50
+
51
+ describe('BillHistory', () => {
52
+ afterEach(() => {
53
+ jest.clearAllMocks();
54
+ });
55
+
56
+ test('should render loading datatable skeleton', () => {
57
+ mockbills.mockReturnValueOnce({ isLoading: true, isValidating: false, error: null, bills: [], mutate: jest.fn() });
58
+ render(<BillHistory {...testProps} />);
59
+ const loadingSkeleton = screen.getByRole('table');
60
+ expect(loadingSkeleton).toBeInTheDocument();
61
+ expect(loadingSkeleton).toHaveClass('cds--skeleton cds--data-table cds--data-table--zebra');
62
+ });
63
+
64
+ test('should render error state when API call fails', () => {
65
+ mockbills.mockReturnValueOnce({
66
+ isLoading: false,
67
+ isValidating: false,
68
+ error: new Error('some error'),
69
+ bills: [],
70
+ mutate: jest.fn(),
71
+ });
72
+ render(<BillHistory {...testProps} />);
73
+ const errorState = screen.getByText(/Sorry, there was a problem displaying this information./);
74
+ expect(errorState).toBeInTheDocument();
75
+ });
76
+
77
+ xtest('should render bills table', async () => {
78
+ const user = userEvent.setup();
79
+ mockbills.mockReturnValueOnce({
80
+ isLoading: false,
81
+ isValidating: false,
82
+ error: null,
83
+ bills: mockBillsData as any,
84
+ mutate: jest.fn(),
85
+ });
86
+ render(<BillHistory {...testProps} />);
87
+ expect(screen.getByText('Visit time')).toBeInTheDocument();
88
+ expect(screen.getByText('Identifier')).toBeInTheDocument();
89
+ const expectedColumnHeaders = [/Visit time/, /Identifier/, /Billing service/, /Bill total/];
90
+ expectedColumnHeaders.forEach((header) => {
91
+ expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
92
+ });
93
+
94
+ const tableRowGroup = screen.getAllByRole('rowgroup');
95
+ expect(tableRowGroup).toHaveLength(2);
96
+
97
+ // Page navigation should work as expected
98
+ const nextPageButton = screen.getByRole('button', { name: /Next page/ });
99
+ const prevPageButton = screen.getByRole('button', { name: /Previous page/ });
100
+
101
+ expect(nextPageButton).toBeInTheDocument();
102
+ expect(prevPageButton).toBeInTheDocument();
103
+
104
+ expect(screen.getByText(/1–10 of 12 items/)).toBeInTheDocument();
105
+ await user.click(nextPageButton);
106
+ expect(screen.getByText(/11–12 of 12 items/)).toBeInTheDocument();
107
+ await user.click(prevPageButton);
108
+ expect(screen.getByText(/1–10 of 12 items/)).toBeInTheDocument();
109
+
110
+ // clicking the row should expand the row
111
+ const expandAllRowButton = screen.getByRole('button', { name: /Expand all rows/ });
112
+ expect(expandAllRowButton).toBeInTheDocument();
113
+ await user.click(expandAllRowButton);
114
+ });
115
+
116
+ test('should render empty state view when there are no bills', () => {
117
+ mockbills.mockReturnValueOnce({ isLoading: false, isValidating: false, error: null, bills: [], mutate: jest.fn() });
118
+ render(<BillHistory {...testProps} />);
119
+ const emptyState = screen.getByText(/There are no bills to display./);
120
+ expect(emptyState).toBeInTheDocument();
121
+ });
122
+ });
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import {
3
+ Checkbox,
4
+ Layer,
5
+ StructuredListBody,
6
+ StructuredListCell,
7
+ StructuredListHead,
8
+ StructuredListRow,
9
+ StructuredListWrapper,
10
+ } from '@carbon/react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { convertToCurrency } from '../../helpers';
13
+ import { type MappedBill, type LineItem } from '../../types';
14
+ import BillWaiverForm from './bill-waiver-form.component';
15
+ import styles from './bill-waiver.scss';
16
+ import { useConfig } from '@openmrs/esm-framework';
17
+
18
+ const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (patientUuid) => void }> = ({
19
+ bills,
20
+ setPatientUuid,
21
+ }) => {
22
+ const { t } = useTranslation();
23
+ const [selectedBills, setSelectedBills] = React.useState<Array<LineItem>>([]);
24
+ const { defaultCurrency } = useConfig();
25
+
26
+ const checkBoxLabel = (lineItem) => {
27
+ return `${lineItem.item === '' ? lineItem.billableService : lineItem.item} ${convertToCurrency(lineItem.price, defaultCurrency)}`;
28
+ };
29
+
30
+ const handleOnCheckBoxChange = (event, { checked, id }) => {
31
+ const selectedLineItem = bills.lineItems.find((lineItem) => lineItem.uuid === id);
32
+ if (checked) {
33
+ setSelectedBills([...selectedBills, selectedLineItem]);
34
+ } else {
35
+ setSelectedBills(selectedBills.filter((lineItem) => lineItem.uuid !== id));
36
+ }
37
+ };
38
+ return (
39
+ <Layer>
40
+ <StructuredListWrapper className={styles.billListContainer} isCondensed selection={true}>
41
+ <StructuredListHead>
42
+ <StructuredListRow head>
43
+ <StructuredListCell head>{t('billItem', 'Bill item')}</StructuredListCell>
44
+ <StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell>
45
+ <StructuredListCell head>{t('unitPrice', 'Unit Price')}</StructuredListCell>
46
+ <StructuredListCell head>{t('total', 'Total')}</StructuredListCell>
47
+ <StructuredListCell head>{t('actions', 'Actions')}</StructuredListCell>
48
+ </StructuredListRow>
49
+ </StructuredListHead>
50
+ <StructuredListBody>
51
+ {bills?.lineItems.map((lineItem) => (
52
+ <StructuredListRow>
53
+ <StructuredListCell>{lineItem.item === '' ? lineItem.billableService : lineItem.item}</StructuredListCell>
54
+ <StructuredListCell>{lineItem.quantity}</StructuredListCell>
55
+ <StructuredListCell>{convertToCurrency(lineItem.price, defaultCurrency)}</StructuredListCell>
56
+ <StructuredListCell>
57
+ {convertToCurrency(lineItem.price * lineItem.quantity, defaultCurrency)}
58
+ </StructuredListCell>
59
+ <StructuredListCell>
60
+ <Checkbox
61
+ hideLabel
62
+ onChange={(event, { checked, id }) => handleOnCheckBoxChange(event, { checked, id })}
63
+ labelText={checkBoxLabel(lineItem)}
64
+ id={lineItem.uuid}
65
+ />
66
+ </StructuredListCell>
67
+ </StructuredListRow>
68
+ ))}
69
+ </StructuredListBody>
70
+ </StructuredListWrapper>
71
+ <BillWaiverForm bill={bills} lineItems={selectedBills} setPatientUuid={setPatientUuid} />
72
+ </Layer>
73
+ );
74
+ };
75
+
76
+ export default PatientBillsSelections;
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { Form, Stack, FormGroup, Layer, Button, NumberInput } from '@carbon/react';
3
+ import { TaskAdd } from '@carbon/react/icons';
4
+ import { mutate } from 'swr';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { restBaseUrl, showSnackbar, useConfig } from '@openmrs/esm-framework';
7
+ import { createBillWaiverPayload } from './utils';
8
+ import { convertToCurrency } from '../../helpers';
9
+ import { processBillPayment } from '../../billing.resource';
10
+ import { useBillableItems } from '../../billing-form/billing-form.resource';
11
+ import type { LineItem, MappedBill } from '../../types';
12
+ import styles from './bill-waiver-form.scss';
13
+ import { apiBasePath } from '../../constants';
14
+
15
+ type BillWaiverFormProps = {
16
+ bill: MappedBill;
17
+ lineItems: Array<LineItem>;
18
+ setPatientUuid: (patientUuid) => void;
19
+ };
20
+
21
+ const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPatientUuid }) => {
22
+ const { t } = useTranslation();
23
+ const [waiverAmount, setWaiverAmount] = React.useState(0);
24
+ const { lineItems: billableLineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
25
+ const totalAmount = lineItems.reduce((acc, curr) => acc + curr.price * curr.quantity, 0);
26
+ const { defaultCurrency } = useConfig();
27
+
28
+ if (lineItems?.length === 0) {
29
+ return null;
30
+ }
31
+
32
+ const handleProcessPayment = (event) => {
33
+ const waiverEndPointPayload = createBillWaiverPayload(
34
+ bill,
35
+ waiverAmount,
36
+ totalAmount,
37
+ lineItems,
38
+ billableLineItems,
39
+ );
40
+
41
+ processBillPayment(waiverEndPointPayload, bill.uuid).then(
42
+ (resp) => {
43
+ showSnackbar({
44
+ title: t('billWaiver', 'Bill waiver'),
45
+ subtitle: t('billWaiverSuccess', 'Bill waiver successful'),
46
+ kind: 'success',
47
+ timeoutInMs: 3500,
48
+ isLowContrast: true,
49
+ });
50
+ setPatientUuid('');
51
+ mutate((key) => typeof key === 'string' && key.startsWith(`${apiBasePath}bill?v=full`), undefined, {
52
+ revalidate: true,
53
+ });
54
+ },
55
+ (err) => {
56
+ showSnackbar({
57
+ title: t('billWaiver', 'Bill waiver'),
58
+ subtitle: t('billWaiverError', 'Bill waiver failed {{error}}', { error: err.message }),
59
+ kind: 'error',
60
+ timeoutInMs: 3500,
61
+ isLowContrast: true,
62
+ });
63
+ },
64
+ );
65
+ };
66
+
67
+ return (
68
+ <Form className={styles.billWaiverForm} aria-label={t('waiverForm', 'Waiver form')}>
69
+ <hr />
70
+ <Stack gap={7}>
71
+ <FormGroup>
72
+ <section className={styles.billWaiverDescription}>
73
+ <label className={styles.label}>{t('billItems', 'Bill Items')}</label>
74
+ <p className={styles.value}>
75
+ {t('billName', ' {{billName}} ', {
76
+ billName: lineItems.map((item) => item.item || item.billableService).join(', ') ?? '--',
77
+ })}
78
+ </p>
79
+ </section>
80
+ <section className={styles.billWaiverDescription}>
81
+ <label className={styles.label}>{t('billTotal', 'Bill total')}</label>
82
+ <p className={styles.value}>{convertToCurrency(totalAmount, defaultCurrency)}</p>
83
+ </section>
84
+
85
+ <Layer className={styles.formControlLayer}>
86
+ <NumberInput
87
+ label={t('amountToWaiveLabel', 'Amount to Waive')}
88
+ helperText={t('amountToWaiveHelper', 'Specify the amount to be deducted from the bill')}
89
+ aria-label={t('amountToWaiveAriaLabel', 'Enter amount to waive')}
90
+ hideSteppers
91
+ disableWheel
92
+ min={0}
93
+ max={totalAmount}
94
+ invalidText={t('invalidWaiverAmount', 'Invalid waiver amount')}
95
+ value={waiverAmount}
96
+ onChange={(event) => setWaiverAmount(event.target.value)}
97
+ />
98
+ </Layer>
99
+ </FormGroup>
100
+ <div className={styles.buttonContainer}>
101
+ <Button kind="tertiary" renderIcon={TaskAdd} onClick={handleProcessPayment}>
102
+ {t('postWaiver', 'Post waiver')}
103
+ </Button>
104
+ </div>
105
+ </Stack>
106
+ </Form>
107
+ );
108
+ };
109
+
110
+ export default BillWaiverForm;
@@ -0,0 +1,34 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+
4
+ .billWaiverForm {
5
+ margin-top: layout.$spacing-05;
6
+ padding: 0 layout.$spacing-05;
7
+ }
8
+
9
+ .buttonContainer {
10
+ display: flex;
11
+ justify-content: flex-end;
12
+ margin-top: -1rem;
13
+ }
14
+
15
+ .formControlLayer {
16
+ padding: layout.$spacing-05 0;
17
+ }
18
+
19
+ .billWaiverDescription {
20
+ display: grid;
21
+ grid-template-columns: 5rem 1fr;
22
+ column-gap: 1rem;
23
+ align-items: center;
24
+ margin-top: 0.125rem;
25
+ margin-bottom: 0.5rem;
26
+ }
27
+
28
+ .label {
29
+ @include type.type-style('heading-compact-01');
30
+ }
31
+
32
+ .value {
33
+ @include type.type-style('body-01');
34
+ }