@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,181 @@
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
+ .billListContainer {
15
+ background-color: $ui-02;
16
+ border: 1px solid $ui-03;
17
+ width: 100%;
18
+ margin: 0 auto;
19
+ max-width: 95vw;
20
+ padding-bottom: 0;
21
+
22
+ :has(.filterEmptyState) {
23
+ border-bottom: none;
24
+ }
25
+ }
26
+
27
+ .filterContainer {
28
+ flex: 1;
29
+
30
+ :global(.cds--dropdown__wrapper--inline) {
31
+ gap: 0;
32
+ }
33
+
34
+ :global(.cds--list-box__menu-icon) {
35
+ height: 1rem;
36
+ }
37
+
38
+ :global(.cds--list-box__menu) {
39
+ min-width: max-content;
40
+ }
41
+
42
+ :global(.cds--list-box) {
43
+ margin-left: layout.$spacing-03;
44
+ }
45
+ }
46
+
47
+ .menu {
48
+ margin-left: layout.$spacing-03;
49
+ }
50
+
51
+ .headerContainer {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ padding: layout.$spacing-04 layout.$spacing-05;
56
+ background-color: $ui-02;
57
+ }
58
+
59
+ .backgroundDataFetchingIndicator {
60
+ align-items: center;
61
+ display: flex;
62
+ flex: 1;
63
+ justify-content: space-between;
64
+
65
+ &:global(.cds--inline-loading) {
66
+ max-height: 1rem;
67
+ }
68
+ }
69
+
70
+ .tableContainer section {
71
+ position: relative;
72
+ }
73
+
74
+ .tableContainer a {
75
+ text-decoration: none;
76
+ }
77
+
78
+ .pagination {
79
+ overflow: hidden;
80
+
81
+ &:global(.cds--pagination) {
82
+ border-top: none;
83
+ }
84
+ }
85
+
86
+ .hiddenRow {
87
+ display: none;
88
+ }
89
+
90
+ .emptyRow {
91
+ padding: 0 1rem;
92
+ display: flex;
93
+ align-items: center;
94
+ }
95
+
96
+ .visitSummaryContainer {
97
+ width: 100%;
98
+ max-width: 768px;
99
+ margin: 1rem auto;
100
+ }
101
+
102
+ .expandedActiveVisitRow > td > div {
103
+ max-height: max-content !important;
104
+ }
105
+
106
+ .expandedActiveVisitRow td {
107
+ padding: 0 2rem;
108
+ }
109
+
110
+ .expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
111
+ padding: 0 1rem;
112
+ }
113
+
114
+ .action {
115
+ margin-bottom: layout.$spacing-03;
116
+ }
117
+
118
+ .illo {
119
+ margin-top: layout.$spacing-05;
120
+ }
121
+
122
+ .content {
123
+ @include type.type-style('heading-compact-01');
124
+ color: $text-02;
125
+ margin-top: layout.$spacing-05;
126
+ margin-bottom: layout.$spacing-03;
127
+ }
128
+
129
+ .desktopHeading,
130
+ .tabletHeading {
131
+ text-align: left;
132
+ text-transform: capitalize;
133
+ flex: 1;
134
+
135
+ h4 {
136
+ @include type.type-style('heading-compact-02');
137
+ color: $text-02;
138
+
139
+ &:after {
140
+ content: '';
141
+ display: block;
142
+ width: 2rem;
143
+ padding-top: 3px;
144
+ border-bottom: 0.375rem solid;
145
+ @include brand-03(border-bottom-color);
146
+ }
147
+ }
148
+ }
149
+
150
+ .tile {
151
+ text-align: center;
152
+ border: 1px solid $ui-03;
153
+ }
154
+
155
+ .menuitem {
156
+ max-width: none;
157
+ }
158
+
159
+ .filterEmptyState {
160
+ display: flex;
161
+ justify-content: center;
162
+ align-items: center;
163
+ padding: layout.$spacing-05;
164
+ margin: layout.$spacing-09;
165
+ text-align: center;
166
+ }
167
+
168
+ .filterEmptyStateTile {
169
+ margin: auto;
170
+ }
171
+
172
+ .filterEmptyStateContent {
173
+ @include type.type-style('heading-compact-02');
174
+ color: $text-02;
175
+ margin-bottom: 0.5rem;
176
+ }
177
+
178
+ .filterEmptyStateHelper {
179
+ @include type.type-style('body-compact-01');
180
+ color: $text-02;
181
+ }
@@ -0,0 +1,154 @@
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 BillsTable from './bills-table.component';
6
+
7
+ const mockbills = useBills as jest.Mock;
8
+
9
+ const mockBillsData = [
10
+ { uuid: '1', patientName: 'John Doe', identifier: '12345678', visitType: 'Checkup', patientUuid: 'uuid1' },
11
+ { uuid: '2', patientName: 'Mary Smith', identifier: '98765432', visitType: 'Wake up', patientUuid: 'uuid2' },
12
+ ];
13
+
14
+ jest.mock('../billing.resource', () => ({
15
+ ...jest.requireActual('../billing.resource'),
16
+ useBills: jest.fn(() => ({
17
+ bills: mockBillsData,
18
+ isLoading: false,
19
+ isValidating: false,
20
+ error: null,
21
+ })),
22
+ }));
23
+
24
+ jest.mock('@openmrs/esm-framework', () => ({
25
+ ...jest.requireActual('@openmrs/esm-framework'),
26
+ ErrorState: jest.fn(() => (
27
+ <div>
28
+ Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site
29
+ administrator and quote the error code above.
30
+ </div>
31
+ )),
32
+ useConfig: jest.fn(() => ({ bills: { pageSizes: [10, 20, 30, 40, 50], pageSize: 10 } })),
33
+ usePagination: jest.fn().mockImplementation((data) => ({
34
+ currentPage: 1,
35
+ goTo: () => {},
36
+ results: data,
37
+ paginated: false,
38
+ })),
39
+ }));
40
+
41
+ describe('BillsTable', () => {
42
+ let user;
43
+
44
+ beforeEach(() => {
45
+ user = userEvent.setup();
46
+ });
47
+
48
+ xit('renders data table with pending bills', () => {
49
+ render(<BillsTable />);
50
+
51
+ expect(screen.getByText('Visit time')).toBeInTheDocument();
52
+ expect(screen.getByText('Identifier')).toBeInTheDocument();
53
+ const expectedColumnHeaders = [/Visit time/, /Identifier/, /Name/, /Billing service/];
54
+ expectedColumnHeaders.forEach((header) => {
55
+ expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
56
+ });
57
+
58
+ const patientNameLink = screen.getByText('John Doe');
59
+ expect(patientNameLink).toBeInTheDocument();
60
+ expect(patientNameLink.tagName).toBe('A');
61
+ });
62
+
63
+ it('displays empty state when there are no bills', () => {
64
+ mockbills.mockImplementationOnce(() => ({
65
+ bills: [],
66
+ isLoading: false,
67
+ isValidating: false,
68
+ error: null,
69
+ }));
70
+
71
+ render(<BillsTable />);
72
+
73
+ expect(screen.getByText(/there are no bills to display/i)).toBeInTheDocument();
74
+ });
75
+
76
+ it('should not display the table when the data is loading', () => {
77
+ mockbills.mockImplementationOnce(() => ({
78
+ bills: undefined,
79
+ isLoading: true,
80
+ isValidating: false,
81
+ error: null,
82
+ }));
83
+
84
+ render(<BillsTable />);
85
+
86
+ const expectedColumnHeaders = [/Visit time/, /Identifier/, /Name/, /Billing service/, /Department/];
87
+ expectedColumnHeaders.forEach((header) => {
88
+ expect(screen.queryByRole('columnheader', { name: new RegExp(header, 'i') })).not.toBeInTheDocument();
89
+ });
90
+ });
91
+
92
+ it('should display the error state when there is error', () => {
93
+ mockbills.mockImplementationOnce(() => ({
94
+ activeVisits: undefined,
95
+ isLoading: false,
96
+ isValidating: false,
97
+ error: 'Error in fetching data',
98
+ }));
99
+
100
+ render(<BillsTable />);
101
+
102
+ expect(screen.getByText(/sorry, there was a problem displaying this information/i)).toBeInTheDocument();
103
+ expect(screen.queryByRole('table')).not.toBeInTheDocument();
104
+ });
105
+
106
+ test('should filter bills by search term and bill payment status', async () => {
107
+ render(<BillsTable />);
108
+
109
+ const searchInput = screen.getByRole('searchbox');
110
+ await user.type(searchInput, 'John Doe');
111
+
112
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
113
+ expect(screen.queryByText('Mary Smith')).not.toBeInTheDocument();
114
+
115
+ await user.clear(searchInput);
116
+ await user.type(searchInput, 'Mary Smith');
117
+
118
+ expect(screen.getByText('Mary Smith')).toBeInTheDocument();
119
+ expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
120
+
121
+ // Should filter the table when bill payment status combobox is changed
122
+ const billCategorySelect = screen.getByRole('combobox');
123
+ expect(billCategorySelect).toBeInTheDocument();
124
+ await user.click(billCategorySelect, { name: 'All bills' });
125
+ expect(mockbills).toHaveBeenCalledWith('', '');
126
+
127
+ await user.click(screen.getByText('Pending bills'));
128
+ expect(screen.getByText('Pending bills')).toBeInTheDocument();
129
+ expect(mockbills).toHaveBeenCalledWith('', 'PENDING');
130
+ });
131
+
132
+ test('should show the loading spinner while retrieving data', () => {
133
+ mockbills.mockImplementationOnce(() => ({
134
+ bills: undefined,
135
+ isLoading: true,
136
+ isValidating: false,
137
+ error: null,
138
+ }));
139
+
140
+ render(<BillsTable />);
141
+
142
+ const dataTableSkeleton = screen.getByRole('table');
143
+ expect(dataTableSkeleton).toBeInTheDocument();
144
+ expect(dataTableSkeleton).toHaveClass('cds--skeleton cds--data-table cds--data-table--zebra');
145
+ });
146
+
147
+ test('should render patient name as a link', async () => {
148
+ render(<BillsTable />);
149
+
150
+ const patientNameLink = screen.getByRole('link', { name: 'John Doe' });
151
+ expect(patientNameLink).toBeInTheDocument();
152
+ expect(patientNameLink).toHaveAttribute('href', '/openmrs/spa/home/billing/patient/uuid1/1');
153
+ });
154
+ });
@@ -0,0 +1,3 @@
1
+ export interface BillingConfig {}
2
+
3
+ export const configSchema = {};
@@ -0,0 +1,6 @@
1
+ export const dashboardMeta = {
2
+ slot: 'patient-chart-billing-dashboard-slot',
3
+ columns: 1,
4
+ title: 'Billing history',
5
+ path: 'Billing history',
6
+ };
@@ -0,0 +1,4 @@
1
+ declare module '*.css';
2
+ declare module '*.scss';
3
+ declare module '@carbon/react';
4
+ declare type SideNavProps = object;
@@ -0,0 +1,63 @@
1
+ import { type Payment, type LineItem } from '../types';
2
+
3
+ // amount already paid
4
+ export function calculateTotalAmountTendered(payments: Array<Payment>) {
5
+ return Array.isArray(payments)
6
+ ? payments.reduce((totalAmount, item) => {
7
+ // Ensure that "amount" property is present and numeric
8
+ if (typeof item.amount === 'number' && item.voided !== true) {
9
+ return totalAmount + item.amount;
10
+ }
11
+ return totalAmount;
12
+ }, 0)
13
+ : 0;
14
+ }
15
+
16
+ // balance
17
+ export function calculateTotalBalance(lineItems: Array<LineItem>, payments: Array<Payment>) {
18
+ return Math.min(this.calculateTotalAmount(lineItems) - this.calculateTotalAmountTendered(payments));
19
+ }
20
+
21
+ // total bill
22
+ export function calculateTotalAmount(lineItems: Array<LineItem>) {
23
+ return Array.isArray(lineItems)
24
+ ? lineItems.reduce((totalAmount, item) => {
25
+ // Ensure that "price" and "quantity" properties are present and numeric
26
+ if (typeof item.price === 'number' && typeof item.quantity === 'number' && item.voided !== true) {
27
+ return totalAmount + item.price * item.quantity;
28
+ }
29
+ return totalAmount;
30
+ }, 0)
31
+ : 0;
32
+ }
33
+
34
+ export const convertToCurrency = (amountToConvert: number) => {
35
+ const formatter = new Intl.NumberFormat('en-KE', {
36
+ style: 'currency',
37
+ currency: 'KES',
38
+ minimumFractionDigits: 2,
39
+ });
40
+
41
+ let formattedAmount = formatter.format(Math.abs(amountToConvert));
42
+
43
+ if (amountToConvert < 0) {
44
+ formattedAmount = `(${formattedAmount})`;
45
+ }
46
+
47
+ return formattedAmount;
48
+ };
49
+
50
+ export const getGender = (gender: string, t) => {
51
+ switch (gender) {
52
+ case 'male':
53
+ return t('male', 'Male');
54
+ case 'female':
55
+ return t('female', 'Female');
56
+ case 'other':
57
+ return t('other', 'Other');
58
+ case 'unknown':
59
+ return t('unknown', 'Unknown');
60
+ default:
61
+ return gender;
62
+ }
63
+ };
@@ -0,0 +1 @@
1
+ export * from './functions';
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { configSchema } from './config-schema';
2
+ import { createDashboardLink } from '@openmrs/esm-patient-common-lib';
3
+ import { createLeftPanelLink } from './left-panel-link.component';
4
+ import { dashboardMeta } from './dashboard.meta';
5
+ import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework';
6
+ import BillableServiceHome from './billable-services/billable-services-home.component';
7
+ import BillableServicesCardLink from './billable-services-admin-card-link.component';
8
+ import BillHistory from './bill-history/bill-history.component';
9
+ import BillingCheckInForm from './billing-form/billing-checkin-form.component';
10
+ import BillingForm from './billing-form/billing-form.component';
11
+ import RequirePaymentModal from './modal/require-payment-modal.component';
12
+ import RootComponent from './root.component';
13
+ import VisitAttributeTags from './invoice/payments/visit-tags/visit-attribute.component';
14
+ import BillableServicesDashboard from './billable-services/dashboard/dashboard.component';
15
+ import ServiceMetrics from './billable-services/dashboard/service-metrics.component';
16
+
17
+ const moduleName = '@openmrs/esm-billing-app';
18
+
19
+ const options = {
20
+ featureName: 'billing',
21
+ moduleName,
22
+ };
23
+
24
+ // t('billing', 'Billing')
25
+ export const billingDashboardLink = getSyncLifecycle(
26
+ createLeftPanelLink({
27
+ name: 'billing',
28
+ title: 'Billing',
29
+ }),
30
+ options,
31
+ );
32
+
33
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
34
+
35
+ export function startupApp() {
36
+ defineConfigSchema(moduleName, configSchema);
37
+ }
38
+
39
+ export const billingSummaryDashboardLink = getSyncLifecycle(
40
+ createDashboardLink({ ...dashboardMeta, moduleName }),
41
+ options,
42
+ );
43
+
44
+ export const billingServicesTiles = getSyncLifecycle(ServiceMetrics, {
45
+ featureName: 'billing-home-tiles',
46
+ moduleName,
47
+ });
48
+
49
+ export const billableServicesCardLink = getSyncLifecycle(BillableServicesCardLink, options);
50
+ export const billableServicesHome = getSyncLifecycle(BillableServiceHome, options);
51
+ export const billingCheckInForm = getSyncLifecycle(BillingCheckInForm, options);
52
+ export const billingForm = getSyncLifecycle(BillingForm, options);
53
+ export const billingPatientSummary = getSyncLifecycle(BillHistory, options);
54
+ export const requirePaymentModal = getSyncLifecycle(RequirePaymentModal, options);
55
+ export const root = getSyncLifecycle(RootComponent, options);
56
+ export const visitAttributeTags = getSyncLifecycle(VisitAttributeTags, options);
@@ -0,0 +1,185 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import fuzzy from 'fuzzy';
4
+ import {
5
+ DataTable,
6
+ DataTableSkeleton,
7
+ Layer,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableContainer,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ TableToolbar,
16
+ TableToolbarContent,
17
+ TableToolbarSearch,
18
+ TableSelectRow,
19
+ Tile,
20
+ type DataTableHeader,
21
+ type DataTableRow,
22
+ } from '@carbon/react';
23
+ import { isDesktop, useDebounce, useLayoutType } from '@openmrs/esm-framework';
24
+ import { type LineItem, type MappedBill } from '../types';
25
+ import styles from './invoice-table.scss';
26
+
27
+ type InvoiceTableProps = {
28
+ bill: MappedBill;
29
+ isSelectable?: boolean;
30
+ isLoadingBill?: boolean;
31
+ onSelectItem?: (selectedLineItems: LineItem[]) => void;
32
+ };
33
+
34
+ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
35
+ const { t } = useTranslation();
36
+ const { lineItems } = bill;
37
+ const layout = useLayoutType();
38
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
39
+ const [selectedLineItems, setSelectedLineItems] = useState([]);
40
+ const [searchTerm, setSearchTerm] = useState('');
41
+ const debouncedSearchTerm = useDebounce(searchTerm);
42
+
43
+ const filteredLineItems = useMemo(() => {
44
+ if (!debouncedSearchTerm) {
45
+ return lineItems;
46
+ }
47
+
48
+ return debouncedSearchTerm
49
+ ? fuzzy
50
+ .filter(debouncedSearchTerm, lineItems, {
51
+ extract: (lineItem: LineItem) => `${lineItem.item}`,
52
+ })
53
+ .sort((r1, r2) => r1.score - r2.score)
54
+ .map((result) => result.original)
55
+ : lineItems;
56
+ }, [debouncedSearchTerm, lineItems]);
57
+
58
+ const tableHeaders: Array<typeof DataTableHeader> = [
59
+ { header: 'No', key: 'no' },
60
+ { header: 'Bill item', key: 'billItem' },
61
+ { header: 'Bill code', key: 'billCode' },
62
+ { header: 'Status', key: 'status' },
63
+ { header: 'Quantity', key: 'quantity' },
64
+ { header: 'Price', key: 'price' },
65
+ { header: 'Total', key: 'total' },
66
+ ];
67
+
68
+ const tableRows: Array<typeof DataTableRow> = useMemo(
69
+ () =>
70
+ filteredLineItems?.map((item, index) => {
71
+ return {
72
+ no: `${index + 1}`,
73
+ id: `${item.uuid}`,
74
+ billItem: item.item || item.billableService,
75
+ billCode: bill.receiptNumber,
76
+ status: bill.status,
77
+ quantity: item.quantity,
78
+ price: item.price,
79
+ total: item.price * item.quantity,
80
+ };
81
+ }) ?? [],
82
+ [bill.receiptNumber, bill.status, filteredLineItems],
83
+ );
84
+
85
+ if (isLoadingBill) {
86
+ return (
87
+ <div className={styles.loaderContainer}>
88
+ <DataTableSkeleton
89
+ columnCount={tableHeaders.length}
90
+ showHeader={false}
91
+ showToolbar={false}
92
+ size={responsiveSize}
93
+ zebra
94
+ />
95
+ </div>
96
+ );
97
+ }
98
+
99
+ const handleRowSelection = (row: typeof DataTableRow, checked: boolean) => {
100
+ const matchingRow = filteredLineItems.find((item) => item.uuid === row.id);
101
+ let newSelectedLineItems;
102
+
103
+ if (checked) {
104
+ newSelectedLineItems = [...selectedLineItems, matchingRow];
105
+ } else {
106
+ newSelectedLineItems = selectedLineItems.filter((item) => item.uuid !== row.id);
107
+ }
108
+ setSelectedLineItems(newSelectedLineItems);
109
+ onSelectItem(newSelectedLineItems);
110
+ };
111
+
112
+ return (
113
+ <div className={styles.invoiceContainer}>
114
+ <DataTable headers={tableHeaders} isSortable rows={tableRows} size={responsiveSize} useZebraStyles>
115
+ {({ rows, headers, getRowProps, getSelectionProps, getTableProps, getToolbarProps }) => (
116
+ <TableContainer
117
+ description={
118
+ <span className={styles.tableDescription}>
119
+ <span>{t('itemsToBeBilled', 'Items to be billed')}</span>
120
+ </span>
121
+ }
122
+ title={t('lineItems', 'Line items')}>
123
+ <div className={styles.toolbarWrapper}>
124
+ <TableToolbar {...getToolbarProps()} className={styles.tableToolbar} size={responsiveSize}>
125
+ <TableToolbarContent className={styles.headerContainer}>
126
+ <TableToolbarSearch
127
+ className={styles.searchbox}
128
+ expanded
129
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
130
+ placeholder={t('searchThisTable', 'Search this table')}
131
+ size={responsiveSize}
132
+ />
133
+ </TableToolbarContent>
134
+ </TableToolbar>
135
+ </div>
136
+ <Table {...getTableProps()} aria-label="Invoice line items" className={styles.table}>
137
+ <TableHead>
138
+ <TableRow>
139
+ {rows.length > 1 && isSelectable ? <TableHeader /> : null}
140
+ {headers.map((header) => (
141
+ <TableHeader key={header.key}>{header.header}</TableHeader>
142
+ ))}
143
+ </TableRow>
144
+ </TableHead>
145
+ <TableBody>
146
+ {rows.map((row) => (
147
+ <TableRow
148
+ key={row.id}
149
+ {...getRowProps({
150
+ row,
151
+ })}>
152
+ {rows.length > 1 && isSelectable && (
153
+ <TableSelectRow
154
+ aria-label="Select row"
155
+ {...getSelectionProps({ row })}
156
+ onChange={(checked: boolean) => handleRowSelection(row, checked)}
157
+ />
158
+ )}
159
+ {row.cells.map((cell) => (
160
+ <TableCell key={cell.id}>{cell.value}</TableCell>
161
+ ))}
162
+ </TableRow>
163
+ ))}
164
+ </TableBody>
165
+ </Table>
166
+ </TableContainer>
167
+ )}
168
+ </DataTable>
169
+ {filteredLineItems?.length === 0 && (
170
+ <div className={styles.filterEmptyState}>
171
+ <Layer>
172
+ <Tile className={styles.filterEmptyStateTile}>
173
+ <p className={styles.filterEmptyStateContent}>
174
+ {t('noMatchingItemsToDisplay', 'No matching items to display')}
175
+ </p>
176
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
177
+ </Tile>
178
+ </Layer>
179
+ </div>
180
+ )}
181
+ </div>
182
+ );
183
+ };
184
+
185
+ export default InvoiceTable;