@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,466 @@
1
+ import React from 'react';
2
+ import {
3
+ Button,
4
+ InlineLoading,
5
+ Tabs,
6
+ TabList,
7
+ Tab,
8
+ TabPanels,
9
+ TabPanel,
10
+ InlineNotification,
11
+ } from '@carbon/react';
12
+ import { Save, Download } from '@carbon/icons-react';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { useParams } from 'react-router-dom';
15
+
16
+ import Header from '../shared/header/header.component';
17
+ import AiAssistButton from '../ai-support/ai-assist-button.component';
18
+
19
+ import ReportDetail, { type ReportDetailModel } from './panels/report-detail.component';
20
+ import ReportDefinitionEditor from './definition/report-definition-editor.component';
21
+ import ReportDesignEditor, { type DesignSectionSource } from './design/report-design-editor.component';
22
+
23
+ import type { ReportDefinitionDraft } from './definition/report-definition.types';
24
+ import type { ReportDesignDraft } from './design/report-design.types';
25
+ import { createEmptyReportDesignDraft } from './design/report-design.utils';
26
+
27
+ import {
28
+ createReport,
29
+ updateReport,
30
+ getReport,
31
+ compileReport,
32
+ type ReportDto,
33
+ } from '../../resources/report/reports.api';
34
+ import { listSections } from '../../resources/report-section/report-sections.api';
35
+
36
+ type TabKey = 'details' | 'definition' | 'design';
37
+ type BuilderMode = 'create' | 'edit';
38
+
39
+ type ReportFormState = {
40
+ uuid: string;
41
+ name: string;
42
+ description: string;
43
+ code: string;
44
+ sections: ReportDefinitionDraft['sections'];
45
+ design: ReportDesignDraft;
46
+ };
47
+
48
+ type SectionDtoLike = {
49
+ uuid: string;
50
+ name?: string;
51
+ configJson?: string;
52
+ retired?: boolean;
53
+ };
54
+
55
+ function buildEmptyForm(): ReportFormState {
56
+ return {
57
+ uuid: crypto.randomUUID(),
58
+ name: '',
59
+ description: '',
60
+ code: '',
61
+ sections: [],
62
+ design: createEmptyReportDesignDraft(),
63
+ };
64
+ }
65
+
66
+ function slugifyCode(name: string): string {
67
+ return (name || '')
68
+ .trim()
69
+ .toUpperCase()
70
+ .replace(/[^A-Z0-9]+/g, '_')
71
+ .replace(/^_+|_+$/g, '');
72
+ }
73
+
74
+ function parseSavedReportToForm(report: ReportDto): ReportFormState {
75
+ let parsed: any = {};
76
+ try {
77
+ parsed = report.configJson ? JSON.parse(report.configJson) : {};
78
+ } catch {
79
+ parsed = {};
80
+ }
81
+
82
+ const definition = parsed?.definition ?? {};
83
+ const legacySections = Array.isArray(parsed?.sections) ? parsed.sections : [];
84
+ const design = parsed?.design ?? null;
85
+
86
+ return {
87
+ uuid: report.uuid,
88
+ name: report.name ?? parsed?.name ?? '',
89
+ description: report.description ?? parsed?.description ?? '',
90
+ code: report.code ?? parsed?.code ?? '',
91
+ sections: Array.isArray(definition?.sections) ? definition.sections : legacySections,
92
+ design: design ?? createEmptyReportDesignDraft(),
93
+ };
94
+ }
95
+
96
+ function safeParseJson(raw?: string | null): any {
97
+ try {
98
+ return raw ? JSON.parse(raw) : {};
99
+ } catch {
100
+ return {};
101
+ }
102
+ }
103
+
104
+ export default function ReportEditorPage() {
105
+ const { t } = useTranslation();
106
+ const { reportId } = useParams();
107
+
108
+ const mode: BuilderMode = reportId ? 'edit' : 'create';
109
+
110
+ const [tab, setTab] = React.useState<TabKey>('details');
111
+ const [form, setForm] = React.useState<ReportFormState>(() => buildEmptyForm());
112
+
113
+ const [savedReport, setSavedReport] = React.useState<ReportDto | null>(null);
114
+ const [allSections, setAllSections] = React.useState<SectionDtoLike[]>([]);
115
+
116
+ const [loading, setLoading] = React.useState(mode === 'edit');
117
+ const [sectionsLoading, setSectionsLoading] = React.useState(true);
118
+ const [saving, setSaving] = React.useState(false);
119
+ const [compiling, setCompiling] = React.useState(false);
120
+
121
+ const [saveError, setSaveError] = React.useState<string | null>(null);
122
+ const [saveSuccess, setSaveSuccess] = React.useState<string | null>(null);
123
+ const [compileError, setCompileError] = React.useState<string | null>(null);
124
+ const [compileSuccess, setCompileSuccess] = React.useState<string | null>(null);
125
+
126
+ React.useEffect(() => {
127
+ const ac = new AbortController();
128
+
129
+ setSectionsLoading(true);
130
+ listSections({ v: 'full', includeRetired: false }, ac.signal)
131
+ .then((rows: SectionDtoLike[]) => setAllSections(Array.isArray(rows) ? rows : []))
132
+ .catch(() => setAllSections([]))
133
+ .finally(() => setSectionsLoading(false));
134
+
135
+ return () => ac.abort();
136
+ }, []);
137
+
138
+ React.useEffect(() => {
139
+ let cancelled = false;
140
+
141
+ async function loadReport() {
142
+ if (mode !== 'edit' || !reportId) {
143
+ setForm(buildEmptyForm());
144
+ setSavedReport(null);
145
+ setLoading(false);
146
+ return;
147
+ }
148
+
149
+ try {
150
+ setLoading(true);
151
+ setSaveError(null);
152
+ setCompileError(null);
153
+
154
+ const report = await getReport(reportId);
155
+ if (cancelled) return;
156
+
157
+ setSavedReport(report);
158
+ setForm(parseSavedReportToForm(report));
159
+ } catch (e: any) {
160
+ if (!cancelled) {
161
+ setSaveError(e?.message ?? 'Failed to load report');
162
+ }
163
+ } finally {
164
+ if (!cancelled) {
165
+ setLoading(false);
166
+ }
167
+ }
168
+ }
169
+
170
+ loadReport();
171
+ return () => {
172
+ cancelled = true;
173
+ };
174
+ }, [mode, reportId]);
175
+
176
+ const detailModel: ReportDetailModel = {
177
+ uuid: form.uuid,
178
+ name: form.name,
179
+ description: form.description,
180
+ };
181
+
182
+ const definitionDraft: ReportDefinitionDraft = {
183
+ name: form.name,
184
+ description: form.description,
185
+ code: form.code,
186
+ sections: form.sections,
187
+ };
188
+
189
+ const designDraft: ReportDesignDraft = form.design;
190
+
191
+ const sectionNameLookup = React.useMemo(() => {
192
+ return allSections.reduce<Record<string, string>>((acc, s) => {
193
+ acc[s.uuid] = s.name ?? s.uuid;
194
+ return acc;
195
+ }, {});
196
+ }, [allSections]);
197
+
198
+ const sectionSources = React.useMemo<DesignSectionSource[]>(() => {
199
+ const chosen = Array.isArray(form.sections) ? form.sections : [];
200
+ const sectionMap = new Map(allSections.map((s) => [s.uuid, s]));
201
+
202
+ return chosen
203
+ .slice()
204
+ .sort((a: any, b: any) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0))
205
+ .map((ref: any) => {
206
+ const section = sectionMap.get(ref.sectionUuid);
207
+ const cfg = safeParseJson(section?.configJson);
208
+ const indicators = Array.isArray(cfg?.indicators) ? cfg.indicators : [];
209
+
210
+ return {
211
+ sectionUuid: ref.sectionUuid,
212
+ title: ref.titleOverride?.trim() || section?.name || ref.sectionUuid,
213
+ indicators: indicators
214
+ .slice()
215
+ .sort((a: any, b: any) => Number(a.sortOrder ?? 0) - Number(b.sortOrder ?? 0))
216
+ .map((i: any) => ({
217
+ id: String(i.indicatorUuid ?? i.id ?? i.code ?? crypto.randomUUID()),
218
+ code: String(i.code ?? ''),
219
+ name: String(i.name ?? i.code ?? ''),
220
+ type: String(i.kind ?? 'indicator'),
221
+ })),
222
+ };
223
+ });
224
+ }, [allSections, form.sections]);
225
+
226
+ const handleDetailChange = React.useCallback((next: ReportDetailModel) => {
227
+ setForm((prev) => ({
228
+ ...prev,
229
+ name: next.name,
230
+ description: next.description,
231
+ uuid: next.uuid,
232
+ code: prev.code?.trim() ? prev.code : slugifyCode(next.name),
233
+ }));
234
+ }, []);
235
+
236
+ const handleDefinitionChange = React.useCallback((next: ReportDefinitionDraft) => {
237
+ setForm((prev) => ({
238
+ ...prev,
239
+ code: next.code,
240
+ sections: next.sections,
241
+ }));
242
+ }, []);
243
+
244
+ const handleDesignChange = React.useCallback((next: ReportDesignDraft) => {
245
+ setForm((prev) => ({
246
+ ...prev,
247
+ design: next,
248
+ }));
249
+ }, []);
250
+
251
+ const handleDraftSave = React.useCallback(async () => {
252
+ try {
253
+ setSaving(true);
254
+ setSaveError(null);
255
+ setSaveSuccess(null);
256
+ setCompileError(null);
257
+
258
+ const authoringJson = {
259
+ version: 1,
260
+ uuid: savedReport?.uuid ?? form.uuid,
261
+ name: form.name,
262
+ code: form.code || slugifyCode(form.name),
263
+ description: form.description,
264
+ definition: {
265
+ sections: form.sections ?? [],
266
+ },
267
+ design: form.design,
268
+ };
269
+
270
+ const payload = {
271
+ name: authoringJson.name,
272
+ description: authoringJson.description,
273
+ code: authoringJson.code,
274
+ configJson: JSON.stringify(authoringJson, null, 2),
275
+ };
276
+
277
+ const result =
278
+ mode === 'edit' && (savedReport?.uuid || reportId)
279
+ ? await updateReport((savedReport?.uuid || reportId) as string, payload)
280
+ : await createReport(payload);
281
+
282
+ setSavedReport(result);
283
+ setForm((prev) => ({
284
+ ...prev,
285
+ uuid: result.uuid,
286
+ code: result.code ?? prev.code,
287
+ }));
288
+ setSaveSuccess('Draft saved successfully.');
289
+ return result;
290
+ } catch (e: any) {
291
+ setSaveError(e?.message ?? 'Failed to save report draft');
292
+ throw e;
293
+ } finally {
294
+ setSaving(false);
295
+ }
296
+ }, [form, mode, reportId, savedReport?.uuid]);
297
+
298
+ const handleCompile = React.useCallback(async () => {
299
+ try {
300
+ setCompiling(true);
301
+ setCompileError(null);
302
+ setCompileSuccess(null);
303
+ setSaveError(null);
304
+
305
+ let targetUuid = savedReport?.uuid || reportId || null;
306
+
307
+ if (!targetUuid) {
308
+ const saved = await handleDraftSave();
309
+ targetUuid = saved?.uuid ?? null;
310
+ }
311
+
312
+ if (!targetUuid) {
313
+ throw new Error('Save the report first before compiling.');
314
+ }
315
+
316
+ const result = await compileReport(targetUuid);
317
+
318
+ setCompileSuccess(
319
+ result?.reportDefinitionUuid
320
+ ? `Compiled successfully. Runtime report UUID: ${result.reportDefinitionUuid}`
321
+ : 'Compiled successfully.',
322
+ );
323
+ } catch (e: any) {
324
+ setCompileError(e?.message ?? 'Failed to compile report');
325
+ } finally {
326
+ setCompiling(false);
327
+ }
328
+ }, [handleDraftSave, reportId, savedReport?.uuid]);
329
+
330
+ const handleCancel = React.useCallback(() => {
331
+ setForm(buildEmptyForm());
332
+ setSavedReport(null);
333
+ setSaveError(null);
334
+ setSaveSuccess(null);
335
+ setCompileError(null);
336
+ setCompileSuccess(null);
337
+ setTab('details');
338
+ }, []);
339
+
340
+ const canSave = Boolean(form.name.trim()) && !saving && !loading;
341
+ const canCompile = Boolean((savedReport?.uuid || reportId || form.name.trim()) && !saving && !compiling && !loading);
342
+
343
+ return (
344
+ <>
345
+ <Header
346
+ title={t('reportBuilder', 'Report Builder')}
347
+ subtitle={t('reportBuilderSubtitle', 'Create and manage reports as collections of reusable sections.')}
348
+ />
349
+
350
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'end', marginBottom: '1rem' }}>
351
+ <div>
352
+ {loading ? <InlineLoading description="Loading report…" /> : null}
353
+ {sectionsLoading ? <InlineLoading description="Loading sections…" /> : null}
354
+ {saving ? <InlineLoading description="Saving draft…" /> : null}
355
+ {compiling ? <InlineLoading description="Compiling report…" /> : null}
356
+
357
+ {!loading && !saving && saveError ? (
358
+ <InlineNotification lowContrast kind="error" title="Save failed" subtitle={saveError} />
359
+ ) : null}
360
+
361
+ {!loading && !saving && saveSuccess ? (
362
+ <InlineNotification lowContrast kind="success" title="Saved" subtitle={saveSuccess} />
363
+ ) : null}
364
+
365
+ {!loading && !compiling && compileError ? (
366
+ <InlineNotification lowContrast kind="error" title="Compile failed" subtitle={compileError} />
367
+ ) : null}
368
+
369
+ {!loading && !compiling && compileSuccess ? (
370
+ <InlineNotification lowContrast kind="success" title="Compiled" subtitle={compileSuccess} />
371
+ ) : null}
372
+ </div>
373
+
374
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
375
+ <AiAssistButton context={{ page: 'Report Builder' }} size="sm" kind="secondary" />
376
+
377
+ <Button size="sm" kind="secondary" renderIcon={Download} disabled>
378
+ Export
379
+ </Button>
380
+
381
+ <Button size="sm" kind="secondary" onClick={handleCompile} disabled={!canCompile}>
382
+ Compile
383
+ </Button>
384
+
385
+ <Button size="sm" kind="primary" renderIcon={Save} onClick={handleDraftSave} disabled={!canSave}>
386
+ Save Draft
387
+ </Button>
388
+ </div>
389
+ </div>
390
+
391
+ <Tabs
392
+ selectedIndex={tab === 'details' ? 0 : tab === 'definition' ? 1 : 2}
393
+ onChange={({ selectedIndex }) =>
394
+ setTab(selectedIndex === 0 ? 'details' : selectedIndex === 1 ? 'definition' : 'design')
395
+ }
396
+ >
397
+ <TabList aria-label="Report editor tabs">
398
+ <Tab>Report Detail</Tab>
399
+ <Tab>Report Definition</Tab>
400
+ <Tab>Report Design</Tab>
401
+ </TabList>
402
+
403
+ <TabPanels>
404
+ <TabPanel>
405
+ <ReportDetail
406
+ value={detailModel}
407
+ onChange={handleDetailChange}
408
+ onSaveDraft={handleDraftSave}
409
+ isSaving={saving}
410
+ onCancel={handleCancel}
411
+ />
412
+ </TabPanel>
413
+
414
+ <TabPanel>
415
+ <ReportDefinitionEditor
416
+ value={definitionDraft}
417
+ onChange={handleDefinitionChange}
418
+ />
419
+ </TabPanel>
420
+
421
+ <TabPanel>
422
+ <ReportDesignEditor
423
+ value={designDraft}
424
+ onChange={handleDesignChange}
425
+ definitionDraft={definitionDraft}
426
+ sectionSources={sectionSources}
427
+ sectionNameLookup={sectionNameLookup}
428
+ />
429
+ </TabPanel>
430
+ </TabPanels>
431
+ </Tabs>
432
+
433
+ <div
434
+ style={{
435
+ marginTop: '1rem',
436
+ padding: '0.9rem 1rem',
437
+ background: '#fff',
438
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
439
+ borderRadius: 12,
440
+ }}
441
+ >
442
+ <div style={{ fontWeight: 600, marginBottom: '0.5rem' }}>Current Report Form</div>
443
+
444
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: '0.85rem' }}>
445
+ {JSON.stringify(
446
+ {
447
+ uuid: form.uuid,
448
+ name: form.name,
449
+ description: form.description,
450
+ code: form.code,
451
+ definition: {
452
+ sections: form.sections,
453
+ },
454
+ design: form.design,
455
+ sectionSources,
456
+ mode,
457
+ savedReportUuid: savedReport?.uuid ?? null,
458
+ },
459
+ null,
460
+ 2,
461
+ )}
462
+ </pre>
463
+ </div>
464
+ </>
465
+ );
466
+ }
@@ -0,0 +1,30 @@
1
+
2
+ import React from 'react';
3
+ import { Routes, Route, Navigate } from 'react-router-dom';
4
+
5
+ import ReportBuilderShell from '../app-shell/report-builder-shell.component';
6
+ import ReportBuilderLandingPage from '../landing/report-builder-landing-page.component';
7
+ import ReportDashboardPage from './report-dashboard.page.component';
8
+ import ReportEditorPage from './report-editor.page.component';
9
+ import IndicatorsPage from '../indicators/indicators-page.component';
10
+ import SectionsPage from '../report-sections/report-sections-page.component';
11
+ import RunReportsPage from '../run-reports/run-reports-page.component';
12
+ import DataThemesPage from '../data-themes/data-themes-page.component';
13
+
14
+ const ReportRoutes: React.FC = () => (
15
+ <Routes>
16
+ <Route element={<ReportBuilderShell />}>
17
+ <Route path="/" element={<ReportBuilderLandingPage />} />
18
+ <Route path="/reports" element={<ReportDashboardPage />} />
19
+ <Route path="/new" element={<ReportEditorPage />} />
20
+ <Route path="/edit/:reportId" element={<ReportEditorPage />} />
21
+ <Route path="/indicators" element={<IndicatorsPage />} />
22
+ <Route path="/sections" element={<SectionsPage />} />
23
+ <Route path="/themes" element={<DataThemesPage />} />
24
+ <Route path="/run" element={<RunReportsPage />} />
25
+ <Route path="*" element={<Navigate to="/" replace />} />
26
+ </Route>
27
+ </Routes>
28
+ );
29
+
30
+ export default ReportRoutes;