@kenyaemr/esm-patient-registration-app 8.0.1-pre.99 → 8.1.0
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 +18 -16
- package/dist/108.js +1 -0
- package/dist/108.js.map +1 -0
- package/dist/574.js +1 -1
- package/dist/623.js +1 -1
- package/dist/735.js +1 -1
- package/dist/745.js +2 -0
- package/dist/{831.js.LICENSE.txt → 745.js.LICENSE.txt} +10 -0
- package/dist/745.js.map +1 -0
- package/dist/{59.js → 76.js} +1 -1
- package/dist/{59.js.map → 76.js.map} +1 -1
- package/dist/830.js +1 -0
- package/dist/830.js.map +1 -0
- package/dist/kenyaemr-esm-patient-registration-app.js +1 -1
- package/dist/kenyaemr-esm-patient-registration-app.js.buildmanifest.json +69 -69
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +10 -0
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -3
- package/src/client-registry/client-registry.component.tsx +22 -0
- package/src/client-registry/hie-client-registry/hie-client-registry.component.tsx +134 -0
- package/src/client-registry/hie-client-registry/hie-client-registry.scss +53 -0
- package/src/client-registry/hie-client-registry/hie-resource.ts +160 -0
- package/src/client-registry/hie-client-registry/hie-types.ts +29 -0
- package/src/client-registry/hie-client-registry/modal/confirm-hie.modal.tsx +82 -0
- package/src/client-registry/hie-client-registry/modal/confirm-hie.scss +10 -0
- package/src/{patient-verification → client-registry/patient-verification}/patient-verification-hook.tsx +1 -1
- package/src/{patient-verification → client-registry/patient-verification}/patient-verification-utils.ts +1 -1
- package/src/{patient-verification → client-registry/patient-verification}/patient-verification.component.tsx +1 -1
- package/src/{patient-verification → client-registry/patient-verification}/patient-verification.scss +1 -1
- package/src/{patient-verification → client-registry/patient-verification}/verification-modal/empty-prompt.component.tsx +9 -6
- package/src/config-schema.ts +37 -0
- package/src/index.ts +5 -2
- package/src/patient-registration/patient-registration-hooks.ts +4 -1
- package/src/patient-registration/patient-registration.component.tsx +24 -17
- package/src/routes.json +7 -2
- package/translations/en.json +12 -0
- package/dist/330.js +0 -1
- package/dist/330.js.map +0 -1
- package/dist/564.js +0 -1
- package/dist/564.js.map +0 -1
- package/dist/831.js +0 -2
- package/dist/831.js.map +0 -1
- /package/src/{patient-verification → client-registry/patient-verification}/assets/counties.json +0 -0
- /package/src/{patient-verification → client-registry/patient-verification}/assets/verification-assets.ts +0 -0
- /package/src/{patient-verification → client-registry/patient-verification}/verification-modal/confirm-prompt.component.tsx +0 -0
- /package/src/{patient-verification → client-registry/patient-verification}/verification-types.ts +0 -0
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.24.0"},"pages":[{"component":"root","route":"patient-registration","online":true,"offline":true},{"component":"editPatient","routeRegex":"patient\\/([a-zA-Z0-9\\-]+)\\/edit","online":true,"offline":true}],"extensions":[{"component":"addPatientLink","name":"add-patient-action","slot":"top-nav-actions-slot","online":true,"offline":true},{"component":"cancelPatientEditModal","name":"cancel-patient-edit-modal","online":true,"offline":true},{"component":"patientPhotoExtension","name":"patient-photo-widget","slot":"patient-photo-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-actions-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-search-actions-slot","online":true,"offline":true},{"component":"deleteIdentifierConfirmationModal","name":"delete-identifier-confirmation-modal","online":true,"offline":true},{"component":"emptyClientRegistryModal","name":"empty-client-registry-modal","online":true,"offline":true},{"component":"confirmClientRegistryModal","name":"confirm-client-registry-modal","online":true,"offline":true}],"version":"8.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.24.0"},"pages":[{"component":"root","route":"patient-registration","online":true,"offline":true},{"component":"editPatient","routeRegex":"patient\\/([a-zA-Z0-9\\-]+)\\/edit","online":true,"offline":true}],"extensions":[{"component":"addPatientLink","name":"add-patient-action","slot":"top-nav-actions-slot","online":true,"offline":true},{"component":"cancelPatientEditModal","name":"cancel-patient-edit-modal","online":true,"offline":true},{"component":"patientPhotoExtension","name":"patient-photo-widget","slot":"patient-photo-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-actions-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-search-actions-slot","online":true,"offline":true},{"component":"deleteIdentifierConfirmationModal","name":"delete-identifier-confirmation-modal","online":true,"offline":true},{"component":"emptyClientRegistryModal","name":"empty-client-registry-modal","online":true,"offline":true},{"component":"confirmClientRegistryModal","name":"confirm-client-registry-modal","online":true,"offline":true}],"modals":[{"component":"hieConfirmationModal","name":"hie-confirmation-modal"}],"version":"8.1.0"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenyaemr/esm-patient-registration-app",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
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",
|
|
@@ -53,6 +53,5 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"webpack": "^5.74.0"
|
|
56
|
-
}
|
|
57
|
-
"stableVersion": "8.0.0"
|
|
56
|
+
}
|
|
58
57
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FormikProps } from 'formik';
|
|
2
|
+
import React, { type Dispatch } from 'react';
|
|
3
|
+
import { type FormValues } from '../patient-registration/patient-registration.types';
|
|
4
|
+
import { useFeatureFlag } from '@openmrs/esm-framework';
|
|
5
|
+
import PatientVerification from './patient-verification/patient-verification.component';
|
|
6
|
+
import HIEClientRegistry from './hie-client-registry/hie-client-registry.component';
|
|
7
|
+
|
|
8
|
+
type ClientRegistryProps = {
|
|
9
|
+
props: FormikProps<FormValues>;
|
|
10
|
+
setInitialFormValues: Dispatch<FormValues>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ClientRegistry: React.FC<ClientRegistryProps> = (clientRegistryProps) => {
|
|
14
|
+
const healthInformationExchangeFlag = useFeatureFlag('healthInformationExchange');
|
|
15
|
+
|
|
16
|
+
if (healthInformationExchangeFlag) {
|
|
17
|
+
return <HIEClientRegistry {...clientRegistryProps} />;
|
|
18
|
+
}
|
|
19
|
+
return <PatientVerification {...clientRegistryProps} />;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default ClientRegistry;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React, { type Dispatch } from 'react';
|
|
2
|
+
import styles from './hie-client-registry.scss';
|
|
3
|
+
import { type FormikProps } from 'formik';
|
|
4
|
+
import { type FormValues } from '../../patient-registration/patient-registration.types';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Tile, ComboBox, Button, TextInput, InlineLoading } from '@carbon/react';
|
|
7
|
+
import { Search } from '@carbon/react/icons';
|
|
8
|
+
import { showModal, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { type RegistrationConfig } from '../../config-schema';
|
|
11
|
+
import { useForm, Controller, type SubmitHandler } from 'react-hook-form';
|
|
12
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
13
|
+
import { fetchPatientFromHIE, mapHIEPatientToFormValues } from './hie-resource';
|
|
14
|
+
import { type HIEPatient } from './hie-types';
|
|
15
|
+
|
|
16
|
+
type HIEClientRegistryProps = {
|
|
17
|
+
props: FormikProps<FormValues>;
|
|
18
|
+
setInitialFormValues: Dispatch<FormValues>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type HIEFormValues = {
|
|
22
|
+
identifierType: string;
|
|
23
|
+
identifierValue: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const HIEFormSchema = z.object({
|
|
27
|
+
identifierType: z.string().min(1, { message: 'Identifier type is required' }),
|
|
28
|
+
identifierValue: z.string().min(1, { message: 'Identifier value is required' }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const HIEClientRegistry: React.FC<HIEClientRegistryProps> = ({ setInitialFormValues, props }) => {
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
const { control, handleSubmit, watch, formState } = useForm<HIEFormValues>({
|
|
34
|
+
mode: 'all',
|
|
35
|
+
defaultValues: { identifierType: '', identifierValue: '' },
|
|
36
|
+
resolver: zodResolver(HIEFormSchema),
|
|
37
|
+
});
|
|
38
|
+
const {
|
|
39
|
+
hieClientRegistry: { identifierTypes },
|
|
40
|
+
} = useConfig<RegistrationConfig>();
|
|
41
|
+
|
|
42
|
+
const onSubmit: SubmitHandler<HIEFormValues> = async (data: HIEFormValues, event: React.BaseSyntheticEvent) => {
|
|
43
|
+
try {
|
|
44
|
+
const hieClientRegistry = await fetchPatientFromHIE(data.identifierType, data.identifierValue);
|
|
45
|
+
|
|
46
|
+
if (hieClientRegistry && hieClientRegistry.resourceType === 'Patient') {
|
|
47
|
+
const dispose = showModal('hie-confirmation-modal', {
|
|
48
|
+
patient: hieClientRegistry,
|
|
49
|
+
closeModal: () => dispose(),
|
|
50
|
+
onUseValues: () =>
|
|
51
|
+
setInitialFormValues(mapHIEPatientToFormValues(hieClientRegistry as HIEPatient, props.values)),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hieClientRegistry && hieClientRegistry.resourceType === 'OperationOutcome') {
|
|
56
|
+
const issueMessage = hieClientRegistry?.['issue']?.map((issue) => issue.diagnostics).join(', ');
|
|
57
|
+
const dispose = showModal('empty-client-registry-modal', {
|
|
58
|
+
onConfirm: () => dispose(),
|
|
59
|
+
close: () => dispose(),
|
|
60
|
+
title: t('clientRegistryEmpty', 'Create & Post Patient'),
|
|
61
|
+
message: issueMessage,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
showSnackbar({
|
|
66
|
+
title: t('errorFetchingPatient', 'Error fetching patient'),
|
|
67
|
+
subtitle: error.message,
|
|
68
|
+
kind: 'error',
|
|
69
|
+
isLowContrast: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<form onSubmit={handleSubmit(onSubmit)} className={styles.hieContainer}>
|
|
76
|
+
<h3 className={styles.productiveHeading02} style={{ color: '#161616' }}>
|
|
77
|
+
{t('patientVerificationFromHIE', 'Patient verification from HIE')}
|
|
78
|
+
</h3>
|
|
79
|
+
<span className={styles.label01}>
|
|
80
|
+
{t('allFieldsRequiredText', 'All fields are required unless marked optional')}
|
|
81
|
+
</span>
|
|
82
|
+
<Tile className={styles.grid}>
|
|
83
|
+
<Controller
|
|
84
|
+
control={control}
|
|
85
|
+
name="identifierType"
|
|
86
|
+
render={({ field: { onChange, value, onBlur, ref }, fieldState }) => (
|
|
87
|
+
<ComboBox
|
|
88
|
+
light
|
|
89
|
+
style={{ borderBottom: 'none' }}
|
|
90
|
+
onChange={({ selectedItem }) => onChange(selectedItem?.identifierValue)}
|
|
91
|
+
id="identifier-combobox"
|
|
92
|
+
items={identifierTypes}
|
|
93
|
+
placeholder={t('selectIdentifierType', 'Select identifier type')}
|
|
94
|
+
itemToString={(item) => (item ? item.identifierType : '')}
|
|
95
|
+
titleText={t('identifierType', 'Identifier type')}
|
|
96
|
+
invalid={!!fieldState?.error?.message}
|
|
97
|
+
invalidText={fieldState?.error?.message}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
<Controller
|
|
102
|
+
control={control}
|
|
103
|
+
name="identifierValue"
|
|
104
|
+
render={({ field, fieldState }) => (
|
|
105
|
+
<TextInput
|
|
106
|
+
disabled={watch('identifierType') === ''}
|
|
107
|
+
light
|
|
108
|
+
id="identifier-search"
|
|
109
|
+
placeholder={t('enterIdentifierSearchValue', 'Enter identifier search value')}
|
|
110
|
+
type="text"
|
|
111
|
+
labelText={t('identifierSearch', 'Identifier search')}
|
|
112
|
+
invalid={!!fieldState?.error?.message}
|
|
113
|
+
invalidText={fieldState?.error?.message}
|
|
114
|
+
{...field}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
/>
|
|
118
|
+
<Button
|
|
119
|
+
onClick={handleSubmit(onSubmit)}
|
|
120
|
+
renderIcon={(props) => <Search size={24} {...props} />}
|
|
121
|
+
iconDescription={t('searchRegistry', 'Search registry')}
|
|
122
|
+
size="md"
|
|
123
|
+
disabled={!watch('identifierType') || !watch('identifierValue') || formState.isSubmitting}
|
|
124
|
+
kind="tertiary">
|
|
125
|
+
{formState.isSubmitting
|
|
126
|
+
? t('searchingRegistry', 'Searching registry...')
|
|
127
|
+
: t('searchRegistry', 'Search registry')}
|
|
128
|
+
</Button>
|
|
129
|
+
</Tile>
|
|
130
|
+
</form>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default HIEClientRegistry;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@use '@carbon/type';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.hieContainer {
|
|
6
|
+
margin-bottom: layout.$layout-01;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.productiveHeading02 {
|
|
10
|
+
@include type.type-style('heading-compact-02');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.label01 {
|
|
14
|
+
@include type.type-style('label-01');
|
|
15
|
+
margin-top: layout.$spacing-05;
|
|
16
|
+
margin-bottom: layout.$spacing-05;
|
|
17
|
+
color: colors.$gray-50;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Tablet viewport */
|
|
21
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
22
|
+
.grid {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
row-gap: layout.$spacing-05;
|
|
26
|
+
margin: layout.$spacing-01 0;
|
|
27
|
+
padding-bottom: layout.$layout-02;
|
|
28
|
+
column-gap: layout.$spacing-05;
|
|
29
|
+
|
|
30
|
+
& > button {
|
|
31
|
+
width: 50%;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Desktop viewport */
|
|
37
|
+
:global(.omrs-breakpoint-gt-tablet) {
|
|
38
|
+
.grid {
|
|
39
|
+
margin: layout.$spacing-01 0;
|
|
40
|
+
padding-bottom: layout.$layout-02;
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-columns: 1fr 1fr 0.5fr;
|
|
43
|
+
column-gap: layout.$spacing-05;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
|
|
46
|
+
& > button {
|
|
47
|
+
align-self: flex-start;
|
|
48
|
+
margin-top: 1.4rem;
|
|
49
|
+
max-width: 14rem;
|
|
50
|
+
min-width: 14rem;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { add, capitalize } from 'lodash-es';
|
|
2
|
+
import { type PatientIdentifierValue, type FormValues } from '../../patient-registration/patient-registration.types';
|
|
3
|
+
import { type MapperConfig, type HIEPatient, type ErrorResponse } from './hie-types';
|
|
4
|
+
import { getConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { type RegistrationConfig } from '../../config-schema';
|
|
6
|
+
import { v4 } from 'uuid';
|
|
7
|
+
/**
|
|
8
|
+
* Represents a client for interacting with a Health Information Exchange (HIE) resource.
|
|
9
|
+
* @template T - The type of the resource being fetched.
|
|
10
|
+
*/
|
|
11
|
+
class HealthInformationExchangeClient<T> {
|
|
12
|
+
async fetchResource(resourceType: string, params: Record<string, string>): Promise<T> {
|
|
13
|
+
const {
|
|
14
|
+
hieClientRegistry: { baseUrl, encodedCredentials },
|
|
15
|
+
} = await getConfig<RegistrationConfig>('@kenyaemr/esm-patient-registration-app');
|
|
16
|
+
const urlParams = new URLSearchParams(params);
|
|
17
|
+
const response = await fetch(`${baseUrl}${resourceType}?${urlParams}`, {
|
|
18
|
+
headers: new Headers({
|
|
19
|
+
Authorization: `Basic ${encodedCredentials}`,
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return response.json();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents a generic Mapper class.
|
|
29
|
+
* @template T - The source type.
|
|
30
|
+
* @template U - The target type.
|
|
31
|
+
*/
|
|
32
|
+
class Mapper<T, U> {
|
|
33
|
+
protected config: MapperConfig;
|
|
34
|
+
|
|
35
|
+
constructor(config: MapperConfig) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Maps HIEPatient objects to FormValues objects.
|
|
42
|
+
*/
|
|
43
|
+
class PatientMapper extends Mapper<HIEPatient, FormValues> {
|
|
44
|
+
mapHIEPatientToFormValues(hiePatient: HIEPatient, currentFormValues: FormValues): FormValues {
|
|
45
|
+
const name = hiePatient.name?.[0] || {};
|
|
46
|
+
const telecom = hiePatient.telecom || [];
|
|
47
|
+
|
|
48
|
+
const telecomAttributes = this.mapTelecomToAttributes(telecom);
|
|
49
|
+
const updatedIdentifiers = this.mapIdentifiers(hiePatient, currentFormValues);
|
|
50
|
+
const extensionAddressEntries = this.mapExtensionsToAddress(hiePatient.extension);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
isDead: hiePatient.deceasedBoolean || false,
|
|
54
|
+
gender: hiePatient.gender || '',
|
|
55
|
+
birthdate: hiePatient.birthDate || '',
|
|
56
|
+
givenName: name.given?.[0] || '',
|
|
57
|
+
familyName: name.family || '',
|
|
58
|
+
telephoneNumber: telecom.find((t) => t.system === 'phone')?.value || '',
|
|
59
|
+
middleName: name.given?.[1],
|
|
60
|
+
address: extensionAddressEntries,
|
|
61
|
+
identifiers: updatedIdentifiers,
|
|
62
|
+
attributes: telecomAttributes,
|
|
63
|
+
relationships: [],
|
|
64
|
+
patientUuid: v4(),
|
|
65
|
+
} as FormValues;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private mapTelecomToAttributes(telecom: Array<fhir.ContactPoint>): Record<string, string> {
|
|
69
|
+
return telecom.reduce<Record<string, string>>((acc, { system, value }) => {
|
|
70
|
+
if (system && value && this.config.teleComMap[system]) {
|
|
71
|
+
const filteredValue = value.replace(/^254/, '0');
|
|
72
|
+
if (filteredValue) {
|
|
73
|
+
acc[this.config.teleComMap[system]] = filteredValue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return acc;
|
|
77
|
+
}, {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private mapIdentifiers(
|
|
81
|
+
hiePatient: HIEPatient,
|
|
82
|
+
currentFormValues: FormValues,
|
|
83
|
+
): Record<string, PatientIdentifierValue> {
|
|
84
|
+
const updatedIdentifiers: Record<string, PatientIdentifierValue> = { ...currentFormValues.identifiers };
|
|
85
|
+
|
|
86
|
+
// Map Social Health Authority Unique Identification Number to HIE Patient ID
|
|
87
|
+
// See https://github.com/palladiumkenya/openmrs-module-kenyaemr/blob/1e1d281eaba8041c45318e60ca0730449b8e4197/api/src/main/distro/metadata/identifierTypes.xml#L33
|
|
88
|
+
updatedIdentifiers.socialHealthAuthorityIdentificationNumber = {
|
|
89
|
+
...currentFormValues.identifiers['socialHealthAuthorityIdentificationNumber'],
|
|
90
|
+
identifierValue: hiePatient.id,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Map fhir.Patient.Identifier to identifiers
|
|
94
|
+
hiePatient.identifier?.forEach((identifier) => {
|
|
95
|
+
const system = identifier.system?.split('/').pop();
|
|
96
|
+
if (system && this.config.identifierMap[system]) {
|
|
97
|
+
const key = this.config.identifierMap[system];
|
|
98
|
+
updatedIdentifiers[key] = {
|
|
99
|
+
...currentFormValues.identifiers[key],
|
|
100
|
+
identifierValue: identifier.value || '',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Filter out undefined keys and values
|
|
106
|
+
Object.keys(updatedIdentifiers).forEach((key) => {
|
|
107
|
+
if (updatedIdentifiers[key] === undefined || updatedIdentifiers[key].identifierValue === undefined) {
|
|
108
|
+
delete updatedIdentifiers[key];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return updatedIdentifiers;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private mapExtensionsToAddress(extensions: HIEPatient['extension']): Record<string, string> {
|
|
116
|
+
return extensions.reduce<Record<string, string>>((acc, ext) => {
|
|
117
|
+
const identifierType = ext.url.split('/').pop();
|
|
118
|
+
const mappedKey = this.config.addressHierarchyMap[identifierType];
|
|
119
|
+
|
|
120
|
+
if (mappedKey && ext.valueString) {
|
|
121
|
+
acc[mappedKey] = capitalize(ext.valueString);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return acc;
|
|
125
|
+
}, {});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Update MapperConfig interface in hie-types.ts
|
|
130
|
+
|
|
131
|
+
const mapperConfig: MapperConfig = {
|
|
132
|
+
teleComMap: {
|
|
133
|
+
email: 'b8d0b331-1d2d-4a9a-b741-1816f498bdb6',
|
|
134
|
+
phone: 'b2c38640-2603-4629-aebd-3b54f33f1e3a',
|
|
135
|
+
},
|
|
136
|
+
addressHierarchyMap: {
|
|
137
|
+
county: 'countyDistrict',
|
|
138
|
+
'sub-county': 'stateProvince',
|
|
139
|
+
ward: 'address4',
|
|
140
|
+
},
|
|
141
|
+
addressMap: {},
|
|
142
|
+
// Map FHIR Patient identifiers to identifiers, at the moment HIE teams returns null for all identifiers type codings
|
|
143
|
+
identifierMap: {},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Create instances
|
|
147
|
+
const hieApiClient = new HealthInformationExchangeClient<HIEPatient | ErrorResponse>();
|
|
148
|
+
const patientMapper = new PatientMapper(mapperConfig);
|
|
149
|
+
|
|
150
|
+
// Exported functions
|
|
151
|
+
export const fetchPatientFromHIE = async (
|
|
152
|
+
identifierType: string,
|
|
153
|
+
identifierValue: string,
|
|
154
|
+
): Promise<HIEPatient | ErrorResponse> => {
|
|
155
|
+
return hieApiClient.fetchResource('Patient', { [identifierType]: identifierValue });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const mapHIEPatientToFormValues = (hiePatient: HIEPatient, currentFormValues: FormValues): FormValues => {
|
|
159
|
+
return patientMapper.mapHIEPatientToFormValues(hiePatient, currentFormValues);
|
|
160
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type HIEPatient = fhir.Patient & {
|
|
2
|
+
extension: Array<{
|
|
3
|
+
url: string;
|
|
4
|
+
valueString: string;
|
|
5
|
+
}>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type APIClientConfig = {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
credentials: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface MapperConfig {
|
|
14
|
+
teleComMap: Record<string, string>;
|
|
15
|
+
addressHierarchyMap: Record<string, string>;
|
|
16
|
+
identifierMap: Record<string, string>;
|
|
17
|
+
addressMap: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ErrorResponse = {
|
|
21
|
+
resourceType: string;
|
|
22
|
+
issue: Array<Issue>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type Issue = {
|
|
26
|
+
severity: string;
|
|
27
|
+
code: string;
|
|
28
|
+
diagnostics: string;
|
|
29
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalHeader, ModalFooter, Accordion, AccordionItem, CodeSnippet } from '@carbon/react';
|
|
4
|
+
import styles from './confirm-hie.scss';
|
|
5
|
+
import { age, ExtensionSlot, formatDate } from '@openmrs/esm-framework';
|
|
6
|
+
import { type HIEPatient } from '../hie-types';
|
|
7
|
+
import capitalize from 'lodash-es/capitalize';
|
|
8
|
+
|
|
9
|
+
const PatientInfo: React.FC<{ label: string; value: string }> = ({ label, value }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div style={{ display: 'grid', gridTemplateColumns: '0.25fr 0.75fr', margin: '0.25rem' }}>
|
|
12
|
+
<span style={{ minWidth: '5rem', fontWeight: 'bold' }}>{label}</span>
|
|
13
|
+
<span>{value}</span>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface HIEConfirmationModalProps {
|
|
19
|
+
closeModal: () => void;
|
|
20
|
+
patient: HIEPatient;
|
|
21
|
+
onUseValues: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const HIEConfirmationModal: React.FC<HIEConfirmationModalProps> = ({ closeModal, patient, onUseValues }) => {
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const firstName = patient?.name[0]['given']?.[0];
|
|
27
|
+
const lastName = patient?.name[0]['family'];
|
|
28
|
+
|
|
29
|
+
const handleUseValues = () => {
|
|
30
|
+
onUseValues();
|
|
31
|
+
closeModal();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div>
|
|
36
|
+
<ModalHeader closeModal={closeModal}>
|
|
37
|
+
<span className={styles.header}>{t('hieModal', 'HIE Patient Record Found')}</span>
|
|
38
|
+
</ModalHeader>
|
|
39
|
+
<ModalBody>
|
|
40
|
+
<div style={{ display: 'flex', margin: '1rem' }}>
|
|
41
|
+
<ExtensionSlot
|
|
42
|
+
style={{ display: 'flex', alignItems: 'center' }}
|
|
43
|
+
name="patient-photo-slot"
|
|
44
|
+
state={{ patientName: `${firstName} ${lastName}` }}
|
|
45
|
+
/>
|
|
46
|
+
<div style={{ width: '100%', marginLeft: '0.625rem' }}>
|
|
47
|
+
<PatientInfo label={t('healthID', 'HealthID')} value={patient?.id} />
|
|
48
|
+
<PatientInfo label={t('patientName', 'Patient name')} value={`${firstName} ${lastName}`} />
|
|
49
|
+
<PatientInfo label={t('age', 'Age')} value={age(patient?.birthDate)} />
|
|
50
|
+
<PatientInfo label={t('dateOfBirth', 'Date of birth')} value={formatDate(new Date(patient?.birthDate))} />
|
|
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('')}
|
|
55
|
+
/>
|
|
56
|
+
<PatientInfo label={t('dependents', 'Dependents')} value="--" />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<Accordion>
|
|
61
|
+
<AccordionItem title={t('viewFullResponse', 'View full response')}>
|
|
62
|
+
<CodeSnippet type="multi" feedback="Copied to clipboard">
|
|
63
|
+
{JSON.stringify(patient, null, 2)}
|
|
64
|
+
</CodeSnippet>
|
|
65
|
+
</AccordionItem>
|
|
66
|
+
</Accordion>
|
|
67
|
+
</div>
|
|
68
|
+
</ModalBody>
|
|
69
|
+
<ModalFooter>
|
|
70
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
71
|
+
{t('cancel', 'Cancel')}
|
|
72
|
+
</Button>
|
|
73
|
+
|
|
74
|
+
<Button onClick={handleUseValues} kind="primary">
|
|
75
|
+
{t('useValues', 'Use values')}
|
|
76
|
+
</Button>
|
|
77
|
+
</ModalFooter>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default HIEConfirmationModal;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ConceptAnswers,
|
|
7
7
|
type ConceptResponse,
|
|
8
8
|
type FormValues,
|
|
9
|
-
} from '
|
|
9
|
+
} from '../../patient-registration/patient-registration.types';
|
|
10
10
|
|
|
11
11
|
export function searchClientRegistry(
|
|
12
12
|
identifierType: string,
|
|
@@ -2,7 +2,7 @@ import { showModal } from '@openmrs/esm-framework';
|
|
|
2
2
|
import { type FormikProps } from 'formik';
|
|
3
3
|
import { type ClientRegistryPatient, type RegistryPatient } from './verification-types';
|
|
4
4
|
import counties from './assets/counties.json';
|
|
5
|
-
import { type FormValues } from '
|
|
5
|
+
import { type FormValues } from '../../patient-registration/patient-registration.types';
|
|
6
6
|
import { capitalize } from 'lodash-es';
|
|
7
7
|
|
|
8
8
|
export function handleClientRegistryResponse(
|
|
@@ -7,7 +7,7 @@ import { searchClientRegistry, useGlobalProperties } from './patient-verificatio
|
|
|
7
7
|
import { showSnackbar, showToast } from '@openmrs/esm-framework';
|
|
8
8
|
import { handleClientRegistryResponse } from './patient-verification-utils';
|
|
9
9
|
import { type FormikProps } from 'formik';
|
|
10
|
-
import { type FormValues } from '
|
|
10
|
+
import { type FormValues } from '../../patient-registration/patient-registration.types';
|
|
11
11
|
|
|
12
12
|
interface PatientVerificationProps {
|
|
13
13
|
props: FormikProps<FormValues>;
|
package/src/{patient-verification → client-registry/patient-verification}/patient-verification.scss
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
@use '@carbon/layout';
|
|
3
3
|
@use '@carbon/type';
|
|
4
4
|
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
-
@
|
|
5
|
+
@use '../../patient-registration/patient-registration.scss' as *;
|
|
6
6
|
|
|
7
7
|
/* Desktop */
|
|
8
8
|
:global(.omrs-breakpoint-gt-tablet) {
|
|
@@ -5,21 +5,24 @@ import { Button } from '@carbon/react';
|
|
|
5
5
|
interface EmptyPromptProps {
|
|
6
6
|
onConfirm: void;
|
|
7
7
|
close: void;
|
|
8
|
+
title?: string;
|
|
9
|
+
message?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
const EmptyPrompt: React.FC<EmptyPromptProps> = ({ close, onConfirm }) => {
|
|
12
|
+
const EmptyPrompt: React.FC<EmptyPromptProps> = ({ close, onConfirm, title, message }) => {
|
|
11
13
|
const { t } = useTranslation();
|
|
12
14
|
return (
|
|
13
15
|
<>
|
|
14
16
|
<div className="cds--modal-header">
|
|
15
|
-
<h3 className="cds--modal-header__heading">{t('clientRegistryEmpty', 'Create & Post Patient')}</h3>
|
|
17
|
+
<h3 className="cds--modal-header__heading">{title ?? t('clientRegistryEmpty', 'Create & Post Patient')}</h3>
|
|
16
18
|
</div>
|
|
17
19
|
<div className="cds--modal-content">
|
|
18
20
|
<p>
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
{message ??
|
|
22
|
+
t(
|
|
23
|
+
'patientNotFound',
|
|
24
|
+
'The patient records could not be found in Client registry, do you want to continue to create and post patient to registry',
|
|
25
|
+
)}
|
|
23
26
|
</p>
|
|
24
27
|
</div>
|
|
25
28
|
<div className="cds--modal-footer">
|
package/src/config-schema.ts
CHANGED
|
@@ -83,6 +83,11 @@ export interface RegistrationConfig {
|
|
|
83
83
|
encounterProviderRoleUuid: string;
|
|
84
84
|
registrationFormUuid: string | null;
|
|
85
85
|
};
|
|
86
|
+
hieClientRegistry: {
|
|
87
|
+
identifierTypes: Array<{ identifierType: string; identifierValue: string }>;
|
|
88
|
+
baseUrl: string;
|
|
89
|
+
encodedCredentials: string;
|
|
90
|
+
};
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export const builtInSections: Array<SectionDefinition> = [
|
|
@@ -364,6 +369,38 @@ export const esmPatientRegistrationSchema = {
|
|
|
364
369
|
'The form UUID to associate with the registration encounter. By default no form will be associated.',
|
|
365
370
|
},
|
|
366
371
|
},
|
|
372
|
+
hieClientRegistry: {
|
|
373
|
+
identifierTypes: {
|
|
374
|
+
_type: Type.Array,
|
|
375
|
+
_elements: {
|
|
376
|
+
identifierType: {
|
|
377
|
+
_type: Type.String,
|
|
378
|
+
_description: 'The label of the identifier type',
|
|
379
|
+
},
|
|
380
|
+
identifierValue: {
|
|
381
|
+
_type: Type.String,
|
|
382
|
+
_description: 'The value of the identifier type',
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
_default: [
|
|
386
|
+
{ identifierType: 'National ID', identifierValue: 'national-id' },
|
|
387
|
+
{ identifierType: 'Passport Number', identifierValue: 'passport-number' },
|
|
388
|
+
{ identifierType: 'Birth Certificate Number', identifierValue: 'birth-certificate-number' },
|
|
389
|
+
{ identifierType: 'Alien ID Number', identifierValue: 'alien-id-number' },
|
|
390
|
+
{ identifierType: 'Refugee ID Number', identifierValue: 'refugee-number' },
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
baseUrl: {
|
|
394
|
+
_type: Type.String,
|
|
395
|
+
_default: 'https://hie.paperless.co.ke/v4/custom/',
|
|
396
|
+
_description: 'The base URL for the HIE API',
|
|
397
|
+
},
|
|
398
|
+
encodedCredentials: {
|
|
399
|
+
_type: Type.String,
|
|
400
|
+
_default: 'a2VueWFfZW1yOkFsZG5iJmtmayZKc2whMjM0',
|
|
401
|
+
_description: 'The base64 encoded credentials for the HIE API',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
367
404
|
_validators: [
|
|
368
405
|
validator(
|
|
369
406
|
(config: RegistrationConfig) =>
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import rootComponent from './root.component';
|
|
|
6
6
|
import addPatientLinkComponent from './add-patient-link';
|
|
7
7
|
import editPatientDetailsButtonComponent from './widgets/edit-patient-details-button.component';
|
|
8
8
|
import { PatientPhotoExtension } from './patient-photo.extension';
|
|
9
|
+
import HIEConfirmationModal from './client-registry/hie-client-registry/modal/confirm-hie.modal';
|
|
9
10
|
|
|
10
11
|
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
11
12
|
|
|
@@ -68,11 +69,13 @@ export const deleteIdentifierConfirmationModal = getAsyncLifecycle(
|
|
|
68
69
|
);
|
|
69
70
|
|
|
70
71
|
export const confirmClientRegistryModal = getAsyncLifecycle(
|
|
71
|
-
() => import('./patient-verification/verification-modal/confirm-prompt.component'),
|
|
72
|
+
() => import('./client-registry/patient-verification/verification-modal/confirm-prompt.component'),
|
|
72
73
|
options,
|
|
73
74
|
);
|
|
74
75
|
|
|
75
76
|
export const emptyClientRegistryModal = getAsyncLifecycle(
|
|
76
|
-
() => import('./patient-verification/verification-modal/empty-prompt.component'),
|
|
77
|
+
() => import('./client-registry/patient-verification/verification-modal/empty-prompt.component'),
|
|
77
78
|
options,
|
|
78
79
|
);
|
|
80
|
+
|
|
81
|
+
export const hieConfirmationModal = getSyncLifecycle(HIEConfirmationModal, options);
|