@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,185 @@
|
|
|
1
|
+
import type { DataThemeConfig, ThemeCondition } from '../types/data-theme-config.types';
|
|
2
|
+
import type { IndicatorCondition } from '../types/indicator-types';
|
|
3
|
+
|
|
4
|
+
import { normalizeOperator, isInOperator } from '../../../types/condition-operators';
|
|
5
|
+
|
|
6
|
+
const DEMO_JOIN_TABLE = 'mamba_fact_patients_latest_patient_demographics';
|
|
7
|
+
|
|
8
|
+
function sqlQuote(v: string) {
|
|
9
|
+
return `'${String(v).replace(/'/g, "''")}'`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function qualifyColumn(col: string) {
|
|
13
|
+
const c = String(col ?? '').trim();
|
|
14
|
+
if (!c) return c;
|
|
15
|
+
if (c.includes('.') || c.includes('(') || c.includes(' ') || c.includes('`')) return c;
|
|
16
|
+
return `a.${c}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseQaColumnsFromExpr(expr?: string | null) {
|
|
20
|
+
const raw = String(expr ?? '').trim();
|
|
21
|
+
if (!raw) return { questionColumn: undefined as string | undefined, answerColumn: undefined as string | undefined };
|
|
22
|
+
|
|
23
|
+
const m = raw.match(/QA\s*$begin:math:text$\(\.\*\)$end:math:text$/i);
|
|
24
|
+
if (!m) return { questionColumn: undefined, answerColumn: undefined };
|
|
25
|
+
|
|
26
|
+
const inner = (m[1] ?? '').trim();
|
|
27
|
+
if (!inner) return { questionColumn: undefined, answerColumn: undefined };
|
|
28
|
+
|
|
29
|
+
const parts = inner
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((x) => x.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
|
|
34
|
+
return { questionColumn: parts[0], answerColumn: parts[1] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function looksLikeQaExpression(expr?: string | null) {
|
|
38
|
+
const raw = String(expr ?? '').trim();
|
|
39
|
+
return /^([a-z]\.)?QA\s*\(/i.test(raw);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type QAValue = {
|
|
43
|
+
question?: string | number | null;
|
|
44
|
+
questions?: Array<string | number>;
|
|
45
|
+
answers?: Array<string | number>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function isQAValue(v: any): v is QAValue {
|
|
49
|
+
return v && typeof v === 'object' && ('question' in v || 'questions' in v || 'answers' in v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeArrayValue(v: any) {
|
|
53
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
54
|
+
return arr.map((x) => String(x)).filter((x) => x.trim().length > 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildSqlPreview(themeCfg: DataThemeConfig) {
|
|
58
|
+
const src = themeCfg.sourceTable;
|
|
59
|
+
const pid = themeCfg.patientIdColumn;
|
|
60
|
+
const dateCol = themeCfg.dateColumn;
|
|
61
|
+
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
lines.push(`SELECT`);
|
|
64
|
+
lines.push(` COUNT(*) AS total`);
|
|
65
|
+
lines.push(`FROM ${src} a`);
|
|
66
|
+
lines.push(`JOIN ${DEMO_JOIN_TABLE} mdp`);
|
|
67
|
+
lines.push(` ON mdp.patient_id = a.${pid}`);
|
|
68
|
+
lines.push(`WHERE a.${dateCol} >= ':startDate'`);
|
|
69
|
+
lines.push(` AND a.${dateCol} < ':endDate'`);
|
|
70
|
+
lines.push(` AND mdp.birthdate IS NOT NULL`);
|
|
71
|
+
lines.push(` AND mdp.gender IS NOT NULL`);
|
|
72
|
+
lines.push(`;`);
|
|
73
|
+
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function applyConditionClauses(baseSql: string, themeConditions: ThemeCondition[], picked: IndicatorCondition[]) {
|
|
78
|
+
const sqlLines = baseSql.split('\n');
|
|
79
|
+
|
|
80
|
+
const demoBirthIdx = sqlLines.findIndex((l) => l.includes('mdp.birthdate IS NOT NULL'));
|
|
81
|
+
const insertAt = demoBirthIdx > -1 ? demoBirthIdx : sqlLines.length;
|
|
82
|
+
|
|
83
|
+
const clauses: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const tc of themeConditions ?? []) {
|
|
86
|
+
const pc = picked.find((x) => x.key === tc.key);
|
|
87
|
+
if (!pc) continue;
|
|
88
|
+
|
|
89
|
+
const v: any = pc.value;
|
|
90
|
+
if (v === null || v === undefined) continue;
|
|
91
|
+
|
|
92
|
+
// ✅ Normalize operator tokens from theme/UI
|
|
93
|
+
const op = normalizeOperator((pc.operator as any) ?? (tc.operator as any) ?? 'IN');
|
|
94
|
+
|
|
95
|
+
// QUESTION_ANSWER_CONCEPT_SEARCH
|
|
96
|
+
if (tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH' && isQAValue(v)) {
|
|
97
|
+
const qVals: any[] = Array.isArray(v.questions)
|
|
98
|
+
? v.questions
|
|
99
|
+
: v.question !== null && v.question !== undefined && String(v.question).trim() !== ''
|
|
100
|
+
? [v.question]
|
|
101
|
+
: [];
|
|
102
|
+
|
|
103
|
+
const av = Array.isArray(v.answers) ? v.answers : [];
|
|
104
|
+
|
|
105
|
+
let questionColumn = (tc as any)?.columns?.question ?? (tc as any).questionColumn;
|
|
106
|
+
let answerColumn = (tc as any)?.columns?.answer ?? (tc as any).answerColumn;
|
|
107
|
+
|
|
108
|
+
if ((!questionColumn || !answerColumn) && (tc as any)?.column) {
|
|
109
|
+
const parsed = parseQaColumnsFromExpr((tc as any).column);
|
|
110
|
+
questionColumn = questionColumn ?? parsed.questionColumn;
|
|
111
|
+
answerColumn = answerColumn ?? parsed.answerColumn;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const tcCol = (tc as any)?.column as string | undefined;
|
|
115
|
+
if (!questionColumn && tcCol) questionColumn = tcCol;
|
|
116
|
+
if (!answerColumn && tcCol && !looksLikeQaExpression(tcCol)) answerColumn = tcCol;
|
|
117
|
+
|
|
118
|
+
if (questionColumn && qVals.length) {
|
|
119
|
+
const arr = normalizeArrayValue(qVals);
|
|
120
|
+
if (arr.length) {
|
|
121
|
+
const isNumericList = tc.valueType === 'conceptId' || arr.every((x) => /^[0-9]+$/.test(x));
|
|
122
|
+
const rendered = arr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
|
|
123
|
+
const col = qualifyColumn(questionColumn);
|
|
124
|
+
|
|
125
|
+
// For questions we keep original behavior: single => =, many => IN (...)
|
|
126
|
+
if (arr.length === 1) clauses.push(` AND ${col} = ${rendered}`);
|
|
127
|
+
else clauses.push(` AND ${col} IN (${rendered})`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (answerColumn && Array.isArray(av) && av.length) {
|
|
132
|
+
const arr = normalizeArrayValue(av);
|
|
133
|
+
if (arr.length) {
|
|
134
|
+
const isNumericList = tc.valueType === 'conceptId' || arr.every((x) => /^[0-9]+$/.test(x));
|
|
135
|
+
const rendered = arr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
|
|
136
|
+
const col = qualifyColumn(answerColumn);
|
|
137
|
+
|
|
138
|
+
// ✅ Always bracket IN/NOT IN
|
|
139
|
+
if (op === 'NOT IN') clauses.push(` AND ${col} NOT IN (${rendered})`);
|
|
140
|
+
else clauses.push(` AND ${col} IN (${rendered})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// array values
|
|
148
|
+
if (Array.isArray(v)) {
|
|
149
|
+
const arr = normalizeArrayValue(v);
|
|
150
|
+
if (!arr.length) continue;
|
|
151
|
+
|
|
152
|
+
const isNumericList = tc.valueType === 'conceptId' || arr.every((x) => /^[0-9]+$/.test(x));
|
|
153
|
+
const rendered = arr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
|
|
154
|
+
|
|
155
|
+
const col = qualifyColumn(tc.column);
|
|
156
|
+
|
|
157
|
+
if (op === 'NOT IN') clauses.push(` AND ${col} NOT IN (${rendered})`);
|
|
158
|
+
else if (op === 'IN') clauses.push(` AND ${col} IN (${rendered})`);
|
|
159
|
+
else clauses.push(` AND ${col} ${op} (${rendered})`);
|
|
160
|
+
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// other object not supported
|
|
165
|
+
if (typeof v === 'object') continue;
|
|
166
|
+
|
|
167
|
+
// scalar
|
|
168
|
+
const sval = String(v);
|
|
169
|
+
const isNumeric = tc.valueType === 'conceptId' || /^[0-9]+$/.test(sval);
|
|
170
|
+
const renderedScalar = isNumeric ? sval : sqlQuote(sval);
|
|
171
|
+
|
|
172
|
+
const col = qualifyColumn(tc.column);
|
|
173
|
+
|
|
174
|
+
// ✅ IN must always have brackets, even for scalar
|
|
175
|
+
if (isInOperator(op)) {
|
|
176
|
+
if (op === 'NOT IN') clauses.push(` AND ${col} NOT IN (${renderedScalar})`);
|
|
177
|
+
else clauses.push(` AND ${col} IN (${renderedScalar})`);
|
|
178
|
+
} else {
|
|
179
|
+
clauses.push(` AND ${col} ${op} ${renderedScalar}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
sqlLines.splice(insertAt, 0, ...clauses);
|
|
184
|
+
return sqlLines.join('\n');
|
|
185
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronRight } from '@carbon/icons-react';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
import styles from './report-builder-landing-page.scss';
|
|
6
|
+
|
|
7
|
+
type LandingActionItemBase = {
|
|
8
|
+
icon: React.ReactNode;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type LandingActionItemLink = LandingActionItemBase & {
|
|
13
|
+
to: string;
|
|
14
|
+
onClick?: never;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type LandingActionItemButton = LandingActionItemBase & {
|
|
18
|
+
onClick: () => void;
|
|
19
|
+
to?: never;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LandingActionItemProps = LandingActionItemLink | LandingActionItemButton;
|
|
23
|
+
|
|
24
|
+
const LandingActionItem: React.FC<LandingActionItemProps> = (props) => {
|
|
25
|
+
const content = (
|
|
26
|
+
<>
|
|
27
|
+
<div className={styles.rbActionLeft}>
|
|
28
|
+
{props.icon}
|
|
29
|
+
<span className={styles.rbActionLabel}>{props.label}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<ChevronRight />
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if ('to' in props) {
|
|
36
|
+
return (
|
|
37
|
+
<Link className={styles.rbAction} to={props.to}>
|
|
38
|
+
{content}
|
|
39
|
+
</Link>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<button className={styles.rbAction} type="button" onClick={props.onClick}>
|
|
45
|
+
{content}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default LandingActionItem;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './report-builder-landing-page.scss';
|
|
3
|
+
|
|
4
|
+
type LandingActionListProps = {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const LandingActionList: React.FC<LandingActionListProps> = ({ children }) => {
|
|
9
|
+
return <div className={styles.rbActions}>{children}</div>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default LandingActionList;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronRight, Document, Report } from '@carbon/icons-react';
|
|
3
|
+
import styles from './report-builder-landing-page.scss';
|
|
4
|
+
|
|
5
|
+
type RecentIndicator = { name: string; when: string };
|
|
6
|
+
|
|
7
|
+
type LandingActivityListProps = {
|
|
8
|
+
recentIndicators: RecentIndicator[];
|
|
9
|
+
savedReports: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const cx = (...parts: Array<string | undefined | false>) => parts.filter(Boolean).join(' ');
|
|
13
|
+
|
|
14
|
+
const LandingActivityList: React.FC<LandingActivityListProps> = ({ recentIndicators, savedReports }) => {
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<div className={styles.rbListSectionTitle}>Recent Activity</div>
|
|
18
|
+
|
|
19
|
+
<div className={styles.rbListSubTitle}>Recently Edited Indicators</div>
|
|
20
|
+
{recentIndicators.map((x) => (
|
|
21
|
+
<div key={x.name} className={styles.rbListItem}>
|
|
22
|
+
<div className={styles.rbListLeft}>
|
|
23
|
+
<Document />
|
|
24
|
+
<div>
|
|
25
|
+
<div className={styles.rbListName}>{x.name}</div>
|
|
26
|
+
<div className={styles.rbListMeta}>{x.when}</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<ChevronRight />
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
|
|
33
|
+
<div className={cx(styles.rbListSubTitle, styles.rbMt12)}>Saved Reports</div>
|
|
34
|
+
{savedReports.map((r) => (
|
|
35
|
+
<div key={r} className={styles.rbListItem}>
|
|
36
|
+
<div className={styles.rbListLeft}>
|
|
37
|
+
<Report />
|
|
38
|
+
<div className={styles.rbListName}>{r}</div>
|
|
39
|
+
</div>
|
|
40
|
+
<ChevronRight />
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default LandingActivityList;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Tag } from '@carbon/react';
|
|
3
|
+
import { Play, Time, Save, ChartColumn } from '@carbon/icons-react';
|
|
4
|
+
|
|
5
|
+
import styles from './report-builder-landing-page.scss';
|
|
6
|
+
import LandingSectionCard from './landing-section-card.component';
|
|
7
|
+
import LandingActionList from './landing-action-list.component';
|
|
8
|
+
import LandingActionItem from './landing-action-item.component';
|
|
9
|
+
|
|
10
|
+
type LandingFooterGridProps = {
|
|
11
|
+
onNavigate: (path: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const cx = (...parts: Array<string | undefined | false>) => parts.filter(Boolean).join(' ');
|
|
15
|
+
|
|
16
|
+
const LandingFooterGrid: React.FC<LandingFooterGridProps> = ({ onNavigate }) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className={cx(styles.rbLandingGrid, styles.rbLandingGridBottom)}>
|
|
19
|
+
<LandingSectionCard title="Indicators" tone="blue" headerIcon={<ChartColumn />}>
|
|
20
|
+
<div className={styles.rbFooterActions}>
|
|
21
|
+
<Button size="sm" kind="secondary" renderIcon={Save} onClick={() => onNavigate('/indicators/save')}>
|
|
22
|
+
Save Indicator
|
|
23
|
+
</Button>
|
|
24
|
+
<Tag type="blue">Result: 124</Tag>
|
|
25
|
+
</div>
|
|
26
|
+
</LandingSectionCard>
|
|
27
|
+
|
|
28
|
+
<LandingSectionCard title="Saved Reports" tone="gold" headerIcon={<Save />}>
|
|
29
|
+
<LandingActionList>
|
|
30
|
+
<LandingActionItem icon={<Time />} label="Run a report" onClick={() => onNavigate('/run')} />
|
|
31
|
+
<LandingActionItem icon={<Time />} label="View recent runs" onClick={() => onNavigate('/runs')} />
|
|
32
|
+
<div className={styles.rbActionsRight}>
|
|
33
|
+
<Button size="sm" kind="primary" renderIcon={Play} onClick={() => onNavigate('/run')}>
|
|
34
|
+
Run a report
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
</LandingActionList>
|
|
38
|
+
</LandingSectionCard>
|
|
39
|
+
|
|
40
|
+
<div />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default LandingFooterGrid;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './report-builder-landing-page.scss';
|
|
3
|
+
|
|
4
|
+
type LandingHeroProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
subtitle: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const LandingHero: React.FC<LandingHeroProps> = ({ title, subtitle }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={styles.rbLandingHero}>
|
|
12
|
+
<h2 className={styles.rbLandingTitle}>{title}</h2>
|
|
13
|
+
<div className={styles.rbLandingSubtitle}>{subtitle}</div>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default LandingHero;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './report-builder-landing-page.scss';
|
|
3
|
+
|
|
4
|
+
export type CardTone = 'blue' | 'green' | 'gold';
|
|
5
|
+
|
|
6
|
+
const cx = (...parts: Array<string | undefined | false>) => parts.filter(Boolean).join(' ');
|
|
7
|
+
|
|
8
|
+
type LandingSectionCardProps = {
|
|
9
|
+
title: string;
|
|
10
|
+
tone: CardTone;
|
|
11
|
+
headerIcon: React.ReactNode;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const toneClass: Record<CardTone, string> = {
|
|
16
|
+
blue: styles.rbCardHeaderBlue,
|
|
17
|
+
green: styles.rbCardHeaderGreen,
|
|
18
|
+
gold: styles.rbCardHeaderGold,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const LandingSectionCard: React.FC<LandingSectionCardProps> = ({ title, tone, headerIcon, children }) => {
|
|
22
|
+
return (
|
|
23
|
+
<div className={styles.rbCard}>
|
|
24
|
+
<div className={cx(styles.rbCardHeader, toneClass[tone])}>
|
|
25
|
+
{headerIcon}
|
|
26
|
+
<span>{title}</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div className={styles.rbCardBody}>{children}</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default LandingSectionCard;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Tile } from '@carbon/react';
|
|
3
|
+
import { Add, ChartColumn, Play, Report } from '@carbon/icons-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useNavigate } from 'react-router-dom';
|
|
6
|
+
|
|
7
|
+
import styles from './report-builder-landing-page.scss';
|
|
8
|
+
import Header from '../shared/header/header.component';
|
|
9
|
+
|
|
10
|
+
const ReportBuilderLandingPage: React.FC = () => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={styles.page}>
|
|
16
|
+
{/* HERO (matches screenshot layout) */}
|
|
17
|
+
<Header
|
|
18
|
+
title={t('welcomeReporting', 'Welcome Report Builder')}
|
|
19
|
+
subtitle={t('welcomeReportingHint', 'Manage health data reports, define indicators, and run reports with ease.')}
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
{/* CARDS */}
|
|
23
|
+
<div className={styles.cardsGrid}>
|
|
24
|
+
<Tile className={styles.card}>
|
|
25
|
+
<div className={styles.cardIllustration} aria-hidden>
|
|
26
|
+
<div className={styles.illCircle}>
|
|
27
|
+
<Report size={56} />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<h3 className={styles.cardTitle}>{t('createReports', 'Create Reports')}</h3>
|
|
32
|
+
<p className={styles.cardBody}>
|
|
33
|
+
{t('createReportsHint', 'Define and configure health data reports tailored to your needs.')}
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<div className={styles.cardActions}>
|
|
37
|
+
<Button
|
|
38
|
+
kind="primary"
|
|
39
|
+
size="lg"
|
|
40
|
+
renderIcon={Add}
|
|
41
|
+
className={styles.cardButton}
|
|
42
|
+
onClick={() => navigate('/new')}
|
|
43
|
+
>
|
|
44
|
+
{t('getStarted', 'Get started')}
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
</Tile>
|
|
48
|
+
|
|
49
|
+
<Tile className={styles.card}>
|
|
50
|
+
<div className={styles.cardIllustration} aria-hidden>
|
|
51
|
+
<div className={styles.illCircle}>
|
|
52
|
+
<ChartColumn size={56} />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<h3 className={styles.cardTitle}>{t('manageIndicators', 'Manage Indicators')}</h3>
|
|
57
|
+
<p className={styles.cardBody}>
|
|
58
|
+
{t('manageIndicatorsHint', 'Create, edit, and organize indicators to measure key health metrics.')}
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<div className={styles.cardActions}>
|
|
62
|
+
<Button
|
|
63
|
+
kind="primary"
|
|
64
|
+
size="lg"
|
|
65
|
+
renderIcon={ChartColumn}
|
|
66
|
+
className={styles.cardButton}
|
|
67
|
+
onClick={() => navigate('/indicators')}
|
|
68
|
+
>
|
|
69
|
+
{t('viewIndicators', 'View indicators')}
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
</Tile>
|
|
73
|
+
|
|
74
|
+
<Tile className={styles.card}>
|
|
75
|
+
<div className={styles.cardIllustration} aria-hidden>
|
|
76
|
+
<div className={styles.illCircle}>
|
|
77
|
+
<Play size={56} />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h3 className={styles.cardTitle}>{t('runReports', 'Run Reports')}</h3>
|
|
82
|
+
<p className={styles.cardBody}>
|
|
83
|
+
{t('runReportsHint', 'Choose a report and run it for specific time periods and locations.')}
|
|
84
|
+
</p>
|
|
85
|
+
|
|
86
|
+
<div className={styles.cardActions}>
|
|
87
|
+
<Button
|
|
88
|
+
kind="primary"
|
|
89
|
+
size="lg"
|
|
90
|
+
renderIcon={Play}
|
|
91
|
+
className={styles.cardButton}
|
|
92
|
+
onClick={() => navigate('/run')}
|
|
93
|
+
>
|
|
94
|
+
{t('runNow', 'Run now')}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
</Tile>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default ReportBuilderLandingPage;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
width: 100%;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 1.5rem 0 2rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* HERO */
|
|
8
|
+
.hero {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: 380px 1fr;
|
|
11
|
+
gap: 1.5rem;
|
|
12
|
+
align-items: center;
|
|
13
|
+
|
|
14
|
+
padding: 1.25rem 1.5rem;
|
|
15
|
+
border-radius: 12px;
|
|
16
|
+
border: 1px solid var(--cds-border-subtle, #e0e0e0);
|
|
17
|
+
background: linear-gradient(90deg, #eef4ff 0%, #ffffff 55%, #ffffff 100%);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.heroArt {
|
|
21
|
+
height: 150px;
|
|
22
|
+
border-radius: 10px;
|
|
23
|
+
background: radial-gradient(circle at 30% 30%, #dbe7ff 0%, #eef4ff 55%, #ffffff 100%);
|
|
24
|
+
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
25
|
+
display: grid;
|
|
26
|
+
place-items: center;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.heroArtInner {
|
|
30
|
+
width: 112px;
|
|
31
|
+
height: 112px;
|
|
32
|
+
border-radius: 14px;
|
|
33
|
+
display: grid;
|
|
34
|
+
place-items: center;
|
|
35
|
+
background: rgba(255, 255, 255, 0.75);
|
|
36
|
+
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
37
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.heroTitle {
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: 1.75rem;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
letter-spacing: 0.1px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.heroSubtitle {
|
|
48
|
+
margin: 0.5rem 0 0;
|
|
49
|
+
font-size: 1rem;
|
|
50
|
+
line-height: 1.45;
|
|
51
|
+
opacity: 0.85;
|
|
52
|
+
max-width: 54ch;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* CARDS GRID */
|
|
56
|
+
.cardsGrid {
|
|
57
|
+
margin-top: 1.5rem;
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: repeat(3, 1fr);
|
|
60
|
+
gap: 1.25rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.card {
|
|
64
|
+
border-radius: 12px;
|
|
65
|
+
border: 1px solid var(--cds-border-subtle, #e0e0e0);
|
|
66
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
|
|
67
|
+
background: #fff;
|
|
68
|
+
|
|
69
|
+
padding: 1.25rem 1.25rem 1.25rem;
|
|
70
|
+
min-height: 340px;
|
|
71
|
+
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.cardIllustration {
|
|
77
|
+
display: grid;
|
|
78
|
+
place-items: center;
|
|
79
|
+
height: 140px;
|
|
80
|
+
border-radius: 12px;
|
|
81
|
+
background: linear-gradient(180deg, #f2f6ff 0%, #ffffff 100%);
|
|
82
|
+
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.illCircle {
|
|
86
|
+
width: 92px;
|
|
87
|
+
height: 92px;
|
|
88
|
+
border-radius: 999px;
|
|
89
|
+
display: grid;
|
|
90
|
+
place-items: center;
|
|
91
|
+
background: rgba(255, 255, 255, 0.9);
|
|
92
|
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
93
|
+
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.08);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cardTitle {
|
|
97
|
+
margin: 1rem 0 0;
|
|
98
|
+
font-size: 1.4rem;
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.cardBody {
|
|
103
|
+
margin: 0.65rem 0 0;
|
|
104
|
+
line-height: 1.5;
|
|
105
|
+
opacity: 0.85;
|
|
106
|
+
max-width: 46ch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.cardActions {
|
|
110
|
+
margin-top: auto;
|
|
111
|
+
padding-top: 1.25rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.cardButton {
|
|
115
|
+
width: 100%;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* RESPONSIVE */
|
|
119
|
+
@media (max-width: 1050px) {
|
|
120
|
+
.hero {
|
|
121
|
+
grid-template-columns: 1fr;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.heroArt {
|
|
125
|
+
height: 180px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cardsGrid {
|
|
129
|
+
grid-template-columns: 1fr;
|
|
130
|
+
}
|
|
131
|
+
}
|