@ampath/esm-patient-registration-app 6.0.1-pre.6
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 +41 -0
- package/README.md +7 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/249.js +2 -0
- package/dist/249.js.LICENSE.txt +46 -0
- package/dist/249.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/255.js.LICENSE.txt +9 -0
- package/dist/255.js.map +1 -0
- package/dist/271.js +1 -0
- package/dist/303.js +1 -0
- package/dist/303.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/365.js +1 -0
- package/dist/365.js.map +1 -0
- package/dist/460.js +1 -0
- package/dist/525.js +1 -0
- package/dist/525.js.map +1 -0
- package/dist/537.js +1 -0
- package/dist/537.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/621.js +1 -0
- package/dist/621.js.map +1 -0
- package/dist/644.js +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/735.js +1 -0
- package/dist/735.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/879.js +1 -0
- package/dist/879.js.map +1 -0
- package/dist/ampath-esm-patient-registration-app.js +1 -0
- package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
- package/dist/ampath-esm-patient-registration-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +56 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/docs/images/patient-registration-hierarchy.png +0 -0
- package/jest.config.js +3 -0
- package/package.json +61 -0
- package/src/add-patient-link.scss +3 -0
- package/src/add-patient-link.test.tsx +20 -0
- package/src/add-patient-link.tsx +21 -0
- package/src/config-schema.ts +410 -0
- package/src/constants.ts +14 -0
- package/src/declarations.d.ts +6 -0
- package/src/index.ts +71 -0
- package/src/nav-link.test.tsx +13 -0
- package/src/nav-link.tsx +10 -0
- package/src/offline.resources.ts +155 -0
- package/src/offline.ts +91 -0
- package/src/patient-registration/before-save-prompt.tsx +73 -0
- package/src/patient-registration/date-util.ts +52 -0
- package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
- package/src/patient-registration/field/address/address-field.component.tsx +153 -0
- package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
- package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
- package/src/patient-registration/field/address/address-search.component.tsx +85 -0
- package/src/patient-registration/field/address/address-search.scss +53 -0
- package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
- package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
- package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
- package/src/patient-registration/field/custom-field.component.tsx +25 -0
- package/src/patient-registration/field/dob/dob.component.tsx +159 -0
- package/src/patient-registration/field/dob/dob.test.tsx +75 -0
- package/src/patient-registration/field/field.component.tsx +47 -0
- package/src/patient-registration/field/field.resource.ts +35 -0
- package/src/patient-registration/field/field.scss +127 -0
- package/src/patient-registration/field/field.test.tsx +294 -0
- package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
- package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
- package/src/patient-registration/field/id/id-field.component.tsx +144 -0
- package/src/patient-registration/field/id/id-field.test.tsx +107 -0
- package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
- package/src/patient-registration/field/id/identifier-selection.scss +37 -0
- package/src/patient-registration/field/name/name-field.component.tsx +142 -0
- package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
- package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
- package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
- package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
- package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
- package/src/patient-registration/form-manager.test.ts +67 -0
- package/src/patient-registration/form-manager.ts +414 -0
- package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
- package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
- package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
- package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
- package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
- package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
- package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
- package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
- package/src/patient-registration/input/input.scss +118 -0
- package/src/patient-registration/patient-registration-context.ts +24 -0
- package/src/patient-registration/patient-registration-hooks.ts +287 -0
- package/src/patient-registration/patient-registration-utils.ts +216 -0
- package/src/patient-registration/patient-registration.component.tsx +240 -0
- package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
- package/src/patient-registration/patient-registration.resource.ts +250 -0
- package/src/patient-registration/patient-registration.scss +122 -0
- package/src/patient-registration/patient-registration.test.tsx +471 -0
- package/src/patient-registration/patient-registration.types.ts +318 -0
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
- package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
- package/src/patient-registration/section/generic-section.component.tsx +17 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
- package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
- package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
- package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
- package/src/patient-registration/section/section.component.tsx +23 -0
- package/src/patient-registration/section/section.scss +1 -0
- package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
- package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
- package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
- package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
- package/src/patient-verification/client-registry-constants.ts +13 -0
- package/src/patient-verification/client-registry.component.tsx +66 -0
- package/src/patient-verification/client-registry.scss +1 -0
- package/src/patient-verification/utils.tsx +56 -0
- package/src/patient-verification/verification-modal.scss +20 -0
- package/src/patient-verification/verification.component.tsx +48 -0
- package/src/resource.ts +12 -0
- package/src/root.component.tsx +63 -0
- package/src/root.scss +7 -0
- package/src/root.test.tsx +32 -0
- package/src/routes.json +66 -0
- package/src/widgets/cancel-patient-edit.component.tsx +37 -0
- package/src/widgets/cancel-patient-edit.test.tsx +27 -0
- package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
- package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
- package/src/widgets/delete-identifier-modal.scss +34 -0
- package/src/widgets/display-photo.component.tsx +30 -0
- package/src/widgets/display-photo.test.tsx +37 -0
- package/src/widgets/edit-patient-details-button.component.tsx +34 -0
- package/src/widgets/edit-patient-details-button.scss +3 -0
- package/src/widgets/edit-patient-details-button.test.tsx +41 -0
- package/translations/am.json +97 -0
- package/translations/ar.json +97 -0
- package/translations/en.json +103 -0
- package/translations/es.json +97 -0
- package/translations/fr.json +97 -0
- package/translations/he.json +97 -0
- package/translations/km.json +97 -0
- package/translations/zh.json +89 -0
- package/translations/zh_CN.json +89 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Button, Link, InlineLoading, Dropdown } from '@carbon/react';
|
|
4
|
+
import { XAxis } from '@carbon/react/icons';
|
|
5
|
+
import { useLocation, useParams } from 'react-router-dom';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Formik, Form, type FormikHelpers } from 'formik';
|
|
8
|
+
import {
|
|
9
|
+
createErrorHandler,
|
|
10
|
+
showSnackbar,
|
|
11
|
+
useConfig,
|
|
12
|
+
interpolateUrl,
|
|
13
|
+
usePatient,
|
|
14
|
+
showModal,
|
|
15
|
+
} from '@openmrs/esm-framework';
|
|
16
|
+
import { getValidationSchema } from './validation/patient-registration-validation';
|
|
17
|
+
import { type FormValues, type CapturePhotoProps } from './patient-registration.types';
|
|
18
|
+
import { PatientRegistrationContext } from './patient-registration-context';
|
|
19
|
+
import { type SavePatientForm, SavePatientTransactionManager } from './form-manager';
|
|
20
|
+
import { fetchPatientRecordFromClientRegistry, usePatientPhoto, fetchPerson } from './patient-registration.resource';
|
|
21
|
+
import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
|
|
22
|
+
import { cancelRegistration, filterUndefinedPatientIdenfier, scrollIntoView } from './patient-registration-utils';
|
|
23
|
+
import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks';
|
|
24
|
+
import { ResourcesContext } from '../offline.resources';
|
|
25
|
+
import { builtInSections, type RegistrationConfig, type SectionDefinition } from '../config-schema';
|
|
26
|
+
import { SectionWrapper } from './section/section-wrapper.component';
|
|
27
|
+
import BeforeSavePrompt from './before-save-prompt';
|
|
28
|
+
import styles from './patient-registration.scss';
|
|
29
|
+
import { TextInput } from '@carbon/react';
|
|
30
|
+
import { ClientRegistry } from '../patient-verification/client-registry.component';
|
|
31
|
+
|
|
32
|
+
let exportedInitialFormValuesForTesting = {} as FormValues;
|
|
33
|
+
|
|
34
|
+
export interface PatientRegistrationProps {
|
|
35
|
+
savePatientForm: SavePatientForm;
|
|
36
|
+
isOffline: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePatientForm, isOffline }) => {
|
|
40
|
+
const { currentSession, addressTemplate, identifierTypes } = useContext(ResourcesContext);
|
|
41
|
+
const { search } = useLocation();
|
|
42
|
+
const config = useConfig() as RegistrationConfig;
|
|
43
|
+
const [target, setTarget] = useState<undefined | string>();
|
|
44
|
+
const { patientUuid: uuidOfPatientToEdit } = useParams();
|
|
45
|
+
const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(uuidOfPatientToEdit);
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
const [capturePhotoProps, setCapturePhotoProps] = useState<CapturePhotoProps | null>(null);
|
|
48
|
+
const [initialFormValues, setInitialFormValues] = useInitialFormValues(uuidOfPatientToEdit);
|
|
49
|
+
const [initialAddressFieldValues] = useInitialAddressFieldValues(uuidOfPatientToEdit);
|
|
50
|
+
const [patientUuidMap] = usePatientUuidMap(uuidOfPatientToEdit);
|
|
51
|
+
const location = currentSession?.sessionLocation?.uuid;
|
|
52
|
+
const inEditMode = isLoadingPatientToEdit ? undefined : !!(uuidOfPatientToEdit && patientToEdit);
|
|
53
|
+
const showDummyData = useMemo(() => localStorage.getItem('openmrs:devtools') === 'true' && !inEditMode, [inEditMode]);
|
|
54
|
+
const { data: photo } = usePatientPhoto(patientToEdit?.id);
|
|
55
|
+
const savePatientTransactionManager = useRef(new SavePatientTransactionManager());
|
|
56
|
+
const fieldDefinition = config?.fieldDefinitions?.filter((def) => def.type === 'address');
|
|
57
|
+
const validationSchema = getValidationSchema(config);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
exportedInitialFormValuesForTesting = initialFormValues;
|
|
61
|
+
}, [initialFormValues]);
|
|
62
|
+
|
|
63
|
+
const sections: Array<SectionDefinition> = useMemo(() => {
|
|
64
|
+
return config.sections
|
|
65
|
+
.map(
|
|
66
|
+
(sectionName) =>
|
|
67
|
+
config.sectionDefinitions.filter((s) => s.id == sectionName)[0] ??
|
|
68
|
+
builtInSections.filter((s) => s.id == sectionName)[0],
|
|
69
|
+
)
|
|
70
|
+
.filter((s) => s);
|
|
71
|
+
}, [config.sections, config.sectionDefinitions]);
|
|
72
|
+
|
|
73
|
+
const onFormSubmit = async (values: FormValues, helpers: FormikHelpers<FormValues>) => {
|
|
74
|
+
const abortController = new AbortController();
|
|
75
|
+
helpers.setSubmitting(true);
|
|
76
|
+
|
|
77
|
+
const updatedFormValues = { ...values, identifiers: filterUndefinedPatientIdenfier(values.identifiers) };
|
|
78
|
+
try {
|
|
79
|
+
await savePatientForm(
|
|
80
|
+
!inEditMode,
|
|
81
|
+
updatedFormValues,
|
|
82
|
+
patientUuidMap,
|
|
83
|
+
initialAddressFieldValues,
|
|
84
|
+
capturePhotoProps,
|
|
85
|
+
location,
|
|
86
|
+
initialFormValues['identifiers'],
|
|
87
|
+
currentSession,
|
|
88
|
+
config,
|
|
89
|
+
savePatientTransactionManager.current,
|
|
90
|
+
abortController,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
showSnackbar({
|
|
94
|
+
subtitle: inEditMode
|
|
95
|
+
? t('updatePatientSuccessSnackbarSubtitle', "The patient's information has been successfully updated")
|
|
96
|
+
: t(
|
|
97
|
+
'registerPatientSuccessSnackbarSubtitle',
|
|
98
|
+
'The patient can now be found by searching for them using their name or ID number',
|
|
99
|
+
),
|
|
100
|
+
title: inEditMode
|
|
101
|
+
? t('updatePatientSuccessSnackbarTitle', 'Patient Details Updated')
|
|
102
|
+
: t('registerPatientSuccessSnackbarTitle', 'New Patient Created'),
|
|
103
|
+
kind: 'success',
|
|
104
|
+
isLowContrast: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const afterUrl = new URLSearchParams(search).get('afterUrl');
|
|
108
|
+
const redirectUrl = interpolateUrl(afterUrl || config.links.submitButton, { patientUuid: values.patientUuid });
|
|
109
|
+
|
|
110
|
+
setTarget(redirectUrl);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error.responseBody?.error?.globalErrors) {
|
|
113
|
+
error.responseBody.error.globalErrors.forEach((error) => {
|
|
114
|
+
showSnackbar({
|
|
115
|
+
title: inEditMode
|
|
116
|
+
? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
|
|
117
|
+
: t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
|
|
118
|
+
subtitle: error.message,
|
|
119
|
+
kind: 'error',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
} else if (error.responseBody?.error?.message) {
|
|
123
|
+
showSnackbar({
|
|
124
|
+
title: inEditMode
|
|
125
|
+
? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
|
|
126
|
+
: t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
|
|
127
|
+
subtitle: error.responseBody.error.message,
|
|
128
|
+
kind: 'error',
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
createErrorHandler()(error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
helpers.setSubmitting(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const displayErrors = (errors) => {
|
|
139
|
+
if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
|
|
140
|
+
Object.keys(errors).forEach((error) => {
|
|
141
|
+
showSnackbar({
|
|
142
|
+
subtitle: t(`${error}LabelText`, error),
|
|
143
|
+
title: t('incompleteForm', 'The following field has errors:'),
|
|
144
|
+
kind: 'warning',
|
|
145
|
+
isLowContrast: true,
|
|
146
|
+
timeoutInMs: 5000,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Formik
|
|
154
|
+
enableReinitialize
|
|
155
|
+
initialValues={initialFormValues}
|
|
156
|
+
validationSchema={validationSchema}
|
|
157
|
+
onSubmit={onFormSubmit}>
|
|
158
|
+
{(props) => (
|
|
159
|
+
<Form className={styles.form}>
|
|
160
|
+
<BeforeSavePrompt when={props.dirty} redirect={target} />
|
|
161
|
+
<div className={styles.formContainer}>
|
|
162
|
+
<div>
|
|
163
|
+
<div className={styles.stickyColumn}>
|
|
164
|
+
<h4>
|
|
165
|
+
{inEditMode ? t('edit', 'Edit') : t('createNew', 'Create New')} {t('patient', 'Patient')}
|
|
166
|
+
</h4>
|
|
167
|
+
{showDummyData && <DummyDataInput setValues={props.setValues} />}
|
|
168
|
+
<p className={styles.label01}>{t('jumpTo', 'Jump to')}</p>
|
|
169
|
+
{sections.map((section) => (
|
|
170
|
+
<div className={classNames(styles.space05, styles.touchTarget)} key={section.name}>
|
|
171
|
+
<Link className={styles.linkName} onClick={() => scrollIntoView(section.id)}>
|
|
172
|
+
<XAxis size={16} /> {t(`${section.id}Section`, section.name)}
|
|
173
|
+
</Link>
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
<Button
|
|
177
|
+
className={styles.submitButton}
|
|
178
|
+
type="submit"
|
|
179
|
+
onClick={() => props.validateForm().then((errors) => displayErrors(errors))}
|
|
180
|
+
// Current session and identifiers are required for patient registration.
|
|
181
|
+
// If currentSession or identifierTypes are not available, then the
|
|
182
|
+
// user should be blocked to register the patient.
|
|
183
|
+
disabled={!currentSession || !identifierTypes || props.isSubmitting}>
|
|
184
|
+
{props.isSubmitting ? (
|
|
185
|
+
<InlineLoading
|
|
186
|
+
className={styles.spinner}
|
|
187
|
+
description={`${t('submitting', 'Submitting')} ...`}
|
|
188
|
+
iconDescription="submitting"
|
|
189
|
+
status="active"
|
|
190
|
+
/>
|
|
191
|
+
) : inEditMode ? (
|
|
192
|
+
t('updatePatient', 'Update Patient')
|
|
193
|
+
) : (
|
|
194
|
+
t('registerPatient', 'Register Patient')
|
|
195
|
+
)}
|
|
196
|
+
</Button>
|
|
197
|
+
<Button className={styles.cancelButton} kind="tertiary" onClick={cancelRegistration}>
|
|
198
|
+
{t('cancel', 'Cancel')}
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
<div className={styles.infoGrid}>
|
|
203
|
+
<PatientRegistrationContext.Provider
|
|
204
|
+
value={{
|
|
205
|
+
identifierTypes: identifierTypes,
|
|
206
|
+
validationSchema,
|
|
207
|
+
values: props.values,
|
|
208
|
+
inEditMode,
|
|
209
|
+
setFieldValue: props.setFieldValue,
|
|
210
|
+
setCapturePhotoProps,
|
|
211
|
+
currentPhoto: photo?.imageSrc,
|
|
212
|
+
isOffline,
|
|
213
|
+
initialFormValues: props.initialValues,
|
|
214
|
+
setInitialFormValues,
|
|
215
|
+
}}>
|
|
216
|
+
<ClientRegistry initialFormValues={initialFormValues} setInitialFormValues={setInitialFormValues} />
|
|
217
|
+
{sections.map((section, index) => (
|
|
218
|
+
<SectionWrapper
|
|
219
|
+
key={`registration-section-${section.id}`}
|
|
220
|
+
sectionDefinition={section}
|
|
221
|
+
index={index}
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
</PatientRegistrationContext.Provider>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</Form>
|
|
228
|
+
)}
|
|
229
|
+
</Formik>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @internal
|
|
235
|
+
* Just exported for testing
|
|
236
|
+
*/
|
|
237
|
+
export { exportedInitialFormValuesForTesting as initialFormValues };
|
|
238
|
+
function dispose() {
|
|
239
|
+
throw new Error('Function not implemented.');
|
|
240
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
|
+
import { savePatient } from './patient-registration.resource';
|
|
3
|
+
|
|
4
|
+
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
5
|
+
|
|
6
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
7
|
+
openmrsFetch: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('savePatient', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
jest.resetAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('appends patient uuid in url if provided', () => {
|
|
16
|
+
mockOpenmrsFetch.mockImplementationOnce((url) => url);
|
|
17
|
+
savePatient(null, '1234');
|
|
18
|
+
expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual('/ws/rest/v1/patient/1234');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not append patient uuid in url', () => {
|
|
22
|
+
mockOpenmrsFetch.mockImplementationOnce(() => {});
|
|
23
|
+
savePatient(null);
|
|
24
|
+
expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual('/ws/rest/v1/patient/');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import { openmrsFetch, useConfig } from '@openmrs/esm-framework';
|
|
3
|
+
import { type Patient, type Relationship, type PatientIdentifier, type Encounter } from './patient-registration.types';
|
|
4
|
+
|
|
5
|
+
export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334';
|
|
6
|
+
export const uuidTelephoneNumber = '14d4f066-15f5-102d-96e4-000c29c2a5d7';
|
|
7
|
+
|
|
8
|
+
function dataURItoFile(dataURI: string) {
|
|
9
|
+
const byteString = atob(dataURI.split(',')[1]);
|
|
10
|
+
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
|
11
|
+
// write the bytes of the string to a typed array
|
|
12
|
+
const buffer = new Uint8Array(byteString.length);
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
15
|
+
buffer[i] = byteString.charCodeAt(i);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const blob = new Blob([buffer], { type: mimeString });
|
|
19
|
+
return new File([blob], 'patient-photo.png');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function savePatient(patient: Patient | null, updatePatientUuid?: string) {
|
|
23
|
+
const abortController = new AbortController();
|
|
24
|
+
|
|
25
|
+
return openmrsFetch(`/ws/rest/v1/patient/${updatePatientUuid || ''}`, {
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
},
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: patient,
|
|
31
|
+
signal: abortController.signal,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveEncounter(encounter: Encounter) {
|
|
36
|
+
const abortController = new AbortController();
|
|
37
|
+
|
|
38
|
+
return openmrsFetch(`/ws/rest/v1/encounter`, {
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: encounter,
|
|
44
|
+
signal: abortController.signal,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function generateIdentifier(source: string) {
|
|
49
|
+
const abortController = new AbortController();
|
|
50
|
+
|
|
51
|
+
return openmrsFetch(`/ws/rest/v1/idgen/identifiersource/${source}/identifier`, {
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
method: 'POST',
|
|
56
|
+
body: {},
|
|
57
|
+
signal: abortController.signal,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function deletePersonName(nameUuid: string, personUuid: string) {
|
|
62
|
+
const abortController = new AbortController();
|
|
63
|
+
|
|
64
|
+
return openmrsFetch(`/ws/rest/v1/person/${personUuid}/name/${nameUuid}`, {
|
|
65
|
+
method: 'DELETE',
|
|
66
|
+
signal: abortController.signal,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveRelationship(relationship: Relationship) {
|
|
71
|
+
const abortController = new AbortController();
|
|
72
|
+
|
|
73
|
+
return openmrsFetch('/ws/rest/v1/relationship', {
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: relationship,
|
|
79
|
+
signal: abortController.signal,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function updateRelationship(relationshipUuid, relationship: { relationshipType: string }) {
|
|
84
|
+
const abortController = new AbortController();
|
|
85
|
+
|
|
86
|
+
return openmrsFetch(`/ws/rest/v1/relationship/${relationshipUuid}`, {
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
},
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: { relationshipType: relationship.relationshipType },
|
|
92
|
+
signal: abortController.signal,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function deleteRelationship(relationshipUuid) {
|
|
97
|
+
const abortController = new AbortController();
|
|
98
|
+
|
|
99
|
+
return openmrsFetch(`/ws/rest/v1/relationship/${relationshipUuid}`, {
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
method: 'DELETE',
|
|
104
|
+
signal: abortController.signal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function savePatientPhoto(
|
|
109
|
+
patientUuid: string,
|
|
110
|
+
content: string,
|
|
111
|
+
url: string,
|
|
112
|
+
date: string,
|
|
113
|
+
conceptUuid: string,
|
|
114
|
+
) {
|
|
115
|
+
const abortController = new AbortController();
|
|
116
|
+
|
|
117
|
+
const formData = new FormData();
|
|
118
|
+
formData.append('patient', patientUuid);
|
|
119
|
+
formData.append('file', dataURItoFile(content));
|
|
120
|
+
formData.append(
|
|
121
|
+
'json',
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
person: patientUuid,
|
|
124
|
+
concept: conceptUuid,
|
|
125
|
+
groupMembers: [],
|
|
126
|
+
obsDatetime: date,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return openmrsFetch(url, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
signal: abortController.signal,
|
|
133
|
+
body: formData,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ObsFetchResponse {
|
|
138
|
+
results: Array<PhotoObs>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface PhotoObs {
|
|
142
|
+
display: string;
|
|
143
|
+
obsDatetime: string;
|
|
144
|
+
uuid: string;
|
|
145
|
+
value: {
|
|
146
|
+
display: string;
|
|
147
|
+
links: {
|
|
148
|
+
rel: string;
|
|
149
|
+
uri: string;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface UsePatientPhotoResult {
|
|
155
|
+
data: { dateTime: string; imageSrc: string } | null;
|
|
156
|
+
isError: Error;
|
|
157
|
+
isLoading: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function usePatientPhoto(patientUuid: string): UsePatientPhotoResult {
|
|
161
|
+
const {
|
|
162
|
+
concepts: { patientPhotoUuid },
|
|
163
|
+
} = useConfig();
|
|
164
|
+
const url = `/ws/rest/v1/obs?patient=${patientUuid}&concept=${patientPhotoUuid}&v=full`;
|
|
165
|
+
|
|
166
|
+
const { data, error, isLoading } = useSWR<{ data: ObsFetchResponse }, Error>(patientUuid ? url : null, openmrsFetch);
|
|
167
|
+
|
|
168
|
+
const item = data?.data?.results[0];
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
data: item
|
|
172
|
+
? {
|
|
173
|
+
dateTime: item?.obsDatetime,
|
|
174
|
+
imageSrc: item?.value?.links?.uri,
|
|
175
|
+
}
|
|
176
|
+
: null,
|
|
177
|
+
isError: error,
|
|
178
|
+
isLoading,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function fetchPerson(query: string, abortController: AbortController) {
|
|
183
|
+
const [patientsRes, personsRes] = await Promise.all([
|
|
184
|
+
openmrsFetch(`/ws/rest/v1/patient?q=${query}`, {
|
|
185
|
+
signal: abortController.signal,
|
|
186
|
+
}),
|
|
187
|
+
openmrsFetch(`/ws/rest/v1/person?q=${query}`, {
|
|
188
|
+
signal: abortController.signal,
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const results = [...patientsRes.data.results];
|
|
193
|
+
|
|
194
|
+
personsRes.data.results.forEach((person) => {
|
|
195
|
+
if (!results.some((patient) => patient.uuid === person.uuid)) {
|
|
196
|
+
results.push(person);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function addPatientIdentifier(patientUuid: string, patientIdentifier: PatientIdentifier) {
|
|
204
|
+
const abortController = new AbortController();
|
|
205
|
+
return openmrsFetch(`/ws/rest/v1/patient/${patientUuid}/identifier/`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: {
|
|
208
|
+
'Content-Type': 'application/json',
|
|
209
|
+
},
|
|
210
|
+
signal: abortController.signal,
|
|
211
|
+
body: patientIdentifier,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function updatePatientIdentifier(patientUuid: string, identifierUuid: string, identifier: string) {
|
|
216
|
+
const abortController = new AbortController();
|
|
217
|
+
return openmrsFetch(`/ws/rest/v1/patient/${patientUuid}/identifier/${identifierUuid}`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: {
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
},
|
|
222
|
+
signal: abortController.signal,
|
|
223
|
+
body: { identifier },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function deletePatientIdentifier(patientUuid: string, patientIdentifierUuid: string) {
|
|
228
|
+
const abortController = new AbortController();
|
|
229
|
+
return openmrsFetch(`/ws/rest/v1/patient/${patientUuid}/identifier/${patientIdentifierUuid}?purge`, {
|
|
230
|
+
method: 'DELETE',
|
|
231
|
+
headers: {
|
|
232
|
+
'Content-Type': 'application/json',
|
|
233
|
+
},
|
|
234
|
+
signal: abortController.signal,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
export const fetchPatientRecordFromClientRegistry = (
|
|
238
|
+
patientIdentifier: string,
|
|
239
|
+
identifierType: string,
|
|
240
|
+
country: string,
|
|
241
|
+
) => {
|
|
242
|
+
const url = `
|
|
243
|
+
https://ngx.ampath.or.ke/registry/api/uno?uno=${patientIdentifier}&idType=${identifierType}&countryCode=${country}`;
|
|
244
|
+
|
|
245
|
+
return fetch(url)
|
|
246
|
+
.then((response) => {
|
|
247
|
+
return response.json();
|
|
248
|
+
})
|
|
249
|
+
.then((data) => data);
|
|
250
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.title {
|
|
6
|
+
color: var(--omrs-color-brand-teal);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.submit {
|
|
10
|
+
width: 250px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.submit:hover {
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cancelButton {
|
|
18
|
+
width: 11.688rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.submitButton {
|
|
22
|
+
margin-bottom: spacing.$spacing-05;
|
|
23
|
+
width: 11.688rem;
|
|
24
|
+
display: block;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.infoGrid {
|
|
28
|
+
width: 100%;
|
|
29
|
+
padding-left: spacing.$spacing-07;
|
|
30
|
+
margin-bottom: 40vh;
|
|
31
|
+
margin-top: spacing.$spacing-05;
|
|
32
|
+
max-width: 50rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.label01 {
|
|
36
|
+
@include type.type-style('label-01');
|
|
37
|
+
margin-top: spacing.$spacing-05;
|
|
38
|
+
margin-bottom: spacing.$spacing-05;
|
|
39
|
+
color: $ui-04;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.productiveHeading02 {
|
|
43
|
+
@include type.type-style('heading-compact-02');
|
|
44
|
+
color: $ui-04;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.space05 {
|
|
49
|
+
margin: spacing.$spacing-05 0 spacing.$spacing-05 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.formContainer {
|
|
53
|
+
display: flex;
|
|
54
|
+
width: 100%;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.stickyColumn {
|
|
58
|
+
position: sticky;
|
|
59
|
+
margin-top: spacing.$spacing-05;
|
|
60
|
+
// 3rem for the nav height and 1rem for top margin
|
|
61
|
+
top: 4rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.touchTarget a:active {
|
|
65
|
+
color: $color-gray-100;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.linkName {
|
|
69
|
+
color: $color-gray-70;
|
|
70
|
+
line-height: 1.38;
|
|
71
|
+
font-size: spacing.$spacing-05;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
|
|
74
|
+
&:active {
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
color: $color-gray-100;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&:hover {
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
color: $color-gray-100;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.main {
|
|
87
|
+
background-color: white;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
91
|
+
.infoGrid {
|
|
92
|
+
max-width: unset;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.spinner {
|
|
97
|
+
&:global(.cds--inline-loading) {
|
|
98
|
+
min-height: 1rem;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Overriding styles for RTL support
|
|
103
|
+
html[dir='rtl'] {
|
|
104
|
+
.linkName {
|
|
105
|
+
& > svg {
|
|
106
|
+
transform: scale(-1, 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.infoGrid {
|
|
111
|
+
padding-left: unset;
|
|
112
|
+
padding-right: spacing.$spacing-07;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
.patientVerification {
|
|
116
|
+
& > * {
|
|
117
|
+
margin-top: spacing.$spacing-03;
|
|
118
|
+
}
|
|
119
|
+
& > :last-child {
|
|
120
|
+
margin-top: spacing.$spacing-03;
|
|
121
|
+
}
|
|
122
|
+
}
|