@digihmis/esm-patient-registration-app 1.0.0
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/README.md +4 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/131.js +1 -0
- package/dist/131.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/247.js +1 -0
- package/dist/247.js.map +1 -0
- package/dist/28.js +1 -0
- package/dist/28.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/459.js +12 -0
- package/dist/459.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/957.js +15 -0
- package/dist/957.js.map +1 -0
- package/dist/digihmis-esm-patient-registration-app.js +6 -0
- package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
- package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/active-visit-table/active-visit-table.component.tsx +240 -0
- package/src/active-visit-table/active-visits.resource.ts +338 -0
- package/src/active-visit-table/active-visits.scss +211 -0
- package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
- package/src/checkin-form/base-visit-type.component.tsx +102 -0
- package/src/checkin-form/base-visit-type.scss +75 -0
- package/src/checkin-form/checkin-form.scss +167 -0
- package/src/checkin-form/checkin-form.workspace.tsx +703 -0
- package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
- package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
- package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
- package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
- package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
- package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
- package/src/checkin-form/location-selector.component.tsx +86 -0
- package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
- package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
- package/src/checkin-form/visit-attribute-type.scss +5 -0
- package/src/checkin-form/visit-date-time.component.tsx +201 -0
- package/src/checkin-form/visit-form.resource.ts +308 -0
- package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
- package/src/components/custom-card/compact-patient-banner.scss +34 -0
- package/src/components/custom-card/custom-loader.component.tsx +22 -0
- package/src/components/custom-card/patient-search-context.ts +30 -0
- package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
- package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
- package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
- package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
- package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
- package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
- package/src/config-schema.ts +511 -0
- package/src/constant.ts +6 -0
- package/src/constants/index.ts +1 -0
- package/src/dashboard.meta.ts +8 -0
- package/src/declarations.d.ts +6 -0
- package/src/empty-svg/empty-svg.component.tsx +565 -0
- package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
- package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
- package/src/identifier-workspace/identifier-configure.scss +106 -0
- package/src/identifier-workspace/identifier.resource.ts +90 -0
- package/src/index.ts +33 -0
- package/src/left-panel/left-panel.component.tsx +54 -0
- package/src/left-panel/left-panel.scss +13 -0
- package/src/page-header/page-header.component.tsx +35 -0
- package/src/page-header/page-header.scss +76 -0
- package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
- package/src/patient-tags/deceased-patient-tag.scss +15 -0
- package/src/patient-tags/patient-tags.resource.ts +30 -0
- package/src/patient-tags/visit-tag.extension.tsx +43 -0
- package/src/patient-tags/visit-tag.scss +19 -0
- package/src/registration-form/form-manager.ts +449 -0
- package/src/registration-form/registration-form.component.tsx +1565 -0
- package/src/registration-form/registration-form.resource.ts +264 -0
- package/src/registration-form/registration-form.scss +282 -0
- package/src/routes.json +62 -0
- package/src/search-bar/search-bar.component.tsx +285 -0
- package/src/search-bar/search-bar.resource.ts +274 -0
- package/src/search-bar/search-bar.scss +427 -0
- package/src/search-dashboard/search-dashboard.component.tsx +17 -0
- package/src/search-dashboard/search-dashboard.scss +13 -0
- package/src/setup-tests.ts +1 -0
- package/src/type/index.ts +463 -0
- package/src/utils/fhir-mapper.ts +73 -0
- package/src/visit-summary/visit-summary.component.tsx +32 -0
- package/src/visit-summary/visit-summary.resource.ts +56 -0
- package/src/visit-summary/visit-summary.scss +74 -0
- package/translations/am.json +112 -0
- package/translations/ar.json +120 -0
- package/translations/en.json +112 -0
- package/translations/es.json +114 -0
- package/translations/fr.json +114 -0
- package/translations/he.json +114 -0
- package/translations/km.json +111 -0
- package/translations/pt.json +114 -0
- package/translations/sw.json +112 -0
- package/translations/vi.json +111 -0
- package/translations/zh.json +111 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FetchResponse, openmrsFetch, useConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWRImmutable from 'swr/immutable';
|
|
3
|
+
import { type ChartConfig } from '../../config-schema';
|
|
4
|
+
|
|
5
|
+
export const useDefaultFacilityLocation = () => {
|
|
6
|
+
const config = useConfig() as ChartConfig;
|
|
7
|
+
const apiUrl = config.defaultFacilityUrl;
|
|
8
|
+
const { data, error, isLoading } = useSWRImmutable<FetchResponse>(apiUrl, openmrsFetch);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
defaultFacility: data ? data?.data : null,
|
|
12
|
+
isLoading: isLoading,
|
|
13
|
+
error: error,
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type FetchResponse, type Location, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetches the default visit location based on the location and the restrictByVisitLocationTag
|
|
7
|
+
* If restrictByVisitLocationTag is true, it fetches the nearest ancestor location that supports visits, otherwise it returns the passed-in location
|
|
8
|
+
*
|
|
9
|
+
* @param location
|
|
10
|
+
* @param restrictByVisitLocationTag
|
|
11
|
+
*/
|
|
12
|
+
export function useDefaultVisitLocation(location: Location, restrictByVisitLocationTag: boolean) {
|
|
13
|
+
let url = `${restBaseUrl}/emrapi/locationThatSupportsVisits?location=${location?.uuid}`;
|
|
14
|
+
|
|
15
|
+
const { data, error } = useSWR<FetchResponse>(restrictByVisitLocationTag ? url : null, openmrsFetch);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (error) {
|
|
19
|
+
console.error(error);
|
|
20
|
+
}
|
|
21
|
+
}, [error]);
|
|
22
|
+
|
|
23
|
+
return !restrictByVisitLocationTag ? location : data?.data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type VisitType, useConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import { type ChartConfig } from '../../config-schema';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
export const useOfflineVisitType = () => {
|
|
7
|
+
const config = useConfig() as ChartConfig;
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const [visitTypes, setVisitTypes] = useState<Array<VisitType>>([]);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setVisitTypes([
|
|
13
|
+
{ uuid: config.offlineVisitTypeUuid, name: 'Offline Visit', display: t('offlineVisit', 'Offline Visit') },
|
|
14
|
+
]);
|
|
15
|
+
}, [t, config.offlineVisitTypeUuid]);
|
|
16
|
+
|
|
17
|
+
return visitTypes;
|
|
18
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, type Visit } from '@openmrs/esm-framework';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
|
|
5
|
+
export const usePatientVisitHistory = (patientUuid: string) => {
|
|
6
|
+
const { data, error, isValidating } = useSWR<{
|
|
7
|
+
data: { results: Array<Visit> };
|
|
8
|
+
}>(patientUuid ? `${restBaseUrl}/visit?patient=${patientUuid}&v=default` : null, openmrsFetch);
|
|
9
|
+
|
|
10
|
+
const visitHistory = useMemo(() => {
|
|
11
|
+
if (!data) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const visits = data.data.results || [];
|
|
16
|
+
const activeVisits = visits.filter((v) => !v.voided);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
totalVisits: activeVisits.length,
|
|
20
|
+
isNewPatient: activeVisits.length === 0,
|
|
21
|
+
isRevisit: activeVisits.length > 0,
|
|
22
|
+
visits: activeVisits,
|
|
23
|
+
firstVisit:
|
|
24
|
+
activeVisits.length > 0
|
|
25
|
+
? activeVisits.sort((a, b) => new Date(a.startDatetime).getTime() - new Date(b.startDatetime).getTime())[0]
|
|
26
|
+
: null,
|
|
27
|
+
lastVisit:
|
|
28
|
+
activeVisits.length > 0
|
|
29
|
+
? activeVisits.sort((a, b) => new Date(b.startDatetime).getTime() - new Date(a.startDatetime).getTime())[0]
|
|
30
|
+
: null,
|
|
31
|
+
};
|
|
32
|
+
}, [data]);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
visitHistory,
|
|
36
|
+
isLoading: !data && !error,
|
|
37
|
+
error,
|
|
38
|
+
isValidating,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { openmrsFetch, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { type ChartConfig } from '../../config-schema';
|
|
5
|
+
|
|
6
|
+
interface EnrollmentVisitType {
|
|
7
|
+
dataDependencies: Array<string>;
|
|
8
|
+
enrollmentOptions: object;
|
|
9
|
+
incompatibleWith: Array<string>;
|
|
10
|
+
name: string;
|
|
11
|
+
visitTypes: { allowed: Array<EnrollmentVisitType>; disallowed: Array<EnrollmentVisitType> };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useRecommendedVisitTypes = (
|
|
15
|
+
patientUuid: string,
|
|
16
|
+
enrollmentUuid: string,
|
|
17
|
+
programUuid: string,
|
|
18
|
+
locationUuid: string,
|
|
19
|
+
) => {
|
|
20
|
+
const { visitTypeResourceUrl, showRecommendedVisitTypeTab } = useConfig() as ChartConfig;
|
|
21
|
+
const url = `${visitTypeResourceUrl}${patientUuid}/program/${programUuid}/enrollment/${enrollmentUuid}?intendedLocationUuid=${locationUuid}`;
|
|
22
|
+
|
|
23
|
+
const { data, error, isLoading } = useSWR<{ data: EnrollmentVisitType }>(
|
|
24
|
+
showRecommendedVisitTypeTab && patientUuid && enrollmentUuid && programUuid ? url : null,
|
|
25
|
+
openmrsFetch,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const recommendedVisitTypes = useMemo(() => data?.data?.visitTypes?.allowed.map(mapToVisitType) ?? [], [data]);
|
|
29
|
+
return { recommendedVisitTypes, error, isLoading };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mapToVisitType = (visitType) => {
|
|
33
|
+
return { ...visitType, display: visitType.name };
|
|
34
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
3
|
+
import useSWRImmutable from 'swr/immutable';
|
|
4
|
+
|
|
5
|
+
interface VisitAttributeType {
|
|
6
|
+
uuid: string;
|
|
7
|
+
display: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string | null;
|
|
10
|
+
datatypeClassname:
|
|
11
|
+
| 'org.openmrs.customdatatype.datatype.ConceptDatatype'
|
|
12
|
+
| 'org.openmrs.customdatatype.datatype.FloatDatatype'
|
|
13
|
+
| 'org.openmrs.customdatatype.datatype.BooleanDatatype'
|
|
14
|
+
| 'org.openmrs.customdatatype.datatype.LongFreeTextDatatype'
|
|
15
|
+
| 'org.openmrs.customdatatype.datatype.FreeTextDatatype'
|
|
16
|
+
| 'org.openmrs.customdatatype.datatype.DateDatatype';
|
|
17
|
+
datatypeConfig: string;
|
|
18
|
+
preferredHandlerClassname: any;
|
|
19
|
+
retired: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Concept {
|
|
23
|
+
uuid: string;
|
|
24
|
+
name: {
|
|
25
|
+
display: string;
|
|
26
|
+
};
|
|
27
|
+
display: string;
|
|
28
|
+
answers: Array<{
|
|
29
|
+
display: string;
|
|
30
|
+
uuid: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const visitAttributeTypeCustomRepresentation =
|
|
35
|
+
'custom:(uuid,display,name,description,datatypeClassname,datatypeConfig)';
|
|
36
|
+
|
|
37
|
+
export function useVisitAttributeTypes() {
|
|
38
|
+
const { data, error, isLoading } = useSWRImmutable<FetchResponse<{ results: VisitAttributeType[] }>, Error>(
|
|
39
|
+
`${restBaseUrl}/visitattributetype?v=${visitAttributeTypeCustomRepresentation}`,
|
|
40
|
+
openmrsFetch,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
console.error('Failed to fetch visit attribute types: ', error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const results = useMemo(
|
|
48
|
+
() => ({
|
|
49
|
+
isLoading,
|
|
50
|
+
error,
|
|
51
|
+
visitAttributeTypes: data?.data?.results ?? [],
|
|
52
|
+
}),
|
|
53
|
+
[data, error, isLoading],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useVisitAttributeType(uuid) {
|
|
60
|
+
const { data, error, isLoading } = useSWRImmutable<FetchResponse<VisitAttributeType>, Error>(
|
|
61
|
+
`${restBaseUrl}/visitattributetype/${uuid}?v=${visitAttributeTypeCustomRepresentation}`,
|
|
62
|
+
openmrsFetch,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (error) {
|
|
66
|
+
console.error(`Failed to fetch visit attribute type ${uuid}: `, error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const results = useMemo(
|
|
70
|
+
() => ({
|
|
71
|
+
isLoading,
|
|
72
|
+
error: error,
|
|
73
|
+
data: data?.data,
|
|
74
|
+
}),
|
|
75
|
+
[data, error, isLoading],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useConceptAnswersForVisitAttributeType(conceptUuid) {
|
|
82
|
+
const { data, error, isLoading } = useSWRImmutable<FetchResponse<Concept>, Error>(
|
|
83
|
+
conceptUuid ? `${restBaseUrl}/concept/${conceptUuid}` : null,
|
|
84
|
+
openmrsFetch,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (error) {
|
|
88
|
+
console.error(`Failed to fetch concept answers for visit attribute type ${conceptUuid}: `, error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const results = useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
isLoading,
|
|
94
|
+
error: error,
|
|
95
|
+
data: data?.data,
|
|
96
|
+
answers: data?.data?.answers,
|
|
97
|
+
}),
|
|
98
|
+
[data, error, isLoading],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import isEmpty from 'lodash-es/isEmpty';
|
|
4
|
+
import { ComboBox } from '@carbon/react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { type Control, Controller } from 'react-hook-form';
|
|
7
|
+
import {
|
|
8
|
+
type Location,
|
|
9
|
+
type OpenmrsResource,
|
|
10
|
+
useConfig,
|
|
11
|
+
useFeatureFlag,
|
|
12
|
+
useLocations,
|
|
13
|
+
useSession,
|
|
14
|
+
} from '@openmrs/esm-framework';
|
|
15
|
+
import styles from './checkin-form.scss';
|
|
16
|
+
import { type VisitFormData } from './visit-form.resource';
|
|
17
|
+
import { useDefaultVisitLocation } from './hooks/useDefaultVisitLocation';
|
|
18
|
+
import { useDefaultFacilityLocation } from './hooks/useDefaultFacilityLocation';
|
|
19
|
+
import { ChartConfig } from '../config-schema';
|
|
20
|
+
|
|
21
|
+
interface LocationSelectorProps {
|
|
22
|
+
control: Control<VisitFormData>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LocationSelector: React.FC<LocationSelectorProps> = ({ control }) => {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const config = useConfig<ChartConfig>();
|
|
28
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
29
|
+
const sessionLocation = useSession().sessionLocation;
|
|
30
|
+
const isEmrApiModuleInstalled = useFeatureFlag('emrapi-module');
|
|
31
|
+
const defaultVisitLocation = useDefaultVisitLocation(
|
|
32
|
+
sessionLocation,
|
|
33
|
+
config.restrictByVisitLocationTag && isEmrApiModuleInstalled,
|
|
34
|
+
);
|
|
35
|
+
const locations = useLocations(
|
|
36
|
+
config.restrictByVisitLocationTag && isEmrApiModuleInstalled ? 'Visit Location' : null,
|
|
37
|
+
searchTerm,
|
|
38
|
+
);
|
|
39
|
+
const { defaultFacility, isLoading: loadingDefaultFacility } = useDefaultFacilityLocation();
|
|
40
|
+
const disableChangingVisitLocation = config?.disableChangingVisitLocation;
|
|
41
|
+
const locationsToShow: Array<OpenmrsResource> =
|
|
42
|
+
!loadingDefaultFacility && !isEmpty(defaultFacility) ? [defaultFacility] : locations ? locations : [];
|
|
43
|
+
|
|
44
|
+
const handleSearch = (searchString) => {
|
|
45
|
+
setSearchTerm(searchString);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (config.restrictByVisitLocationTag && !isEmrApiModuleInstalled) {
|
|
50
|
+
console.warn('EMR API module is not installed. Visit location will not be restricted by location tag.');
|
|
51
|
+
}
|
|
52
|
+
}, [config.restrictByVisitLocationTag, isEmrApiModuleInstalled]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<section data-testid="combo">
|
|
56
|
+
<div className={styles.sectionTitle}>{t('visitLocation', 'Visit Location')}</div>
|
|
57
|
+
<div className={classNames(styles.selectContainer, styles.sectionField)}>
|
|
58
|
+
{!disableChangingVisitLocation ? (
|
|
59
|
+
<Controller
|
|
60
|
+
control={control}
|
|
61
|
+
name="visitLocation"
|
|
62
|
+
render={({ field: { onBlur, onChange, value } }) => (
|
|
63
|
+
<ComboBox
|
|
64
|
+
aria-label={t('selectLocation', 'Select a location')}
|
|
65
|
+
id="location"
|
|
66
|
+
invalidText={t('required', 'Required')}
|
|
67
|
+
items={locationsToShow}
|
|
68
|
+
itemToString={(location: Location) => location?.display}
|
|
69
|
+
onBlur={onBlur}
|
|
70
|
+
onChange={({ selectedItem }) => onChange(selectedItem)}
|
|
71
|
+
onInputChange={(searchTerm) => handleSearch(searchTerm)}
|
|
72
|
+
readOnly={disableChangingVisitLocation}
|
|
73
|
+
selectedItem={value}
|
|
74
|
+
titleText={t('selectLocation', 'Select a location')}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<p className={styles.bodyShort02}>{defaultVisitLocation?.display}</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default LocationSelector;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StructuredListSkeleton } from '@carbon/react';
|
|
3
|
+
import { type PatientProgram } from '@openmrs/esm-patient-common-lib';
|
|
4
|
+
import BaseVisitType from './base-visit-type.component';
|
|
5
|
+
import { useRecommendedVisitTypes } from './hooks/useRecommendedVisitTypes';
|
|
6
|
+
|
|
7
|
+
interface RecommendedVisitTypeProp {
|
|
8
|
+
patientUuid: string;
|
|
9
|
+
patientProgramEnrollment: PatientProgram;
|
|
10
|
+
locationUuid: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RecommendedVisitType: React.FC<RecommendedVisitTypeProp> = ({
|
|
14
|
+
patientUuid,
|
|
15
|
+
patientProgramEnrollment,
|
|
16
|
+
locationUuid,
|
|
17
|
+
}) => {
|
|
18
|
+
const { recommendedVisitTypes, isLoading } = useRecommendedVisitTypes(
|
|
19
|
+
patientUuid,
|
|
20
|
+
patientProgramEnrollment?.uuid,
|
|
21
|
+
patientProgramEnrollment?.program?.uuid,
|
|
22
|
+
locationUuid,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div style={{ marginTop: '0.625rem' }}>
|
|
27
|
+
{isLoading ? <StructuredListSkeleton /> : <BaseVisitType visitTypes={recommendedVisitTypes} />}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const MemoizedRecommendedVisitType = React.memo(RecommendedVisitType);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import React, { useEffect, useId, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Checkbox,
|
|
4
|
+
NumberInput,
|
|
5
|
+
Select,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectSkeleton,
|
|
8
|
+
TextArea,
|
|
9
|
+
TextInput,
|
|
10
|
+
TextInputSkeleton,
|
|
11
|
+
} from '@carbon/react';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import { Controller, type ControllerFieldState, type ControllerRenderProps, useFormContext } from 'react-hook-form';
|
|
14
|
+
import { OpenmrsDatePicker, useConfig } from '@openmrs/esm-framework';
|
|
15
|
+
import styles from './visit-attribute-type.scss';
|
|
16
|
+
import { useConceptAnswersForVisitAttributeType, useVisitAttributeType } from './hooks/useVisitAttributeType';
|
|
17
|
+
import { VisitFormData } from './visit-form.resource';
|
|
18
|
+
import { ChartConfig } from '../config-schema';
|
|
19
|
+
|
|
20
|
+
interface VisitAttributes {
|
|
21
|
+
[uuid: string]: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface VisitAttributeTypeFieldsProps {
|
|
25
|
+
setErrorFetchingResources: React.Dispatch<
|
|
26
|
+
React.SetStateAction<{
|
|
27
|
+
blockSavingForm: boolean;
|
|
28
|
+
}>
|
|
29
|
+
>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Evaluates a given expression using the provided visitAttributes.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} expression - The expression to be evaluated. This should be a string of JavaScript code.
|
|
36
|
+
* @param {VisitAttributes} visitAttributes - An object containing visit attributes which will be used in the evaluation of the expression.
|
|
37
|
+
*
|
|
38
|
+
* @returns {boolean} - The boolean value of the result of the evaluated expression.
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
function evaluateExpression(expression: string, visitAttributes: VisitAttributes) {
|
|
42
|
+
const func = new Function('visitAttributes', `return ${expression};`);
|
|
43
|
+
|
|
44
|
+
const result = func(visitAttributes);
|
|
45
|
+
|
|
46
|
+
return Boolean(result);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const VisitAttributeTypeFields: React.FC<VisitAttributeTypeFieldsProps> = ({ setErrorFetchingResources }) => {
|
|
50
|
+
const { visitAttributeTypes } = useConfig<ChartConfig>();
|
|
51
|
+
const { control, getValues } = useFormContext<VisitFormData>();
|
|
52
|
+
|
|
53
|
+
if (visitAttributeTypes?.length) {
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
{visitAttributeTypes.map((attributeType) => {
|
|
57
|
+
const { visitAttributes } = getValues();
|
|
58
|
+
|
|
59
|
+
const showAttributeType = attributeType?.showWhenExpression
|
|
60
|
+
? evaluateExpression(attributeType?.showWhenExpression, visitAttributes)
|
|
61
|
+
: true;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
showAttributeType && (
|
|
65
|
+
<Controller
|
|
66
|
+
key={attributeType.uuid}
|
|
67
|
+
name={`visitAttributes.${attributeType.uuid}`}
|
|
68
|
+
control={control}
|
|
69
|
+
render={({ field, fieldState }) => (
|
|
70
|
+
<AttributeTypeField
|
|
71
|
+
key={attributeType.uuid}
|
|
72
|
+
attributeType={attributeType}
|
|
73
|
+
setErrorFetchingResources={setErrorFetchingResources}
|
|
74
|
+
fieldProps={field}
|
|
75
|
+
fieldState={fieldState}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
interface AttributeTypeFieldProps {
|
|
90
|
+
fieldProps: ControllerRenderProps<VisitFormData, `visitAttributes.${string}`>;
|
|
91
|
+
fieldState: ControllerFieldState;
|
|
92
|
+
attributeType: {
|
|
93
|
+
uuid: string;
|
|
94
|
+
required: boolean;
|
|
95
|
+
};
|
|
96
|
+
setErrorFetchingResources: React.Dispatch<
|
|
97
|
+
React.SetStateAction<{
|
|
98
|
+
blockSavingForm: boolean;
|
|
99
|
+
}>
|
|
100
|
+
>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const AttributeTypeField: React.FC<AttributeTypeFieldProps> = ({
|
|
104
|
+
attributeType,
|
|
105
|
+
setErrorFetchingResources,
|
|
106
|
+
fieldProps,
|
|
107
|
+
fieldState,
|
|
108
|
+
}) => {
|
|
109
|
+
const { uuid, required } = attributeType;
|
|
110
|
+
const { data, isLoading, error: errorFetchingVisitAttributeType } = useVisitAttributeType(uuid);
|
|
111
|
+
const {
|
|
112
|
+
answers,
|
|
113
|
+
isLoading: isLoadingAnswers,
|
|
114
|
+
error: errorFetchingVisitAttributeAnswers,
|
|
115
|
+
} = useConceptAnswersForVisitAttributeType(data?.datatypeConfig);
|
|
116
|
+
const { t } = useTranslation();
|
|
117
|
+
const baseId = useId();
|
|
118
|
+
const labelText = !required ? `${data?.display} (${t('optional', 'optional')})` : data?.display;
|
|
119
|
+
|
|
120
|
+
const {
|
|
121
|
+
formState: { errors },
|
|
122
|
+
} = useFormContext<VisitFormData>();
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (errorFetchingVisitAttributeType || errorFetchingVisitAttributeAnswers) {
|
|
126
|
+
setErrorFetchingResources((prev) => ({
|
|
127
|
+
blockSavingForm: prev?.blockSavingForm || required,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
}, [errorFetchingVisitAttributeAnswers, errorFetchingVisitAttributeType, required, setErrorFetchingResources]);
|
|
131
|
+
|
|
132
|
+
const fieldToRender = useMemo(() => {
|
|
133
|
+
if (isLoading) {
|
|
134
|
+
return <></>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (errorFetchingVisitAttributeType) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
switch (data?.datatypeClassname) {
|
|
142
|
+
case 'org.openmrs.customdatatype.datatype.ConceptDatatype':
|
|
143
|
+
if (isLoadingAnswers) {
|
|
144
|
+
return <SelectSkeleton />;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (errorFetchingVisitAttributeAnswers) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Select
|
|
153
|
+
id={`select-${baseId}`}
|
|
154
|
+
{...fieldProps}
|
|
155
|
+
labelText={labelText}
|
|
156
|
+
invalid={!!fieldState?.error?.message}
|
|
157
|
+
invalidText={fieldState?.error?.message}>
|
|
158
|
+
<SelectItem text={t('selectAnOption', 'Select an option')} value={''} />
|
|
159
|
+
{answers.map((ans, indx) => (
|
|
160
|
+
<SelectItem key={indx} text={ans.display} value={ans.uuid} />
|
|
161
|
+
))}
|
|
162
|
+
</Select>
|
|
163
|
+
);
|
|
164
|
+
case 'org.openmrs.customdatatype.datatype.FloatDatatype':
|
|
165
|
+
return (
|
|
166
|
+
<NumberInput
|
|
167
|
+
{...fieldProps}
|
|
168
|
+
id={`number-${baseId}`}
|
|
169
|
+
label={labelText}
|
|
170
|
+
hideSteppers
|
|
171
|
+
invalid={!!fieldState?.error?.message}
|
|
172
|
+
invalidText={fieldState?.error?.message}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
case 'org.openmrs.customdatatype.datatype.FreeTextDatatype':
|
|
176
|
+
return (
|
|
177
|
+
<TextInput
|
|
178
|
+
{...fieldProps}
|
|
179
|
+
id={`text-${baseId}`}
|
|
180
|
+
labelText={labelText}
|
|
181
|
+
placeholder={labelText}
|
|
182
|
+
invalid={!!fieldState?.error?.message}
|
|
183
|
+
invalidText={fieldState?.error?.message}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
case 'org.openmrs.customdatatype.datatype.LongFreeTextDatatype':
|
|
187
|
+
return (
|
|
188
|
+
<TextArea
|
|
189
|
+
{...fieldProps}
|
|
190
|
+
id={`textarea-${baseId}`}
|
|
191
|
+
labelText={labelText}
|
|
192
|
+
invalid={!!fieldState?.error?.message}
|
|
193
|
+
invalidText={fieldState?.error?.message}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
case 'org.openmrs.customdatatype.datatype.BooleanDatatype':
|
|
197
|
+
return (
|
|
198
|
+
<Checkbox
|
|
199
|
+
{...fieldProps}
|
|
200
|
+
id={`checkbox-${baseId}`}
|
|
201
|
+
labelText={labelText}
|
|
202
|
+
invalid={!!fieldState?.error?.message}
|
|
203
|
+
invalidText={fieldState?.error?.message}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
case 'org.openmrs.customdatatype.datatype.DateDatatype':
|
|
207
|
+
return (
|
|
208
|
+
<OpenmrsDatePicker
|
|
209
|
+
{...fieldProps}
|
|
210
|
+
id={`date-${baseId}`}
|
|
211
|
+
labelText={labelText}
|
|
212
|
+
aria-invalid={!!fieldState?.error?.message}
|
|
213
|
+
invalidText={fieldState?.error?.message}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
default:
|
|
217
|
+
return (
|
|
218
|
+
<TextInput
|
|
219
|
+
{...fieldProps}
|
|
220
|
+
id={`text-${baseId}`}
|
|
221
|
+
labelText={labelText}
|
|
222
|
+
invalid={!!fieldState?.error?.message}
|
|
223
|
+
invalidText={fieldState?.error?.message}
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}, [
|
|
228
|
+
isLoading,
|
|
229
|
+
errorFetchingVisitAttributeType,
|
|
230
|
+
data?.datatypeClassname,
|
|
231
|
+
isLoadingAnswers,
|
|
232
|
+
errorFetchingVisitAttributeAnswers,
|
|
233
|
+
baseId,
|
|
234
|
+
fieldProps,
|
|
235
|
+
labelText,
|
|
236
|
+
fieldState?.error?.message,
|
|
237
|
+
t,
|
|
238
|
+
answers,
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
if (isLoading) {
|
|
242
|
+
return (
|
|
243
|
+
<div className={styles.visitAttributeField}>
|
|
244
|
+
<TextInputSkeleton />
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (errorFetchingVisitAttributeType) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return <div className={styles.visitAttributeField}>{fieldToRender}</div>;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export default VisitAttributeTypeFields;
|