@digihmis/esm-billing-app 1.0.0 → 1.0.1
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/package.json +2 -1
- package/src/billing-checkin-form/billing-checkin-form.extension.tsx +90 -88
- package/src/billing-checkin-form/billing-checkin-form.resource.ts +92 -27
- package/src/billing-checkin-form/visit-attributes/visit-attributes-form.resource.ts +28 -16
- package/src/billing-dashboard/billing-dashboard.resource.ts +16 -7
- package/src/billing-header/page-header/page-icon/icon-wrapper.extension.tsx +2 -17
- package/src/billing-timesheet-modal/billing-timesheet-modal.tsx +4 -2
- package/src/billing-timesheet-modal/billing-timesheet-resource-modal.ts +7 -28
- package/src/config-schema.ts +8 -2
- package/src/leftpanel/left-panel.component.tsx +6 -98
- package/src/routes.json +12 -8
- package/dist/117.js +0 -1
- package/dist/117.js.map +0 -1
- package/dist/132.js +0 -1
- package/dist/132.js.map +0 -1
- package/dist/152.js +0 -1
- package/dist/152.js.map +0 -1
- package/dist/16.js +0 -1
- package/dist/16.js.map +0 -1
- package/dist/209.js +0 -1
- package/dist/209.js.map +0 -1
- package/dist/317.js +0 -1
- package/dist/317.js.map +0 -1
- package/dist/349.js +0 -1
- package/dist/349.js.map +0 -1
- package/dist/358.js +0 -1
- package/dist/358.js.map +0 -1
- package/dist/371.js +0 -1
- package/dist/371.js.map +0 -1
- package/dist/378.js +0 -1
- package/dist/378.js.map +0 -1
- package/dist/442.js +0 -1
- package/dist/442.js.map +0 -1
- package/dist/45.js +0 -1
- package/dist/45.js.map +0 -1
- package/dist/466.js +0 -1
- package/dist/466.js.map +0 -1
- package/dist/496.js +0 -12
- package/dist/496.js.map +0 -1
- package/dist/508.js +0 -1
- package/dist/508.js.map +0 -1
- package/dist/61.js +0 -1
- package/dist/61.js.map +0 -1
- package/dist/640.js +0 -1
- package/dist/640.js.map +0 -1
- package/dist/689.js +0 -1
- package/dist/689.js.map +0 -1
- package/dist/709.js +0 -1
- package/dist/709.js.map +0 -1
- package/dist/712.js +0 -1
- package/dist/712.js.map +0 -1
- package/dist/720.js +0 -1
- package/dist/720.js.map +0 -1
- package/dist/742.js +0 -1
- package/dist/742.js.map +0 -1
- package/dist/771.js +0 -1
- package/dist/771.js.map +0 -1
- package/dist/806.js +0 -1
- package/dist/806.js.map +0 -1
- package/dist/839.js +0 -1
- package/dist/839.js.map +0 -1
- package/dist/912.js +0 -1
- package/dist/912.js.map +0 -1
- package/dist/913.js +0 -1
- package/dist/913.js.map +0 -1
- package/dist/940.js +0 -1
- package/dist/940.js.map +0 -1
- package/dist/965.js +0 -15
- package/dist/965.js.map +0 -1
- package/dist/digihmis-esm-billing-app.js +0 -6
- package/dist/digihmis-esm-billing-app.js.buildmanifest.json +0 -827
- package/dist/digihmis-esm-billing-app.js.map +0 -1
- package/dist/main.js +0 -17
- package/dist/main.js.map +0 -1
- package/dist/routes.json +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digihmis/esm-billing-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Billing microfrontend for the OpenMRS SPA",
|
|
5
5
|
"browser": "dist/digihmis-esm-billing-app.js",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"url": "https://github.com/its-kios09/DigiHMIS-apps/issues"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@digihmis/esm-commons-lib": "1.0.0",
|
|
40
41
|
"lodash-es": "^4.17.15",
|
|
41
42
|
"react-to-print": "2.14.13"
|
|
42
43
|
},
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { FilterableMultiSelect, InlineLoading, InlineNotification, Tag } from '@carbon/react';
|
|
2
2
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3
|
-
import { showSnackbar, useConfig
|
|
4
|
-
import React, {
|
|
3
|
+
import { showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
5
5
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { BillingConfig } from '../config-schema';
|
|
8
8
|
import styles from './billing-checkin-form.scss';
|
|
9
9
|
import VisitAttributesForm from './visit-attributes/visit-attributes-form.component';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
addBillLinesForVisit,
|
|
12
|
+
priceForMode,
|
|
13
|
+
useBillableItems,
|
|
14
|
+
usePatientType,
|
|
15
|
+
type BillableItem,
|
|
16
|
+
type SelectedService,
|
|
17
|
+
} from './billing-checkin-form.resource';
|
|
11
18
|
import { visitAttributesFormSchema, VisitAttributesFormValue } from './billing-checkin-form-schema';
|
|
12
19
|
import { hasPatientBeenExempted } from './helper';
|
|
13
|
-
import { EXEMPTED_PAYMENT_STATUS, PENDING_PAYMENT_STATUS } from '../constants';
|
|
14
20
|
import { useCurrencyFormatting } from '../utils';
|
|
15
21
|
|
|
16
22
|
type BillingCheckInFormProps = {
|
|
@@ -21,12 +27,12 @@ type BillingCheckInFormProps = {
|
|
|
21
27
|
const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setExtraVisitInfo }) => {
|
|
22
28
|
const { t } = useTranslation();
|
|
23
29
|
const {
|
|
24
|
-
visitAttributeTypes: { isPatientExempted },
|
|
30
|
+
visitAttributeTypes: { isPatientExempted, patientType: patientTypeAttrUuid },
|
|
25
31
|
} = useConfig<BillingConfig>();
|
|
26
|
-
const { cashPoints, isLoading: isLoadingCashPoints, error: cashError } = useCashPoint();
|
|
27
32
|
const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
|
|
33
|
+
const { patientType } = usePatientType(patientUuid);
|
|
28
34
|
const [attributes, setAttributes] = useState([]);
|
|
29
|
-
const [selectedServices, setSelectedServices] = useState([]);
|
|
35
|
+
const [selectedServices, setSelectedServices] = useState<Array<SelectedService>>([]);
|
|
30
36
|
const { formatSimple } = useCurrencyFormatting();
|
|
31
37
|
|
|
32
38
|
const formMethods = useForm<VisitAttributesFormValue>({
|
|
@@ -41,70 +47,78 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
41
47
|
resolver: zodResolver(visitAttributesFormSchema),
|
|
42
48
|
});
|
|
43
49
|
|
|
44
|
-
const isPatientExemptedValue = formMethods.watch('isPatientExempted');
|
|
45
50
|
const paymentMethod = formMethods.watch('paymentMethods');
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const errorMessage = JSON.stringify(error?.responseBody?.error?.message?.replace(/\[/g, '').replace(/\]/g, ''));
|
|
54
|
-
showSnackbar({
|
|
55
|
-
title: 'Patient Bill Error',
|
|
56
|
-
subtitle: `An error has occurred while creating patient bill, Contact system administrator quoting this error ${errorMessage}`,
|
|
57
|
-
kind: 'error',
|
|
58
|
-
isLowContrast: true,
|
|
59
|
-
});
|
|
60
|
-
},
|
|
61
|
-
);
|
|
62
|
-
}, []);
|
|
63
|
-
|
|
64
|
-
const handleBillingService = (selectedItems) => {
|
|
65
|
-
setSelectedServices(selectedItems);
|
|
66
|
-
|
|
67
|
-
const cashPointUuid = cashPoints?.[0]?.uuid ?? '';
|
|
68
|
-
const billStatus = hasPatientBeenExempted(attributes, isPatientExempted)
|
|
69
|
-
? EXEMPTED_PAYMENT_STATUS
|
|
70
|
-
: PENDING_PAYMENT_STATUS;
|
|
71
|
-
|
|
72
|
-
const lineItems = selectedItems.map((item, index) => {
|
|
73
|
-
const priceForPaymentMode =
|
|
74
|
-
item.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || item?.servicePrices[0];
|
|
75
|
-
return {
|
|
76
|
-
billableService: item?.uuid ?? '',
|
|
77
|
-
quantity: 1,
|
|
78
|
-
price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
|
|
79
|
-
priceName: 'Default',
|
|
80
|
-
priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
|
|
81
|
-
lineItemOrder: index,
|
|
82
|
-
paymentStatus: billStatus,
|
|
83
|
-
};
|
|
84
|
-
});
|
|
52
|
+
// Only offer services actually priced for the chosen payment mode. A service with no price for this
|
|
53
|
+
// mode is not billable under it (it must not quote another mode's price), so it is left out entirely.
|
|
54
|
+
const availableItems = useMemo(
|
|
55
|
+
() => lineItems.filter((item) => priceForMode(item, paymentMethod) != null),
|
|
56
|
+
[lineItems, paymentMethod],
|
|
57
|
+
);
|
|
85
58
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
payments: [],
|
|
92
|
-
};
|
|
59
|
+
// The price is per mode, so a service picked under one mode can't carry over to another. Clear the
|
|
60
|
+
// selection whenever the payment mode changes -- the clerk re-picks against the new mode's prices.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setSelectedServices([]);
|
|
63
|
+
}, [paymentMethod]);
|
|
93
64
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
attributes,
|
|
97
|
-
});
|
|
98
|
-
};
|
|
65
|
+
const formatPrice = (price: number | string | undefined) =>
|
|
66
|
+
formatSimple(parseFloat(String(price ?? 0)), { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
99
67
|
|
|
68
|
+
// Hand the visit form a callback it runs *after* the visit is saved: DigiHMIS bills are
|
|
69
|
+
// visit-scoped, so the selected services can only be charged once the visit uuid exists. An
|
|
70
|
+
// exempted patient (or no selection) is a no-op. The captured payment/insurance/exemption details
|
|
71
|
+
// still ride along as visit attributes.
|
|
100
72
|
useEffect(() => {
|
|
73
|
+
const exempt = hasPatientBeenExempted(attributes, isPatientExempted);
|
|
74
|
+
// New vs revisit is derived from the patient's visit history, not asked on the form.
|
|
75
|
+
const attributesWithType = patientTypeAttrUuid
|
|
76
|
+
? [...attributes, { attributeType: patientTypeAttrUuid, value: patientType }]
|
|
77
|
+
: attributes;
|
|
101
78
|
setExtraVisitInfo({
|
|
102
|
-
handleCreateExtraVisitInfo: () => {
|
|
103
|
-
|
|
79
|
+
handleCreateExtraVisitInfo: (visitUuid?: string) => {
|
|
80
|
+
if (exempt || !visitUuid || selectedServices.length === 0) {
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
return addBillLinesForVisit(visitUuid, selectedServices, paymentMethod).then(
|
|
84
|
+
() =>
|
|
85
|
+
showSnackbar({
|
|
86
|
+
title: t('patientBill', 'Patient bill'),
|
|
87
|
+
subtitle: t('patientBilledSuccessfully', 'Patient has been billed successfully'),
|
|
88
|
+
kind: 'success',
|
|
89
|
+
}),
|
|
90
|
+
(error) =>
|
|
91
|
+
showSnackbar({
|
|
92
|
+
title: t('patientBillError', 'Patient bill error'),
|
|
93
|
+
subtitle:
|
|
94
|
+
error?.responseBody?.error?.message ??
|
|
95
|
+
error?.message ??
|
|
96
|
+
t('billErrorGeneric', 'An error occurred while billing the patient'),
|
|
97
|
+
kind: 'error',
|
|
98
|
+
isLowContrast: true,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
attributes: attributesWithType,
|
|
104
103
|
});
|
|
105
|
-
}, [
|
|
104
|
+
}, [
|
|
105
|
+
attributes,
|
|
106
|
+
isPatientExempted,
|
|
107
|
+
patientType,
|
|
108
|
+
patientTypeAttrUuid,
|
|
109
|
+
paymentMethod,
|
|
110
|
+
selectedServices,
|
|
111
|
+
setExtraVisitInfo,
|
|
112
|
+
t,
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const handleBillingService = (selectedItems: Array<BillableItem>) => {
|
|
116
|
+
setSelectedServices(
|
|
117
|
+
selectedItems.map((item) => ({ uuid: item.uuid, name: item.name, price: item.price, prices: item.prices })),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
106
120
|
|
|
107
|
-
if (isLoadingLineItems
|
|
121
|
+
if (isLoadingLineItems) {
|
|
108
122
|
return (
|
|
109
123
|
<InlineLoading
|
|
110
124
|
status="active"
|
|
@@ -114,7 +128,7 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
114
128
|
);
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
if (
|
|
131
|
+
if (lineError) {
|
|
118
132
|
return (
|
|
119
133
|
<InlineNotification
|
|
120
134
|
kind="error"
|
|
@@ -131,34 +145,19 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
131
145
|
{paymentMethod && (
|
|
132
146
|
<section className={styles.sectionContainer}>
|
|
133
147
|
<div className={styles.sectionField}>
|
|
134
|
-
{
|
|
148
|
+
{availableItems && availableItems.length > 0 ? (
|
|
135
149
|
<>
|
|
136
|
-
{selectedServices.map((item, index) =>
|
|
137
|
-
|
|
138
|
-
item.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<Tag key={index} type="cyan" className={styles.selectedServicesTag}>
|
|
142
|
-
{item.name} - {formatSimple(price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
143
|
-
</Tag>
|
|
144
|
-
);
|
|
145
|
-
})}
|
|
150
|
+
{selectedServices.map((item, index) => (
|
|
151
|
+
<Tag key={index} type="cyan" className={styles.selectedServicesTag}>
|
|
152
|
+
{item.name} - {formatPrice(priceForMode(item, paymentMethod))}
|
|
153
|
+
</Tag>
|
|
154
|
+
))}
|
|
146
155
|
<FilterableMultiSelect
|
|
147
|
-
key={isPatientExemptedValue}
|
|
148
156
|
id="billing-service"
|
|
149
157
|
titleText={t('searchServices', 'Search services')}
|
|
150
|
-
items={
|
|
151
|
-
itemToString={(item) => {
|
|
152
|
-
|
|
153
|
-
return '';
|
|
154
|
-
}
|
|
155
|
-
const priceForPaymentMode =
|
|
156
|
-
item.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || item?.servicePrices[0];
|
|
157
|
-
const price = priceForPaymentMode ? parseFloat(priceForPaymentMode.price) : 0;
|
|
158
|
-
return `${item.name} - ${formatSimple(price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
159
|
-
}}
|
|
160
|
-
onChange={({ selectedItems }) => handleBillingService(selectedItems)}
|
|
161
|
-
disabled={isPatientExemptedValue === ''}
|
|
158
|
+
items={availableItems}
|
|
159
|
+
itemToString={(item) => (item ? `${item.name} - ${formatPrice(priceForMode(item, paymentMethod))}` : '')}
|
|
160
|
+
onChange={({ selectedItems }) => handleBillingService(selectedItems as Array<BillableItem>)}
|
|
162
161
|
/>
|
|
163
162
|
</>
|
|
164
163
|
) : (
|
|
@@ -166,7 +165,10 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
166
165
|
kind="info"
|
|
167
166
|
lowContrast
|
|
168
167
|
title={t('noBillableServices', 'No billable services')}
|
|
169
|
-
subtitle={t(
|
|
168
|
+
subtitle={t(
|
|
169
|
+
'noServicesPricedForMode',
|
|
170
|
+
'No services are priced for the selected payment method. Configure a price for this payment method to bill it here.',
|
|
171
|
+
)}
|
|
170
172
|
/>
|
|
171
173
|
)}
|
|
172
174
|
</div>
|
|
@@ -1,42 +1,107 @@
|
|
|
1
|
-
import { openmrsFetch,
|
|
1
|
+
import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import useSWR from 'swr';
|
|
4
|
-
import { BillingConfig } from '../config-schema';
|
|
4
|
+
import { type BillingConfig } from '../config-schema';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export interface BillableItemPrice {
|
|
7
|
+
paymentMode: string;
|
|
8
|
+
price: number | string;
|
|
9
|
+
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export interface BillableItem {
|
|
12
|
+
uuid: string;
|
|
13
|
+
name: string;
|
|
14
|
+
price: number | string;
|
|
15
|
+
prices?: Array<BillableItemPrice>;
|
|
16
|
+
concept?: { uuid: string; display: string };
|
|
17
|
+
serviceType?: { uuid: string; display: string };
|
|
18
|
+
description?: string;
|
|
19
|
+
retired?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SelectedService {
|
|
23
|
+
uuid?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
price: number | string;
|
|
26
|
+
prices?: Array<BillableItemPrice>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The price to charge for a payment mode.
|
|
31
|
+
*
|
|
32
|
+
* Per-mode pricing is strict: once a service has any per-mode prices, it is only offered at the price
|
|
33
|
+
* set for the *selected* mode. A mode with no price of its own is not priced for that service -- it
|
|
34
|
+
* must never borrow another mode's price (that is what made a cash-only service quote the cash price
|
|
35
|
+
* under Mobile Money). Returns undefined in that case so the caller can hide/skip it. Only a service
|
|
36
|
+
* with no per-mode prices at all falls back to its single flat price.
|
|
37
|
+
*/
|
|
38
|
+
export const priceForMode = (
|
|
39
|
+
item: { price: number | string; prices?: Array<BillableItemPrice> },
|
|
40
|
+
mode?: string,
|
|
41
|
+
): number | string | undefined => {
|
|
42
|
+
const overrides = item.prices ?? [];
|
|
43
|
+
if (overrides.length > 0) {
|
|
44
|
+
const match = mode ? overrides.find((entry) => entry.paymentMode === mode) : undefined;
|
|
45
|
+
return match?.price;
|
|
46
|
+
}
|
|
47
|
+
return item.price;
|
|
15
48
|
};
|
|
16
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The facility's billable items (concept + price) from the DigiHMIS billing module.
|
|
52
|
+
*/
|
|
17
53
|
export const useBillableItems = () => {
|
|
18
54
|
const { registrationServiceType } = useConfig<BillingConfig>();
|
|
19
|
-
const url = `${restBaseUrl}/
|
|
20
|
-
const { data, isLoading, error } = useSWR<{ data: { results: Array<
|
|
55
|
+
const url = `${restBaseUrl}/digihmisbilling/billableitem`;
|
|
56
|
+
const { data, isLoading, error } = useSWR<{ data: { results: Array<BillableItem> } }>(url, openmrsFetch);
|
|
21
57
|
const [searchTerm, setSearchTerm] = useState('');
|
|
22
58
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
const lineItems = useMemo(() => {
|
|
60
|
+
let items = (data?.data?.results ?? []).filter((item) => !item.retired);
|
|
61
|
+
// Check-in only offers the registration service(s) -- items in the configured registration
|
|
62
|
+
// service type. If none is configured, fall back to showing all so nothing silently disappears.
|
|
63
|
+
if (registrationServiceType) {
|
|
64
|
+
items = items.filter((item) => item.serviceType?.uuid === registrationServiceType);
|
|
26
65
|
}
|
|
66
|
+
const query = searchTerm.trim().toLowerCase();
|
|
67
|
+
return query ? items.filter((item) => item.name?.toLowerCase().includes(query)) : items;
|
|
68
|
+
}, [data?.data?.results, searchTerm, registrationServiceType]);
|
|
27
69
|
|
|
28
|
-
|
|
70
|
+
return { lineItems, isLoading, error, searchTerm, setSearchTerm };
|
|
71
|
+
};
|
|
29
72
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
73
|
+
/**
|
|
74
|
+
* New vs revisit is derived, never asked -- a manual choice only confuses the clerk. Checked at
|
|
75
|
+
* check-in before the new visit is saved: a patient with any existing visit is a REVISIT, otherwise
|
|
76
|
+
* NEW. The value is written to the patient-type visit attribute alongside the payment details.
|
|
77
|
+
*/
|
|
78
|
+
export const usePatientType = (patientUuid: string) => {
|
|
79
|
+
const url = patientUuid
|
|
80
|
+
? `${restBaseUrl}/visit?patient=${patientUuid}&includeInactive=true&limit=1&v=custom:(uuid)`
|
|
81
|
+
: null;
|
|
82
|
+
const { data, isLoading } = useSWR<{ data: { results: Array<{ uuid: string }> } }>(url, openmrsFetch);
|
|
83
|
+
const patientType = (data?.data?.results?.length ?? 0) > 0 ? 'REVISIT' : 'NEW';
|
|
84
|
+
return { patientType, isLoading };
|
|
85
|
+
};
|
|
34
86
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Charge the selected services to the visit's bill. DigiHMIS bills are visit-scoped, so this runs
|
|
89
|
+
* once the visit has been saved and its uuid is known; each service becomes a bill line.
|
|
90
|
+
*/
|
|
91
|
+
export const addBillLinesForVisit = (
|
|
92
|
+
visitUuid: string,
|
|
93
|
+
services: Array<SelectedService>,
|
|
94
|
+
paymentMode?: string,
|
|
95
|
+
) => {
|
|
96
|
+
return Promise.all(
|
|
97
|
+
services.map((service) => {
|
|
98
|
+
const params = new URLSearchParams({
|
|
99
|
+
visit: visitUuid,
|
|
100
|
+
description: service.name ?? '',
|
|
101
|
+
unitPrice: String(priceForMode(service, paymentMode) ?? '0'),
|
|
102
|
+
quantity: '1',
|
|
103
|
+
});
|
|
104
|
+
return openmrsFetch(`${restBaseUrl}/digihmisbilling/bill/line?${params.toString()}`, { method: 'POST' });
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
42
107
|
};
|
|
@@ -1,23 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
2
2
|
import { BillingConfig } from '../../config-schema';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
export interface PaymentModeOption {
|
|
5
|
+
uuid: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DigiHMIS models the payment mode as a fixed enum
|
|
12
|
+
* (org.openmrs.module.digihmisbilling.model.PaymentMode), not a REST-listed entity, so the check-in
|
|
13
|
+
* payment-method options come from that closed set. The `uuid` carries the enum name, which is what
|
|
14
|
+
* the visit attribute stores and what the payment endpoint expects.
|
|
15
|
+
*/
|
|
16
|
+
const PAYMENT_MODES: Array<PaymentModeOption> = [
|
|
17
|
+
{ uuid: 'CASH', name: 'Cash', description: 'Physical cash at the till' },
|
|
18
|
+
{ uuid: 'MOBILE_MONEY', name: 'Mobile money', description: 'M-Pesa and the like' },
|
|
19
|
+
{ uuid: 'CARD', name: 'Card', description: 'Card payment' },
|
|
20
|
+
{ uuid: 'INSURANCE', name: 'Insurance', description: 'Settled by an insurer or scheme' },
|
|
21
|
+
{ uuid: 'WAIVER', name: 'Waiver', description: 'Written off (waiver / exemption)' },
|
|
22
|
+
];
|
|
5
23
|
|
|
6
24
|
export const usePaymentModes = (excludeWaiver: boolean = true) => {
|
|
7
25
|
const { excludedPaymentMode } = useConfig<BillingConfig>();
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
});
|
|
12
|
-
const allowedPaymentModes =
|
|
13
|
-
excludedPaymentMode?.length > 0
|
|
14
|
-
? (data?.data?.results.filter((mode) => !excludedPaymentMode.some((excluded) => excluded.uuid === mode.uuid)) ??
|
|
15
|
-
[])
|
|
16
|
-
: (data?.data?.results ?? []);
|
|
26
|
+
const excluded = new Set((excludedPaymentMode ?? []).map((mode) => mode.uuid));
|
|
27
|
+
const allowedPaymentModes = PAYMENT_MODES.filter((mode) => !excluded.has(mode.uuid));
|
|
28
|
+
|
|
17
29
|
return {
|
|
18
|
-
paymentModes: excludeWaiver ? allowedPaymentModes :
|
|
19
|
-
isLoading,
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
paymentModes: excludeWaiver ? allowedPaymentModes.filter((mode) => mode.uuid !== 'WAIVER') : allowedPaymentModes,
|
|
31
|
+
isLoading: false,
|
|
32
|
+
error: undefined,
|
|
33
|
+
mutate: () => {},
|
|
22
34
|
};
|
|
23
35
|
};
|
|
@@ -6,15 +6,24 @@ import { CashPoint, Timesheet } from './type';
|
|
|
6
6
|
* Hook to get the current cashier's active timesheet (clocked-in session)
|
|
7
7
|
*/
|
|
8
8
|
export const useCurrentTimesheet = () => {
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<Timesheet> } }>(
|
|
13
|
-
providerUuid ? `${restBaseUrl}/cashier/timesheet?cashier=${providerUuid}&getProviderOpenTimesheet=true` : null,
|
|
9
|
+
// The DigiHMIS shift is scoped to the logged-in user server-side; /shift/active returns it or {}.
|
|
10
|
+
const { data, error, isLoading, mutate } = useSWR<{ data: any }>(
|
|
11
|
+
`${restBaseUrl}/digihmisbilling/shift/active`,
|
|
14
12
|
openmrsFetch,
|
|
15
13
|
);
|
|
16
14
|
|
|
17
|
-
const
|
|
15
|
+
const shift = data?.data && data.data.uuid ? data.data : null;
|
|
16
|
+
const activeTimesheet = shift
|
|
17
|
+
? {
|
|
18
|
+
uuid: shift.uuid,
|
|
19
|
+
cashPoint: { uuid: shift.paymentPoint?.uuid, name: shift.paymentPoint?.display },
|
|
20
|
+
cashier: shift.cashier,
|
|
21
|
+
clockIn: shift.clockIn,
|
|
22
|
+
clockOut: shift.clockOut ?? null,
|
|
23
|
+
totalCollected: shift.totalCollected,
|
|
24
|
+
collectedByMode: shift.collectedByMode,
|
|
25
|
+
}
|
|
26
|
+
: null;
|
|
18
27
|
const isClockedIn = activeTimesheet !== null && activeTimesheet.clockOut === null;
|
|
19
28
|
|
|
20
29
|
return {
|
|
@@ -69,7 +78,7 @@ export const useTimesheets = (
|
|
|
69
78
|
* Hook to fetch all available cash points
|
|
70
79
|
*/
|
|
71
80
|
export const useCashPoints = () => {
|
|
72
|
-
const url = `${restBaseUrl}/
|
|
81
|
+
const url = `${restBaseUrl}/digihmisbilling/paymentpoint`;
|
|
73
82
|
|
|
74
83
|
const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<CashPoint> } }>(url, openmrsFetch);
|
|
75
84
|
|
|
@@ -1,17 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface IconWrapperProps {
|
|
4
|
-
src: string;
|
|
5
|
-
alt?: string;
|
|
6
|
-
className?: string;
|
|
7
|
-
style?: React.CSSProperties;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const IconWrapper: React.FC<IconWrapperProps> = ({
|
|
11
|
-
src,
|
|
12
|
-
alt = 'Icon',
|
|
13
|
-
className,
|
|
14
|
-
style = { width: '100px', height: '148px' },
|
|
15
|
-
}) => {
|
|
16
|
-
return <img src={src} alt={alt} className={className} style={style} />;
|
|
17
|
-
};
|
|
1
|
+
// Moved to the shared library so every app uses one implementation.
|
|
2
|
+
export { IconWrapper, type IconWrapperProps } from '@digihmis/esm-commons-lib';
|
|
@@ -59,7 +59,7 @@ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
|
|
|
59
59
|
setIsSubmitting(true);
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
|
-
const response = await clockIn(
|
|
62
|
+
const response = await clockIn(selectedCashPoint);
|
|
63
63
|
await mutateTimesheet();
|
|
64
64
|
|
|
65
65
|
showSnackbar({
|
|
@@ -114,7 +114,7 @@ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
|
|
|
114
114
|
await clockOut(activeTimesheet.uuid);
|
|
115
115
|
|
|
116
116
|
// Clock in to new cash point
|
|
117
|
-
const response = await clockIn(
|
|
117
|
+
const response = await clockIn(selectedCashPoint);
|
|
118
118
|
await mutateTimesheet();
|
|
119
119
|
|
|
120
120
|
showSnackbar({
|
|
@@ -181,6 +181,8 @@ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
|
|
|
181
181
|
{t('currentlyActiveAt', 'Currently active at {{location}}', {
|
|
182
182
|
location: activeTimesheet.cashPoint?.name || t('unknown', 'Unknown'),
|
|
183
183
|
})}
|
|
184
|
+
{' · '}
|
|
185
|
+
{t('collectedThisShift', 'Collected: {{amount}}', { amount: activeTimesheet.totalCollected ?? 0 })}
|
|
184
186
|
</span>
|
|
185
187
|
)}
|
|
186
188
|
</div>
|
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Open a cashier shift at a payment point. The cashier is the logged-in user, resolved server-side,
|
|
5
|
+
* so only the payment point is passed.
|
|
5
6
|
*/
|
|
6
|
-
export const clockIn = async (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return openmrsFetch(url, {
|
|
7
|
+
export const clockIn = async (paymentPointUuid: string) => {
|
|
8
|
+
return openmrsFetch(`${restBaseUrl}/digihmisbilling/shift/clockin?paymentPoint=${paymentPointUuid}`, {
|
|
10
9
|
method: 'POST',
|
|
11
|
-
headers: {
|
|
12
|
-
'Content-Type': 'application/json',
|
|
13
|
-
},
|
|
14
|
-
body: {
|
|
15
|
-
cashier: cashierUuid,
|
|
16
|
-
cashPoint: cashPointUuid,
|
|
17
|
-
clockIn: new Date().toISOString(),
|
|
18
|
-
},
|
|
19
10
|
});
|
|
20
11
|
};
|
|
21
12
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const clockOut = async (timesheetUuid: string) => {
|
|
26
|
-
const url = `${restBaseUrl}/cashier/timesheet/${timesheetUuid}`;
|
|
27
|
-
|
|
28
|
-
return openmrsFetch(url, {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
},
|
|
33
|
-
body: {
|
|
34
|
-
clockOut: new Date().toISOString(),
|
|
35
|
-
},
|
|
36
|
-
});
|
|
13
|
+
/** Close an open shift. */
|
|
14
|
+
export const clockOut = async (shiftUuid: string) => {
|
|
15
|
+
return openmrsFetch(`${restBaseUrl}/digihmisbilling/shift/${shiftUuid}/clockout`, { method: 'POST' });
|
|
37
16
|
};
|
package/src/config-schema.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface BillingConfig {
|
|
|
8
8
|
exemptionCategory: string;
|
|
9
9
|
billPaymentStatus: string;
|
|
10
10
|
shaBenefitPackagesAndInterventions: string;
|
|
11
|
+
patientType: string;
|
|
11
12
|
};
|
|
12
13
|
insurancePaymentMethod: string;
|
|
13
14
|
patientExemptionCategories: Array<{ value: string; label: string }>;
|
|
@@ -62,11 +63,16 @@ export const configSchema: ConfigSchema = {
|
|
|
62
63
|
_description: 'The bill payment status visit attribute uuid',
|
|
63
64
|
_default: '919b51c9-8e2e-468f-8354-181bf3e55786',
|
|
64
65
|
},
|
|
66
|
+
patientType: {
|
|
67
|
+
_type: Type.String,
|
|
68
|
+
_description: 'The patient type (new vs revisit) visit attribute uuid',
|
|
69
|
+
_default: '6b9e1f7a-2c3d-4e5f-8a1b-9c0d1e2f3a4b',
|
|
70
|
+
},
|
|
65
71
|
},
|
|
66
72
|
insurancePaymentMethod: {
|
|
67
73
|
_type: Type.String,
|
|
68
|
-
_description: '
|
|
69
|
-
_default: '
|
|
74
|
+
_description: 'The payment-method value that reveals the insurance fields (DigiHMIS PaymentMode enum)',
|
|
75
|
+
_default: 'INSURANCE',
|
|
70
76
|
},
|
|
71
77
|
patientExemptionCategories: {
|
|
72
78
|
_type: Type.Array,
|