@kenyaemr/esm-patient-registration-app 8.1.1-pre.118 → 8.1.1-pre.119
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 +13 -13
- package/dist/10.js +1 -0
- package/dist/10.js.map +1 -0
- package/dist/130.js +1 -1
- package/dist/130.js.map +1 -1
- package/dist/574.js +1 -1
- package/dist/kenyaemr-esm-patient-registration-app.js +1 -1
- package/dist/kenyaemr-esm-patient-registration-app.js.buildmanifest.json +37 -37
- package/dist/kenyaemr-esm-patient-registration-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/client-registry/hie-client-registry/hie-resource.ts +5 -12
- package/src/config-schema.ts +7 -0
- package/src/patient-registration/field/field.scss +17 -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/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/form-manager.test.ts +18 -0
- package/src/patient-registration/form-manager.ts +10 -5
- package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +17 -9
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +106 -58
- package/src/patient-registration/input/input.scss +5 -0
- package/translations/en.json +2 -0
- package/dist/471.js +0 -1
- package/dist/471.js.map +0 -1
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.24.0"},"pages":[{"component":"root","route":"patient-registration","online":true,"offline":true},{"component":"editPatient","routeRegex":"patient\\/([a-zA-Z0-9\\-]+)\\/edit","online":true,"offline":true}],"extensions":[{"component":"addPatientLink","name":"add-patient-action","slot":"top-nav-actions-slot","online":true,"offline":true},{"component":"patientPhotoExtension","name":"patient-photo-widget","slot":"patient-photo-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-actions-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-search-actions-slot","online":true,"offline":true}],"modals":[{"name":"cancel-patient-edit-modal","component":"cancelPatientEditModal"},{"name":"delete-identifier-confirmation-modal","component":"deleteIdentifierConfirmationModal"},{"component":"emptyClientRegistryModal","name":"empty-client-registry-modal"},{"component":"confirmClientRegistryModal","name":"confirm-client-registry-modal"},{"component":"hieConfirmationModal","name":"hie-confirmation-modal"}],"version":"8.1.1-pre.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.24.0"},"pages":[{"component":"root","route":"patient-registration","online":true,"offline":true},{"component":"editPatient","routeRegex":"patient\\/([a-zA-Z0-9\\-]+)\\/edit","online":true,"offline":true}],"extensions":[{"component":"addPatientLink","name":"add-patient-action","slot":"top-nav-actions-slot","online":true,"offline":true},{"component":"patientPhotoExtension","name":"patient-photo-widget","slot":"patient-photo-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-actions-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-search-actions-slot","online":true,"offline":true}],"modals":[{"name":"cancel-patient-edit-modal","component":"cancelPatientEditModal"},{"name":"delete-identifier-confirmation-modal","component":"deleteIdentifierConfirmationModal"},{"component":"emptyClientRegistryModal","name":"empty-client-registry-modal"},{"component":"confirmClientRegistryModal","name":"confirm-client-registry-modal"},{"component":"hieConfirmationModal","name":"hie-confirmation-modal"}],"version":"8.1.1-pre.119"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenyaemr/esm-patient-registration-app",
|
|
3
|
-
"version": "8.1.1-pre.
|
|
3
|
+
"version": "8.1.1-pre.119",
|
|
4
4
|
"description": "Patient registration microfrontend for the OpenMRS SPA",
|
|
5
5
|
"browser": "dist/kenyaemr-esm-patient-registration-app.js",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import capitalize from 'lodash-es/capitalize';
|
|
2
2
|
import { type PatientIdentifierValue, type FormValues } from '../../patient-registration/patient-registration.types';
|
|
3
3
|
import { type MapperConfig, type HIEPatient, type ErrorResponse } from './hie-types';
|
|
4
|
-
import {
|
|
5
|
-
import { type RegistrationConfig } from '../../config-schema';
|
|
4
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
6
5
|
import { v4 } from 'uuid';
|
|
7
6
|
/**
|
|
8
7
|
* Represents a client for interacting with a Health Information Exchange (HIE) resource.
|
|
@@ -10,16 +9,10 @@ import { v4 } from 'uuid';
|
|
|
10
9
|
*/
|
|
11
10
|
class HealthInformationExchangeClient<T> {
|
|
12
11
|
async fetchResource(resourceType: string, params: Record<string, string>): Promise<T> {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
} = await getConfig<RegistrationConfig>('@kenyaemr/esm-patient-registration-app');
|
|
16
|
-
const urlParams = new URLSearchParams(params);
|
|
17
|
-
const response = await fetch(`${baseUrl}${resourceType}?${urlParams}`, {
|
|
18
|
-
headers: new Headers({
|
|
19
|
-
Authorization: `Basic ${encodedCredentials}`,
|
|
20
|
-
}),
|
|
21
|
-
});
|
|
12
|
+
const [identifierType, identifierValue] = Object.entries(params)[0];
|
|
13
|
+
const url = `${restBaseUrl}/kenyaemr/getSHAPatient/${identifierValue}/${identifierType}`;
|
|
22
14
|
|
|
15
|
+
const response = await openmrsFetch(url);
|
|
23
16
|
return response.json();
|
|
24
17
|
}
|
|
25
18
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface FieldDefinition {
|
|
|
18
18
|
required: boolean;
|
|
19
19
|
matches?: string;
|
|
20
20
|
};
|
|
21
|
+
locationTag?: string;
|
|
21
22
|
answerConceptSetUuid?: string;
|
|
22
23
|
customConceptAnswers?: Array<CustomConceptAnswer>;
|
|
23
24
|
showWhenExpression?: {
|
|
@@ -193,6 +194,12 @@ export const esmPatientRegistrationSchema = {
|
|
|
193
194
|
_description: 'Optional RegEx for testing the validity of the input.',
|
|
194
195
|
},
|
|
195
196
|
},
|
|
197
|
+
locationTag: {
|
|
198
|
+
_type: Type.String,
|
|
199
|
+
_default: null,
|
|
200
|
+
_description:
|
|
201
|
+
'Only for fields with "person attribute" type `org.openmrs.Location`. This filters the list of location options in the dropdown based on their location tag. By default, all locations are shown.',
|
|
202
|
+
},
|
|
196
203
|
answerConceptSetUuid: {
|
|
197
204
|
_type: Type.ConceptUuid,
|
|
198
205
|
_default: null,
|
|
@@ -99,7 +99,13 @@
|
|
|
99
99
|
align-items: center;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
.arrowRightIcon {
|
|
103
|
+
fill: currentColor !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
102
106
|
.configureIdentifiersButton {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
103
109
|
margin: 0 0 layout.$spacing-05 layout.$spacing-05;
|
|
104
110
|
|
|
105
111
|
svg {
|
|
@@ -115,6 +121,17 @@
|
|
|
115
121
|
margin-bottom: layout.$spacing-05;
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
.locationAttributeFieldContainer {
|
|
125
|
+
position: relative;
|
|
126
|
+
|
|
127
|
+
.loadingContainer {
|
|
128
|
+
background-color: colors.$white;
|
|
129
|
+
position: absolute;
|
|
130
|
+
right: layout.$spacing-07;
|
|
131
|
+
bottom: layout.$spacing-02;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
:global(.omrs-breakpoint-lt-desktop) {
|
|
119
136
|
.grid {
|
|
120
137
|
grid-template-columns: 1fr;
|
|
@@ -25,14 +25,16 @@ export function setIdentifierSource(
|
|
|
25
25
|
selectedSource: IdentifierSource;
|
|
26
26
|
} {
|
|
27
27
|
const autoGeneration = identifierSource?.autoGenerationOption?.automaticGenerationEnabled;
|
|
28
|
+
const manualEntryEnabled = identifierSource?.autoGenerationOption?.manualEntryEnabled;
|
|
28
29
|
return {
|
|
29
30
|
selectedSource: identifierSource,
|
|
30
31
|
autoGeneration,
|
|
31
|
-
identifierValue:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
identifierValue:
|
|
33
|
+
autoGeneration && !manualEntryEnabled
|
|
34
|
+
? 'auto-generated'
|
|
35
|
+
: identifierValue !== 'auto-generated'
|
|
36
|
+
? identifierValue
|
|
37
|
+
: initialValue,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -126,7 +128,7 @@ export const Identifiers: React.FC = () => {
|
|
|
126
128
|
className={styles.configureIdentifiersButton}
|
|
127
129
|
onClick={() => setShowIdentifierOverlay(true)}
|
|
128
130
|
size={isDesktop(layout) ? 'sm' : 'md'}>
|
|
129
|
-
{t('configure', 'Configure')} <ArrowRight size={16} />
|
|
131
|
+
{t('configure', 'Configure')} <ArrowRight className={styles.arrowRightIcon} size={16} />
|
|
130
132
|
</Button>
|
|
131
133
|
</div>
|
|
132
134
|
</UserHasAccess>
|
|
@@ -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
|
+
});
|
|
@@ -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">
|
|
@@ -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: '',
|
|
@@ -66,5 +69,20 @@ describe('FormManager', () => {
|
|
|
66
69
|
},
|
|
67
70
|
]);
|
|
68
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
|
+
});
|
|
69
87
|
});
|
|
70
88
|
});
|
|
@@ -240,11 +240,16 @@ export class FormManager {
|
|
|
240
240
|
initialValue,
|
|
241
241
|
} = patientIdentifier;
|
|
242
242
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
|
|
248
253
|
const identifierToCreate = {
|
|
249
254
|
uuid: identifierUuid,
|
|
250
255
|
identifier,
|
package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx
CHANGED
|
@@ -26,7 +26,8 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
26
26
|
() => identifierTypes.find((identifierType) => identifierType.uuid === patientIdentifier.identifierTypeUuid),
|
|
27
27
|
[patientIdentifier, identifierTypes],
|
|
28
28
|
);
|
|
29
|
-
const { autoGeneration, initialValue, identifierValue, identifierName, required } = patientIdentifier;
|
|
29
|
+
const { autoGeneration, initialValue, identifierValue, identifierName, required, selectedSource } = patientIdentifier;
|
|
30
|
+
const manualEntryEnabled = selectedSource?.autoGenerationOption?.manualEntryEnabled;
|
|
30
31
|
const [hideInputField, setHideInputField] = useState(autoGeneration || initialValue === identifierValue);
|
|
31
32
|
const name = `identifiers.${fieldName}.identifierValue`;
|
|
32
33
|
const [identifierField, identifierFieldMeta] = useField(name);
|
|
@@ -46,8 +47,8 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
46
47
|
setFieldValue(`identifiers.${fieldName}`, {
|
|
47
48
|
...patientIdentifier,
|
|
48
49
|
identifierValue: initialValue,
|
|
49
|
-
selectedSource
|
|
50
|
-
autoGeneration
|
|
50
|
+
selectedSource,
|
|
51
|
+
autoGeneration,
|
|
51
52
|
} as PatientIdentifierValue);
|
|
52
53
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
54
|
}, [initialValue, setHideInputField]);
|
|
@@ -57,6 +58,7 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
57
58
|
setFieldValue(`identifiers.${fieldName}`, {
|
|
58
59
|
...patientIdentifier,
|
|
59
60
|
...setIdentifierSource(identifierType?.identifierSources?.[0], initialValue, initialValue),
|
|
61
|
+
...(autoGeneration && manualEntryEnabled && { identifierValue: initialValue ?? '' }),
|
|
60
62
|
});
|
|
61
63
|
};
|
|
62
64
|
|
|
@@ -83,9 +85,12 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
87
|
|
|
88
|
+
const showEditButton = !required && hideInputField && (!!initialValue || manualEntryEnabled);
|
|
89
|
+
const showResetButton =
|
|
90
|
+
(!!initialValue && initialValue !== identifierValue) || (!hideInputField && manualEntryEnabled);
|
|
86
91
|
return (
|
|
87
92
|
<div className={styles.IDInput}>
|
|
88
|
-
{!
|
|
93
|
+
{!hideInputField ? (
|
|
89
94
|
<Input
|
|
90
95
|
id={name}
|
|
91
96
|
labelText={identifierName}
|
|
@@ -99,8 +104,10 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
99
104
|
/>
|
|
100
105
|
) : (
|
|
101
106
|
<div className={styles.textID}>
|
|
102
|
-
<p className={styles.label}>
|
|
103
|
-
|
|
107
|
+
<p data-testid="identifier-label" className={styles.label}>
|
|
108
|
+
{required ? identifierName : `${t('optionalIdentifierLabel', { identifierName })}`}
|
|
109
|
+
</p>
|
|
110
|
+
<p data-testid="identifier-placeholder" className={styles.bodyShort02}>
|
|
104
111
|
{autoGeneration ? t('autoGeneratedPlaceholderText', 'Auto-generated') : identifierValue}
|
|
105
112
|
</p>
|
|
106
113
|
<input data-testid="identifier-input" type="hidden" {...identifierField} disabled />
|
|
@@ -110,10 +117,11 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
110
117
|
)}
|
|
111
118
|
</div>
|
|
112
119
|
)}
|
|
113
|
-
<div
|
|
114
|
-
{
|
|
120
|
+
<div className={styles.actionButtonContainer}>
|
|
121
|
+
{showEditButton && (
|
|
115
122
|
<UserHasAccess privilege="Edit Patient Identifiers">
|
|
116
123
|
<Button
|
|
124
|
+
data-testid="edit-button"
|
|
117
125
|
size="md"
|
|
118
126
|
kind="ghost"
|
|
119
127
|
onClick={handleEdit}
|
|
@@ -124,7 +132,7 @@ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fi
|
|
|
124
132
|
</Button>
|
|
125
133
|
</UserHasAccess>
|
|
126
134
|
)}
|
|
127
|
-
{
|
|
135
|
+
{showResetButton && (
|
|
128
136
|
<UserHasAccess privilege="Edit Patient Identifiers">
|
|
129
137
|
<Button
|
|
130
138
|
size="md"
|