@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.
- package/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +57 -0
- package/.husky/pre-commit +7 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +14 -0
- package/.turbo.json +18 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/LICENSE +401 -0
- package/README.md +7 -0
- package/__mocks__/bills.mock.ts +392 -0
- package/__mocks__/delivery-summary.mock.ts +87 -0
- package/__mocks__/encounter-observation.mock.ts +10649 -0
- package/__mocks__/encounter-observations.mock.ts +6187 -0
- package/__mocks__/hiv-summary.mock.ts +22 -0
- package/__mocks__/patient-summary.mock.ts +32 -0
- package/__mocks__/patient.mock.ts +59 -0
- package/__mocks__/program-summary.mock.ts +43 -0
- package/__mocks__/react-i18next.js +57 -0
- package/dist/294.js +2 -0
- package/dist/294.js.LICENSE.txt +9 -0
- package/dist/294.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/384.js +1 -0
- package/dist/384.js.map +1 -0
- package/dist/421.js +1 -0
- package/dist/421.js.map +1 -0
- package/dist/450.js +1 -0
- package/dist/450.js.map +1 -0
- package/dist/476.js +1 -0
- package/dist/476.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/757.js +1 -0
- package/dist/788.js +1 -0
- package/dist/800.js +2 -0
- package/dist/800.js.LICENSE.txt +3 -0
- package/dist/800.js.map +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/935.js +2 -0
- package/dist/935.js.LICENSE.txt +19 -0
- package/dist/935.js.map +1 -0
- package/dist/96.js +2 -0
- package/dist/96.js.LICENSE.txt +47 -0
- package/dist/96.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +47 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-billing-app.js +1 -0
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +462 -0
- package/dist/openmrs-esm-billing-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/README.md +115 -0
- package/e2e/core/global-setup.ts +32 -0
- package/e2e/core/index.ts +1 -0
- package/e2e/core/test.ts +20 -0
- package/e2e/fixtures/api.ts +26 -0
- package/e2e/fixtures/index.ts +1 -0
- package/e2e/pages/home-page.ts +9 -0
- package/e2e/pages/index.ts +1 -0
- package/e2e/specs/sample-test.spec.ts +11 -0
- package/e2e/support/github/Dockerfile +34 -0
- package/e2e/support/github/docker-compose.yml +24 -0
- package/e2e/support/github/run-e2e-docker-env.sh +49 -0
- package/example.env +6 -0
- package/i18next-parser.config.js +89 -0
- package/jest.config.js +34 -0
- package/package.json +123 -0
- package/playwright.config.ts +32 -0
- package/prettier.config.js +8 -0
- package/src/bill-history/bill-history.component.tsx +187 -0
- package/src/bill-history/bill-history.scss +151 -0
- package/src/bill-history/bill-history.test.tsx +122 -0
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +72 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +108 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
- package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +135 -0
- package/src/billable-services/bill-waiver/utils.ts +41 -0
- package/src/billable-services/billable-service.resource.ts +71 -0
- package/src/billable-services/billable-services-home.component.tsx +51 -0
- package/src/billable-services/billable-services.component.tsx +255 -0
- package/src/billable-services/billable-services.scss +218 -0
- package/src/billable-services/billable-services.test.tsx +16 -0
- package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
- package/src/billable-services/create-edit/add-billable-service.scss +131 -0
- package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
- package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
- package/src/billable-services/dashboard/dashboard.scss +27 -0
- package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
- package/src/billable-services/dashboard/service-metrics.component.tsx +42 -0
- package/src/billable-services-admin-card-link.component.test.tsx +21 -0
- package/src/billable-services-admin-card-link.component.tsx +25 -0
- package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
- package/src/billing-dashboard/billing-dashboard.scss +27 -0
- package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
- package/src/billing-form/billing-checkin-form.component.tsx +131 -0
- package/src/billing-form/billing-checkin-form.scss +13 -0
- package/src/billing-form/billing-checkin-form.test.tsx +134 -0
- package/src/billing-form/billing-form.component.tsx +25 -0
- package/src/billing-form/billing-form.resource.ts +31 -0
- package/src/billing-form/billing-form.scss +5 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
- package/src/billing-header/billing-header.component.tsx +43 -0
- package/src/billing-header/billing-header.scss +83 -0
- package/src/billing-header/billing-illustration.component.tsx +30 -0
- package/src/billing.resource.ts +120 -0
- package/src/bills-table/bills-table.component.tsx +280 -0
- package/src/bills-table/bills-table.scss +181 -0
- package/src/bills-table/bills-table.test.tsx +154 -0
- package/src/config-schema.ts +3 -0
- package/src/dashboard.meta.ts +6 -0
- package/src/declarations.d.ts +4 -0
- package/src/helpers/functions.ts +63 -0
- package/src/helpers/index.ts +1 -0
- package/src/index.ts +56 -0
- package/src/invoice/invoice-table.component.tsx +185 -0
- package/src/invoice/invoice-table.scss +91 -0
- package/src/invoice/invoice.component.tsx +138 -0
- package/src/invoice/invoice.scss +93 -0
- package/src/invoice/invoice.test.tsx +242 -0
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
- package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
- package/src/invoice/payments/payment-form/payment-form.scss +54 -0
- package/src/invoice/payments/payment-history/payment-history.component.tsx +68 -0
- package/src/invoice/payments/payment.resource.ts +43 -0
- package/src/invoice/payments/payments.component.tsx +140 -0
- package/src/invoice/payments/payments.scss +46 -0
- package/src/invoice/payments/utils.ts +30 -0
- package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
- package/src/invoice/printable-invoice/print-receipt.component.tsx +28 -0
- package/src/invoice/printable-invoice/print-receipt.scss +14 -0
- package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
- package/src/invoice/printable-invoice/printable-footer.scss +17 -0
- package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
- package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
- package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
- package/src/left-panel-link.component.tsx +41 -0
- package/src/left-panel-link.test.tsx +38 -0
- package/src/metrics-cards/card.component.tsx +11 -0
- package/src/metrics-cards/card.scss +20 -0
- package/src/metrics-cards/metrics-cards.component.tsx +42 -0
- package/src/metrics-cards/metrics-cards.scss +12 -0
- package/src/metrics-cards/metrics-cards.test.tsx +41 -0
- package/src/metrics-cards/metrics.resource.ts +45 -0
- package/src/modal/require-payment-modal.component.tsx +81 -0
- package/src/modal/require-payment.scss +6 -0
- package/src/root.component.tsx +19 -0
- package/src/root.scss +30 -0
- package/src/routes.json +79 -0
- package/src/setup-tests.ts +13 -0
- package/src/types/index.ts +167 -0
- package/test-helpers.tsx +23 -0
- package/translations/am.json +107 -0
- package/translations/en.json +107 -0
- package/translations/es.json +107 -0
- package/translations/fr.json +107 -0
- package/translations/he.json +107 -0
- package/translations/km.json +107 -0
- package/tsconfig.json +16 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* eslint-disable curly */
|
|
2
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ComboBox,
|
|
6
|
+
Dropdown,
|
|
7
|
+
Form,
|
|
8
|
+
FormLabel,
|
|
9
|
+
InlineLoading,
|
|
10
|
+
Layer,
|
|
11
|
+
Search,
|
|
12
|
+
TextInput,
|
|
13
|
+
Tile,
|
|
14
|
+
} from '@carbon/react';
|
|
15
|
+
import { navigate, showSnackbar, useDebounce, useLayoutType, useSession } from '@openmrs/esm-framework';
|
|
16
|
+
import { Add, TrashCan, WarningFilled } from '@carbon/react/icons';
|
|
17
|
+
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|
18
|
+
import { useTranslation } from 'react-i18next';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
21
|
+
import {
|
|
22
|
+
createBillableSerice,
|
|
23
|
+
useConceptsSearch,
|
|
24
|
+
usePaymentModes,
|
|
25
|
+
useServiceTypes,
|
|
26
|
+
} from '../billable-service.resource';
|
|
27
|
+
import { type ServiceConcept } from '../../types';
|
|
28
|
+
import styles from './add-billable-service.scss';
|
|
29
|
+
|
|
30
|
+
type PaymentMode = {
|
|
31
|
+
paymentMode: string;
|
|
32
|
+
price: string | number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type PaymentModeFormValue = {
|
|
36
|
+
payment: Array<PaymentMode>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const servicePriceSchema = z.object({
|
|
40
|
+
paymentMode: z.string().refine((value) => !!value, 'Payment method is required'),
|
|
41
|
+
price: z.union([
|
|
42
|
+
z.number().refine((value) => !!value, 'Price is required'),
|
|
43
|
+
z.string().refine((value) => !!value, 'Price is required'),
|
|
44
|
+
]),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const paymentFormSchema = z.object({ payment: z.array(servicePriceSchema) });
|
|
48
|
+
|
|
49
|
+
const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: 0 };
|
|
50
|
+
|
|
51
|
+
const AddBillableService: React.FC = () => {
|
|
52
|
+
const { t } = useTranslation();
|
|
53
|
+
|
|
54
|
+
const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes();
|
|
55
|
+
const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes();
|
|
56
|
+
const [billableServicePayload, setBillableServicePayload] = useState<any>({});
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
control,
|
|
60
|
+
handleSubmit,
|
|
61
|
+
formState: { errors },
|
|
62
|
+
} = useForm<any>({
|
|
63
|
+
mode: 'all',
|
|
64
|
+
defaultValues: {},
|
|
65
|
+
resolver: zodResolver(paymentFormSchema),
|
|
66
|
+
});
|
|
67
|
+
const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
|
|
68
|
+
|
|
69
|
+
const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]);
|
|
70
|
+
const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
|
|
71
|
+
|
|
72
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
73
|
+
const searchInputRef = useRef(null);
|
|
74
|
+
const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(event.target.value);
|
|
75
|
+
|
|
76
|
+
const [selectedConcept, setSelectedConcept] = useState<ServiceConcept>(null);
|
|
77
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
78
|
+
const debouncedSearchTerm = useDebounce(searchTerm);
|
|
79
|
+
const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm);
|
|
80
|
+
const handleConceptChange = useCallback((selectedConcept: any) => {
|
|
81
|
+
setSelectedConcept(selectedConcept);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const handleNavigateToServiceDashboard = () =>
|
|
85
|
+
navigate({
|
|
86
|
+
to: window.getOpenmrsSpaBase() + 'billable-services',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (isLoadingPaymentModes && isLoadingServicesTypes) {
|
|
90
|
+
return (
|
|
91
|
+
<InlineLoading
|
|
92
|
+
status="active"
|
|
93
|
+
iconDescription={t('loadingDescription', 'Loading')}
|
|
94
|
+
description={t('loading', 'Loading data...')}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const onSubmit = (data) => {
|
|
100
|
+
const payload: any = {};
|
|
101
|
+
|
|
102
|
+
let servicePrices = [];
|
|
103
|
+
data.payment.forEach((element) => {
|
|
104
|
+
element.name = paymentModes.filter((p) => p.uuid === element.paymentMode)[0].name;
|
|
105
|
+
servicePrices.push(element);
|
|
106
|
+
});
|
|
107
|
+
payload.name = billableServicePayload.serviceName;
|
|
108
|
+
payload.shortName = billableServicePayload.shortName;
|
|
109
|
+
payload.serviceType = billableServicePayload.serviceType.uuid;
|
|
110
|
+
payload.servicePrices = servicePrices;
|
|
111
|
+
payload.serviceStatus = 'ENABLED';
|
|
112
|
+
payload.concept = selectedConcept?.concept?.uuid;
|
|
113
|
+
|
|
114
|
+
createBillableSerice(payload).then(
|
|
115
|
+
(resp) => {
|
|
116
|
+
showSnackbar({
|
|
117
|
+
title: t('billableService', 'Billable service'),
|
|
118
|
+
subtitle: 'Billable service created successfully',
|
|
119
|
+
kind: 'success',
|
|
120
|
+
timeoutInMs: 3000,
|
|
121
|
+
});
|
|
122
|
+
handleNavigateToServiceDashboard();
|
|
123
|
+
},
|
|
124
|
+
(error) => {
|
|
125
|
+
showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error });
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Form className={styles.form}>
|
|
132
|
+
<h4>{t('addBillableServices', 'Add Billable Services')}</h4>
|
|
133
|
+
<section className={styles.section}>
|
|
134
|
+
<Layer>
|
|
135
|
+
<TextInput
|
|
136
|
+
id="serviceName"
|
|
137
|
+
type="text"
|
|
138
|
+
labelText={t('serviceName', 'Service Name')}
|
|
139
|
+
size="md"
|
|
140
|
+
onChange={(e) =>
|
|
141
|
+
setBillableServicePayload({
|
|
142
|
+
...billableServicePayload,
|
|
143
|
+
serviceName: e.target.value,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
placeholder="Enter service name"
|
|
147
|
+
/>
|
|
148
|
+
</Layer>
|
|
149
|
+
</section>
|
|
150
|
+
<section className={styles.section}>
|
|
151
|
+
<Layer>
|
|
152
|
+
<TextInput
|
|
153
|
+
id="serviceShortName"
|
|
154
|
+
type="text"
|
|
155
|
+
labelText={t('serviceShortName', 'Short Name')}
|
|
156
|
+
size="md"
|
|
157
|
+
onChange={(e) =>
|
|
158
|
+
setBillableServicePayload({
|
|
159
|
+
...billableServicePayload,
|
|
160
|
+
shortName: e.target.value,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
placeholder="Enter service short name"
|
|
164
|
+
/>
|
|
165
|
+
</Layer>
|
|
166
|
+
</section>
|
|
167
|
+
<section>
|
|
168
|
+
<FormLabel className={styles.conceptLabel}>Associated Concept</FormLabel>
|
|
169
|
+
<Controller
|
|
170
|
+
name="search"
|
|
171
|
+
control={control}
|
|
172
|
+
render={({ field: { onChange, value, onBlur } }) => (
|
|
173
|
+
<ResponsiveWrapper isTablet={isTablet}>
|
|
174
|
+
<Search
|
|
175
|
+
ref={searchInputRef}
|
|
176
|
+
size="md"
|
|
177
|
+
id="conceptsSearch"
|
|
178
|
+
labelText={t('enterConcept', 'Associated concept')}
|
|
179
|
+
placeholder={t('searchConcepts', 'Search associated concept')}
|
|
180
|
+
className={errors?.search && styles.serviceError}
|
|
181
|
+
onChange={(e) => {
|
|
182
|
+
onChange(e);
|
|
183
|
+
handleSearchTermChange(e);
|
|
184
|
+
}}
|
|
185
|
+
renderIcon={errors?.search && <WarningFilled />}
|
|
186
|
+
onBlur={onBlur}
|
|
187
|
+
onClear={() => {
|
|
188
|
+
setSearchTerm('');
|
|
189
|
+
setSelectedConcept(null);
|
|
190
|
+
}}
|
|
191
|
+
value={(() => {
|
|
192
|
+
if (selectedConcept) {
|
|
193
|
+
return selectedConcept.display;
|
|
194
|
+
}
|
|
195
|
+
if (debouncedSearchTerm) {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
})()}
|
|
199
|
+
/>
|
|
200
|
+
</ResponsiveWrapper>
|
|
201
|
+
)}
|
|
202
|
+
/>
|
|
203
|
+
{(() => {
|
|
204
|
+
if (!debouncedSearchTerm || selectedConcept) return null;
|
|
205
|
+
if (isSearching)
|
|
206
|
+
return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
|
|
207
|
+
if (searchResults && searchResults.length) {
|
|
208
|
+
return (
|
|
209
|
+
<ul className={styles.conceptsList}>
|
|
210
|
+
{/*TODO: use uuid instead of index as the key*/}
|
|
211
|
+
{searchResults?.map((searchResult, index) => (
|
|
212
|
+
<li
|
|
213
|
+
role="menuitem"
|
|
214
|
+
className={styles.service}
|
|
215
|
+
key={index}
|
|
216
|
+
onClick={() => handleConceptChange(searchResult)}>
|
|
217
|
+
{searchResult.display}
|
|
218
|
+
</li>
|
|
219
|
+
))}
|
|
220
|
+
</ul>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return (
|
|
224
|
+
<Layer>
|
|
225
|
+
<Tile className={styles.emptyResults}>
|
|
226
|
+
<span>
|
|
227
|
+
{t('noResultsFor', 'No results for')} <strong>"{debouncedSearchTerm}"</strong>
|
|
228
|
+
</span>
|
|
229
|
+
</Tile>
|
|
230
|
+
</Layer>
|
|
231
|
+
);
|
|
232
|
+
})()}
|
|
233
|
+
</section>
|
|
234
|
+
<section className={styles.section}>
|
|
235
|
+
<Layer>
|
|
236
|
+
<ComboBox
|
|
237
|
+
id="serviceType"
|
|
238
|
+
items={serviceTypes ?? []}
|
|
239
|
+
titleText={t('serviceType', 'Service Type')}
|
|
240
|
+
itemToString={(item) => item?.display}
|
|
241
|
+
onChange={({ selectedItem }) => {
|
|
242
|
+
setBillableServicePayload({
|
|
243
|
+
...billableServicePayload,
|
|
244
|
+
display: selectedItem?.display,
|
|
245
|
+
serviceType: selectedItem,
|
|
246
|
+
});
|
|
247
|
+
}}
|
|
248
|
+
placeholder="Select service type"
|
|
249
|
+
required
|
|
250
|
+
/>
|
|
251
|
+
</Layer>
|
|
252
|
+
</section>
|
|
253
|
+
|
|
254
|
+
<section>
|
|
255
|
+
<div className={styles.container}>
|
|
256
|
+
{fields.map((field, index) => (
|
|
257
|
+
<div key={field.id} className={styles.paymentMethodContainer}>
|
|
258
|
+
<Controller
|
|
259
|
+
control={control}
|
|
260
|
+
name={`payment.${index}.paymentMode`}
|
|
261
|
+
render={({ field }) => (
|
|
262
|
+
<Layer>
|
|
263
|
+
<Dropdown
|
|
264
|
+
onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
|
|
265
|
+
titleText={t('paymentMode', 'Payment Mode')}
|
|
266
|
+
label={t('selectPaymentMethod', 'Select payment method')}
|
|
267
|
+
items={paymentModes ?? []}
|
|
268
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
269
|
+
invalid={!!errors?.payment?.[index]?.paymentMode}
|
|
270
|
+
invalidText={errors?.payment?.[index]?.paymentMode?.message}
|
|
271
|
+
/>
|
|
272
|
+
</Layer>
|
|
273
|
+
)}
|
|
274
|
+
/>
|
|
275
|
+
<Controller
|
|
276
|
+
control={control}
|
|
277
|
+
name={`payment.${index}.price`}
|
|
278
|
+
render={({ field }) => (
|
|
279
|
+
<Layer>
|
|
280
|
+
<TextInput
|
|
281
|
+
{...field}
|
|
282
|
+
invalid={!!errors?.payment?.[index]?.price}
|
|
283
|
+
invalidText={errors?.payment?.[index]?.price?.message}
|
|
284
|
+
labelText={t('sellingPrice', 'Selling Price')}
|
|
285
|
+
placeholder={t('sellingAmount', 'Enter selling price')}
|
|
286
|
+
/>
|
|
287
|
+
</Layer>
|
|
288
|
+
)}
|
|
289
|
+
/>
|
|
290
|
+
<div className={styles.removeButtonContainer}>
|
|
291
|
+
<TrashCan onClick={handleRemovePaymentMode} className={styles.removeButton} size={20} />
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
295
|
+
<Button
|
|
296
|
+
size="md"
|
|
297
|
+
onClick={handleAppendPaymentMode}
|
|
298
|
+
className={styles.paymentButtons}
|
|
299
|
+
renderIcon={(props) => <Add size={24} {...props} />}
|
|
300
|
+
iconDescription="Add">
|
|
301
|
+
{t('addPaymentOptions', 'Add payment option')}
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
</section>
|
|
305
|
+
|
|
306
|
+
<section>
|
|
307
|
+
<Button kind="secondary" onClick={handleNavigateToServiceDashboard}>
|
|
308
|
+
{t('cancel', 'Cancel')}
|
|
309
|
+
</Button>
|
|
310
|
+
<Button type="submit" onClick={handleSubmit(onSubmit)}>
|
|
311
|
+
{t('save', 'Save')}
|
|
312
|
+
</Button>
|
|
313
|
+
</section>
|
|
314
|
+
</Form>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) {
|
|
319
|
+
return isTablet ? <Layer>{children} </Layer> : <>{children}</>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default AddBillableService;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
5
|
+
|
|
6
|
+
.form {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.section {
|
|
14
|
+
margin: layout.$spacing-03;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.sectionTitle {
|
|
18
|
+
@include type.type-style('heading-compact-02');
|
|
19
|
+
color: $text-02;
|
|
20
|
+
margin-bottom: layout.$spacing-04;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.modalBody {
|
|
24
|
+
padding-bottom: layout.$spacing-05;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.container {
|
|
28
|
+
margin: 1rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.paymentContainer {
|
|
32
|
+
margin: layout.$layout-01;
|
|
33
|
+
padding: layout.$layout-01;
|
|
34
|
+
width: 70%;
|
|
35
|
+
border-right: 1px solid colors.$cool-gray-40;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.paymentButtons {
|
|
39
|
+
margin: layout.$layout-01 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.paymentMethodContainer {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: repeat(4, minmax(auto, 1fr));
|
|
45
|
+
align-items: flex-start;
|
|
46
|
+
column-gap: 1rem;
|
|
47
|
+
margin: 0.625rem 0;
|
|
48
|
+
width: 100%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.paymentTotals {
|
|
52
|
+
margin-top: layout.$spacing-01;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.processPayments {
|
|
56
|
+
display: flex;
|
|
57
|
+
justify-content: flex-end;
|
|
58
|
+
margin: layout.$spacing-05;
|
|
59
|
+
column-gap: layout.$spacing-04;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.errorPaymentContainer {
|
|
63
|
+
margin: layout.$spacing-04;
|
|
64
|
+
min-height: layout.$spacing-09;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.removeButtonContainer {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-self: center;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
margin-left: layout.$spacing-07;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.removeButton {
|
|
75
|
+
color: colors.$red-60;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.service {
|
|
79
|
+
padding: 1rem 0.75rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.conceptsList {
|
|
83
|
+
background-color: $ui-02;
|
|
84
|
+
max-height: 14rem;
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
border: 1px solid $ui-03;
|
|
87
|
+
|
|
88
|
+
li:hover {
|
|
89
|
+
background-color: $ui-03;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.emptyResults {
|
|
94
|
+
@include type.type-style('body-compact-01');
|
|
95
|
+
color: $text-02;
|
|
96
|
+
min-height: 1rem;
|
|
97
|
+
border: 1px solid $ui-03;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.conceptLabel {
|
|
101
|
+
@include type.type-style('label-02');
|
|
102
|
+
margin: 1rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.errorContainer {
|
|
106
|
+
margin: 1rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.serviceError {
|
|
110
|
+
:global(.cds--search-input):focus {
|
|
111
|
+
outline: 2.5px solid $danger;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
:global(.cds--search-magnifier) {
|
|
115
|
+
svg {
|
|
116
|
+
fill: $danger;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.errorMessage {
|
|
122
|
+
@include type.type-style('label-02');
|
|
123
|
+
color: $danger;
|
|
124
|
+
margin-top: 0.5rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.spinner {
|
|
128
|
+
&:global(.cds--inline-loading) {
|
|
129
|
+
min-height: 1rem;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import AddBillableService from './add-billable-service.component';
|
|
5
|
+
import {
|
|
6
|
+
useBillableServices,
|
|
7
|
+
usePaymentModes,
|
|
8
|
+
useServiceTypes,
|
|
9
|
+
createBillableSerice,
|
|
10
|
+
} from '../billable-service.resource';
|
|
11
|
+
import { FetchResponse, navigate, showSnackbar } from '@openmrs/esm-framework';
|
|
12
|
+
|
|
13
|
+
const mockUseBillableServices = useBillableServices as jest.MockedFunction<typeof useBillableServices>;
|
|
14
|
+
const mockUsePaymentModes = usePaymentModes as jest.MockedFunction<typeof usePaymentModes>;
|
|
15
|
+
const mockUseServiceTypes = useServiceTypes as jest.MockedFunction<typeof useServiceTypes>;
|
|
16
|
+
const mockCreateBillableSerice = createBillableSerice as jest.MockedFunction<typeof createBillableSerice>;
|
|
17
|
+
const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
|
|
18
|
+
const mockShowSnackbar = showSnackbar as jest.MockedFunction<typeof showSnackbar>;
|
|
19
|
+
|
|
20
|
+
jest.mock('../billable-service.resource', () => ({
|
|
21
|
+
useBillableServices: jest.fn(),
|
|
22
|
+
usePaymentModes: jest.fn(),
|
|
23
|
+
useServiceTypes: jest.fn(),
|
|
24
|
+
createBillableSerice: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const mockPaymentModes = [
|
|
28
|
+
{ uuid: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74', name: 'Cash', description: 'Cash Payment', retired: false },
|
|
29
|
+
{
|
|
30
|
+
uuid: 'beac329b-f1dc-4a33-9e7c-d95821a137a6',
|
|
31
|
+
name: 'Insurance',
|
|
32
|
+
description: 'Insurance method of payment',
|
|
33
|
+
retired: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
uuid: '28989582-e8c3-46b0-96d0-c249cb06d5c6',
|
|
37
|
+
name: 'MPESA',
|
|
38
|
+
description: 'Mobile money method of payment',
|
|
39
|
+
retired: false,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const mockServiceTypes = [
|
|
44
|
+
{ uuid: 'c9604249-db0a-4d03-b074-fc6bc2fa13e6', display: 'Lab service' },
|
|
45
|
+
{ uuid: 'b75e466f-a6f5-4d5e-849a-84424d3c85cd', display: 'Pharmacy service' },
|
|
46
|
+
{ uuid: 'ce914b2d-44f6-4b6c-933f-c57a3938e35b', display: 'Peer educator service' },
|
|
47
|
+
{ uuid: 'c23d3224-2218-4007-8f22-e1f3d5a8e58a', display: 'Nutrition service' },
|
|
48
|
+
{ uuid: '65487ff4-63b3-452a-8985-6a1f4a0cc08d', display: 'TB service' },
|
|
49
|
+
{ uuid: '9db142d5-5cc4-4c05-9f83-06ed294caa67', display: 'Family planning service' },
|
|
50
|
+
{ uuid: 'a487a743-62ce-4f93-a66b-c5154ee8987d', display: 'Adherence counselling service' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
xdescribe('AddBillableService', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
jest.resetAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should render billable services form and generate correct payload', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
mockUseBillableServices.mockReturnValue({
|
|
61
|
+
billableServices: [],
|
|
62
|
+
isLoading: false,
|
|
63
|
+
error: null,
|
|
64
|
+
mutate: jest.fn(),
|
|
65
|
+
isValidating: false,
|
|
66
|
+
});
|
|
67
|
+
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoading: false });
|
|
68
|
+
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoading: false });
|
|
69
|
+
render(<AddBillableService />);
|
|
70
|
+
|
|
71
|
+
const formTtile = screen.getByRole('heading', { name: /Add Billable Services/i });
|
|
72
|
+
expect(formTtile).toBeInTheDocument();
|
|
73
|
+
|
|
74
|
+
const serviceNameTextInp = screen.getByRole('textbox', { name: /Service Name/i });
|
|
75
|
+
expect(serviceNameTextInp).toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
const serviceShortNameTextInp = screen.getByRole('textbox', { name: /Short Name/i });
|
|
78
|
+
expect(serviceShortNameTextInp).toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
await user.type(serviceNameTextInp, 'Test Service Name');
|
|
81
|
+
await user.type(serviceShortNameTextInp, 'Test Short Name');
|
|
82
|
+
|
|
83
|
+
expect(serviceNameTextInp).toHaveValue('Test Service Name');
|
|
84
|
+
expect(serviceShortNameTextInp).toHaveValue('Test Short Name');
|
|
85
|
+
|
|
86
|
+
const serviceTypeComboBox = screen.getByRole('combobox', { name: /Service Type/i });
|
|
87
|
+
expect(serviceTypeComboBox).toBeInTheDocument();
|
|
88
|
+
await user.click(serviceTypeComboBox);
|
|
89
|
+
const serviceTypeOptions = screen.getByRole('option', { name: /Lab service/i });
|
|
90
|
+
expect(serviceTypeOptions).toBeInTheDocument();
|
|
91
|
+
await user.click(serviceTypeOptions);
|
|
92
|
+
|
|
93
|
+
const addPaymentMethodBtn = screen.getByRole('button', { name: /Add payment option/i });
|
|
94
|
+
expect(addPaymentMethodBtn).toBeInTheDocument();
|
|
95
|
+
|
|
96
|
+
await user.click(addPaymentMethodBtn);
|
|
97
|
+
|
|
98
|
+
const paymentMethodComboBox = screen.getByRole('combobox', { name: /Payment Mode/i });
|
|
99
|
+
expect(paymentMethodComboBox).toBeInTheDocument();
|
|
100
|
+
await user.click(paymentMethodComboBox);
|
|
101
|
+
const paymentMethodOptions = screen.getByRole('option', { name: /Cash/i });
|
|
102
|
+
expect(paymentMethodOptions).toBeInTheDocument();
|
|
103
|
+
await user.click(paymentMethodOptions);
|
|
104
|
+
|
|
105
|
+
const priceTextInp = screen.getByRole('textbox', { name: /Price/i });
|
|
106
|
+
expect(priceTextInp).toBeInTheDocument();
|
|
107
|
+
await user.type(priceTextInp, '1000');
|
|
108
|
+
|
|
109
|
+
mockCreateBillableSerice.mockReturnValue(Promise.resolve({} as FetchResponse<any>));
|
|
110
|
+
const saveBtn = screen.getByRole('button', { name: /Save/i });
|
|
111
|
+
expect(saveBtn).toBeInTheDocument();
|
|
112
|
+
await user.click(saveBtn);
|
|
113
|
+
|
|
114
|
+
expect(mockCreateBillableSerice).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(mockCreateBillableSerice).toHaveBeenCalledWith({
|
|
116
|
+
name: 'Test Service Name',
|
|
117
|
+
shortName: 'Test Short Name',
|
|
118
|
+
serviceType: undefined,
|
|
119
|
+
servicePrices: [
|
|
120
|
+
{
|
|
121
|
+
paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
|
|
122
|
+
price: '01000',
|
|
123
|
+
name: 'Cash',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
serviceStatus: 'ENABLED',
|
|
127
|
+
});
|
|
128
|
+
expect(mockNavigate).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("should navigate back to billable services dashboard when 'Cancel' button is clicked", async () => {
|
|
133
|
+
const user = userEvent.setup();
|
|
134
|
+
mockUseBillableServices.mockReturnValue({
|
|
135
|
+
billableServices: [],
|
|
136
|
+
isLoading: false,
|
|
137
|
+
error: null,
|
|
138
|
+
mutate: jest.fn(),
|
|
139
|
+
isValidating: false,
|
|
140
|
+
});
|
|
141
|
+
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoading: false });
|
|
142
|
+
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoading: false });
|
|
143
|
+
render(<AddBillableService />);
|
|
144
|
+
|
|
145
|
+
const cancelBtn = screen.getByRole('button', { name: /Cancel/i });
|
|
146
|
+
expect(cancelBtn).toBeInTheDocument();
|
|
147
|
+
await user.click(cancelBtn);
|
|
148
|
+
|
|
149
|
+
expect(mockNavigate).toHaveBeenCalledTimes(1);
|
|
150
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BillableServices from '../billable-services.component';
|
|
3
|
+
import ServiceMetrics from './service-metrics.component';
|
|
4
|
+
import styles from './dashboard.scss';
|
|
5
|
+
|
|
6
|
+
export default function BillableServicesDashboard() {
|
|
7
|
+
return (
|
|
8
|
+
<main className={styles.container}>
|
|
9
|
+
<ServiceMetrics />
|
|
10
|
+
<main className={styles.servicesTableContainer}>
|
|
11
|
+
<BillableServices />
|
|
12
|
+
</main>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
height: calc(100vh - 3rem);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.servicesTableContainer {
|
|
10
|
+
margin: 2rem 1rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.illo {
|
|
14
|
+
margin-top: layout.$spacing-05;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.content {
|
|
18
|
+
@include type.type-style('heading-compact-01');
|
|
19
|
+
color: colors.$gray-70;
|
|
20
|
+
margin-top: layout.$spacing-05;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tile {
|
|
24
|
+
border: 1px solid colors.$gray-20;
|
|
25
|
+
padding: 1.5rem 0;
|
|
26
|
+
text-align: center;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, render } from '@testing-library/react';
|
|
3
|
+
import BillableServicesDashboard from './dashboard.component';
|
|
4
|
+
|
|
5
|
+
test('renders an empty state when there are no services', () => {
|
|
6
|
+
renderBillingDashboard();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function renderBillingDashboard() {
|
|
10
|
+
render(<BillableServicesDashboard />);
|
|
11
|
+
}
|