@openmrs/esm-cohort-builder-app 3.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 (103) hide show
  1. package/LICENSE +401 -0
  2. package/README.md +16 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +9 -0
  5. package/dist/139.js +1 -0
  6. package/dist/247.js +1 -0
  7. package/dist/294.js +2 -0
  8. package/dist/294.js.LICENSE.txt +9 -0
  9. package/dist/434.js +2 -0
  10. package/dist/434.js.LICENSE.txt +12 -0
  11. package/dist/480.js +2 -0
  12. package/dist/480.js.LICENSE.txt +12 -0
  13. package/dist/484.js +1 -0
  14. package/dist/574.js +1 -0
  15. package/dist/595.js +2 -0
  16. package/dist/595.js.LICENSE.txt +3 -0
  17. package/dist/690.js +1 -0
  18. package/dist/873.js +1 -0
  19. package/dist/935.js +2 -0
  20. package/dist/935.js.LICENSE.txt +19 -0
  21. package/dist/964.js +2 -0
  22. package/dist/964.js.LICENSE.txt +14 -0
  23. package/dist/main.js +1 -0
  24. package/dist/openmrs-esm-cohort-builder-app.js +1 -0
  25. package/dist/openmrs-esm-cohort-builder-app.js.buildmanifest.json +426 -0
  26. package/dist/openmrs-esm-cohort-builder-app.old +1 -0
  27. package/package.json +99 -0
  28. package/src/cohort-builder-app-menu-link.tsx +14 -0
  29. package/src/cohort-builder.resources.ts +84 -0
  30. package/src/cohort-builder.scss +126 -0
  31. package/src/cohort-builder.test.tsx +12 -0
  32. package/src/cohort-builder.tsx +213 -0
  33. package/src/cohort-builder.utils.ts +197 -0
  34. package/src/components/composition/composition.component.tsx +109 -0
  35. package/src/components/composition/composition.style.css +3 -0
  36. package/src/components/composition/composition.test.tsx +112 -0
  37. package/src/components/composition/composition.utils.ts +53 -0
  38. package/src/components/empty-data/empty-data.component.tsx +25 -0
  39. package/src/components/empty-data/empty-data.style.scss +19 -0
  40. package/src/components/saved-cohorts/saved-cohorts-options/saved-cohort-options.test.tsx +49 -0
  41. package/src/components/saved-cohorts/saved-cohorts-options/saved-cohorts-options.component.tsx +112 -0
  42. package/src/components/saved-cohorts/saved-cohorts.component.tsx +139 -0
  43. package/src/components/saved-cohorts/saved-cohorts.resources.ts +39 -0
  44. package/src/components/saved-cohorts/saved-cohorts.scss +15 -0
  45. package/src/components/saved-cohorts/saved-cohorts.test.tsx +51 -0
  46. package/src/components/saved-queries/saved-queries-options/saved-queries-options.component.tsx +112 -0
  47. package/src/components/saved-queries/saved-queries-options/saved-queries-options.test.tsx +47 -0
  48. package/src/components/saved-queries/saved-queries.component.tsx +139 -0
  49. package/src/components/saved-queries/saved-queries.resources.ts +39 -0
  50. package/src/components/saved-queries/saved-queries.scss +23 -0
  51. package/src/components/saved-queries/saved-queries.test.tsx +50 -0
  52. package/src/components/search-button-set/search-button-set.css +9 -0
  53. package/src/components/search-button-set/search-button-set.test.tsx +26 -0
  54. package/src/components/search-button-set/search-button-set.tsx +47 -0
  55. package/src/components/search-by-concepts/search-by-concepts.component.tsx +344 -0
  56. package/src/components/search-by-concepts/search-by-concepts.style.scss +48 -0
  57. package/src/components/search-by-concepts/search-by-concepts.test.tsx +129 -0
  58. package/src/components/search-by-concepts/search-concept/search-concept.component.tsx +130 -0
  59. package/src/components/search-by-concepts/search-concept/search-concept.resource.ts +53 -0
  60. package/src/components/search-by-concepts/search-concept/search-concept.style.css +25 -0
  61. package/src/components/search-by-concepts/search-concept/search-concept.test.tsx +89 -0
  62. package/src/components/search-by-demographics/search-by-demographics.component.tsx +209 -0
  63. package/src/components/search-by-demographics/search-by-demographics.style.scss +41 -0
  64. package/src/components/search-by-demographics/search-by-demographics.test.tsx +92 -0
  65. package/src/components/search-by-demographics/search-by-demographics.utils.ts +129 -0
  66. package/src/components/search-by-drug-orders/search-by-drug-orders.component.tsx +185 -0
  67. package/src/components/search-by-drug-orders/search-by-drug-orders.resources.ts +60 -0
  68. package/src/components/search-by-drug-orders/search-by-drug-orders.style.scss +4 -0
  69. package/src/components/search-by-drug-orders/search-by-drug-orders.test.tsx +159 -0
  70. package/src/components/search-by-drug-orders/search-by-drug-orders.utils.ts +118 -0
  71. package/src/components/search-by-encounters/search-by-encounters.component.tsx +188 -0
  72. package/src/components/search-by-encounters/search-by-encounters.resources.ts +51 -0
  73. package/src/components/search-by-encounters/search-by-encounters.style.scss +12 -0
  74. package/src/components/search-by-encounters/search-by-encounters.test.tsx +202 -0
  75. package/src/components/search-by-encounters/search-by-encounters.utils.ts +113 -0
  76. package/src/components/search-by-enrollments/search-by-enrollments.component.tsx +183 -0
  77. package/src/components/search-by-enrollments/search-by-enrollments.resources.ts +31 -0
  78. package/src/components/search-by-enrollments/search-by-enrollments.style.scss +4 -0
  79. package/src/components/search-by-enrollments/search-by-enrollments.test.tsx +158 -0
  80. package/src/components/search-by-enrollments/search-by-enrollments.utils.ts +69 -0
  81. package/src/components/search-by-location/search-by-location.component.tsx +97 -0
  82. package/src/components/search-by-location/search-by-location.style.scss +4 -0
  83. package/src/components/search-by-location/search-by-location.test.tsx +110 -0
  84. package/src/components/search-by-location/search-by-location.utils.ts +40 -0
  85. package/src/components/search-by-person-attributes/search-by-person-attributes.component.tsx +90 -0
  86. package/src/components/search-by-person-attributes/search-by-person-attributes.resource.ts +27 -0
  87. package/src/components/search-by-person-attributes/search-by-person-attributes.style.scss +4 -0
  88. package/src/components/search-by-person-attributes/search-by-person-attributes.test.tsx +133 -0
  89. package/src/components/search-by-person-attributes/search-by-person-attributes.utils.ts +42 -0
  90. package/src/components/search-history/search-history-options/search-history-options.component.tsx +290 -0
  91. package/src/components/search-history/search-history-options/search-history-options.resources.ts +29 -0
  92. package/src/components/search-history/search-history-options/search-history-options.test.tsx +144 -0
  93. package/src/components/search-history/search-history.component.tsx +174 -0
  94. package/src/components/search-history/search-history.style.scss +12 -0
  95. package/src/components/search-history/search-history.test.tsx +106 -0
  96. package/src/components/search-history/search-history.utils.ts +14 -0
  97. package/src/components/search-results-table/search-results-table.component.tsx +105 -0
  98. package/src/components/search-results-table/search-results-table.scss +4 -0
  99. package/src/components/search-results-table/search-results-table.test.tsx +47 -0
  100. package/src/declarations.d.tsx +2 -0
  101. package/src/index.ts +47 -0
  102. package/src/setup-tests.ts +1 -0
  103. package/src/types/index.ts +120 -0
