@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,481 @@
1
+ // src/report-builder/components/indicators/indicators-page.component.tsx
2
+
3
+ import React from 'react';
4
+ import {
5
+ Button,
6
+ Search,
7
+ Stack,
8
+ InlineLoading,
9
+ Tabs,
10
+ TabList,
11
+ Tab,
12
+ TabPanels,
13
+ TabPanel,
14
+ } from '@carbon/react';
15
+ import { Add, Download } from '@carbon/icons-react';
16
+ import { useTranslation } from 'react-i18next';
17
+
18
+ import Header from '../shared/header/header.component';
19
+ import IndicatorsTable, { type IndicatorRow } from './indicators-table.component';
20
+
21
+ import CreateBaseIndicatorModal from './create-base-indicator-modal.component';
22
+ import type { QAUiState } from './types/condition-ui.types';
23
+
24
+ import CreateCompositeBaseIndicatorModal from './create-composite-base-indicator-modal.component';
25
+ import { type BaseIndicatorOption } from './types/composite-indicator.types';
26
+
27
+ import CreateFinalIndicatorModal from './create-final-indicator-modal.component';
28
+ import AiAssistButton from '../ai-support/ai-assist-button.component';
29
+
30
+ import {
31
+ listIndicators,
32
+ getIndicator,
33
+ createIndicator,
34
+ updateIndicator,
35
+ deleteIndicator,
36
+ type IndicatorDto,
37
+ } from '../../resources/indicator/indicators.api';
38
+
39
+ import { getDataTheme } from '../../resources/theme/data-theme.api';
40
+ import type { DataThemeConfig } from './types/data-theme-config.types';
41
+ import type { IndicatorCondition } from './types/indicator-types';
42
+ import { hydrateConditionUiState } from './utils/indicator-conditions-hydration.utils';
43
+ import type { SelectedConcept } from './handler/concept-search-multiselect.component';
44
+
45
+ type TabKey = 'base' | 'final';
46
+
47
+ type ThemeMeta = { color?: string };
48
+
49
+ function parseThemeColor(metaJson?: string | null): string | undefined {
50
+ if (!metaJson) return undefined;
51
+ try {
52
+ const p = JSON.parse(metaJson);
53
+ const inner: ThemeMeta = p?.metaJson ?? p ?? {};
54
+ return inner?.color;
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ type BaseIndicatorAuthoringV1 = {
61
+ version: 1;
62
+ themeUuid: string;
63
+ themeConfig: DataThemeConfig;
64
+ conditions: IndicatorCondition[];
65
+ sqlPreview: string;
66
+ };
67
+
68
+ function safeParse<T>(raw: string | undefined | null, fallback: T): T {
69
+ try {
70
+ if (!raw) return fallback;
71
+ const p = JSON.parse(raw);
72
+ return (p ?? fallback) as T;
73
+ } catch {
74
+ return fallback;
75
+ }
76
+ }
77
+
78
+ function normalizeThemeConfig(rawConfigJson: string | undefined | null): DataThemeConfig | null {
79
+ const base = safeParse<any>(rawConfigJson, null);
80
+ if (!base || typeof base !== 'object') return null;
81
+ if (base.configJson && typeof base.configJson === 'object') return base.configJson as DataThemeConfig;
82
+ return base as DataThemeConfig;
83
+ }
84
+
85
+ function normalizeAuthoring(ind: IndicatorDto | null | undefined): BaseIndicatorAuthoringV1 | null {
86
+ if (!ind?.configJson) return null;
87
+
88
+ let parsed: any;
89
+ try {
90
+ parsed = JSON.parse(ind.configJson);
91
+ } catch {
92
+ return null;
93
+ }
94
+ if (!parsed || typeof parsed !== 'object') return null;
95
+
96
+ // flat v1
97
+ if (parsed.themeUuid && parsed.themeConfig) {
98
+ return {
99
+ version: 1,
100
+ themeUuid: parsed.themeUuid,
101
+ themeConfig: parsed.themeConfig,
102
+ conditions: Array.isArray(parsed.conditions) ? parsed.conditions : [],
103
+ sqlPreview: parsed.sqlPreview || ind.sqlTemplate || '',
104
+ };
105
+ }
106
+
107
+ // { base: {...} }
108
+ if (parsed.base?.themeUuid && parsed.base?.themeConfig) {
109
+ return {
110
+ version: 1,
111
+ themeUuid: parsed.base.themeUuid,
112
+ themeConfig: parsed.base.themeConfig,
113
+ conditions: Array.isArray(parsed.base.conditions) ? parsed.base.conditions : [],
114
+ sqlPreview: parsed.base.sqlPreview || ind.sqlTemplate || '',
115
+ };
116
+ }
117
+
118
+ // { authoring: { base: {...} } }
119
+ if (parsed.authoring?.base?.themeUuid && parsed.authoring?.base?.themeConfig) {
120
+ const b = parsed.authoring.base;
121
+ return {
122
+ version: 1,
123
+ themeUuid: b.themeUuid,
124
+ themeConfig: b.themeConfig,
125
+ conditions: Array.isArray(b.conditions) ? b.conditions : [],
126
+ sqlPreview: b.sqlPreview || ind.sqlTemplate || '',
127
+ };
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ export default function IndicatorsPage() {
134
+ const { t } = useTranslation();
135
+
136
+ const [tab, setTab] = React.useState<TabKey>('base');
137
+ const [q, setQ] = React.useState('');
138
+ const [rows, setRows] = React.useState<IndicatorRow[]>([]);
139
+ const [loading, setLoading] = React.useState(false);
140
+ const [error, setError] = React.useState<string | null>(null);
141
+
142
+ // modals
143
+ const [openBase, setOpenBase] = React.useState(false);
144
+ const [openComposite, setOpenComposite] = React.useState(false);
145
+ const [openFinal, setOpenFinal] = React.useState(false);
146
+
147
+ const [mode, setMode] = React.useState<'create' | 'edit'>('create');
148
+ const [editing, setEditing] = React.useState<IndicatorDto | null>(null);
149
+
150
+ // ✅ Preloaded UI state for base edit, built BEFORE opening modal
151
+ const [editingConceptUi, setEditingConceptUi] = React.useState<Record<string, SelectedConcept[]>>({});
152
+ const [editingQaUi, setEditingQaUi] = React.useState<Record<string, QAUiState>>({});
153
+
154
+ // cache theme info so table can show name+color
155
+ const themeCache = React.useRef<Record<string, { name: string; color?: string }>>({});
156
+
157
+ // --------------------------------------------
158
+ // LOAD INDICATORS (default view + theme name/color resolution)
159
+ // --------------------------------------------
160
+ const load = React.useCallback(
161
+ async (signal?: AbortSignal) => {
162
+ setLoading(true);
163
+ setError(null);
164
+
165
+ try {
166
+ // Keep default view here (lighter + matches old working version)
167
+ const indicators = await listIndicators({ q, v: 'default', includeRetired: false }, signal);
168
+
169
+ const missingThemeUuids = Array.from(
170
+ new Set(indicators.map((x) => x.themeUuid || '').filter((u) => u && !themeCache.current[u])),
171
+ );
172
+
173
+ if (missingThemeUuids.length) {
174
+ await Promise.all(
175
+ missingThemeUuids.map(async (uuid) => {
176
+ try {
177
+ const full = await getDataTheme(uuid, signal);
178
+ const name = full?.name ? `${full.name}${full.code ? ` (${full.code})` : ''}` : uuid;
179
+ const color = parseThemeColor(full?.metaJson);
180
+ themeCache.current[uuid] = { name, color };
181
+ } catch {
182
+ themeCache.current[uuid] = { name: uuid };
183
+ }
184
+ }),
185
+ );
186
+ }
187
+
188
+ const mapped: IndicatorRow[] = indicators.map((x) => {
189
+ const themeUuid = x.themeUuid || '';
190
+ const themeInfo = themeUuid ? themeCache.current[themeUuid] : undefined;
191
+
192
+ return {
193
+ id: x.uuid,
194
+ code: x.code ?? '',
195
+ name: x.name ?? '',
196
+ kind: x.kind ?? 'BASE',
197
+ themeName: themeInfo?.name,
198
+ themeColor: themeInfo?.color,
199
+ status: x.retired ? 'Retired' : 'Draft',
200
+ };
201
+ });
202
+
203
+ setRows(mapped);
204
+ } catch (e: any) {
205
+ setError(e?.message ?? 'Failed to load indicators');
206
+ } finally {
207
+ setLoading(false);
208
+ }
209
+ },
210
+ [q],
211
+ );
212
+
213
+ React.useEffect(() => {
214
+ const ac = new AbortController();
215
+ load(ac.signal);
216
+ return () => ac.abort();
217
+ }, [load]);
218
+
219
+ // --------------------------------------------
220
+ // FILTER BY TAB
221
+ // --------------------------------------------
222
+ const filteredRows = React.useMemo(() => {
223
+ if (tab === 'base') return rows.filter((r) => r.kind === 'BASE' || r.kind === 'COMPOSITE');
224
+ return rows.filter((r) => r.kind === 'FINAL');
225
+ }, [rows, tab]);
226
+
227
+ // Composite builder should only get BASE indicators
228
+ const baseIndicators: BaseIndicatorOption[] = React.useMemo(() => {
229
+ return rows
230
+ .filter((r) => r.kind === 'BASE')
231
+ .map((r) => ({
232
+ id: r.id,
233
+ code: r.code,
234
+ name: r.name,
235
+ unit: 'Patients',
236
+ }));
237
+ }, [rows]);
238
+
239
+ // --------------------------------------------
240
+ // ACTIONS
241
+ // --------------------------------------------
242
+ const onCreateBase = () => {
243
+ setMode('create');
244
+ setEditing(null);
245
+ setEditingConceptUi({});
246
+ setEditingQaUi({});
247
+ setOpenBase(true);
248
+ };
249
+
250
+ const onEdit = async (uuid: string, kind?: string) => {
251
+ const ac = new AbortController();
252
+
253
+ try {
254
+ setLoading(true);
255
+ setError(null);
256
+
257
+ const full = await getIndicator(uuid, ac.signal, 'full');
258
+
259
+ const k = String(full.kind ?? kind ?? 'BASE').toUpperCase();
260
+
261
+ // ✅ Composite edit routes to composite modal
262
+ if (k === 'COMPOSITE') {
263
+ setEditing(full);
264
+ setMode('edit');
265
+ setOpenComposite(true);
266
+ return;
267
+ }
268
+
269
+ // block final edit for now
270
+ if (k === 'FINAL') {
271
+ setError('Editing final indicators is not yet supported.');
272
+ return;
273
+ }
274
+
275
+ // ✅ BASE edit: restore hydration pipeline
276
+ const authoring = normalizeAuthoring(full);
277
+
278
+ let resolvedThemeUuid = full.themeUuid ?? '';
279
+ let resolvedThemeConfig: DataThemeConfig | null = null;
280
+ let pickedConditions: IndicatorCondition[] = [];
281
+
282
+ if (authoring?.themeUuid && authoring?.themeConfig) {
283
+ resolvedThemeUuid = authoring.themeUuid;
284
+ resolvedThemeConfig = authoring.themeConfig;
285
+ pickedConditions = authoring.conditions ?? [];
286
+ } else {
287
+ if (resolvedThemeUuid) {
288
+ const theme = await getDataTheme(resolvedThemeUuid, ac.signal);
289
+ resolvedThemeConfig = normalizeThemeConfig(theme?.configJson);
290
+ }
291
+ pickedConditions = [];
292
+ }
293
+
294
+ const { conceptUi, qaUi } = await hydrateConditionUiState(
295
+ resolvedThemeConfig?.conditions ?? [],
296
+ pickedConditions,
297
+ {},
298
+ {},
299
+ ac.signal,
300
+ { force: true, dedupe: true },
301
+ );
302
+
303
+ setEditing(full);
304
+ setEditingConceptUi(conceptUi);
305
+ setEditingQaUi(qaUi);
306
+
307
+ setMode('edit');
308
+ setOpenBase(true);
309
+ } catch (e: any) {
310
+ setError(e?.message ?? 'Failed to load indicator for editing');
311
+ } finally {
312
+ setLoading(false);
313
+ }
314
+ };
315
+
316
+ const onDelete = async (uuid: string) => {
317
+ try {
318
+ await deleteIndicator(uuid, false, 'Retired via UI');
319
+ const ac = new AbortController();
320
+ await load(ac.signal);
321
+ } catch (e: any) {
322
+ setError(e?.message ?? 'Failed to delete indicator');
323
+ }
324
+ };
325
+
326
+ const onRun = (uuid: string) => console.log('Run indicator:', uuid);
327
+ const onOpen = (uuid: string) => console.log('Open indicator:', uuid);
328
+
329
+ // --------------------------------------------
330
+ // RENDER
331
+ // --------------------------------------------
332
+ return (
333
+ <Stack gap={5}>
334
+ <Header title={t('indicators', 'Indicators')} subtitle="Create and manage report indicators." />
335
+
336
+ {/* Top Bar */}
337
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'end' }}>
338
+ <Search
339
+ size="lg"
340
+ labelText="Search"
341
+ placeholder="Search indicators…"
342
+ value={q}
343
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQ(e.target.value)}
344
+ />
345
+
346
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
347
+ <AiAssistButton context={{ page: 'Indicators' }} size="sm" kind="secondary" />
348
+
349
+ <Button size="sm" kind="secondary" renderIcon={Download}>
350
+ CSV
351
+ </Button>
352
+
353
+ {tab === 'base' ? (
354
+ <>
355
+ <Button
356
+ size="sm"
357
+ kind="secondary"
358
+ renderIcon={Add}
359
+ onClick={() => {
360
+ setMode('create');
361
+ setEditing(null);
362
+ setOpenComposite(true);
363
+ }}
364
+ >
365
+ Create Composite Base
366
+ </Button>
367
+
368
+ <Button size="sm" kind="primary" renderIcon={Add} onClick={onCreateBase}>
369
+ Create Base Indicator
370
+ </Button>
371
+ </>
372
+ ) : (
373
+ <Button size="sm" kind="primary" renderIcon={Add} onClick={() => setOpenFinal(true)}>
374
+ Create Final Indicator
375
+ </Button>
376
+ )}
377
+ </div>
378
+ </div>
379
+
380
+ {/* Tabs */}
381
+ <Tabs
382
+ selectedIndex={tab === 'base' ? 0 : 1}
383
+ onChange={({ selectedIndex }) => setTab(selectedIndex === 0 ? 'base' : 'final')}
384
+ >
385
+ <TabList aria-label="Indicator tabs">
386
+ <Tab>Base Indicators</Tab>
387
+ <Tab>Final Indicators</Tab>
388
+ </TabList>
389
+
390
+ <TabPanels>
391
+ <TabPanel>
392
+ {loading ? <InlineLoading description="Loading…" /> : null}
393
+ {!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
394
+
395
+ <IndicatorsTable rows={filteredRows} onOpen={onOpen} onEdit={onEdit} onRun={onRun} onDelete={onDelete} />
396
+ </TabPanel>
397
+
398
+ <TabPanel>
399
+ {loading ? <InlineLoading description="Loading…" /> : null}
400
+ {!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
401
+
402
+ <IndicatorsTable rows={filteredRows} onOpen={onOpen} onEdit={onEdit} onRun={onRun} onDelete={onDelete} />
403
+ </TabPanel>
404
+ </TabPanels>
405
+ </Tabs>
406
+
407
+ {/* Modals */}
408
+ <CreateBaseIndicatorModal
409
+ open={openBase}
410
+ mode={mode}
411
+ initial={editing}
412
+ initialConceptUi={mode === 'edit' ? editingConceptUi : undefined}
413
+ initialQaUi={mode === 'edit' ? editingQaUi : undefined}
414
+ onClose={() => {
415
+ setOpenBase(false);
416
+ setEditing(null);
417
+ setEditingConceptUi({});
418
+ setEditingQaUi({});
419
+ setMode('create');
420
+ }}
421
+ onSaved={async () => {
422
+ setOpenBase(false);
423
+ setEditing(null);
424
+ setEditingConceptUi({});
425
+ setEditingQaUi({});
426
+ setMode('create');
427
+ const ac = new AbortController();
428
+ await load(ac.signal);
429
+ }}
430
+ onCreate={async (payload) => {
431
+ await createIndicator(payload);
432
+ }}
433
+ onUpdate={async (id, payload) => {
434
+ await updateIndicator(id, payload);
435
+ }}
436
+ />
437
+
438
+ <CreateCompositeBaseIndicatorModal
439
+ open={openComposite}
440
+ mode={mode}
441
+ initial={editing}
442
+ onClose={() => {
443
+ setOpenComposite(false);
444
+ setEditing(null);
445
+ setMode('create');
446
+ }}
447
+ baseIndicators={baseIndicators}
448
+ onCreate={async (payload) => {
449
+ await createIndicator(payload);
450
+ }}
451
+ onUpdate={async (uuid, payload) => {
452
+ await updateIndicator(uuid, payload);
453
+ }}
454
+ onSaved={async () => {
455
+ setOpenComposite(false);
456
+ setEditing(null);
457
+ setMode('create');
458
+ const ac = new AbortController();
459
+ await load(ac.signal);
460
+ }}
461
+ />
462
+
463
+ {/* Final indicator is still stubbed in your project; keep it create-only for now */}
464
+ <CreateFinalIndicatorModal
465
+ open={openFinal}
466
+ mode="create"
467
+ initial={null}
468
+ baseIndicators={baseIndicators}
469
+ onClose={() => setOpenFinal(false)}
470
+ onCreate={async (payload) => {
471
+ await createIndicator(payload);
472
+ }}
473
+ onSaved={async () => {
474
+ setOpenFinal(false);
475
+ const ac = new AbortController();
476
+ await load(ac.signal);
477
+ }}
478
+ />
479
+ </Stack>
480
+ );
481
+ }
@@ -0,0 +1,121 @@
1
+ .page {
2
+ width: 100%;
3
+ margin: 0 auto;
4
+ padding: 1.5rem 0 2rem;
5
+ }
6
+
7
+ .indicatorsPage {
8
+ width: 100%;
9
+ padding: 0; /* let O3 page chrome handle padding */
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ /* Header row like "Manage Reports" */
14
+ .headerRow {
15
+ display: flex;
16
+ align-items: flex-end;
17
+ justify-content: space-between;
18
+ gap: 1rem;
19
+
20
+ padding: 1rem 1.5rem;
21
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
22
+ background: var(--cds-layer, #ffffff);
23
+ }
24
+
25
+ .title {
26
+ font-size: 1.5rem;
27
+ font-weight: 600;
28
+ line-height: 1.2;
29
+ margin: 0;
30
+ }
31
+
32
+ /* Actions on the right */
33
+ .headerActions {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 0.5rem;
37
+ flex-wrap: wrap;
38
+ }
39
+
40
+ /* Tabs area should feel like it belongs to the page, full-width */
41
+ :global(.cds--tabs) {
42
+ padding: 0 1.5rem;
43
+ background: var(--cds-layer, #ffffff);
44
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
45
+ }
46
+
47
+ /* Remove extra padding Carbon sometimes adds */
48
+ :global(.cds--tab-content) {
49
+ padding: 0;
50
+ }
51
+
52
+ /* Tab panel wrapper: make it full-width like list pages */
53
+ .panel {
54
+ padding: 1rem 1.5rem 1.5rem;
55
+ background: var(--cds-background, #f4f4f4);
56
+ }
57
+
58
+ /* Create a white "content surface" like Manage Reports list area */
59
+ .panel :global(.cds--stack) {
60
+ background: var(--cds-layer, #ffffff);
61
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
62
+ border-radius: 0.25rem;
63
+ padding: 0.75rem;
64
+ }
65
+
66
+ /* Search should span wide (Manage Reports style) */
67
+ .panel :global(.cds--search) {
68
+ width: 100%;
69
+ max-width: none;
70
+ }
71
+
72
+ /* DataTable should span full width */
73
+ .panel :global(.cds--data-table-container) {
74
+ width: 100%;
75
+ margin-top: 0.5rem;
76
+ }
77
+
78
+ /* Ensure the table itself takes the width */
79
+ .panel :global(.cds--data-table) {
80
+ width: 100%;
81
+ }
82
+
83
+ /* Make rows feel clickable */
84
+ .panel :global(.cds--data-table tbody tr) {
85
+ cursor: pointer;
86
+ }
87
+
88
+ /* Hover feedback */
89
+ .panel :global(.cds--data-table tbody tr:hover) {
90
+ background-color: var(--cds-layer-hover, #f4f4f4);
91
+ }
92
+
93
+ /* Keep the table header background subtle */
94
+ .panel :global(.cds--data-table thead th) {
95
+ background: var(--cds-layer-accent, #f4f4f4);
96
+ }
97
+
98
+ /* Responsive */
99
+ @media (max-width: 768px) {
100
+ .headerRow {
101
+ flex-direction: column;
102
+ align-items: flex-start;
103
+ }
104
+
105
+ .headerActions {
106
+ width: 100%;
107
+ justify-content: flex-end;
108
+ }
109
+
110
+ :global(.cds--tabs) {
111
+ padding: 0 1rem;
112
+ }
113
+
114
+ .panel {
115
+ padding: 1rem;
116
+ }
117
+
118
+ .headerRow {
119
+ padding: 1rem;
120
+ }
121
+ }