@kenyaemr/esm-express-workflow-app 5.4.4-pre.299 → 5.4.4-pre.3
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 +3 -3
- package/dist/126.js +1 -0
- package/dist/126.js.map +1 -0
- package/dist/2024.js +1 -1
- package/dist/2024.js.map +1 -1
- package/dist/2225.js +1 -1
- package/dist/2225.js.map +1 -1
- package/dist/2386.js +1 -0
- package/dist/2386.js.map +1 -0
- package/dist/2438.js +1 -1
- package/dist/2438.js.map +1 -1
- package/dist/4504.js +1 -0
- package/dist/4504.js.map +1 -0
- package/dist/4670.js +1 -0
- package/dist/4670.js.map +1 -0
- package/dist/4682.js +1 -1
- package/dist/4682.js.map +1 -1
- package/dist/5047.js +1 -0
- package/dist/5047.js.map +1 -0
- package/dist/586.js.map +1 -1
- package/dist/5986.js +1 -1
- package/dist/5986.js.map +1 -1
- package/dist/6075.js +1 -0
- package/dist/6075.js.map +1 -0
- package/dist/6328.js +1 -1
- package/dist/6328.js.map +1 -1
- package/dist/64.js +1 -0
- package/dist/64.js.map +1 -0
- package/dist/6456.js +1 -1
- package/dist/6720.js +1 -0
- package/dist/6720.js.map +1 -0
- package/dist/7607.js +1 -1
- package/dist/8566.js +1 -0
- package/dist/8566.js.map +1 -0
- package/dist/8727.js +1 -1
- package/dist/8799.js +1 -0
- package/dist/8799.js.map +1 -0
- package/dist/8911.js +1 -1
- package/dist/8911.js.map +1 -1
- package/dist/9157.js +1 -1
- package/dist/9157.js.map +1 -1
- package/dist/9161.js +1 -0
- package/dist/9161.js.map +1 -0
- package/dist/9182.js +1 -1
- package/dist/9343.js +1 -0
- package/dist/9343.js.map +1 -0
- package/dist/9535.js +1 -1
- package/dist/9535.js.map +1 -1
- package/dist/9834.js +1 -0
- package/dist/{4432.js.map → 9834.js.map} +1 -1
- package/dist/kenyaemr-esm-express-workflow-app.js +2 -2
- package/dist/kenyaemr-esm-express-workflow-app.js.buildmanifest.json +294 -151
- package/dist/kenyaemr-esm-express-workflow-app.js.map +1 -1
- 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/components/admissions/index.tsx +0 -1
- package/src/components/anaesthetic/anaesthetic.component.tsx +10 -4
- package/src/components/anaesthetic/forms/interoperative-record-drug-given-form.component.tsx +2 -26
- package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph-wrapper.component.tsx +66 -25
- package/src/components/anaesthetic/resources/anaesthetic.resource.ts +2 -2
- package/src/components/consultation/clinical-encounter/clinical-encounter.component.tsx +4 -20
- package/src/components/consultation/index.ts +1 -1
- package/src/components/consultation/tabs/index.ts +67 -0
- package/src/components/facility-dashboard/components/header/home-header.component.tsx +8 -13
- package/src/components/laboratory/index.ts +11 -0
- package/src/components/laboratory/lab-table.component.tsx +25 -12
- package/src/components/laboratory/laboratory-tabs.component.tsx +3 -17
- package/src/components/pharmacy/index.ts +1 -1
- package/src/components/pharmacy/pharmacy-tabs.component.tsx +4 -20
- package/src/components/pharmacy/tabs/index.ts +21 -0
- package/src/components/procedures/index.ts +16 -0
- package/src/components/procedures/procedures-table.component.tsx +33 -15
- package/src/components/procedures/procedures-tabs.component.tsx +3 -22
- package/src/components/radiology-and-imaging/index.ts +11 -0
- package/src/components/radiology-and-imaging/radiology-and-imaging-table.component.tsx +35 -15
- package/src/components/radiology-and-imaging/radiology-and-imaging.component.tsx +3 -19
- package/src/components/registration/card/Local-card/local-card.component.tsx +3 -3
- package/src/components/registration/dependants/dependants.component.tsx +6 -1
- package/src/components/registration/end-visit-modal/end-visit-dialog.modal.tsx +320 -0
- package/src/components/registration/end-visit-modal/end-visit-dialog.scss +12 -0
- package/src/components/registration/end-visit-modal/end-visit.resource.tsx +59 -0
- package/src/components/registration/end-visit-modal/sha-end-visit.resource.ts +285 -0
- package/src/components/registration/start-visit-form/overflow-menu-extension/custom-end-active-visit.extension.tsx +44 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/exported-visit-form.workspace.tsx +16 -11
- package/src/components/registration/type/index.ts +101 -0
- package/src/config-schema.ts +23 -1
- package/src/index.ts +36 -17
- package/src/routes.json +205 -12
- package/src/shared/otp-verification/index.ts +3 -0
- package/src/shared/otp-verification/otp-verification.modal.tsx +51 -17
- package/src/shared/patient-chart/patient-summary-dashboard/patient-summary-dashboard.component.tsx +5 -73
- package/src/shared/tabs/create-tab-extension.tsx +33 -0
- package/src/shared/tabs/extension-tabs.component.tsx +51 -33
- package/src/shared/utils/index.ts +6 -6
- package/translations/am.json +32 -24
- package/translations/en.json +23 -16
- package/translations/fr.json +23 -16
- package/translations/sw.json +23 -21
- package/Failed to delete encounter/am.json +0 -4
- package/Failed to delete encounter/en.json +0 -4
- package/Failed to delete encounter/fr.json +0 -4
- package/Failed to delete encounter/sw.json +0 -4
- package/dist/2332.js +0 -1
- package/dist/2332.js.map +0 -1
- package/dist/4432.js +0 -1
- package/dist/4666.js +0 -1
- package/dist/4666.js.map +0 -1
- package/dist/5733.js +0 -1
- package/dist/5733.js.map +0 -1
- package/dist/644.js +0 -1
- package/dist/644.js.map +0 -1
- package/dist/655.js +0 -1
- package/dist/655.js.map +0 -1
- package/dist/70.js +0 -1
- package/dist/70.js.map +0 -1
- package/dist/9387.js +0 -1
- package/dist/9387.js.map +0 -1
- package/src/components/admissions/admission-medication-summary.tsx +0 -302
- package/src/components/admissions/admissions-dashboard.utils.test.ts +0 -48
- package/src/components/admissions/admissions-dashboard.utils.tsx +0 -63
- package/src/shared/orders/OrdersTabs.tsx +0 -76
- /package/src/shared/{patient-chart/patient-summary-dashboard → tabs}/patient-summary-dashboard.scss +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, Dropdown, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
import { showModal, showSnackbar, useSession, useVisit } from '@openmrs/esm-framework';
|
|
5
|
+
import { extractFetchError, extractUpstreamError } from '../../../shared/utils';
|
|
6
|
+
import styles from './end-visit-dialog.scss';
|
|
7
|
+
import { processVisitInsuranceClaim, sendDischargeOtp, submitVisitClaim } from './end-visit.resource';
|
|
8
|
+
import {
|
|
9
|
+
checkBiometricAuthorizationStatus,
|
|
10
|
+
createSHABiometricAuthorize,
|
|
11
|
+
detectAuthorizingDeviceOS,
|
|
12
|
+
fetchWhitelistStatus,
|
|
13
|
+
rejectBiometricAuthorization,
|
|
14
|
+
submitOtpWhitelist,
|
|
15
|
+
useBiometricAgentStatus,
|
|
16
|
+
useBiometricConfig,
|
|
17
|
+
useOtpWhitelistReasons,
|
|
18
|
+
usePatientCRId,
|
|
19
|
+
usePatientPhone,
|
|
20
|
+
useSHAEligibility,
|
|
21
|
+
useProviderNationalId,
|
|
22
|
+
} from './sha-end-visit.resource';
|
|
23
|
+
import { DISCHARGE_REASONS } from '../type';
|
|
24
|
+
|
|
25
|
+
interface EndVisitDialogProps {
|
|
26
|
+
patientUuid: string;
|
|
27
|
+
closeModal: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const EndVisitDialog: React.FC<EndVisitDialogProps> = ({ patientUuid, closeModal }) => {
|
|
31
|
+
const { t } = useTranslation();
|
|
32
|
+
const { activeVisit } = useVisit(patientUuid);
|
|
33
|
+
const [step, setStep] = useState<'confirm' | 'discharge-reason'>('confirm');
|
|
34
|
+
const [isProcessingClaim, setIsProcessingClaim] = useState(false);
|
|
35
|
+
const [reason, setReason] = useState('');
|
|
36
|
+
const processingClaimRef = useRef(false);
|
|
37
|
+
const serviceTypeRef = useRef('OUTPATIENT');
|
|
38
|
+
const consentTokenRef = useRef<string>('');
|
|
39
|
+
const invoiceNumberRef = useRef<string>('');
|
|
40
|
+
const interventionsRef = useRef<string[]>([]);
|
|
41
|
+
const phoneNumber = usePatientPhone(patientUuid);
|
|
42
|
+
const patientCRId = usePatientCRId(patientUuid);
|
|
43
|
+
const { isPatientWhiteListed, facilityBiometricsEnforced } = useSHAEligibility(patientUuid);
|
|
44
|
+
const { currentProvider } = useSession();
|
|
45
|
+
const { providerNationalid } = useProviderNationalId(currentProvider?.uuid ?? '');
|
|
46
|
+
const { agentUrl } = useBiometricConfig();
|
|
47
|
+
const { workstationId } = useBiometricAgentStatus(agentUrl);
|
|
48
|
+
const deviceOs = detectAuthorizingDeviceOS();
|
|
49
|
+
const { reasons: whitelistReasons } = useOtpWhitelistReasons();
|
|
50
|
+
|
|
51
|
+
const buildBiometricStarter = useCallback(
|
|
52
|
+
(crId: string) => {
|
|
53
|
+
return async () => {
|
|
54
|
+
if (!providerNationalid) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
t(
|
|
57
|
+
'biometricMissingNationalId',
|
|
58
|
+
'Provider National ID not configured. Please add it to your provider profile.',
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!workstationId) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
t(
|
|
65
|
+
'biometricAgentNotReachable',
|
|
66
|
+
'Biometric agent not running. Please start the agent on this workstation and try again.',
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const res = await createSHABiometricAuthorize({
|
|
72
|
+
agent_id: providerNationalid,
|
|
73
|
+
patient_id: crId,
|
|
74
|
+
interventions: interventionsRef.current,
|
|
75
|
+
service_type: serviceTypeRef.current,
|
|
76
|
+
workstation_id: workstationId,
|
|
77
|
+
authorizing_device_os: deviceOs,
|
|
78
|
+
patient_uuid: patientUuid,
|
|
79
|
+
is_biometrics_discharge_authorization: true,
|
|
80
|
+
});
|
|
81
|
+
if (!res.success || !res.embed_url || !res.token || !res.guid) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
extractUpstreamError(res as any, t('biometricStartFailed', 'Could not start biometric session.')),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
embed_url: res.embed_url,
|
|
88
|
+
authorization_code: res.authorization_code ?? '',
|
|
89
|
+
consent_token: res.consent_token ?? res.token,
|
|
90
|
+
token: res.token,
|
|
91
|
+
guid: res.guid,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
[providerNationalid, workstationId, deviceOs, patientUuid, t],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const handleSubmitWhitelist = useCallback(
|
|
99
|
+
async (params: { reasonType: string; reason: string; biometricAttempts: number; attachment: File | null }) => {
|
|
100
|
+
if (!patientCRId) {
|
|
101
|
+
return { success: false, error: t('noCRNumber', 'Patient has no SHA CR number.') };
|
|
102
|
+
}
|
|
103
|
+
return submitOtpWhitelist({
|
|
104
|
+
beneficiaryCrId: patientCRId,
|
|
105
|
+
reasonType: params.reasonType,
|
|
106
|
+
reason: params.reason,
|
|
107
|
+
biometricAttempts: params.biometricAttempts,
|
|
108
|
+
attachment: params.attachment,
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
[patientCRId, t],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const handleCheckWhitelistStatus = useCallback(async (crId: string) => {
|
|
115
|
+
const status = await fetchWhitelistStatus(crId);
|
|
116
|
+
return {
|
|
117
|
+
is_whitelisted: status.is_whitelisted,
|
|
118
|
+
has_pending: status.has_pending,
|
|
119
|
+
is_rejected: status.is_rejected,
|
|
120
|
+
latest_status: status.latest_status,
|
|
121
|
+
reviewer_note: status.requests?.[0]?.reviewer_note ?? null,
|
|
122
|
+
};
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const handleEndVisit = async () => {
|
|
126
|
+
if (!activeVisit || processingClaimRef.current) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
processingClaimRef.current = true;
|
|
130
|
+
setIsProcessingClaim(true);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await processVisitInsuranceClaim(activeVisit.uuid);
|
|
134
|
+
serviceTypeRef.current = response?.data?.service_type ?? 'OUTPATIENT';
|
|
135
|
+
consentTokenRef.current = String(response?.data?.consentToken ?? '');
|
|
136
|
+
invoiceNumberRef.current = String(response?.data?.invoiceNumber ?? '');
|
|
137
|
+
interventionsRef.current = Array.isArray(response?.data?.intervention_codes)
|
|
138
|
+
? response?.data?.intervention_codes
|
|
139
|
+
: [];
|
|
140
|
+
setStep('discharge-reason');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
showSnackbar({
|
|
143
|
+
isLowContrast: true,
|
|
144
|
+
kind: 'error',
|
|
145
|
+
title: t('claimProcessingError', 'Error'),
|
|
146
|
+
subtitle: extractFetchError(error, t('insuranceClaimProcessingFailed', 'Insurance claim processing failed')),
|
|
147
|
+
});
|
|
148
|
+
closeModal();
|
|
149
|
+
} finally {
|
|
150
|
+
processingClaimRef.current = false;
|
|
151
|
+
setIsProcessingClaim(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleContinue = useCallback(() => {
|
|
156
|
+
if (!reason || !patientCRId || !activeVisit) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
closeModal();
|
|
161
|
+
|
|
162
|
+
const crId = patientCRId;
|
|
163
|
+
const visitUuid = activeVisit.uuid;
|
|
164
|
+
const dischargeReason = reason;
|
|
165
|
+
const invoiceNumber = invoiceNumberRef.current;
|
|
166
|
+
let settled = false;
|
|
167
|
+
|
|
168
|
+
const settle = (fn?: () => void) => {
|
|
169
|
+
if (!settled) {
|
|
170
|
+
settled = true;
|
|
171
|
+
fn?.();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
const dispose = showModal('otp-verification-modal', {
|
|
177
|
+
onClose: () => {
|
|
178
|
+
settle();
|
|
179
|
+
dispose();
|
|
180
|
+
},
|
|
181
|
+
phoneNumber,
|
|
182
|
+
otpLength: 6,
|
|
183
|
+
expiryMinutes: 5,
|
|
184
|
+
centerBoxes: true,
|
|
185
|
+
authMode: 'multi',
|
|
186
|
+
whitelistedForOTP: isPatientWhiteListed,
|
|
187
|
+
facilityBiometricsEnforced,
|
|
188
|
+
patientCRId: crId,
|
|
189
|
+
visitAction: 'end',
|
|
190
|
+
|
|
191
|
+
onRequestOtp: async () => {
|
|
192
|
+
const res = await sendDischargeOtp(crId, consentTokenRef.current);
|
|
193
|
+
if (res.success === false) {
|
|
194
|
+
throw new Error(res.error ?? t('otpRequestFailed', 'Failed to send OTP. Please try again.'));
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
onVerify: async (otp: string) => {
|
|
199
|
+
const res = await submitVisitClaim({ visitUuid, otp, dischargeReason, invoiceNumber });
|
|
200
|
+
showSnackbar({
|
|
201
|
+
isLowContrast: true,
|
|
202
|
+
kind: res?.success === true ? 'success' : 'error',
|
|
203
|
+
title: t('visitEnded', 'Visit ended'),
|
|
204
|
+
subtitle: res?.message ?? res?.error ?? '',
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
onVerificationSuccess: () => {
|
|
209
|
+
settle(() => {
|
|
210
|
+
dispose();
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
onCleanup: () => {},
|
|
215
|
+
|
|
216
|
+
onStartBiometric: buildBiometricStarter(crId),
|
|
217
|
+
onCheckBiometricStatus: checkBiometricAuthorizationStatus,
|
|
218
|
+
|
|
219
|
+
onBiometricSuccess: async (result: { authorization_code: string; consent_token: string; guid: string }) => {
|
|
220
|
+
const res = await submitVisitClaim({
|
|
221
|
+
visitUuid,
|
|
222
|
+
dischargeAuthGuid: result.guid,
|
|
223
|
+
dischargeReason,
|
|
224
|
+
invoiceNumber,
|
|
225
|
+
});
|
|
226
|
+
showSnackbar({
|
|
227
|
+
isLowContrast: true,
|
|
228
|
+
kind: res?.success === true ? 'success' : 'error',
|
|
229
|
+
title: t('visitEnded', 'Visit ended'),
|
|
230
|
+
subtitle: res?.message ?? res?.error ?? '',
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
onBiometricCancel: async (biometricConsentToken: string | null) => {
|
|
235
|
+
if (biometricConsentToken) {
|
|
236
|
+
try {
|
|
237
|
+
await rejectBiometricAuthorization(biometricConsentToken);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.warn('Failed to reject biometric auth:', err);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
whitelistReasons,
|
|
245
|
+
onSubmitWhitelist: handleSubmitWhitelist,
|
|
246
|
+
onCheckWhitelistStatus: handleCheckWhitelistStatus,
|
|
247
|
+
});
|
|
248
|
+
}, 300);
|
|
249
|
+
}, [
|
|
250
|
+
reason,
|
|
251
|
+
patientCRId,
|
|
252
|
+
activeVisit,
|
|
253
|
+
closeModal,
|
|
254
|
+
phoneNumber,
|
|
255
|
+
isPatientWhiteListed,
|
|
256
|
+
facilityBiometricsEnforced,
|
|
257
|
+
buildBiometricStarter,
|
|
258
|
+
whitelistReasons,
|
|
259
|
+
handleSubmitWhitelist,
|
|
260
|
+
handleCheckWhitelistStatus,
|
|
261
|
+
t,
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
if (step === 'discharge-reason') {
|
|
265
|
+
return (
|
|
266
|
+
<div>
|
|
267
|
+
<ModalHeader closeModal={closeModal} title={t('endVisitTitle', 'End Visit')} />
|
|
268
|
+
<ModalBody className={styles.dischargeModalBody}>
|
|
269
|
+
<p className={styles.bodyShort02}>
|
|
270
|
+
{t(
|
|
271
|
+
'dischargeReasonIntro',
|
|
272
|
+
'Select the reason for ending this visit. You will then be asked to verify your identity.',
|
|
273
|
+
)}
|
|
274
|
+
</p>
|
|
275
|
+
<Dropdown
|
|
276
|
+
id="discharge-reason-dropdown"
|
|
277
|
+
titleText={t('dischargeReason', 'Discharge reason')}
|
|
278
|
+
label={t('selectDischargeReason', 'Select a reason')}
|
|
279
|
+
items={[...DISCHARGE_REASONS]}
|
|
280
|
+
itemToString={(item) => (item ? item.label : '')}
|
|
281
|
+
selectedItem={DISCHARGE_REASONS.find((r) => r.id === reason) ?? null}
|
|
282
|
+
onChange={({ selectedItem }) => setReason(selectedItem?.id ?? '')}
|
|
283
|
+
/>
|
|
284
|
+
</ModalBody>
|
|
285
|
+
<ModalFooter>
|
|
286
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
287
|
+
{t('cancel', 'Cancel')}
|
|
288
|
+
</Button>
|
|
289
|
+
<Button kind="primary" disabled={!reason} onClick={handleContinue}>
|
|
290
|
+
{t('continue', 'Continue')}
|
|
291
|
+
</Button>
|
|
292
|
+
</ModalFooter>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div>
|
|
299
|
+
<ModalHeader
|
|
300
|
+
closeModal={closeModal}
|
|
301
|
+
title={t('endActiveVisitConfirmation', 'Are you sure you want to end this active visit?')}
|
|
302
|
+
/>
|
|
303
|
+
<ModalBody>
|
|
304
|
+
<p className={styles.bodyShort02}>
|
|
305
|
+
{t('youWillProcessTheClaim', 'You will process the insurance claim for this visit.')}
|
|
306
|
+
</p>
|
|
307
|
+
</ModalBody>
|
|
308
|
+
<ModalFooter>
|
|
309
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
310
|
+
{t('cancel', 'Cancel')}
|
|
311
|
+
</Button>
|
|
312
|
+
<Button kind="danger" disabled={!activeVisit || isProcessingClaim} onClick={handleEndVisit}>
|
|
313
|
+
{t('endVisit_title', 'End Visit')}
|
|
314
|
+
</Button>
|
|
315
|
+
</ModalFooter>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export default EndVisitDialog;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
export type ProcessVisitResponse = {
|
|
4
|
+
service_type: string;
|
|
5
|
+
consentToken?: string;
|
|
6
|
+
invoiceNumber?: string | null;
|
|
7
|
+
interventions?: string[];
|
|
8
|
+
message?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SubmitVisitResponse = {
|
|
13
|
+
success?: boolean;
|
|
14
|
+
message?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function processVisitInsuranceClaim(visitUuid: string) {
|
|
19
|
+
return openmrsFetch<ProcessVisitResponse>(`${restBaseUrl}/insuranceclaims/phc/processVisit?visitUuid=${visitUuid}`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function sendDischargeOtp(
|
|
26
|
+
patientCRId: string,
|
|
27
|
+
consentToken: string,
|
|
28
|
+
): Promise<{ success?: boolean; error?: string }> {
|
|
29
|
+
const params = new URLSearchParams({ patientId: patientCRId, consentToken });
|
|
30
|
+
const response = await openmrsFetch<{ success?: boolean; error?: string }>(
|
|
31
|
+
`${restBaseUrl}/insuranceclaims/phc/sendDischargeOtp?${params.toString()}`,
|
|
32
|
+
{ method: 'POST' },
|
|
33
|
+
);
|
|
34
|
+
return response.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function submitVisitClaim(params: {
|
|
38
|
+
visitUuid: string;
|
|
39
|
+
otp?: string;
|
|
40
|
+
dischargeReason?: string;
|
|
41
|
+
dischargeAuthGuid?: string;
|
|
42
|
+
invoiceNumber?: string;
|
|
43
|
+
}): Promise<SubmitVisitResponse> {
|
|
44
|
+
const query = new URLSearchParams({ visitUuid: params.visitUuid, invoiceNumber: params.invoiceNumber ?? '' });
|
|
45
|
+
if (params.otp) {
|
|
46
|
+
query.set('otp', params.otp);
|
|
47
|
+
}
|
|
48
|
+
if (params.dischargeReason) {
|
|
49
|
+
query.set('dischargeReason', params.dischargeReason);
|
|
50
|
+
}
|
|
51
|
+
if (params.dischargeAuthGuid) {
|
|
52
|
+
query.set('dischargeAuthGuid', params.dischargeAuthGuid);
|
|
53
|
+
}
|
|
54
|
+
const response = await openmrsFetch<SubmitVisitResponse>(
|
|
55
|
+
`${restBaseUrl}/insuranceclaims/phc/submitVisit?${query.toString()}`,
|
|
56
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' } },
|
|
57
|
+
);
|
|
58
|
+
return response.data;
|
|
59
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, useConfig, usePatient, type FetchResponse } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { type ExpressWorkflowConfig } from '../../../config-schema';
|
|
4
|
+
import {
|
|
5
|
+
AuthorizingDeviceOS,
|
|
6
|
+
BiometricAuthorizationStatus,
|
|
7
|
+
BiometricAuthorizeRequest,
|
|
8
|
+
BiometricAuthorizeResponse,
|
|
9
|
+
BiometricConfigResponse,
|
|
10
|
+
OTPResponse,
|
|
11
|
+
ProviderAttributesResponse,
|
|
12
|
+
WhitelistReason,
|
|
13
|
+
WhitelistStatusPoll,
|
|
14
|
+
WhitelistSubmitResponse,
|
|
15
|
+
} from '../type';
|
|
16
|
+
|
|
17
|
+
const virtualClaimBaseUrl = `${restBaseUrl}/virtualclaims`;
|
|
18
|
+
|
|
19
|
+
export const usePatientPhone = (patientUuid: string): string => {
|
|
20
|
+
const { data } = useSWR<
|
|
21
|
+
FetchResponse<{ person: { attributes: Array<{ attributeType: { display: string }; value: string }> } }>
|
|
22
|
+
>(
|
|
23
|
+
patientUuid
|
|
24
|
+
? `${restBaseUrl}/patient/${patientUuid}?v=custom:(person:(attributes:(attributeType:(display),value)))`
|
|
25
|
+
: null,
|
|
26
|
+
openmrsFetch,
|
|
27
|
+
);
|
|
28
|
+
return (
|
|
29
|
+
data?.data?.person?.attributes?.find(
|
|
30
|
+
(attr) =>
|
|
31
|
+
attr.attributeType?.display?.toLowerCase().includes('phone') ||
|
|
32
|
+
attr.attributeType?.display?.toLowerCase().includes('telephone'),
|
|
33
|
+
)?.value ?? ''
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const usePatientCRId = (patientUuid: string): string | null => {
|
|
38
|
+
const { crIdentificationNumberUUID } = useConfig<ExpressWorkflowConfig>();
|
|
39
|
+
const { data } = useSWR<
|
|
40
|
+
FetchResponse<{ identifiers: Array<{ identifierType: { uuid: string }; identifier: string }> }>
|
|
41
|
+
>(
|
|
42
|
+
patientUuid
|
|
43
|
+
? `${restBaseUrl}/patient/${patientUuid}?v=custom:(identifiers:(identifierType:(uuid),identifier))`
|
|
44
|
+
: null,
|
|
45
|
+
openmrsFetch,
|
|
46
|
+
);
|
|
47
|
+
return (
|
|
48
|
+
data?.data?.identifiers?.find((id) => id.identifierType?.uuid === crIdentificationNumberUUID)?.identifier ?? null
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface EligibilityResponse {
|
|
53
|
+
requestIdType: number;
|
|
54
|
+
requestIdNumber: string;
|
|
55
|
+
memberCrNumber: string;
|
|
56
|
+
whitelistedForOTP: boolean;
|
|
57
|
+
facilityBiometricsEnforced: boolean;
|
|
58
|
+
fullName: string;
|
|
59
|
+
statusCode: string;
|
|
60
|
+
statusDesc: string;
|
|
61
|
+
schemes: Array<any>;
|
|
62
|
+
dateOfBirth?: string;
|
|
63
|
+
gender?: string;
|
|
64
|
+
age?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const useSHAEligibility = (patientUuid: string) => {
|
|
68
|
+
const { patient } = usePatient(patientUuid);
|
|
69
|
+
const { nationalIdUUID } = useConfig<ExpressWorkflowConfig>();
|
|
70
|
+
|
|
71
|
+
const nationalId = patient?.identifier?.find((id: any) =>
|
|
72
|
+
id?.type?.coding?.some((c: any) => c?.code === nationalIdUUID),
|
|
73
|
+
)?.value;
|
|
74
|
+
|
|
75
|
+
const url = nationalId
|
|
76
|
+
? `${virtualClaimBaseUrl}/eligibility?identification_number=${encodeURIComponent(
|
|
77
|
+
nationalId,
|
|
78
|
+
)}&identification_type=${encodeURIComponent('National ID')}`
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
const { data, error, isLoading, mutate } = useSWR<{ data: EligibilityResponse }>(url, openmrsFetch, {
|
|
82
|
+
errorRetryCount: 0,
|
|
83
|
+
revalidateOnFocus: false,
|
|
84
|
+
dedupingInterval: 60_000,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
data: data?.data,
|
|
89
|
+
isPatientWhiteListed: data?.data?.whitelistedForOTP ?? false,
|
|
90
|
+
facilityBiometricsEnforced: data?.data?.facilityBiometricsEnforced ?? false,
|
|
91
|
+
isLoading,
|
|
92
|
+
error,
|
|
93
|
+
mutate,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const useProviderNationalId = (providerUuid: string) => {
|
|
98
|
+
const { providerNationalIdUuid } = useConfig<ExpressWorkflowConfig>();
|
|
99
|
+
const customRep = 'custom:(attributes:(attributeType:(uuid,display),value))';
|
|
100
|
+
const url = providerUuid ? `${restBaseUrl}/provider/${providerUuid}?v=${customRep}` : null;
|
|
101
|
+
const { data, isLoading, error } = useSWR<FetchResponse<ProviderAttributesResponse>>(url, openmrsFetch);
|
|
102
|
+
const providerNationalid = data?.data?.attributes?.find(
|
|
103
|
+
(attr) => attr.attributeType.uuid === providerNationalIdUuid,
|
|
104
|
+
)?.value;
|
|
105
|
+
return { providerNationalid, isLoading, error };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const useBiometricConfig = () => {
|
|
109
|
+
const { data, error, isLoading } = useSWR<FetchResponse<BiometricConfigResponse>>(
|
|
110
|
+
`${virtualClaimBaseUrl}/biometric-config`,
|
|
111
|
+
openmrsFetch,
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
agentUrl: data?.data?.agent_url,
|
|
115
|
+
agentTimeoutMs: data?.data?.agent_timeout_ms,
|
|
116
|
+
isLoading,
|
|
117
|
+
error,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export interface BiometricAgentStatus {
|
|
122
|
+
devices: any[];
|
|
123
|
+
card_readers: any[];
|
|
124
|
+
isAuthed: boolean;
|
|
125
|
+
workstationID: string;
|
|
126
|
+
version: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const useBiometricAgentStatus = (agentUrl: string | undefined) => {
|
|
130
|
+
const { data, error, isLoading } = useSWR<BiometricAgentStatus>(
|
|
131
|
+
agentUrl ?? null,
|
|
132
|
+
async (url: string) => {
|
|
133
|
+
const res = await fetch(url, { method: 'GET', headers: { Accept: 'application/json' } });
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
throw new Error(`Biometric agent returned ${res.status}`);
|
|
136
|
+
}
|
|
137
|
+
return res.json();
|
|
138
|
+
},
|
|
139
|
+
{ revalidateOnFocus: false, shouldRetryOnError: false, dedupingInterval: 30_000 },
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
workstationId: data?.workstationID,
|
|
143
|
+
isAuthed: data?.isAuthed ?? false,
|
|
144
|
+
isLoading,
|
|
145
|
+
error,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const useOtpWhitelistReasons = () => {
|
|
150
|
+
const { data, isLoading, error } = useSWR<FetchResponse<{ count: number; reasons: Array<WhitelistReason> }>>(
|
|
151
|
+
`${virtualClaimBaseUrl}/otp-whitelist-reasons`,
|
|
152
|
+
openmrsFetch,
|
|
153
|
+
{
|
|
154
|
+
revalidateOnFocus: false,
|
|
155
|
+
dedupingInterval: 5 * 60_000,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
return { reasons: data?.data?.reasons ?? [], isLoading, error };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const detectAuthorizingDeviceOS = (): AuthorizingDeviceOS => {
|
|
162
|
+
if (typeof navigator === 'undefined') {
|
|
163
|
+
return 'windows';
|
|
164
|
+
}
|
|
165
|
+
const uaDataPlatform = (
|
|
166
|
+
navigator as Navigator & { userAgentData?: { platform?: string } }
|
|
167
|
+
).userAgentData?.platform?.toLowerCase();
|
|
168
|
+
if (uaDataPlatform?.includes('android')) {
|
|
169
|
+
return 'android';
|
|
170
|
+
}
|
|
171
|
+
if (uaDataPlatform?.includes('windows')) {
|
|
172
|
+
return 'windows';
|
|
173
|
+
}
|
|
174
|
+
const userAgent = navigator.userAgent?.toLowerCase() ?? '';
|
|
175
|
+
if (userAgent.includes('android')) {
|
|
176
|
+
return 'android';
|
|
177
|
+
}
|
|
178
|
+
return 'windows';
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const sendSHAOtp = async (
|
|
182
|
+
patientCRId: string,
|
|
183
|
+
options: string[] | { consentToken: string },
|
|
184
|
+
): Promise<OTPResponse> => {
|
|
185
|
+
const body = Array.isArray(options)
|
|
186
|
+
? { patient_id: patientCRId, intervention_codes: options }
|
|
187
|
+
: { patient_id: patientCRId, consent_token: options.consentToken };
|
|
188
|
+
|
|
189
|
+
const response = await openmrsFetch(`${virtualClaimBaseUrl}/otp`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body,
|
|
193
|
+
});
|
|
194
|
+
return response.data;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const createSHABiometricAuthorize = async (
|
|
198
|
+
payload: BiometricAuthorizeRequest,
|
|
199
|
+
): Promise<BiometricAuthorizeResponse> => {
|
|
200
|
+
const response = await openmrsFetch(`${virtualClaimBaseUrl}/biometric-authorize`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: payload,
|
|
204
|
+
});
|
|
205
|
+
return response.data;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const checkBiometricAuthorizationStatus = async (token: string): Promise<BiometricAuthorizationStatus> => {
|
|
209
|
+
const response = await openmrsFetch<BiometricAuthorizationStatus>(
|
|
210
|
+
`${virtualClaimBaseUrl}/biometric-authorization-status?token=${encodeURIComponent(token)}`,
|
|
211
|
+
);
|
|
212
|
+
return response.data;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const rejectBiometricAuthorization = async (
|
|
216
|
+
token: string,
|
|
217
|
+
): Promise<{ success: boolean; message?: string; error?: string }> => {
|
|
218
|
+
const response = await openmrsFetch(`${virtualClaimBaseUrl}/biometric-reject`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
body: { token },
|
|
222
|
+
});
|
|
223
|
+
return response.data;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const fetchWhitelistStatus = async (beneficiaryCrId: string): Promise<WhitelistStatusPoll> => {
|
|
227
|
+
const response = await openmrsFetch(
|
|
228
|
+
`${virtualClaimBaseUrl}/otp-whitelist-status?beneficiary_cr_id=${encodeURIComponent(beneficiaryCrId)}`,
|
|
229
|
+
);
|
|
230
|
+
return response.data;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Submits a visit insurance claim and ends the visit in a single call.
|
|
235
|
+
* Call this after obtaining OTP or biometric authorisation.
|
|
236
|
+
*
|
|
237
|
+
* @param visitUuid - UUID of the active visit to end.
|
|
238
|
+
* @param otp - OTP supplied by the patient (optional — omit when using biometric auth).
|
|
239
|
+
* @param dischargeReason - Discharge reason code (e.g. RECOVERED, REFERRED, ABSCONDED, OTHER).
|
|
240
|
+
* @param dischargeAuthGuid - GUID returned by the biometric authorisation flow (optional — omit when using OTP).
|
|
241
|
+
*/
|
|
242
|
+
export const submitVisitClaim = async (params: {
|
|
243
|
+
visitUuid: string;
|
|
244
|
+
otp?: string;
|
|
245
|
+
dischargeReason?: string;
|
|
246
|
+
dischargeAuthGuid?: string;
|
|
247
|
+
}): Promise<{ success: boolean; message?: string; error?: string }> => {
|
|
248
|
+
const query = new URLSearchParams({ visitUuid: params.visitUuid });
|
|
249
|
+
if (params.otp) {
|
|
250
|
+
query.set('otp', params.otp);
|
|
251
|
+
}
|
|
252
|
+
if (params.dischargeReason) {
|
|
253
|
+
query.set('dischargeReason', params.dischargeReason);
|
|
254
|
+
}
|
|
255
|
+
if (params.dischargeAuthGuid) {
|
|
256
|
+
query.set('dischargeAuthGuid', params.dischargeAuthGuid);
|
|
257
|
+
}
|
|
258
|
+
const response = await openmrsFetch(`${restBaseUrl}/insuranceclaims/phc/submitVisit?${query.toString()}`, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
});
|
|
262
|
+
return response.data;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const submitOtpWhitelist = async (params: {
|
|
266
|
+
beneficiaryCrId: string;
|
|
267
|
+
reasonType: string;
|
|
268
|
+
reason: string;
|
|
269
|
+
biometricAttempts: number;
|
|
270
|
+
attachment: File | null;
|
|
271
|
+
}): Promise<WhitelistSubmitResponse> => {
|
|
272
|
+
const formData = new FormData();
|
|
273
|
+
formData.append('beneficiary_cr_id', params.beneficiaryCrId);
|
|
274
|
+
formData.append('reason_type', params.reasonType);
|
|
275
|
+
formData.append('reason', params.reason);
|
|
276
|
+
formData.append('biometric_attempts', String(params.biometricAttempts));
|
|
277
|
+
if (params.attachment && params.attachment.size > 0) {
|
|
278
|
+
formData.append('attachment', params.attachment, params.attachment.name);
|
|
279
|
+
}
|
|
280
|
+
const response = await openmrsFetch(`${virtualClaimBaseUrl}/otp-whitelists`, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
body: formData,
|
|
283
|
+
});
|
|
284
|
+
return response.data;
|
|
285
|
+
};
|