@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,18 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { render, screen } from '@testing-library/react';
|
|
4
2
|
import { Form, Formik } from 'formik';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { type RegistrationConfig
|
|
9
|
-
import { type Resources
|
|
6
|
+
import { type AddressTemplate, type IdentifierSource } from '../../patient-registration.types';
|
|
7
|
+
import { mockIdentifierTypes, mockOpenmrsId, mockPatient, mockSession } from '__mocks__';
|
|
8
|
+
import { esmPatientRegistrationSchema, type RegistrationConfig } from '../../../config-schema';
|
|
9
|
+
import { ResourcesContext, type Resources } from '../../../offline.resources';
|
|
10
10
|
import { PatientRegistrationContext, type PatientRegistrationContextProps } from '../../patient-registration-context';
|
|
11
|
+
import { Identifiers, setIdentifierSource } from './id-field.component';
|
|
11
12
|
|
|
12
13
|
const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
|
|
13
14
|
|
|
14
15
|
const mockResourcesContextValue = {
|
|
15
|
-
addressTemplate: null,
|
|
16
|
+
addressTemplate: null as unknown as AddressTemplate,
|
|
16
17
|
currentSession: mockSession.data,
|
|
17
18
|
identifierTypes: [],
|
|
18
19
|
relationshipTypes: [],
|
|
@@ -52,7 +53,7 @@ const mockContextValues: PatientRegistrationContextProps = {
|
|
|
52
53
|
setInitialFormValues: jest.fn(),
|
|
53
54
|
validationSchema: null,
|
|
54
55
|
values: mockInitialFormValues,
|
|
55
|
-
};
|
|
56
|
+
} as unknown as PatientRegistrationContextProps;
|
|
56
57
|
|
|
57
58
|
describe('Identifiers', () => {
|
|
58
59
|
beforeEach(() => {
|
|
@@ -120,3 +121,21 @@ describe('Identifiers', () => {
|
|
|
120
121
|
expect(screen.getByRole('button', { name: 'Close overlay' })).toBeInTheDocument();
|
|
121
122
|
});
|
|
122
123
|
});
|
|
124
|
+
|
|
125
|
+
describe('setIdentifierSource', () => {
|
|
126
|
+
describe('auto-generation', () => {
|
|
127
|
+
it('should return auto-generated as the identifier value', () => {
|
|
128
|
+
const identifierSource = { autoGenerationOption: { automaticGenerationEnabled: true } } as IdentifierSource;
|
|
129
|
+
const { identifierValue } = setIdentifierSource(identifierSource, '', '');
|
|
130
|
+
expect(identifierValue).toBe('auto-generated');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return the identifier value when manual entry enabled', () => {
|
|
134
|
+
const identifierSource = {
|
|
135
|
+
autoGenerationOption: { automaticGenerationEnabled: true, manualEntryEnabled: true },
|
|
136
|
+
} as IdentifierSource;
|
|
137
|
+
const { identifierValue } = setIdentifierSource(identifierSource, '10001V', '');
|
|
138
|
+
expect(identifierValue).toBe('10001V');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -21,7 +21,7 @@ function checkNumber(value: string) {
|
|
|
21
21
|
|
|
22
22
|
export const NameField = () => {
|
|
23
23
|
const { t } = useTranslation();
|
|
24
|
-
const { setCapturePhotoProps, currentPhoto, setFieldValue } = useContext(PatientRegistrationContext);
|
|
24
|
+
const { setCapturePhotoProps, currentPhoto, setFieldValue, setFieldTouched } = useContext(PatientRegistrationContext);
|
|
25
25
|
const {
|
|
26
26
|
fieldConfigurations: {
|
|
27
27
|
name: {
|
|
@@ -48,6 +48,7 @@ export const NameField = () => {
|
|
|
48
48
|
imageData: dataUri,
|
|
49
49
|
dateTime: photoDateTime,
|
|
50
50
|
});
|
|
51
|
+
setFieldTouched('photo', true, false);
|
|
51
52
|
}
|
|
52
53
|
},
|
|
53
54
|
[setCapturePhotoProps],
|
|
@@ -63,6 +64,9 @@ export const NameField = () => {
|
|
|
63
64
|
setFieldValue('familyName', defaultUnknownFamilyName);
|
|
64
65
|
setUnknownPatient('true');
|
|
65
66
|
}
|
|
67
|
+
setFieldTouched('givenName', true);
|
|
68
|
+
setFieldTouched('familyName', true);
|
|
69
|
+
setFieldTouched(`attributes.${unidentifiedPatientAttributeTypeUuid}`, true, false);
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
const firstNameField = (
|
package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Field } from 'formik';
|
|
1
|
+
import React, { useContext, useEffect } from 'react';
|
|
2
|
+
import { Field, type FieldProps } from 'formik';
|
|
3
3
|
import { Layer, Select, SelectItem } from '@carbon/react';
|
|
4
|
-
import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
|
|
5
4
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import styles from './../field.scss';
|
|
7
5
|
import classNames from 'classnames';
|
|
6
|
+
import { PatientRegistrationContext } from '../../patient-registration-context';
|
|
7
|
+
import styles from './../field.scss';
|
|
8
|
+
import { ResourcesContext } from '../../../offline.resources';
|
|
9
|
+
import useUpdateIdentifierRequirement from './useUpdateIdentifierRequirement';
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
interface PersonAttributeTypeResponse {
|
|
12
|
+
uuid: string;
|
|
13
|
+
display?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ConceptAnswer {
|
|
17
|
+
uuid?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
showServiceExpression?: {
|
|
21
|
+
attributeTypeUuid: string;
|
|
22
|
+
value: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CustomPersonAttributeFieldProps {
|
|
10
27
|
id: string;
|
|
11
28
|
personAttributeType: PersonAttributeTypeResponse;
|
|
12
29
|
answerConceptSetUuid: string;
|
|
13
30
|
label?: string;
|
|
14
|
-
customConceptAnswers:
|
|
31
|
+
customConceptAnswers: ConceptAnswer[];
|
|
15
32
|
required: boolean;
|
|
16
|
-
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PatientRegistrationContextType {
|
|
36
|
+
setFieldValue: (field: string, value: any) => void;
|
|
37
|
+
values: {
|
|
38
|
+
attributes?: Record<string, string>;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
17
41
|
|
|
18
42
|
const CustomPersonAttributeField: React.FC<CustomPersonAttributeFieldProps> = ({
|
|
19
43
|
personAttributeType,
|
|
@@ -24,30 +48,55 @@ const CustomPersonAttributeField: React.FC<CustomPersonAttributeFieldProps> = ({
|
|
|
24
48
|
}) => {
|
|
25
49
|
const { t } = useTranslation();
|
|
26
50
|
const fieldName = `attributes.${personAttributeType.uuid}`;
|
|
51
|
+
const { setFieldValue, values } = useContext(PatientRegistrationContext);
|
|
52
|
+
useUpdateIdentifierRequirement(setFieldValue, values);
|
|
53
|
+
// TODO: Improve this logic
|
|
54
|
+
const filteredCustomConceptAnswers = customConceptAnswers.filter((answer) => {
|
|
55
|
+
const showExpression = answer.showServiceExpression;
|
|
56
|
+
if (!showExpression) return true;
|
|
57
|
+
|
|
58
|
+
const attributeValue = values?.attributes?.[showExpression.attributeTypeUuid];
|
|
59
|
+
const answerCadreId = answer.name;
|
|
60
|
+
|
|
61
|
+
if (answerCadreId == null) return true;
|
|
62
|
+
|
|
63
|
+
return showExpression.value.toLowerCase() === attributeValue?.toLowerCase();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
return () => {
|
|
68
|
+
setFieldValue(fieldName, '');
|
|
69
|
+
};
|
|
70
|
+
}, [fieldName, setFieldValue]);
|
|
71
|
+
|
|
72
|
+
const renderSelect = ({ field, form: { touched, errors } }: FieldProps) => {
|
|
73
|
+
const hasError = errors[fieldName] && touched[fieldName];
|
|
74
|
+
const displayLabel = label ?? personAttributeType?.display ?? '';
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Select
|
|
78
|
+
id={id}
|
|
79
|
+
name={`person-attribute-${personAttributeType.uuid}`}
|
|
80
|
+
labelText={displayLabel}
|
|
81
|
+
invalid={Boolean(hasError)}
|
|
82
|
+
required={required}
|
|
83
|
+
{...field}>
|
|
84
|
+
<SelectItem value="" text={t('selectAnOption', 'Select an option')} />
|
|
85
|
+
{filteredCustomConceptAnswers.map((answer) => (
|
|
86
|
+
<SelectItem
|
|
87
|
+
key={answer.uuid ?? answer.name}
|
|
88
|
+
value={answer.uuid ?? answer.name ?? ''}
|
|
89
|
+
text={answer.label ?? answer.uuid ?? answer.name ?? ''}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</Select>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
27
95
|
|
|
28
96
|
return (
|
|
29
97
|
<div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
|
|
30
98
|
<Layer>
|
|
31
|
-
<Field name={fieldName}>
|
|
32
|
-
{({ field, form: { touched, errors }, meta }) => {
|
|
33
|
-
return (
|
|
34
|
-
<>
|
|
35
|
-
<Select
|
|
36
|
-
id={id}
|
|
37
|
-
name={`person-attribute-${personAttributeType.uuid}`}
|
|
38
|
-
labelText={label ?? personAttributeType?.display}
|
|
39
|
-
invalid={errors[fieldName] && touched[fieldName]}
|
|
40
|
-
required={required}
|
|
41
|
-
{...field}>
|
|
42
|
-
<SelectItem value={''} text={t('selectAnOption', 'Select an option')} />
|
|
43
|
-
{customConceptAnswers.map((answer) => (
|
|
44
|
-
<SelectItem key={answer.uuid} value={answer.uuid} text={answer.uuid} />
|
|
45
|
-
))}
|
|
46
|
-
</Select>
|
|
47
|
-
</>
|
|
48
|
-
);
|
|
49
|
-
}}
|
|
50
|
-
</Field>
|
|
99
|
+
<Field name={fieldName}>{renderSelect}</Field>
|
|
51
100
|
</Layer>
|
|
52
101
|
</div>
|
|
53
102
|
);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Field, useField } from 'formik';
|
|
4
|
+
import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
|
|
5
|
+
import styles from './../field.scss';
|
|
6
|
+
import { useLocations } from './location-person-attribute-field.resource';
|
|
7
|
+
import { ComboBox, InlineLoading, Layer } from '@carbon/react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
|
|
10
|
+
export interface LocationPersonAttributeFieldProps {
|
|
11
|
+
id: string;
|
|
12
|
+
personAttributeType: PersonAttributeTypeResponse;
|
|
13
|
+
label?: string;
|
|
14
|
+
locationTag: string;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function LocationPersonAttributeField({
|
|
19
|
+
personAttributeType,
|
|
20
|
+
id,
|
|
21
|
+
label,
|
|
22
|
+
locationTag,
|
|
23
|
+
required,
|
|
24
|
+
}: LocationPersonAttributeFieldProps) {
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const fieldName = `attributes.${personAttributeType.uuid}`;
|
|
27
|
+
const [field, meta, { setValue }] = useField(`attributes.${personAttributeType.uuid}`);
|
|
28
|
+
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
29
|
+
const { locations, isLoading, loadingNewData } = useLocations(locationTag || null, searchQuery);
|
|
30
|
+
const prevLocationOptions = useRef([]);
|
|
31
|
+
|
|
32
|
+
const locationOptions = useMemo(() => {
|
|
33
|
+
if (!(isLoading && loadingNewData)) {
|
|
34
|
+
const newOptions = locations.map(({ resource: { id, name } }) => ({ value: id, label: name }));
|
|
35
|
+
prevLocationOptions.current = newOptions;
|
|
36
|
+
return newOptions;
|
|
37
|
+
}
|
|
38
|
+
return prevLocationOptions.current;
|
|
39
|
+
}, [locations, isLoading, loadingNewData]);
|
|
40
|
+
|
|
41
|
+
const selectedItem = useMemo(() => {
|
|
42
|
+
if (typeof meta.value === 'string') {
|
|
43
|
+
return locationOptions.find(({ value }) => value === meta.value) || null;
|
|
44
|
+
}
|
|
45
|
+
if (typeof meta.value === 'object' && meta.value) {
|
|
46
|
+
return locationOptions.find(({ value }) => value === meta.value.uuid) || null;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}, [locationOptions, meta.value]);
|
|
50
|
+
|
|
51
|
+
// Callback for when updating the combobox input
|
|
52
|
+
const handleInputChange = useCallback(
|
|
53
|
+
(value: string | null) => {
|
|
54
|
+
if (value) {
|
|
55
|
+
// If the value exists in the locationOptions (i.e. a label matches the input), exit the function
|
|
56
|
+
if (locationOptions.find(({ label }) => label === value)) return;
|
|
57
|
+
// If the input is a new value, set the search query
|
|
58
|
+
setSearchQuery(value);
|
|
59
|
+
// Clear the current selected value since the input doesn't match any existing options
|
|
60
|
+
setValue(null);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[locationOptions, setValue],
|
|
64
|
+
);
|
|
65
|
+
const handleSelect = useCallback(
|
|
66
|
+
({ selectedItem }) => {
|
|
67
|
+
if (selectedItem) {
|
|
68
|
+
setValue(selectedItem.value);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[setValue],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={classNames(styles.customField, styles.halfWidthInDesktopView, styles.locationAttributeFieldContainer)}>
|
|
77
|
+
<Layer>
|
|
78
|
+
<Field name={fieldName}>
|
|
79
|
+
{({ field, form: { touched, errors } }) => {
|
|
80
|
+
return (
|
|
81
|
+
<ComboBox
|
|
82
|
+
id={id}
|
|
83
|
+
name={`person-attribute-${personAttributeType.uuid}`}
|
|
84
|
+
titleText={label}
|
|
85
|
+
items={locationOptions}
|
|
86
|
+
placeholder={t('searchLocationPersonAttribute', 'Search location')}
|
|
87
|
+
onInputChange={handleInputChange}
|
|
88
|
+
required={required}
|
|
89
|
+
onChange={handleSelect}
|
|
90
|
+
selectedItem={selectedItem}
|
|
91
|
+
invalid={errors[fieldName] && touched[fieldName]}
|
|
92
|
+
typeahead
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}}
|
|
96
|
+
</Field>
|
|
97
|
+
</Layer>
|
|
98
|
+
{loadingNewData && (
|
|
99
|
+
<div className={styles.loadingContainer}>
|
|
100
|
+
<InlineLoading />
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { type FetchResponse, fhirBaseUrl, openmrsFetch, useDebounce } from '@openmrs/esm-framework';
|
|
3
|
+
import { type LocationEntry, type LocationResponse } from '@kenyaemr/esm-service-queues-app/src/types';
|
|
4
|
+
import useSWR from 'swr';
|
|
5
|
+
|
|
6
|
+
interface UseLocationsResult {
|
|
7
|
+
locations: Array<LocationEntry>;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
loadingNewData: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useLocations(locationTag: string | null, searchQuery: string = ''): UseLocationsResult {
|
|
13
|
+
const debouncedSearchQuery = useDebounce(searchQuery);
|
|
14
|
+
|
|
15
|
+
const constructUrl = useMemo(() => {
|
|
16
|
+
let url = `${fhirBaseUrl}/Location?`;
|
|
17
|
+
let urlSearchParameters = new URLSearchParams();
|
|
18
|
+
urlSearchParameters.append('_summary', 'data');
|
|
19
|
+
|
|
20
|
+
if (!debouncedSearchQuery) {
|
|
21
|
+
urlSearchParameters.append('_count', '10');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (locationTag) {
|
|
25
|
+
urlSearchParameters.append('_tag', locationTag);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof debouncedSearchQuery === 'string' && debouncedSearchQuery != '') {
|
|
29
|
+
urlSearchParameters.append('name:contains', debouncedSearchQuery);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return url + urlSearchParameters.toString();
|
|
33
|
+
}, [locationTag, debouncedSearchQuery]);
|
|
34
|
+
|
|
35
|
+
const { data, error, isLoading, isValidating } = useSWR<FetchResponse<LocationResponse>, Error>(
|
|
36
|
+
constructUrl,
|
|
37
|
+
openmrsFetch,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return useMemo(
|
|
41
|
+
() => ({
|
|
42
|
+
locations: data?.data?.entry || [],
|
|
43
|
+
isLoading,
|
|
44
|
+
loadingNewData: isValidating,
|
|
45
|
+
}),
|
|
46
|
+
[data, isLoading, isValidating],
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import { InlineNotification, TextInputSkeleton
|
|
2
|
+
import { InlineNotification, TextInputSkeleton } from '@carbon/react';
|
|
3
3
|
import { type FieldDefinition } from '../../../config-schema';
|
|
4
4
|
import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
|
|
5
5
|
import { usePersonAttributeType } from './person-attributes.resource';
|
|
6
6
|
import { TextPersonAttributeField } from './text-person-attribute-field.component';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import styles from '../field.scss';
|
|
9
|
+
import { LocationPersonAttributeField } from './location-person-attribute-field.component';
|
|
9
10
|
import CustomPersonAttributeField from './custom-person-attribute-field.component';
|
|
10
11
|
|
|
11
12
|
export interface PersonAttributeFieldProps {
|
|
@@ -55,6 +56,16 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
|
|
|
55
56
|
required={fieldDefinition.validation?.required ?? false}
|
|
56
57
|
/>
|
|
57
58
|
);
|
|
59
|
+
case 'org.openmrs.Location':
|
|
60
|
+
return (
|
|
61
|
+
<LocationPersonAttributeField
|
|
62
|
+
personAttributeType={personAttributeType}
|
|
63
|
+
locationTag={fieldDefinition.locationTag}
|
|
64
|
+
label={fieldDefinition.label}
|
|
65
|
+
id={fieldDefinition?.id}
|
|
66
|
+
required={fieldDefinition.validation?.required ?? false}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
58
69
|
default:
|
|
59
70
|
return (
|
|
60
71
|
<InlineNotification kind="error" title="Error">
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef, useMemo, useContext } from 'react';
|
|
2
|
+
import { deleteIdentifierType, initializeIdentifier } from '../id/id-field.component';
|
|
3
|
+
import { ResourcesContext } from '../../../offline.resources';
|
|
4
|
+
|
|
5
|
+
const useUpdateIdentifierRequirement = (setFieldValue, values) => {
|
|
6
|
+
const { identifierTypes = [] } = useContext(ResourcesContext);
|
|
7
|
+
const previousAttributes = useRef(values.attributes);
|
|
8
|
+
const previousIdentifiers = useRef(values.identifiers);
|
|
9
|
+
|
|
10
|
+
const publicationNumberIdentifier = useMemo(
|
|
11
|
+
() => identifierTypes?.find((identifier) => identifier.name === 'Publication Number'),
|
|
12
|
+
[identifierTypes],
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// Memoize the civilian check
|
|
16
|
+
const isCivilian = useMemo(() => Object.values(values.attributes ?? {}).includes('Civilian'), [values.attributes]);
|
|
17
|
+
|
|
18
|
+
// Memoize the identifier initialization logic
|
|
19
|
+
const initializePublicationIdentifier = useCallback(
|
|
20
|
+
(currentIdentifiers) => {
|
|
21
|
+
if (!publicationNumberIdentifier) {
|
|
22
|
+
console.warn('Publication Number identifier type not found');
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const initializedIdentifier = initializeIdentifier(
|
|
27
|
+
publicationNumberIdentifier,
|
|
28
|
+
currentIdentifiers[publicationNumberIdentifier.uuid],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return initializedIdentifier;
|
|
32
|
+
},
|
|
33
|
+
[publicationNumberIdentifier],
|
|
34
|
+
);
|
|
35
|
+
// Only run the effect if isCivilian is true
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// Skip if we don't have the required data
|
|
38
|
+
if (!values.attributes || !publicationNumberIdentifier) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if relevant values have actually changed
|
|
43
|
+
const attributesChanged = previousAttributes.current !== values.attributes;
|
|
44
|
+
const identifiersChanged = previousIdentifiers.current !== values.identifiers;
|
|
45
|
+
|
|
46
|
+
if (!attributesChanged && !identifiersChanged) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Update refs
|
|
51
|
+
previousAttributes.current = values.attributes;
|
|
52
|
+
previousIdentifiers.current = values.identifiers;
|
|
53
|
+
const isDependant = Object.values(values.attributes ?? {}).includes('Dependant');
|
|
54
|
+
// Only proceed if the user is a civilian
|
|
55
|
+
if (isCivilian && isDependant) {
|
|
56
|
+
const initializedIdentifier = initializePublicationIdentifier(values.identifiers);
|
|
57
|
+
|
|
58
|
+
// check if values.identifiers already has the publication number identifier
|
|
59
|
+
const hasPublicationNumberIdentifier = values.identifiers[publicationNumberIdentifier.fieldName];
|
|
60
|
+
|
|
61
|
+
if (initializedIdentifier && !hasPublicationNumberIdentifier) {
|
|
62
|
+
setFieldValue('identifiers', {
|
|
63
|
+
...values.identifiers,
|
|
64
|
+
[publicationNumberIdentifier.fieldName]: { ...initializedIdentifier, required: true },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Before deleting the publication number identifier, check if it exists
|
|
69
|
+
if (values.identifiers[publicationNumberIdentifier.fieldName]) {
|
|
70
|
+
setFieldValue('identifiers', deleteIdentifierType(values.identifiers, publicationNumberIdentifier.fieldName));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}, [
|
|
74
|
+
values.attributes,
|
|
75
|
+
values.identifiers,
|
|
76
|
+
isCivilian,
|
|
77
|
+
publicationNumberIdentifier,
|
|
78
|
+
initializePublicationIdentifier,
|
|
79
|
+
setFieldValue,
|
|
80
|
+
]);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default useUpdateIdentifierRequirement;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { FormManager } from './form-manager';
|
|
2
2
|
import { type FormValues } from './patient-registration.types';
|
|
3
|
+
import { generateIdentifier } from './patient-registration.resource';
|
|
3
4
|
|
|
4
5
|
jest.mock('./patient-registration.resource');
|
|
5
6
|
|
|
7
|
+
const mockGenerateIdentifier = generateIdentifier as jest.Mock;
|
|
8
|
+
|
|
6
9
|
const formValues: FormValues = {
|
|
7
10
|
patientUuid: '',
|
|
8
11
|
givenName: '',
|
|
@@ -20,7 +23,10 @@ const formValues: FormValues = {
|
|
|
20
23
|
telephoneNumber: '',
|
|
21
24
|
isDead: false,
|
|
22
25
|
deathDate: 'string',
|
|
26
|
+
deathTime: '',
|
|
27
|
+
deathTimeFormat: 'AM',
|
|
23
28
|
deathCause: 'string',
|
|
29
|
+
nonCodedCauseOfDeath: '',
|
|
24
30
|
relationships: [],
|
|
25
31
|
address: {
|
|
26
32
|
address1: '',
|
|
@@ -63,5 +69,20 @@ describe('FormManager', () => {
|
|
|
63
69
|
},
|
|
64
70
|
]);
|
|
65
71
|
});
|
|
72
|
+
|
|
73
|
+
it('should generate identifier if it has autoGeneration and manual entry disabled', async () => {
|
|
74
|
+
formValues.identifiers.foo.autoGeneration = true;
|
|
75
|
+
formValues.identifiers.foo.selectedSource.autoGenerationOption.manualEntryEnabled = false;
|
|
76
|
+
mockGenerateIdentifier.mockResolvedValue({ data: { identifier: '10001V' } });
|
|
77
|
+
await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
|
|
78
|
+
expect(mockGenerateIdentifier.mock.calls).toHaveLength(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should not generate identifiers if manual entry enabled and identifier value given', async () => {
|
|
82
|
+
formValues.identifiers.foo.autoGeneration = true;
|
|
83
|
+
formValues.identifiers.foo.selectedSource.autoGenerationOption.manualEntryEnabled = true;
|
|
84
|
+
await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
|
|
85
|
+
expect(mockGenerateIdentifier.mock.calls).toHaveLength(0);
|
|
86
|
+
});
|
|
66
87
|
});
|
|
67
88
|
});
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type FetchResponse,
|
|
3
|
-
type Session,
|
|
4
|
-
type StyleguideConfigObject,
|
|
5
3
|
getConfig,
|
|
6
4
|
openmrsFetch,
|
|
7
5
|
queueSynchronizationItem,
|
|
8
6
|
restBaseUrl,
|
|
7
|
+
type Session,
|
|
8
|
+
type StyleguideConfigObject,
|
|
9
|
+
toOmrsIsoString,
|
|
9
10
|
} from '@openmrs/esm-framework';
|
|
10
11
|
import { patientRegistration } from '../constants';
|
|
11
12
|
import {
|
|
12
|
-
type FormValues,
|
|
13
13
|
type AttributeValue,
|
|
14
|
-
type PatientUuidMapType,
|
|
15
|
-
type Patient,
|
|
16
14
|
type CapturePhotoProps,
|
|
15
|
+
type Encounter,
|
|
16
|
+
type FormValues,
|
|
17
|
+
type Patient,
|
|
17
18
|
type PatientIdentifier,
|
|
18
19
|
type PatientRegistration,
|
|
20
|
+
type PatientUuidMapType,
|
|
19
21
|
type RelationshipValue,
|
|
20
|
-
type Encounter,
|
|
21
22
|
} from './patient-registration.types';
|
|
22
23
|
import {
|
|
23
24
|
addPatientIdentifier,
|
|
@@ -25,14 +26,16 @@ import {
|
|
|
25
26
|
deletePersonName,
|
|
26
27
|
deleteRelationship,
|
|
27
28
|
generateIdentifier,
|
|
29
|
+
getDatetime,
|
|
30
|
+
saveEncounter,
|
|
28
31
|
savePatient,
|
|
29
32
|
savePatientPhoto,
|
|
30
33
|
saveRelationship,
|
|
31
|
-
updateRelationship,
|
|
32
34
|
updatePatientIdentifier,
|
|
33
|
-
|
|
35
|
+
updateRelationship,
|
|
34
36
|
} from './patient-registration.resource';
|
|
35
37
|
import { type RegistrationConfig } from '../config-schema';
|
|
38
|
+
import dayjs from 'dayjs';
|
|
36
39
|
|
|
37
40
|
export type SavePatientForm = (
|
|
38
41
|
isNewPatient: boolean,
|
|
@@ -62,7 +65,7 @@ export class FormManager {
|
|
|
62
65
|
) => {
|
|
63
66
|
const syncItem: PatientRegistration = {
|
|
64
67
|
fhirPatient: FormManager.mapPatientToFhirPatient(
|
|
65
|
-
FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, []),
|
|
68
|
+
FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, [], config),
|
|
66
69
|
),
|
|
67
70
|
_patientRegistrationData: {
|
|
68
71
|
isNewPatient,
|
|
@@ -115,6 +118,7 @@ export class FormManager {
|
|
|
115
118
|
patientUuidMap,
|
|
116
119
|
initialAddressFieldValues,
|
|
117
120
|
patientIdentifiers,
|
|
121
|
+
config,
|
|
118
122
|
);
|
|
119
123
|
|
|
120
124
|
FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
|
|
@@ -236,11 +240,16 @@ export class FormManager {
|
|
|
236
240
|
initialValue,
|
|
237
241
|
} = patientIdentifier;
|
|
238
242
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
const autoGenerationManualEntry =
|
|
244
|
+
autoGeneration && selectedSource?.autoGenerationOption?.manualEntryEnabled && !!identifierValue;
|
|
245
|
+
|
|
246
|
+
const identifier =
|
|
247
|
+
!autoGeneration || autoGenerationManualEntry
|
|
248
|
+
? identifierValue
|
|
249
|
+
: await (
|
|
250
|
+
await generateIdentifier(selectedSource.uuid)
|
|
251
|
+
).data.identifier;
|
|
252
|
+
|
|
244
253
|
const identifierToCreate = {
|
|
245
254
|
uuid: identifierUuid,
|
|
246
255
|
identifier,
|
|
@@ -296,6 +305,7 @@ export class FormManager {
|
|
|
296
305
|
patientUuidMap: PatientUuidMapType,
|
|
297
306
|
initialAddressFieldValues: Record<string, any>,
|
|
298
307
|
identifiers: Array<PatientIdentifier>,
|
|
308
|
+
config?: RegistrationConfig,
|
|
299
309
|
): Patient {
|
|
300
310
|
let birthdate;
|
|
301
311
|
if (values.birthdate instanceof Date) {
|
|
@@ -316,7 +326,7 @@ export class FormManager {
|
|
|
316
326
|
birthdateEstimated: values.birthdateEstimated,
|
|
317
327
|
attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
|
|
318
328
|
addresses: [values.address],
|
|
319
|
-
...FormManager.getPatientDeathInfo(values),
|
|
329
|
+
...FormManager.getPatientDeathInfo(values, config),
|
|
320
330
|
},
|
|
321
331
|
identifiers,
|
|
322
332
|
};
|
|
@@ -375,12 +385,22 @@ export class FormManager {
|
|
|
375
385
|
return attributes;
|
|
376
386
|
}
|
|
377
387
|
|
|
378
|
-
static getPatientDeathInfo(values: FormValues) {
|
|
379
|
-
const { isDead, deathDate, deathCause } = values;
|
|
388
|
+
static getPatientDeathInfo(values: FormValues, config?: RegistrationConfig) {
|
|
389
|
+
const { isDead, deathDate, deathTime, deathTimeFormat, deathCause, nonCodedCauseOfDeath } = values;
|
|
390
|
+
|
|
391
|
+
if (!isDead) {
|
|
392
|
+
return {
|
|
393
|
+
dead: false,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const dateTimeOfDeath = toOmrsIsoString(getDatetime(deathDate, deathTime, deathTimeFormat));
|
|
397
|
+
|
|
380
398
|
return {
|
|
381
|
-
dead:
|
|
382
|
-
deathDate:
|
|
383
|
-
|
|
399
|
+
dead: true,
|
|
400
|
+
deathDate: dateTimeOfDeath,
|
|
401
|
+
...(deathCause === config?.freeTextFieldConceptUuid
|
|
402
|
+
? { causeOfDeathNonCoded: nonCodedCauseOfDeath, causeOfDeath: null }
|
|
403
|
+
: { causeOfDeath: deathCause, causeOfDeathNonCoded: null }),
|
|
384
404
|
};
|
|
385
405
|
}
|
|
386
406
|
|