@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.
Files changed (124) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/126.js +1 -0
  3. package/dist/126.js.map +1 -0
  4. package/dist/2024.js +1 -1
  5. package/dist/2024.js.map +1 -1
  6. package/dist/2225.js +1 -1
  7. package/dist/2225.js.map +1 -1
  8. package/dist/2386.js +1 -0
  9. package/dist/2386.js.map +1 -0
  10. package/dist/2438.js +1 -1
  11. package/dist/2438.js.map +1 -1
  12. package/dist/4504.js +1 -0
  13. package/dist/4504.js.map +1 -0
  14. package/dist/4670.js +1 -0
  15. package/dist/4670.js.map +1 -0
  16. package/dist/4682.js +1 -1
  17. package/dist/4682.js.map +1 -1
  18. package/dist/5047.js +1 -0
  19. package/dist/5047.js.map +1 -0
  20. package/dist/586.js.map +1 -1
  21. package/dist/5986.js +1 -1
  22. package/dist/5986.js.map +1 -1
  23. package/dist/6075.js +1 -0
  24. package/dist/6075.js.map +1 -0
  25. package/dist/6328.js +1 -1
  26. package/dist/6328.js.map +1 -1
  27. package/dist/64.js +1 -0
  28. package/dist/64.js.map +1 -0
  29. package/dist/6456.js +1 -1
  30. package/dist/6720.js +1 -0
  31. package/dist/6720.js.map +1 -0
  32. package/dist/7607.js +1 -1
  33. package/dist/8566.js +1 -0
  34. package/dist/8566.js.map +1 -0
  35. package/dist/8727.js +1 -1
  36. package/dist/8799.js +1 -0
  37. package/dist/8799.js.map +1 -0
  38. package/dist/8911.js +1 -1
  39. package/dist/8911.js.map +1 -1
  40. package/dist/9157.js +1 -1
  41. package/dist/9157.js.map +1 -1
  42. package/dist/9161.js +1 -0
  43. package/dist/9161.js.map +1 -0
  44. package/dist/9182.js +1 -1
  45. package/dist/9343.js +1 -0
  46. package/dist/9343.js.map +1 -0
  47. package/dist/9535.js +1 -1
  48. package/dist/9535.js.map +1 -1
  49. package/dist/9834.js +1 -0
  50. package/dist/{4432.js.map → 9834.js.map} +1 -1
  51. package/dist/kenyaemr-esm-express-workflow-app.js +2 -2
  52. package/dist/kenyaemr-esm-express-workflow-app.js.buildmanifest.json +294 -151
  53. package/dist/kenyaemr-esm-express-workflow-app.js.map +1 -1
  54. package/dist/main.js +2 -2
  55. package/dist/main.js.map +1 -1
  56. package/dist/routes.json +1 -1
  57. package/package.json +1 -1
  58. package/src/components/admissions/index.tsx +0 -1
  59. package/src/components/anaesthetic/anaesthetic.component.tsx +10 -4
  60. package/src/components/anaesthetic/forms/interoperative-record-drug-given-form.component.tsx +2 -26
  61. package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph-wrapper.component.tsx +66 -25
  62. package/src/components/anaesthetic/resources/anaesthetic.resource.ts +2 -2
  63. package/src/components/consultation/clinical-encounter/clinical-encounter.component.tsx +4 -20
  64. package/src/components/consultation/index.ts +1 -1
  65. package/src/components/consultation/tabs/index.ts +67 -0
  66. package/src/components/facility-dashboard/components/header/home-header.component.tsx +8 -13
  67. package/src/components/laboratory/index.ts +11 -0
  68. package/src/components/laboratory/lab-table.component.tsx +25 -12
  69. package/src/components/laboratory/laboratory-tabs.component.tsx +3 -17
  70. package/src/components/pharmacy/index.ts +1 -1
  71. package/src/components/pharmacy/pharmacy-tabs.component.tsx +4 -20
  72. package/src/components/pharmacy/tabs/index.ts +21 -0
  73. package/src/components/procedures/index.ts +16 -0
  74. package/src/components/procedures/procedures-table.component.tsx +33 -15
  75. package/src/components/procedures/procedures-tabs.component.tsx +3 -22
  76. package/src/components/radiology-and-imaging/index.ts +11 -0
  77. package/src/components/radiology-and-imaging/radiology-and-imaging-table.component.tsx +35 -15
  78. package/src/components/radiology-and-imaging/radiology-and-imaging.component.tsx +3 -19
  79. package/src/components/registration/card/Local-card/local-card.component.tsx +3 -3
  80. package/src/components/registration/dependants/dependants.component.tsx +6 -1
  81. package/src/components/registration/end-visit-modal/end-visit-dialog.modal.tsx +320 -0
  82. package/src/components/registration/end-visit-modal/end-visit-dialog.scss +12 -0
  83. package/src/components/registration/end-visit-modal/end-visit.resource.tsx +59 -0
  84. package/src/components/registration/end-visit-modal/sha-end-visit.resource.ts +285 -0
  85. package/src/components/registration/start-visit-form/overflow-menu-extension/custom-end-active-visit.extension.tsx +44 -0
  86. package/src/components/registration/start-visit-form/start-visit-workspace/exported-visit-form.workspace.tsx +16 -11
  87. package/src/components/registration/type/index.ts +101 -0
  88. package/src/config-schema.ts +23 -1
  89. package/src/index.ts +36 -17
  90. package/src/routes.json +205 -12
  91. package/src/shared/otp-verification/index.ts +3 -0
  92. package/src/shared/otp-verification/otp-verification.modal.tsx +51 -17
  93. package/src/shared/patient-chart/patient-summary-dashboard/patient-summary-dashboard.component.tsx +5 -73
  94. package/src/shared/tabs/create-tab-extension.tsx +33 -0
  95. package/src/shared/tabs/extension-tabs.component.tsx +51 -33
  96. package/src/shared/utils/index.ts +6 -6
  97. package/translations/am.json +32 -24
  98. package/translations/en.json +23 -16
  99. package/translations/fr.json +23 -16
  100. package/translations/sw.json +23 -21
  101. package/Failed to delete encounter/am.json +0 -4
  102. package/Failed to delete encounter/en.json +0 -4
  103. package/Failed to delete encounter/fr.json +0 -4
  104. package/Failed to delete encounter/sw.json +0 -4
  105. package/dist/2332.js +0 -1
  106. package/dist/2332.js.map +0 -1
  107. package/dist/4432.js +0 -1
  108. package/dist/4666.js +0 -1
  109. package/dist/4666.js.map +0 -1
  110. package/dist/5733.js +0 -1
  111. package/dist/5733.js.map +0 -1
  112. package/dist/644.js +0 -1
  113. package/dist/644.js.map +0 -1
  114. package/dist/655.js +0 -1
  115. package/dist/655.js.map +0 -1
  116. package/dist/70.js +0 -1
  117. package/dist/70.js.map +0 -1
  118. package/dist/9387.js +0 -1
  119. package/dist/9387.js.map +0 -1
  120. package/src/components/admissions/admission-medication-summary.tsx +0 -302
  121. package/src/components/admissions/admissions-dashboard.utils.test.ts +0 -48
  122. package/src/components/admissions/admissions-dashboard.utils.tsx +0 -63
  123. package/src/shared/orders/OrdersTabs.tsx +0 -76
  124. /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,12 @@
1
+ @use '@carbon/type';
2
+ @use '@carbon/colors';
3
+
4
+ .bodyShort02 {
5
+ @include type.type-style('body-compact-02');
6
+ color: colors.$gray-70;
7
+ }
8
+
9
+ .dischargeModalBody {
10
+ overflow: visible;
11
+ min-height: 14rem;
12
+ }
@@ -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
+ };