@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,344 @@
1
+ import React, { useState } from "react";
2
+
3
+ import {
4
+ DatePicker,
5
+ DatePickerInput,
6
+ Column,
7
+ Dropdown,
8
+ NumberInput,
9
+ Switch,
10
+ ContentSwitcher,
11
+ } from "@carbon/react";
12
+ import dayjs from "dayjs";
13
+ import { useTranslation } from "react-i18next";
14
+
15
+ import {
16
+ composeJson,
17
+ queryDescriptionBuilder,
18
+ } from "../../cohort-builder.utils";
19
+ import { Concept, SearchByProps } from "../../types";
20
+ import SearchButtonSet from "../search-button-set/search-button-set";
21
+ import styles from "./search-by-concepts.style.scss";
22
+ import { SearchConcept } from "./search-concept/search-concept.component";
23
+
24
+ const operators = [
25
+ {
26
+ id: 0,
27
+ label: "<",
28
+ value: "LESS_THAN",
29
+ },
30
+ {
31
+ id: 1,
32
+ label: "<=",
33
+ value: "LESS_EQUAL",
34
+ },
35
+ {
36
+ id: 2,
37
+ label: "=",
38
+ value: "EQUAL",
39
+ },
40
+ {
41
+ id: 3,
42
+ label: ">=",
43
+ value: "GREATER_EQUAL",
44
+ },
45
+ {
46
+ id: 4,
47
+ label: ">",
48
+ value: "GREATER_THAN",
49
+ },
50
+ ];
51
+
52
+ interface Observation {
53
+ timeModifier: string;
54
+ question: string;
55
+ operator1: string;
56
+ modifier: string;
57
+ onOrBefore: string;
58
+ onOrAfter: string;
59
+ value1: string;
60
+ }
61
+
62
+ const types = {
63
+ CWE: "codedObsSearchAdvanced",
64
+ NM: "numericObsSearchAdvanced",
65
+ DT: "dateObsSearchAdvanced",
66
+ ST: "dateObsSearchAdvanced",
67
+ TS: "textObsSearchAdvanced",
68
+ ZZ: "codedObsSearchAdvanced",
69
+ BIT: "codedObsSearchAdvanced",
70
+ };
71
+
72
+ const SearchByConcepts: React.FC<SearchByProps> = ({ onSubmit }) => {
73
+ const { t } = useTranslation();
74
+ const [concept, setConcept] = useState<Concept>(null);
75
+ const [lastDays, setLastDays] = useState(0);
76
+ const [lastMonths, setLastMonths] = useState(0);
77
+ const [operatorValue, setOperatorValue] = useState(0);
78
+ const [operator, setOperator] = useState("LESS_THAN");
79
+ const [timeModifier, setTimeModifier] = useState("ANY");
80
+ const [onOrAfter, setOnOrAfter] = useState("");
81
+ const [onOrBefore, setOnOrBefore] = useState("");
82
+ const [searchText, setSearchText] = useState("");
83
+ const [isLoading, setIsLoading] = useState(false);
84
+
85
+ const observationOptions = [
86
+ {
87
+ id: "option-0",
88
+ label: t("haveObservations", "Patients who have these observations"),
89
+ value: "ANY",
90
+ },
91
+ {
92
+ id: "option-1",
93
+ label: t(
94
+ "haveNoObservations",
95
+ "Patients who do not have these observations"
96
+ ),
97
+ value: "NO",
98
+ },
99
+ ];
100
+
101
+ const whichObservation = [
102
+ {
103
+ id: "option-0",
104
+ label: t("any", "Any"),
105
+ value: "ANY",
106
+ },
107
+ {
108
+ id: "option-1",
109
+ label: t("none", "None"),
110
+ value: "NO",
111
+ },
112
+ {
113
+ id: "option-2",
114
+ label: t("earliest", "Earliest"),
115
+ value: "FIRST",
116
+ },
117
+ {
118
+ id: "option-3",
119
+ label: t("recent", "Most Recent"),
120
+ value: "LAST",
121
+ },
122
+ {
123
+ id: "option-4",
124
+ label: t("lowest", "Lowest"),
125
+ value: "MIN",
126
+ },
127
+ {
128
+ id: "option-5",
129
+ label: t("highest", "Highest"),
130
+ value: "MAX",
131
+ },
132
+ {
133
+ id: "option-6",
134
+ label: t("average", "Average"),
135
+ value: "AVG",
136
+ },
137
+ ];
138
+
139
+ const reset = () => {
140
+ setConcept(null);
141
+ setLastDays(0);
142
+ setSearchText("");
143
+ setOnOrAfter("");
144
+ setOnOrBefore("");
145
+ setLastMonths(0);
146
+ setOperatorValue(0);
147
+ setOperator("LESS_THAN");
148
+ setTimeModifier("ANY");
149
+ };
150
+
151
+ const getOnOrBefore = () => {
152
+ if (lastDays > 0 || lastMonths > 0) {
153
+ return dayjs()
154
+ .subtract(lastDays, "days")
155
+ .subtract(lastMonths, "months")
156
+ .format();
157
+ }
158
+ };
159
+
160
+ const submit = async () => {
161
+ setIsLoading(true);
162
+ const observations: Observation = {
163
+ modifier: "",
164
+ operator1: operator,
165
+ value1: operatorValue > 0 ? operatorValue.toString() : "",
166
+ question: concept.uuid,
167
+ onOrBefore: getOnOrBefore() || onOrBefore,
168
+ onOrAfter,
169
+ timeModifier,
170
+ };
171
+ const dataType = types[concept.hl7Abbrev];
172
+ const params = { [dataType]: [] };
173
+ Object.keys(observations).forEach((key) => {
174
+ observations[key] !== ""
175
+ ? params[dataType].push({
176
+ name:
177
+ key === "modifier"
178
+ ? ["CWE", "TS"].includes(concept.hl7Abbrev)
179
+ ? "values"
180
+ : "value1"
181
+ : key,
182
+ value:
183
+ key === "modifier" && ["CWE", "TS"].includes(concept.hl7Abbrev)
184
+ ? [observations[key]]
185
+ : observations[key],
186
+ })
187
+ : "";
188
+ });
189
+ await onSubmit(
190
+ composeJson(params),
191
+ queryDescriptionBuilder(observations, concept.name)
192
+ );
193
+ setIsLoading(false);
194
+ };
195
+
196
+ return (
197
+ <>
198
+ <div>
199
+ <SearchConcept
200
+ setConcept={setConcept}
201
+ concept={concept}
202
+ searchText={searchText}
203
+ setSearchText={setSearchText}
204
+ />
205
+ {concept?.hl7Abbrev === "NM" ? (
206
+ <>
207
+ <Column className={styles.column}>
208
+ <div style={{ display: "flex" }}>
209
+ <div className={styles.multipleInputs}>
210
+ <p style={{ paddingRight: 20 }}>
211
+ {t("whatObservations", "What observations")}
212
+ </p>
213
+ <Dropdown
214
+ id="timeModifier"
215
+ onChange={(data) =>
216
+ setTimeModifier(data.selectedItem.value)
217
+ }
218
+ initialSelectedItem={whichObservation[0]}
219
+ items={whichObservation}
220
+ className={styles.timeModifier}
221
+ label=""
222
+ />
223
+ </div>
224
+ </div>
225
+ </Column>
226
+ <Column className={styles.column}>
227
+ <p className={styles.value}>{t("whatValues", "What values")}</p>
228
+ <div className={styles.whatValuesInputs}>
229
+ <div className={styles.operators}>
230
+ <ContentSwitcher
231
+ selectedIndex={operators[0].id}
232
+ className={styles.contentSwitcher}
233
+ size="lg"
234
+ onChange={({ index }) =>
235
+ setOperator(operators[index].value)
236
+ }
237
+ >
238
+ {operators.map((operator) => (
239
+ <Switch
240
+ key={operator.id}
241
+ name={operator.value}
242
+ text={operator.label}
243
+ />
244
+ ))}
245
+ </ContentSwitcher>
246
+ </div>
247
+ <div className={styles.multipleInputs}>
248
+ <NumberInput
249
+ hideSteppers={true}
250
+ id="operator-value"
251
+ invalidText={t("numberIsNotValid", "Number is not valid")}
252
+ label={t("valueIn", "Enter a value in ") + concept.units}
253
+ min={0}
254
+ size="sm"
255
+ value={operatorValue}
256
+ onChange={(event, { value }) => setOperatorValue(value)}
257
+ />
258
+ </div>
259
+ </div>
260
+ </Column>
261
+ </>
262
+ ) : (
263
+ <Column className={styles.column}>
264
+ <Dropdown
265
+ id="timeModifier"
266
+ data-testid="timeModifier"
267
+ onChange={(data) => setTimeModifier(data.selectedItem.value)}
268
+ initialSelectedItem={observationOptions[0]}
269
+ items={observationOptions}
270
+ label=""
271
+ />
272
+ </Column>
273
+ )}
274
+ <Column className={styles.dateRange}>
275
+ <Column>
276
+ <NumberInput
277
+ hideSteppers={true}
278
+ id="last-months"
279
+ data-testid="last-months"
280
+ label={t("withinTheLast", "Within the last months")}
281
+ invalidText={t("numberIsNotValid", "Number is not valid")}
282
+ min={0}
283
+ size="sm"
284
+ value={lastMonths}
285
+ onChange={(event, { value }) => setLastMonths(value)}
286
+ />
287
+ </Column>
288
+ <Column>
289
+ <NumberInput
290
+ hideSteppers={true}
291
+ label={t("lastDays", "and / or days")}
292
+ id="last-days"
293
+ data-testid="last-days"
294
+ invalidText={t("numberIsNotValid", "Number is not valid")}
295
+ min={0}
296
+ size="sm"
297
+ value={lastDays}
298
+ onChange={(event, { value }) => setLastDays(value)}
299
+ />
300
+ </Column>
301
+ </Column>
302
+ <div className={styles.dateRange}>
303
+ <Column>
304
+ <DatePicker
305
+ datePickerType="single"
306
+ allowInput={false}
307
+ onChange={(date) => setOnOrAfter(dayjs(date[0]).format())}
308
+ >
309
+ <DatePickerInput
310
+ id="startDate"
311
+ value={onOrAfter && dayjs(onOrAfter).format("DD-MM-YYYY")}
312
+ labelText={t("dateRange", "Date range start date")}
313
+ placeholder="DD-MM-YYYY"
314
+ size="md"
315
+ />
316
+ </DatePicker>
317
+ </Column>
318
+ <Column>
319
+ <DatePicker
320
+ datePickerType="single"
321
+ allowInput={false}
322
+ onChange={(date) => setOnOrBefore(dayjs(date[0]).format())}
323
+ >
324
+ <DatePickerInput
325
+ id="endDate"
326
+ value={onOrBefore && dayjs(onOrBefore).format("DD-MM-YYYY")}
327
+ labelText={t("endDate", "End date")}
328
+ placeholder="DD-MM-YYYY"
329
+ size="md"
330
+ />
331
+ </DatePicker>
332
+ </Column>
333
+ </div>
334
+ </div>
335
+ <SearchButtonSet
336
+ isLoading={isLoading}
337
+ onHandleSubmit={submit}
338
+ onHandleReset={reset}
339
+ />
340
+ </>
341
+ );
342
+ };
343
+
344
+ export default SearchByConcepts;
@@ -0,0 +1,48 @@
1
+ @import "~@openmrs/esm-styleguide/src/vars";
2
+ @import "~carbon-components/src/globals/scss/vars";
3
+ @import "~carbon-components/src/globals/scss/mixins";
4
+
5
+ .contentSwitcher {
6
+ height: $spacing-08;
7
+ }
8
+
9
+ .column {
10
+ padding-top: 1.5rem;
11
+ }
12
+
13
+ .dateRange {
14
+ padding-top: 1.5rem;
15
+ display: flex;
16
+ }
17
+
18
+ .text {
19
+ padding-top: 1.5rem;
20
+ }
21
+
22
+ .multipleInputs {
23
+ display: flex;
24
+ align-items: center;
25
+ }
26
+
27
+ .timeModifier {
28
+ width: 300px;
29
+ }
30
+
31
+ .lastTime {
32
+ padding-left: 1rem;
33
+ padding-right: 1rem;
34
+ }
35
+
36
+ .operators {
37
+ width: 60%;
38
+ padding-right: 1rem;
39
+ }
40
+
41
+ .value {
42
+ padding-right: 2rem;
43
+ }
44
+
45
+ .whatValuesInputs{
46
+ display: flex;
47
+ align-items: end;
48
+ }
@@ -0,0 +1,129 @@
1
+ import React from "react";
2
+
3
+ import { render, cleanup, waitFor, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import dayjs from "dayjs";
6
+
7
+ import { Concept } from "../../types";
8
+ import SearchByConcepts from "./search-by-concepts.component";
9
+ import * as apis from "./search-concept/search-concept.resource";
10
+
11
+ jest.mock("./search-concept/search-concept.resource.ts");
12
+
13
+ const expectedQuery = {
14
+ query: {
15
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
16
+ columns: [
17
+ {
18
+ name: "firstname",
19
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.givenName",
20
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
21
+ },
22
+ {
23
+ name: "lastname",
24
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.familyName",
25
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
26
+ },
27
+ {
28
+ name: "gender",
29
+ key: "reporting.library.patientDataDefinition.builtIn.gender",
30
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
31
+ },
32
+ {
33
+ name: "age",
34
+ key: "reporting.library.patientDataDefinition.builtIn.ageOnDate.fullYears",
35
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
36
+ },
37
+ {
38
+ name: "patientId",
39
+ key: "reporting.library.patientDataDefinition.builtIn.patientId",
40
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
41
+ },
42
+ ],
43
+ rowFilters: [
44
+ {
45
+ key: "reporting.library.cohortDefinition.builtIn.numericObsSearchAdvanced",
46
+ parameterValues: {
47
+ onOrBefore: "",
48
+ operator1: "LESS_THAN",
49
+ question: "2a08da66-f326-4cac-b4cc-6efd68333847",
50
+ timeModifier: "ANY",
51
+ },
52
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
53
+ },
54
+ ],
55
+ customRowFilterCombination: "1",
56
+ },
57
+ };
58
+ const concepts: Concept[] = [
59
+ {
60
+ uuid: "1000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
61
+ units: "",
62
+ answers: [],
63
+ hl7Abbrev: "ZZ",
64
+ name: "Whole blood sample",
65
+ description: "Blood samples not seperated into subtypes",
66
+ datatype: {
67
+ uuid: "8d4a4c94-c2cc-11de-8d13-0010c6dffd0f",
68
+ name: "N/A",
69
+ description: "Not associated with a datatype (e.g., term answers, sets)",
70
+ hl7Abbreviation: "ZZ",
71
+ },
72
+ },
73
+ {
74
+ uuid: "2a08da66-f326-4cac-b4cc-6efd68333847",
75
+ units: "mg/dl",
76
+ answers: [],
77
+ hl7Abbrev: "NM",
78
+ name: "BLOOD SUGAR",
79
+ description: "Laboratory measurement of the glucose level in the blood.",
80
+ datatype: {
81
+ uuid: "8d4a4488-c2cc-11de-8d13-0010c6dffd0f",
82
+ name: "Numeric",
83
+ description:
84
+ "Numeric value, including integer or float (e.g., creatinine, weight)",
85
+ hl7Abbreviation: "NM",
86
+ },
87
+ },
88
+ ];
89
+
90
+ describe("Test the search by concept component", () => {
91
+ afterEach(cleanup);
92
+
93
+ it("should be able to select input values", async () => {
94
+ const user = userEvent.setup();
95
+ jest.spyOn(apis, "getConcepts").mockResolvedValue(concepts);
96
+ const submit = jest.fn();
97
+ render(<SearchByConcepts onSubmit={submit} />);
98
+ const searchInput = screen.getByPlaceholderText("Search Concepts");
99
+ const lastDaysInput = screen.getByTestId("last-days");
100
+ const lastMonthsInput = screen.getByTestId("last-months");
101
+
102
+ await waitFor(() => user.click(searchInput));
103
+ await waitFor(() => user.type(searchInput, "blood sugar"));
104
+ await waitFor(() =>
105
+ expect(jest.spyOn(apis, "getConcepts")).toBeCalledWith("blood sugar")
106
+ );
107
+
108
+ await waitFor(() => user.click(screen.getByText("BLOOD SUGAR")));
109
+ await waitFor(() => user.click(lastDaysInput));
110
+ await waitFor(() => user.clear(lastDaysInput));
111
+ await waitFor(() => user.type(lastDaysInput, "15"));
112
+ await waitFor(() => user.click(lastMonthsInput));
113
+ await waitFor(() => user.clear(lastMonthsInput));
114
+ await waitFor(() => user.type(lastMonthsInput, "4"));
115
+ await waitFor(() => user.click(screen.getByText("Any")));
116
+
117
+ const date = dayjs().subtract(15, "days").subtract(4, "months");
118
+ expectedQuery.query.rowFilters[0].parameterValues.onOrBefore =
119
+ date.format();
120
+ await waitFor(() => user.click(screen.getByTestId("search-btn")));
121
+
122
+ await waitFor(() => {
123
+ expect(submit).toBeCalledWith(
124
+ expectedQuery,
125
+ "Patients with ANY BLOOD SUGAR until " + date.format("D/M/YYYY")
126
+ );
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,130 @@
1
+ import React, {
2
+ Dispatch,
3
+ SetStateAction,
4
+ useState,
5
+ useRef,
6
+ useEffect,
7
+ } from "react";
8
+
9
+ import { Button, Column, Search, CodeSnippetSkeleton } from "@carbon/react";
10
+ import _debounce from "lodash/debounce";
11
+ import { useTranslation } from "react-i18next";
12
+
13
+ import { Concept } from "../../../types";
14
+ import { getConcepts } from "./search-concept.resource";
15
+ import styles from "./search-concept.style.css";
16
+
17
+ interface SearchConceptProps {
18
+ concept: Concept;
19
+ searchText: string;
20
+ setConcept: Dispatch<SetStateAction<Concept>>;
21
+ setSearchText: Dispatch<SetStateAction<String>>;
22
+ }
23
+
24
+ export const SearchConcept: React.FC<SearchConceptProps> = ({
25
+ concept,
26
+ searchText,
27
+ setConcept,
28
+ setSearchText,
29
+ }) => {
30
+ const { t } = useTranslation();
31
+ const [searchResults, setSearchResults] = useState<Concept[]>([]);
32
+ const [searchError, setSearchError] = useState("");
33
+ const [isSearching, setIsSearching] = useState(false);
34
+ const [isSearchResultsEmpty, setIsSearchResultsEmpty] = useState(false);
35
+
36
+ const onSearch = async (search: string) => {
37
+ setSearchResults([]);
38
+ setConcept(null);
39
+ setIsSearching(true);
40
+ setIsSearchResultsEmpty(false);
41
+ try {
42
+ const concepts = await getConcepts(search);
43
+ if (concepts.length) {
44
+ setSearchResults(concepts);
45
+ } else {
46
+ setIsSearchResultsEmpty(true);
47
+ }
48
+ setIsSearching(false);
49
+ } catch (error) {
50
+ setSearchError(error.toString());
51
+ setIsSearching(false);
52
+ }
53
+ };
54
+
55
+ const debouncedSearch = useRef(
56
+ _debounce(async (searchText: string) => {
57
+ if (searchText) {
58
+ await onSearch(searchText);
59
+ }
60
+ }, 500)
61
+ ).current;
62
+
63
+ useEffect(() => {
64
+ return () => {
65
+ debouncedSearch.cancel();
66
+ };
67
+ }, [debouncedSearch]);
68
+
69
+ const onSearchClear = () => {
70
+ setIsSearchResultsEmpty(false);
71
+ setSearchResults([]);
72
+ };
73
+
74
+ const handleConceptClick = (concept: Concept) => {
75
+ setConcept(concept);
76
+ setSearchResults([]);
77
+ setIsSearchResultsEmpty(false);
78
+ };
79
+
80
+ const handleWithDebounce = (event) => {
81
+ setSearchText(event.target.value);
82
+ debouncedSearch(event.target.value);
83
+ };
84
+
85
+ return (
86
+ <div>
87
+ <Column className={styles.column}>
88
+ <Search
89
+ closeButtonLabelText={t("clearSearch", "Clear search")}
90
+ id="concept-search"
91
+ labelText={t("searchConcepts", "Search Concepts")}
92
+ placeholder={t("searchConcepts", "Search Concepts")}
93
+ onChange={handleWithDebounce}
94
+ onClear={onSearchClear}
95
+ size="lg"
96
+ value={searchText}
97
+ />
98
+ <div className={styles.search}>
99
+ {isSearching ? (
100
+ <CodeSnippetSkeleton type="multi" />
101
+ ) : (
102
+ searchResults.map((concept: Concept) => (
103
+ <div key={concept.uuid}>
104
+ <Button
105
+ kind="ghost"
106
+ onClick={() => handleConceptClick(concept)}
107
+ >
108
+ {concept.name}
109
+ </Button>
110
+ <br />
111
+ </div>
112
+ ))
113
+ )}
114
+ </div>
115
+ {concept && (
116
+ <p className={styles.text}>
117
+ {t("whoseAnswer", "Patients with observations whose answer is ")}
118
+ <span className={styles.concept}>{concept.name}</span>
119
+ </p>
120
+ )}
121
+ {isSearchResultsEmpty && (
122
+ <p className={styles.text}>
123
+ {t("noSearchItems", "There are no search items")}
124
+ </p>
125
+ )}
126
+ {searchError && <span>{searchError}</span>}
127
+ </Column>
128
+ </div>
129
+ );
130
+ };
@@ -0,0 +1,53 @@
1
+ import { FetchResponse, openmrsFetch } from "@openmrs/esm-framework";
2
+
3
+ import { Concept, DataType } from "../../../types";
4
+
5
+ interface ConceptResponse {
6
+ uuid: string;
7
+ descriptions: Description[];
8
+ units: string;
9
+ answers: string[];
10
+ datatype: DataType;
11
+ name: { name: string };
12
+ }
13
+
14
+ interface Description {
15
+ locale: string;
16
+ description: string;
17
+ }
18
+
19
+ /**
20
+ * @returns Concepts
21
+ * @param conceptName
22
+ */
23
+ export async function getConcepts(conceptName: String): Promise<Concept[]> {
24
+ const searchResult: FetchResponse<{ results: ConceptResponse[] }> =
25
+ await openmrsFetch(`/ws/rest/v1/concept?v=full&q=${conceptName}`, {
26
+ method: "GET",
27
+ });
28
+
29
+ let concepts: Concept[] = [];
30
+ if (searchResult.data.results.length > 0) {
31
+ concepts = searchResult.data.results.map((concept) => {
32
+ const description = concept.descriptions.filter(
33
+ (description: Description) =>
34
+ description.locale == "en" ? description.description : ""
35
+ );
36
+ const conceptData: Concept = {
37
+ uuid: concept.uuid,
38
+ units: concept.units || "",
39
+ answers: concept.answers,
40
+ hl7Abbrev: concept.datatype.hl7Abbreviation,
41
+ name: concept.name.name,
42
+ description:
43
+ description.length > 0
44
+ ? description[0].description
45
+ : "no description available",
46
+ datatype: concept.datatype,
47
+ };
48
+ return conceptData;
49
+ });
50
+ }
51
+
52
+ return concepts;
53
+ }