@epcare/esm-report-builder 0.1.1-pre.13
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/.eslintignore +5 -0
- package/.eslintrc.js +19 -0
- package/.prettierrc +8 -0
- package/LICENSE +401 -0
- package/README.md +317 -0
- package/dist/184.js +2 -0
- package/dist/184.js.LICENSE.txt +14 -0
- package/dist/184.js.map +1 -0
- package/dist/211.js +1 -0
- package/dist/211.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/311.js +1 -0
- package/dist/311.js.map +1 -0
- package/dist/44.js +2 -0
- package/dist/44.js.LICENSE.txt +30 -0
- package/dist/44.js.map +1 -0
- package/dist/478.js +2 -0
- package/dist/478.js.LICENSE.txt +9 -0
- package/dist/478.js.map +1 -0
- package/dist/535.js +1 -0
- package/dist/535.js.map +1 -0
- package/dist/540.js +2 -0
- package/dist/540.js.LICENSE.txt +9 -0
- package/dist/540.js.map +1 -0
- package/dist/710.js +1 -0
- package/dist/710.js.map +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/872.js +1 -0
- package/dist/872.js.map +1 -0
- package/dist/917.js +1 -0
- package/dist/917.js.map +1 -0
- package/dist/961.js +2 -0
- package/dist/961.js.LICENSE.txt +19 -0
- package/dist/961.js.map +1 -0
- package/dist/972.js +2 -0
- package/dist/972.js.LICENSE.txt +21 -0
- package/dist/972.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-report-builder.js +1 -0
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +482 -0
- package/dist/openmrs-esm-report-builder.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/core/env.ts +5 -0
- package/e2e/core/global-setup.ts +34 -0
- package/e2e/core/test.ts +21 -0
- package/e2e/pages/app-shell.page.ts +34 -0
- package/e2e/pages/etl-sources.page.ts +35 -0
- package/e2e/pages/report-sections.page.ts +20 -0
- package/e2e/pages/section-preview.modal.ts +29 -0
- package/e2e/specs/etl-sources.spec.ts +51 -0
- package/e2e/specs/navigation.spec.ts +13 -0
- package/e2e/specs/section-preview-validation.spec.ts +38 -0
- package/e2e/specs/section-preview.spec.ts +94 -0
- package/e2e/tsconfig.json +11 -0
- package/example.env +5 -0
- package/jest.config.js +28 -0
- package/package.json +102 -0
- package/playwright.config.ts +28 -0
- package/src/admin-card-link.extension.module.scss +55 -0
- package/src/admin-card-link.extension.tsx +24 -0
- package/src/components/admin/admin-page.component.tsx +31 -0
- package/src/components/admin/age-categories-page.component.tsx +109 -0
- package/src/components/admin/age-groups-page.component.tsx +146 -0
- package/src/components/admin/etl-sources-page.component.tsx +344 -0
- package/src/components/admin/report-categories-page.component.tsx +256 -0
- package/src/components/admin/report-library-page.component.tsx +540 -0
- package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
- package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
- package/src/components/ai-support/ai.service.ts +21 -0
- package/src/components/ai-support/ai.types.ts +12 -0
- package/src/components/app-shell/report-builder-shell.component.tsx +120 -0
- package/src/components/app-shell/report-builder-shell.scss +15 -0
- package/src/components/data-themes/data-theme-form.component.tsx +59 -0
- package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
- package/src/components/data-themes/data-themes-page.component.tsx +132 -0
- package/src/components/data-themes/data-themes-table.component.tsx +91 -0
- package/src/components/data-themes/icon-registry.ts +35 -0
- package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
- package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
- package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
- package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
- package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
- package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
- package/src/components/indicators/README.md +24 -0
- package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
- package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
- package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
- package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
- package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
- package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
- package/src/components/indicators/create-base-indicator-modal.component.tsx +501 -0
- package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +297 -0
- package/src/components/indicators/create-final-indicator-modal.component.tsx +260 -0
- package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
- package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
- package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
- package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
- package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
- package/src/components/indicators/indicators-page.component.tsx +481 -0
- package/src/components/indicators/indicators-page.scss +121 -0
- package/src/components/indicators/indicators-table.component.tsx +166 -0
- package/src/components/indicators/preview-table.component.tsx +31 -0
- package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
- package/src/components/indicators/sections/composite-indicator-picker.section.tsx +161 -0
- package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
- package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
- package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
- package/src/components/indicators/sections/final-indicator-picker.section.tsx +92 -0
- package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
- package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
- package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
- package/src/components/indicators/sections/indicator-conditions.section.tsx +194 -0
- package/src/components/indicators/sections/indicator-sql-preview.section.tsx +26 -0
- package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
- package/src/components/indicators/sql-preview.component.tsx +19 -0
- package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
- package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
- package/src/components/indicators/types/composite-indicator.types.ts +21 -0
- package/src/components/indicators/types/condition-ui.types.ts +11 -0
- package/src/components/indicators/types/data-theme-config.types.ts +88 -0
- package/src/components/indicators/types/indicator-types.ts +56 -0
- package/src/components/indicators/types/sql-builders.ts +43 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.ts +206 -0
- package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
- package/src/components/indicators/utils/final-indicator-sql.utils.ts +124 -0
- package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
- package/src/components/indicators/utils/indicator-sql.utils.ts +185 -0
- package/src/components/landing/landing-action-item.component.tsx +50 -0
- package/src/components/landing/landing-action-list.component.tsx +12 -0
- package/src/components/landing/landing-activity-list.component.tsx +47 -0
- package/src/components/landing/landing-footer-grid.component.tsx +45 -0
- package/src/components/landing/landing-hero.component.tsx +18 -0
- package/src/components/landing/landing-section-card.component.tsx +33 -0
- package/src/components/landing/report-builder-landing-page.component.tsx +103 -0
- package/src/components/landing/report-builder-landing-page.scss +131 -0
- package/src/components/report/dashboard/reports-table.component.tsx +145 -0
- package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
- package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
- package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
- package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
- package/src/components/report/definition/report-definition.types.ts +66 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
- package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
- package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
- package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
- package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
- package/src/components/report/design/panels/template-structure-panel.scss +81 -0
- package/src/components/report/design/report-design-editor.component.tsx +894 -0
- package/src/components/report/design/report-design.types.ts +43 -0
- package/src/components/report/design/report-design.utils.ts +86 -0
- package/src/components/report/panels/report-detail.component.tsx +71 -0
- package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
- package/src/components/report/report-dashboard.page.component.tsx +75 -0
- package/src/components/report/report-editor.page.component.tsx +466 -0
- package/src/components/report/report.routes.tsx +30 -0
- package/src/components/report-sections/create-report-section-modal.component.tsx +278 -0
- package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
- package/src/components/report-sections/hooks/useSectionEditorState.ts +306 -0
- package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
- package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
- package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +143 -0
- package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
- package/src/components/report-sections/report-section-preview-modal.component.tsx +418 -0
- package/src/components/report-sections/report-sections-page.component.tsx +313 -0
- package/src/components/report-sections/section-types.ts +55 -0
- package/src/components/report-sections/section-utils.ts +46 -0
- package/src/components/report-sections/sections-page.scss +79 -0
- package/src/components/run-reports/run-reports-page.component.tsx +23 -0
- package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
- package/src/components/shared/header/header.component.tsx +85 -0
- package/src/components/shared/header/header.scss +150 -0
- package/src/components/shared/header/illustration.component.tsx +34 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
- package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
- package/src/config-schema.ts +23 -0
- package/src/globals.d.ts +14 -0
- package/src/hooks/theme/useDataThemes.ts +28 -0
- package/src/hooks/theme/useMambaTableMeta.ts +34 -0
- package/src/hooks/theme/useMambaTables.ts +63 -0
- package/src/index.ts +23 -0
- package/src/resources/agegroup/agegroups.api.ts +58 -0
- package/src/resources/concepts/concept-mapper.ts +21 -0
- package/src/resources/concepts/concept-types.ts +31 -0
- package/src/resources/concepts/concepts.resource.ts +44 -0
- package/src/resources/concepts/useConceptSearch.ts +41 -0
- package/src/resources/etl-source/etl-source.api.ts +65 -0
- package/src/resources/indicator/indicators.api.ts +125 -0
- package/src/resources/openmrs-api.ts +24 -0
- package/src/resources/preview/sql-preview.api.ts +54 -0
- package/src/resources/report/reports.api.ts +117 -0
- package/src/resources/report-category/report-category.api.ts +41 -0
- package/src/resources/report-library/report-library.api.ts +63 -0
- package/src/resources/report-section/report-sections.api.ts +73 -0
- package/src/resources/report-section/section-preview.api.ts +21 -0
- package/src/resources/theme/data-theme.api.ts +88 -0
- package/src/resources/theme/mamba-schema.api.ts +31 -0
- package/src/resources/theme/mamba-table-meta.api.ts +45 -0
- package/src/root.component.tsx +20 -0
- package/src/routes/report-builder.routes.tsx +66 -0
- package/src/routes/report-builder.scss +56 -0
- package/src/routes.json +21 -0
- package/src/sample-template.ts +121 -0
- package/src/template-utils.ts +110 -0
- package/src/types/carbon-react-inert.d.ts +13 -0
- package/src/types/condition-operators.ts +49 -0
- package/src/types/theme/data-theme.types.ts +88 -0
- package/src/utils/html-entities.utils.ts +23 -0
- package/tools/setup-tests.ts +1 -0
- package/translations/en.json +4 -0
- package/tsconfig.json +43 -0
- package/webpack.config.js +14 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextInput } from '@carbon/react';
|
|
3
|
+
|
|
4
|
+
import QueryResultsPreview from '../../shared/preview/query-results-preview.component';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
sql: string;
|
|
8
|
+
maxRows?: number;
|
|
9
|
+
|
|
10
|
+
startDate: string;
|
|
11
|
+
endDate: string;
|
|
12
|
+
onChangeStartDate: (v: string) => void;
|
|
13
|
+
onChangeEndDate: (v: string) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function FinalIndicatorResultsPreviewSection({
|
|
17
|
+
sql,
|
|
18
|
+
maxRows = 200,
|
|
19
|
+
startDate,
|
|
20
|
+
endDate,
|
|
21
|
+
onChangeStartDate,
|
|
22
|
+
onChangeEndDate,
|
|
23
|
+
}: Props) {
|
|
24
|
+
const canRun = Boolean(sql?.trim()) && Boolean(startDate) && Boolean(endDate);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
|
28
|
+
<div
|
|
29
|
+
style={{
|
|
30
|
+
display: 'grid',
|
|
31
|
+
gridTemplateColumns: '320px 1fr',
|
|
32
|
+
gap: '1rem',
|
|
33
|
+
alignItems: 'start',
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{/* Left column: dates */}
|
|
37
|
+
<div style={{ display: 'grid', gap: '1rem' }}>
|
|
38
|
+
<TextInput
|
|
39
|
+
id="final-preview-start"
|
|
40
|
+
labelText="Start date"
|
|
41
|
+
type="date"
|
|
42
|
+
value={startDate}
|
|
43
|
+
onChange={(e) => onChangeStartDate((e.target as HTMLInputElement).value)}
|
|
44
|
+
/>
|
|
45
|
+
<TextInput
|
|
46
|
+
id="final-preview-end"
|
|
47
|
+
labelText="End date"
|
|
48
|
+
type="date"
|
|
49
|
+
value={endDate}
|
|
50
|
+
onChange={(e) => onChangeEndDate((e.target as HTMLInputElement).value)}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Right column: query preview */}
|
|
55
|
+
<QueryResultsPreview
|
|
56
|
+
title="Query results preview"
|
|
57
|
+
sql={sql}
|
|
58
|
+
params={{ startDate, endDate }}
|
|
59
|
+
maxRows={maxRows}
|
|
60
|
+
canRun={canRun}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Stack, TextInput, TextArea } from '@carbon/react';
|
|
3
|
+
|
|
4
|
+
type Value = {
|
|
5
|
+
name: string;
|
|
6
|
+
code: string;
|
|
7
|
+
description: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
value: Value;
|
|
12
|
+
onChange: (next: Value) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function IndicatorBasicsSection({ value, onChange }: Props) {
|
|
16
|
+
return (
|
|
17
|
+
<Stack gap={6}>
|
|
18
|
+
<TextInput
|
|
19
|
+
id="indicator-code"
|
|
20
|
+
labelText="Code (optional)"
|
|
21
|
+
value={value.code}
|
|
22
|
+
onChange={(e) => onChange({ ...value, code: (e.target as HTMLInputElement).value })}
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
<TextInput
|
|
26
|
+
id="indicator-name"
|
|
27
|
+
labelText="Name"
|
|
28
|
+
value={value.name}
|
|
29
|
+
onChange={(e) => onChange({ ...value, name: (e.target as HTMLInputElement).value })}
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<TextArea
|
|
33
|
+
id="indicator-desc"
|
|
34
|
+
labelText="Description (optional)"
|
|
35
|
+
value={value.description}
|
|
36
|
+
onChange={(e) => onChange({ ...value, description: (e.target as HTMLTextAreaElement).value })}
|
|
37
|
+
/>
|
|
38
|
+
</Stack>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Stack, TextInput } from '@carbon/react';
|
|
3
|
+
|
|
4
|
+
import type { ThemeCondition } from '../types/data-theme-config.types';
|
|
5
|
+
import type { IndicatorCondition } from '../types/indicator-types';
|
|
6
|
+
|
|
7
|
+
import ConceptSearchMultiSelect, { type SelectedConcept } from '../handler/concept-search-multiselect.component';
|
|
8
|
+
import QuestionAnswerConceptSearch from '../handler/question-answer-concept-search.component';
|
|
9
|
+
|
|
10
|
+
import type { QAUiState } from '../types/condition-ui.types';
|
|
11
|
+
|
|
12
|
+
type QAValue = { questions: Array<string | number>; answers: Array<string | number> };
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
conditions: ThemeCondition[];
|
|
16
|
+
|
|
17
|
+
picked: IndicatorCondition[];
|
|
18
|
+
/**
|
|
19
|
+
* We accept a React state setter signature so callers can pass setState directly
|
|
20
|
+
* and we can safely use functional updates.
|
|
21
|
+
*/
|
|
22
|
+
onPickedChange: React.Dispatch<React.SetStateAction<IndicatorCondition[]>>;
|
|
23
|
+
|
|
24
|
+
conceptUi: Record<string, SelectedConcept[]>;
|
|
25
|
+
onConceptUiChange: (next: Record<string, SelectedConcept[]>) => void;
|
|
26
|
+
|
|
27
|
+
qaUi: Record<string, QAUiState>;
|
|
28
|
+
onQaUiChange: (next: Record<string, QAUiState>) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function normalizeNumericIds(xs: Array<any>) {
|
|
32
|
+
return xs.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeStrings(xs: Array<any>) {
|
|
36
|
+
return xs.map((x) => String(x)).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function upsert(list: IndicatorCondition[], next: IndicatorCondition) {
|
|
40
|
+
const idx = (list ?? []).findIndex((p) => p.key === next.key);
|
|
41
|
+
if (idx === -1) return [...(list ?? []), next];
|
|
42
|
+
const copy = [...(list ?? [])];
|
|
43
|
+
copy[idx] = { ...copy[idx], ...next };
|
|
44
|
+
return copy;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function IndicatorConditionsSection({
|
|
48
|
+
conditions,
|
|
49
|
+
picked,
|
|
50
|
+
onPickedChange,
|
|
51
|
+
conceptUi,
|
|
52
|
+
onConceptUiChange,
|
|
53
|
+
qaUi,
|
|
54
|
+
onQaUiChange,
|
|
55
|
+
}: Props) {
|
|
56
|
+
const upsertPicked = React.useCallback(
|
|
57
|
+
(tc: ThemeCondition, patch: Partial<IndicatorCondition>) => {
|
|
58
|
+
const existing =
|
|
59
|
+
(picked ?? []).find((p) => p.key === tc.key) ??
|
|
60
|
+
({
|
|
61
|
+
key: tc.key,
|
|
62
|
+
operator: tc.operator,
|
|
63
|
+
valueType: tc.valueType,
|
|
64
|
+
value:
|
|
65
|
+
tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH'
|
|
66
|
+
? ({ questions: [], answers: [] } as any)
|
|
67
|
+
: tc.operator === 'IN' || tc.operator === 'NOT_IN'
|
|
68
|
+
? []
|
|
69
|
+
: '',
|
|
70
|
+
} as IndicatorCondition);
|
|
71
|
+
|
|
72
|
+
onPickedChange(upsert(picked ?? [], { ...existing, ...patch, key: tc.key }));
|
|
73
|
+
},
|
|
74
|
+
[onPickedChange, picked],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const mergeQaUi = React.useCallback(
|
|
78
|
+
(key: string, next: Partial<QAUiState>) => {
|
|
79
|
+
onQaUiChange({
|
|
80
|
+
...(qaUi ?? {}),
|
|
81
|
+
[key]: {
|
|
82
|
+
...(qaUi?.[key] ?? { questions: [], answers: [] }),
|
|
83
|
+
...next,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
[onQaUiChange, qaUi],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const mergeConceptUi = React.useCallback(
|
|
91
|
+
(key: string, nextSelected: SelectedConcept[]) => {
|
|
92
|
+
onConceptUiChange({
|
|
93
|
+
...(conceptUi ?? {}),
|
|
94
|
+
[key]: nextSelected,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
[onConceptUiChange, conceptUi],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const renderCondition = (tc: ThemeCondition) => {
|
|
101
|
+
const current = (picked ?? []).find((x) => x.key === tc.key);
|
|
102
|
+
|
|
103
|
+
// QUESTION + ANSWERS
|
|
104
|
+
if (tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH') {
|
|
105
|
+
const ui = qaUi?.[tc.key] ?? { questions: [], answers: [] };
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div key={tc.key}>
|
|
109
|
+
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{tc.label || tc.key}</div>
|
|
110
|
+
|
|
111
|
+
<QuestionAnswerConceptSearch
|
|
112
|
+
id={`cond-${tc.key}`}
|
|
113
|
+
labelText={tc.label || 'Select question(s) & answers'}
|
|
114
|
+
value={{ questions: ui.questions, answers: ui.answers }}
|
|
115
|
+
onChange={(next) => {
|
|
116
|
+
// UI
|
|
117
|
+
mergeQaUi(tc.key, { questions: next.questions, answers: next.answers, error: undefined });
|
|
118
|
+
|
|
119
|
+
// Payload
|
|
120
|
+
const qVals =
|
|
121
|
+
tc.valueType === 'conceptUuid'
|
|
122
|
+
? normalizeStrings((next.questions ?? []).map((q) => q.uuid))
|
|
123
|
+
: normalizeNumericIds((next.questions ?? []).map((q) => q.id));
|
|
124
|
+
|
|
125
|
+
const aVals =
|
|
126
|
+
tc.valueType === 'conceptUuid'
|
|
127
|
+
? normalizeStrings((next.answers ?? []).map((a) => a.uuid))
|
|
128
|
+
: normalizeNumericIds((next.answers ?? []).map((a) => a.id));
|
|
129
|
+
|
|
130
|
+
upsertPicked(tc, { value: { questions: qVals, answers: aVals } as QAValue as any });
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
{qaUi?.[tc.key]?.error ? (
|
|
135
|
+
<div style={{ marginTop: '0.5rem', color: 'var(--cds-text-error, #da1e28)', fontSize: '0.875rem' }}>
|
|
136
|
+
{qaUi[tc.key].error}
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CONCEPT_SEARCH
|
|
144
|
+
if (tc.handler === 'CONCEPT_SEARCH') {
|
|
145
|
+
const uiSelected = conceptUi?.[tc.key] ?? [];
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div key={tc.key}>
|
|
149
|
+
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{tc.label || tc.key}</div>
|
|
150
|
+
|
|
151
|
+
<ConceptSearchMultiSelect
|
|
152
|
+
id={`cond-${tc.key}`}
|
|
153
|
+
labelText={tc.label || 'Select concepts'}
|
|
154
|
+
value={uiSelected}
|
|
155
|
+
onChange={(nextSelected) => {
|
|
156
|
+
mergeConceptUi(tc.key, nextSelected);
|
|
157
|
+
|
|
158
|
+
if (tc.valueType === 'conceptUuid') {
|
|
159
|
+
upsertPicked(tc, { value: normalizeStrings(nextSelected.map((c) => c.uuid)) as any });
|
|
160
|
+
} else {
|
|
161
|
+
upsertPicked(tc, { value: normalizeNumericIds(nextSelected.map((c) => c.id)) as any });
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// default: text input
|
|
170
|
+
return (
|
|
171
|
+
<div key={tc.key}>
|
|
172
|
+
<TextInput
|
|
173
|
+
id={`cond-${tc.key}`}
|
|
174
|
+
labelText={tc.label || tc.key}
|
|
175
|
+
value={typeof current?.value === 'string' ? current.value : ''}
|
|
176
|
+
onChange={(e) =>
|
|
177
|
+
upsertPicked(tc, {
|
|
178
|
+
value: (e.target as HTMLInputElement).value as any,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (!conditions?.length) return <div style={{ opacity: 0.8 }}>No conditions configured for this theme.</div>;
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div>
|
|
190
|
+
<div style={{ fontWeight: 600, marginBottom: '0.75rem' }}>Conditions</div>
|
|
191
|
+
<Stack gap={5}>{conditions.map(renderCondition)}</Stack>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
sql: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function IndicatorSqlPreviewSection({ sql }: Props) {
|
|
8
|
+
if (!sql) return <div style={{ opacity: 0.8 }}>SQL preview will appear once a theme is selected.</div>;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<div style={{ fontWeight: 600, marginBottom: '0.5rem' }}>SQL Preview</div>
|
|
13
|
+
<pre
|
|
14
|
+
style={{
|
|
15
|
+
fontSize: '0.875rem',
|
|
16
|
+
background: 'var(--cds-layer-01)',
|
|
17
|
+
padding: '0.75rem',
|
|
18
|
+
borderRadius: 6,
|
|
19
|
+
overflowX: 'auto',
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
{sql}
|
|
23
|
+
</pre>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { InlineLoading, Select, SelectItem, Stack } from '@carbon/react';
|
|
3
|
+
import type { DataThemeDto } from '../../../resources/theme/data-theme.api';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
themes: DataThemeDto[];
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error?: string | null;
|
|
9
|
+
|
|
10
|
+
themeUuid: string;
|
|
11
|
+
onThemeUuidChange: (uuid: string) => void;
|
|
12
|
+
|
|
13
|
+
themeConfigError?: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function IndicatorThemeSection({
|
|
17
|
+
themes,
|
|
18
|
+
loading,
|
|
19
|
+
error,
|
|
20
|
+
themeUuid,
|
|
21
|
+
onThemeUuidChange,
|
|
22
|
+
themeConfigError,
|
|
23
|
+
}: Props) {
|
|
24
|
+
return (
|
|
25
|
+
<Stack gap={4}>
|
|
26
|
+
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>Data Theme</div>
|
|
27
|
+
|
|
28
|
+
{loading ? <InlineLoading description="Loading themes…" /> : null}
|
|
29
|
+
{!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
|
|
30
|
+
|
|
31
|
+
<Select
|
|
32
|
+
id="indicator-theme"
|
|
33
|
+
labelText="Select a theme"
|
|
34
|
+
value={themeUuid}
|
|
35
|
+
onChange={(e) => onThemeUuidChange((e.target as HTMLSelectElement).value)}
|
|
36
|
+
>
|
|
37
|
+
<SelectItem value="" text="Select…" />
|
|
38
|
+
{(themes ?? []).map((t) => (
|
|
39
|
+
<SelectItem key={t.uuid} value={t.uuid} text={`${t.name}${t.code ? ` (${t.code})` : ''}`} />
|
|
40
|
+
))}
|
|
41
|
+
</Select>
|
|
42
|
+
|
|
43
|
+
{themeConfigError ? (
|
|
44
|
+
<div style={{ color: 'var(--cds-text-error, #da1e28)', marginTop: '0.25rem' }}>{themeConfigError}</div>
|
|
45
|
+
) : null}
|
|
46
|
+
</Stack>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextArea } from '@carbon/react';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const SqlPreview: React.FC<Props> = ({ value }) => {
|
|
9
|
+
return (
|
|
10
|
+
<TextArea
|
|
11
|
+
id="sql-preview"
|
|
12
|
+
labelText="Generated SQL (base indicator)"
|
|
13
|
+
value={value}
|
|
14
|
+
readOnly
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default SqlPreview;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type IndicatorUnit = 'Patients' | 'Encounters';
|
|
2
|
+
|
|
3
|
+
export type CreateBaseIndicatorPayload = {
|
|
4
|
+
code: string;
|
|
5
|
+
theme: string; // e.g. DIAGNOSIS, OBSERVATIONS, MEDICATIONS, LAB
|
|
6
|
+
indicatorName: string;
|
|
7
|
+
description: string;
|
|
8
|
+
|
|
9
|
+
// Concept selection (multi)
|
|
10
|
+
conditionConceptUuids: string[];
|
|
11
|
+
conditionConceptLabels: string[];
|
|
12
|
+
|
|
13
|
+
pregnancyStatus: string;
|
|
14
|
+
|
|
15
|
+
unit: IndicatorUnit;
|
|
16
|
+
countBy: IndicatorUnit;
|
|
17
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const THEME_OPTIONS = ['DIAGNOSIS', 'OBSERVATIONS', 'MEDICATIONS', 'LAB'] as const;
|
|
2
|
+
|
|
3
|
+
export const THEME_LABELS: Record<(typeof THEME_OPTIONS)[number], string> = {
|
|
4
|
+
DIAGNOSIS: 'Diagnosis Data',
|
|
5
|
+
OBSERVATIONS: 'Observations',
|
|
6
|
+
MEDICATIONS: 'Medications',
|
|
7
|
+
LAB: 'Laboratory',
|
|
8
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CompositeOperator = 'AND' | 'OR' | 'A_AND_NOT_B';
|
|
2
|
+
|
|
3
|
+
export type BaseIndicatorOption = {
|
|
4
|
+
id: string; // uuid
|
|
5
|
+
code: string;
|
|
6
|
+
name: string;
|
|
7
|
+
unit?: 'Patients' | 'Encounters';
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CreateCompositeBaseIndicatorPayload = {
|
|
11
|
+
code: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
|
|
15
|
+
indicatorAId: string; // uuid
|
|
16
|
+
indicatorBId: string; // uuid
|
|
17
|
+
operator: CompositeOperator;
|
|
18
|
+
|
|
19
|
+
unit?: 'Patients' | 'Encounters';
|
|
20
|
+
sqlTemplate?: string; // composite COUNT SQL
|
|
21
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SelectedConcept } from '../handler/concept-search-multiselect.component';
|
|
2
|
+
|
|
3
|
+
export type QAUiState = {
|
|
4
|
+
/** Selected question concepts (multi-select) */
|
|
5
|
+
questions: SelectedConcept[];
|
|
6
|
+
answers: SelectedConcept[];
|
|
7
|
+
error?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ConceptUiMap = Record<string, SelectedConcept[]>;
|
|
11
|
+
export type QaUiMap = Record<string, QAUiState>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/report-builder/types/theme/data-theme.types.ts
|
|
2
|
+
|
|
3
|
+
export type MainDataDomain =
|
|
4
|
+
| 'OBSERVATIONS'
|
|
5
|
+
| 'TEST_ORDERS'
|
|
6
|
+
| 'MEDICATION_ORDERS'
|
|
7
|
+
| 'APPOINTMENTS'
|
|
8
|
+
| 'MEDICATION_DISPENSE'
|
|
9
|
+
| 'DIAGNOSIS';
|
|
10
|
+
|
|
11
|
+
export type FieldType = 'string' | 'number' | 'date' | 'datetime' | 'boolean' | 'coded' | 'json';
|
|
12
|
+
|
|
13
|
+
export type ThemeField = {
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
expr: string;
|
|
17
|
+
type: FieldType;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ConditionHandler =
|
|
21
|
+
| 'CONCEPT_SEARCH'
|
|
22
|
+
| 'QUESTION_ANSWER_CONCEPT_SEARCH'
|
|
23
|
+
| 'TEXT'
|
|
24
|
+
| 'NUMBER'
|
|
25
|
+
| 'DATE_RANGE'
|
|
26
|
+
| 'BOOLEAN'
|
|
27
|
+
| 'LOCATION_PICKER'
|
|
28
|
+
| 'CODED_LIST';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Theme-side operator tokens (legacy + UI-friendly).
|
|
32
|
+
* NOTE: SQL generation will normalize these to real SQL operators.
|
|
33
|
+
*/
|
|
34
|
+
export type ConditionOperator = 'EQUALS' | 'IN' | 'NOT_IN' | 'LIKE' | 'BETWEEN' | 'GTE' | 'LTE';
|
|
35
|
+
|
|
36
|
+
export type ConditionValueType =
|
|
37
|
+
| 'conceptUuid'
|
|
38
|
+
| 'conceptId'
|
|
39
|
+
| 'string'
|
|
40
|
+
| 'number'
|
|
41
|
+
| 'date'
|
|
42
|
+
| 'datetime'
|
|
43
|
+
| 'boolean';
|
|
44
|
+
|
|
45
|
+
export type ThemeCondition = {
|
|
46
|
+
key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
handler: ConditionHandler;
|
|
49
|
+
column: string;
|
|
50
|
+
operator: ConditionOperator;
|
|
51
|
+
valueType: ConditionValueType;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type DataThemeConfig = {
|
|
55
|
+
sourceTable: string;
|
|
56
|
+
patientIdColumn: string;
|
|
57
|
+
dateColumn: string;
|
|
58
|
+
locationColumn?: string;
|
|
59
|
+
|
|
60
|
+
joins?: Array<{ alias: string; joinSql: string }>;
|
|
61
|
+
defaultFilters?: string[];
|
|
62
|
+
|
|
63
|
+
fields: ThemeField[];
|
|
64
|
+
|
|
65
|
+
conditions?: ThemeCondition[];
|
|
66
|
+
|
|
67
|
+
conditionColumns?: Record<string, string>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type DataTheme = {
|
|
71
|
+
uuid?: string;
|
|
72
|
+
name: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
code: string;
|
|
75
|
+
domain: MainDataDomain;
|
|
76
|
+
configJson: string;
|
|
77
|
+
metaJson?: string;
|
|
78
|
+
retired?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type DataThemeRow = {
|
|
82
|
+
uuid: string;
|
|
83
|
+
name: string;
|
|
84
|
+
code: string;
|
|
85
|
+
domain: MainDataDomain;
|
|
86
|
+
description?: string;
|
|
87
|
+
retired?: boolean;
|
|
88
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DataThemeConfig, ThemeCondition } from './data-theme-config.types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the indicator builder saves/uses.
|
|
5
|
+
* Keep it flexible because you already have a backend model.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type IndicatorKind = 'BASE' | 'FINAL';
|
|
9
|
+
|
|
10
|
+
export type IndicatorConditionValue =
|
|
11
|
+
| string
|
|
12
|
+
| number
|
|
13
|
+
| Array<string | number>
|
|
14
|
+
| { start?: string; end?: string }
|
|
15
|
+
| null;
|
|
16
|
+
|
|
17
|
+
export type IndicatorCondition = {
|
|
18
|
+
key: string; // matches ThemeCondition.key
|
|
19
|
+
operator?: ThemeCondition['operator'];
|
|
20
|
+
valueType?: ThemeCondition['valueType'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Actual chosen value(s).
|
|
24
|
+
* - concept search returns ids/uuids depending on your handler config
|
|
25
|
+
*/
|
|
26
|
+
value: IndicatorConditionValue;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type BaseIndicatorDraft = {
|
|
30
|
+
uuid?: string;
|
|
31
|
+
name: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
code?: string;
|
|
34
|
+
|
|
35
|
+
kind: IndicatorKind;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The selected data theme
|
|
39
|
+
*/
|
|
40
|
+
themeUuid: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optional snapshot of theme config at save time (good for debugging / auditing)
|
|
44
|
+
*/
|
|
45
|
+
themeConfig?: DataThemeConfig;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* User-picked conditions (values), keys should match theme config conditions
|
|
49
|
+
*/
|
|
50
|
+
conditions?: IndicatorCondition[];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional SQL preview used by UI
|
|
54
|
+
*/
|
|
55
|
+
sqlPreview?: string;
|
|
56
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
import type { DiagnosisBaseConfig } from './indicator-types';
|
|
3
|
+
|
|
4
|
+
function normalizeCsvToSqlInList(csv: string) {
|
|
5
|
+
const cleaned = (csv ?? '')
|
|
6
|
+
.split(',')
|
|
7
|
+
.map((x) => x.trim())
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.map((x) => String(Number(x)))
|
|
10
|
+
.filter((x) => x !== 'NaN');
|
|
11
|
+
|
|
12
|
+
return cleaned.length ? cleaned.join(',') : '1';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function diagnosisClause(ids: number[]) {
|
|
16
|
+
if (!ids || ids.length === 0) {
|
|
17
|
+
return `-- AND a.diagnosis_coded = <select a concept>`;
|
|
18
|
+
}
|
|
19
|
+
if (ids.length === 1) {
|
|
20
|
+
return `AND a.diagnosis_coded = ${ids[0]}`;
|
|
21
|
+
}
|
|
22
|
+
return `AND a.diagnosis_coded IN (${ids.join(',')})`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildDiagnosisBaseSql(cfg: DiagnosisBaseConfig) {
|
|
26
|
+
const ranks = normalizeCsvToSqlInList(cfg.dxRanksCsv);
|
|
27
|
+
|
|
28
|
+
return `
|
|
29
|
+
SELECT
|
|
30
|
+
COUNT(*) AS total
|
|
31
|
+
FROM mamba_fact_encounter_diagnosis a
|
|
32
|
+
JOIN mamba_fact_patients_latest_patient_demographics mdp
|
|
33
|
+
ON mdp.patient_id = a.patient_id
|
|
34
|
+
WHERE a.date_created >= ':startDate'
|
|
35
|
+
AND a.date_created < ':endDate'
|
|
36
|
+
${diagnosisClause(cfg.conceptIds)}
|
|
37
|
+
AND a.certainty = '${cfg.certainty}'
|
|
38
|
+
AND a.dx_rank IN (${ranks})
|
|
39
|
+
${cfg.onlyNotVoided ? 'AND a.voided = 0' : ''}
|
|
40
|
+
${cfg.requireNonNullBirthdate ? 'AND mdp.birthdate IS NOT NULL' : ''}
|
|
41
|
+
${cfg.requireNonNullGender ? 'AND mdp.gender IS NOT NULL' : ''}
|
|
42
|
+
;`.trim();
|
|
43
|
+
}*/
|