@ampath/esm-patient-registration-app 9.2.0-next.15 → 9.2.0-next.17
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/dist/4300.js +1 -1
- package/dist/4395.js +1 -0
- package/dist/4395.js.map +1 -0
- package/dist/5239.js +1 -1
- package/dist/5239.js.LICENSE.txt +10 -0
- package/dist/5239.js.map +1 -1
- package/dist/6276.js +1 -1
- package/dist/6687.js +1 -0
- package/dist/6687.js.map +1 -0
- package/dist/6996.js +1 -0
- package/dist/6996.js.map +1 -0
- package/dist/7125.js +2 -0
- package/dist/7125.js.map +1 -0
- package/dist/7821.js +1 -0
- package/dist/7821.js.map +1 -0
- package/dist/8414.js +1 -0
- package/dist/8414.js.map +1 -0
- package/dist/8434.js +1 -1
- package/dist/8882.js +1 -0
- package/dist/8882.js.map +1 -0
- package/dist/9898.js +1 -0
- package/dist/9898.js.map +1 -0
- package/dist/9933.js +2 -0
- package/dist/{2615.js.LICENSE.txt → 9933.js.LICENSE.txt} +9 -0
- package/dist/9933.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-registration-app.js +1 -1
- package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +226 -150
- package/dist/openmrs-esm-patient-registration-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +9 -0
- package/src/patient-registration/client-registry/client-registry.resource.ts +1 -1
- package/src/patient-registration/client-registry-search/client-registry-dependant-details.component.tsx +237 -0
- package/src/patient-registration/client-registry-search/client-registry-details.component.tsx +111 -0
- package/src/patient-registration/client-registry-search/client-registry-patient-details.component.tsx +234 -0
- package/src/patient-registration/client-registry-search/client-registry-search.component.tsx +234 -0
- package/src/patient-registration/client-registry-search/client-registry-verification-tag.component.tsx +78 -0
- package/src/patient-registration/client-registry-search/client-registry.resource.ts +135 -0
- package/src/patient-registration/client-registry-search/client-registry.types.ts +243 -0
- package/src/patient-registration/client-registry-search/map-client-registry-to-form-utils.ts +590 -0
- package/src/patient-registration/field/field.component.tsx +3 -0
- package/src/patient-registration/field/id/id-field.component.tsx +1 -1
- package/src/patient-registration/field/id/id-field.test.tsx +1 -1
- package/src/patient-registration/form-manager.test.ts +1 -0
- package/src/patient-registration/patient-registration.component.tsx +0 -1
- package/src/patient-registration/patient-registration.resource.ts +1 -1
- package/src/patient-registration/patient-registration.scss +4 -0
- package/src/routes.json +12 -1
- package/src/widgets/client-registry-verification.modal.tsx +27 -0
- package/translations/en.json +4 -0
- package/dist/2450.js +0 -1
- package/dist/2450.js.map +0 -1
- package/dist/2615.js +0 -2
- package/dist/2615.js.map +0 -1
- package/dist/320.js +0 -2
- package/dist/320.js.LICENSE.txt +0 -8
- package/dist/320.js.map +0 -1
- package/dist/3474.js +0 -2
- package/dist/3474.js.LICENSE.txt +0 -8
- package/dist/3474.js.map +0 -1
- package/dist/7071.js +0 -1
- package/dist/7071.js.map +0 -1
- package/dist/729.js +0 -2
- package/dist/729.js.map +0 -1
- /package/dist/{729.js.LICENSE.txt → 7125.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TableRow,
|
|
4
|
+
TableCell,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Table,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableHeader,
|
|
9
|
+
TableBody,
|
|
10
|
+
Button,
|
|
11
|
+
Row,
|
|
12
|
+
InlineLoading,
|
|
13
|
+
} from '@carbon/react';
|
|
14
|
+
import { HieIdentificationType, type AmrsPerson, type ClientRegistryBody } from './client-registry.types';
|
|
15
|
+
import {
|
|
16
|
+
addressFields,
|
|
17
|
+
getIdentifierUuid,
|
|
18
|
+
identifiersSyncFields,
|
|
19
|
+
mapFieldValue,
|
|
20
|
+
nameFields,
|
|
21
|
+
patientObjFields,
|
|
22
|
+
personSyncFields,
|
|
23
|
+
} from './map-client-registry-to-form-utils';
|
|
24
|
+
import { updatePerson, updateAmrsPersonIdentifiers } from './client-registry.resource';
|
|
25
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
26
|
+
|
|
27
|
+
interface ClientRegistryPatientDetailsProps {
|
|
28
|
+
hieData: ClientRegistryBody;
|
|
29
|
+
amrsPerson: AmrsPerson;
|
|
30
|
+
fromDependant?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface GetTableDataRowProps {
|
|
34
|
+
field: string;
|
|
35
|
+
label: string;
|
|
36
|
+
amrsPerson: string;
|
|
37
|
+
hiePatient: string;
|
|
38
|
+
onChange?(e: boolean, field: string, value: string, multiple: boolean): void;
|
|
39
|
+
allChecked: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TableDataRow: React.FC<GetTableDataRowProps> = ({
|
|
43
|
+
field,
|
|
44
|
+
label,
|
|
45
|
+
amrsPerson,
|
|
46
|
+
hiePatient,
|
|
47
|
+
onChange,
|
|
48
|
+
allChecked,
|
|
49
|
+
}) => {
|
|
50
|
+
const [checked, setChecked] = useState(false);
|
|
51
|
+
const randomString = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
onChange?.(allChecked, field, hiePatient, true);
|
|
54
|
+
setChecked(allChecked);
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [allChecked]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<TableRow>
|
|
60
|
+
<TableCell>{label}</TableCell>
|
|
61
|
+
<TableCell>{amrsPerson}</TableCell>
|
|
62
|
+
<TableCell>{hiePatient}</TableCell>
|
|
63
|
+
<TableCell>
|
|
64
|
+
<Checkbox
|
|
65
|
+
id={`cbox-${randomString}`}
|
|
66
|
+
onChange={(e) => {
|
|
67
|
+
onChange(e.target.checked, field, hiePatient, false);
|
|
68
|
+
setChecked(e.target.checked);
|
|
69
|
+
}}
|
|
70
|
+
checked={checked}
|
|
71
|
+
/>
|
|
72
|
+
</TableCell>
|
|
73
|
+
</TableRow>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ClientRegistryPatientDetails: React.FC<ClientRegistryPatientDetailsProps> = ({
|
|
78
|
+
hieData,
|
|
79
|
+
amrsPerson,
|
|
80
|
+
fromDependant,
|
|
81
|
+
}) => {
|
|
82
|
+
const [syncFields, setSyncFields] = useState<Array<Record<string, string>>>([]);
|
|
83
|
+
const [allChecked, setAllChecked] = useState(false);
|
|
84
|
+
const [loading, setLoading] = useState(false);
|
|
85
|
+
const locationUuid = '18c343eb-b353-462a-9139-b16606e6b6c2';
|
|
86
|
+
const randomString = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
87
|
+
|
|
88
|
+
const handleFieldChange = (checked: boolean, field: string, value: string, multiple: boolean) => {
|
|
89
|
+
if (multiple) {
|
|
90
|
+
if (checked) {
|
|
91
|
+
setSyncFields((prev) => [...prev, { [field]: value }]);
|
|
92
|
+
} else {
|
|
93
|
+
setSyncFields([]);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (checked) {
|
|
97
|
+
setSyncFields([...syncFields, { [field]: value }]);
|
|
98
|
+
} else {
|
|
99
|
+
setSyncFields((prev) => prev.filter((p) => !Object.keys(p).includes(field)));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleCheckAll = (e) => {
|
|
105
|
+
setAllChecked(e.target.checked);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleSync = async () => {
|
|
109
|
+
try {
|
|
110
|
+
const payload = {};
|
|
111
|
+
syncFields.forEach((field) => {
|
|
112
|
+
let key = Object.keys(field)[0];
|
|
113
|
+
payload[key] = field[key];
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Person
|
|
117
|
+
const patientPayload = {};
|
|
118
|
+
const names = {};
|
|
119
|
+
const addresses = {};
|
|
120
|
+
const otherFields = {};
|
|
121
|
+
Object.entries(payload).forEach(([k, v]) => {
|
|
122
|
+
if (personSyncFields.includes(k)) {
|
|
123
|
+
// names
|
|
124
|
+
if (nameFields.includes(k)) {
|
|
125
|
+
names[k] = v;
|
|
126
|
+
}
|
|
127
|
+
// addresses
|
|
128
|
+
else if (addressFields.includes(k)) {
|
|
129
|
+
if (v) {
|
|
130
|
+
addresses[k] = v;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
otherFields[k] = v;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
Object.assign(patientPayload, otherFields, { addresses: [addresses] }, { names: [names] });
|
|
138
|
+
await updatePerson(amrsPerson.person.uuid, patientPayload);
|
|
139
|
+
showSnackbar({
|
|
140
|
+
kind: 'success',
|
|
141
|
+
title: 'Patient successfully synced.',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Identifiers
|
|
145
|
+
Object.entries(payload).forEach(async ([k, v]) => {
|
|
146
|
+
if (identifiersSyncFields().includes(k)) {
|
|
147
|
+
if (v) {
|
|
148
|
+
const identifierUuid = getIdentifierUuid(HieIdentificationType[k]);
|
|
149
|
+
const identifierPayload = {
|
|
150
|
+
identifier: v,
|
|
151
|
+
location: locationUuid,
|
|
152
|
+
identifierType: identifierUuid,
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
// Check if the identifier exists
|
|
156
|
+
if (amrsPerson?.person?.identifiers?.find((i) => i.identifierType.uuid === identifierUuid)) {
|
|
157
|
+
// update to have the selected identifier
|
|
158
|
+
await updateAmrsPersonIdentifiers(
|
|
159
|
+
amrsPerson.person.uuid,
|
|
160
|
+
identifierUuid + '',
|
|
161
|
+
identifierPayload,
|
|
162
|
+
fromDependant,
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
// create to have the blank identifier
|
|
166
|
+
await updateAmrsPersonIdentifiers(amrsPerson.person.uuid, '', identifierPayload, fromDependant);
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
showSnackbar({
|
|
170
|
+
kind: 'error',
|
|
171
|
+
title: 'Error syncing patient identifiers.',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
showSnackbar({
|
|
178
|
+
kind: 'success',
|
|
179
|
+
title: 'Patient identifiers successfully synced.',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
setSyncFields([]);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
showSnackbar({
|
|
185
|
+
kind: 'error',
|
|
186
|
+
title: 'Error syncing patient data.',
|
|
187
|
+
subtitle: JSON.stringify(err?.error?.message),
|
|
188
|
+
});
|
|
189
|
+
} finally {
|
|
190
|
+
setLoading(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<>
|
|
196
|
+
<Row>
|
|
197
|
+
{syncFields.length ? <Button onClick={handleSync}>Sync data</Button> : null}
|
|
198
|
+
{loading ? <InlineLoading description="Syncing patient details..." /> : null}
|
|
199
|
+
</Row>
|
|
200
|
+
<Table>
|
|
201
|
+
<TableHead>
|
|
202
|
+
<TableRow>
|
|
203
|
+
<TableHeader>Field</TableHeader>
|
|
204
|
+
<TableHeader>AMRS Person</TableHeader>
|
|
205
|
+
<TableHeader>HIE Patient</TableHeader>
|
|
206
|
+
<TableHeader>
|
|
207
|
+
<Checkbox id={`cbox-multiple-${randomString}`} onChange={(e) => handleCheckAll(e)} />
|
|
208
|
+
</TableHeader>
|
|
209
|
+
</TableRow>
|
|
210
|
+
</TableHead>
|
|
211
|
+
<TableBody>
|
|
212
|
+
{patientObjFields.map((field) => {
|
|
213
|
+
const fieldValue = mapFieldValue(field, hieData, amrsPerson);
|
|
214
|
+
const amrsField = fieldValue[0];
|
|
215
|
+
const hieField = fieldValue[1];
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<TableDataRow
|
|
219
|
+
label={field}
|
|
220
|
+
field={field}
|
|
221
|
+
amrsPerson={amrsField}
|
|
222
|
+
hiePatient={hieField}
|
|
223
|
+
onChange={handleFieldChange}
|
|
224
|
+
allChecked={allChecked}
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</TableBody>
|
|
229
|
+
</Table>
|
|
230
|
+
</>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export default ClientRegistryPatientDetails;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, TextInput, InlineLoading, InlineNotification, Select, SelectItem } from '@carbon/react';
|
|
3
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
4
|
+
import { useFormikContext } from 'formik';
|
|
5
|
+
import styles from '../patient-registration.scss';
|
|
6
|
+
import { requestCustomOtp, validateCustomOtp, fetchClientRegistryData } from './client-registry.resource';
|
|
7
|
+
import { applyClientRegistryMapping } from './map-client-registry-to-form-utils';
|
|
8
|
+
import { type RequestCustomOtpDto } from './client-registry.types';
|
|
9
|
+
|
|
10
|
+
export interface ClientRegistryLookupSectionProps {
|
|
11
|
+
onClientVerified?: (payload: RequestCustomOtpDto) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ClientRegistryLookupSection: React.FC<ClientRegistryLookupSectionProps> = ({ onClientVerified }) => {
|
|
15
|
+
const { setFieldValue, values } = useFormikContext<any>();
|
|
16
|
+
const [identifier, setIdentifier] = useState('');
|
|
17
|
+
const [otp, setOtp] = useState('');
|
|
18
|
+
const [otpSent, setOtpSent] = useState(false);
|
|
19
|
+
const [otpVerified, setOtpVerified] = useState(false);
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [sessionId, setSessionId] = useState('');
|
|
22
|
+
const [error, setError] = useState<string>('');
|
|
23
|
+
const [identificationType, setIdentificationType] = useState('National ID');
|
|
24
|
+
|
|
25
|
+
const locationUuid = '18c343eb-b353-462a-9139-b16606e6b6c2';
|
|
26
|
+
const identificationTypes = [
|
|
27
|
+
{ text: 'National ID', value: 'National ID' },
|
|
28
|
+
{ text: 'Refugee ID', value: 'Refugee ID' },
|
|
29
|
+
{ text: 'Alien ID', value: 'Alien ID' },
|
|
30
|
+
{ text: 'Mandate Number', value: 'Mandate Number' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
async function withTimeout<T>(promise: Promise<T>, ms = 10000): Promise<T> {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), ms);
|
|
36
|
+
try {
|
|
37
|
+
const response = await promise;
|
|
38
|
+
return response;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.name === 'AbortError') {
|
|
41
|
+
throw new Error('Request timeout');
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleFetchCR = async () => {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
setError('');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const payload = {
|
|
55
|
+
identificationNumber: identifier,
|
|
56
|
+
identificationType: identificationType,
|
|
57
|
+
locationUuid,
|
|
58
|
+
};
|
|
59
|
+
const result = await withTimeout(fetchClientRegistryData(payload));
|
|
60
|
+
const patients = Array.isArray(result) ? result : [];
|
|
61
|
+
|
|
62
|
+
if (patients.length === 0) {
|
|
63
|
+
throw new Error('No matching patient found in Client Registry.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const patient = patients[0];
|
|
67
|
+
applyClientRegistryMapping(patient, setFieldValue);
|
|
68
|
+
|
|
69
|
+
showSnackbar({
|
|
70
|
+
kind: 'success',
|
|
71
|
+
title: 'Client Data Loaded',
|
|
72
|
+
subtitle: `Patient ${patient.first_name} ${patient.last_name} fetched successfully. Loaded education, next of kin, and relationships.`,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const errorMessage = err.message || 'Failed to fetch client data';
|
|
76
|
+
setError(errorMessage);
|
|
77
|
+
showSnackbar({
|
|
78
|
+
kind: 'error',
|
|
79
|
+
title: 'Fetch Failed',
|
|
80
|
+
subtitle: errorMessage,
|
|
81
|
+
});
|
|
82
|
+
} finally {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleSendOtp = async () => {
|
|
88
|
+
if (!identifier.trim()) {
|
|
89
|
+
setError('Please enter a valid National/Alien ID');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setLoading(true);
|
|
94
|
+
setError('');
|
|
95
|
+
try {
|
|
96
|
+
const payload = {
|
|
97
|
+
identificationNumber: identifier,
|
|
98
|
+
identificationType: identificationType,
|
|
99
|
+
locationUuid,
|
|
100
|
+
};
|
|
101
|
+
const response = await withTimeout(requestCustomOtp(payload));
|
|
102
|
+
setSessionId(response.sessionId);
|
|
103
|
+
setOtpSent(true);
|
|
104
|
+
|
|
105
|
+
showSnackbar({
|
|
106
|
+
kind: 'success',
|
|
107
|
+
title: 'OTP sent successfully',
|
|
108
|
+
subtitle: `A code was sent to ${response.maskedPhone}`,
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const errorMessage = err.message || 'Failed to send OTP';
|
|
112
|
+
setError(errorMessage);
|
|
113
|
+
showSnackbar({
|
|
114
|
+
kind: 'error',
|
|
115
|
+
title: 'Error sending OTP',
|
|
116
|
+
subtitle: errorMessage,
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
setLoading(false);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleVerifyOtp = async () => {
|
|
124
|
+
if (!otp.trim()) {
|
|
125
|
+
setError('Please enter the OTP code');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setLoading(true);
|
|
130
|
+
setError('');
|
|
131
|
+
try {
|
|
132
|
+
const payload = {
|
|
133
|
+
sessionId,
|
|
134
|
+
otp,
|
|
135
|
+
locationUuid,
|
|
136
|
+
};
|
|
137
|
+
await withTimeout(validateCustomOtp(payload));
|
|
138
|
+
|
|
139
|
+
const customOtpPayload = {
|
|
140
|
+
identificationNumber: identifier,
|
|
141
|
+
identificationType: identificationType,
|
|
142
|
+
locationUuid,
|
|
143
|
+
};
|
|
144
|
+
setOtpVerified(true);
|
|
145
|
+
onClientVerified?.(customOtpPayload);
|
|
146
|
+
showSnackbar({
|
|
147
|
+
kind: 'success',
|
|
148
|
+
title: 'OTP Verified',
|
|
149
|
+
subtitle: 'You can now fetch data from Client Registry.',
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const errorMessage = err.message || 'OTP verification failed';
|
|
153
|
+
setError(errorMessage);
|
|
154
|
+
showSnackbar({
|
|
155
|
+
kind: 'error',
|
|
156
|
+
title: 'OTP Verification Failed',
|
|
157
|
+
subtitle: errorMessage,
|
|
158
|
+
});
|
|
159
|
+
} finally {
|
|
160
|
+
setLoading(false);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className={styles.section}>
|
|
166
|
+
<h4 className={styles.sectionTitle}>Client Registry Verification</h4>
|
|
167
|
+
|
|
168
|
+
{error && (
|
|
169
|
+
<div className={styles.notificationSpacing}>
|
|
170
|
+
<InlineNotification title="Error" subtitle={error} kind="error" lowContrast />
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
<div className={styles.fieldGroup}>
|
|
175
|
+
<Select labelText="Select Identification Type" onChange={(e) => setIdentificationType(e.target.value)}>
|
|
176
|
+
{identificationTypes.map((item) => (
|
|
177
|
+
<SelectItem text={item.text} value={item.value} />
|
|
178
|
+
))}
|
|
179
|
+
</Select>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className={styles.fieldGroup}>
|
|
183
|
+
<TextInput
|
|
184
|
+
id="client-registry-id"
|
|
185
|
+
labelText="National ID or Alien ID"
|
|
186
|
+
value={identifier}
|
|
187
|
+
onChange={(e) => setIdentifier(e.target.value)}
|
|
188
|
+
disabled={otpSent}
|
|
189
|
+
placeholder="Enter identification number"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
194
|
+
{!otpSent ? (
|
|
195
|
+
<Button kind="secondary" onClick={handleSendOtp} disabled={loading}>
|
|
196
|
+
{loading ? <InlineLoading description="Sending..." /> : 'Send OTP'}
|
|
197
|
+
</Button>
|
|
198
|
+
) : (
|
|
199
|
+
<>
|
|
200
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
201
|
+
<TextInput
|
|
202
|
+
id="otp-input"
|
|
203
|
+
labelText="Enter OTP"
|
|
204
|
+
value={otp}
|
|
205
|
+
onChange={(e) => setOtp(e.target.value)}
|
|
206
|
+
disabled={otpVerified}
|
|
207
|
+
placeholder="Enter the code sent to your phone"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div style={{ marginTop: '0.5rem', display: 'flex', gap: '0.5rem' }}>
|
|
212
|
+
{!otpVerified ? (
|
|
213
|
+
<Button size="sm" kind="secondary" onClick={handleVerifyOtp} disabled={loading}>
|
|
214
|
+
{loading ? <InlineLoading description="Verifying..." /> : 'Verify OTP'}
|
|
215
|
+
</Button>
|
|
216
|
+
) : (
|
|
217
|
+
<Button kind="primary" onClick={handleFetchCR} disabled={loading}>
|
|
218
|
+
{loading ? <InlineLoading description="Fetching..." /> : 'Fetch Client Registry Data'}
|
|
219
|
+
</Button>
|
|
220
|
+
)}
|
|
221
|
+
{!otpVerified && (
|
|
222
|
+
<Button size="sm" kind="tertiary" onClick={() => setOtpSent(false)}>
|
|
223
|
+
Change ID
|
|
224
|
+
</Button>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export default ClientRegistryLookupSection;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Button } from '@carbon/react';
|
|
3
|
+
import { showModal, age, usePatient } from '@openmrs/esm-framework';
|
|
4
|
+
import ClientRegistryLookupSection from './client-registry-search.component';
|
|
5
|
+
import { Formik } from 'formik';
|
|
6
|
+
import { type RequestCustomOtpDto } from './client-registry.types';
|
|
7
|
+
import ClientRegistryDetails from './client-registry-details.component';
|
|
8
|
+
|
|
9
|
+
const ClientRegistryVerificationTag = () => {
|
|
10
|
+
const { patient } = usePatient();
|
|
11
|
+
const [showCrBtn, setShowCrBtn] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (patient && patient.birthDate) {
|
|
15
|
+
const ageArr = age(patient.birthDate).split(' ');
|
|
16
|
+
if (ageArr.includes('yrs')) {
|
|
17
|
+
const yrs = Number(ageArr[0]);
|
|
18
|
+
setShowCrBtn(yrs > 17);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}, [patient]);
|
|
22
|
+
|
|
23
|
+
const handleClientRegistryVerification = () => {
|
|
24
|
+
const dispose = showModal(
|
|
25
|
+
'client-registry-verification-modal',
|
|
26
|
+
{
|
|
27
|
+
onConfirm: () => {
|
|
28
|
+
dispose();
|
|
29
|
+
},
|
|
30
|
+
Component: CrFormikComponent,
|
|
31
|
+
props: {
|
|
32
|
+
forceClose: () => {
|
|
33
|
+
dispose();
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
() => {},
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return showCrBtn ? (
|
|
42
|
+
<Button kind="ghost" onClick={handleClientRegistryVerification}>
|
|
43
|
+
Verify CR
|
|
44
|
+
</Button>
|
|
45
|
+
) : (
|
|
46
|
+
<></>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface CrFormicComponentProps {
|
|
51
|
+
forceClose?(): void;
|
|
52
|
+
}
|
|
53
|
+
const CrFormikComponent: React.FC<CrFormicComponentProps> = ({ forceClose }) => {
|
|
54
|
+
const initialFormValues = {};
|
|
55
|
+
|
|
56
|
+
const handleOnClientVerified = (payload: RequestCustomOtpDto) => {
|
|
57
|
+
forceClose?.();
|
|
58
|
+
const dispose = showModal(
|
|
59
|
+
'client-registry-verification-modal',
|
|
60
|
+
{
|
|
61
|
+
onConfirm: () => {
|
|
62
|
+
dispose();
|
|
63
|
+
},
|
|
64
|
+
Component: ClientRegistryDetails,
|
|
65
|
+
props: { payload: payload },
|
|
66
|
+
},
|
|
67
|
+
() => {},
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Formik initialValues={initialFormValues} onSubmit={null}>
|
|
73
|
+
<ClientRegistryLookupSection onClientVerified={handleOnClientVerified} />
|
|
74
|
+
</Formik>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default ClientRegistryVerificationTag;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import {
|
|
3
|
+
type AmrsPerson,
|
|
4
|
+
type ClientRegistrySearchRequest,
|
|
5
|
+
type ClientRegistrySearchResponse,
|
|
6
|
+
type RequestCustomOtpDto,
|
|
7
|
+
type RequestCustomOtpResponse,
|
|
8
|
+
type ValidateCustomOtpResponse,
|
|
9
|
+
type ValidateHieCustomOtpDto,
|
|
10
|
+
} from './client-registry.types';
|
|
11
|
+
import { mapAmrsPatientRelationship } from './map-client-registry-to-form-utils';
|
|
12
|
+
|
|
13
|
+
const HIE_BASE_URL = 'https://staging.ampath.or.ke/hie';
|
|
14
|
+
|
|
15
|
+
async function postJson<T>(url: string, payload: unknown): Promise<T> {
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify(payload),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const errorText = await response.text();
|
|
24
|
+
throw new Error(`Request failed with ${response.status}: ${errorText}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return response.json() as Promise<T>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function requestCustomOtp(payload: RequestCustomOtpDto): Promise<RequestCustomOtpResponse> {
|
|
31
|
+
const url = `${HIE_BASE_URL}/client/send-custom-otp`;
|
|
32
|
+
const formattedPayload = {
|
|
33
|
+
identificationNumber: payload.identificationNumber,
|
|
34
|
+
identificationType: payload.identificationType,
|
|
35
|
+
locationUuid: payload.locationUuid,
|
|
36
|
+
};
|
|
37
|
+
return postJson<RequestCustomOtpResponse>(url, formattedPayload);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function validateCustomOtp(payload: ValidateHieCustomOtpDto): Promise<ValidateCustomOtpResponse> {
|
|
41
|
+
const url = `${HIE_BASE_URL}/client/validate-custom-otp`;
|
|
42
|
+
const formattedPayload = {
|
|
43
|
+
sessionId: payload.sessionId,
|
|
44
|
+
otp: payload.otp,
|
|
45
|
+
locationUuid: payload.locationUuid,
|
|
46
|
+
};
|
|
47
|
+
return postJson<ValidateCustomOtpResponse>(url, formattedPayload);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function fetchClientRegistryData(
|
|
51
|
+
payload: ClientRegistrySearchRequest,
|
|
52
|
+
): Promise<ClientRegistrySearchResponse> {
|
|
53
|
+
const url = `${HIE_BASE_URL}/client/search`;
|
|
54
|
+
const formattedPayload = {
|
|
55
|
+
identificationNumber: payload.identificationNumber,
|
|
56
|
+
identificationType: payload.identificationType,
|
|
57
|
+
locationUuid: payload.locationUuid,
|
|
58
|
+
};
|
|
59
|
+
return postJson<ClientRegistrySearchResponse>(url, formattedPayload);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchAmrsPatientData(patientUuid: string) {
|
|
63
|
+
return await openmrsFetch<AmrsPerson>(`${restBaseUrl}/patient/${patientUuid}?v=full`, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
}).catch((err) => {
|
|
66
|
+
console.error(err);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function updateAmrsPersonIdentifiers(
|
|
71
|
+
patientUuid: string,
|
|
72
|
+
identifierUuid: string,
|
|
73
|
+
payload: unknown,
|
|
74
|
+
fromDependant = false,
|
|
75
|
+
) {
|
|
76
|
+
const resource = fromDependant ? 'person' : 'patient';
|
|
77
|
+
return await openmrsFetch(`${restBaseUrl}/${resource}/${patientUuid}/identifier/${identifierUuid}`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: payload,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function fetchAmrsPersonData(personUuid: string) {
|
|
87
|
+
return await openmrsFetch(`${restBaseUrl}/person/${personUuid}?v=full`, {
|
|
88
|
+
method: 'GET',
|
|
89
|
+
}).catch((err) => {
|
|
90
|
+
console.error(err);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function updatePerson(patientUuid: string, payload: unknown) {
|
|
95
|
+
return await openmrsFetch<AmrsPerson>(`${restBaseUrl}/person/${patientUuid}`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
},
|
|
100
|
+
body: payload,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function createPerson(payload: unknown) {
|
|
105
|
+
return await openmrsFetch<AmrsPerson>(`${restBaseUrl}/person`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
},
|
|
110
|
+
body: payload,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function createRelationship(payload: unknown) {
|
|
115
|
+
return await openmrsFetch(`${restBaseUrl}/relationship`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
},
|
|
120
|
+
body: payload,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function getRelationships(patientUuid: string) {
|
|
125
|
+
const response = await openmrsFetch(`${restBaseUrl}/relationship?person=${patientUuid}&v=full`, {
|
|
126
|
+
method: 'GET',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
if (response && response.data) {
|
|
132
|
+
return mapAmrsPatientRelationship(patientUuid, response.data.results);
|
|
133
|
+
}
|
|
134
|
+
return [];
|
|
135
|
+
}
|