@kenyaemr/esm-patient-registration-app 8.1.1-pre.129 → 8.1.2-pre.152
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 +21 -23
- package/dist/108.js +1 -1
- package/dist/130.js +1 -1
- package/dist/130.js.map +1 -1
- package/dist/173.js +2 -0
- package/dist/{895.js.LICENSE.txt → 173.js.LICENSE.txt} +25 -0
- package/dist/173.js.map +1 -0
- package/dist/236.js +1 -0
- package/dist/240.js +1 -0
- package/dist/261.js +1 -0
- package/dist/271.js +1 -1
- package/dist/272.js +1 -0
- package/dist/319.js +1 -1
- package/dist/336.js +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/460.js +1 -1
- package/dist/501.js +1 -1
- package/dist/501.js.map +1 -1
- package/dist/539.js +1 -0
- package/dist/566.js +1 -0
- package/dist/574.js +1 -1
- package/dist/623.js +1 -0
- package/dist/623.js.map +1 -0
- package/dist/644.js +1 -1
- package/dist/652.js +1 -0
- package/dist/657.js +1 -0
- package/dist/657.js.map +1 -0
- package/dist/673.js +1 -0
- package/dist/705.js +1 -0
- package/dist/711.js +1 -0
- package/dist/727.js +1 -0
- package/dist/737.js +1 -0
- package/dist/744.js +1 -0
- package/dist/757.js +1 -1
- package/dist/759.js +1 -0
- package/dist/759.js.map +1 -0
- package/dist/76.js +1 -1
- package/dist/788.js +1 -1
- package/dist/807.js +1 -1
- package/dist/833.js +1 -1
- package/dist/899.js +1 -0
- package/dist/kenyaemr-esm-patient-registration-app.js +1 -1
- package/dist/kenyaemr-esm-patient-registration-app.js.buildmanifest.json +445 -93
- package/dist/kenyaemr-esm-patient-registration-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +25 -0
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package-lock.json +2052 -1699
- package/package.json +4 -4
- package/src/client-registry/hie-client-registry/dependants/dependants.component.tsx +46 -0
- package/src/client-registry/hie-client-registry/hie-client-registry.component.tsx +38 -8
- package/src/client-registry/hie-client-registry/hie-resource.ts +126 -21
- package/src/client-registry/hie-client-registry/hie-types.ts +102 -0
- package/src/client-registry/hie-client-registry/modal/confirm-hie.modal.tsx +78 -62
- package/src/client-registry/hie-client-registry/modal/confirm-hie.scss +55 -3
- package/src/client-registry/hie-client-registry/modal/hie-otp-verification-form.component.tsx +88 -0
- package/src/client-registry/hie-client-registry/modal/hie-patient-detail-preview.component.tsx +77 -0
- package/src/client-registry/hie-client-registry/patient-info/patient-info.component.tsx +17 -0
- package/src/config-schema.ts +30 -2
- package/src/patient-registration/field/address/address-search.component.tsx +5 -2
- package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +1 -1
- package/src/patient-registration/field/dob/dob.component.tsx +1 -1
- package/src/patient-registration/field/gender/gender-field.component.tsx +6 -2
- package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +3 -3
- package/src/patient-registration/field/name/name-field.component.tsx +2 -2
- package/src/patient-registration/field/obs/obs-field.component.tsx +9 -5
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +3 -3
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +22 -11
- package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +1 -1
- package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +12 -4
- package/src/patient-registration/form-manager.test.ts +4 -1
- package/src/patient-registration/form-manager.ts +0 -1
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +52 -62
- package/src/patient-registration/mpi/mpi-patient.resource.ts +21 -0
- package/src/patient-registration/patient-registration-hooks.ts +90 -25
- package/src/patient-registration/patient-registration-utils.test.ts +33 -0
- package/src/patient-registration/patient-registration-utils.ts +63 -13
- package/src/patient-registration/patient-registration.component.tsx +17 -2
- package/src/patient-registration/patient-registration.test.tsx +442 -56
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +3 -3
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +1 -1
- package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +28 -28
- package/src/widgets/cancel-patient-edit.modal.tsx +2 -0
- package/src/widgets/cancel-patient-edit.scss +29 -0
- package/src/widgets/delete-identifier-confirmation.modal.tsx +2 -0
- package/src/widgets/delete-identifier-confirmation.scss +29 -0
- package/translations/am.json +1 -0
- package/translations/ar.json +6 -4
- package/translations/de.json +118 -0
- package/translations/en.json +17 -0
- package/translations/es.json +2 -0
- package/translations/fr.json +1 -0
- package/translations/he.json +1 -0
- package/translations/hi.json +118 -0
- package/translations/hi_IN.json +118 -0
- package/translations/id.json +118 -0
- package/translations/it.json +118 -0
- package/translations/km.json +1 -0
- package/translations/ne.json +118 -0
- package/translations/pt.json +118 -0
- package/translations/pt_BR.json +118 -0
- package/translations/qu.json +118 -0
- package/translations/si.json +118 -0
- package/translations/sw.json +118 -0
- package/translations/sw_KE.json +118 -0
- package/translations/tr.json +118 -0
- package/translations/tr_TR.json +118 -0
- package/translations/uk.json +118 -0
- package/translations/vi.json +118 -0
- package/translations/zh.json +3 -1
- package/translations/zh_CN.json +2 -0
- package/dist/250.js +0 -1
- package/dist/250.js.map +0 -1
- package/dist/66.js +0 -1
- package/dist/66.js.map +0 -1
- package/dist/662.js +0 -1
- package/dist/662.js.map +0 -1
- package/dist/753.js +0 -1
- package/dist/753.js.map +0 -1
- package/dist/895.js +0 -2
- package/dist/895.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenyaemr/esm-patient-registration-app",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.2-pre.152",
|
|
4
4
|
"description": "Patient registration microfrontend for the OpenMRS SPA",
|
|
5
5
|
"browser": "dist/kenyaemr-esm-patient-registration-app.js",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"url": "https://github.com/openmrs/openmrs-esm-patient-management/issues"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@carbon/react": "
|
|
40
|
+
"@carbon/react": "^1.71.0",
|
|
41
41
|
"formik": "^2.1.5",
|
|
42
42
|
"lodash-es": "^4.17.15",
|
|
43
43
|
"uuid": "^8.3.2",
|
|
44
44
|
"yup": "^0.29.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@openmrs/esm-framework": "
|
|
47
|
+
"@openmrs/esm-framework": "6.x",
|
|
48
48
|
"dayjs": "1.x",
|
|
49
49
|
"react": "18.x",
|
|
50
50
|
"react-i18next": "11.x",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"webpack": "^5.74.0"
|
|
56
56
|
},
|
|
57
|
-
"stableVersion": "8.
|
|
57
|
+
"stableVersion": "8.0.2"
|
|
58
58
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import styles from '../modal/confirm-hie.scss';
|
|
4
|
+
import PatientInfo from '../patient-info/patient-info.component';
|
|
5
|
+
|
|
6
|
+
const DependentInfo: React.FC<{ dependents: any[] }> = ({ dependents }) => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
|
|
9
|
+
if (dependents && dependents.length > 0) {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<span className={styles.header}>{t('dependants', 'Dependants')}</span>
|
|
13
|
+
{dependents.map((dependent, index) => {
|
|
14
|
+
const name = dependent?.name?.text;
|
|
15
|
+
const relationship =
|
|
16
|
+
dependent?.relationship?.[0]?.coding?.[0]?.display || t('unknownRelationship', 'Unknown');
|
|
17
|
+
|
|
18
|
+
const nationalID = dependent?.extension?.find(
|
|
19
|
+
(ext) => ext?.valueIdentifier?.type?.coding?.some((coding) => coding.code === 'national-id'),
|
|
20
|
+
)?.valueIdentifier?.value;
|
|
21
|
+
|
|
22
|
+
const birthCertificate = dependent?.extension?.find(
|
|
23
|
+
(ext) => ext?.valueIdentifier?.type?.coding?.some((coding) => coding.code === 'birth-certificate'),
|
|
24
|
+
)?.valueIdentifier?.value;
|
|
25
|
+
|
|
26
|
+
const primaryIdentifier = nationalID || birthCertificate;
|
|
27
|
+
const identifierLabel = nationalID
|
|
28
|
+
? t('nationalID', 'National ID')
|
|
29
|
+
: t('birthCertificate', 'Birth Certificate');
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div key={index} className={styles.dependentInfo}>
|
|
33
|
+
<PatientInfo label={t('name', 'Name')} value={name} />
|
|
34
|
+
<PatientInfo label={t('relationship', 'Relationship')} value={relationship} />
|
|
35
|
+
{primaryIdentifier && <PatientInfo label={identifierLabel} value={primaryIdentifier} />}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
})}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default DependentInfo;
|
|
@@ -11,7 +11,7 @@ import { type RegistrationConfig } from '../../config-schema';
|
|
|
11
11
|
import { useForm, Controller, type SubmitHandler } from 'react-hook-form';
|
|
12
12
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
13
13
|
import { fetchPatientFromHIE, mapHIEPatientToFormValues } from './hie-resource';
|
|
14
|
-
import { type HIEPatient } from './hie-types';
|
|
14
|
+
import { type HIEPatientResponse, type HIEPatient, type ErrorResponse } from './hie-types';
|
|
15
15
|
|
|
16
16
|
type HIEClientRegistryProps = {
|
|
17
17
|
props: FormikProps<FormValues>;
|
|
@@ -39,26 +39,56 @@ const HIEClientRegistry: React.FC<HIEClientRegistryProps> = ({ setInitialFormVal
|
|
|
39
39
|
hieClientRegistry: { identifierTypes },
|
|
40
40
|
} = useConfig<RegistrationConfig>();
|
|
41
41
|
|
|
42
|
+
const isHIEPatientResponse = (
|
|
43
|
+
response: HIEPatientResponse | ErrorResponse | undefined,
|
|
44
|
+
): response is HIEPatientResponse => {
|
|
45
|
+
return response?.resourceType === 'Bundle' && 'total' in response;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isOperationOutcome = (response: HIEPatientResponse | ErrorResponse | undefined): response is ErrorResponse => {
|
|
49
|
+
return response?.resourceType === 'OperationOutcome' && 'issue' in response;
|
|
50
|
+
};
|
|
51
|
+
|
|
42
52
|
const onSubmit: SubmitHandler<HIEFormValues> = async (data: HIEFormValues, event: React.BaseSyntheticEvent) => {
|
|
43
53
|
try {
|
|
44
54
|
const hieClientRegistry = await fetchPatientFromHIE(data.identifierType, data.identifierValue);
|
|
45
55
|
|
|
46
|
-
if (hieClientRegistry
|
|
56
|
+
if (isHIEPatientResponse(hieClientRegistry)) {
|
|
57
|
+
if (hieClientRegistry.total === 0) {
|
|
58
|
+
const dispose = showModal('empty-client-registry-modal', {
|
|
59
|
+
onConfirm: () => dispose(),
|
|
60
|
+
close: () => dispose(),
|
|
61
|
+
title: t('clientRegistryEmptys', 'Create Patient'),
|
|
62
|
+
message: t(
|
|
63
|
+
'patientNotFounds',
|
|
64
|
+
`The patient records could not be found in the client registry, proceed to create patient or try again.`,
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
const dispose = showModal('hie-confirmation-modal', {
|
|
48
71
|
patient: hieClientRegistry,
|
|
49
72
|
closeModal: () => dispose(),
|
|
50
73
|
onUseValues: () =>
|
|
51
|
-
setInitialFormValues(
|
|
74
|
+
setInitialFormValues(
|
|
75
|
+
mapHIEPatientToFormValues(hieClientRegistry as unknown as HIEPatientResponse, props.values),
|
|
76
|
+
),
|
|
52
77
|
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (hieClientRegistry && hieClientRegistry.resourceType === 'OperationOutcome') {
|
|
56
|
-
const issueMessage = hieClientRegistry?.['issue']?.map((issue) => issue.diagnostics).join(', ');
|
|
78
|
+
} else if (isOperationOutcome(hieClientRegistry)) {
|
|
79
|
+
const issueMessage = hieClientRegistry?.issue?.map((issue) => issue.diagnostics).join(', ');
|
|
57
80
|
const dispose = showModal('empty-client-registry-modal', {
|
|
58
81
|
onConfirm: () => dispose(),
|
|
59
82
|
close: () => dispose(),
|
|
60
83
|
title: t('clientRegistryEmpty', 'Create & Post Patient'),
|
|
61
|
-
message: issueMessage,
|
|
84
|
+
message: issueMessage || t('errorOccurred', ' There was an error processing the request. Try again later'),
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
showSnackbar({
|
|
88
|
+
title: t('unexpectedResponse', 'Unexpected Response'),
|
|
89
|
+
subtitle: t('contactAdmin', 'Please contact the administrator.'),
|
|
90
|
+
kind: 'error',
|
|
91
|
+
isLowContrast: true,
|
|
62
92
|
});
|
|
63
93
|
}
|
|
64
94
|
} catch (error) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import capitalize from 'lodash-es/capitalize';
|
|
2
2
|
import { type PatientIdentifierValue, type FormValues } from '../../patient-registration/patient-registration.types';
|
|
3
|
-
import { type MapperConfig, type HIEPatient, type ErrorResponse } from './hie-types';
|
|
3
|
+
import { type MapperConfig, type HIEPatient, type ErrorResponse, type HIEPatientResponse } from './hie-types';
|
|
4
4
|
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
5
5
|
import { v4 } from 'uuid';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import dayjs from 'dayjs';
|
|
6
8
|
/**
|
|
7
9
|
* Represents a client for interacting with a Health Information Exchange (HIE) resource.
|
|
8
10
|
* @template T - The type of the resource being fetched.
|
|
@@ -11,7 +13,6 @@ class HealthInformationExchangeClient<T> {
|
|
|
11
13
|
async fetchResource(resourceType: string, params: Record<string, string>): Promise<T> {
|
|
12
14
|
const [identifierType, identifierValue] = Object.entries(params)[0];
|
|
13
15
|
const url = `${restBaseUrl}/kenyaemr/getSHAPatient/${identifierValue}/${identifierType}`;
|
|
14
|
-
|
|
15
16
|
const response = await openmrsFetch(url);
|
|
16
17
|
return response.json();
|
|
17
18
|
}
|
|
@@ -33,23 +34,23 @@ class Mapper<T, U> {
|
|
|
33
34
|
/**
|
|
34
35
|
* Maps HIEPatient objects to FormValues objects.
|
|
35
36
|
*/
|
|
36
|
-
class PatientMapper extends Mapper<
|
|
37
|
-
mapHIEPatientToFormValues(hiePatient:
|
|
38
|
-
const
|
|
39
|
-
const telecom = hiePatient.telecom || [];
|
|
37
|
+
class PatientMapper extends Mapper<HIEPatientResponse, FormValues> {
|
|
38
|
+
mapHIEPatientToFormValues(hiePatient: HIEPatientResponse, currentFormValues: FormValues): FormValues {
|
|
39
|
+
const { familyName, givenName, middleName } = getPatientName(hiePatient);
|
|
40
|
+
const telecom = hiePatient?.entry[0]?.resource.telecom || [];
|
|
40
41
|
|
|
41
42
|
const telecomAttributes = this.mapTelecomToAttributes(telecom);
|
|
42
43
|
const updatedIdentifiers = this.mapIdentifiers(hiePatient, currentFormValues);
|
|
43
|
-
const extensionAddressEntries = this.mapExtensionsToAddress(hiePatient.extension);
|
|
44
|
-
|
|
44
|
+
const extensionAddressEntries = this.mapExtensionsToAddress(hiePatient?.entry[0]?.resource.extension);
|
|
45
|
+
// TODO: In the event isDead is true, additional information such as caused of death, date e.tc is required
|
|
45
46
|
return {
|
|
46
|
-
isDead: hiePatient
|
|
47
|
-
gender: hiePatient.gender
|
|
48
|
-
birthdate: hiePatient
|
|
49
|
-
givenName
|
|
50
|
-
familyName
|
|
47
|
+
isDead: hiePatient?.entry[0]?.resource?.active ? false : true,
|
|
48
|
+
gender: hiePatient?.entry[0]?.resource.gender ?? '',
|
|
49
|
+
birthdate: hiePatient?.entry[0]?.resource?.birthDate ?? '',
|
|
50
|
+
givenName,
|
|
51
|
+
familyName,
|
|
51
52
|
telephoneNumber: telecom.find((t) => t.system === 'phone')?.value || '',
|
|
52
|
-
middleName
|
|
53
|
+
middleName,
|
|
53
54
|
address: extensionAddressEntries,
|
|
54
55
|
identifiers: updatedIdentifiers,
|
|
55
56
|
attributes: telecomAttributes,
|
|
@@ -71,7 +72,7 @@ class PatientMapper extends Mapper<HIEPatient, FormValues> {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
private mapIdentifiers(
|
|
74
|
-
hiePatient:
|
|
75
|
+
hiePatient: HIEPatientResponse,
|
|
75
76
|
currentFormValues: FormValues,
|
|
76
77
|
): Record<string, PatientIdentifierValue> {
|
|
77
78
|
const updatedIdentifiers: Record<string, PatientIdentifierValue> = { ...currentFormValues.identifiers };
|
|
@@ -79,11 +80,11 @@ class PatientMapper extends Mapper<HIEPatient, FormValues> {
|
|
|
79
80
|
// See https://github.com/palladiumkenya/openmrs-module-kenyaemr/blob/1e1d281eaba8041c45318e60ca0730449b8e4197/api/src/main/distro/metadata/identifierTypes.xml#L33
|
|
80
81
|
updatedIdentifiers.socialHealthAuthorityIdentificationNumber = {
|
|
81
82
|
...currentFormValues.identifiers['socialHealthAuthorityIdentificationNumber'],
|
|
82
|
-
identifierValue: hiePatient
|
|
83
|
+
identifierValue: hiePatient?.entry[0]?.resource?.id,
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
// Map fhir.Patient.Identifier to identifiers
|
|
86
|
-
hiePatient.identifier?.forEach((identifier: fhir.Identifier) => {
|
|
87
|
+
hiePatient.entry[0]?.resource.identifier?.forEach((identifier: fhir.Identifier) => {
|
|
87
88
|
const identifierType = identifier.type?.coding?.[0]?.code;
|
|
88
89
|
const mappedIdentifierType = this.convertToCamelCase(identifierType);
|
|
89
90
|
const identifierValue = identifier.value;
|
|
@@ -146,17 +147,121 @@ const mapperConfig: MapperConfig = {
|
|
|
146
147
|
};
|
|
147
148
|
|
|
148
149
|
// Create instances
|
|
149
|
-
const hieApiClient = new HealthInformationExchangeClient<
|
|
150
|
+
const hieApiClient = new HealthInformationExchangeClient<HIEPatientResponse | ErrorResponse>();
|
|
150
151
|
const patientMapper = new PatientMapper(mapperConfig);
|
|
151
152
|
|
|
152
153
|
// Exported functions
|
|
153
154
|
export const fetchPatientFromHIE = async (
|
|
154
155
|
identifierType: string,
|
|
155
156
|
identifierValue: string,
|
|
156
|
-
): Promise<
|
|
157
|
-
return hieApiClient.fetchResource('
|
|
157
|
+
): Promise<HIEPatientResponse | ErrorResponse> => {
|
|
158
|
+
return hieApiClient.fetchResource('Bundle', { [identifierType]: identifierValue });
|
|
158
159
|
};
|
|
159
160
|
|
|
160
|
-
export const mapHIEPatientToFormValues = (
|
|
161
|
+
export const mapHIEPatientToFormValues = (
|
|
162
|
+
hiePatient: HIEPatientResponse,
|
|
163
|
+
currentFormValues: FormValues,
|
|
164
|
+
): FormValues => {
|
|
161
165
|
return patientMapper.mapHIEPatientToFormValues(hiePatient, currentFormValues);
|
|
162
166
|
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Mask sensitive data by replacing end digits starting from the 2nd to last with '*'
|
|
170
|
+
* @param data {string} - The data to mask
|
|
171
|
+
* @returns {string} - The masked data
|
|
172
|
+
*/
|
|
173
|
+
export const maskData = (data: string): string => {
|
|
174
|
+
const maskedData = data.slice(0, 2) + '*'.repeat(Math.max(0, data.length - 2));
|
|
175
|
+
return maskedData;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get patient name from FHIR Patient resource
|
|
180
|
+
* @param patient {fhir.Patient} - The FHIR Patient resource
|
|
181
|
+
* @returns {object} - The patient name
|
|
182
|
+
*/
|
|
183
|
+
export const getPatientName = (patient: HIEPatientResponse) => {
|
|
184
|
+
const familyName = patient?.entry?.[0]?.resource?.name?.[0]?.family ?? ''; // Safely access the family name
|
|
185
|
+
const givenNames = patient?.entry?.[0]?.resource?.name?.[0]?.given ?? []; // Safely access the given names array
|
|
186
|
+
|
|
187
|
+
const givenName = givenNames?.[0] ?? ''; // The first item is the given name (first name)
|
|
188
|
+
const middleName = givenNames.slice(1).join(' ').trim(); // Combine all other given names as middle name(s)
|
|
189
|
+
|
|
190
|
+
return { familyName, givenName, middleName };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const authorizationFormSchema = z.object({
|
|
194
|
+
otp: z.string().min(1, 'Required'),
|
|
195
|
+
receiver: z
|
|
196
|
+
.string()
|
|
197
|
+
.regex(/^(\+?254|0)((7|1)\d{8})$/)
|
|
198
|
+
.optional(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export function generateOTP(length = 5) {
|
|
202
|
+
let otpNumbers = '0123456789';
|
|
203
|
+
let OTP = '';
|
|
204
|
+
const len = otpNumbers.length;
|
|
205
|
+
for (let i = 0; i < length; i++) {
|
|
206
|
+
OTP += otpNumbers[Math.floor(Math.random() * len)];
|
|
207
|
+
}
|
|
208
|
+
return OTP;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function persistOTP(otp: string, patientUuid: string) {
|
|
212
|
+
sessionStorage.setItem(
|
|
213
|
+
patientUuid,
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
otp,
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function sendOtp({ otp, receiver }: z.infer<typeof authorizationFormSchema>, patientName: string) {
|
|
222
|
+
const payload = parseMessage(
|
|
223
|
+
{ otp, patient_name: patientName, expiry_time: 5 },
|
|
224
|
+
'Dear {{patient_name}}, your OTP for accessing your Shared Health Records (SHR) is {{otp}}. Please enter this code to proceed. The code is valid for {{expiry_time}} minutes.',
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const url = `${restBaseUrl}/kenyaemr/send-kenyaemr-sms?message=${payload}&phone=${receiver}`;
|
|
228
|
+
|
|
229
|
+
const res = await openmrsFetch(url, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
redirect: 'follow',
|
|
232
|
+
});
|
|
233
|
+
if (res.ok) {
|
|
234
|
+
return await res.json();
|
|
235
|
+
}
|
|
236
|
+
throw new Error('Error sending otp');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseMessage(object, template) {
|
|
240
|
+
const placeholderRegex = /{{(.*?)}}/g;
|
|
241
|
+
|
|
242
|
+
const parsedMessage = template.replace(placeholderRegex, (match, fieldName) => {
|
|
243
|
+
if (object.hasOwnProperty(fieldName)) {
|
|
244
|
+
return object[fieldName];
|
|
245
|
+
} else {
|
|
246
|
+
return match;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return parsedMessage;
|
|
251
|
+
}
|
|
252
|
+
export function verifyOtp(otp: string, patientUuid: string) {
|
|
253
|
+
const data = sessionStorage.getItem(patientUuid);
|
|
254
|
+
if (!data) {
|
|
255
|
+
throw new Error('Invalid OTP');
|
|
256
|
+
}
|
|
257
|
+
const { otp: storedOtp, timestamp } = JSON.parse(data);
|
|
258
|
+
const isExpired = dayjs(timestamp).add(5, 'minutes').isBefore(dayjs());
|
|
259
|
+
if (storedOtp !== otp) {
|
|
260
|
+
throw new Error('Invalid OTP');
|
|
261
|
+
}
|
|
262
|
+
if (isExpired) {
|
|
263
|
+
throw new Error('OTP Expired');
|
|
264
|
+
}
|
|
265
|
+
sessionStorage.removeItem(patientUuid);
|
|
266
|
+
return 'Verification success';
|
|
267
|
+
}
|
|
@@ -5,6 +5,108 @@ export type HIEPatient = fhir.Patient & {
|
|
|
5
5
|
}>;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
export interface HIEPatientResponse {
|
|
9
|
+
resourceType: string;
|
|
10
|
+
id: string;
|
|
11
|
+
total: number;
|
|
12
|
+
meta: Metadata;
|
|
13
|
+
link: Link[];
|
|
14
|
+
entry: Entry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Metadata {
|
|
18
|
+
lastUpdated: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Link {
|
|
22
|
+
relation: string;
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Entry {
|
|
27
|
+
resource: Resource;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Resource {
|
|
31
|
+
id: string;
|
|
32
|
+
extension: Extension[];
|
|
33
|
+
identifier: Identifier[];
|
|
34
|
+
active: boolean;
|
|
35
|
+
name: Name[];
|
|
36
|
+
telecom: Telecom[];
|
|
37
|
+
birthDate: string;
|
|
38
|
+
address: Address[];
|
|
39
|
+
gender: string;
|
|
40
|
+
maritalStatus: MaritalStatus;
|
|
41
|
+
contact: Contact[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Extension {
|
|
45
|
+
url: string;
|
|
46
|
+
valueString: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Identifier {
|
|
50
|
+
type: CodingType;
|
|
51
|
+
value: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CodingType {
|
|
55
|
+
coding: Coding[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Coding {
|
|
59
|
+
system?: string;
|
|
60
|
+
code: string;
|
|
61
|
+
display: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Name {
|
|
65
|
+
text: string;
|
|
66
|
+
family: string;
|
|
67
|
+
given: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface Telecom {
|
|
71
|
+
system: string;
|
|
72
|
+
value?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Address {
|
|
76
|
+
extension: AddressExtension[];
|
|
77
|
+
city: string;
|
|
78
|
+
country: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface AddressExtension {
|
|
82
|
+
url: string;
|
|
83
|
+
valueString: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface MaritalStatus {
|
|
87
|
+
coding: Coding[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface Contact {
|
|
91
|
+
id: string;
|
|
92
|
+
extension: ContactExtension[];
|
|
93
|
+
relationship: Relationship[];
|
|
94
|
+
name: Name;
|
|
95
|
+
telecom: Telecom[];
|
|
96
|
+
address: Address;
|
|
97
|
+
gender: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ContactExtension {
|
|
101
|
+
url: string;
|
|
102
|
+
valueIdentifier?: Identifier;
|
|
103
|
+
valueString?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface Relationship {
|
|
107
|
+
coding: Coding[];
|
|
108
|
+
}
|
|
109
|
+
|
|
8
110
|
export type APIClientConfig = {
|
|
9
111
|
baseUrl: string;
|
|
10
112
|
credentials: string;
|
|
@@ -1,82 +1,98 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Button, ModalBody, ModalFooter, ModalHeader , Form } from '@carbon/react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
2
3
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import {
|
|
4
|
+
import { type HIEPatientResponse, type HIEPatient } from '../hie-types';
|
|
4
5
|
import styles from './confirm-hie.scss';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<span style={{ minWidth: '5rem', fontWeight: 'bold' }}>{label}</span>
|
|
13
|
-
<span>{value}</span>
|
|
14
|
-
</div>
|
|
15
|
-
);
|
|
16
|
-
};
|
|
6
|
+
import { authorizationFormSchema, generateOTP, getPatientName, persistOTP, sendOtp, verifyOtp } from '../hie-resource';
|
|
7
|
+
import HIEPatientDetailPreview from './hie-patient-detail-preview.component';
|
|
8
|
+
import HIEOTPVerficationForm from './hie-otp-verification-form.component';
|
|
9
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
10
|
+
import { type z } from 'zod';
|
|
11
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
12
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
17
13
|
|
|
18
14
|
interface HIEConfirmationModalProps {
|
|
19
15
|
closeModal: () => void;
|
|
20
|
-
patient:
|
|
16
|
+
patient: HIEPatientResponse;
|
|
21
17
|
onUseValues: () => void;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
const HIEConfirmationModal: React.FC<HIEConfirmationModalProps> = ({ closeModal, patient, onUseValues }) => {
|
|
25
21
|
const { t } = useTranslation();
|
|
26
|
-
const
|
|
27
|
-
const
|
|
22
|
+
const [mode, setMode] = useState<'authorization' | 'preview'>('preview');
|
|
23
|
+
const [status, setStatus] = useState<'loadingOtp' | 'otpSendSuccessfull' | 'otpFetchError'>();
|
|
24
|
+
const phoneNumber = patient?.entry[0]?.resource.telecom?.find((num) => num.system === 'phone')?.value;
|
|
25
|
+
const getidentifier = (code: string) =>
|
|
26
|
+
patient?.entry[0]?.resource.identifier?.find(
|
|
27
|
+
(identifier) => identifier?.type?.coding?.some((coding) => coding?.code === code),
|
|
28
|
+
);
|
|
29
|
+
const patientId = patient?.id ?? getidentifier('SHA-number')?.value;
|
|
30
|
+
const form = useForm<z.infer<typeof authorizationFormSchema>>({
|
|
31
|
+
defaultValues: {
|
|
32
|
+
receiver: phoneNumber,
|
|
33
|
+
},
|
|
34
|
+
resolver: zodResolver(authorizationFormSchema),
|
|
35
|
+
});
|
|
36
|
+
const patientName = getPatientName(patient);
|
|
28
37
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
const onSubmit = async (values: z.infer<typeof authorizationFormSchema>) => {
|
|
39
|
+
try {
|
|
40
|
+
verifyOtp(values.otp, patientId);
|
|
41
|
+
showSnackbar({ title: 'Success', kind: 'success', subtitle: 'Access granted successfully' });
|
|
42
|
+
onUseValues();
|
|
43
|
+
closeModal();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
showSnackbar({ title: 'Faulure', kind: 'error', subtitle: `${error}` });
|
|
46
|
+
}
|
|
32
47
|
};
|
|
33
48
|
|
|
34
49
|
return (
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<PatientInfo label={t('gender', 'Gender')} value={capitalize(patient?.gender)} />
|
|
52
|
-
<PatientInfo
|
|
53
|
-
label={t('maritalStatus', 'Marital status')}
|
|
54
|
-
value={patient?.maritalStatus?.coding?.map((m) => m.code).join('')}
|
|
50
|
+
<FormProvider {...form}>
|
|
51
|
+
<Form onSubmit={form.handleSubmit(onSubmit)}>
|
|
52
|
+
<ModalHeader closeModal={closeModal}>
|
|
53
|
+
<span className={styles.header}>
|
|
54
|
+
{mode === 'authorization'
|
|
55
|
+
? t('hiePatientVerification', 'HIE Patient Verification')
|
|
56
|
+
: t('hieModal', 'HIE Patient Record Found')}
|
|
57
|
+
</span>
|
|
58
|
+
</ModalHeader>
|
|
59
|
+
<ModalBody>
|
|
60
|
+
{mode === 'authorization' ? (
|
|
61
|
+
<HIEOTPVerficationForm
|
|
62
|
+
name={`${patientName.givenName} ${patientName.middleName}`}
|
|
63
|
+
patientId={patientId}
|
|
64
|
+
status={status}
|
|
65
|
+
setStatus={setStatus}
|
|
55
66
|
/>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</CodeSnippet>
|
|
65
|
-
</AccordionItem>
|
|
66
|
-
</Accordion>
|
|
67
|
-
</div>
|
|
68
|
-
</ModalBody>
|
|
69
|
-
<ModalFooter>
|
|
70
|
-
<Button kind="secondary" onClick={closeModal}>
|
|
71
|
-
{t('cancel', 'Cancel')}
|
|
72
|
-
</Button>
|
|
67
|
+
) : (
|
|
68
|
+
<HIEPatientDetailPreview patient={patient} />
|
|
69
|
+
)}
|
|
70
|
+
</ModalBody>
|
|
71
|
+
<ModalFooter>
|
|
72
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
73
|
+
{t('cancel', 'Cancel')}
|
|
74
|
+
</Button>
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
{mode === 'preview' && (
|
|
77
|
+
<Button onClick={() => setMode('authorization')} kind="primary">
|
|
78
|
+
{t('useValues', 'Use values')}
|
|
79
|
+
</Button>
|
|
80
|
+
)}
|
|
81
|
+
{mode === 'authorization' && (
|
|
82
|
+
<Button
|
|
83
|
+
kind="primary"
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={form.formState.isSubmitting || status !== 'otpSendSuccessfull'}>
|
|
86
|
+
{t('verifyAndUseValues', 'Verify & Use values')}
|
|
87
|
+
</Button>
|
|
88
|
+
)}
|
|
89
|
+
</ModalFooter>
|
|
90
|
+
</Form>
|
|
91
|
+
</FormProvider>
|
|
79
92
|
);
|
|
80
93
|
};
|
|
81
94
|
|
|
82
95
|
export default HIEConfirmationModal;
|
|
96
|
+
function onVerificationSuccesfull() {
|
|
97
|
+
throw new Error('Function not implemented.');
|
|
98
|
+
}
|