@kenyaemr/esm-billing-app 5.4.2-pre.2548 → 5.4.2-pre.2553
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 +4 -4
- package/dist/{113.js → 476.js} +1 -1
- package/dist/476.js.map +1 -0
- package/dist/kenyaemr-esm-billing-app.js +3 -3
- package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +27 -27
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/claims/dashboard/form/claims-form.component.tsx +281 -63
- package/src/config-schema.ts +6 -0
- package/src/hooks/useOTP.ts +292 -0
- package/src/hooks/usePhoneNumber.ts +35 -0
- package/src/types/index.ts +35 -0
- package/dist/113.js.map +0 -1
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"accountingDashboardLink","name":"accounting-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"accounting","slot":"accounting-dashboard-slot","title":"Accounting"}},{"name":"billing-dashboard","component":"billingDashboard","slot":"accounting-dashboard-slot"},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","slots":["claims-management-dashboard-link-slot"]},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slots":["claims-management-dashboard-link-slot"]},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"patient-info-sha-status","component":"patientBannerShaStatus","slot":"patient-banner-tags-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"},{"name":"payment-workspace","component":"paymentWorkspace","title":"Payment Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"version":"5.4.2-pre.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"accountingDashboardLink","name":"accounting-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"accounting","slot":"accounting-dashboard-slot","title":"Accounting"}},{"name":"billing-dashboard","component":"billingDashboard","slot":"accounting-dashboard-slot"},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","slots":["claims-management-dashboard-link-slot"]},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slots":["claims-management-dashboard-link-slot"]},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"patient-info-sha-status","component":"patientBannerShaStatus","slot":"patient-banner-tags-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"},{"name":"payment-workspace","component":"paymentWorkspace","title":"Payment Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"version":"5.4.2-pre.2553"}
|
package/package.json
CHANGED
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
ComboBox,
|
|
15
15
|
} from '@carbon/react';
|
|
16
16
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
17
|
-
import { navigate, showSnackbar, toOmrsIsoString, useConfig, useSession } from '@openmrs/esm-framework';
|
|
18
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
17
|
+
import { navigate, showModal, showSnackbar, toOmrsIsoString, useConfig, useSession } from '@openmrs/esm-framework';
|
|
18
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
19
19
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
|
20
20
|
import { useTranslation } from 'react-i18next';
|
|
21
21
|
import { useParams } from 'react-router-dom';
|
|
@@ -25,7 +25,7 @@ import { BillingConfig } from '../../../config-schema';
|
|
|
25
25
|
import { useSystemSetting } from '../../../hooks/getMflCode';
|
|
26
26
|
import usePatientDiagnosis from '../../../hooks/usePatientDiagnosis';
|
|
27
27
|
import useProvider from '../../../hooks/useProvider';
|
|
28
|
-
import { LineItem, MappedBill } from '../../../types';
|
|
28
|
+
import { ClaimSummary, LineItem, MappedBill, OTPVerificationModalOptions } from '../../../types';
|
|
29
29
|
import ClaimExplanationAndJusificationInput from './claims-explanation-and-justification-form-input.component';
|
|
30
30
|
import { processClaims, SHAPackagesAndInterventionVisitAttribute, useVisit } from './claims-form.resource';
|
|
31
31
|
import useProviderList from '../../../hooks/useProviderList';
|
|
@@ -33,6 +33,8 @@ import useProviderList from '../../../hooks/useProviderList';
|
|
|
33
33
|
import styles from './claims-form.scss';
|
|
34
34
|
import debounce from 'lodash-es/debounce';
|
|
35
35
|
import { formatDateTime } from '../../utils';
|
|
36
|
+
import { otpManager } from '../../../hooks/useOTP';
|
|
37
|
+
import { usePhoneNumberAttribute } from '../../../hooks/usePhoneNumber';
|
|
36
38
|
|
|
37
39
|
type ClaimsFormProps = {
|
|
38
40
|
bill: MappedBill;
|
|
@@ -65,6 +67,12 @@ const ClaimsFormSchema = z.object({
|
|
|
65
67
|
provider: z.string().min(1, { message: 'Provider is required' }),
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
enum OTPState {
|
|
71
|
+
NOT_STARTED = 'not_started',
|
|
72
|
+
REQUESTED = 'requested',
|
|
73
|
+
VERIFIED = 'verified',
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
69
77
|
const { t } = useTranslation();
|
|
70
78
|
const { mflCodeValue } = useSystemSetting('facility.mflcode');
|
|
@@ -74,33 +82,22 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
74
82
|
const {
|
|
75
83
|
currentProvider: { uuid: providerUuid },
|
|
76
84
|
} = useSession();
|
|
77
|
-
const { providerLoading
|
|
85
|
+
const { providerLoading, provider, error: providerError } = useProvider(providerUuid);
|
|
78
86
|
const { visitAttributeTypes } = useConfig<BillingConfig>();
|
|
79
87
|
const { providers, providersLoading } = useProviderList();
|
|
88
|
+
const { phoneNumber } = usePhoneNumberAttribute(patientUuid);
|
|
80
89
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const values = recentVisit.attributes?.find(
|
|
84
|
-
(attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
|
|
85
|
-
)?.value;
|
|
86
|
-
if (values) {
|
|
87
|
-
const payload: SHAPackagesAndInterventionVisitAttribute = JSON.parse(values);
|
|
88
|
-
return payload;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}, [recentVisit, visitAttributeTypes]);
|
|
93
|
-
|
|
94
|
-
const encounterUuid = recentVisit?.encounters[0]?.uuid;
|
|
95
|
-
const visitTypeUuid = recentVisit?.visitType.uuid;
|
|
96
|
-
const [loading, setLoading] = useState(false);
|
|
90
|
+
const [otpState, setOtpState] = useState<OTPState>(OTPState.NOT_STARTED);
|
|
91
|
+
const [pendingClaimData, setPendingClaimData] = useState<z.infer<typeof ClaimsFormSchema> | null>(null);
|
|
97
92
|
const [formInitialized, setFormInitialized] = useState(false);
|
|
98
93
|
const [validationEnabled, setValidationEnabled] = useState(false);
|
|
94
|
+
const [loading, setLoading] = useState(false);
|
|
95
|
+
const [currentOtpPhoneNumber, setCurrentOtpPhoneNumber] = useState<string>('');
|
|
99
96
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
const currentPhoneRef = useRef<string>('');
|
|
98
|
+
|
|
99
|
+
const patientName = `${bill.patientName}`;
|
|
100
|
+
const otpExpiryMinutes = 5;
|
|
104
101
|
|
|
105
102
|
const form = useForm<z.infer<typeof ClaimsFormSchema>>({
|
|
106
103
|
mode: 'onTouched',
|
|
@@ -122,17 +119,14 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
122
119
|
const {
|
|
123
120
|
control,
|
|
124
121
|
handleSubmit,
|
|
125
|
-
formState: { errors, isValid,
|
|
122
|
+
formState: { errors, isValid, touchedFields },
|
|
126
123
|
setValue,
|
|
127
|
-
reset,
|
|
128
124
|
trigger,
|
|
129
125
|
watch,
|
|
130
126
|
} = form;
|
|
131
127
|
|
|
132
128
|
const packages = watch('packages');
|
|
133
129
|
const interventions = watch('interventions');
|
|
134
|
-
const claimExplanation = watch('claimExplanation');
|
|
135
|
-
const claimJustification = watch('claimJustification');
|
|
136
130
|
|
|
137
131
|
const debouncedValidation = useCallback(
|
|
138
132
|
debounce(() => {
|
|
@@ -140,7 +134,6 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
140
134
|
trigger();
|
|
141
135
|
}
|
|
142
136
|
}, 500),
|
|
143
|
-
// eslint-disable-line react-hooks/exhaustive-deps
|
|
144
137
|
[formInitialized, trigger],
|
|
145
138
|
);
|
|
146
139
|
|
|
@@ -157,8 +150,24 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
157
150
|
facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`,
|
|
158
151
|
treatmentStart: formatDateTime(recentVisit?.startDatetime || ''),
|
|
159
152
|
treatmentEnd: formatDateTime(recentVisit?.stopDatetime || ''),
|
|
160
|
-
packages:
|
|
161
|
-
|
|
153
|
+
packages: recentVisit?.attributes?.find(
|
|
154
|
+
(attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
|
|
155
|
+
)?.value
|
|
156
|
+
? JSON.parse(
|
|
157
|
+
recentVisit.attributes.find(
|
|
158
|
+
(attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
|
|
159
|
+
).value,
|
|
160
|
+
).packages
|
|
161
|
+
: [],
|
|
162
|
+
interventions: recentVisit?.attributes?.find(
|
|
163
|
+
(attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
|
|
164
|
+
)?.value
|
|
165
|
+
? JSON.parse(
|
|
166
|
+
recentVisit.attributes.find(
|
|
167
|
+
(attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
|
|
168
|
+
).value,
|
|
169
|
+
).interventions
|
|
170
|
+
: [],
|
|
162
171
|
provider: providerUuid,
|
|
163
172
|
};
|
|
164
173
|
|
|
@@ -176,21 +185,87 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
176
185
|
mflCodeValue,
|
|
177
186
|
setValue,
|
|
178
187
|
provider,
|
|
179
|
-
packagesAndinterventions,
|
|
180
188
|
visitLoading,
|
|
181
189
|
diagnosisLoading,
|
|
182
190
|
providerLoading,
|
|
183
191
|
providerUuid,
|
|
192
|
+
visitAttributeTypes,
|
|
184
193
|
]);
|
|
185
194
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
const generateClaimSummary = (data: z.infer<typeof ClaimsFormSchema>): ClaimSummary => {
|
|
196
|
+
const billServiceNames = selectedLineItems.map((item) => item.billableService);
|
|
197
|
+
const services = billServiceNames.map((service) => service.replace(/^[a-f0-9-]+:/, '').trim());
|
|
198
|
+
const totalAmount = selectedLineItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
totalAmount,
|
|
202
|
+
facility: recentVisit?.location?.display || '',
|
|
203
|
+
totalItems: selectedLineItems.length,
|
|
204
|
+
services: services.join(', '),
|
|
205
|
+
startDate: data.treatmentStart,
|
|
206
|
+
endDate: data.treatmentEnd,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const createDynamicOTPHandlers = useCallback(
|
|
211
|
+
(initialPhone: string) => {
|
|
212
|
+
currentPhoneRef.current = initialPhone;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
onRequestOtp: async (phoneNumber: string): Promise<void> => {
|
|
216
|
+
if (currentPhoneRef.current && currentPhoneRef.current !== phoneNumber) {
|
|
217
|
+
otpManager.transferOTP(currentPhoneRef.current, phoneNumber);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setCurrentOtpPhoneNumber(phoneNumber);
|
|
221
|
+
currentPhoneRef.current = phoneNumber;
|
|
222
|
+
|
|
223
|
+
const currentFormData = form.getValues();
|
|
224
|
+
if (!currentFormData || !selectedLineItems?.length) {
|
|
225
|
+
throw new Error('No claim data available for OTP request');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const claimSummary = generateClaimSummary(currentFormData);
|
|
229
|
+
await otpManager.requestOTP(phoneNumber, patientName, claimSummary, otpExpiryMinutes);
|
|
230
|
+
},
|
|
231
|
+
onVerify: async (otp: string): Promise<void> => {
|
|
232
|
+
const phoneForVerification = currentPhoneRef.current;
|
|
233
|
+
|
|
234
|
+
if (!phoneForVerification) {
|
|
235
|
+
throw new Error('No phone number available for verification');
|
|
236
|
+
}
|
|
192
237
|
|
|
238
|
+
const isValid = await otpManager.verifyOTP(phoneForVerification, otp);
|
|
239
|
+
if (!isValid) {
|
|
240
|
+
throw new Error('OTP verification failed');
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
[patientName, form, selectedLineItems, otpExpiryMinutes],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const launchOtpVerificationModal = (props: OTPVerificationModalOptions) => {
|
|
249
|
+
const dispose = showModal('otp-verification-modal', {
|
|
250
|
+
...props,
|
|
251
|
+
onClose: () => {
|
|
252
|
+
if (otpState === OTPState.REQUESTED) {
|
|
253
|
+
setOtpState(OTPState.NOT_STARTED);
|
|
254
|
+
}
|
|
255
|
+
dispose();
|
|
256
|
+
},
|
|
257
|
+
size: 'xs',
|
|
258
|
+
});
|
|
259
|
+
return dispose;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const handleOTPVerificationSuccess = async (): Promise<void> => {
|
|
263
|
+
setOtpState(OTPState.VERIFIED);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const processClaim = async (data: z.infer<typeof ClaimsFormSchema>) => {
|
|
193
267
|
setLoading(true);
|
|
268
|
+
|
|
194
269
|
const providedItems = selectedLineItems.reduce((acc, item) => {
|
|
195
270
|
acc[item.uuid] = {
|
|
196
271
|
items: [
|
|
@@ -216,12 +291,12 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
216
291
|
diagnoses: data.diagnoses,
|
|
217
292
|
paidInFacility: true,
|
|
218
293
|
patient: patientUuid,
|
|
219
|
-
visitType:
|
|
294
|
+
visitType: recentVisit?.visitType?.uuid,
|
|
220
295
|
guaranteeId: 'G-001',
|
|
221
296
|
claimCode: 'C-001',
|
|
222
297
|
provider: data.provider,
|
|
223
|
-
visitUuid: recentVisit
|
|
224
|
-
encounterUuid:
|
|
298
|
+
visitUuid: recentVisit?.uuid,
|
|
299
|
+
encounterUuid: recentVisit?.encounters?.[0]?.uuid,
|
|
225
300
|
use: 'claim',
|
|
226
301
|
insurer: 'SHA',
|
|
227
302
|
billNumber: billUuid,
|
|
@@ -231,20 +306,27 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
231
306
|
|
|
232
307
|
try {
|
|
233
308
|
await processClaims(payload);
|
|
309
|
+
|
|
234
310
|
showSnackbar({
|
|
235
311
|
kind: 'success',
|
|
236
312
|
title: t('processClaim', 'Process Claim'),
|
|
237
|
-
subtitle: t('
|
|
238
|
-
timeoutInMs:
|
|
313
|
+
subtitle: t('claimProcessedSuccessfully', 'Claim processed and sent successfully'),
|
|
314
|
+
timeoutInMs: 4000,
|
|
239
315
|
isLowContrast: true,
|
|
240
316
|
});
|
|
317
|
+
|
|
318
|
+
setOtpState(OTPState.NOT_STARTED);
|
|
319
|
+
setPendingClaimData(null);
|
|
320
|
+
setCurrentOtpPhoneNumber('');
|
|
321
|
+
currentPhoneRef.current = '';
|
|
322
|
+
otpManager.clearAllOTPs();
|
|
323
|
+
|
|
241
324
|
setTimeout(() => {
|
|
242
325
|
navigate({
|
|
243
326
|
to: window.getOpenmrsSpaBase() + 'home/billing/',
|
|
244
327
|
});
|
|
245
328
|
}, 1000);
|
|
246
329
|
} catch (err) {
|
|
247
|
-
console.error(err);
|
|
248
330
|
showSnackbar({
|
|
249
331
|
kind: 'error',
|
|
250
332
|
title: t('claimError', 'Claim Error'),
|
|
@@ -257,6 +339,78 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
257
339
|
}
|
|
258
340
|
};
|
|
259
341
|
|
|
342
|
+
const handleInitiateOTPVerification = async (data: z.infer<typeof ClaimsFormSchema>) => {
|
|
343
|
+
setValidationEnabled(true);
|
|
344
|
+
const isFormValid = await trigger();
|
|
345
|
+
if (!isFormValid) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!phoneNumber) {
|
|
350
|
+
showSnackbar({
|
|
351
|
+
kind: 'error',
|
|
352
|
+
title: t('noPhoneNumber', 'No Phone Number'),
|
|
353
|
+
subtitle: t(
|
|
354
|
+
'noPhoneNumberMessage',
|
|
355
|
+
'No phone number found for this patient. Please update patient information.',
|
|
356
|
+
),
|
|
357
|
+
timeoutInMs: 4000,
|
|
358
|
+
isLowContrast: false,
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
setPendingClaimData(data);
|
|
364
|
+
setOtpState(OTPState.REQUESTED);
|
|
365
|
+
setCurrentOtpPhoneNumber(phoneNumber);
|
|
366
|
+
currentPhoneRef.current = phoneNumber;
|
|
367
|
+
|
|
368
|
+
const dynamicHandlers = createDynamicOTPHandlers(phoneNumber);
|
|
369
|
+
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
launchOtpVerificationModal({
|
|
372
|
+
otpLength: 5,
|
|
373
|
+
obscureText: false,
|
|
374
|
+
phoneNumber: phoneNumber,
|
|
375
|
+
expiryMinutes: otpExpiryMinutes,
|
|
376
|
+
onRequestOtp: dynamicHandlers.onRequestOtp,
|
|
377
|
+
onVerify: dynamicHandlers.onVerify,
|
|
378
|
+
onVerificationSuccess: handleOTPVerificationSuccess,
|
|
379
|
+
});
|
|
380
|
+
}, 0);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const handleProcessVerifiedClaim = async () => {
|
|
384
|
+
if (pendingClaimData && otpState === OTPState.VERIFIED) {
|
|
385
|
+
await processClaim(pendingClaimData);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const handleReopenOTPModal = () => {
|
|
390
|
+
const dynamicHandlers = createDynamicOTPHandlers(phoneNumber);
|
|
391
|
+
|
|
392
|
+
launchOtpVerificationModal({
|
|
393
|
+
otpLength: 5,
|
|
394
|
+
obscureText: false,
|
|
395
|
+
phoneNumber: currentOtpPhoneNumber || phoneNumber,
|
|
396
|
+
expiryMinutes: otpExpiryMinutes,
|
|
397
|
+
onRequestOtp: dynamicHandlers.onRequestOtp,
|
|
398
|
+
onVerify: dynamicHandlers.onVerify,
|
|
399
|
+
onVerificationSuccess: handleOTPVerificationSuccess,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const handleDiscardClaim = () => {
|
|
404
|
+
setOtpState(OTPState.NOT_STARTED);
|
|
405
|
+
setPendingClaimData(null);
|
|
406
|
+
setCurrentOtpPhoneNumber('');
|
|
407
|
+
currentPhoneRef.current = '';
|
|
408
|
+
otpManager.clearAllOTPs();
|
|
409
|
+
navigate({
|
|
410
|
+
to: window.getOpenmrsSpaBase() + `home/billing/patient/${patientUuid}/${billUuid}`,
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
|
|
260
414
|
if (visitLoading || diagnosisLoading || providerLoading) {
|
|
261
415
|
return (
|
|
262
416
|
<Layer className={styles.loading}>
|
|
@@ -288,9 +442,12 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
288
442
|
return validationEnabled && errors[fieldName] && (touchedFields[fieldName] || formInitialized);
|
|
289
443
|
};
|
|
290
444
|
|
|
445
|
+
const isFormValid = isValid && packages?.length > 0 && interventions?.length > 0 && selectedLineItems?.length > 0;
|
|
446
|
+
const displayPhoneNumber = currentOtpPhoneNumber || phoneNumber;
|
|
447
|
+
|
|
291
448
|
return (
|
|
292
449
|
<FormProvider {...form}>
|
|
293
|
-
<Form className={styles.form}
|
|
450
|
+
<Form className={styles.form}>
|
|
294
451
|
{!selectedLineItems?.length && (
|
|
295
452
|
<div className={styles.notificationContainer}>
|
|
296
453
|
<InlineNotification
|
|
@@ -303,6 +460,49 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
303
460
|
/>
|
|
304
461
|
</div>
|
|
305
462
|
)}
|
|
463
|
+
|
|
464
|
+
{!displayPhoneNumber && (
|
|
465
|
+
<div className={styles.notificationContainer}>
|
|
466
|
+
<InlineNotification
|
|
467
|
+
kind="warning"
|
|
468
|
+
title={t('noPhoneNumber', 'No Phone Number')}
|
|
469
|
+
subtitle={t(
|
|
470
|
+
'noPhoneNumberFound',
|
|
471
|
+
'No phone number found for this patient. OTP verification will not be available.',
|
|
472
|
+
)}
|
|
473
|
+
hideCloseButton={true}
|
|
474
|
+
lowContrast={true}
|
|
475
|
+
className={styles.notification}
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
|
|
480
|
+
{otpState === OTPState.REQUESTED && (
|
|
481
|
+
<div className={styles.notificationContainer}>
|
|
482
|
+
<InlineNotification
|
|
483
|
+
kind="info"
|
|
484
|
+
title={t('otpVerificationPending', 'OTP Verification Pending')}
|
|
485
|
+
subtitle={t('otpVerificationPendingMessage', 'Please complete OTP verification to process the claim')}
|
|
486
|
+
hideCloseButton={true}
|
|
487
|
+
lowContrast={true}
|
|
488
|
+
className={styles.notification}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{otpState === OTPState.VERIFIED && (
|
|
494
|
+
<div className={styles.notificationContainer}>
|
|
495
|
+
<InlineNotification
|
|
496
|
+
kind="success"
|
|
497
|
+
title={t('otpVerified', 'OTP Verified')}
|
|
498
|
+
subtitle={t('otpVerifiedReadyToProcess', 'OTP has been verified. Click "Process Claim" to submit.')}
|
|
499
|
+
hideCloseButton={true}
|
|
500
|
+
lowContrast={true}
|
|
501
|
+
className={styles.notification}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
|
|
306
506
|
<Stack gap={4} className={styles.grid}>
|
|
307
507
|
<span className={styles.claimFormTitle}>{t('formTitle', 'Fill in the form details')}</span>
|
|
308
508
|
<Row className={styles.formClaimRow}>
|
|
@@ -440,27 +640,45 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
440
640
|
validationEnabled={validationEnabled}
|
|
441
641
|
onInteraction={() => setValidationEnabled(true)}
|
|
442
642
|
/>
|
|
643
|
+
|
|
443
644
|
<ButtonSet className={styles.buttonSet}>
|
|
444
|
-
<Button className={styles.button} kind="secondary" onClick={
|
|
645
|
+
<Button className={styles.button} kind="secondary" onClick={handleDiscardClaim}>
|
|
445
646
|
{t('discardClaim', 'Discard Claim')}
|
|
446
647
|
</Button>
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
648
|
+
|
|
649
|
+
{otpState === OTPState.NOT_STARTED && (
|
|
650
|
+
<Button
|
|
651
|
+
className={styles.button}
|
|
652
|
+
kind="primary"
|
|
653
|
+
onClick={handleSubmit(handleInitiateOTPVerification)}
|
|
654
|
+
disabled={!isFormValid || !displayPhoneNumber}
|
|
655
|
+
tooltipPosition="top"
|
|
656
|
+
tooltipAlignment="center">
|
|
657
|
+
{t('sendOtp', 'Send OTP')}
|
|
658
|
+
</Button>
|
|
659
|
+
)}
|
|
660
|
+
|
|
661
|
+
{otpState === OTPState.REQUESTED && (
|
|
662
|
+
<Button className={styles.button} kind="ghost" onClick={handleReopenOTPModal}>
|
|
663
|
+
{t('enterOtp', 'Enter OTP')}
|
|
664
|
+
</Button>
|
|
665
|
+
)}
|
|
666
|
+
|
|
667
|
+
{otpState === OTPState.VERIFIED && (
|
|
668
|
+
<Button
|
|
669
|
+
className={styles.button}
|
|
670
|
+
kind="primary"
|
|
671
|
+
onClick={handleProcessVerifiedClaim}
|
|
672
|
+
disabled={!pendingClaimData || loading}
|
|
673
|
+
tooltipPosition="top"
|
|
674
|
+
tooltipAlignment="center">
|
|
675
|
+
{loading ? (
|
|
676
|
+
<InlineLoading description={t('processing', 'Processing claim...')} />
|
|
677
|
+
) : (
|
|
678
|
+
t('processClaim', 'Process Claim')
|
|
679
|
+
)}
|
|
680
|
+
</Button>
|
|
681
|
+
)}
|
|
464
682
|
</ButtonSet>
|
|
465
683
|
</Stack>
|
|
466
684
|
</Form>
|
package/src/config-schema.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface BillingConfig {
|
|
|
35
35
|
duration: number;
|
|
36
36
|
};
|
|
37
37
|
localeCurrencyMapping: Record<string, string>;
|
|
38
|
+
phoneNumberAttributeTypeUUID: string;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export const configSchema: ConfigSchema = {
|
|
@@ -68,6 +69,11 @@ export const configSchema: ConfigSchema = {
|
|
|
68
69
|
_description: 'The base url that will be used to make any backend calls related to mpesa.',
|
|
69
70
|
_default: 'https://billing.kenyahmis.org',
|
|
70
71
|
},
|
|
72
|
+
phoneNumberAttributeTypeUUID: {
|
|
73
|
+
_type: Type.String,
|
|
74
|
+
_description: 'The person attribute type uuid for phone number',
|
|
75
|
+
_default: '78b4630d-3446-4db2-b570-2e553231a589',
|
|
76
|
+
},
|
|
71
77
|
hieBaseUrl: {
|
|
72
78
|
_type: Type.String,
|
|
73
79
|
_description: 'HIE Base URL for getting interventions and benefit packages',
|