@kenyaemr/esm-billing-app 5.4.2-pre.2301 → 5.4.2-pre.2305
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/.turbo/turbo-build.log +10 -10
- package/dist/197.js +1 -1
- package/dist/294.js +1 -1
- package/dist/300.js +1 -1
- package/dist/985.js +1 -0
- package/dist/985.js.map +1 -0
- package/dist/kenyaemr-esm-billing-app.js +1 -1
- package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +36 -36
- package/dist/kenyaemr-esm-billing-app.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/bill-deposit/utils/bill-deposit.utils.ts +3 -5
- package/src/billable-services/bill-manager/workspaces/edit-bill/edit-bill-form.workspace.tsx +2 -4
- package/src/billing.resource.ts +5 -0
- package/src/config-schema.ts +13 -0
- package/src/helpers/README.md +174 -0
- package/src/helpers/currency.test.ts +114 -0
- package/src/helpers/currency.ts +138 -0
- package/src/helpers/functions.ts +2 -13
- package/src/index.ts +2 -0
- package/src/invoice/invoice.component.tsx +31 -16
- package/src/invoice/payments/payment-form/payment.scss +33 -0
- package/src/invoice/payments/payment-form/payment.workspace.tsx +195 -0
- package/src/invoice/payments/payment-form/use-payment-form.ts +77 -0
- package/src/routes.json +6 -0
- package/translations/am.json +1 -0
- package/translations/en.json +1 -0
- package/translations/sw.json +1 -0
- package/dist/115.js +0 -1
- package/dist/115.js.map +0 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.form {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
height: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.formContainer {
|
|
13
|
+
margin: layout.$spacing-05;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.tablet {
|
|
17
|
+
padding: layout.$spacing-06 layout.$spacing-05;
|
|
18
|
+
background-color: colors.$white;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.desktop {
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.paymentMethods {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
row-gap: layout.$spacing-05;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.formStackControl {
|
|
32
|
+
row-gap: layout.$layout-01;
|
|
33
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { MappedBill } from '../../../types';
|
|
4
|
+
import styles from './payment.scss';
|
|
5
|
+
import { Stack, TextInput, Button, ButtonSet, InlineLoading, Dropdown } from '@carbon/react';
|
|
6
|
+
import {
|
|
7
|
+
DefaultWorkspaceProps,
|
|
8
|
+
ResponsiveWrapper,
|
|
9
|
+
showNotification,
|
|
10
|
+
showSnackbar,
|
|
11
|
+
useLayoutType,
|
|
12
|
+
} from '@openmrs/esm-framework';
|
|
13
|
+
import classNames from 'classnames';
|
|
14
|
+
import { Controller } from 'react-hook-form';
|
|
15
|
+
import { addPaymentToBill, usePaymentModes } from '../../../billing.resource';
|
|
16
|
+
import { usePaymentForm } from './use-payment-form';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { mutate } from 'swr';
|
|
19
|
+
|
|
20
|
+
type PaymentWorkspaceProps = DefaultWorkspaceProps & {
|
|
21
|
+
bill: MappedBill;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PaymentWorkspace: React.FC<PaymentWorkspaceProps> = ({
|
|
25
|
+
bill,
|
|
26
|
+
closeWorkspace,
|
|
27
|
+
promptBeforeClosing,
|
|
28
|
+
closeWorkspaceWithSavedChanges,
|
|
29
|
+
}) => {
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
32
|
+
const { formMethods, paymentSchema } = usePaymentForm(t, bill.balance);
|
|
33
|
+
|
|
34
|
+
type PaymentFormData = z.infer<typeof paymentSchema>;
|
|
35
|
+
|
|
36
|
+
const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes();
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
formState: { isSubmitting, errors },
|
|
40
|
+
control,
|
|
41
|
+
handleSubmit,
|
|
42
|
+
} = formMethods;
|
|
43
|
+
|
|
44
|
+
const onSubmit = async (data: PaymentFormData) => {
|
|
45
|
+
const payment = {
|
|
46
|
+
instanceType: data.instanceType?.uuid,
|
|
47
|
+
amount: data.amountTendered,
|
|
48
|
+
amountTendered: data.amountTendered,
|
|
49
|
+
attributes: data.attributes
|
|
50
|
+
? Object.entries(data.attributes).map(([uuid, value]) => ({
|
|
51
|
+
attributeType: {
|
|
52
|
+
uuid,
|
|
53
|
+
},
|
|
54
|
+
value,
|
|
55
|
+
}))
|
|
56
|
+
: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await addPaymentToBill(bill.uuid, payment);
|
|
61
|
+
if (response.ok) {
|
|
62
|
+
showSnackbar({
|
|
63
|
+
title: t('paymentSaved', 'Payment saved'),
|
|
64
|
+
kind: 'success',
|
|
65
|
+
subtitle: t('paymentSavedSuccessfully', 'Payment saved successfully'),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const url = `/ws/rest/v1/cashier/bill/${bill.uuid}`;
|
|
69
|
+
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
|
|
70
|
+
closeWorkspaceWithSavedChanges();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
showSnackbar({
|
|
73
|
+
title: t('errorSavingPayment', 'Error saving payment'),
|
|
74
|
+
kind: 'error',
|
|
75
|
+
subtitle: error.message,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleError = (error: any) => {
|
|
81
|
+
showSnackbar({
|
|
82
|
+
title: t('errorSavingPayment', 'Error generating payment'),
|
|
83
|
+
kind: 'error',
|
|
84
|
+
subtitle: JSON.stringify(error, null, 2),
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
promptBeforeClosing(() => formMethods.formState.isDirty);
|
|
90
|
+
}, [formMethods.formState.isDirty, promptBeforeClosing]);
|
|
91
|
+
|
|
92
|
+
if (isLoadingPaymentModes) {
|
|
93
|
+
return <InlineLoading status="active" iconDescription="Loading payment modes" />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const attributeTypes = (formMethods.watch('instanceType')?.attributeTypes as Array<Record<string, string>>) || [];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<form onSubmit={handleSubmit(onSubmit, handleError)} className={styles.form}>
|
|
100
|
+
<div className={styles.formContainer}>
|
|
101
|
+
<Stack className={styles.formStackControl} gap={7}>
|
|
102
|
+
<ResponsiveWrapper>
|
|
103
|
+
<Stack gap={4}>
|
|
104
|
+
<Controller
|
|
105
|
+
name="instanceType"
|
|
106
|
+
control={control}
|
|
107
|
+
render={({ field }) => (
|
|
108
|
+
<Dropdown
|
|
109
|
+
{...field}
|
|
110
|
+
id="instanceType"
|
|
111
|
+
titleText={t('instanceType', 'Instance Type')}
|
|
112
|
+
label={t('selectInstanceType', 'Select instance type')}
|
|
113
|
+
items={paymentModes}
|
|
114
|
+
onChange={({ selectedItem }) => field.onChange(selectedItem)}
|
|
115
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
116
|
+
invalid={!!errors.instanceType}
|
|
117
|
+
invalidText={errors.instanceType?.message}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
/>
|
|
121
|
+
</Stack>
|
|
122
|
+
</ResponsiveWrapper>
|
|
123
|
+
<ResponsiveWrapper>
|
|
124
|
+
<Controller
|
|
125
|
+
name="amountTendered"
|
|
126
|
+
control={control}
|
|
127
|
+
render={({ field }) => (
|
|
128
|
+
<TextInput
|
|
129
|
+
{...field}
|
|
130
|
+
id="amountTendered"
|
|
131
|
+
labelText={t('amountTendered', 'Amount Tendered')}
|
|
132
|
+
placeholder={t('enterAmountTendered', 'Enter amount tendered, max is {{max}}', {
|
|
133
|
+
max: bill.balance,
|
|
134
|
+
})}
|
|
135
|
+
type="number"
|
|
136
|
+
step="0.01"
|
|
137
|
+
max={bill.balance}
|
|
138
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
139
|
+
invalid={!!errors.amountTendered}
|
|
140
|
+
invalidText={errors.amountTendered?.message}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
</ResponsiveWrapper>
|
|
145
|
+
<ResponsiveWrapper>
|
|
146
|
+
{attributeTypes.map((attributeType) => (
|
|
147
|
+
<Controller
|
|
148
|
+
key={attributeType.uuid}
|
|
149
|
+
name={`attributes.${attributeType.uuid}`}
|
|
150
|
+
control={control}
|
|
151
|
+
render={({ field }) => (
|
|
152
|
+
<TextInput
|
|
153
|
+
{...field}
|
|
154
|
+
id={attributeType.uuid}
|
|
155
|
+
labelText={`${attributeType.name || 'Attribute'}${
|
|
156
|
+
attributeType.required ? t('required', ' (Required)') : ''
|
|
157
|
+
}`}
|
|
158
|
+
placeholder={attributeType.description || 'Enter value'}
|
|
159
|
+
invalid={!!errors.attributes?.[attributeType.uuid] || (attributeType.required && !field.value)}
|
|
160
|
+
invalidText={
|
|
161
|
+
errors.attributes?.[attributeType.uuid]?.message ||
|
|
162
|
+
(attributeType.required && !field.value
|
|
163
|
+
? t('attributeValueRequired', 'Attribute value is required')
|
|
164
|
+
: '')
|
|
165
|
+
}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
/>
|
|
169
|
+
))}
|
|
170
|
+
</ResponsiveWrapper>
|
|
171
|
+
</Stack>
|
|
172
|
+
</div>
|
|
173
|
+
<ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
|
|
174
|
+
<Button style={{ maxWidth: '50%' }} kind="secondary" onClick={() => closeWorkspace()}>
|
|
175
|
+
{t('cancel', 'Cancel')}
|
|
176
|
+
</Button>
|
|
177
|
+
<Button
|
|
178
|
+
disabled={isSubmitting || Object.keys(errors).length > 0}
|
|
179
|
+
style={{ maxWidth: '50%' }}
|
|
180
|
+
kind="primary"
|
|
181
|
+
type="submit">
|
|
182
|
+
{isSubmitting ? (
|
|
183
|
+
<span style={{ display: 'flex', justifyItems: 'center' }}>
|
|
184
|
+
{t('submitting', 'Submitting...')} <InlineLoading status="active" iconDescription="Loading" />
|
|
185
|
+
</span>
|
|
186
|
+
) : (
|
|
187
|
+
t('saveAndClose', 'Save & close')
|
|
188
|
+
)}
|
|
189
|
+
</Button>
|
|
190
|
+
</ButtonSet>
|
|
191
|
+
</form>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export default PaymentWorkspace;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useForm } from 'react-hook-form';
|
|
2
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export const createPaymentSchema = (t: (key: string, defaultValue?: string) => string, billBalance: number) =>
|
|
6
|
+
z
|
|
7
|
+
.object({
|
|
8
|
+
instanceType: z
|
|
9
|
+
.object({
|
|
10
|
+
uuid: z.string().min(1, t('instanceTypeUuidRequired', 'Instance type UUID is required')),
|
|
11
|
+
name: z.string().min(1, t('instanceTypeNameRequired', 'Instance type name is required')),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
retired: z.boolean().optional(),
|
|
14
|
+
retireReason: z.string().nullable().optional(),
|
|
15
|
+
attributeTypes: z
|
|
16
|
+
.array(
|
|
17
|
+
z.object({
|
|
18
|
+
uuid: z.string().optional(),
|
|
19
|
+
name: z.string().optional(),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
required: z.boolean().optional(),
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
.optional(),
|
|
25
|
+
sortOrder: z.number().nullable().optional(),
|
|
26
|
+
resourceVersion: z.string().optional(),
|
|
27
|
+
})
|
|
28
|
+
.optional(),
|
|
29
|
+
amountTendered: z
|
|
30
|
+
.number()
|
|
31
|
+
.positive(t('amountTenderedPositive', 'Amount tendered must be positive'))
|
|
32
|
+
.max(billBalance, t('amountTenderedExceedsBalance', 'Amount tendered cannot exceed bill balance')),
|
|
33
|
+
attributes: z.record(z.string(), z.string()).optional(),
|
|
34
|
+
})
|
|
35
|
+
.refine(
|
|
36
|
+
(data) => {
|
|
37
|
+
// Check if all required attributes have values
|
|
38
|
+
if (!data.instanceType?.attributeTypes) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const requiredAttributeTypes = data.instanceType.attributeTypes.filter((attr) => attr.required && attr.uuid);
|
|
43
|
+
const providedAttributes = data.attributes || {};
|
|
44
|
+
|
|
45
|
+
for (const requiredAttr of requiredAttributeTypes) {
|
|
46
|
+
if (!providedAttributes[requiredAttr.uuid] || providedAttributes[requiredAttr.uuid].trim() === '') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
message: t('requiredAttributesMissing', 'Required attributes are missing'),
|
|
55
|
+
path: ['attributes'],
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export const usePaymentForm = (t: (key: string, defaultValue?: string) => string, billBalance: number) => {
|
|
60
|
+
const paymentSchema = createPaymentSchema(t, billBalance);
|
|
61
|
+
|
|
62
|
+
type PaymentFormData = z.infer<typeof paymentSchema>;
|
|
63
|
+
|
|
64
|
+
const formMethods = useForm<PaymentFormData>({
|
|
65
|
+
resolver: zodResolver(paymentSchema),
|
|
66
|
+
defaultValues: {
|
|
67
|
+
instanceType: undefined,
|
|
68
|
+
amountTendered: undefined,
|
|
69
|
+
attributes: {},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
formMethods,
|
|
75
|
+
paymentSchema,
|
|
76
|
+
};
|
|
77
|
+
};
|
package/src/routes.json
CHANGED
|
@@ -279,6 +279,12 @@
|
|
|
279
279
|
"component": "depositTransactionWorkspace",
|
|
280
280
|
"title": "Deposit Transaction",
|
|
281
281
|
"type": "other-form"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "payment-workspace",
|
|
285
|
+
"component": "paymentWorkspace",
|
|
286
|
+
"title": "Payment Workspace",
|
|
287
|
+
"type": "other-form"
|
|
282
288
|
}
|
|
283
289
|
],
|
|
284
290
|
"modals": [
|
package/translations/am.json
CHANGED
package/translations/en.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"addBill": "Add bill item(s)",
|
|
7
7
|
"addCommodityChargeItem": "Add charge item",
|
|
8
8
|
"addDeposit": "Add deposit",
|
|
9
|
+
"additionalPayment": "Additional Payment",
|
|
9
10
|
"addPaymentOptions": "Add payment option",
|
|
10
11
|
"addSchema": "Add Schema",
|
|
11
12
|
"addServiceChargeItem": "Add charge service",
|
package/translations/sw.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"addBill": "Add bill item(s)",
|
|
7
7
|
"addCommodityChargeItem": "Add charge item",
|
|
8
8
|
"addDeposit": "Add Deposit",
|
|
9
|
+
"additionalPayment": "Additional Payment",
|
|
9
10
|
"addPaymentOptions": "Add payment option",
|
|
10
11
|
"addSchema": "Add Schema",
|
|
11
12
|
"addServiceChargeItem": "Add charge service",
|