Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) - O3-1831 Registration: Support person attribute of type Location #1032

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/esm-patient-registration-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface FieldDefinition {
required: boolean;
matches?: string;
};
locationTag?: string;
answerConceptSetUuid?: string;
customConceptAnswers?: Array<CustomConceptAnswer>;
}
Expand Down Expand Up @@ -136,8 +137,8 @@ export const esmPatientRegistrationSchema = {
},
type: {
_type: Type.String,
_description: "How this field's data will be stored—a person attribute or an obs.",
_validators: [validators.oneOf(['person attribute', 'obs'])],
_description: "How this field's data will be stored—a person attribute, an obs or an address.",
_validators: [validators.oneOf(['person attribute', 'obs', 'address'])],
},
uuid: {
_type: Type.UUID,
Expand Down Expand Up @@ -166,6 +167,12 @@ export const esmPatientRegistrationSchema = {
_description: 'Optional RegEx for testing the validity of the input.',
},
},
locationTag: {
_type: Type.String,
_default: null,
_description:
'For locations questions only. A concept which has the possible responses either as answers or as set members.',
},
answerConceptSetUuid: {
_type: Type.ConceptUuid,
_default: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import classNames from 'classnames';
import { useField } from 'formik';
import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
import styles from './../field.scss';
import { useLocations } from './location-person-attribute-field.resource';
import { ComboBox, Layer } from '@carbon/react';
import { useTranslation } from 'react-i18next';
export interface LocationPersonAttributeFieldProps {
id: string;
personAttributeType: PersonAttributeTypeResponse;
validationRegex?: string;
label?: string;
locationTag: string;
required?: boolean;
}

export function LocationPersonAttributeField({
personAttributeType,
id,
label,
locationTag,
required,
}: LocationPersonAttributeFieldProps) {
const { t } = useTranslation();
const [field, meta, { setValue }] = useField(`attributes.${personAttributeType.uuid}`);
const [searchQuery, setSearchQuery] = useState<string>('');
const { locations } = useLocations(locationTag || null, 5, searchQuery);

const locationOptions = useMemo(() => {
return locations.map(({ resource: { id, name } }) => ({ value: id, label: name }));
}, [locations]);

useEffect(() => {
if (meta?.value?.uuid) {
setValue(meta.value.uuid);
}
}, [meta, setValue]);

const onInputChange = useCallback(
(value: string | null) => {
if (value) {
if (locationOptions.find(({ label }) => label === value)) return;
setSearchQuery(value);
setValue(null);
}
},
[locationOptions, setValue],
);

const handleSelect = useCallback(
({ selectedItem }) => {
if (selectedItem) {
setValue(selectedItem.value);
}
},
[setValue],
);

return (
<div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
<Layer>
<ComboBox
titleText={label}
items={locationOptions}
id={id}
placeholder={t('searchLocationPersonAttribute', 'Search location')}
onInputChange={onInputChange}
required={required}
onChange={handleSelect}
selectedItem={locationOptions.find(({ value }) => value === field.value) || null}
/>
</Layer>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useCallback, useMemo } from 'react';
import { type FetchResponse, fhirBaseUrl, openmrsFetch, useDebounce } from '@openmrs/esm-framework';
import { type LocationEntry, type LocationResponse } from '@openmrs/esm-service-queues-app/src/types';
import useSWR from 'swr';
interface IUseLocations {
locations: Array<LocationEntry>;
isLoading: boolean;
loadingNewData: boolean;
fetchLocationByUuid: (uuid: string) => void;
}

export function useLocations(locationTag: string | null, count: number = 0, searchQuery: string = ''): IUseLocations {
const debouncedSearchQuery = useDebounce(searchQuery);

const fetchLocationByUuid = useCallback((uuid: string) => {
const { data } = useSWR<FetchResponse<LocationResponse>, Error>(`${fhirBaseUrl}/Location/${uuid}`, openmrsFetch);

Check failure on line 16 in packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook "useSWR" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
}, []);

const constructUrl = useMemo(() => {
let url = `${fhirBaseUrl}/Location?`;
let urlSearchParameters = new URLSearchParams();
urlSearchParameters.append('_summary', 'data');

if (count && !debouncedSearchQuery) {
urlSearchParameters.append('_count', '' + count);
}

if (locationTag) {
urlSearchParameters.append('_tag', locationTag);
}

if (typeof debouncedSearchQuery === 'string' && debouncedSearchQuery != '') {
urlSearchParameters.append('name:contains', debouncedSearchQuery);
}

return url + urlSearchParameters.toString();
}, [count, locationTag, debouncedSearchQuery]);

const { data, error, isLoading, isValidating } = useSWR<FetchResponse<LocationResponse>, Error>(
constructUrl,
openmrsFetch,
);

return useMemo(
() => ({
locations: data?.data?.entry || [],
isLoading,
loadingNewData: isValidating,
error,
fetchLocationByUuid,
}),
[isLoading, data, isValidating, fetchLocationByUuid],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePersonAttributeType } from './person-attributes.resource';
import { TextPersonAttributeField } from './text-person-attribute-field.component';
import { useTranslation } from 'react-i18next';
import styles from '../field.scss';
import { LocationPersonAttributeField } from './location-person-attribute-field.component';

export interface PersonAttributeFieldProps {
fieldDefinition: FieldDefinition;
Expand Down Expand Up @@ -41,6 +42,15 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
required={fieldDefinition.validation?.required ?? false}
/>
);
case 'org.openmrs.Location':
return (
<LocationPersonAttributeField
personAttributeType={personAttributeType}
locationTag={fieldDefinition.locationTag}
label={fieldDefinition.label}
id={fieldDefinition?.id}
/>
);
default:
return (
<InlineNotification kind="error" title="Error">
Expand Down
1 change: 1 addition & 0 deletions packages/esm-patient-registration-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"restoreRelationshipActionButton": "Undo",
"searchAddress": "Search address",
"searchIdentifierPlaceholder": "Search identifier",
"searchLocationPersonAttribute": "Search location",
"selectAnOption": "Select an option",
"sexFieldLabelText": "Sex",
"source": "Source",
Expand Down