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

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 (167) 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 +392 -0
  12. package/__mocks__/delivery-summary.mock.ts +87 -0
  13. package/__mocks__/encounter-observation.mock.ts +10649 -0
  14. package/__mocks__/encounter-observations.mock.ts +6187 -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/294.js +2 -0
  21. package/dist/294.js.LICENSE.txt +9 -0
  22. package/dist/294.js.map +1 -0
  23. package/dist/319.js +1 -0
  24. package/dist/384.js +1 -0
  25. package/dist/384.js.map +1 -0
  26. package/dist/421.js +1 -0
  27. package/dist/421.js.map +1 -0
  28. package/dist/450.js +1 -0
  29. package/dist/450.js.map +1 -0
  30. package/dist/476.js +1 -0
  31. package/dist/476.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/757.js +1 -0
  34. package/dist/788.js +1 -0
  35. package/dist/800.js +2 -0
  36. package/dist/800.js.LICENSE.txt +3 -0
  37. package/dist/800.js.map +1 -0
  38. package/dist/807.js +1 -0
  39. package/dist/833.js +1 -0
  40. package/dist/935.js +2 -0
  41. package/dist/935.js.LICENSE.txt +19 -0
  42. package/dist/935.js.map +1 -0
  43. package/dist/96.js +2 -0
  44. package/dist/96.js.LICENSE.txt +47 -0
  45. package/dist/96.js.map +1 -0
  46. package/dist/main.js +2 -0
  47. package/dist/main.js.LICENSE.txt +47 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/openmrs-esm-billing-app.js +1 -0
  50. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +462 -0
  51. package/dist/openmrs-esm-billing-app.js.map +1 -0
  52. package/dist/routes.json +1 -0
  53. package/e2e/README.md +115 -0
  54. package/e2e/core/global-setup.ts +32 -0
  55. package/e2e/core/index.ts +1 -0
  56. package/e2e/core/test.ts +20 -0
  57. package/e2e/fixtures/api.ts +26 -0
  58. package/e2e/fixtures/index.ts +1 -0
  59. package/e2e/pages/home-page.ts +9 -0
  60. package/e2e/pages/index.ts +1 -0
  61. package/e2e/specs/sample-test.spec.ts +11 -0
  62. package/e2e/support/github/Dockerfile +34 -0
  63. package/e2e/support/github/docker-compose.yml +24 -0
  64. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  65. package/example.env +6 -0
  66. package/i18next-parser.config.js +89 -0
  67. package/jest.config.js +34 -0
  68. package/package.json +123 -0
  69. package/playwright.config.ts +32 -0
  70. package/prettier.config.js +8 -0
  71. package/src/bill-history/bill-history.component.tsx +187 -0
  72. package/src/bill-history/bill-history.scss +151 -0
  73. package/src/bill-history/bill-history.test.tsx +122 -0
  74. package/src/billable-services/bill-waiver/bill-selection.component.tsx +72 -0
  75. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +108 -0
  76. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  77. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  78. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  79. package/src/billable-services/bill-waiver/patient-bills.component.tsx +135 -0
  80. package/src/billable-services/bill-waiver/utils.ts +41 -0
  81. package/src/billable-services/billable-service.resource.ts +71 -0
  82. package/src/billable-services/billable-services-home.component.tsx +51 -0
  83. package/src/billable-services/billable-services.component.tsx +255 -0
  84. package/src/billable-services/billable-services.scss +218 -0
  85. package/src/billable-services/billable-services.test.tsx +16 -0
  86. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  87. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  88. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  89. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  90. package/src/billable-services/dashboard/dashboard.scss +27 -0
  91. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  92. package/src/billable-services/dashboard/service-metrics.component.tsx +42 -0
  93. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  94. package/src/billable-services-admin-card-link.component.tsx +25 -0
  95. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  96. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  97. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  98. package/src/billing-form/billing-checkin-form.component.tsx +131 -0
  99. package/src/billing-form/billing-checkin-form.scss +13 -0
  100. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  101. package/src/billing-form/billing-form.component.tsx +25 -0
  102. package/src/billing-form/billing-form.resource.ts +31 -0
  103. package/src/billing-form/billing-form.scss +5 -0
  104. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  105. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  106. package/src/billing-header/billing-header.component.tsx +43 -0
  107. package/src/billing-header/billing-header.scss +83 -0
  108. package/src/billing-header/billing-illustration.component.tsx +30 -0
  109. package/src/billing.resource.ts +120 -0
  110. package/src/bills-table/bills-table.component.tsx +280 -0
  111. package/src/bills-table/bills-table.scss +181 -0
  112. package/src/bills-table/bills-table.test.tsx +154 -0
  113. package/src/config-schema.ts +3 -0
  114. package/src/dashboard.meta.ts +6 -0
  115. package/src/declarations.d.ts +4 -0
  116. package/src/helpers/functions.ts +63 -0
  117. package/src/helpers/index.ts +1 -0
  118. package/src/index.ts +56 -0
  119. package/src/invoice/invoice-table.component.tsx +185 -0
  120. package/src/invoice/invoice-table.scss +91 -0
  121. package/src/invoice/invoice.component.tsx +138 -0
  122. package/src/invoice/invoice.scss +93 -0
  123. package/src/invoice/invoice.test.tsx +242 -0
  124. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  125. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  126. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  127. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  128. package/src/invoice/payments/payment-history/payment-history.component.tsx +68 -0
  129. package/src/invoice/payments/payment.resource.ts +43 -0
  130. package/src/invoice/payments/payments.component.tsx +140 -0
  131. package/src/invoice/payments/payments.scss +46 -0
  132. package/src/invoice/payments/utils.ts +30 -0
  133. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  134. package/src/invoice/printable-invoice/print-receipt.component.tsx +28 -0
  135. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  136. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  137. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  138. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  139. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  140. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  141. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  142. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  143. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  144. package/src/left-panel-link.component.tsx +41 -0
  145. package/src/left-panel-link.test.tsx +38 -0
  146. package/src/metrics-cards/card.component.tsx +11 -0
  147. package/src/metrics-cards/card.scss +20 -0
  148. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  149. package/src/metrics-cards/metrics-cards.scss +12 -0
  150. package/src/metrics-cards/metrics-cards.test.tsx +41 -0
  151. package/src/metrics-cards/metrics.resource.ts +45 -0
  152. package/src/modal/require-payment-modal.component.tsx +81 -0
  153. package/src/modal/require-payment.scss +6 -0
  154. package/src/root.component.tsx +19 -0
  155. package/src/root.scss +30 -0
  156. package/src/routes.json +79 -0
  157. package/src/setup-tests.ts +13 -0
  158. package/src/types/index.ts +167 -0
  159. package/test-helpers.tsx +23 -0
  160. package/translations/am.json +107 -0
  161. package/translations/en.json +107 -0
  162. package/translations/es.json +107 -0
  163. package/translations/fr.json +107 -0
  164. package/translations/he.json +107 -0
  165. package/translations/km.json +107 -0
  166. package/tsconfig.json +16 -0
  167. package/webpack.config.js +1 -0
