@kenyaemr/esm-billing-app 5.4.2-pre.2746 → 5.4.2-pre.2748
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/127.js +1 -1
- package/dist/40.js +1 -1
- package/dist/84.js +1 -1
- package/dist/84.js.map +1 -1
- package/dist/916.js +1 -1
- package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +15 -15
- package/dist/main.js +1 -1
- 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 +46 -9
- package/src/config-schema.ts +6 -0
- package/src/hooks/useOTP.ts +212 -149
- package/src/hooks/usePatientIdentifierType.ts +26 -0
- package/src/types/index.ts +17 -0
- package/translations/am.json +4 -1
- package/translations/en.json +3 -0
- package/translations/sw.json +3 -0
package/src/hooks/useOTP.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generates a random OTP of a specified length.
|
|
6
|
-
*/
|
|
7
|
-
export function generateOTP(length = 5) {
|
|
8
|
-
let otpNumbers = '0123456789';
|
|
9
|
-
let OTP = '';
|
|
10
|
-
const len = otpNumbers.length;
|
|
11
|
-
for (let i = 0; i < length; i++) {
|
|
12
|
-
OTP += otpNumbers[Math.floor(Math.random() * len)];
|
|
13
|
-
}
|
|
14
|
-
return OTP;
|
|
15
|
-
}
|
|
2
|
+
import { ClaimSummary } from '../types';
|
|
3
|
+
import { formatKenyanPhoneNumber } from '../invoice/payments/utils';
|
|
16
4
|
|
|
17
5
|
/**
|
|
18
6
|
* Replaces placeholders in a template string with values from a given context.
|
|
@@ -31,35 +19,96 @@ export function parseMessage<T extends Record<string, string | number>>(context:
|
|
|
31
19
|
}
|
|
32
20
|
|
|
33
21
|
/**
|
|
34
|
-
* Builds a URL for sending an SMS message.
|
|
22
|
+
* Builds a URL for sending an SMS message using the KenyaEMR SMS API.
|
|
35
23
|
*/
|
|
36
|
-
function buildSmsUrl(message: string, receiver: string): string {
|
|
24
|
+
function buildSmsUrl(message: string, receiver: string, nationalId: string | null = null): string {
|
|
37
25
|
const encodedMessage = encodeURIComponent(message);
|
|
38
|
-
|
|
26
|
+
const formattedReceiver = formatKenyanPhoneNumber(receiver);
|
|
27
|
+
let url = `${restBaseUrl}/kenyaemr/send-kenyaemr-sms?message=${encodedMessage}&phone=${formattedReceiver}`;
|
|
28
|
+
|
|
29
|
+
if (nationalId?.trim()) {
|
|
30
|
+
url += `&nationalId=${encodeURIComponent(nationalId)}`;
|
|
31
|
+
}
|
|
32
|
+
return url;
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
/**
|
|
42
36
|
* Validates that the required parameters for sending an OTP message are present.
|
|
43
37
|
*/
|
|
44
|
-
function validateOtpInputs(
|
|
45
|
-
if (!
|
|
46
|
-
throw new Error('Missing required parameters:
|
|
38
|
+
function validateOtpInputs(receiver: string, patientName: string): void {
|
|
39
|
+
if (!receiver?.trim() || !patientName?.trim()) {
|
|
40
|
+
throw new Error('Missing required parameters: receiver or patientName');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Verifies the OTP by calling the server's validation endpoint.
|
|
46
|
+
*/
|
|
47
|
+
export async function verifyOtpWithServer(otpId: string, otp: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const url = `${restBaseUrl}/kenyaemr/validate-otp`;
|
|
50
|
+
|
|
51
|
+
const response = await openmrsFetch(url, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
id: otpId,
|
|
58
|
+
otp: otp.trim(),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rawText = await response.text();
|
|
67
|
+
|
|
68
|
+
let parsedResponse;
|
|
69
|
+
try {
|
|
70
|
+
parsedResponse = JSON.parse(rawText);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
throw new Error('Invalid response from server');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let data = parsedResponse;
|
|
76
|
+
if (parsedResponse.response && typeof parsedResponse.response === 'string') {
|
|
77
|
+
try {
|
|
78
|
+
data = JSON.parse(parsedResponse.response);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new Error('Invalid nested response from server');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (data.status === 'success' || data.valid === true) {
|
|
85
|
+
return true;
|
|
86
|
+
} else {
|
|
87
|
+
const errorMessage = data.message || 'Invalid OTP';
|
|
88
|
+
throw new Error(errorMessage);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const errorMessage = error instanceof Error ? error.message : 'OTP verification failed';
|
|
92
|
+
throw new Error(errorMessage);
|
|
47
93
|
}
|
|
48
94
|
}
|
|
49
95
|
|
|
50
96
|
/**
|
|
51
|
-
* Sends an OTP
|
|
97
|
+
* Sends an OTP request to the server with claim details which generates and sends the OTP via SMS.
|
|
98
|
+
* Returns the OTP session ID needed for verification.
|
|
52
99
|
*/
|
|
53
|
-
export async function
|
|
54
|
-
|
|
100
|
+
export async function requestOtpFromServer(
|
|
101
|
+
receiver: string,
|
|
55
102
|
patientName: string,
|
|
56
103
|
claimSummary: ClaimSummary,
|
|
57
104
|
expiryMinutes: number = 5,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
validateOtpInputs(
|
|
105
|
+
nationalId: string | null = null,
|
|
106
|
+
): Promise<{ id: string; message: string }> {
|
|
107
|
+
validateOtpInputs(receiver, patientName);
|
|
61
108
|
|
|
62
|
-
const
|
|
109
|
+
const formattedPhone = formatKenyanPhoneNumber(receiver);
|
|
110
|
+
|
|
111
|
+
const context = {
|
|
63
112
|
patientName: patientName,
|
|
64
113
|
claimAmount: `KES ${claimSummary.totalAmount.toLocaleString()}`,
|
|
65
114
|
servicesSummary:
|
|
@@ -68,7 +117,7 @@ export async function sendOtp(
|
|
|
68
117
|
endDate: claimSummary.endDate,
|
|
69
118
|
facility: claimSummary.facility,
|
|
70
119
|
expiryTime: expiryMinutes,
|
|
71
|
-
otp:
|
|
120
|
+
otp: '{{OTP}}',
|
|
72
121
|
};
|
|
73
122
|
|
|
74
123
|
const claimConsentTemplate =
|
|
@@ -79,8 +128,8 @@ export async function sendOtp(
|
|
|
79
128
|
'Your OTP for consent is {{otp}} (valid {{expiryTime}} mins). ';
|
|
80
129
|
|
|
81
130
|
try {
|
|
82
|
-
const message = parseMessage(
|
|
83
|
-
const url = buildSmsUrl(message,
|
|
131
|
+
const message = parseMessage(context, claimConsentTemplate);
|
|
132
|
+
const url = buildSmsUrl(message, formattedPhone, nationalId);
|
|
84
133
|
|
|
85
134
|
const response = await openmrsFetch(url, {
|
|
86
135
|
method: 'POST',
|
|
@@ -91,162 +140,211 @@ export async function sendOtp(
|
|
|
91
140
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
92
141
|
}
|
|
93
142
|
|
|
94
|
-
|
|
143
|
+
let responseText = await response.text();
|
|
144
|
+
|
|
145
|
+
let unwrappedText: string;
|
|
146
|
+
try {
|
|
147
|
+
unwrappedText = JSON.parse(responseText);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
unwrappedText = responseText;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const jsonMatch = unwrappedText.match(/\{.*\}/);
|
|
153
|
+
if (!jsonMatch) {
|
|
154
|
+
throw new Error('No valid JSON in server response');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let data;
|
|
158
|
+
try {
|
|
159
|
+
data = JSON.parse(jsonMatch[0]);
|
|
160
|
+
} catch (parseError) {
|
|
161
|
+
throw new Error('Invalid JSON response from server');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (data.status === 'success' && data.id) {
|
|
165
|
+
return {
|
|
166
|
+
id: data.id,
|
|
167
|
+
message: data.message || 'OTP sent successfully',
|
|
168
|
+
};
|
|
169
|
+
} else {
|
|
170
|
+
const errorMessage = data.message || 'Failed to send OTP - no ID returned';
|
|
171
|
+
throw new Error(errorMessage);
|
|
172
|
+
}
|
|
95
173
|
} catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
174
|
+
if (error instanceof Error) {
|
|
175
|
+
if (error.message.startsWith('Failed to send OTP')) {
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`Failed to send OTP: ${error.message}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error('Failed to send OTP: Unknown error occurred');
|
|
98
182
|
}
|
|
99
183
|
}
|
|
100
184
|
|
|
101
185
|
export class OTPManager {
|
|
102
|
-
private
|
|
186
|
+
private otpSessions: Map<
|
|
187
|
+
string,
|
|
188
|
+
{
|
|
189
|
+
otpId: string;
|
|
190
|
+
timestamp: number;
|
|
191
|
+
attempts: number;
|
|
192
|
+
expiryTime: number;
|
|
193
|
+
nationalId?: string | null;
|
|
194
|
+
phoneNumber: string;
|
|
195
|
+
}
|
|
196
|
+
> = new Map();
|
|
103
197
|
private readonly MAX_ATTEMPTS = 3;
|
|
104
|
-
private readonly DEFAULT_EXPIRY_TIME = 5 * 60 * 1000;
|
|
105
198
|
|
|
106
199
|
async requestOTP(
|
|
107
200
|
phoneNumber: string,
|
|
108
201
|
patientName: string,
|
|
109
202
|
claimSummary: ClaimSummary,
|
|
110
|
-
expiryMinutes: number =
|
|
203
|
+
expiryMinutes: number = 5,
|
|
204
|
+
nationalId: string | null = null,
|
|
111
205
|
): Promise<void> {
|
|
112
|
-
const otp = generateOTP(5);
|
|
113
206
|
const expiryTime = expiryMinutes * 60 * 1000;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
207
|
+
const formattedPhone = formatKenyanPhoneNumber(phoneNumber);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const { id, message } = await requestOtpFromServer(
|
|
211
|
+
formattedPhone,
|
|
212
|
+
patientName,
|
|
213
|
+
claimSummary,
|
|
214
|
+
expiryMinutes,
|
|
215
|
+
nationalId,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const sessionData = {
|
|
219
|
+
otpId: id,
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
attempts: 0,
|
|
222
|
+
expiryTime,
|
|
223
|
+
nationalId,
|
|
224
|
+
phoneNumber: formattedPhone,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
this.otpSessions.set(formattedPhone, sessionData);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
127
231
|
}
|
|
128
232
|
|
|
129
233
|
async verifyOTP(phoneNumber: string, inputOtp: string): Promise<boolean> {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const storedData = this.otpStore.get(normalizedPhone);
|
|
234
|
+
const formattedPhone = formatKenyanPhoneNumber(phoneNumber);
|
|
235
|
+
const sessionData = this.otpSessions.get(formattedPhone);
|
|
133
236
|
|
|
134
|
-
if (!
|
|
135
|
-
throw new Error('No OTP found for this phone number. Please request a new OTP.');
|
|
237
|
+
if (!sessionData) {
|
|
238
|
+
throw new Error('No OTP session found for this phone number. Please request a new OTP.');
|
|
136
239
|
}
|
|
137
240
|
|
|
138
|
-
if (Date.now() -
|
|
139
|
-
this.
|
|
241
|
+
if (Date.now() - sessionData.timestamp > sessionData.expiryTime) {
|
|
242
|
+
this.otpSessions.delete(formattedPhone);
|
|
140
243
|
throw new Error('OTP has expired. Please request a new OTP.');
|
|
141
244
|
}
|
|
142
245
|
|
|
143
|
-
|
|
246
|
+
sessionData.attempts++;
|
|
144
247
|
|
|
145
|
-
if (
|
|
146
|
-
this.
|
|
248
|
+
if (sessionData.attempts > this.MAX_ATTEMPTS) {
|
|
249
|
+
this.otpSessions.delete(formattedPhone);
|
|
147
250
|
throw new Error('Maximum OTP attempts exceeded. Please request a new OTP.');
|
|
148
251
|
}
|
|
149
252
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
253
|
+
try {
|
|
254
|
+
const isValid = await verifyOtpWithServer(sessionData.otpId, inputOtp);
|
|
255
|
+
|
|
256
|
+
if (isValid) {
|
|
257
|
+
this.otpSessions.delete(formattedPhone);
|
|
258
|
+
return true;
|
|
259
|
+
} else {
|
|
260
|
+
this.otpSessions.set(formattedPhone, sessionData);
|
|
261
|
+
throw new Error(`Invalid OTP. ${this.MAX_ATTEMPTS - sessionData.attempts} attempts remaining.`);
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.otpSessions.set(formattedPhone, sessionData);
|
|
265
|
+
|
|
266
|
+
const errorMessage = error instanceof Error ? error.message : 'Invalid OTP';
|
|
267
|
+
const remainingAttempts = this.MAX_ATTEMPTS - sessionData.attempts;
|
|
268
|
+
|
|
269
|
+
if (remainingAttempts > 0) {
|
|
270
|
+
throw new Error(`${errorMessage} ${remainingAttempts} attempts remaining.`);
|
|
271
|
+
} else {
|
|
272
|
+
this.otpSessions.delete(formattedPhone);
|
|
273
|
+
throw new Error('Maximum OTP attempts exceeded. Please request a new OTP.');
|
|
274
|
+
}
|
|
156
275
|
}
|
|
157
276
|
}
|
|
158
277
|
|
|
159
278
|
clearOTP(phoneNumber: string): void {
|
|
160
|
-
const
|
|
161
|
-
this.
|
|
279
|
+
const formattedPhone = formatKenyanPhoneNumber(phoneNumber);
|
|
280
|
+
this.otpSessions.delete(formattedPhone);
|
|
162
281
|
}
|
|
163
282
|
|
|
164
283
|
clearAllOTPs(): void {
|
|
165
|
-
this.
|
|
284
|
+
this.otpSessions.clear();
|
|
166
285
|
}
|
|
167
286
|
|
|
168
287
|
cleanupExpiredOTPs(): void {
|
|
169
288
|
const now = Date.now();
|
|
170
|
-
for (const [phoneNumber, data] of this.
|
|
289
|
+
for (const [phoneNumber, data] of this.otpSessions.entries()) {
|
|
171
290
|
if (now - data.timestamp > data.expiryTime) {
|
|
172
|
-
this.
|
|
291
|
+
this.otpSessions.delete(phoneNumber);
|
|
173
292
|
}
|
|
174
293
|
}
|
|
175
294
|
}
|
|
176
295
|
|
|
177
296
|
hasValidOTP(phoneNumber: string): boolean {
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
297
|
+
const formattedPhone = formatKenyanPhoneNumber(phoneNumber);
|
|
298
|
+
const sessionData = this.otpSessions.get(formattedPhone);
|
|
299
|
+
if (!sessionData) {
|
|
181
300
|
return false;
|
|
182
301
|
}
|
|
183
302
|
|
|
184
|
-
return Date.now() -
|
|
303
|
+
return Date.now() - sessionData.timestamp <= sessionData.expiryTime;
|
|
185
304
|
}
|
|
186
305
|
|
|
187
306
|
getRemainingTimeMinutes(phoneNumber: string): number {
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
if (!
|
|
307
|
+
const formattedPhone = formatKenyanPhoneNumber(phoneNumber);
|
|
308
|
+
const sessionData = this.otpSessions.get(formattedPhone);
|
|
309
|
+
if (!sessionData) {
|
|
191
310
|
return 0;
|
|
192
311
|
}
|
|
193
312
|
|
|
194
|
-
const elapsed = Date.now() -
|
|
195
|
-
const remaining = Math.max(0,
|
|
313
|
+
const elapsed = Date.now() - sessionData.timestamp;
|
|
314
|
+
const remaining = Math.max(0, sessionData.expiryTime - elapsed);
|
|
196
315
|
return Math.ceil(remaining / (60 * 1000));
|
|
197
316
|
}
|
|
198
317
|
|
|
199
318
|
getRemainingAttempts(phoneNumber: string): number {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
319
|
+
const formattedPhone = formatKenyanPhoneNumber(phoneNumber);
|
|
320
|
+
const sessionData = this.otpSessions.get(formattedPhone);
|
|
321
|
+
if (!sessionData) {
|
|
203
322
|
return 0;
|
|
204
323
|
}
|
|
205
324
|
|
|
206
|
-
return Math.max(0, this.MAX_ATTEMPTS -
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
getStoredOTP(phoneNumber: string): string | null {
|
|
210
|
-
const normalizedPhone = this.normalizePhoneNumber(phoneNumber);
|
|
211
|
-
const storedData = this.otpStore.get(normalizedPhone);
|
|
212
|
-
return storedData ? storedData.otp : null;
|
|
325
|
+
return Math.max(0, this.MAX_ATTEMPTS - sessionData.attempts);
|
|
213
326
|
}
|
|
214
327
|
|
|
215
328
|
transferOTP(oldPhoneNumber: string, newPhoneNumber: string): boolean {
|
|
216
|
-
const
|
|
217
|
-
const
|
|
329
|
+
const oldFormatted = formatKenyanPhoneNumber(oldPhoneNumber);
|
|
330
|
+
const newFormatted = formatKenyanPhoneNumber(newPhoneNumber);
|
|
218
331
|
|
|
219
|
-
if (
|
|
332
|
+
if (oldFormatted === newFormatted) {
|
|
220
333
|
return true;
|
|
221
334
|
}
|
|
222
335
|
|
|
223
|
-
const
|
|
224
|
-
if (!
|
|
336
|
+
const sessionData = this.otpSessions.get(oldFormatted);
|
|
337
|
+
if (!sessionData) {
|
|
225
338
|
return false;
|
|
226
339
|
}
|
|
227
340
|
|
|
228
|
-
this.
|
|
229
|
-
this.
|
|
341
|
+
this.otpSessions.set(newFormatted, { ...sessionData, phoneNumber: newFormatted });
|
|
342
|
+
this.otpSessions.delete(oldFormatted);
|
|
230
343
|
|
|
231
344
|
return true;
|
|
232
345
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!phoneNumber) {
|
|
236
|
-
return '';
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
let normalized = phoneNumber.replace(/\D/g, '');
|
|
240
|
-
|
|
241
|
-
if (normalized.startsWith('254') && normalized.length === 12) {
|
|
242
|
-
return normalized;
|
|
243
|
-
} else if (normalized.startsWith('0') && normalized.length === 10) {
|
|
244
|
-
return '254' + normalized.substring(1);
|
|
245
|
-
} else if (normalized.length === 9) {
|
|
246
|
-
return '254' + normalized;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return normalized;
|
|
346
|
+
formatPhone(phoneNumber: string): string {
|
|
347
|
+
return formatKenyanPhoneNumber(phoneNumber);
|
|
250
348
|
}
|
|
251
349
|
}
|
|
252
350
|
|
|
@@ -255,38 +353,3 @@ export const otpManager = new OTPManager();
|
|
|
255
353
|
setInterval(() => {
|
|
256
354
|
otpManager.cleanupExpiredOTPs();
|
|
257
355
|
}, 2 * 60 * 1000);
|
|
258
|
-
|
|
259
|
-
export function createOtpHandlers(patientName: string, expiryMinutes: number) {
|
|
260
|
-
return {
|
|
261
|
-
onRequestOtp: async (phone: string, claimSummary: ClaimSummary): Promise<void> => {
|
|
262
|
-
await otpManager.requestOTP(phone, patientName, claimSummary, expiryMinutes);
|
|
263
|
-
},
|
|
264
|
-
onVerify: async (otp: string, phoneNumber: string): Promise<void> => {
|
|
265
|
-
const isValid = await otpManager.verifyOTP(phoneNumber, otp);
|
|
266
|
-
if (!isValid) {
|
|
267
|
-
throw new Error('OTP verification failed');
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
hasValidOTP: (phone: string): boolean => {
|
|
271
|
-
return otpManager.hasValidOTP(phone);
|
|
272
|
-
},
|
|
273
|
-
getRemainingTime: (phone: string): number => {
|
|
274
|
-
return otpManager.getRemainingTimeMinutes(phone);
|
|
275
|
-
},
|
|
276
|
-
getRemainingAttempts: (phone: string): number => {
|
|
277
|
-
return otpManager.getRemainingAttempts(phone);
|
|
278
|
-
},
|
|
279
|
-
clearOTP: (phone: string): void => {
|
|
280
|
-
otpManager.clearOTP(phone);
|
|
281
|
-
},
|
|
282
|
-
clearAllOTPs: (): void => {
|
|
283
|
-
otpManager.clearAllOTPs();
|
|
284
|
-
},
|
|
285
|
-
getStoredOTP: (phone: string): string | null => {
|
|
286
|
-
return otpManager.getStoredOTP(phone);
|
|
287
|
-
},
|
|
288
|
-
transferOTP: (oldPhone: string, newPhone: string): boolean => {
|
|
289
|
-
return otpManager.transferOTP(oldPhone, newPhone);
|
|
290
|
-
},
|
|
291
|
-
};
|
|
292
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import { BillingConfig } from '../config-schema';
|
|
3
|
+
import { type PatientIdentifierResponse } from '../types';
|
|
4
|
+
import useSWR from 'swr';
|
|
5
|
+
|
|
6
|
+
export const usePatientIdentifier = (patientUuid: string) => {
|
|
7
|
+
const url = `${restBaseUrl}/patient/${patientUuid}/identifier?v=custom:(uuid,identifier,identifierType:(uuid,required,name),preferred)`;
|
|
8
|
+
const { nationalIdIdentifierTypeUUID } = useConfig<BillingConfig>();
|
|
9
|
+
|
|
10
|
+
const { data, isLoading, error } = useSWR<{ data: PatientIdentifierResponse }>(
|
|
11
|
+
patientUuid ? url : null,
|
|
12
|
+
openmrsFetch,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const nationalIdIdentifier = data?.data?.results?.find(
|
|
16
|
+
(identifier) => identifier.identifierType?.uuid === nationalIdIdentifierTypeUUID,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
identifier: nationalIdIdentifier,
|
|
21
|
+
nationalId: nationalIdIdentifier?.identifier || null,
|
|
22
|
+
isLoading,
|
|
23
|
+
error,
|
|
24
|
+
allIdentifiers: data?.data?.results,
|
|
25
|
+
};
|
|
26
|
+
};
|
package/src/types/index.ts
CHANGED
|
@@ -672,3 +672,20 @@ export interface DashboardConfig {
|
|
|
672
672
|
slot: string;
|
|
673
673
|
title: string;
|
|
674
674
|
}
|
|
675
|
+
|
|
676
|
+
interface IdentifierType {
|
|
677
|
+
uuid: string;
|
|
678
|
+
required: boolean;
|
|
679
|
+
name: string;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
interface PatientIdentifier {
|
|
683
|
+
uuid: string;
|
|
684
|
+
identifier: string;
|
|
685
|
+
identifierType: IdentifierType;
|
|
686
|
+
preferred: boolean;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export interface PatientIdentifierResponse {
|
|
690
|
+
results: Array<PatientIdentifier>;
|
|
691
|
+
}
|
package/translations/am.json
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"claimsItems": "የጥያቄ ዕቃዎች",
|
|
83
83
|
"claimsManagement": "የጥያቄዎች አስተዳደር",
|
|
84
84
|
"claimSummary": "የጥያቄ ማጠቃለያ",
|
|
85
|
+
"claimSummaryMissing": "Claim summary is missing. Please try again.",
|
|
85
86
|
"clear": "ይጽዳ",
|
|
86
87
|
"clearSearch": "የፍለጋ ግቤት ይጽዳ",
|
|
87
88
|
"clinicalCharges": "የክሊኒክ ክፍያዎች",
|
|
@@ -203,6 +204,8 @@
|
|
|
203
204
|
"noItemsSelected": "ምንም ዕቃዎች አልተመረጡም",
|
|
204
205
|
"noMatchingBillsToDisplay": "የሚታዩ ተመሳሳይ ሂሳቦች የሉም",
|
|
205
206
|
"noMatchingItemsToDisplay": "የሚታዩ ተመሳሳይ ዕቃዎች የሉም",
|
|
207
|
+
"noNationalId": "No National ID",
|
|
208
|
+
"noNationalIdFound": "No national ID found for this patient. This may affect claim processing.",
|
|
206
209
|
"noPaymentModes": "ምንም የክፍያ ዘዴዎች አልተገኙም",
|
|
207
210
|
"noPaymentModesSubtitle": "ለተመረጡ ማጣሪያዎች ምንም የክፍያ ዘዴዎች አልተገኙም",
|
|
208
211
|
"noPhoneNumber": "ስልክ ቁጥር የለም",
|
|
@@ -357,4 +360,4 @@
|
|
|
357
360
|
"visittype": "የጉብኝት ዓይነት",
|
|
358
361
|
"waiveBill": "ሂሳብ ይቅር",
|
|
359
362
|
"yes": "አዎ"
|
|
360
|
-
}
|
|
363
|
+
}
|
package/translations/en.json
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"claimsItems": "Claims Items",
|
|
83
83
|
"claimsManagement": "Claims management",
|
|
84
84
|
"claimSummary": "Claim Summary",
|
|
85
|
+
"claimSummaryMissing": "Claim summary is missing. Please try again.",
|
|
85
86
|
"clear": "Clear",
|
|
86
87
|
"clearSearch": "Clear search input",
|
|
87
88
|
"clinicalCharges": "Clinical Charges",
|
|
@@ -203,6 +204,8 @@
|
|
|
203
204
|
"noItemsSelected": "No items selected",
|
|
204
205
|
"noMatchingBillsToDisplay": "No matching bills to display",
|
|
205
206
|
"noMatchingItemsToDisplay": "No matching items to display",
|
|
207
|
+
"noNationalId": "No National ID",
|
|
208
|
+
"noNationalIdFound": "No national ID found for this patient. This may affect claim processing.",
|
|
206
209
|
"noPaymentModes": "No payment modes found",
|
|
207
210
|
"noPaymentModesSubtitle": "No payment modes found for the selected filters",
|
|
208
211
|
"noPhoneNumber": "No Phone Number",
|
package/translations/sw.json
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"claimsItems": "Claims Items",
|
|
83
83
|
"claimsManagement": "Claims management",
|
|
84
84
|
"claimSummary": "Claim Summary",
|
|
85
|
+
"claimSummaryMissing": "Claim summary is missing. Please try again.",
|
|
85
86
|
"clear": "Clear",
|
|
86
87
|
"clearSearch": "Clear search input",
|
|
87
88
|
"clinicalCharges": "Clinical Charges",
|
|
@@ -203,6 +204,8 @@
|
|
|
203
204
|
"noItemsSelected": "No items selected",
|
|
204
205
|
"noMatchingBillsToDisplay": "No matching bills to display",
|
|
205
206
|
"noMatchingItemsToDisplay": "No matching items to display",
|
|
207
|
+
"noNationalId": "No National ID",
|
|
208
|
+
"noNationalIdFound": "No national ID found for this patient. This may affect claim processing.",
|
|
206
209
|
"noPaymentModes": "No payment modes found",
|
|
207
210
|
"noPaymentModesSubtitle": "No payment modes found for the selected filters",
|
|
208
211
|
"noPhoneNumber": "No Phone Number",
|