@@ -0,0 +1,183 @@
1
+ import React, { useState } from "react";
2
+
3
+ import {
4
+ Column,
5
+ DatePicker,
6
+ DatePickerInput,
7
+ MultiSelect,
8
+ } from "@carbon/react";
9
+ import { showToast } from "@openmrs/esm-framework";
10
+ import dayjs from "dayjs";
11
+ import { useTranslation } from "react-i18next";
12
+
13
+ import { useLocations } from "../../cohort-builder.resources";
14
+ import { DropdownValue, SearchByProps } from "../../types";
15
+ import SearchButtonSet from "../search-button-set/search-button-set";
16
+ import { usePrograms } from "./search-by-enrollments.resources";
17
+ import styles from "./search-by-enrollments.style.scss";
18
+ import { getQueryDetails, getDescription } from "./search-by-enrollments.utils";
19
+
20
+ const SearchByEnrollments: React.FC<SearchByProps> = ({ onSubmit }) => {
21
+ const { t } = useTranslation();
22
+ const { programs, programsError } = usePrograms();
23
+ const { locations, locationsError } = useLocations();
24
+ const [enrolledOnOrAfter, setEnrolledOnOrAfter] = useState("");
25
+ const [enrolledOnOrBefore, setEnrolledOnOrBefore] = useState("");
26
+ const [completedOnOrAfter, setCompletedOnOrAfter] = useState("");
27
+ const [completedOnOrBefore, setCompletedOnOrBefore] = useState("");
28
+ const [selectedLocations, setSelectedLocations] =
29
+ useState<DropdownValue[]>(null);
30
+ const [selectedPrograms, setSelectedPrograms] =
31
+ useState<DropdownValue[]>(null);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+
34
+ if (programsError) {
35
+ showToast({
36
+ title: t("error", "Error"),
37
+ kind: "error",
38
+ critical: true,
39
+ description: programsError?.message,
40
+ });
41
+ }
42
+
43
+ if (locationsError) {
44
+ showToast({
45
+ title: t("error", "Error"),
46
+ kind: "error",
47
+ critical: true,
48
+ description: locationsError?.message,
49
+ });
50
+ }
51
+
52
+ const handleResetInputs = () => {
53
+ setSelectedPrograms(null);
54
+ setEnrolledOnOrAfter("");
55
+ setEnrolledOnOrBefore("");
56
+ setCompletedOnOrAfter("");
57
+ setCompletedOnOrBefore("");
58
+ };
59
+
60
+ const submit = async () => {
61
+ setIsLoading(true);
62
+ const searchParams = {
63
+ enrolledOnOrAfter,
64
+ enrolledOnOrBefore,
65
+ completedOnOrAfter,
66
+ completedOnOrBefore,
67
+ selectedPrograms,
68
+ selectedLocations,
69
+ };
70
+ await onSubmit(getQueryDetails(searchParams), getDescription(searchParams));
71
+ setIsLoading(false);
72
+ };
73
+
74
+ return (
75
+ <>
76
+ <Column>
77
+ <div>
78
+ <MultiSelect
79
+ id="programs"
80
+ data-testid="programs"
81
+ onChange={(data) => setSelectedPrograms(data.selectedItems)}
82
+ items={programs}
83
+ label={t("selectPrograms", "Select programs")}
84
+ />
85
+ </div>
86
+ </Column>
87
+ <Column>
88
+ <div>
89
+ <MultiSelect
90
+ id="locations"
91
+ data-testid="locations"
92
+ onChange={(data) => setSelectedLocations(data.selectedItems)}
93
+ items={locations}
94
+ label={t("selectLocations", "Select locations")}
95
+ />
96
+ </div>
97
+ </Column>
98
+ <div className={styles.column}>
99
+ <Column>
100
+ <DatePicker
101
+ datePickerType="single"
102
+ allowInput={false}
103
+ onChange={(date) => setEnrolledOnOrAfter(dayjs(date[0]).format())}
104
+ >
105
+ <DatePickerInput
106
+ id="enrolledOnOrAfter"
107
+ labelText={t("enrolledBetween", "Enrolled between")}
108
+ value={
109
+ enrolledOnOrAfter &&
110
+ dayjs(enrolledOnOrAfter).format("DD-MM-YYYY")
111
+ }
112
+ placeholder="DD-MM-YYYY"
113
+ size="md"
114
+ />
115
+ </DatePicker>
116
+ </Column>
117
+ <Column>
118
+ <DatePicker
119
+ datePickerType="single"
120
+ allowInput={false}
121
+ onChange={(date) => setEnrolledOnOrBefore(dayjs(date[0]).format())}
122
+ >
123
+ <DatePickerInput
124
+ id="enrolledOnOrBefore"
125
+ labelText={t("and", "and")}
126
+ value={
127
+ enrolledOnOrBefore &&
128
+ dayjs(enrolledOnOrBefore).format("DD-MM-YYYY")
129
+ }
130
+ placeholder="DD-MM-YYYY"
131
+ size="md"
132
+ />
133
+ </DatePicker>
134
+ </Column>
135
+ </div>
136
+ <div className={styles.column}>
137
+ <Column>
138
+ <DatePicker
139
+ datePickerType="single"
140
+ allowInput={false}
141
+ onChange={(date) => setCompletedOnOrAfter(dayjs(date[0]).format())}
142
+ >
143
+ <DatePickerInput
144
+ id="completedOnOrAfter"
145
+ labelText={t("completedBetween", "Completed between")}
146
+ value={
147
+ completedOnOrAfter &&
148
+ dayjs(completedOnOrAfter).format("DD-MM-YYYY")
149
+ }
150
+ placeholder="DD-MM-YYYY"
151
+ size="md"
152
+ />
153
+ </DatePicker>
154
+ </Column>
155
+ <Column>
156
+ <DatePicker
157
+ datePickerType="single"
158
+ allowInput={false}
159
+ onChange={(date) => setCompletedOnOrBefore(dayjs(date[0]).format())}
160
+ >
161
+ <DatePickerInput
162
+ id="completedOnOrBefore"
163
+ labelText={t("and", "and")}
164
+ value={
165
+ completedOnOrBefore &&
166
+ dayjs(completedOnOrBefore).format("DD-MM-YYYY")
167
+ }
168
+ placeholder="DD-MM-YYYY"
169
+ size="md"
170
+ />
171
+ </DatePicker>
172
+ </Column>
173
+ </div>
174
+ <SearchButtonSet
175
+ onHandleReset={handleResetInputs}
176
+ onHandleSubmit={submit}
177
+ isLoading={isLoading}
178
+ />
179
+ </>
180
+ );
181
+ };
182
+
183
+ export default SearchByEnrollments;
@@ -0,0 +1,31 @@
1
+ import { openmrsFetch } from "@openmrs/esm-framework";
2
+ import useSWRImmutable from "swr/immutable";
3
+
4
+ import { DropdownValue, Response } from "../../types";
5
+
6
+ interface ProgramsResponse extends Response {
7
+ name: string;
8
+ }
9
+
10
+ /**
11
+ * @returns Programs
12
+ */
13
+ export function usePrograms() {
14
+ const { data, error } = useSWRImmutable<{
15
+ data: { results: ProgramsResponse[] };
16
+ }>("/ws/rest/v1/program", openmrsFetch);
17
+
18
+ const programs: DropdownValue[] = [];
19
+ data?.data.results.map((program: ProgramsResponse, index: number) => {
20
+ programs.push({
21
+ id: index,
22
+ label: program.name,
23
+ value: program.uuid,
24
+ });
25
+ });
26
+ return {
27
+ isLoading: !data && !error,
28
+ programs,
29
+ programsError: error,
30
+ };
31
+ }
@@ -0,0 +1,4 @@
1
+ .column {
2
+ padding-top: 1.5rem;
3
+ display: flex;
4
+ }
@@ -0,0 +1,158 @@
1
+ import React from "react";
2
+
3
+ import { openmrsFetch } from "@openmrs/esm-framework";
4
+ import { render, fireEvent, waitFor } from "@testing-library/react";
5
+
6
+ import translations from "../../../translations/en.json";
7
+ import { useLocations } from "../../cohort-builder.resources";
8
+ import SearchByEnrollments from "./search-by-enrollments.component";
9
+ import { usePrograms } from "./search-by-enrollments.resources";
10
+
11
+ const mockLocations = [
12
+ {
13
+ id: 0,
14
+ label: "Isolation Ward",
15
+ value: "ac7d7773-fe9f-11ec-8b9b-0242ac1b0002",
16
+ },
17
+ {
18
+ id: 1,
19
+ label: "Armani Hospital",
20
+ value: "8d8718c2-c2cc-11de-8d13-0010c6dffd0f",
21
+ },
22
+ {
23
+ id: 2,
24
+ label: "Pharmacy",
25
+ value: "8d871afc-c2cc-11de-8d13-0010c6dffd0f",
26
+ },
27
+ ];
28
+
29
+ const mockPrograms = [
30
+ {
31
+ id: 0,
32
+ value: "64f950e6-1b07-4ac0-8e7e-f3e148f3463f",
33
+ label: "HIV Care and Treatment",
34
+ },
35
+ {
36
+ id: 1,
37
+ value: "ac1bbc45-8c35-49ff-a574-9553ff789527",
38
+ label: "HIV Preventative Services (PEP/PrEP)",
39
+ },
40
+ ];
41
+
42
+ const expectedQuery = {
43
+ query: {
44
+ columns: [
45
+ {
46
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.givenName",
47
+ name: "firstname",
48
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
49
+ },
50
+ {
51
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.familyName",
52
+ name: "lastname",
53
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
54
+ },
55
+ {
56
+ key: "reporting.library.patientDataDefinition.builtIn.gender",
57
+ name: "gender",
58
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
59
+ },
60
+ {
61
+ key: "reporting.library.patientDataDefinition.builtIn.ageOnDate.fullYears",
62
+ name: "age",
63
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
64
+ },
65
+ {
66
+ key: "reporting.library.patientDataDefinition.builtIn.patientId",
67
+ name: "patientId",
68
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
69
+ },
70
+ ],
71
+ customRowFilterCombination: "1",
72
+ rowFilters: [
73
+ {
74
+ key: "reporting.library.cohortDefinition.builtIn.patientsWithEnrollment",
75
+ parameterValues: {
76
+ programs: [mockPrograms[0].value],
77
+ locationList: [mockLocations[2].value],
78
+ },
79
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
80
+ },
81
+ ],
82
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
83
+ },
84
+ };
85
+
86
+ const mockOpenmrsFetch = openmrsFetch as jest.Mock;
87
+
88
+ jest.mock("./search-by-enrollments.resources", () => {
89
+ const original = jest.requireActual("./search-by-enrollments.resources");
90
+ return {
91
+ ...original,
92
+ usePrograms: jest.fn(),
93
+ };
94
+ });
95
+
96
+ jest.mock("../../cohort-builder.resources", () => {
97
+ const original = jest.requireActual("../../cohort-builder.resources");
98
+ return {
99
+ ...original,
100
+ useLocations: jest.fn(),
101
+ };
102
+ });
103
+
104
+ describe("Test the search by enrollments component", () => {
105
+ it("should be able to select input values", async () => {
106
+ // @ts-ignore
107
+ useLocations.mockImplementation(() => ({
108
+ locations: mockLocations,
109
+ isLoading: false,
110
+ locationsError: undefined,
111
+ }));
112
+ mockOpenmrsFetch.mockReturnValueOnce({
113
+ data: { results: mockLocations },
114
+ });
115
+
116
+ // @ts-ignore
117
+ usePrograms.mockImplementation(() => ({
118
+ programs: mockPrograms,
119
+ isLoading: false,
120
+ programsError: undefined,
121
+ }));
122
+ mockOpenmrsFetch.mockReturnValueOnce({
123
+ data: { results: mockPrograms },
124
+ });
125
+
126
+ const submit = jest.fn();
127
+ const { getByTestId, getByText } = render(
128
+ <SearchByEnrollments onSubmit={submit} />
129
+ );
130
+
131
+ waitFor(() => {
132
+ fireEvent.click(getByText(translations.selectLocations));
133
+ });
134
+
135
+ waitFor(() => {
136
+ fireEvent.click(getByText(mockLocations[2].label));
137
+ });
138
+
139
+ waitFor(() => {
140
+ fireEvent.click(getByText(translations.selectPrograms));
141
+ });
142
+
143
+ waitFor(() => {
144
+ fireEvent.click(getByText(mockPrograms[0].label));
145
+ });
146
+
147
+ waitFor(() => {
148
+ fireEvent.click(getByTestId("search-btn"));
149
+ });
150
+
151
+ await waitFor(() => {
152
+ expect(submit).toBeCalledWith(
153
+ expectedQuery,
154
+ `Patients enrolled in ${mockPrograms[0].label} at ${mockLocations[2].label}`
155
+ );
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,69 @@
1
+ import { composeJson } from "../../cohort-builder.utils";
2
+ import { DropdownValue } from "../../types";
3
+
4
+ interface EnrollmentsSearchParams {
5
+ enrolledOnOrAfter: string;
6
+ enrolledOnOrBefore: string;
7
+ completedOnOrAfter: string;
8
+ completedOnOrBefore: string;
9
+ selectedPrograms: DropdownValue[];
10
+ selectedLocations: DropdownValue[];
11
+ }
12
+
13
+ export const getQueryDetails = ({
14
+ enrolledOnOrAfter,
15
+ enrolledOnOrBefore,
16
+ completedOnOrAfter,
17
+ completedOnOrBefore,
18
+ selectedPrograms,
19
+ selectedLocations,
20
+ }: EnrollmentsSearchParams) => {
21
+ const searchParameter = {
22
+ patientsWithEnrollment: [
23
+ {
24
+ name: "programs",
25
+ value: selectedPrograms?.map((location) => location.value),
26
+ },
27
+ enrolledOnOrAfter && {
28
+ name: "enrolledOnOrAfter",
29
+ value: enrolledOnOrAfter,
30
+ },
31
+ enrolledOnOrBefore && {
32
+ name: "enrolledOnOrBefore",
33
+ value: enrolledOnOrBefore,
34
+ },
35
+ completedOnOrAfter && {
36
+ name: "completedOnOrAfter",
37
+ value: completedOnOrAfter,
38
+ },
39
+ completedOnOrBefore && {
40
+ name: "completedOnOrBefore",
41
+ value: completedOnOrBefore,
42
+ },
43
+ {
44
+ name: "locationList",
45
+ value: selectedLocations?.map((location) => location.value),
46
+ },
47
+ ],
48
+ };
49
+ const queryDetails = composeJson(searchParameter);
50
+
51
+ return queryDetails;
52
+ };
53
+
54
+ export const getDescription = ({
55
+ selectedPrograms,
56
+ selectedLocations,
57
+ }: EnrollmentsSearchParams) => {
58
+ let description = `Patients enrolled in ${selectedPrograms
59
+ ?.map((location) => location.label)
60
+ .join(", ")}`;
61
+
62
+ if (selectedLocations?.length) {
63
+ description =
64
+ description +
65
+ ` at ${selectedLocations?.map((location) => location.label).join(", ")}`;
66
+ }
67
+
68
+ return description;
69
+ };
@@ -0,0 +1,97 @@
1
+ import React, { useState } from "react";
2
+
3
+ import { Column, Dropdown, MultiSelect } from "@carbon/react";
4
+ import { showToast } from "@openmrs/esm-framework";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ import { useLocations } from "../../cohort-builder.resources";
8
+ import { DropdownValue, SearchByProps } from "../../types";
9
+ import SearchButtonSet from "../search-button-set/search-button-set";
10
+ import styles from "./search-by-location.style.scss";
11
+ import { getQueryDetails, getDescription } from "./search-by-location.utils";
12
+
13
+ const SearchByLocation: React.FC<SearchByProps> = ({ onSubmit }) => {
14
+ const { t } = useTranslation();
15
+ const methods = [
16
+ {
17
+ id: 0,
18
+ label: t("anyEncounter", "Any Encounter"),
19
+ value: "ANY",
20
+ },
21
+ {
22
+ id: 1,
23
+ label: t("mostRecentEncounter", "Most Recent Encounter"),
24
+ value: "LAST",
25
+ },
26
+ {
27
+ id: 2,
28
+ label: t("earliestEncounter", "Earliest Encounter"),
29
+ value: "FIRST",
30
+ },
31
+ ];
32
+ const { locations, locationsError } = useLocations();
33
+ const [selectedLocations, setSelectedLocations] =
34
+ useState<DropdownValue[]>(null);
35
+ const [selectedMethod, setSelectedMethod] = useState<DropdownValue>(
36
+ methods[0]
37
+ );
38
+ const [isLoading, setIsLoading] = useState(false);
39
+
40
+ if (locationsError) {
41
+ showToast({
42
+ title: t("error", "Error"),
43
+ kind: "error",
44
+ critical: true,
45
+ description: locationsError?.message,
46
+ });
47
+ }
48
+
49
+ const handleResetInputs = () => {
50
+ setSelectedLocations(null);
51
+ setSelectedMethod(null);
52
+ };
53
+
54
+ const submit = async () => {
55
+ setIsLoading(true);
56
+ await onSubmit(
57
+ getQueryDetails(selectedMethod.value, selectedLocations),
58
+ getDescription(selectedMethod.label, selectedLocations)
59
+ );
60
+ setIsLoading(false);
61
+ };
62
+
63
+ return (
64
+ <>
65
+ <Column>
66
+ <div>
67
+ <MultiSelect
68
+ id="locations"
69
+ data-testid="locations"
70
+ onChange={(data) => setSelectedLocations(data.selectedItems)}
71
+ items={locations}
72
+ label={t("selectLocations", "Select locations")}
73
+ />
74
+ </div>
75
+ </Column>
76
+ <div className={styles.column}>
77
+ <Column>
78
+ <Dropdown
79
+ id="methods"
80
+ data-testid="methods"
81
+ onChange={(data) => setSelectedMethod(data.selectedItem)}
82
+ initialSelectedItem={methods[0]}
83
+ items={methods}
84
+ label={t("selectMethod", "Select a method")}
85
+ />
86
+ </Column>
87
+ </div>
88
+ <SearchButtonSet
89
+ onHandleReset={handleResetInputs}
90
+ onHandleSubmit={submit}
91
+ isLoading={isLoading}
92
+ />
93
+ </>
94
+ );
95
+ };
96
+
97
+ export default SearchByLocation;
@@ -0,0 +1,4 @@
1
+ .column {
2
+ padding-top: 1.5rem;
3
+ display: flex;
4
+ }
@@ -0,0 +1,110 @@
1
+ import React from "react";
2
+
3
+ import { openmrsFetch } from "@openmrs/esm-framework";
4
+ import { render, fireEvent, waitFor } from "@testing-library/react";
5
+
6
+ import translations from "../../../translations/en.json";
7
+ import { useLocations } from "../../cohort-builder.resources";
8
+ import SearchByLocation from "./search-by-location.component";
9
+
10
+ const mockLocations = [
11
+ {
12
+ id: 0,
13
+ label: "Isolation Ward",
14
+ value: "ac7d7773-fe9f-11ec-8b9b-0242ac1b0002",
15
+ },
16
+ {
17
+ id: 1,
18
+ label: "Armani Hospital",
19
+ value: "8d8718c2-c2cc-11de-8d13-0010c6dffd0f",
20
+ },
21
+ {
22
+ id: 2,
23
+ label: "Pharmacy",
24
+ value: "8d871afc-c2cc-11de-8d13-0010c6dffd0f",
25
+ },
26
+ ];
27
+
28
+ const expectedQuery = {
29
+ query: {
30
+ columns: [
31
+ {
32
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.givenName",
33
+ name: "firstname",
34
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
35
+ },
36
+ {
37
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.familyName",
38
+ name: "lastname",
39
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
40
+ },
41
+ {
42
+ key: "reporting.library.patientDataDefinition.builtIn.gender",
43
+ name: "gender",
44
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
45
+ },
46
+ {
47
+ key: "reporting.library.patientDataDefinition.builtIn.ageOnDate.fullYears",
48
+ name: "age",
49
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
50
+ },
51
+ {
52
+ key: "reporting.library.patientDataDefinition.builtIn.patientId",
53
+ name: "patientId",
54
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
55
+ },
56
+ ],
57
+ customRowFilterCombination: "1",
58
+ rowFilters: [
59
+ {
60
+ key: "reporting.library.cohortDefinition.builtIn.encounterSearchAdvanced",
61
+ parameterValues: {
62
+ locationList: [mockLocations[2].value],
63
+ timeQualifier: "LAST",
64
+ },
65
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
66
+ },
67
+ ],
68
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
69
+ },
70
+ };
71
+
72
+ const mockOpenmrsFetch = openmrsFetch as jest.Mock;
73
+
74
+ jest.mock("../../cohort-builder.resources", () => {
75
+ const original = jest.requireActual("../../cohort-builder.resources");
76
+ return {
77
+ ...original,
78
+ useLocations: jest.fn(),
79
+ };
80
+ });
81
+
82
+ describe("Test the search by location component", () => {
83
+ it("should be able to select input values", async () => {
84
+ // @ts-ignore
85
+ useLocations.mockImplementation(() => ({
86
+ locations: mockLocations,
87
+ isLoading: false,
88
+ locationsError: undefined,
89
+ }));
90
+ mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockLocations } });
91
+
92
+ const submit = jest.fn();
93
+ const { getByTestId, getByTitle, getByText } = render(
94
+ <SearchByLocation onSubmit={submit} />
95
+ );
96
+
97
+ fireEvent.click(getByText(translations.selectLocations));
98
+ fireEvent.click(getByText(mockLocations[2].label));
99
+ fireEvent.click(getByTitle("Any Encounter"));
100
+ fireEvent.click(getByText("Most Recent Encounter"));
101
+ fireEvent.click(getByTestId("search-btn"));
102
+
103
+ await waitFor(() => {
104
+ expect(submit).toBeCalledWith(
105
+ expectedQuery,
106
+ `Patients in ${mockLocations[2].label} (by method ANY_ENCOUNTER).`
107
+ );
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,40 @@
1
+ import { composeJson } from "../../cohort-builder.utils";
2
+ import { DropdownValue } from "../../types";
3
+
4
+ export const getQueryDetails = (
5
+ method: string,
6
+ selectedLocations: DropdownValue[]
7
+ ) => {
8
+ const locations = [];
9
+ selectedLocations?.map((location) => locations.push(location.value));
10
+ const searchParameter = {
11
+ encounterSearchAdvanced: [
12
+ { name: "locationList", value: locations },
13
+ { name: "timeQualifier", value: method },
14
+ ],
15
+ };
16
+ const queryDetails = composeJson(searchParameter);
17
+
18
+ return queryDetails;
19
+ };
20
+
21
+ export const getDescription = (
22
+ method: string,
23
+ selectedLocations: DropdownValue[]
24
+ ) => {
25
+ let description = `Patients in ${selectedLocations
26
+ ?.map((location) => location.label)
27
+ .join(", ")}`;
28
+ switch (method) {
29
+ case "FIRST":
30
+ description += " (by method EARLIEST_ENCOUNTER).";
31
+ break;
32
+ case "LAST":
33
+ description += " (by method LATEST_ENCOUNTER).";
34
+ break;
35
+ default:
36
+ description += " (by method ANY_ENCOUNTER).";
37
+ break;
38
+ }
39
+ return description;
40
+ };