@@ -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,72 @@
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
+
17
+ const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (patientUuid) => void }> = ({
18
+ bills,
19
+ setPatientUuid,
20
+ }) => {
21
+ const { t } = useTranslation();
22
+ const [selectedBills, setSelectedBills] = React.useState<Array<LineItem>>([]);
23
+
24
+ const checkBoxLabel = (lineItem) => {
25
+ return `${lineItem.item === '' ? lineItem.billableService : lineItem.item} ${convertToCurrency(lineItem.price)}`;
26
+ };
27
+
28
+ const handleOnCheckBoxChange = (event, { checked, id }) => {
29
+ const selectedLineItem = bills.lineItems.find((lineItem) => lineItem.uuid === id);
30
+ if (checked) {
31
+ setSelectedBills([...selectedBills, selectedLineItem]);
32
+ } else {
33
+ setSelectedBills(selectedBills.filter((lineItem) => lineItem.uuid !== id));
34
+ }
35
+ };
36
+ return (
37
+ <Layer>
38
+ <StructuredListWrapper className={styles.billListContainer} isCondensed selection={true}>
39
+ <StructuredListHead>
40
+ <StructuredListRow head>
41
+ <StructuredListCell head>{t('billItem', 'Bill item')}</StructuredListCell>
42
+ <StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell>
43
+ <StructuredListCell head>{t('unitPrice', 'Unit Price')}</StructuredListCell>
44
+ <StructuredListCell head>{t('total', 'Total')}</StructuredListCell>
45
+ <StructuredListCell head>{t('actions', 'Actions')}</StructuredListCell>
46
+ </StructuredListRow>
47
+ </StructuredListHead>
48
+ <StructuredListBody>
49
+ {bills?.lineItems.map((lineItem) => (
50
+ <StructuredListRow>
51
+ <StructuredListCell>{lineItem.item === '' ? lineItem.billableService : lineItem.item}</StructuredListCell>
52
+ <StructuredListCell>{lineItem.quantity}</StructuredListCell>
53
+ <StructuredListCell>{convertToCurrency(lineItem.price)}</StructuredListCell>
54
+ <StructuredListCell>{convertToCurrency(lineItem.price * lineItem.quantity)}</StructuredListCell>
55
+ <StructuredListCell>
56
+ <Checkbox
57
+ hideLabel
58
+ onChange={(event, { checked, id }) => handleOnCheckBoxChange(event, { checked, id })}
59
+ labelText={checkBoxLabel(lineItem)}
60
+ id={lineItem.uuid}
61
+ />
62
+ </StructuredListCell>
63
+ </StructuredListRow>
64
+ ))}
65
+ </StructuredListBody>
66
+ </StructuredListWrapper>
67
+ <BillWaiverForm bill={bills} lineItems={selectedBills} setPatientUuid={setPatientUuid} />
68
+ </Layer>
69
+ );
70
+ };
71
+
72
+ export default PatientBillsSelections;
@@ -0,0 +1,108 @@
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 { showSnackbar } 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
+
14
+ type BillWaiverFormProps = {
15
+ bill: MappedBill;
16
+ lineItems: Array<LineItem>;
17
+ setPatientUuid: (patientUuid) => void;
18
+ };
19
+
20
+ const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPatientUuid }) => {
21
+ const { t } = useTranslation();
22
+ const [waiverAmount, setWaiverAmount] = React.useState(0);
23
+ const { lineItems: billableLineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
24
+ const totalAmount = lineItems.reduce((acc, curr) => acc + curr.price * curr.quantity, 0);
25
+
26
+ if (lineItems?.length === 0) {
27
+ return null;
28
+ }
29
+
30
+ const handleProcessPayment = (event) => {
31
+ const waiverEndPointPayload = createBillWaiverPayload(
32
+ bill,
33
+ waiverAmount,
34
+ totalAmount,
35
+ lineItems,
36
+ billableLineItems,
37
+ );
38
+
39
+ processBillPayment(waiverEndPointPayload, bill.uuid).then(
40
+ (resp) => {
41
+ showSnackbar({
42
+ title: t('billWaiver', 'Bill waiver'),
43
+ subtitle: t('billWaiverSuccess', 'Bill waiver successful'),
44
+ kind: 'success',
45
+ timeoutInMs: 3500,
46
+ isLowContrast: true,
47
+ });
48
+ setPatientUuid('');
49
+ mutate((key) => typeof key === 'string' && key.startsWith('/ws/rest/v1/cashier/bill?v=full'), undefined, {
50
+ revalidate: true,
51
+ });
52
+ },
53
+ (err) => {
54
+ showSnackbar({
55
+ title: t('billWaiver', 'Bill waiver'),
56
+ subtitle: t('billWaiverError', 'Bill waiver failed {{error}}', { error: err.message }),
57
+ kind: 'error',
58
+ timeoutInMs: 3500,
59
+ isLowContrast: true,
60
+ });
61
+ },
62
+ );
63
+ };
64
+
65
+ return (
66
+ <Form className={styles.billWaiverForm} aria-label={t('waiverForm', 'Waiver form')}>
67
+ <hr />
68
+ <Stack gap={7}>
69
+ <FormGroup>
70
+ <section className={styles.billWaiverDescription}>
71
+ <label className={styles.label}>{t('billItems', 'Bill Items')}</label>
72
+ <p className={styles.value}>
73
+ {t('billName', ' {{billName}} ', {
74
+ billName: lineItems.map((item) => item.item || item.billableService).join(', ') ?? '--',
75
+ })}
76
+ </p>
77
+ </section>
78
+ <section className={styles.billWaiverDescription}>
79
+ <label className={styles.label}>{t('billTotal', 'Bill total')}</label>
80
+ <p className={styles.value}>{convertToCurrency(totalAmount)}</p>
81
+ </section>
82
+
83
+ <Layer className={styles.formControlLayer}>
84
+ <NumberInput
85
+ label={t('amountToWaiveLabel', 'Amount to Waive')}
86
+ helperText={t('amountToWaiveHelper', 'Specify the amount to be deducted from the bill')}
87
+ aria-label={t('amountToWaiveAriaLabel', 'Enter amount to waive')}
88
+ hideSteppers
89
+ disableWheel
90
+ min={0}
91
+ max={totalAmount}
92
+ invalidText={t('invalidWaiverAmount', 'Invalid waiver amount')}
93
+ value={waiverAmount}
94
+ onChange={(event) => setWaiverAmount(event.target.value)}
95
+ />
96
+ </Layer>
97
+ </FormGroup>
98
+ <div className={styles.buttonContainer}>
99
+ <Button kind="tertiary" renderIcon={TaskAdd} onClick={handleProcessPayment}>
100
+ {t('postWaiver', 'Post waiver')}
101
+ </Button>
102
+ </div>
103
+ </Stack>
104
+ </Form>
105
+ );
106
+ };
107
+
108
+ 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
+ }
@@ -0,0 +1,32 @@
1
+ import React, { useState } from 'react';
2
+ import { ExtensionSlot, UserHasAccess } from '@openmrs/esm-framework';
3
+ import PatientBills from './patient-bills.component';
4
+ import { useBills } from '../../billing.resource';
5
+ import styles from './bill-waiver.scss';
6
+
7
+ type BillWaiverProps = {};
8
+
9
+ const BillWaiver: React.FC<BillWaiverProps> = () => {
10
+ const [patientUuid, setPatientUuid] = useState<string>('');
11
+ const { bills } = useBills(patientUuid);
12
+ const filterBills = bills.filter((bill) => bill.status !== 'PAID' && patientUuid === bill.patientUuid) ?? [];
13
+ return (
14
+ <UserHasAccess privilege="coreapps.systemAdministration">
15
+ <div className={styles.billWaiverContainer}>
16
+ <ExtensionSlot
17
+ name="patient-search-bar-slot"
18
+ state={{
19
+ selectPatientAction: (patientUuid) => setPatientUuid(patientUuid),
20
+ buttonProps: {
21
+ kind: 'primary',
22
+ },
23
+ }}
24
+ />
25
+
26
+ <PatientBills patientUuid={patientUuid} bills={filterBills} setPatientUuid={setPatientUuid} />
27
+ </div>
28
+ </UserHasAccess>
29
+ );
30
+ };
31
+
32
+ export default BillWaiver;
@@ -0,0 +1,10 @@
1
+ @use '@carbon/layout';
2
+
3
+ .billWaiverContainer {
4
+ margin: layout.$layout-01;
5
+ row-gap: layout.$layout-01;
6
+ }
7
+
8
+ .billListContainer {
9
+ background-color: white;
10
+ }
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import {
3
+ DataTable,
4
+ Layer,
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableContainer,
9
+ TableExpandedRow,
10
+ TableExpandHeader,
11
+ TableExpandRow,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ Tile,
16
+ } from '@carbon/react';
17
+ import { useTranslation } from 'react-i18next';
18
+ import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
19
+ import { type MappedBill } from '../../types';
20
+ import { convertToCurrency } from '../../helpers';
21
+ import PatientBillsSelections from './bill-selection.component';
22
+ import styles from '../../bills-table/bills-table.scss';
23
+
24
+ type PatientBillsProps = {
25
+ patientUuid: string;
26
+ bills: Array<MappedBill>;
27
+ setPatientUuid: (patientUuid: string) => void;
28
+ };
29
+
30
+ const PatientBills: React.FC<PatientBillsProps> = ({ patientUuid, bills, setPatientUuid }) => {
31
+ const { t } = useTranslation();
32
+
33
+ if (!patientUuid) {
34
+ return;
35
+ }
36
+
37
+ const tableHeaders = [
38
+ { header: 'Date', key: 'date' },
39
+ { header: 'Billable Service', key: 'billableService' },
40
+ { header: 'Total Amount', key: 'totalAmount' },
41
+ ];
42
+
43
+ const tableRows = bills.map((bill) => ({
44
+ id: `${bill.uuid}`,
45
+ date: bill.dateCreated,
46
+ billableService: bill.billingService,
47
+ totalAmount: convertToCurrency(bill.totalAmount),
48
+ }));
49
+
50
+ if (bills.length === 0 && patientUuid !== '') {
51
+ return (
52
+ <>
53
+ <div style={{ marginTop: '0.625rem' }}>
54
+ <Layer className={styles.emptyStateContainer}>
55
+ <Tile className={styles.tile}>
56
+ <div className={styles.illo}>
57
+ <EmptyDataIllustration />
58
+ </div>
59
+ <p className={styles.content}>{t('noBilltoDisplay', 'There are no bills to display for this patient')}</p>
60
+ </Tile>
61
+ </Layer>
62
+ </div>
63
+ </>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <div style={{ marginTop: '1rem' }}>
69
+ <DataTable
70
+ rows={tableRows}
71
+ headers={tableHeaders}
72
+ size="sm"
73
+ useZebraStyles
74
+ render={({
75
+ rows,
76
+ headers,
77
+ getHeaderProps,
78
+ getExpandHeaderProps,
79
+ getRowProps,
80
+ getExpandedRowProps,
81
+ getTableProps,
82
+ getTableContainerProps,
83
+ }) => (
84
+ <TableContainer
85
+ title={t('patientBills', 'Patient bill')}
86
+ description={t('patientBillsDescription', 'List of patient bills')}
87
+ {...getTableContainerProps()}>
88
+ <Table {...getTableProps()} aria-label="sample table">
89
+ <TableHead>
90
+ <TableRow>
91
+ <TableExpandHeader enableToggle={true} {...getExpandHeaderProps()} />
92
+ {headers.map((header, i) => (
93
+ <TableHeader
94
+ key={i}
95
+ {...getHeaderProps({
96
+ header,
97
+ })}>
98
+ {header.header}
99
+ </TableHeader>
100
+ ))}
101
+ </TableRow>
102
+ </TableHead>
103
+ <TableBody>
104
+ {rows.map((row, index) => (
105
+ <React.Fragment key={row.id}>
106
+ <TableExpandRow
107
+ {...getRowProps({
108
+ row,
109
+ })}>
110
+ {row.cells.map((cell) => (
111
+ <TableCell key={cell.id}>{cell.value}</TableCell>
112
+ ))}
113
+ </TableExpandRow>
114
+ <TableExpandedRow
115
+ colSpan={headers.length + 1}
116
+ className="demo-expanded-td"
117
+ {...getExpandedRowProps({
118
+ row,
119
+ })}>
120
+ <div>
121
+ <PatientBillsSelections bills={bills[index]} setPatientUuid={setPatientUuid} />
122
+ </div>
123
+ </TableExpandedRow>
124
+ </React.Fragment>
125
+ ))}
126
+ </TableBody>
127
+ </Table>
128
+ </TableContainer>
129
+ )}
130
+ />
131
+ </div>
132
+ );
133
+ };
134
+
135
+ export default PatientBills;