@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,63 @@
1
+ import React from 'react';
2
+ import { type PatientDetails } from '../../types';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useDefaultFacility } from '../../billing.resource';
6
+ import styles from './printable-invoice-header.scss';
7
+
8
+ interface PrintableInvoiceHeaderProps {
9
+ patientDetails: PatientDetails;
10
+ }
11
+
12
+ const PrintableInvoiceHeader: React.FC<PrintableInvoiceHeaderProps> = ({ patientDetails }) => {
13
+ const { t } = useTranslation();
14
+ const { logo } = useConfig();
15
+ const { data } = useDefaultFacility();
16
+
17
+ return (
18
+ <div className={styles.container}>
19
+ <div className={styles.printableHeader}>
20
+ <p className={styles.heading}>{t('invoice', 'Invoice')}</p>
21
+ {logo?.src ? (
22
+ <img className={styles.img} src={logo.src} alt={logo.alt} />
23
+ ) : logo?.name ? (
24
+ logo.name
25
+ ) : (
26
+ // OpenMRS Logo
27
+ <svg
28
+ className={styles.img}
29
+ role="img"
30
+ width={110}
31
+ height={40}
32
+ viewBox="0 0 380 119"
33
+ xmlns="http://www.w3.org/2000/svg">
34
+ <path
35
+ fillRule="evenodd"
36
+ clipRule="evenodd"
37
+ d="M40.29 40.328a27.755 27.755 0 0 1 19.688-8.154c7.669 0 14.613 3.102 19.647 8.116l.02-18.54A42.835 42.835 0 0 0 59.978 17c-7.089 0-13.813 1.93-19.709 4.968l.021 18.36ZM79.645 79.671a27.744 27.744 0 0 1-19.684 8.154c-7.67 0-14.614-3.101-19.651-8.116l-.02 18.54A42.857 42.857 0 0 0 59.96 103a42.833 42.833 0 0 0 19.672-4.751l.013-18.578ZM40.328 79.696c-5.038-5.037-8.154-11.995-8.154-19.685 0-7.669 3.102-14.612 8.116-19.65l-18.54-.02A42.85 42.85 0 0 0 17 60.012a42.819 42.819 0 0 0 4.752 19.672l18.576.013ZM79.634 40.289a27.753 27.753 0 0 1 8.154 19.688 27.744 27.744 0 0 1-8.117 19.646l18.542.02a42.842 42.842 0 0 0 4.749-19.666c0-7.09-1.714-13.779-4.751-19.675l-18.577-.013ZM156.184 60.002c0-8.748-6.118-15.776-15.025-15.776-8.909 0-15.025 7.028-15.025 15.776 0 8.749 6.116 15.78 15.025 15.78 8.907 0 15.025-7.031 15.025-15.78Zm-34.881 0c0-11.482 8.318-19.958 19.856-19.958 11.536 0 19.855 8.477 19.855 19.959 0 11.484-8.319 19.964-19.855 19.964-11.538 0-19.856-8.48-19.856-19.965ZM179.514 75.54c5.507 0 9.05-4.14 9.05-9.482 0-5.341-3.543-9.483-9.05-9.483-5.505 0-9.046 4.142-9.046 9.483 0 5.342 3.541 9.482 9.046 9.482ZM166.22 53.306h4.248v3.704h.11c2.344-2.725 5.449-4.36 9.154-4.36 8.014 0 13.408 5.67 13.408 13.408 0 7.63-5.613 13.406-12.752 13.406-4.58 0-8.231-2.29-9.81-5.178h-.11V90.87h-4.248V53.306ZM217.773 63.768c-.163-4.305-3-7.193-7.686-7.193-4.685 0-7.79 2.888-8.335 7.193h16.021Zm3.653 10.412c-3.001 3.868-6.596 5.284-11.339 5.284-8.01 0-12.914-5.993-12.914-13.406 0-7.901 5.559-13.407 13.08-13.407 7.196 0 12.096 4.906 12.096 13.354v1.362h-20.597c.325 4.413 3.704 8.173 8.335 8.173 3.65 0 6.105-1.307 8.12-3.868l3.219 2.508ZM227.854 59.356c0-2.346-.216-4.36-.216-6.05h4.031c0 1.363.11 2.777.11 4.195h.11c1.144-2.505 4.306-4.85 8.5-4.85 6.705 0 9.699 4.252 9.699 10.41v15.748h-4.248v-15.31c0-4.253-1.856-6.924-5.833-6.924-5.503 0-7.903 3.979-7.903 9.811V78.81h-4.25V59.356ZM259.211 41.008h6.708L278.8 70.791h.107l12.982-29.782h6.549v37.99h-4.506V47.124h-.106L280.192 79h-2.738l-13.629-31.875h-.107V79h-4.507V41.01ZM312.392 57.752h4.023c4.992 0 11.487 0 11.487-6.282 0-5.47-4.776-6.276-9.177-6.276h-6.333v12.558Zm-4.506-16.744h9.711c7.352 0 15.132 1.072 15.132 10.462 0 5.527-3.594 9.125-9.495 10.037L334.018 79h-5.525l-10.304-17.063h-5.797V79h-4.506V41.01ZM358.123 47.712c-1.506-2.413-4.187-3.486-6.926-3.486-3.973 0-8.1 1.88-8.1 6.385 0 3.49 1.931 5.047 7.994 6.98 5.903 1.878 11.377 3.809 11.377 11.267 0 7.567-6.495 11.11-13.36 11.11-4.402 0-9.125-1.45-11.7-5.262l3.862-3.165c1.61 2.794 4.83 4.24 8.105 4.24 3.862 0 8.263-2.253 8.263-6.601 0-4.669-3.165-5.474-9.928-7.728-5.366-1.771-9.442-4.134-9.442-10.463 0-7.298 6.277-10.945 12.929-10.945 4.241 0 7.836 1.178 10.625 4.45l-3.699 3.218Z"
38
+ />
39
+ </svg>
40
+ )}
41
+ </div>
42
+
43
+ <div className={styles.printableBody}>
44
+ <div className={styles.billDetails}>
45
+ <p className={styles.itemHeading}>{t('billedTo', 'Billed to')}</p>
46
+ <p className={styles.itemLabel}>{patientDetails?.name}</p>
47
+ <p className={styles.itemLabel}>{patientDetails?.county}</p>
48
+ <p className={styles.itemLabel}>
49
+ {patientDetails?.subCounty}
50
+ {patientDetails?.city ? `, ${patientDetails?.city}` : null}
51
+ </p>
52
+ </div>
53
+
54
+ <div className={styles.facilityDetails}>
55
+ <p className={styles.facilityName}>{data?.display}</p>
56
+ <p className={styles.itemLabel}>Kenya</p>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ );
61
+ };
62
+
63
+ export default PrintableInvoiceHeader;
@@ -0,0 +1,61 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ padding: 0 1rem 2rem;
7
+ border-bottom: 1px solid #ebedf2;
8
+ margin-bottom: 2rem;
9
+ }
10
+
11
+ .printableBody {
12
+ display: flex;
13
+ flex-direction: row;
14
+ justify-content: space-between;
15
+ }
16
+
17
+ .printableHeader {
18
+ display: flex;
19
+ flex-direction: row;
20
+ }
21
+
22
+ .img {
23
+ display: flex;
24
+ margin-left: auto;
25
+ }
26
+
27
+ .billDetails {
28
+ display: flex;
29
+ width: 50%;
30
+ flex-direction: column;
31
+ }
32
+
33
+ .facilityDetails {
34
+ display: flex;
35
+ flex-flow: column wrap;
36
+ text-align: right;
37
+ }
38
+
39
+ .heading {
40
+ font-size: 40px;
41
+ text-transform: uppercase;
42
+ margin-bottom: layout.$spacing-05;
43
+ }
44
+
45
+ .facilityName {
46
+ @include type.type-style('heading-compact-02');
47
+ font-weight: bold;
48
+ color: colors.$green-70;
49
+ }
50
+
51
+ .itemHeading {
52
+ @include type.type-style('body-compact-02');
53
+ margin-bottom: 0.25rem;
54
+ font-weight: bold;
55
+ color: colors.$cool-gray-90;
56
+ }
57
+
58
+ .itemLabel {
59
+ @include type.type-style('body-compact-02');
60
+ color: colors.$cool-gray-90;
61
+ }
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { useDefaultFacility } from '../../billing.resource';
5
+ import PrintableInvoiceHeader from './printable-invoice-header.component';
6
+
7
+ const mockUseDefaultFacility = useDefaultFacility as jest.MockedFunction<typeof useDefaultFacility>;
8
+ const mockUseConfig = useConfig as jest.MockedFunction<typeof useConfig>;
9
+
10
+ jest.mock('../../billing.resource', () => ({
11
+ useDefaultFacility: jest.fn(),
12
+ }));
13
+
14
+ jest.mock('@openmrs/esm-framework', () => ({
15
+ useConfig: jest.fn(),
16
+ }));
17
+ const testProps = {
18
+ patientDetails: {
19
+ name: 'John Doe',
20
+ county: 'Nairobi',
21
+ subCounty: 'Westlands',
22
+ city: 'Nairobi',
23
+ age: '45',
24
+ gender: 'Male',
25
+ },
26
+ };
27
+
28
+ describe('PrintableInvoiceHeader', () => {
29
+ test('should render PrintableInvoiceHeader component', () => {
30
+ mockUseConfig.mockReturnValue({ logo: { src: 'logo.png', alt: 'logo' } });
31
+ mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
32
+ render(<PrintableInvoiceHeader {...testProps} />);
33
+ const header = screen.getByText('Invoice');
34
+ expect(header).toBeInTheDocument();
35
+
36
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
37
+ expect(screen.getByText('Nairobi')).toBeInTheDocument();
38
+ expect(screen.getByText('Westlands, Nairobi')).toBeInTheDocument();
39
+ expect(screen.getByText('MTRH')).toBeInTheDocument();
40
+ expect(screen.getByText('Kenya')).toBeInTheDocument();
41
+ });
42
+
43
+ test('should display the logo when logo is provided', () => {
44
+ mockUseConfig.mockReturnValue({ logo: { src: 'logo.png', alt: 'logo' } });
45
+ mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
46
+ render(<PrintableInvoiceHeader {...testProps} />);
47
+ const logo = screen.getByAltText('logo');
48
+ expect(logo).toBeInTheDocument();
49
+ });
50
+
51
+ test('should display the default logo when logo is not provided', () => {
52
+ mockUseConfig.mockReturnValue({ logo: {} });
53
+ mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
54
+ render(<PrintableInvoiceHeader {...testProps} />);
55
+ const logo = screen.getByRole('img');
56
+ expect(logo).toBeInTheDocument();
57
+ });
58
+ });
@@ -0,0 +1,146 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ DataTable,
4
+ Table,
5
+ TableContainer,
6
+ TableHead,
7
+ TableRow,
8
+ TableBody,
9
+ TableHeader,
10
+ TableCell,
11
+ DataTableSkeleton,
12
+ } from '@carbon/react';
13
+ import { age, isDesktop, useLayoutType } from '@openmrs/esm-framework';
14
+ import { getGender } from '../../helpers';
15
+ import { type MappedBill } from '../../types';
16
+ import { useTranslation } from 'react-i18next';
17
+ import PrintableFooter from './printable-footer.component';
18
+ import PrintableInvoiceHeader from './printable-invoice-header.component';
19
+ import styles from './printable-invoice.scss';
20
+
21
+ type PrintableInvoiceProps = {
22
+ bill: MappedBill;
23
+ patient: fhir.Patient;
24
+ isLoading: boolean;
25
+ };
26
+
27
+ const PrintableInvoice: React.FC<PrintableInvoiceProps> = ({ bill, patient, isLoading }) => {
28
+ const { t } = useTranslation();
29
+ const layout = useLayoutType();
30
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
31
+ const headerData = [
32
+ { header: 'Inventory item', key: 'billItem' },
33
+ { header: 'Quantity', key: 'quantity' },
34
+ { header: 'Unit price', key: 'price' },
35
+ { header: 'Total', key: 'total' },
36
+ ];
37
+
38
+ const rowData =
39
+ bill?.lineItems?.map((item) => {
40
+ return {
41
+ id: `${item.uuid}`,
42
+ billItem: item.item,
43
+ quantity: item.quantity,
44
+ price: item.price,
45
+ total: item.price * item.quantity,
46
+ };
47
+ }) ?? [];
48
+
49
+ const invoiceTotal = {
50
+ 'Total Amount': bill?.totalAmount,
51
+ 'Amount Tendered': bill?.tenderedAmount,
52
+ 'Discount Amount': 0,
53
+ 'Amount due': bill?.totalAmount - bill?.tenderedAmount,
54
+ };
55
+
56
+ const patientDetails = useMemo(() => {
57
+ return {
58
+ name: `${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
59
+ age: age(patient?.birthDate),
60
+ gender: getGender(patient?.gender, t),
61
+ city: patient?.address?.[0].city,
62
+ county: patient?.address?.[0].district,
63
+ subCounty: patient?.address?.[0].state,
64
+ };
65
+ }, [patient, t]);
66
+
67
+ const invoiceDetails = {
68
+ 'Invoice #': bill.receiptNumber,
69
+ 'Invoice date': bill.dateCreated,
70
+ Status: bill.status,
71
+ };
72
+
73
+ if (isLoading) {
74
+ return (
75
+ <div className={styles.loaderContainer}>
76
+ <DataTableSkeleton
77
+ columnCount={headerData?.length ?? 0}
78
+ showHeader={false}
79
+ showToolbar={false}
80
+ size={responsiveSize}
81
+ zebra
82
+ />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div className={styles.container}>
89
+ <PrintableInvoiceHeader patientDetails={patientDetails} />
90
+ <div className={styles.printableInvoiceContainer}>
91
+ <div className={styles.detailsContainer}>
92
+ {Object.entries(invoiceDetails).map(([key, val]) => (
93
+ <div key={key} className={styles.item}>
94
+ <p className={styles.itemHeading}>{key}</p>
95
+ <span>{val}</span>
96
+ </div>
97
+ ))}
98
+ </div>
99
+
100
+ <div className={styles.itemsContainer}>
101
+ <div className={styles.tableContainer}>
102
+ <DataTable isSortable rows={rowData} headers={headerData} size={responsiveSize} useZebraStyles={false}>
103
+ {({ rows, headers, getRowProps, getTableProps }) => (
104
+ <TableContainer>
105
+ <Table {...getTableProps()} aria-label="Invoice line items">
106
+ <TableHead>
107
+ <TableRow>
108
+ {headers.map((header) => (
109
+ <TableHeader key={header.key}>{header.header}</TableHeader>
110
+ ))}
111
+ </TableRow>
112
+ </TableHead>
113
+ <TableBody>
114
+ {rows.map((row) => (
115
+ <TableRow
116
+ key={row.id}
117
+ {...getRowProps({
118
+ row,
119
+ })}>
120
+ {row.cells.map((cell) => (
121
+ <TableCell key={cell.id}>{cell.value}</TableCell>
122
+ ))}
123
+ </TableRow>
124
+ ))}
125
+ </TableBody>
126
+ </Table>
127
+ </TableContainer>
128
+ )}
129
+ </DataTable>
130
+ </div>
131
+
132
+ <div className={styles.totalContainer}>
133
+ {Object.entries(invoiceTotal).map(([key, val]) => (
134
+ <p key={key} className={styles.itemTotal}>
135
+ <span className={styles.itemHeading}>{key}</span>: <span className={styles.itemLabel}>{val}</span>
136
+ </p>
137
+ ))}
138
+ </div>
139
+ </div>
140
+ </div>
141
+ <PrintableFooter />
142
+ </div>
143
+ );
144
+ };
145
+
146
+ export default PrintableInvoice;
@@ -0,0 +1,50 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .printableInvoiceContainer {
6
+ display: flex;
7
+ flex-direction: row;
8
+ }
9
+
10
+ .itemsContainer {
11
+ display: flex;
12
+ flex-direction: column;
13
+ width: 80%;
14
+ padding: layout.$spacing-05;
15
+ margin: layout.$spacing-05;
16
+ }
17
+
18
+ .detailsContainer {
19
+ margin: layout.$spacing-05 0;
20
+ width: 20%;
21
+ }
22
+
23
+ .tableContainer {
24
+ min-height: 50vh;
25
+ }
26
+
27
+ .totalContainer {
28
+ display: flex;
29
+ flex-direction: column;
30
+ }
31
+
32
+ .item {
33
+ margin: layout.$spacing-04;
34
+ }
35
+
36
+ .itemHeading {
37
+ @include type.type-style('body-compact-02');
38
+ font-weight: bold;
39
+ color: colors.$cool-gray-90;
40
+ margin-bottom: layout.$spacing-02;
41
+ }
42
+
43
+ .itemTotal {
44
+ border-top: solid 0.1pt colors.$cool-gray-30;
45
+ padding: layout.$spacing-02;
46
+ }
47
+
48
+ .itemLabel {
49
+ float: right;
50
+ }
@@ -0,0 +1,41 @@
1
+ import React, { useMemo } from 'react';
2
+ import last from 'lodash-es/last';
3
+ import { BrowserRouter, useLocation } from 'react-router-dom';
4
+ import { ConfigurableLink } from '@openmrs/esm-framework';
5
+
6
+ export interface LinkConfig {
7
+ name: string;
8
+ title: string;
9
+ }
10
+
11
+ export function LinkExtension({ config }: { config: LinkConfig }) {
12
+ const { name, title } = config;
13
+ const location = useLocation();
14
+ const spaBasePath = window.getOpenmrsSpaBase() + 'home';
15
+
16
+ let urlSegment = useMemo(() => decodeURIComponent(last(location.pathname.split('/'))), [location.pathname]);
17
+
18
+ const isUUID = (value) => {
19
+ const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
20
+ return regex.test(value);
21
+ };
22
+
23
+ if (isUUID(urlSegment)) {
24
+ urlSegment = 'billing';
25
+ }
26
+
27
+ return (
28
+ <ConfigurableLink
29
+ to={spaBasePath + '/' + name}
30
+ className={`cds--side-nav__link ${name === urlSegment && 'active-left-nav-link'}`}>
31
+ {title}
32
+ </ConfigurableLink>
33
+ );
34
+ }
35
+
36
+ export const createLeftPanelLink = (config: LinkConfig) => () =>
37
+ (
38
+ <BrowserRouter>
39
+ <LinkExtension config={config} />
40
+ </BrowserRouter>
41
+ );
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { LinkExtension, createLeftPanelLink } from './left-panel-link.component';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ window.getOpenmrsSpaBase = () => '/openmrs/spa/';
8
+
9
+ describe('LinkExtension Component', () => {
10
+ const renderWithRouter = (component, { route = '/' } = {}) => {
11
+ window.history.pushState({}, 'Test page', route);
12
+ return render(component, { wrapper: MemoryRouter });
13
+ };
14
+
15
+ test('renders correctly', () => {
16
+ const config = { name: 'billing', title: 'Billing' };
17
+ renderWithRouter(<LinkExtension config={config} />, {
18
+ route: '/billing/6eb8d678-514d-46ad-9554-51e48d96d567',
19
+ });
20
+
21
+ expect(screen.getByText('Billing')).toBeInTheDocument();
22
+ });
23
+ });
24
+
25
+ describe('createLeftPanelLink Function', () => {
26
+ const user = userEvent.setup();
27
+ test('returns a component that renders LinkExtension', () => {
28
+ const config = { name: 'billing', title: 'Billing' };
29
+ const TestComponent = createLeftPanelLink(config);
30
+
31
+ render(<TestComponent />);
32
+ expect(screen.getByText('Billing')).toBeInTheDocument();
33
+ const testLink = screen.getByRole('link', { name: 'Billing' });
34
+ user.click(testLink);
35
+ expect(window.location.pathname).toBe('/billing/6eb8d678-514d-46ad-9554-51e48d96d567');
36
+ // expect(testLink).toHaveClass('active-left-nav-link');
37
+ });
38
+ });
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import styles from './card.scss';
3
+
4
+ export default function Card({ count, title }) {
5
+ return (
6
+ <div className={styles.container}>
7
+ <h1 className={styles.title}>{title}</h1>
8
+ <span className={styles.count}>{count}</span>
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,20 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ border: 1px solid colors.$gray-20;
7
+ padding: layout.$spacing-05;
8
+ flex: 1;
9
+ }
10
+
11
+ .title {
12
+ @include type.type-style('heading-compact-02');
13
+ }
14
+
15
+ .count {
16
+ @include type.type-style('heading-05');
17
+ display: inline-block;
18
+ font-weight: 300;
19
+ margin-top: layout.$spacing-05;
20
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineLoading } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
5
+ import { useBills } from '../billing.resource';
6
+ import { useBillMetrics } from './metrics.resource';
7
+ import Card from './card.component';
8
+ import styles from './metrics-cards.scss';
9
+
10
+ export default function MetricsCards() {
11
+ const { t } = useTranslation();
12
+ const { bills, isLoading, error } = useBills('');
13
+ const { cumulativeBills, pendingBills, paidBills } = useBillMetrics(bills);
14
+
15
+ const cards = useMemo(
16
+ () => [
17
+ { title: 'Cumulative Bills', count: cumulativeBills },
18
+ { title: 'Pending Bills', count: pendingBills },
19
+ { title: 'Paid Bills', count: paidBills },
20
+ ],
21
+ [cumulativeBills, pendingBills, paidBills],
22
+ );
23
+
24
+ if (isLoading) {
25
+ return (
26
+ <section className={styles.container}>
27
+ <InlineLoading status="active" iconDescription="Loading" description="Loading bill metrics..." />
28
+ </section>
29
+ );
30
+ }
31
+
32
+ if (error) {
33
+ return <ErrorState headerTitle={t('billMetrics', 'Bill metrics')} error={error} />;
34
+ }
35
+ return (
36
+ <section className={styles.container}>
37
+ {cards.map((card) => (
38
+ <Card key={card.title} title={card.title} count={card.count} />
39
+ ))}
40
+ </section>
41
+ );
42
+ }
@@ -0,0 +1,12 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ display: flex;
7
+ margin: layout.$spacing-05;
8
+ justify-content: space-between;
9
+ column-gap: layout.$spacing-09;
10
+ row-gap: layout.$spacing-05;
11
+ flex-flow: row wrap;
12
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { billsSummary } from '../../__mocks__/bills.mock';
4
+ import { useBills } from '../billing.resource';
5
+ import MetricsCards from './metrics-cards.component';
6
+
7
+ const mockUseBills = useBills as jest.Mock;
8
+
9
+ jest.mock('../billing.resource', () => ({
10
+ useBills: jest.fn(),
11
+ }));
12
+
13
+ describe('MetricsCards', () => {
14
+ test('renders loading state', () => {
15
+ mockUseBills.mockReturnValue({ isLoading: true, bills: [], error: null });
16
+ renderMetricsCards();
17
+ expect(screen.getByText(/Loading bill metrics.../i)).toBeInTheDocument();
18
+ });
19
+
20
+ test('renders error state', () => {
21
+ mockUseBills.mockReturnValue({ isLoading: false, bills: [], error: new Error('Internal server error') });
22
+ renderMetricsCards();
23
+ expect(
24
+ screen.getByText(
25
+ /Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above./i,
26
+ ),
27
+ ).toBeInTheDocument();
28
+ });
29
+
30
+ test('renders metrics cards', () => {
31
+ mockUseBills.mockReturnValue({ isLoading: false, bills: billsSummary, error: null });
32
+ renderMetricsCards();
33
+ expect(screen.getByRole('heading', { name: /cumulative bills/i })).toBeInTheDocument();
34
+ expect(screen.getByRole('heading', { name: /pending bills/i })).toBeInTheDocument();
35
+ expect(screen.getByRole('heading', { name: /paid bills/i })).toBeInTheDocument();
36
+ });
37
+ });
38
+
39
+ function renderMetricsCards() {
40
+ render(<MetricsCards />);
41
+ }
@@ -0,0 +1,45 @@
1
+ import { calculateTotalAmount, convertToCurrency } from '../helpers';
2
+ import { type MappedBill } from '../types';
3
+
4
+ /**
5
+ * A custom hook for calculating bill metrics.
6
+ *
7
+ * This hook takes in an array of bills and calculates the total amount for different
8
+ * bill statuses (cumulative, pending, paid) using provided helper functions.
9
+ *
10
+ * @param {Array<Object>} bills - An array of bill objects. Each bill object should have a `status` and `lineItems` properties.
11
+ *
12
+ * @returns {{
13
+ * cumulativeBills: string,
14
+ * pendingBills: string,
15
+ * paidBills: string
16
+ * }}
17
+ */
18
+
19
+ export const useBillMetrics = (bills: Array<MappedBill>) => {
20
+ const { paidTotal, pendingTotal, cumulativeTotal } = calculateBillTotals(bills);
21
+ return {
22
+ cumulativeBills: convertToCurrency(cumulativeTotal),
23
+ pendingBills: convertToCurrency(pendingTotal),
24
+ paidBills: convertToCurrency(paidTotal),
25
+ };
26
+ };
27
+
28
+ const calculateBillTotals = (bills: Array<MappedBill>) => {
29
+ let paidTotal = 0;
30
+ let pendingTotal = 0;
31
+ let cumulativeTotal = 0;
32
+
33
+ bills.forEach((bill) => {
34
+ if (bill.status === 'PAID') {
35
+ paidTotal += bill.totalAmount;
36
+ } else if (bill.status === 'PENDING') {
37
+ pendingTotal += bill.totalAmount;
38
+ }
39
+ cumulativeTotal += bill.totalAmount; // Add to cumulative total regardless of status
40
+ });
41
+
42
+ return { paidTotal, pendingTotal, cumulativeTotal };
43
+ };
44
+
45
+ export default calculateBillTotals;