@kenyaemr/esm-patient-registration-app 8.0.1-pre.99 → 8.0.2
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 +23 -22
- package/dist/108.js +1 -0
- package/dist/108.js.map +1 -0
- package/dist/130.js +1 -1
- package/dist/130.js.LICENSE.txt +2 -0
- package/dist/130.js.map +1 -1
- package/dist/2.js +1 -0
- package/dist/2.js.map +1 -0
- package/dist/250.js +1 -0
- package/dist/250.js.map +1 -0
- package/dist/271.js +1 -1
- package/dist/319.js +1 -1
- package/dist/325.js +1 -0
- package/dist/325.js.map +1 -0
- package/dist/372.js +2 -0
- package/dist/372.js.map +1 -0
- package/dist/460.js +1 -1
- package/dist/574.js +1 -1
- package/dist/644.js +1 -1
- package/dist/66.js +1 -0
- package/dist/66.js.map +1 -0
- package/dist/662.js +1 -0
- package/dist/662.js.map +1 -0
- package/dist/757.js +1 -1
- package/dist/{59.js → 76.js} +1 -1
- package/dist/{59.js.map → 76.js.map} +1 -1
- package/dist/788.js +1 -1
- package/dist/807.js +1 -1
- package/dist/833.js +1 -1
- package/dist/895.js +2 -0
- package/dist/895.js.LICENSE.txt +34 -0
- package/dist/895.js.map +1 -0
- package/dist/kenyaemr-esm-patient-registration-app.js +1 -1
- package/dist/kenyaemr-esm-patient-registration-app.js.buildmanifest.json +161 -188
- package/dist/kenyaemr-esm-patient-registration-app.js.map +1 -1
- 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-lock.json +6047 -0
- package/package.json +3 -4
- 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 +162 -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 +72 -2
- package/src/index.ts +6 -6
- package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
- package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +84 -0
- package/src/patient-registration/field/dob/dob.component.tsx +21 -7
- package/src/patient-registration/field/field.component.tsx +11 -5
- package/src/patient-registration/field/field.resource.ts +11 -4
- package/src/patient-registration/field/field.scss +44 -5
- package/src/patient-registration/field/gender/gender-field.component.tsx +2 -1
- package/src/patient-registration/field/gender/gender-field.test.tsx +1 -0
- package/src/patient-registration/field/id/id-field.component.tsx +8 -6
- package/src/patient-registration/field/id/id-field.test.tsx +27 -8
- package/src/patient-registration/field/name/name-field.component.tsx +5 -1
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +1 -0
- package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx +76 -27
- package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
- package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +12 -1
- package/src/patient-registration/field/person-attributes/useUpdateIdentifierRequirement.tsx +83 -0
- package/src/patient-registration/form-manager.test.ts +21 -0
- package/src/patient-registration/form-manager.ts +40 -20
- package/src/patient-registration/input/basic-input/input/input.component.tsx +5 -1
- package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +18 -10
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +166 -67
- package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +3 -0
- package/src/patient-registration/input/input.scss +5 -0
- package/src/patient-registration/patient-registration-context.ts +4 -3
- package/src/patient-registration/patient-registration-hooks.ts +67 -9
- package/src/patient-registration/patient-registration-utils.ts +3 -7
- package/src/patient-registration/patient-registration.component.tsx +44 -30
- package/src/patient-registration/patient-registration.resource.ts +8 -0
- package/src/patient-registration/patient-registration.test.tsx +9 -3
- package/src/patient-registration/patient-registration.types.ts +4 -1
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +22 -17
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +4 -14
- package/src/patient-registration/section/section.component.tsx +1 -1
- package/src/patient-registration/section/section.scss +5 -0
- package/src/patient-registration/validation/{patient-registration-validation.test.tsx → patient-registration-validation.test.ts} +26 -4
- package/src/patient-registration/validation/patient-registration-validation.ts +126 -0
- package/src/routes.json +14 -17
- package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
- package/src/widgets/cancel-patient-edit.test.tsx +2 -3
- package/src/widgets/delete-identifier-confirmation.modal.tsx +22 -15
- package/src/widgets/delete-identifier-confirmation.test.tsx +2 -1
- package/translations/am.json +36 -25
- package/translations/ar.json +37 -26
- package/translations/en.json +51 -20
- package/translations/es.json +38 -26
- package/translations/fr.json +47 -35
- package/translations/he.json +37 -30
- package/translations/km.json +37 -30
- package/translations/zh.json +37 -20
- package/translations/zh_CN.json +37 -20
- package/dist/152.js +0 -1
- package/dist/152.js.map +0 -1
- package/dist/255.js +0 -2
- package/dist/255.js.map +0 -1
- package/dist/303.js +0 -1
- package/dist/303.js.map +0 -1
- 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/623.js +0 -1
- package/dist/623.js.map +0 -1
- package/dist/729.js +0 -1
- package/dist/729.js.map +0 -1
- package/dist/735.js +0 -1
- package/dist/735.js.map +0 -1
- package/dist/831.js +0 -2
- package/dist/831.js.LICENSE.txt +0 -14
- package/dist/831.js.map +0 -1
- package/src/patient-registration/validation/patient-registration-validation.tsx +0 -60
- package/src/widgets/cancel-patient-edit.component.tsx +0 -37
- package/src/widgets/delete-identifier-confirmation.scss +0 -34
- /package/dist/{255.js.LICENSE.txt → 372.js.LICENSE.txt} +0 -0
- /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
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { Button, Link, InlineLoading } from '@carbon/react';
|
|
4
4
|
import { XAxis, ShareKnowledge } from '@carbon/react/icons';
|
|
5
5
|
import { useLocation, useParams } from 'react-router-dom';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import {
|
|
7
|
+
import { Form, Formik, type FormikHelpers } from 'formik';
|
|
8
8
|
import {
|
|
9
9
|
createErrorHandler,
|
|
10
|
+
interpolateUrl,
|
|
10
11
|
showSnackbar,
|
|
11
12
|
useConfig,
|
|
12
|
-
interpolateUrl,
|
|
13
13
|
usePatient,
|
|
14
14
|
usePatientPhoto,
|
|
15
|
+
useFeatureFlag,
|
|
15
16
|
} from '@openmrs/esm-framework';
|
|
16
17
|
import { getValidationSchema } from './validation/patient-registration-validation';
|
|
17
|
-
import { type
|
|
18
|
+
import { type CapturePhotoProps, type FormValues } from './patient-registration.types';
|
|
18
19
|
import { PatientRegistrationContext } from './patient-registration-context';
|
|
19
20
|
import { type SavePatientForm, SavePatientTransactionManager } from './form-manager';
|
|
20
21
|
import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
|
|
21
22
|
import { cancelRegistration, filterOutUndefinedPatientIdentifiers, scrollIntoView } from './patient-registration-utils';
|
|
22
|
-
import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap
|
|
23
|
+
import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks';
|
|
23
24
|
import { ResourcesContext } from '../offline.resources';
|
|
24
25
|
import { builtInSections, type RegistrationConfig, type SectionDefinition } from '../config-schema';
|
|
25
26
|
import { SectionWrapper } from './section/section-wrapper.component';
|
|
26
27
|
import BeforeSavePrompt from './before-save-prompt';
|
|
27
28
|
import styles from './patient-registration.scss';
|
|
28
|
-
import PatientVerification from '../patient-verification/patient-verification.component';
|
|
29
|
-
import { handleSavePatientToClientRegistry } from '../patient-verification/patient-verification-hook';
|
|
29
|
+
import PatientVerification from '../client-registry/patient-verification/patient-verification.component';
|
|
30
|
+
import { handleSavePatientToClientRegistry } from '../client-registry/patient-verification/patient-verification-hook';
|
|
31
|
+
import ClientRegistry from '../client-registry/client-registry.component';
|
|
30
32
|
|
|
31
33
|
let exportedInitialFormValuesForTesting = {} as FormValues;
|
|
32
34
|
|
|
@@ -36,6 +38,7 @@ export interface PatientRegistrationProps {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePatientForm, isOffline }) => {
|
|
41
|
+
const healthInformationExchangeFlag = useFeatureFlag('healthInformationExchange');
|
|
39
42
|
const { currentSession, identifierTypes } = useContext(ResourcesContext);
|
|
40
43
|
const { search } = useLocation();
|
|
41
44
|
const config = useConfig() as RegistrationConfig;
|
|
@@ -158,16 +161,24 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
|
|
|
158
161
|
}
|
|
159
162
|
};
|
|
160
163
|
|
|
164
|
+
const getDescription = (errors) => {
|
|
165
|
+
return (
|
|
166
|
+
<ul style={{ listStyle: 'inside' }}>
|
|
167
|
+
{Object.keys(errors).map((error, index) => {
|
|
168
|
+
return <li key={index}>{t(`${error}LabelText`, error)}</li>;
|
|
169
|
+
})}
|
|
170
|
+
</ul>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
const enableRegistryButton = healthInformationExchangeFlag ? false : !enableClientRegistry;
|
|
174
|
+
|
|
161
175
|
const displayErrors = (errors) => {
|
|
162
176
|
if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
isLowContrast: true,
|
|
169
|
-
timeoutInMs: 5000,
|
|
170
|
-
});
|
|
177
|
+
showSnackbar({
|
|
178
|
+
isLowContrast: true,
|
|
179
|
+
kind: 'warning',
|
|
180
|
+
title: t('fieldsWithErrors', 'The following fields have errors:'),
|
|
181
|
+
subtitle: <>{getDescription(errors)}</>,
|
|
171
182
|
});
|
|
172
183
|
}
|
|
173
184
|
};
|
|
@@ -180,7 +191,7 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
|
|
|
180
191
|
onSubmit={onFormSubmit}>
|
|
181
192
|
{(props) => (
|
|
182
193
|
<Form className={styles.form}>
|
|
183
|
-
<BeforeSavePrompt when={props.
|
|
194
|
+
<BeforeSavePrompt when={Object.keys(props.touched).length > 0} redirect={target} />
|
|
184
195
|
<div className={styles.formContainer}>
|
|
185
196
|
<div>
|
|
186
197
|
<div className={styles.stickyColumn}>
|
|
@@ -198,18 +209,20 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
|
|
|
198
209
|
</Link>
|
|
199
210
|
</div>
|
|
200
211
|
))}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
{!healthInformationExchangeFlag && (
|
|
213
|
+
<Button
|
|
214
|
+
renderIcon={ShareKnowledge}
|
|
215
|
+
disabled={!currentSession || !identifierTypes}
|
|
216
|
+
onClick={() => {
|
|
217
|
+
setEnableClientRegistry(true);
|
|
218
|
+
props.isValid
|
|
219
|
+
? handleSavePatientToClientRegistry(props.values, props.setValues, inEditMode)
|
|
220
|
+
: props.validateForm().then((errors) => displayErrors(errors));
|
|
221
|
+
}}
|
|
222
|
+
className={styles.submitButton}>
|
|
223
|
+
{t('postToRegistry', 'Post to registry')}
|
|
224
|
+
</Button>
|
|
225
|
+
)}
|
|
213
226
|
<Button
|
|
214
227
|
className={styles.submitButton}
|
|
215
228
|
type="submit"
|
|
@@ -217,7 +230,7 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
|
|
|
217
230
|
// Current session and identifiers are required for patient registration.
|
|
218
231
|
// If currentSession or identifierTypes are not available, then the
|
|
219
232
|
// user should be blocked to register the patient.
|
|
220
|
-
disabled={!currentSession || !identifierTypes || props.isSubmitting ||
|
|
233
|
+
disabled={!currentSession || !identifierTypes || props.isSubmitting || enableRegistryButton}>
|
|
221
234
|
{props.isSubmitting ? (
|
|
222
235
|
<InlineLoading
|
|
223
236
|
className={styles.spinner}
|
|
@@ -243,13 +256,14 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
|
|
|
243
256
|
values: props.values,
|
|
244
257
|
inEditMode,
|
|
245
258
|
setFieldValue: props.setFieldValue,
|
|
259
|
+
setFieldTouched: props.setFieldTouched,
|
|
246
260
|
setCapturePhotoProps,
|
|
247
261
|
currentPhoto: photo?.imageSrc,
|
|
248
262
|
isOffline,
|
|
249
263
|
initialFormValues: props.initialValues,
|
|
250
264
|
setInitialFormValues,
|
|
251
265
|
}}>
|
|
252
|
-
<
|
|
266
|
+
<ClientRegistry props={props} setInitialFormValues={setInitialFormValues} />
|
|
253
267
|
{sections.map((section, index) => (
|
|
254
268
|
<SectionWrapper
|
|
255
269
|
key={`registration-section-${section.id}`}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
2
|
import { type Patient, type Relationship, type PatientIdentifier, type Encounter } from './patient-registration.types';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
3
4
|
|
|
4
5
|
export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334';
|
|
5
6
|
export const uuidTelephoneNumber = '14d4f066-15f5-102d-96e4-000c29c2a5d7';
|
|
@@ -188,3 +189,10 @@ export async function deletePatientIdentifier(patientUuid: string, patientIdenti
|
|
|
188
189
|
signal: abortController.signal,
|
|
189
190
|
});
|
|
190
191
|
}
|
|
192
|
+
|
|
193
|
+
export function getDatetime(date: Date | string, time: string, timeFormat: 'AM' | 'PM') {
|
|
194
|
+
const datetime = new Date(date);
|
|
195
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
196
|
+
const fullHours = timeFormat === 'PM' ? (hours % 12) + 12 : hours % 12;
|
|
197
|
+
return dayjs(datetime).hour(fullHours).minute(minutes).second(0).millisecond(0).toDate();
|
|
198
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { mockedAddressTemplate } from '__mocks__';
|
|
15
15
|
import { mockPatient } from 'tools';
|
|
16
16
|
import { saveEncounter, savePatient } from './patient-registration.resource';
|
|
17
|
-
import { type RegistrationConfig
|
|
17
|
+
import { esmPatientRegistrationSchema, type RegistrationConfig } from '../config-schema';
|
|
18
18
|
import type { AddressTemplate, Encounter } from './patient-registration.types';
|
|
19
19
|
import { ResourcesContext } from '../offline.resources';
|
|
20
20
|
import { FormManager } from './form-manager';
|
|
@@ -167,6 +167,9 @@ let mockOpenmrsConfig: RegistrationConfig = {
|
|
|
167
167
|
searchAddressByLevel: true,
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
|
+
causeOfDeath: {
|
|
171
|
+
conceptUuid: 'cause-of-death-concept-uuid',
|
|
172
|
+
},
|
|
170
173
|
},
|
|
171
174
|
links: {
|
|
172
175
|
submitButton: '#',
|
|
@@ -407,7 +410,7 @@ describe('Updating an existing patient record', () => {
|
|
|
407
410
|
const givenNameInput: HTMLInputElement = screen.getByLabelText(/First Name/);
|
|
408
411
|
const familyNameInput: HTMLInputElement = screen.getByLabelText(/Family Name/);
|
|
409
412
|
const middleNameInput: HTMLInputElement = screen.getByLabelText(/Middle Name/);
|
|
410
|
-
const dateOfBirthInput: HTMLInputElement = screen.getByLabelText(
|
|
413
|
+
const dateOfBirthInput: HTMLInputElement = screen.getByLabelText(/Date of Birth/i);
|
|
411
414
|
const genderInput: HTMLInputElement = screen.getByLabelText(/Male/);
|
|
412
415
|
|
|
413
416
|
// assert initial values
|
|
@@ -443,7 +446,10 @@ describe('Updating an existing patient record', () => {
|
|
|
443
446
|
birthdate: new Date('1972-04-04T00:00:00.000Z'),
|
|
444
447
|
birthdateEstimated: false,
|
|
445
448
|
deathCause: '',
|
|
446
|
-
|
|
449
|
+
nonCodedCauseOfDeath: '',
|
|
450
|
+
deathDate: undefined,
|
|
451
|
+
deathTime: undefined,
|
|
452
|
+
deathTimeFormat: 'AM',
|
|
447
453
|
familyName: 'Smith',
|
|
448
454
|
gender: expect.stringMatching(/male/i),
|
|
449
455
|
givenName: 'Eric',
|
|
@@ -167,7 +167,9 @@ export interface FormValues {
|
|
|
167
167
|
birthdate: Date | string;
|
|
168
168
|
birthdateEstimated: boolean;
|
|
169
169
|
deathCause: string;
|
|
170
|
-
deathDate: string;
|
|
170
|
+
deathDate: string | Date;
|
|
171
|
+
deathTime: string;
|
|
172
|
+
deathTimeFormat: 'AM' | 'PM';
|
|
171
173
|
familyName: string;
|
|
172
174
|
gender: string;
|
|
173
175
|
givenName: string;
|
|
@@ -177,6 +179,7 @@ export interface FormValues {
|
|
|
177
179
|
isDead: boolean;
|
|
178
180
|
middleName: string;
|
|
179
181
|
monthsEstimated: number;
|
|
182
|
+
nonCodedCauseOfDeath: string;
|
|
180
183
|
obs?: {
|
|
181
184
|
[conceptUuid: string]: string;
|
|
182
185
|
};
|
|
@@ -1,30 +1,35 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import classNames from 'classnames';
|
|
1
|
+
import React, { useContext } from 'react';
|
|
3
2
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { Checkbox, Layer } from '@carbon/react';
|
|
4
|
+
import { useField } from 'formik';
|
|
5
|
+
import { Field } from '../../field/field.component';
|
|
6
6
|
import { PatientRegistrationContext } from '../../patient-registration-context';
|
|
7
7
|
import styles from './../section.scss';
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
9
|
+
export interface DeathInfoSectionProps {
|
|
10
|
+
fields: Array<string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DeathInfoSection: React.FC<DeathInfoSectionProps> = ({ fields }) => {
|
|
11
14
|
const { t } = useTranslation();
|
|
15
|
+
const { values, setFieldValue } = useContext(PatientRegistrationContext);
|
|
16
|
+
const [deathDate, deathDateMeta] = useField('deathDate');
|
|
17
|
+
const today = new Date();
|
|
12
18
|
|
|
13
19
|
return (
|
|
14
20
|
<section className={styles.formSection} aria-label="Death Info Section">
|
|
15
|
-
<h5 className={classNames('omrs-type-title-5', styles.formSectionTitle)}>Death Info</h5>
|
|
16
21
|
<section className={styles.fieldGroup}>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
name="deathCause"
|
|
22
|
+
<Layer>
|
|
23
|
+
<div className={styles.isDeadFieldContainer}>
|
|
24
|
+
<Checkbox
|
|
25
|
+
checked={values.isDead}
|
|
26
|
+
id="isDead"
|
|
27
|
+
labelText={t('isDeadInputLabel', 'Is dead')}
|
|
28
|
+
onChange={(event, { checked, id }) => setFieldValue(id, checked)}
|
|
25
29
|
/>
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
</div>
|
|
31
|
+
</Layer>
|
|
32
|
+
{values.isDead ? fields.map((field) => <Field key={`death-info-${field}`} name={field} />) : null}
|
|
28
33
|
</section>
|
|
29
34
|
</section>
|
|
30
35
|
);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import {
|
|
3
|
+
import { Form, Formik } from 'formik';
|
|
4
4
|
import { initialFormValues } from '../../patient-registration.component';
|
|
5
|
-
import { DeathInfoSection } from './death-info-section.component';
|
|
6
5
|
import { type FormValues } from '../../patient-registration.types';
|
|
7
6
|
import { PatientRegistrationContext } from '../../patient-registration-context';
|
|
7
|
+
import { DeathInfoSection } from './death-info-section.component';
|
|
8
8
|
|
|
9
9
|
const initialContextValues = {
|
|
10
10
|
currentPhoto: 'data:image/png;base64,1234567890',
|
|
@@ -29,7 +29,7 @@ describe('Death info section', () => {
|
|
|
29
29
|
<PatientRegistrationContext.Provider value={initialContextValues}>
|
|
30
30
|
<Formik initialValues={initialFormValues} onSubmit={jest.fn()}>
|
|
31
31
|
<Form>
|
|
32
|
-
<DeathInfoSection />
|
|
32
|
+
<DeathInfoSection fields={[]} />
|
|
33
33
|
</Form>
|
|
34
34
|
</Formik>
|
|
35
35
|
</PatientRegistrationContext.Provider>,
|
|
@@ -40,16 +40,6 @@ describe('Death info section', () => {
|
|
|
40
40
|
renderDeathInfoSection(true);
|
|
41
41
|
|
|
42
42
|
expect(screen.getByRole('region', { name: /death info section/i })).toBeInTheDocument();
|
|
43
|
-
expect(screen.getByRole('
|
|
44
|
-
expect(screen.getByRole('textbox', { name: /is dead \(optional\)/i })).toBeInTheDocument();
|
|
45
|
-
expect(screen.getByRole('textbox', { name: /date of death \(optional\)/i })).toBeInTheDocument();
|
|
46
|
-
expect(screen.getByRole('combobox', { name: /cause of death \(optional\)/i })).toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('has the correct number of inputs if is dead is not checked', async () => {
|
|
50
|
-
renderDeathInfoSection(false);
|
|
51
|
-
|
|
52
|
-
expect(screen.queryByRole('textbox', { name: /date of death \(optional\)/i })).not.toBeInTheDocument();
|
|
53
|
-
expect(screen.queryByRole('combobox', { name: /cause of death \(optional\)/i })).not.toBeInTheDocument();
|
|
43
|
+
expect(screen.getByRole('checkbox', { name: /is dead/i })).toBeInTheDocument();
|
|
54
44
|
});
|
|
55
45
|
});
|
|
@@ -14,7 +14,7 @@ export function Section({ sectionDefinition }: SectionProps) {
|
|
|
14
14
|
case 'demographics':
|
|
15
15
|
return <DemographicsSection fields={sectionDefinition.fields} />;
|
|
16
16
|
case 'death':
|
|
17
|
-
return <DeathInfoSection />;
|
|
17
|
+
return <DeathInfoSection fields={sectionDefinition.fields} />;
|
|
18
18
|
case 'relationships':
|
|
19
19
|
return <RelationshipsSection />;
|
|
20
20
|
default: // includes 'contact'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getConfig } from '@openmrs/esm-framework';
|
|
2
2
|
import { type RegistrationConfig } from '../../config-schema';
|
|
3
3
|
import { getValidationSchema } from './patient-registration-validation';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
4
5
|
|
|
5
6
|
const mockGetConfig = jest.mocked(getConfig);
|
|
6
7
|
|
|
@@ -35,6 +36,8 @@ describe('Patient registration validation', () => {
|
|
|
35
36
|
additionalGivenName: '',
|
|
36
37
|
birthdate: new Date('1990-01-01'),
|
|
37
38
|
birthdateEstimated: false,
|
|
39
|
+
isDead: false,
|
|
40
|
+
causeOfDeath: null,
|
|
38
41
|
deathDate: null,
|
|
39
42
|
email: 'john.doe@example.com',
|
|
40
43
|
familyName: 'Doe',
|
|
@@ -142,7 +145,7 @@ describe('Patient registration validation', () => {
|
|
|
142
145
|
expect(validationError).toBeFalsy();
|
|
143
146
|
});
|
|
144
147
|
|
|
145
|
-
it('should throw error when date of birth is a future date', async () => {
|
|
148
|
+
it('should throw an error when date of birth is a future date', async () => {
|
|
146
149
|
const invalidFormValues = {
|
|
147
150
|
...validFormValues,
|
|
148
151
|
birthdate: new Date('2100-01-01'),
|
|
@@ -151,6 +154,15 @@ describe('Patient registration validation', () => {
|
|
|
151
154
|
expect(validationError.errors).toContain('birthdayNotInTheFuture');
|
|
152
155
|
});
|
|
153
156
|
|
|
157
|
+
it('should throw an error when date of birth is more than 140 years ago', async () => {
|
|
158
|
+
const invalidFormValues = {
|
|
159
|
+
...validFormValues,
|
|
160
|
+
birthdate: dayjs().subtract(141, 'years').toDate(),
|
|
161
|
+
};
|
|
162
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
163
|
+
expect(validationError.errors).toContain('birthdayNotOver140YearsAgo');
|
|
164
|
+
});
|
|
165
|
+
|
|
154
166
|
it('should require yearsEstimated when birthdateEstimated is true', async () => {
|
|
155
167
|
const invalidFormValues = {
|
|
156
168
|
...validFormValues,
|
|
@@ -160,7 +172,7 @@ describe('Patient registration validation', () => {
|
|
|
160
172
|
expect(validationError.errors).toContain('yearsEstimateRequired');
|
|
161
173
|
});
|
|
162
174
|
|
|
163
|
-
it('should throw error when monthEstimated is negative', async () => {
|
|
175
|
+
it('should throw an error when monthEstimated is negative', async () => {
|
|
164
176
|
const invalidFormValues = {
|
|
165
177
|
...validFormValues,
|
|
166
178
|
birthdateEstimated: true,
|
|
@@ -171,12 +183,22 @@ describe('Patient registration validation', () => {
|
|
|
171
183
|
expect(validationError.errors).toContain('negativeMonths');
|
|
172
184
|
});
|
|
173
185
|
|
|
174
|
-
it('should throw error when
|
|
186
|
+
it('should throw an error when yearsEstimated is more than 140', async () => {
|
|
187
|
+
const invalidFormValues = {
|
|
188
|
+
...validFormValues,
|
|
189
|
+
birthdateEstimated: true,
|
|
190
|
+
yearsEstimated: 141,
|
|
191
|
+
};
|
|
192
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
193
|
+
expect(validationError.errors).toContain('nonsensicalYears');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should throw an error when deathDate is in future', async () => {
|
|
175
197
|
const invalidFormValues = {
|
|
176
198
|
...validFormValues,
|
|
177
199
|
deathDate: new Date('2100-01-01'),
|
|
178
200
|
};
|
|
179
201
|
const validationError = await validateFormValues(invalidFormValues);
|
|
180
|
-
expect(validationError.errors).toContain('
|
|
202
|
+
expect(validationError.errors).toContain('deathDateInFuture');
|
|
181
203
|
});
|
|
182
204
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import * as Yup from 'yup';
|
|
3
|
+
import mapValues from 'lodash/mapValues';
|
|
4
|
+
import { translateFrom } from '@openmrs/esm-framework';
|
|
5
|
+
import { type RegistrationConfig } from '../../config-schema';
|
|
6
|
+
import { type FormValues } from '../patient-registration.types';
|
|
7
|
+
import { getDatetime } from '../patient-registration.resource';
|
|
8
|
+
|
|
9
|
+
const t = (key: string, value: string) => translateFrom('@openmrs/esm-framework', key, value);
|
|
10
|
+
|
|
11
|
+
export function getValidationSchema(config: RegistrationConfig) {
|
|
12
|
+
return Yup.object({
|
|
13
|
+
givenName: Yup.string().required(t('givenNameRequired', 'Given name is required')),
|
|
14
|
+
familyName: Yup.string().required(t('familyNameRequired', 'Family name is required')),
|
|
15
|
+
additionalGivenName: Yup.string().when('addNameInLocalLanguage', {
|
|
16
|
+
is: true,
|
|
17
|
+
then: Yup.string().required(t('givenNameRequired', 'Given name is required')),
|
|
18
|
+
otherwise: Yup.string().notRequired(),
|
|
19
|
+
}),
|
|
20
|
+
additionalFamilyName: Yup.string().when('addNameInLocalLanguage', {
|
|
21
|
+
is: true,
|
|
22
|
+
then: Yup.string().required(t('familyNameRequired', 'Family name is required')),
|
|
23
|
+
otherwise: Yup.string().notRequired(),
|
|
24
|
+
}),
|
|
25
|
+
gender: Yup.string()
|
|
26
|
+
.oneOf(
|
|
27
|
+
config.fieldConfigurations.gender.map((g) => g.value),
|
|
28
|
+
t('genderUnspecified', 'Gender unspecified'),
|
|
29
|
+
)
|
|
30
|
+
.required(t('genderRequired', 'Gender is required')),
|
|
31
|
+
birthdate: Yup.date().when('birthdateEstimated', {
|
|
32
|
+
is: false,
|
|
33
|
+
then: Yup.date()
|
|
34
|
+
.required(t('birthdayRequired', 'Birthday is required'))
|
|
35
|
+
.max(Date(), t('birthdayNotInTheFuture', 'Birthday cannot be in future'))
|
|
36
|
+
.min(
|
|
37
|
+
dayjs().subtract(140, 'years').toDate(),
|
|
38
|
+
t('birthdayNotOver140YearsAgo', 'Birthday cannot be more than 140 years ago'),
|
|
39
|
+
)
|
|
40
|
+
.nullable(),
|
|
41
|
+
otherwise: Yup.date().nullable(),
|
|
42
|
+
}),
|
|
43
|
+
yearsEstimated: Yup.number().when('birthdateEstimated', {
|
|
44
|
+
is: true,
|
|
45
|
+
then: Yup.number()
|
|
46
|
+
.required(t('yearsEstimateRequired', 'Estimated years required'))
|
|
47
|
+
.min(0, t('negativeYears', 'Estimated years cannot be negative'))
|
|
48
|
+
.max(140, t('nonsensicalYears', 'Estimated years cannot be more than 140')),
|
|
49
|
+
otherwise: Yup.number().nullable(),
|
|
50
|
+
}),
|
|
51
|
+
monthsEstimated: Yup.number().min(0, t('negativeMonths', 'Estimated months cannot be negative')),
|
|
52
|
+
isDead: Yup.boolean(),
|
|
53
|
+
deathDate: Yup.date()
|
|
54
|
+
.when('isDead', {
|
|
55
|
+
is: true,
|
|
56
|
+
then: Yup.date().required(t('deathDateRequired', 'Death date is required')),
|
|
57
|
+
otherwise: Yup.date().nullable(),
|
|
58
|
+
})
|
|
59
|
+
.max(new Date(), 'deathDateInFuture')
|
|
60
|
+
.test(
|
|
61
|
+
'deathDate-after-birthdate',
|
|
62
|
+
t('deathdayInvalidDate', 'Death date and time cannot be before the birthday'),
|
|
63
|
+
function (value) {
|
|
64
|
+
const { birthdate } = this.parent;
|
|
65
|
+
if (birthdate && value) {
|
|
66
|
+
return dayjs(value).isAfter(birthdate);
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
.test('deathDate-before-today', t('deathDateInFuture', 'Death date cannot be in future'), function (value) {
|
|
72
|
+
const { deathTime, deathTimeFormat } = this.parent;
|
|
73
|
+
if (value && deathTime && deathTimeFormat && /^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/.test(deathTime)) {
|
|
74
|
+
return dayjs(getDatetime(value, deathTime, deathTimeFormat)).isBefore(dayjs());
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}),
|
|
78
|
+
deathTime: Yup.string()
|
|
79
|
+
.when('isDead', {
|
|
80
|
+
is: true,
|
|
81
|
+
then: Yup.string().required(t('deathTimeRequired', 'Death time is required')),
|
|
82
|
+
otherwise: Yup.string().nullable(),
|
|
83
|
+
})
|
|
84
|
+
.matches(/^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/, t('deathTimeInvalid', "Time doesn't match the format 'hh:mm'")),
|
|
85
|
+
|
|
86
|
+
deathTimeFormat: Yup.string()
|
|
87
|
+
.when('isDead', {
|
|
88
|
+
is: true,
|
|
89
|
+
then: Yup.string().required(t('deathTimeFormatRequired', 'Time format is required')),
|
|
90
|
+
otherwise: Yup.string().nullable(),
|
|
91
|
+
})
|
|
92
|
+
.oneOf(['AM', 'PM'], t('deathTimeFormatInvalid', 'Time format is invalid')),
|
|
93
|
+
|
|
94
|
+
deathCause: Yup.string().when('isDead', {
|
|
95
|
+
is: true,
|
|
96
|
+
then: Yup.string().required(t('deathCauseRequired', 'Cause of death is required')),
|
|
97
|
+
otherwise: Yup.string().nullable(),
|
|
98
|
+
}),
|
|
99
|
+
nonCodedCauseOfDeath: Yup.string().when(['isDead', 'deathCause'], {
|
|
100
|
+
is: (isDead, deathCause) => isDead && deathCause === config.freeTextFieldConceptUuid,
|
|
101
|
+
then: Yup.string().required(t('nonCodedCauseOfDeathRequired', 'Cause of death is required')),
|
|
102
|
+
otherwise: Yup.string().nullable(),
|
|
103
|
+
}),
|
|
104
|
+
email: Yup.string().optional().email(t('invalidEmail', 'Invalid email')),
|
|
105
|
+
identifiers: Yup.lazy((obj: FormValues['identifiers']) =>
|
|
106
|
+
Yup.object(
|
|
107
|
+
mapValues(obj, () =>
|
|
108
|
+
Yup.object({
|
|
109
|
+
required: Yup.bool(),
|
|
110
|
+
identifierValue: Yup.string().when('required', {
|
|
111
|
+
is: true,
|
|
112
|
+
then: Yup.string().required(t('identifierValueRequired', 'Identifier value is required')),
|
|
113
|
+
otherwise: Yup.string().notRequired(),
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
relationships: Yup.array().of(
|
|
120
|
+
Yup.object().shape({
|
|
121
|
+
relatedPersonUuid: Yup.string().required(),
|
|
122
|
+
relationshipType: Yup.string().required(),
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
});
|
|
126
|
+
}
|
package/src/routes.json
CHANGED
|
@@ -25,12 +25,6 @@
|
|
|
25
25
|
"online": true,
|
|
26
26
|
"offline": true
|
|
27
27
|
},
|
|
28
|
-
{
|
|
29
|
-
"component": "cancelPatientEditModal",
|
|
30
|
-
"name": "cancel-patient-edit-modal",
|
|
31
|
-
"online": true,
|
|
32
|
-
"offline": true
|
|
33
|
-
},
|
|
34
28
|
{
|
|
35
29
|
"component": "patientPhotoExtension",
|
|
36
30
|
"name": "patient-photo-widget",
|
|
@@ -51,25 +45,28 @@
|
|
|
51
45
|
"slot": "patient-search-actions-slot",
|
|
52
46
|
"online": true,
|
|
53
47
|
"offline": true
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"modals": [
|
|
51
|
+
{
|
|
52
|
+
"name": "cancel-patient-edit-modal",
|
|
53
|
+
"component": "cancelPatientEditModal"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"component": "deleteIdentifierConfirmationModal",
|
|
57
56
|
"name": "delete-identifier-confirmation-modal",
|
|
58
|
-
"
|
|
59
|
-
"offline": true
|
|
57
|
+
"component": "deleteIdentifierConfirmationModal"
|
|
60
58
|
},
|
|
61
59
|
{
|
|
62
60
|
"component": "emptyClientRegistryModal",
|
|
63
|
-
"name": "empty-client-registry-modal"
|
|
64
|
-
"online": true,
|
|
65
|
-
"offline": true
|
|
61
|
+
"name": "empty-client-registry-modal"
|
|
66
62
|
},
|
|
67
63
|
{
|
|
68
64
|
"component": "confirmClientRegistryModal",
|
|
69
|
-
"name": "confirm-client-registry-modal"
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
"name": "confirm-client-registry-modal"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"component": "hieConfirmationModal",
|
|
69
|
+
"name": "hie-confirmation-modal"
|
|
72
70
|
}
|
|
73
71
|
]
|
|
74
|
-
}
|
|
75
|
-
|
|
72
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
|
|
5
|
+
interface CancelPatientEditPropsModal {
|
|
6
|
+
close(): void;
|
|
7
|
+
onConfirm(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CancelPatientEditModal: React.FC<CancelPatientEditPropsModal> = ({ close, onConfirm }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<ModalHeader
|
|
15
|
+
closeModal={close}
|
|
16
|
+
title={t('confirmDiscardChangesTitle', 'Are you sure you want to discard these changes?')}
|
|
17
|
+
/>
|
|
18
|
+
<ModalBody>
|
|
19
|
+
<p>{t('confirmDiscardChangesBody', 'Your unsaved changes will be lost if you proceed to discard the form')}.</p>
|
|
20
|
+
</ModalBody>
|
|
21
|
+
<ModalFooter>
|
|
22
|
+
<Button kind="secondary" onClick={close}>
|
|
23
|
+
{t('cancel', 'Cancel')}
|
|
24
|
+
</Button>
|
|
25
|
+
<Button kind="danger" onClick={onConfirm}>
|
|
26
|
+
{t('discard', 'Discard')}
|
|
27
|
+
</Button>
|
|
28
|
+
</ModalFooter>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default CancelPatientEditModal;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { screen, render } from '@testing-library/react';
|
|
4
|
-
import CancelPatientEdit from './cancel-patient-edit.
|
|
4
|
+
import CancelPatientEdit from './cancel-patient-edit.modal';
|
|
5
5
|
|
|
6
|
-
describe('CancelPatientEdit
|
|
6
|
+
describe('CancelPatientEdit modal', () => {
|
|
7
7
|
const mockClose = jest.fn();
|
|
8
8
|
const mockOnConfirm = jest.fn();
|
|
9
9
|
|
|
10
10
|
it('renders the modal and triggers close and onConfirm functions', async () => {
|
|
11
11
|
const user = userEvent.setup();
|
|
12
|
-
|
|
13
12
|
render(<CancelPatientEdit close={mockClose} onConfirm={mockOnConfirm} />);
|
|
14
13
|
|
|
15
14
|
const cancelButton = screen.getByRole('button', { name: /Cancel/i });
|