@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.
- package/LICENSE +401 -0
- package/README.md +16 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +9 -0
- package/dist/139.js +1 -0
- package/dist/247.js +1 -0
- package/dist/294.js +2 -0
- package/dist/294.js.LICENSE.txt +9 -0
- package/dist/434.js +2 -0
- package/dist/434.js.LICENSE.txt +12 -0
- package/dist/480.js +2 -0
- package/dist/480.js.LICENSE.txt +12 -0
- package/dist/484.js +1 -0
- package/dist/574.js +1 -0
- package/dist/595.js +2 -0
- package/dist/595.js.LICENSE.txt +3 -0
- package/dist/690.js +1 -0
- package/dist/873.js +1 -0
- package/dist/935.js +2 -0
- package/dist/935.js.LICENSE.txt +19 -0
- package/dist/964.js +2 -0
- package/dist/964.js.LICENSE.txt +14 -0
- package/dist/main.js +1 -0
- package/dist/openmrs-esm-cohort-builder-app.js +1 -0
- package/dist/openmrs-esm-cohort-builder-app.js.buildmanifest.json +426 -0
- package/dist/openmrs-esm-cohort-builder-app.old +1 -0
- package/package.json +99 -0
- package/src/cohort-builder-app-menu-link.tsx +14 -0
- package/src/cohort-builder.resources.ts +84 -0
- package/src/cohort-builder.scss +126 -0
- package/src/cohort-builder.test.tsx +12 -0
- package/src/cohort-builder.tsx +213 -0
- package/src/cohort-builder.utils.ts +197 -0
- package/src/components/composition/composition.component.tsx +109 -0
- package/src/components/composition/composition.style.css +3 -0
- package/src/components/composition/composition.test.tsx +112 -0
- package/src/components/composition/composition.utils.ts +53 -0
- package/src/components/empty-data/empty-data.component.tsx +25 -0
- package/src/components/empty-data/empty-data.style.scss +19 -0
- package/src/components/saved-cohorts/saved-cohorts-options/saved-cohort-options.test.tsx +49 -0
- package/src/components/saved-cohorts/saved-cohorts-options/saved-cohorts-options.component.tsx +112 -0
- package/src/components/saved-cohorts/saved-cohorts.component.tsx +139 -0
- package/src/components/saved-cohorts/saved-cohorts.resources.ts +39 -0
- package/src/components/saved-cohorts/saved-cohorts.scss +15 -0
- package/src/components/saved-cohorts/saved-cohorts.test.tsx +51 -0
- package/src/components/saved-queries/saved-queries-options/saved-queries-options.component.tsx +112 -0
- package/src/components/saved-queries/saved-queries-options/saved-queries-options.test.tsx +47 -0
- package/src/components/saved-queries/saved-queries.component.tsx +139 -0
- package/src/components/saved-queries/saved-queries.resources.ts +39 -0
- package/src/components/saved-queries/saved-queries.scss +23 -0
- package/src/components/saved-queries/saved-queries.test.tsx +50 -0
- package/src/components/search-button-set/search-button-set.css +9 -0
- package/src/components/search-button-set/search-button-set.test.tsx +26 -0
- package/src/components/search-button-set/search-button-set.tsx +47 -0
- package/src/components/search-by-concepts/search-by-concepts.component.tsx +344 -0
- package/src/components/search-by-concepts/search-by-concepts.style.scss +48 -0
- package/src/components/search-by-concepts/search-by-concepts.test.tsx +129 -0
- package/src/components/search-by-concepts/search-concept/search-concept.component.tsx +130 -0
- package/src/components/search-by-concepts/search-concept/search-concept.resource.ts +53 -0
- package/src/components/search-by-concepts/search-concept/search-concept.style.css +25 -0
- package/src/components/search-by-concepts/search-concept/search-concept.test.tsx +89 -0
- package/src/components/search-by-demographics/search-by-demographics.component.tsx +209 -0
- package/src/components/search-by-demographics/search-by-demographics.style.scss +41 -0
- package/src/components/search-by-demographics/search-by-demographics.test.tsx +92 -0
- package/src/components/search-by-demographics/search-by-demographics.utils.ts +129 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.component.tsx +185 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.resources.ts +60 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.style.scss +4 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.test.tsx +159 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.utils.ts +118 -0
- package/src/components/search-by-encounters/search-by-encounters.component.tsx +188 -0
- package/src/components/search-by-encounters/search-by-encounters.resources.ts +51 -0
- package/src/components/search-by-encounters/search-by-encounters.style.scss +12 -0
- package/src/components/search-by-encounters/search-by-encounters.test.tsx +202 -0
- package/src/components/search-by-encounters/search-by-encounters.utils.ts +113 -0
- package/src/components/search-by-enrollments/search-by-enrollments.component.tsx +183 -0
- package/src/components/search-by-enrollments/search-by-enrollments.resources.ts +31 -0
- package/src/components/search-by-enrollments/search-by-enrollments.style.scss +4 -0
- package/src/components/search-by-enrollments/search-by-enrollments.test.tsx +158 -0
- package/src/components/search-by-enrollments/search-by-enrollments.utils.ts +69 -0
- package/src/components/search-by-location/search-by-location.component.tsx +97 -0
- package/src/components/search-by-location/search-by-location.style.scss +4 -0
- package/src/components/search-by-location/search-by-location.test.tsx +110 -0
- package/src/components/search-by-location/search-by-location.utils.ts +40 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.component.tsx +90 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.resource.ts +27 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.style.scss +4 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.test.tsx +133 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.utils.ts +42 -0
- package/src/components/search-history/search-history-options/search-history-options.component.tsx +290 -0
- package/src/components/search-history/search-history-options/search-history-options.resources.ts +29 -0
- package/src/components/search-history/search-history-options/search-history-options.test.tsx +144 -0
- package/src/components/search-history/search-history.component.tsx +174 -0
- package/src/components/search-history/search-history.style.scss +12 -0
- package/src/components/search-history/search-history.test.tsx +106 -0
- package/src/components/search-history/search-history.utils.ts +14 -0
- package/src/components/search-results-table/search-results-table.component.tsx +105 -0
- package/src/components/search-results-table/search-results-table.scss +4 -0
- package/src/components/search-results-table/search-results-table.test.tsx +47 -0
- package/src/declarations.d.tsx +2 -0
- package/src/index.ts +47 -0
- package/src/setup-tests.ts +1 -0
- package/src/types/index.ts +120 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.heading {
|
|
6
|
+
@include type.type-style('productive-heading-02');
|
|
7
|
+
color: $text-02;
|
|
8
|
+
margin-bottom: spacing.$spacing-05;;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.heading:after {
|
|
12
|
+
content: "";
|
|
13
|
+
display: block;
|
|
14
|
+
width: 2rem;
|
|
15
|
+
padding-top: 0.188rem;
|
|
16
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.mainContainer {
|
|
20
|
+
padding-bottom: 1.5rem;
|
|
21
|
+
background: $ui-01;
|
|
22
|
+
display: flex;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.title {
|
|
27
|
+
padding: 1rem;
|
|
28
|
+
font-size: 1.2rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.desktopContainer {
|
|
32
|
+
width: 50%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.tabletContainer {
|
|
36
|
+
width: 90%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.cohortBuilder {
|
|
40
|
+
:global(.cds--date-picker.cds--date-picker--single .cds--date-picker__input) {
|
|
41
|
+
width: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:global(.cds--tab-content) {
|
|
45
|
+
width: 80%;
|
|
46
|
+
padding: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:global(.cds--inline-loading__text) {
|
|
50
|
+
color: white
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:global(.cds--tab--list) {
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
margin: 1rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:global(.cds--tabs) {
|
|
59
|
+
width: auto;
|
|
60
|
+
max-height: 100%;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:global(.cds--tabs .cds--tabs__nav-link) {
|
|
64
|
+
border-bottom: 0;
|
|
65
|
+
border-left: 3px solid $ui-03;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:global(.cds--tabs .cds--tabs__nav-item--selected) {
|
|
69
|
+
border-left: 3px solid var(--brand-03);
|
|
70
|
+
border-bottom: 0;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:global(.cds--number) {
|
|
75
|
+
width: 68%;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:global(.cds--tabs__nav-link) {
|
|
79
|
+
&:active, &:focus {
|
|
80
|
+
outline: 0 !important;
|
|
81
|
+
outline-offset: 0 !important;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:global(.cds--multi-select__wrapper .cds--list-box__wrapper) {
|
|
86
|
+
width: 90% !important;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:global(.cds--col) {
|
|
90
|
+
padding-left: 0;
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.tabletTab {
|
|
96
|
+
width: 25%;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.desktopTab {
|
|
100
|
+
width: 20%;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.tab {
|
|
104
|
+
display: flex;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.tabContainer {
|
|
108
|
+
background: white;
|
|
109
|
+
padding-left: 1.5rem;
|
|
110
|
+
padding-right: 1rem;
|
|
111
|
+
padding-top: 1rem;
|
|
112
|
+
margin-bottom: 1rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.optionCell {
|
|
116
|
+
padding: 0 !important;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.optionHeader {
|
|
120
|
+
width: 1.5rem !important;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.text {
|
|
124
|
+
font-size: 0.9rem;
|
|
125
|
+
padding: 0.5rem;
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { render, cleanup } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import CohortBuilder from "./cohort-builder";
|
|
6
|
+
|
|
7
|
+
describe("Test the cohort builder", () => {
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
it(`renders without dying`, () => {
|
|
10
|
+
render(<CohortBuilder />);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Tab, Tabs, TabList, TabPanels, TabPanel } from "@carbon/react";
|
|
4
|
+
import { showToast, useLayoutType } from "@openmrs/esm-framework";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getCohortMembers,
|
|
9
|
+
getDataSet,
|
|
10
|
+
search,
|
|
11
|
+
} from "./cohort-builder.resources";
|
|
12
|
+
import styles from "./cohort-builder.scss";
|
|
13
|
+
import { addToHistory } from "./cohort-builder.utils";
|
|
14
|
+
import Composition from "./components/composition/composition.component";
|
|
15
|
+
import SavedCohorts from "./components/saved-cohorts/saved-cohorts.component";
|
|
16
|
+
import SavedQueries from "./components/saved-queries/saved-queries.component";
|
|
17
|
+
import SearchByConcepts from "./components/search-by-concepts/search-by-concepts.component";
|
|
18
|
+
import SearchByDemographics from "./components/search-by-demographics/search-by-demographics.component";
|
|
19
|
+
import SearchByDrugOrder from "./components/search-by-drug-orders/search-by-drug-orders.component";
|
|
20
|
+
import SearchByEncounters from "./components/search-by-encounters/search-by-encounters.component";
|
|
21
|
+
import SearchByEnrollments from "./components/search-by-enrollments/search-by-enrollments.component";
|
|
22
|
+
import SearchByLocation from "./components/search-by-location/search-by-location.component";
|
|
23
|
+
import SearchByPersonAttributes from "./components/search-by-person-attributes/search-by-person-attributes.component";
|
|
24
|
+
import SearchHistory from "./components/search-history/search-history.component";
|
|
25
|
+
import SearchResultsTable from "./components/search-results-table/search-results-table.component";
|
|
26
|
+
import { Patient, SearchParams } from "./types";
|
|
27
|
+
|
|
28
|
+
interface TabItem {
|
|
29
|
+
name: string;
|
|
30
|
+
component: JSX.Element;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CohortBuilder: React.FC = () => {
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
const [patients, setPatients] = useState<Patient[]>([]);
|
|
36
|
+
const [isHistoryUpdated, setIsHistoryUpdated] = useState(true);
|
|
37
|
+
const isLayoutTablet = useLayoutType() === "tablet";
|
|
38
|
+
|
|
39
|
+
const runSearch = (
|
|
40
|
+
searchParams: SearchParams,
|
|
41
|
+
queryDescription: string
|
|
42
|
+
): Promise<boolean> => {
|
|
43
|
+
return new Promise(async (resolve) => {
|
|
44
|
+
setPatients([]);
|
|
45
|
+
try {
|
|
46
|
+
const {
|
|
47
|
+
data: { rows },
|
|
48
|
+
} = await search(searchParams);
|
|
49
|
+
rows.map((patient: Patient) => {
|
|
50
|
+
patient.id = patient.patientId.toString();
|
|
51
|
+
patient.name = `${patient.firstname} ${patient.lastname}`;
|
|
52
|
+
});
|
|
53
|
+
setPatients(rows);
|
|
54
|
+
addToHistory(queryDescription, rows, searchParams.query);
|
|
55
|
+
showToast({
|
|
56
|
+
title: t("success", "Success!"),
|
|
57
|
+
kind: "success",
|
|
58
|
+
critical: true,
|
|
59
|
+
description: t(
|
|
60
|
+
"searchIsCompleted",
|
|
61
|
+
`Search is completed with ${rows.length} result(s)`,
|
|
62
|
+
{ numOfResults: rows.length }
|
|
63
|
+
),
|
|
64
|
+
});
|
|
65
|
+
setIsHistoryUpdated(true);
|
|
66
|
+
resolve(true);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
showToast({
|
|
69
|
+
title: t("error", "Error"),
|
|
70
|
+
kind: "error",
|
|
71
|
+
critical: true,
|
|
72
|
+
description: error?.message,
|
|
73
|
+
});
|
|
74
|
+
resolve(true);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getQueryResults = async (queryId: string) => {
|
|
80
|
+
try {
|
|
81
|
+
const patients = await getDataSet(queryId);
|
|
82
|
+
setPatients(patients);
|
|
83
|
+
showToast({
|
|
84
|
+
title: t("success", "Success!"),
|
|
85
|
+
kind: "success",
|
|
86
|
+
critical: true,
|
|
87
|
+
description: t(
|
|
88
|
+
"searchIsCompleted",
|
|
89
|
+
`Search is completed with ${patients.length} result(s)`,
|
|
90
|
+
{ numOfResults: patients.length }
|
|
91
|
+
),
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
showToast({
|
|
95
|
+
title: t("error", "Error"),
|
|
96
|
+
kind: "error",
|
|
97
|
+
critical: true,
|
|
98
|
+
description: error?.message,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getCohortResults = async (cohortId: string) => {
|
|
104
|
+
try {
|
|
105
|
+
const patients = await getCohortMembers(cohortId);
|
|
106
|
+
setPatients(patients);
|
|
107
|
+
showToast({
|
|
108
|
+
title: t("success", "Success!"),
|
|
109
|
+
kind: "success",
|
|
110
|
+
critical: true,
|
|
111
|
+
description: t(
|
|
112
|
+
"searchIsCompleted",
|
|
113
|
+
`Search is completed with ${patients.length} result(s)`,
|
|
114
|
+
{ numOfResults: patients.length }
|
|
115
|
+
),
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
showToast({
|
|
119
|
+
title: t("error", "Error"),
|
|
120
|
+
kind: "error",
|
|
121
|
+
critical: true,
|
|
122
|
+
description: error?.message,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const tabs: TabItem[] = [
|
|
128
|
+
{
|
|
129
|
+
name: t("concepts", "Concepts"),
|
|
130
|
+
component: <SearchByConcepts onSubmit={runSearch} />,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: t("demographics", "Demographics"),
|
|
134
|
+
component: <SearchByDemographics onSubmit={runSearch} />,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: t("personAttributes", "Person Attributes"),
|
|
138
|
+
component: <SearchByPersonAttributes onSubmit={runSearch} />,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: t("encounters", "Encounters"),
|
|
142
|
+
component: <SearchByEncounters onSubmit={runSearch} />,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: t("location", "Location"),
|
|
146
|
+
component: <SearchByLocation onSubmit={runSearch} />,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: t("enrollments", "Enrollments"),
|
|
150
|
+
component: <SearchByEnrollments onSubmit={runSearch} />,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: t("drugOrder", "Drug Order"),
|
|
154
|
+
component: <SearchByDrugOrder onSubmit={runSearch} />,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: t("composition", "Composition"),
|
|
158
|
+
component: <Composition onSubmit={runSearch} />,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: t("savedDefinitions", "Saved Cohorts"),
|
|
162
|
+
component: <SavedCohorts onViewCohort={getCohortResults} />,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: t("savedDefinitions", "Saved Queries"),
|
|
166
|
+
component: <SavedQueries onViewQuery={getQueryResults} />,
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
className={`omrs-main-content ${styles.mainContainer} ${styles.cohortBuilder}`}
|
|
173
|
+
>
|
|
174
|
+
<div
|
|
175
|
+
className={
|
|
176
|
+
isLayoutTablet ? styles.tabletContainer : styles.desktopContainer
|
|
177
|
+
}
|
|
178
|
+
>
|
|
179
|
+
<p className={styles.title}>{t("cohortBuilder", "Cohort Builder")}</p>
|
|
180
|
+
<div className={styles.tabContainer}>
|
|
181
|
+
<p className={styles.heading}>
|
|
182
|
+
{t("searchCriteria", "Search Criteria")}
|
|
183
|
+
</p>
|
|
184
|
+
<div className={styles.tab}>
|
|
185
|
+
<Tabs
|
|
186
|
+
className={`${styles.verticalTabs} ${
|
|
187
|
+
isLayoutTablet ? styles.tabletTab : styles.desktopTab
|
|
188
|
+
}`}
|
|
189
|
+
>
|
|
190
|
+
<TabList aria-label="navigation">
|
|
191
|
+
{tabs.map((tab: TabItem, index: number) => (
|
|
192
|
+
<Tab key={index}>{tab.name}</Tab>
|
|
193
|
+
))}
|
|
194
|
+
</TabList>
|
|
195
|
+
<TabPanels>
|
|
196
|
+
{tabs.map((tab: TabItem, index: number) => (
|
|
197
|
+
<TabPanel key={index}>{tab.component}</TabPanel>
|
|
198
|
+
))}
|
|
199
|
+
</TabPanels>
|
|
200
|
+
</Tabs>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<SearchResultsTable patients={patients} />
|
|
204
|
+
<SearchHistory
|
|
205
|
+
isHistoryUpdated={isHistoryUpdated}
|
|
206
|
+
setIsHistoryUpdated={setIsHistoryUpdated}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export default CohortBuilder;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Column, Patient, Query } from "./types";
|
|
2
|
+
|
|
3
|
+
export const composeJson = (searchParameters) => {
|
|
4
|
+
const query: Query = {
|
|
5
|
+
type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
|
|
6
|
+
columns: [],
|
|
7
|
+
rowFilters: [],
|
|
8
|
+
customRowFilterCombination: "",
|
|
9
|
+
};
|
|
10
|
+
query.columns = addColumnsToDisplay();
|
|
11
|
+
let counter = 0;
|
|
12
|
+
query.rowFilters = [];
|
|
13
|
+
for (let field in searchParameters) {
|
|
14
|
+
if (isNullValues(searchParameters[field])) {
|
|
15
|
+
delete searchParameters[field];
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (searchParameters[field] != "all" && searchParameters != "") {
|
|
19
|
+
query.rowFilters[counter] = {};
|
|
20
|
+
query.rowFilters[counter].key = getDefinitionLibraryKey(
|
|
21
|
+
field,
|
|
22
|
+
searchParameters[field]
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(searchParameters[field])) {
|
|
26
|
+
query.rowFilters[counter].parameterValues = getParameterValues(
|
|
27
|
+
searchParameters[field]
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
searchParameters[field].length >= 1 &&
|
|
32
|
+
searchParameters[field][0].livingStatus === "alive"
|
|
33
|
+
) {
|
|
34
|
+
query.rowFilters[counter].livingStatus = "alive";
|
|
35
|
+
}
|
|
36
|
+
query.rowFilters[counter].type =
|
|
37
|
+
"org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition";
|
|
38
|
+
counter += 1;
|
|
39
|
+
}
|
|
40
|
+
query.customRowFilterCombination = composeFilterCombination(query.rowFilters);
|
|
41
|
+
return { query };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const isNullValues = (fieldValues) => {
|
|
45
|
+
if (Array.isArray(fieldValues) && fieldValues.length >= 1) {
|
|
46
|
+
return !fieldValues[0].value;
|
|
47
|
+
}
|
|
48
|
+
return fieldValues === "all" || !fieldValues;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const getDefinitionLibraryKey = (field: string, value: string) => {
|
|
52
|
+
let definitionLibraryKey = "reporting.library.cohortDefinition.builtIn";
|
|
53
|
+
switch (field) {
|
|
54
|
+
case "gender":
|
|
55
|
+
definitionLibraryKey += `.${value}`;
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
definitionLibraryKey += `.${field}`;
|
|
59
|
+
}
|
|
60
|
+
return definitionLibraryKey;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const getParameterValues = (parameterFields) => {
|
|
64
|
+
const parameter = {};
|
|
65
|
+
parameterFields.forEach((eachParam) => {
|
|
66
|
+
parameter[eachParam.name] = eachParam.value;
|
|
67
|
+
});
|
|
68
|
+
return parameter;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const composeFilterCombination = (filterColumns) => {
|
|
72
|
+
let compositionTitle = "";
|
|
73
|
+
const totalNumber = filterColumns.length;
|
|
74
|
+
for (let index = 1; index <= totalNumber; index++) {
|
|
75
|
+
if (filterColumns[index - 1].livingStatus === "alive") {
|
|
76
|
+
compositionTitle += `NOT ${index}`;
|
|
77
|
+
delete filterColumns[index - 1].livingStatus;
|
|
78
|
+
} else {
|
|
79
|
+
compositionTitle += `${index}`;
|
|
80
|
+
}
|
|
81
|
+
compositionTitle += index < totalNumber ? " AND " : "";
|
|
82
|
+
}
|
|
83
|
+
return compositionTitle;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const addColumnsToDisplay = () => {
|
|
87
|
+
const columns = [
|
|
88
|
+
{
|
|
89
|
+
name: "firstname",
|
|
90
|
+
key: "preferredName.givenName",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "lastname",
|
|
94
|
+
key: "preferredName.familyName",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "gender",
|
|
98
|
+
key: "gender",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "age",
|
|
102
|
+
key: "ageOnDate.fullYears",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "patientId",
|
|
106
|
+
key: "patientId",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const columnValues = columns.map((aColumn: Column) => {
|
|
111
|
+
aColumn.type =
|
|
112
|
+
"org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition";
|
|
113
|
+
aColumn.key = `reporting.library.patientDataDefinition.builtIn.${aColumn.key}`;
|
|
114
|
+
return aColumn;
|
|
115
|
+
});
|
|
116
|
+
return columnValues;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const addToHistory = (
|
|
120
|
+
description: string,
|
|
121
|
+
patients: Patient[],
|
|
122
|
+
parameters: {}
|
|
123
|
+
) => {
|
|
124
|
+
const oldHistory = JSON.parse(
|
|
125
|
+
window.sessionStorage.getItem("openmrsHistory")
|
|
126
|
+
);
|
|
127
|
+
let newHistory = [];
|
|
128
|
+
|
|
129
|
+
if (oldHistory) {
|
|
130
|
+
newHistory = [...oldHistory, { description, patients, parameters }];
|
|
131
|
+
} else {
|
|
132
|
+
newHistory = [{ description, patients, parameters }];
|
|
133
|
+
}
|
|
134
|
+
window.sessionStorage.setItem("openmrsHistory", JSON.stringify(newHistory));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const formatDate = (dateString: string) => {
|
|
138
|
+
const date = new Date(dateString);
|
|
139
|
+
const day = date.getDate();
|
|
140
|
+
const month = date.getMonth() + 1;
|
|
141
|
+
const year = date.getFullYear();
|
|
142
|
+
return `${day}/${month}/${year}`;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* builds a query description based on query input
|
|
147
|
+
* @param {object} state the current state
|
|
148
|
+
* @param {string} conceptName the concept name
|
|
149
|
+
* @returns {string} date in the required format
|
|
150
|
+
*/
|
|
151
|
+
export const queryDescriptionBuilder = (state, conceptName: string) => {
|
|
152
|
+
const { timeModifier, onOrAfter, onOrBefore } = state;
|
|
153
|
+
|
|
154
|
+
const onOrAfterDescription = onOrAfter
|
|
155
|
+
? `since ${formatDate(onOrAfter)}`
|
|
156
|
+
: "";
|
|
157
|
+
const onOrBeforeDescription = onOrBefore
|
|
158
|
+
? `until ${formatDate(onOrBefore)}`
|
|
159
|
+
: "";
|
|
160
|
+
|
|
161
|
+
return `Patients with ${timeModifier} ${conceptName} ${onOrAfterDescription} ${onOrBeforeDescription}`.trim();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const convertToCSV = (patients: Patient[]) => {
|
|
165
|
+
const csv =
|
|
166
|
+
"patient_id, full_name, age, gender\n" +
|
|
167
|
+
patients
|
|
168
|
+
.map((patient) => {
|
|
169
|
+
const orderedPatient = {
|
|
170
|
+
patientId: patient.patientId,
|
|
171
|
+
name: patient.name,
|
|
172
|
+
age: patient.age,
|
|
173
|
+
gender: patient.gender,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return Object.keys(orderedPatient)
|
|
177
|
+
.map((key) => {
|
|
178
|
+
return `"${patient[key]}"`;
|
|
179
|
+
})
|
|
180
|
+
.join(",");
|
|
181
|
+
})
|
|
182
|
+
.join("\n");
|
|
183
|
+
|
|
184
|
+
return csv;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const downloadCSV = (data, filename) => {
|
|
188
|
+
const blob = new Blob([convertToCSV(data)], {
|
|
189
|
+
type: "text/csv;charset=utf-8;",
|
|
190
|
+
});
|
|
191
|
+
const url = URL.createObjectURL(blob);
|
|
192
|
+
|
|
193
|
+
const pom = document.createElement("a");
|
|
194
|
+
pom.href = url;
|
|
195
|
+
pom.setAttribute("download", filename);
|
|
196
|
+
pom.click();
|
|
197
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { TextInput } from "@carbon/react";
|
|
4
|
+
import { showNotification } from "@openmrs/esm-framework";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
|
|
7
|
+
import { SearchByProps } from "../../types";
|
|
8
|
+
import SearchButtonSet from "../search-button-set/search-button-set";
|
|
9
|
+
import styles from "./composition.style.css";
|
|
10
|
+
import {
|
|
11
|
+
createCompositionQuery,
|
|
12
|
+
isCompositionValid,
|
|
13
|
+
} from "./composition.utils";
|
|
14
|
+
|
|
15
|
+
const Composition: React.FC<SearchByProps> = ({ onSubmit }) => {
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [compositionQuery, setCompositionQuery] = useState("");
|
|
18
|
+
const [description, setDescription] = useState("");
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
|
|
21
|
+
const handleResetInputs = () => {
|
|
22
|
+
setDescription("");
|
|
23
|
+
setCompositionQuery("");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleCompositionQuery = (composition: string) => {
|
|
27
|
+
setCompositionQuery(composition);
|
|
28
|
+
setDescription("Composition of " + composition);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const submit = async () => {
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
if (isCompositionValid(compositionQuery)) {
|
|
35
|
+
const searchParams = createCompositionQuery(compositionQuery);
|
|
36
|
+
await onSubmit(searchParams, description);
|
|
37
|
+
} else {
|
|
38
|
+
showNotification({
|
|
39
|
+
title: t("error", "Error!"),
|
|
40
|
+
kind: "error",
|
|
41
|
+
critical: true,
|
|
42
|
+
description: t("invalidComposition", "Composition is not valid"),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
setIsLoading(false);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
showNotification({
|
|
49
|
+
title: t("error", "Error!"),
|
|
50
|
+
kind: "error",
|
|
51
|
+
critical: true,
|
|
52
|
+
description: t("invalidComposition", "Composition is not valid"),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<TextInput
|
|
60
|
+
data-modal-primary-focus
|
|
61
|
+
required
|
|
62
|
+
labelText={t("composition", "Composition")}
|
|
63
|
+
data-testid="composition-query"
|
|
64
|
+
id="composition-query"
|
|
65
|
+
onChange={(e) => handleCompositionQuery(e.target.value)}
|
|
66
|
+
value={compositionQuery}
|
|
67
|
+
/>
|
|
68
|
+
<br />
|
|
69
|
+
<TextInput
|
|
70
|
+
data-modal-primary-focus
|
|
71
|
+
required
|
|
72
|
+
labelText={t("description", "Description")}
|
|
73
|
+
data-testid="composition-description"
|
|
74
|
+
id="composition-description"
|
|
75
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
76
|
+
value={description}
|
|
77
|
+
/>
|
|
78
|
+
<br />
|
|
79
|
+
<p className={styles.text}>
|
|
80
|
+
{t(
|
|
81
|
+
"compositionExplanationOne",
|
|
82
|
+
"A composition query combines together the results of multiple cohorts using the logical operators: AND, OR and NOT."
|
|
83
|
+
)}
|
|
84
|
+
</p>
|
|
85
|
+
<br />
|
|
86
|
+
<p className={styles.text}>
|
|
87
|
+
{t(
|
|
88
|
+
"compositionExplanationTwo",
|
|
89
|
+
"To use this query you need to already have query results in your search history. Those existing query results can then be combined to yield the results of the composition query."
|
|
90
|
+
)}
|
|
91
|
+
</p>
|
|
92
|
+
<br />
|
|
93
|
+
<p className={styles.text}>
|
|
94
|
+
{t(
|
|
95
|
+
"compositionExplanationThree",
|
|
96
|
+
"Example: if the search history #1 is a cohort of patients who are males, and if the search history #2 is a cohort of patients with ages between 23 and 35 years; then '1 AND 2' will result in a cohort of patients who are males with ages between 23 and 35 years."
|
|
97
|
+
)}
|
|
98
|
+
</p>
|
|
99
|
+
<br />
|
|
100
|
+
<SearchButtonSet
|
|
101
|
+
onHandleReset={handleResetInputs}
|
|
102
|
+
onHandleSubmit={submit}
|
|
103
|
+
isLoading={isLoading}
|
|
104
|
+
/>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default Composition;
|