@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.
Files changed (222) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/LICENSE +401 -0
  5. package/README.md +317 -0
  6. package/dist/184.js +2 -0
  7. package/dist/184.js.LICENSE.txt +14 -0
  8. package/dist/184.js.map +1 -0
  9. package/dist/211.js +1 -0
  10. package/dist/211.js.map +1 -0
  11. package/dist/300.js +1 -0
  12. package/dist/311.js +1 -0
  13. package/dist/311.js.map +1 -0
  14. package/dist/44.js +2 -0
  15. package/dist/44.js.LICENSE.txt +30 -0
  16. package/dist/44.js.map +1 -0
  17. package/dist/478.js +2 -0
  18. package/dist/478.js.LICENSE.txt +9 -0
  19. package/dist/478.js.map +1 -0
  20. package/dist/535.js +1 -0
  21. package/dist/535.js.map +1 -0
  22. package/dist/540.js +2 -0
  23. package/dist/540.js.LICENSE.txt +9 -0
  24. package/dist/540.js.map +1 -0
  25. package/dist/710.js +1 -0
  26. package/dist/710.js.map +1 -0
  27. package/dist/729.js +1 -0
  28. package/dist/729.js.map +1 -0
  29. package/dist/872.js +1 -0
  30. package/dist/872.js.map +1 -0
  31. package/dist/917.js +1 -0
  32. package/dist/917.js.map +1 -0
  33. package/dist/961.js +2 -0
  34. package/dist/961.js.LICENSE.txt +19 -0
  35. package/dist/961.js.map +1 -0
  36. package/dist/972.js +2 -0
  37. package/dist/972.js.LICENSE.txt +21 -0
  38. package/dist/972.js.map +1 -0
  39. package/dist/main.js +1 -0
  40. package/dist/main.js.map +1 -0
  41. package/dist/openmrs-esm-report-builder.js +1 -0
  42. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +482 -0
  43. package/dist/openmrs-esm-report-builder.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/e2e/core/env.ts +5 -0
  46. package/e2e/core/global-setup.ts +34 -0
  47. package/e2e/core/test.ts +21 -0
  48. package/e2e/pages/app-shell.page.ts +34 -0
  49. package/e2e/pages/etl-sources.page.ts +35 -0
  50. package/e2e/pages/report-sections.page.ts +20 -0
  51. package/e2e/pages/section-preview.modal.ts +29 -0
  52. package/e2e/specs/etl-sources.spec.ts +51 -0
  53. package/e2e/specs/navigation.spec.ts +13 -0
  54. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  55. package/e2e/specs/section-preview.spec.ts +94 -0
  56. package/e2e/tsconfig.json +11 -0
  57. package/example.env +5 -0
  58. package/jest.config.js +28 -0
  59. package/package.json +102 -0
  60. package/playwright.config.ts +28 -0
  61. package/src/admin-card-link.extension.module.scss +55 -0
  62. package/src/admin-card-link.extension.tsx +24 -0
  63. package/src/components/admin/admin-page.component.tsx +31 -0
  64. package/src/components/admin/age-categories-page.component.tsx +109 -0
  65. package/src/components/admin/age-groups-page.component.tsx +146 -0
  66. package/src/components/admin/etl-sources-page.component.tsx +344 -0
  67. package/src/components/admin/report-categories-page.component.tsx +256 -0
  68. package/src/components/admin/report-library-page.component.tsx +540 -0
  69. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  70. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  71. package/src/components/ai-support/ai.service.ts +21 -0
  72. package/src/components/ai-support/ai.types.ts +12 -0
  73. package/src/components/app-shell/report-builder-shell.component.tsx +120 -0
  74. package/src/components/app-shell/report-builder-shell.scss +15 -0
  75. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  76. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  77. package/src/components/data-themes/data-themes-page.component.tsx +132 -0
  78. package/src/components/data-themes/data-themes-table.component.tsx +91 -0
  79. package/src/components/data-themes/icon-registry.ts +35 -0
  80. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  81. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  82. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  83. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  84. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  85. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  86. package/src/components/indicators/README.md +24 -0
  87. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  88. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  89. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  90. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  91. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  92. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  93. package/src/components/indicators/create-base-indicator-modal.component.tsx +501 -0
  94. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +297 -0
  95. package/src/components/indicators/create-final-indicator-modal.component.tsx +260 -0
  96. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  97. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  98. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  99. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  100. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  101. package/src/components/indicators/indicators-page.component.tsx +481 -0
  102. package/src/components/indicators/indicators-page.scss +121 -0
  103. package/src/components/indicators/indicators-table.component.tsx +166 -0
  104. package/src/components/indicators/preview-table.component.tsx +31 -0
  105. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  106. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +161 -0
  107. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  108. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  109. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  110. package/src/components/indicators/sections/final-indicator-picker.section.tsx +92 -0
  111. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  112. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  113. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  114. package/src/components/indicators/sections/indicator-conditions.section.tsx +194 -0
  115. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +26 -0
  116. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  117. package/src/components/indicators/sql-preview.component.tsx +19 -0
  118. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  119. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  120. package/src/components/indicators/types/composite-indicator.types.ts +21 -0
  121. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  122. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  123. package/src/components/indicators/types/indicator-types.ts +56 -0
  124. package/src/components/indicators/types/sql-builders.ts +43 -0
  125. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +206 -0
  126. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  127. package/src/components/indicators/utils/final-indicator-sql.utils.ts +124 -0
  128. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  129. package/src/components/indicators/utils/indicator-sql.utils.ts +185 -0
  130. package/src/components/landing/landing-action-item.component.tsx +50 -0
  131. package/src/components/landing/landing-action-list.component.tsx +12 -0
  132. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  133. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  134. package/src/components/landing/landing-hero.component.tsx +18 -0
  135. package/src/components/landing/landing-section-card.component.tsx +33 -0
  136. package/src/components/landing/report-builder-landing-page.component.tsx +103 -0
  137. package/src/components/landing/report-builder-landing-page.scss +131 -0
  138. package/src/components/report/dashboard/reports-table.component.tsx +145 -0
  139. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  140. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  141. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  142. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  143. package/src/components/report/definition/report-definition.types.ts +66 -0
  144. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  145. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  146. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  147. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  148. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  149. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  150. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  151. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  152. package/src/components/report/design/report-design-editor.component.tsx +894 -0
  153. package/src/components/report/design/report-design.types.ts +43 -0
  154. package/src/components/report/design/report-design.utils.ts +86 -0
  155. package/src/components/report/panels/report-detail.component.tsx +71 -0
  156. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  157. package/src/components/report/report-dashboard.page.component.tsx +75 -0
  158. package/src/components/report/report-editor.page.component.tsx +466 -0
  159. package/src/components/report/report.routes.tsx +30 -0
  160. package/src/components/report-sections/create-report-section-modal.component.tsx +278 -0
  161. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  162. package/src/components/report-sections/hooks/useSectionEditorState.ts +306 -0
  163. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  164. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  165. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +143 -0
  166. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  167. package/src/components/report-sections/report-section-preview-modal.component.tsx +418 -0
  168. package/src/components/report-sections/report-sections-page.component.tsx +313 -0
  169. package/src/components/report-sections/section-types.ts +55 -0
  170. package/src/components/report-sections/section-utils.ts +46 -0
  171. package/src/components/report-sections/sections-page.scss +79 -0
  172. package/src/components/run-reports/run-reports-page.component.tsx +23 -0
  173. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  174. package/src/components/shared/header/header.component.tsx +85 -0
  175. package/src/components/shared/header/header.scss +150 -0
  176. package/src/components/shared/header/illustration.component.tsx +34 -0
  177. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  178. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  179. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  180. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  181. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  182. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  183. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  184. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  185. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  186. package/src/config-schema.ts +23 -0
  187. package/src/globals.d.ts +14 -0
  188. package/src/hooks/theme/useDataThemes.ts +28 -0
  189. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  190. package/src/hooks/theme/useMambaTables.ts +63 -0
  191. package/src/index.ts +23 -0
  192. package/src/resources/agegroup/agegroups.api.ts +58 -0
  193. package/src/resources/concepts/concept-mapper.ts +21 -0
  194. package/src/resources/concepts/concept-types.ts +31 -0
  195. package/src/resources/concepts/concepts.resource.ts +44 -0
  196. package/src/resources/concepts/useConceptSearch.ts +41 -0
  197. package/src/resources/etl-source/etl-source.api.ts +65 -0
  198. package/src/resources/indicator/indicators.api.ts +125 -0
  199. package/src/resources/openmrs-api.ts +24 -0
  200. package/src/resources/preview/sql-preview.api.ts +54 -0
  201. package/src/resources/report/reports.api.ts +117 -0
  202. package/src/resources/report-category/report-category.api.ts +41 -0
  203. package/src/resources/report-library/report-library.api.ts +63 -0
  204. package/src/resources/report-section/report-sections.api.ts +73 -0
  205. package/src/resources/report-section/section-preview.api.ts +21 -0
  206. package/src/resources/theme/data-theme.api.ts +88 -0
  207. package/src/resources/theme/mamba-schema.api.ts +31 -0
  208. package/src/resources/theme/mamba-table-meta.api.ts +45 -0
  209. package/src/root.component.tsx +20 -0
  210. package/src/routes/report-builder.routes.tsx +66 -0
  211. package/src/routes/report-builder.scss +56 -0
  212. package/src/routes.json +21 -0
  213. package/src/sample-template.ts +121 -0
  214. package/src/template-utils.ts +110 -0
  215. package/src/types/carbon-react-inert.d.ts +13 -0
  216. package/src/types/condition-operators.ts +49 -0
  217. package/src/types/theme/data-theme.types.ts +88 -0
  218. package/src/utils/html-entities.utils.ts +23 -0
  219. package/tools/setup-tests.ts +1 -0
  220. package/translations/en.json +4 -0
  221. package/tsconfig.json +43 -0
  222. 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
+ }