@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,91 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .filterEmptyState {
7
+ align-items: center;
8
+ background-color: white;
9
+ display: flex;
10
+ justify-content: center;
11
+ padding: layout.$spacing-09 !important;
12
+ text-align: center;
13
+ }
14
+
15
+ .filterEmptyStateTile {
16
+ margin: auto;
17
+ }
18
+
19
+ .filterEmptyStateContent {
20
+ @include type.type-style('heading-compact-02');
21
+ color: $text-02;
22
+ margin-bottom: 0.5rem;
23
+ }
24
+
25
+ .filterEmptyStateHelper {
26
+ @include type.type-style('body-compact-01');
27
+ color: $text-02;
28
+ }
29
+
30
+ .headerContainer {
31
+ background-color: colors.$gray-10;
32
+ }
33
+
34
+ .invoiceContainer {
35
+ margin: layout.$spacing-09 layout.$spacing-05 0;
36
+ border: 1px solid $ui-03;
37
+ }
38
+
39
+ .searchbox {
40
+ input:focus {
41
+ outline: 2px solid colors.$orange-40 !important;
42
+ }
43
+ }
44
+
45
+ .table {
46
+ td {
47
+ border-bottom: none !important;
48
+ }
49
+ }
50
+
51
+ .tableDescription {
52
+ display: flex;
53
+ align-items: flex-start;
54
+ margin-top: layout.$spacing-02;
55
+ column-gap: layout.$spacing-01;
56
+
57
+ ::after {
58
+ content: '';
59
+ display: block;
60
+ width: 2rem;
61
+ padding-top: 0.188rem;
62
+ border-bottom: 0.375rem solid var(--brand-03);
63
+ }
64
+
65
+ & > span {
66
+ @include type.type-style('body-01');
67
+ }
68
+ }
69
+
70
+ .tableToolbar {
71
+ width: 20%;
72
+ min-width: 12.5rem;
73
+ }
74
+
75
+ .toolbarWrapper {
76
+ position: relative;
77
+ display: flex;
78
+ justify-content: flex-end;
79
+ }
80
+
81
+ :global(.omrs-breakpoint-lt-desktop) {
82
+ .toolbarWrapper {
83
+ height: layout.$spacing-09;
84
+ }
85
+ }
86
+
87
+ :global(.omrs-breakpoint-gt-tablet) {
88
+ .toolbarWrapper {
89
+ height: layout.$spacing-07;
90
+ }
91
+ }
@@ -0,0 +1,138 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Button, InlineLoading } from '@carbon/react';
3
+ import { Printer } from '@carbon/react/icons';
4
+ import { useParams } from 'react-router-dom';
5
+ import { useReactToPrint } from 'react-to-print';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { ExtensionSlot, usePatient } from '@openmrs/esm-framework';
8
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
9
+ import { convertToCurrency } from '../helpers';
10
+ import { type LineItem } from '../types';
11
+ import { useBill } from '../billing.resource';
12
+ import InvoiceTable from './invoice-table.component';
13
+ import Payments from './payments/payments.component';
14
+ import PrintReceipt from './printable-invoice/print-receipt.component';
15
+ import PrintableInvoice from './printable-invoice/printable-invoice.component';
16
+ import styles from './invoice.scss';
17
+
18
+ interface InvoiceDetailsProps {
19
+ label: string;
20
+ value: string | number;
21
+ }
22
+
23
+ const Invoice: React.FC = () => {
24
+ const { t } = useTranslation();
25
+ const { billUuid, patientUuid } = useParams();
26
+ const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid);
27
+ const { bill, isLoading: isLoadingBill, error } = useBill(billUuid);
28
+ const [isPrinting, setIsPrinting] = useState(false);
29
+ const [selectedLineItems, setSelectedLineItems] = useState([]);
30
+ const componentRef = useRef<HTMLDivElement>(null);
31
+ const onBeforeGetContentResolve = useRef<(() => void) | null>(null);
32
+ const handleSelectItem = (lineItems: Array<LineItem>) => {
33
+ setSelectedLineItems(lineItems);
34
+ };
35
+
36
+ const handleAfterPrint = useCallback(() => {
37
+ onBeforeGetContentResolve.current = null;
38
+ setIsPrinting(false);
39
+ }, []);
40
+
41
+ const reactToPrintContent = useCallback(() => componentRef.current, []);
42
+
43
+ const handleOnBeforeGetContent = useCallback(() => {
44
+ return new Promise<void>((resolve) => {
45
+ if (patient && bill) {
46
+ setIsPrinting(true);
47
+ onBeforeGetContentResolve.current = resolve;
48
+ }
49
+ });
50
+ }, [bill, patient]);
51
+
52
+ const handlePrint = useReactToPrint({
53
+ content: reactToPrintContent,
54
+ documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${
55
+ patient?.name?.[0].family
56
+ }`,
57
+ onBeforeGetContent: handleOnBeforeGetContent,
58
+ onAfterPrint: handleAfterPrint,
59
+ removeAfterPrint: true,
60
+ });
61
+
62
+ useEffect(() => {
63
+ if (isPrinting && onBeforeGetContentResolve.current) {
64
+ onBeforeGetContentResolve.current();
65
+ }
66
+ }, [isPrinting]);
67
+
68
+ const invoiceDetails = {
69
+ 'Total Amount': convertToCurrency(bill?.totalAmount),
70
+ 'Amount Tendered': convertToCurrency(bill?.tenderedAmount),
71
+ 'Invoice Number': bill.receiptNumber,
72
+ 'Date And Time': bill?.dateCreated,
73
+ 'Invoice Status': bill?.status,
74
+ };
75
+
76
+ if (isLoadingPatient && isLoadingBill) {
77
+ return (
78
+ <div className={styles.invoiceContainer}>
79
+ <InlineLoading
80
+ className={styles.loader}
81
+ status="active"
82
+ iconDescription="Loading"
83
+ description="Loading patient header..."
84
+ />
85
+ </div>
86
+ );
87
+ }
88
+
89
+ if (error) {
90
+ return (
91
+ <div className={styles.errorContainer}>
92
+ <ErrorState headerTitle={t('invoiceError', 'Invoice error')} error={error} />
93
+ </div>
94
+ );
95
+ }
96
+
97
+ return (
98
+ <div className={styles.invoiceContainer}>
99
+ {patient && patientUuid && <ExtensionSlot name="patient-header-slot" state={{ patient, patientUuid }} />}
100
+ <div className={styles.detailsContainer}>
101
+ <section className={styles.details}>
102
+ {Object.entries(invoiceDetails).map(([key, val]) => (
103
+ <InvoiceDetails key={key} label={key} value={val} />
104
+ ))}
105
+ </section>
106
+ <div>
107
+ <Button
108
+ disabled={isPrinting}
109
+ onClick={handlePrint}
110
+ renderIcon={(props) => <Printer size={24} {...props} />}
111
+ iconDescription="Print bill"
112
+ size="md">
113
+ {t('printBill', 'Print bill')}
114
+ </Button>
115
+ {bill.status === 'PAID' ? <PrintReceipt billId={bill?.id} /> : null}
116
+ </div>
117
+ </div>
118
+
119
+ <InvoiceTable bill={bill} isLoadingBill={isLoadingBill} onSelectItem={handleSelectItem} />
120
+ <Payments bill={bill} selectedLineItems={selectedLineItems} />
121
+
122
+ <div className={styles.printContainer} ref={componentRef}>
123
+ {isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoadingPatient} />}
124
+ </div>
125
+ </div>
126
+ );
127
+ };
128
+
129
+ function InvoiceDetails({ label, value }: InvoiceDetailsProps) {
130
+ return (
131
+ <div>
132
+ <h1 className={styles.label}>{label}</h1>
133
+ <span className={styles.value}>{value}</span>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ export default Invoice;
@@ -0,0 +1,93 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .invoiceContainer {
6
+ background-color: colors.$gray-10;
7
+ height: calc(100vh - 3rem);
8
+ }
9
+
10
+ .errorContainer {
11
+ margin: layout.$spacing-05;
12
+ }
13
+
14
+ .loader {
15
+ display: flex;
16
+ min-height: layout.$spacing-09;
17
+ justify-content: center;
18
+ }
19
+
20
+ .detailsContainer {
21
+ display: flex;
22
+ }
23
+
24
+ .details {
25
+ display: flex;
26
+ flex: 3;
27
+ flex-flow: row wrap;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ margin: layout.$spacing-05;
31
+ row-gap: 1.5rem;
32
+ }
33
+
34
+ .label {
35
+ @include type.type-style('body-compact-02');
36
+ color: colors.$gray-70;
37
+ margin: layout.$spacing-01;
38
+ }
39
+
40
+ .value {
41
+ @include type.type-style('heading-03');
42
+ display: inline-block;
43
+ margin-top: layout.$spacing-04;
44
+ }
45
+
46
+ .backButton {
47
+ margin: layout.$spacing-04;
48
+
49
+ button {
50
+ display: flex;
51
+ padding-left: 0 !important;
52
+
53
+ svg {
54
+ order: 1;
55
+ margin-right: layout.$spacing-03;
56
+ margin-left: 0 !important;
57
+ }
58
+
59
+ span {
60
+ order: 2;
61
+ }
62
+ }
63
+ }
64
+
65
+ .invoicePaymentsContainer {
66
+ display: flex;
67
+ flex-direction: column;
68
+ margin: layout.$spacing-05;
69
+ }
70
+
71
+ .paymentSection {
72
+ display: flex;
73
+ flex-direction: row;
74
+ }
75
+
76
+ .billDetail {
77
+ font-weight: bold;
78
+ color: colors.$cool-gray-90;
79
+ }
80
+
81
+ @media screen {
82
+ .printContainer {
83
+ background-color: colors.$white;
84
+ display: none;
85
+ }
86
+ }
87
+
88
+ @media print {
89
+ html,
90
+ body {
91
+ background-color: colors.$white !important;
92
+ }
93
+ }
@@ -0,0 +1,242 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { useReactToPrint } from 'react-to-print';
5
+ import { showSnackbar } from '@openmrs/esm-framework';
6
+ import { mockPayments, mockBill } from '../../__mocks__/bills.mock';
7
+ import { useBill, processBillPayment } from '../billing.resource';
8
+ import { usePaymentModes } from './payments/payment.resource';
9
+ import Invoice from './invoice.component';
10
+
11
+ const mockedBill = jest.mocked(useBill);
12
+ const mockedProcessBillPayment = jest.mocked(processBillPayment);
13
+ const mockedUsePaymentModes = jest.mocked(usePaymentModes);
14
+ const mockedUseReactToPrint = jest.mocked(useReactToPrint);
15
+
16
+ jest.mock('./payments/payment.resource', () => ({
17
+ usePaymentModes: jest.fn(),
18
+ updateBillVisitAttribute: jest.fn(),
19
+ }));
20
+
21
+ jest.mock('../billing.resource', () => ({
22
+ useBill: jest.fn(),
23
+ processBillPayment: jest.fn(),
24
+ useDefaultFacility: jest.fn().mockReturnValue({ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590', display: 'MTRH' }),
25
+ }));
26
+
27
+ jest.mock('react-router-dom', () => {
28
+ const originalModule = jest.requireActual('react-router-dom');
29
+
30
+ return {
31
+ ...originalModule,
32
+ useParams: jest.fn().mockReturnValue({ patientUuid: 'patientUuid', billUuid: 'billUuid' }),
33
+ };
34
+ });
35
+
36
+ jest.mock('react-to-print', () => {
37
+ const originalModule = jest.requireActual('react-to-print');
38
+
39
+ return {
40
+ ...originalModule,
41
+ useReactToPrint: jest.fn(),
42
+ };
43
+ });
44
+
45
+ jest.mock('@openmrs/esm-framework', () => {
46
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
47
+
48
+ return {
49
+ ...originalModule,
50
+ usePatient: jest.fn().mockReturnValue({
51
+ patient: {
52
+ id: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
53
+ name: [{ given: ['John'], family: 'Doe' }],
54
+ },
55
+ patientUuid: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
56
+ isLoading: false,
57
+ error: null,
58
+ }),
59
+ };
60
+ });
61
+
62
+ xdescribe('Invoice', () => {
63
+ beforeEach(() => {
64
+ mockedBill.mockReturnValue({
65
+ bill: mockBill,
66
+ isLoading: false,
67
+ error: null,
68
+ isValidating: false,
69
+ mutate: jest.fn(),
70
+ });
71
+
72
+ mockedUsePaymentModes.mockReturnValue({
73
+ paymentModes: [
74
+ { uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
75
+ { uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
76
+ ],
77
+ isLoading: false,
78
+ error: null,
79
+ mutate: jest.fn(),
80
+ });
81
+ });
82
+
83
+ afterEach(() => jest.clearAllMocks());
84
+
85
+ test('should be able to search through the invoice table and settle a bill', async () => {
86
+ const user = userEvent.setup();
87
+
88
+ renderInvoice();
89
+
90
+ const expectedHeaders = [
91
+ /Total amount/i,
92
+ /Amount tendered/i,
93
+ /Date and time/i,
94
+ /Invoice status/i,
95
+ /Invoice number/i,
96
+ ];
97
+
98
+ expectedHeaders.forEach((header) => {
99
+ expect(screen.getByRole('heading', { name: header })).toBeInTheDocument();
100
+ });
101
+
102
+ const printButton = screen.getByRole('button', { name: /Print bill/i });
103
+ expect(printButton).toBeInTheDocument();
104
+
105
+ // Should show the line items table with the correct headers
106
+ const expectedColumnHeaders = [/No/i, /Bill item/i, /Bill code/i, /Status/i, /Quantity/i, /Price/i, /Total/i];
107
+
108
+ expectedColumnHeaders.forEach((columnHeader) => {
109
+ expect(screen.getByRole('columnheader', { name: columnHeader })).toBeInTheDocument();
110
+ });
111
+
112
+ expect(screen.getByRole('heading', { name: /Line items/i })).toBeInTheDocument();
113
+ expect(screen.getByText(/Items to be billed/i)).toBeInTheDocument();
114
+
115
+ // Should be able to search the line items table
116
+ const searchInput = screen.getByRole('searchbox');
117
+ expect(searchInput).toBeInTheDocument();
118
+ await user.type(searchInput, 'Hemoglobin');
119
+ expect(screen.getByText('Hemoglobin')).toBeInTheDocument();
120
+
121
+ await user.type(searchInput, 'Some random text');
122
+ expect(screen.queryByText('Hemoglobin')).not.toBeInTheDocument();
123
+ expect(screen.getByText(/No matching items to display/i)).toBeInTheDocument();
124
+ await user.clear(searchInput);
125
+
126
+ const row = mockBill.lineItems[0].item + ' ' + mockBill.receiptNumber + ' ' + mockBill.status.toUpperCase();
127
+
128
+ expect(screen.getByRole('row', { name: new RegExp(row, 'i') })).toBeInTheDocument();
129
+
130
+ // should be able to handle payments
131
+ const paymentSection = await screen.findByRole('heading', { name: /Payments/i });
132
+ expect(paymentSection).toBeInTheDocument();
133
+
134
+ const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
135
+ expect(addPaymentOptionButton).toBeInTheDocument();
136
+ await user.click(addPaymentOptionButton);
137
+ const paymentModeInput = screen.getByRole('combobox', { name: /Payment method/i });
138
+ expect(paymentModeInput).toBeInTheDocument();
139
+ await user.click(paymentModeInput);
140
+
141
+ // select cash payment mode
142
+ const cashPaymentMode = await screen.findByText('Cash');
143
+ expect(cashPaymentMode).toBeInTheDocument();
144
+ await user.click(cashPaymentMode);
145
+
146
+ // enter payment amount
147
+ const paymentAmountInput = screen.getByPlaceholderText('Enter amount');
148
+ expect(paymentAmountInput).toBeInTheDocument();
149
+ await user.type(paymentAmountInput, '100');
150
+
151
+ // enter payment reference number
152
+ const paymentReferenceNumberInput = screen.getByRole('textbox', { name: /Reference number/ });
153
+ expect(paymentReferenceNumberInput).toBeInTheDocument();
154
+ await user.type(paymentReferenceNumberInput, '123456');
155
+
156
+ expect(addPaymentOptionButton).toBeDisabled();
157
+
158
+ // should process payment
159
+ mockedProcessBillPayment.mockResolvedValueOnce(Promise.resolve({} as any));
160
+ const processPaymentButton = screen.getByRole('button', { name: /Process Payment/i });
161
+ expect(processPaymentButton).toBeInTheDocument();
162
+ await user.click(processPaymentButton);
163
+
164
+ expect(processBillPayment).toHaveBeenCalledTimes(1);
165
+ expect(processBillPayment).toHaveBeenCalledWith(
166
+ {
167
+ cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590',
168
+ cashier: 'fe00dd43-4c39-4ce9-9832-bc3620c80c6c',
169
+ patient: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
170
+ payments: [{ amount: 100, amountTendered: 100, attributes: [], instanceType: 'uuid' }],
171
+ status: 'PAID',
172
+ },
173
+ '6eb8d678-514d-46ad-9554-51e48d96d567',
174
+ );
175
+ expect(showSnackbar).toHaveBeenCalled();
176
+ expect(showSnackbar).toHaveBeenCalledWith({
177
+ kind: 'success',
178
+ subtitle: 'Bill payment processing has been successful',
179
+ timeoutInMs: 3000,
180
+ title: 'Bill payment',
181
+ });
182
+ });
183
+
184
+ test('should show print preview when print button is clicked', async () => {
185
+ const user = userEvent.setup();
186
+
187
+ renderInvoice();
188
+
189
+ const printButton = screen.getByRole('button', { name: /Print bill/i });
190
+ expect(printButton).toBeInTheDocument();
191
+ await user.click(printButton);
192
+ expect(mockedUseReactToPrint).toHaveBeenCalledTimes(1);
193
+ expect(mockedUseReactToPrint).toHaveBeenCalledWith(
194
+ expect.objectContaining({
195
+ documentTitle: 'Invoice 0035-6 - John Doe',
196
+ }),
197
+ );
198
+ });
199
+
200
+ test('should show payment history if bill is paid and disable adding more payments', async () => {
201
+ const user = userEvent.setup();
202
+ mockedBill.mockReturnValue({
203
+ bill: {
204
+ ...mockBill,
205
+ status: 'PAID',
206
+ payments: mockPayments,
207
+ tenderedAmount: 100,
208
+ },
209
+ isLoading: false,
210
+ error: null,
211
+ isValidating: false,
212
+ mutate: jest.fn(),
213
+ });
214
+
215
+ mockedUsePaymentModes.mockReturnValue({
216
+ paymentModes: [
217
+ { uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
218
+ { uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
219
+ ],
220
+ isLoading: false,
221
+ error: null,
222
+ mutate: jest.fn(),
223
+ });
224
+
225
+ renderInvoice();
226
+ const paymentHistorySection = screen.getByRole('heading', { name: /Payments/i });
227
+ expect(paymentHistorySection).toBeInTheDocument();
228
+
229
+ const expectedColumnHeaders = [/Date of payment/, /Bill amount/, /Amount tendered/, /Payment method/];
230
+ expectedColumnHeaders.forEach((header) => {
231
+ expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
232
+ });
233
+
234
+ const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
235
+ expect(addPaymentOptionButton).toBeInTheDocument();
236
+ expect(addPaymentOptionButton).toBeDisabled();
237
+ });
238
+ });
239
+
240
+ function renderInvoice() {
241
+ return render(<Invoice />);
242
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import styles from './invoice-breakdown.scss';
3
+
4
+ type InvoiceBreakDownProps = {
5
+ label: string;
6
+ value: string;
7
+ hasBalance?: Boolean;
8
+ };
9
+
10
+ export const InvoiceBreakDown: React.FC<InvoiceBreakDownProps> = ({ label, value, hasBalance }) => {
11
+ return (
12
+ <div className={styles.invoiceBreakdown}>
13
+ <span className={hasBalance ? styles.extendedLabel : styles.label}>{label}: </span>
14
+ <span className={styles.value}>{value}</span>
15
+ </div>
16
+ );
17
+ };
@@ -0,0 +1,29 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .invoiceBreakdown {
6
+ display: grid;
7
+ grid-template-columns: 1fr 1fr;
8
+ align-items: flex-end;
9
+ margin: layout.$spacing-02 0;
10
+ }
11
+
12
+ .label {
13
+ @include type.type-style('heading-03');
14
+ color: colors.$gray-100;
15
+ text-align: end;
16
+ }
17
+
18
+ .value {
19
+ @extend .label;
20
+ font-weight: bold;
21
+ margin-left: layout.$spacing-03;
22
+ text-align: start;
23
+ }
24
+
25
+ .extendedLabel {
26
+ @extend .label;
27
+ font-weight: bold;
28
+ color: crimson;
29
+ }