@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,105 @@
1
+ import React, { useCallback } from 'react';
2
+ import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { TrashCan, Add } from '@carbon/react/icons';
5
+ import { Button, Dropdown, NumberInputSkeleton, TextInput, NumberInput } from '@carbon/react';
6
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
7
+ import { type PaymentFormValue } from '../payments.component';
8
+ import { usePaymentModes } from '../payment.resource';
9
+ import styles from './payment-form.scss';
10
+
11
+ type PaymentFormProps = { disablePayment: boolean; amountDue: number };
12
+
13
+ const DEFAULT_PAYMENT = { method: '', amount: 0, referenceCode: '' };
14
+
15
+ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue }) => {
16
+ const { t } = useTranslation();
17
+ const {
18
+ control,
19
+ formState: { errors },
20
+ } = useFormContext<PaymentFormValue>();
21
+ const { paymentModes, isLoading, error } = usePaymentModes();
22
+ const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
23
+
24
+ const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT), [append]);
25
+ const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
26
+
27
+ if (isLoading) {
28
+ return <NumberInputSkeleton />;
29
+ }
30
+
31
+ if (error) {
32
+ return (
33
+ <div className={styles.errorPaymentContainer}>
34
+ <ErrorState headerTitle={t('errorLoadingPaymentModes', 'Payment modes error')} error={error} />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ return (
40
+ <div className={styles.container}>
41
+ {fields.map((field, index) => (
42
+ <div key={field.id} className={styles.paymentMethodContainer}>
43
+ <Controller
44
+ control={control}
45
+ name={`payment.${index}.method`}
46
+ render={({ field }) => (
47
+ <Dropdown
48
+ id="paymentMethod"
49
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
50
+ titleText={t('paymentMethod', 'Payment method')}
51
+ label={t('selectPaymentMethod', 'Select payment method')}
52
+ items={paymentModes}
53
+ itemToString={(item) => (item ? item.name : '')}
54
+ invalid={!!errors?.payment?.[index]?.method}
55
+ invalidText={errors?.payment?.[index]?.method?.message}
56
+ />
57
+ )}
58
+ />
59
+ <Controller
60
+ control={control}
61
+ name={`payment.${index}.amount`}
62
+ render={({ field }) => (
63
+ <NumberInput
64
+ id="paymentAmount"
65
+ {...field}
66
+ onChange={(e) => field.onChange(Number(e.target.value))}
67
+ invalid={!!errors?.payment?.[index]?.amount}
68
+ invalidText={errors?.payment?.[index]?.amount?.message}
69
+ label={t('amount', 'Amount')}
70
+ placeholder={t('enterAmount', 'Enter amount')}
71
+ />
72
+ )}
73
+ />
74
+ <Controller
75
+ name={`payment.${index}.referenceCode`}
76
+ control={control}
77
+ render={({ field }) => (
78
+ <TextInput
79
+ id="paymentReferenceCode"
80
+ {...field}
81
+ labelText={t('referenceNumber', 'Reference number')}
82
+ placeholder={t('enterReferenceNumber', 'Enter ref. number')}
83
+ type="text"
84
+ />
85
+ )}
86
+ />
87
+ <div className={styles.removeButtonContainer}>
88
+ <TrashCan onClick={handleRemovePaymentMode} className={styles.removeButton} size={20} />
89
+ </div>
90
+ </div>
91
+ ))}
92
+ <Button
93
+ disabled={disablePayment}
94
+ size="md"
95
+ onClick={handleAppendPaymentMode}
96
+ className={styles.paymentButtons}
97
+ renderIcon={(props) => <Add size={24} {...props} />}
98
+ iconDescription="Add">
99
+ {t('addPaymentOptions', 'Add payment option')}
100
+ </Button>
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default PaymentForm;
@@ -0,0 +1,54 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ margin: 1rem;
7
+ }
8
+
9
+ .paymentContainer {
10
+ margin: layout.$layout-01;
11
+ padding: layout.$layout-01;
12
+ width: 70%;
13
+ border-right: 1px solid colors.$cool-gray-40;
14
+ }
15
+
16
+ .paymentButtons {
17
+ margin: layout.$layout-01 0;
18
+ }
19
+
20
+ .paymentMethodContainer {
21
+ display: grid;
22
+ grid-template-columns: repeat(4, minmax(auto, 1fr));
23
+ align-items: flex-start;
24
+ column-gap: 1rem;
25
+ margin: 0.625rem 0;
26
+ width: 100%;
27
+ }
28
+
29
+ .paymentTotals {
30
+ margin-top: layout.$spacing-01;
31
+ }
32
+
33
+ .processPayments {
34
+ display: flex;
35
+ justify-content: flex-end;
36
+ margin: layout.$spacing-05;
37
+ column-gap: layout.$spacing-04;
38
+ }
39
+
40
+ .errorPaymentContainer {
41
+ margin: layout.$spacing-04;
42
+ min-height: layout.$spacing-09;
43
+ }
44
+
45
+ .removeButtonContainer {
46
+ display: flex;
47
+ align-self: center;
48
+ cursor: pointer;
49
+ margin-left: layout.$spacing-07;
50
+ }
51
+
52
+ .removeButton {
53
+ color: colors.$red-60;
54
+ }
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { DataTable, Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '@carbon/react';
3
+ import { type MappedBill } from '../../../types';
4
+ import { formatDate } from '@openmrs/esm-framework';
5
+ import { convertToCurrency } from '../../../helpers';
6
+
7
+ type PaymentHistoryProps = {
8
+ bill: MappedBill;
9
+ };
10
+
11
+ const PaymentHistory: React.FC<PaymentHistoryProps> = ({ bill }) => {
12
+ const headers = [
13
+ {
14
+ key: 'dateCreated',
15
+ header: 'Date of payment',
16
+ },
17
+ {
18
+ key: 'amount',
19
+ header: 'Bill amount',
20
+ },
21
+ {
22
+ key: 'amountTendered',
23
+ header: 'Amount tendered',
24
+ },
25
+ {
26
+ key: 'paymentMethod',
27
+ header: 'Payment method',
28
+ },
29
+ ];
30
+ const rows = bill?.payments?.map((payment) => ({
31
+ id: `${payment.uuid}`,
32
+ dateCreated: formatDate(new Date(payment.dateCreated)),
33
+ amountTendered: convertToCurrency(payment.amountTendered),
34
+ amount: convertToCurrency(payment.amount),
35
+ paymentMethod: payment.instanceType.name,
36
+ }));
37
+
38
+ if (Object.values(bill?.payments ?? {}).length === 0) {
39
+ return;
40
+ }
41
+
42
+ return (
43
+ <DataTable size="sm" rows={rows} headers={headers}>
44
+ {({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
45
+ <Table {...getTableProps()}>
46
+ <TableHead>
47
+ <TableRow>
48
+ {headers.map((header) => (
49
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
50
+ ))}
51
+ </TableRow>
52
+ </TableHead>
53
+ <TableBody>
54
+ {rows.map((row) => (
55
+ <TableRow {...getRowProps({ row })}>
56
+ {row.cells.map((cell) => (
57
+ <TableCell key={cell.id}>{cell.value}</TableCell>
58
+ ))}
59
+ </TableRow>
60
+ ))}
61
+ </TableBody>
62
+ </Table>
63
+ )}
64
+ </DataTable>
65
+ );
66
+ };
67
+
68
+ export default PaymentHistory;
@@ -0,0 +1,43 @@
1
+ import useSWR from 'swr';
2
+ import { type Visit, openmrsFetch } from '@openmrs/esm-framework';
3
+
4
+ type PaymentMethod = {
5
+ uuid: string;
6
+ description: string;
7
+ name: string;
8
+ retired: boolean;
9
+ };
10
+
11
+ const swrOption = {
12
+ errorRetryCount: 2,
13
+ };
14
+
15
+ export const usePaymentModes = () => {
16
+ const url = `/ws/rest/v1/cashier/paymentMode`;
17
+ const { data, isLoading, error, mutate } = useSWR<{ data: { results: Array<PaymentMethod> } }>(
18
+ url,
19
+ openmrsFetch,
20
+ swrOption,
21
+ );
22
+
23
+ return {
24
+ paymentModes: data?.data?.results ?? [],
25
+ isLoading,
26
+ mutate,
27
+ error,
28
+ };
29
+ };
30
+
31
+ export const updateBillVisitAttribute = async (visit: Visit) => {
32
+ const { uuid, attributes } = visit;
33
+ const pendingPaymentAtrributeUuid = attributes?.find(
34
+ (attribute) => attribute.attributeType.uuid === '919b51c9-8e2e-468f-8354-181bf3e55786',
35
+ )?.uuid;
36
+ return openmrsFetch(`/ws/rest/v1/visit/${uuid}/attribute/${pendingPaymentAtrributeUuid}`, {
37
+ body: { value: false },
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ },
41
+ method: 'POST',
42
+ });
43
+ };
@@ -0,0 +1,140 @@
1
+ import React from 'react';
2
+ import { FormProvider, useForm, useWatch } from 'react-hook-form';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { z } from 'zod';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { navigate, showSnackbar, useVisit } from '@openmrs/esm-framework';
7
+ import { Button } from '@carbon/react';
8
+ import { CardHeader } from '@openmrs/esm-patient-common-lib';
9
+ import { type LineItem, type MappedBill } from '../../types';
10
+ import { convertToCurrency } from '../../helpers';
11
+ import { createPaymentPayload } from './utils';
12
+ import { processBillPayment } from '../../billing.resource';
13
+ import { InvoiceBreakDown } from './invoice-breakdown/invoice-breakdown.component';
14
+ import PaymentHistory from './payment-history/payment-history.component';
15
+ import PaymentForm from './payment-form/payment-form.component';
16
+ import { updateBillVisitAttribute } from './payment.resource';
17
+ import styles from './payments.scss';
18
+
19
+ type PaymentProps = {
20
+ bill: MappedBill;
21
+ selectedLineItems: Array<LineItem>;
22
+ };
23
+
24
+ export type Payment = { method: string; amount: string | number; referenceCode?: number | string };
25
+
26
+ export type PaymentFormValue = {
27
+ payment: Array<Payment>;
28
+ };
29
+
30
+ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
31
+ const { t } = useTranslation();
32
+ const paymentSchema = z.object({
33
+ method: z.string().refine((value) => !!value, 'Payment method is required'),
34
+ amount: z
35
+ .number()
36
+ .lte(bill.totalAmount - bill.tenderedAmount, { message: 'Amount paid should not be greater than amount due' }),
37
+ referenceCode: z.union([z.number(), z.string()]).optional(),
38
+ });
39
+
40
+ const paymentFormSchema = z.object({ payment: z.array(paymentSchema) });
41
+ const { currentVisit } = useVisit(bill?.patientUuid);
42
+ const methods = useForm<PaymentFormValue>({
43
+ mode: 'all',
44
+ defaultValues: {},
45
+ resolver: zodResolver(paymentFormSchema),
46
+ });
47
+
48
+ const formValues = useWatch({
49
+ name: 'payment',
50
+ control: methods.control,
51
+ });
52
+
53
+ const hasMoreThanOneLineItem = bill?.lineItems?.length > 1;
54
+
55
+ const computedTotal = hasMoreThanOneLineItem ? computeTotalPrice(selectedLineItems) : bill.totalAmount ?? 0;
56
+
57
+ const totalAmountTendered = formValues?.reduce((curr: number, prev) => curr + Number(prev.amount) ?? 0, 0) ?? 0;
58
+ const amountDue = Number(computedTotal) - (Number(bill.tenderedAmount) + Number(totalAmountTendered));
59
+
60
+ const handleNavigateToBillingDashboard = () =>
61
+ navigate({
62
+ to: window.getOpenmrsSpaBase() + 'home/billing',
63
+ });
64
+
65
+ const handleProcessPayment = () => {
66
+ const paymentPayload = createPaymentPayload(bill, bill.patientUuid, formValues, amountDue);
67
+ processBillPayment(paymentPayload, bill.uuid).then(
68
+ () => {
69
+ showSnackbar({
70
+ title: t('billPayment', 'Bill payment'),
71
+ subtitle: 'Bill payment processing has been successful',
72
+ kind: 'success',
73
+ timeoutInMs: 3000,
74
+ });
75
+ updateBillVisitAttribute(currentVisit);
76
+ handleNavigateToBillingDashboard();
77
+ },
78
+ (error) => {
79
+ showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error });
80
+ },
81
+ );
82
+ };
83
+
84
+ const amountDueDisplay = (amount: number) => (amount < 0 ? 'Client balance' : 'Amount Due');
85
+
86
+ return (
87
+ <FormProvider {...methods}>
88
+ <div className={styles.wrapper}>
89
+ <div className={styles.paymentContainer}>
90
+ <CardHeader title={t('payments', 'Payments')}>
91
+ <span></span>
92
+ </CardHeader>
93
+ <div>
94
+ {bill && <PaymentHistory bill={bill} />}
95
+ <PaymentForm disablePayment={amountDue <= 0} amountDue={amountDue} />
96
+ </div>
97
+ </div>
98
+ <div className={styles.divider} />
99
+ <div className={styles.paymentTotals}>
100
+ <InvoiceBreakDown label={t('totalAmount', 'Total Amount')} value={convertToCurrency(computedTotal)} />
101
+ <InvoiceBreakDown
102
+ label={t('totalTendered', 'Total Tendered')}
103
+ value={convertToCurrency(bill.tenderedAmount + totalAmountTendered ?? 0)}
104
+ />
105
+ <InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
106
+ <InvoiceBreakDown
107
+ hasBalance={amountDue < 0 ?? false}
108
+ label={amountDueDisplay(amountDue)}
109
+ value={convertToCurrency(amountDue ?? 0)}
110
+ />
111
+ <div className={styles.processPayments}>
112
+ <Button onClick={handleNavigateToBillingDashboard} kind="secondary">
113
+ {t('discard', 'Discard')}
114
+ </Button>
115
+ <Button onClick={() => handleProcessPayment()} disabled={!formValues?.length || !methods.formState.isValid}>
116
+ {t('processPayment', 'Process Payment')}
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </FormProvider>
122
+ );
123
+ };
124
+
125
+ const computeTotalPrice = (items) => {
126
+ if (items && !items.length) {
127
+ return 0;
128
+ }
129
+
130
+ let totalPrice = 0;
131
+
132
+ items?.forEach((item) => {
133
+ const { price, quantity } = item;
134
+ totalPrice += price * quantity;
135
+ });
136
+
137
+ return totalPrice;
138
+ };
139
+
140
+ export default Payments;
@@ -0,0 +1,46 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .wrapper {
6
+ display: flex;
7
+ }
8
+
9
+ .divider {
10
+ background: colors.$gray-20;
11
+ height: 12rem;
12
+ margin: 2rem;
13
+ width: 1px;
14
+ }
15
+
16
+ .paymentContainer {
17
+ margin: layout.$layout-01 0;
18
+ padding: layout.$layout-01;
19
+ width: 70%;
20
+ }
21
+
22
+ .paymentButtons {
23
+ margin: layout.$layout-01 0;
24
+ }
25
+
26
+ .paymentMethodContainer {
27
+ display: grid;
28
+ grid-template-columns: 1fr 1fr 1fr 1fr;
29
+ align-items: flex-end;
30
+ column-gap: 1rem;
31
+ margin: 0.625rem 0;
32
+ }
33
+
34
+ .paymentTotals {
35
+ margin: layout.$spacing-05 0;
36
+ padding: layout.$spacing-07 layout.$spacing-05;
37
+ }
38
+
39
+ .processPayments {
40
+ display: flex;
41
+ justify-content: flex-end;
42
+ margin: layout.$spacing-05;
43
+ padding-top: layout.$spacing-05;
44
+ column-gap: layout.$spacing-04;
45
+ border-top: 1px solid colors.$cool-gray-40;
46
+ }
@@ -0,0 +1,30 @@
1
+ import { type MappedBill } from '../../types';
2
+ import { type Payment } from './payments.component';
3
+
4
+ export const createPaymentPayload = (
5
+ bill: MappedBill,
6
+ patientUuid: string,
7
+ formValues: Array<Payment>,
8
+ amountDue: number,
9
+ ) => {
10
+ const { cashier } = bill;
11
+ const totalAmount = bill?.totalAmount;
12
+ const paymentStatus = amountDue <= 0 ? 'PAID' : 'PENDING';
13
+
14
+ const billPayment = formValues.map((formValue) => ({
15
+ amount: parseFloat(totalAmount.toFixed(2)),
16
+ amountTendered: parseFloat(Number(formValue.amount).toFixed(2)),
17
+ attributes: [],
18
+ instanceType: formValue.method,
19
+ }));
20
+ const processedPayment = {
21
+ cashPoint: bill.cashPointUuid,
22
+ cashier: cashier.uuid,
23
+ lineItems: bill.lineItems.map((lineItem) => ({ ...lineItem, billableService: 'service', paymentStatus: 'PAID' })),
24
+ payments: [...billPayment],
25
+ patient: patientUuid,
26
+ status: paymentStatus,
27
+ };
28
+
29
+ return processedPayment;
30
+ };
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Tag } from '@carbon/react';
3
+ import { usePatientPaymentInfo } from '../../../billing.resource';
4
+
5
+ type VisitAttributeTagsProps = { patientUuid: string };
6
+
7
+ const VisitAttributeTags: React.FC<VisitAttributeTagsProps> = ({ patientUuid }) => {
8
+ const patientBillingInfo = usePatientPaymentInfo(patientUuid);
9
+ return (
10
+ <div>
11
+ {patientBillingInfo?.map((tag) => (
12
+ <React.Fragment key={tag.name}>
13
+ <Tag type="gray">{tag.name}</Tag>
14
+ <Tag type="cool-gray">{tag.value}</Tag>
15
+ </React.Fragment>
16
+ ))}
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default VisitAttributeTags;
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { Button } from '@carbon/react';
3
+ import { Printer } from '@carbon/react/icons';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { ConfigurableLink } from '@openmrs/esm-framework';
6
+ import styles from './print-receipt.scss';
7
+
8
+ interface PrintReceiptProps {
9
+ billId: number;
10
+ }
11
+ const PrintReceipt: React.FC<PrintReceiptProps> = ({ billId }) => {
12
+ const { t } = useTranslation();
13
+ return (
14
+ <Button
15
+ kind="secondary"
16
+ className={styles.button}
17
+ size="md"
18
+ renderIcon={(props) => <Printer size={24} {...props} />}>
19
+ <ConfigurableLink
20
+ className={styles.configurableLink}
21
+ to={`\${openmrsBase}/ws/rest/v1/cashier/receipt?billId=${billId}`}>
22
+ {t('printReceipt', 'Print receipt')}
23
+ </ConfigurableLink>{' '}
24
+ </Button>
25
+ );
26
+ };
27
+
28
+ export default PrintReceipt;
@@ -0,0 +1,14 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .configurableLink {
6
+ color: colors.$white;
7
+ margin: 0;
8
+ padding: 0;
9
+ text-decoration: none;
10
+ }
11
+
12
+ .button {
13
+ margin-left: layout.$spacing-04;
14
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { useDefaultFacility } from '../../billing.resource';
3
+ import styles from './printable-footer.scss';
4
+
5
+ const PrintableFooter = () => {
6
+ const { data, isLoading } = useDefaultFacility();
7
+
8
+ if (isLoading) {
9
+ return <div>--</div>;
10
+ }
11
+
12
+ return (
13
+ <div className={styles.container}>
14
+ <p className={styles.itemFooter}>{data?.display}</p>
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default PrintableFooter;
@@ -0,0 +1,17 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ display: flex;
7
+ flex-direction: column;
8
+ position: fixed;
9
+ width: 100%;
10
+ bottom: 0;
11
+ }
12
+
13
+ .itemFooter {
14
+ padding: 1rem;
15
+ @include type.type-style('body-compact-02');
16
+ color: colors.$cool-gray-90;
17
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import { useDefaultFacility } from '../../billing.resource';
4
+ import PrintableFooter from './printable-footer.component';
5
+
6
+ const mockUseDefaultFacility = useDefaultFacility as jest.MockedFunction<typeof useDefaultFacility>;
7
+
8
+ jest.mock('../../billing.resource', () => ({
9
+ useDefaultFacility: jest.fn(),
10
+ }));
11
+
12
+ describe('PrintableFooter', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ test('should render PrintableFooter component', () => {
18
+ mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
19
+ render(<PrintableFooter />);
20
+ const footer = screen.getByText('MTRH');
21
+ expect(footer).toBeInTheDocument();
22
+ });
23
+
24
+ test('should show placeholder text when facility isLoading', () => {
25
+ mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: true });
26
+ render(<PrintableFooter />);
27
+ const footer = screen.getByText('--');
28
+ expect(footer).toBeInTheDocument();
29
+ });
30
+ });