@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.
@@ -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": [
@@ -6,6 +6,7 @@
6
6
  "addBill": "የሂሳብ ዕቃ(ዎች) ይጨምሩ",
7
7
  "addCommodityChargeItem": "የክፍያ ዕቃ ይጨምሩ",
8
8
  "addDeposit": "ተቀማጭ ገንዘብ ይጨምሩ",
9
+ "additionalPayment": "ተቀማጭ ክፍያ",
9
10
  "addPaymentOptions": "የክፍያ አማራጭ ይጨምሩ",
10
11
  "addSchema": "ስኬማ ይጨምሩ",
11
12
  "addServiceChargeItem": "የክፍያ አገልግሎት ይጨምሩ",
@@ -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",
@@ -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",