@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.
Files changed (146) hide show
  1. package/README.md +4 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/131.js +1 -0
  5. package/dist/131.js.map +1 -0
  6. package/dist/132.js +1 -0
  7. package/dist/132.js.map +1 -0
  8. package/dist/152.js +1 -0
  9. package/dist/152.js.map +1 -0
  10. package/dist/209.js +1 -0
  11. package/dist/209.js.map +1 -0
  12. package/dist/247.js +1 -0
  13. package/dist/247.js.map +1 -0
  14. package/dist/28.js +1 -0
  15. package/dist/28.js.map +1 -0
  16. package/dist/317.js +1 -0
  17. package/dist/317.js.map +1 -0
  18. package/dist/349.js +1 -0
  19. package/dist/349.js.map +1 -0
  20. package/dist/371.js +1 -0
  21. package/dist/371.js.map +1 -0
  22. package/dist/378.js +1 -0
  23. package/dist/378.js.map +1 -0
  24. package/dist/442.js +1 -0
  25. package/dist/442.js.map +1 -0
  26. package/dist/45.js +1 -0
  27. package/dist/45.js.map +1 -0
  28. package/dist/459.js +12 -0
  29. package/dist/459.js.map +1 -0
  30. package/dist/466.js +1 -0
  31. package/dist/466.js.map +1 -0
  32. package/dist/508.js +1 -0
  33. package/dist/508.js.map +1 -0
  34. package/dist/61.js +1 -0
  35. package/dist/61.js.map +1 -0
  36. package/dist/640.js +1 -0
  37. package/dist/640.js.map +1 -0
  38. package/dist/689.js +1 -0
  39. package/dist/689.js.map +1 -0
  40. package/dist/709.js +1 -0
  41. package/dist/709.js.map +1 -0
  42. package/dist/712.js +1 -0
  43. package/dist/712.js.map +1 -0
  44. package/dist/720.js +1 -0
  45. package/dist/720.js.map +1 -0
  46. package/dist/742.js +1 -0
  47. package/dist/742.js.map +1 -0
  48. package/dist/771.js +1 -0
  49. package/dist/771.js.map +1 -0
  50. package/dist/806.js +1 -0
  51. package/dist/806.js.map +1 -0
  52. package/dist/912.js +1 -0
  53. package/dist/912.js.map +1 -0
  54. package/dist/913.js +1 -0
  55. package/dist/913.js.map +1 -0
  56. package/dist/940.js +1 -0
  57. package/dist/940.js.map +1 -0
  58. package/dist/957.js +15 -0
  59. package/dist/957.js.map +1 -0
  60. package/dist/digihmis-esm-patient-registration-app.js +6 -0
  61. package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
  62. package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
  63. package/dist/main.js +17 -0
  64. package/dist/main.js.map +1 -0
  65. package/dist/routes.json +1 -0
  66. package/jest.config.js +26 -0
  67. package/package.json +51 -0
  68. package/rspack.config.js +1 -0
  69. package/src/active-visit-table/active-visit-table.component.tsx +240 -0
  70. package/src/active-visit-table/active-visits.resource.ts +338 -0
  71. package/src/active-visit-table/active-visits.scss +211 -0
  72. package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
  73. package/src/checkin-form/base-visit-type.component.tsx +102 -0
  74. package/src/checkin-form/base-visit-type.scss +75 -0
  75. package/src/checkin-form/checkin-form.scss +167 -0
  76. package/src/checkin-form/checkin-form.workspace.tsx +703 -0
  77. package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
  78. package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
  79. package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
  80. package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
  81. package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
  82. package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
  83. package/src/checkin-form/location-selector.component.tsx +86 -0
  84. package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
  85. package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
  86. package/src/checkin-form/visit-attribute-type.scss +5 -0
  87. package/src/checkin-form/visit-date-time.component.tsx +201 -0
  88. package/src/checkin-form/visit-form.resource.ts +308 -0
  89. package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
  90. package/src/components/custom-card/compact-patient-banner.scss +34 -0
  91. package/src/components/custom-card/custom-loader.component.tsx +22 -0
  92. package/src/components/custom-card/patient-search-context.ts +30 -0
  93. package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
  94. package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
  95. package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
  96. package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
  97. package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
  98. package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
  99. package/src/config-schema.ts +511 -0
  100. package/src/constant.ts +6 -0
  101. package/src/constants/index.ts +1 -0
  102. package/src/dashboard.meta.ts +8 -0
  103. package/src/declarations.d.ts +6 -0
  104. package/src/empty-svg/empty-svg.component.tsx +565 -0
  105. package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
  106. package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
  107. package/src/identifier-workspace/identifier-configure.scss +106 -0
  108. package/src/identifier-workspace/identifier.resource.ts +90 -0
  109. package/src/index.ts +33 -0
  110. package/src/left-panel/left-panel.component.tsx +54 -0
  111. package/src/left-panel/left-panel.scss +13 -0
  112. package/src/page-header/page-header.component.tsx +35 -0
  113. package/src/page-header/page-header.scss +76 -0
  114. package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
  115. package/src/patient-tags/deceased-patient-tag.scss +15 -0
  116. package/src/patient-tags/patient-tags.resource.ts +30 -0
  117. package/src/patient-tags/visit-tag.extension.tsx +43 -0
  118. package/src/patient-tags/visit-tag.scss +19 -0
  119. package/src/registration-form/form-manager.ts +449 -0
  120. package/src/registration-form/registration-form.component.tsx +1565 -0
  121. package/src/registration-form/registration-form.resource.ts +264 -0
  122. package/src/registration-form/registration-form.scss +282 -0
  123. package/src/routes.json +62 -0
  124. package/src/search-bar/search-bar.component.tsx +285 -0
  125. package/src/search-bar/search-bar.resource.ts +274 -0
  126. package/src/search-bar/search-bar.scss +427 -0
  127. package/src/search-dashboard/search-dashboard.component.tsx +17 -0
  128. package/src/search-dashboard/search-dashboard.scss +13 -0
  129. package/src/setup-tests.ts +1 -0
  130. package/src/type/index.ts +463 -0
  131. package/src/utils/fhir-mapper.ts +73 -0
  132. package/src/visit-summary/visit-summary.component.tsx +32 -0
  133. package/src/visit-summary/visit-summary.resource.ts +56 -0
  134. package/src/visit-summary/visit-summary.scss +74 -0
  135. package/translations/am.json +112 -0
  136. package/translations/ar.json +120 -0
  137. package/translations/en.json +112 -0
  138. package/translations/es.json +114 -0
  139. package/translations/fr.json +114 -0
  140. package/translations/he.json +114 -0
  141. package/translations/km.json +111 -0
  142. package/translations/pt.json +114 -0
  143. package/translations/sw.json +112 -0
  144. package/translations/vi.json +111 -0
  145. package/translations/zh.json +111 -0
  146. 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;
@@ -0,0 +1,5 @@
1
+ @use '@carbon/layout';
2
+
3
+ .visitAttributeField {
4
+ margin-bottom: layout.$spacing-05;
5
+ }