@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,313 @@
1
+ import React from 'react';
2
+ import {
3
+ Button,
4
+ Search,
5
+ DataTable,
6
+ Table,
7
+ TableHead,
8
+ TableRow,
9
+ TableHeader,
10
+ TableBody,
11
+ TableCell,
12
+ OverflowMenu,
13
+ OverflowMenuItem,
14
+ Tag,
15
+ } from '@carbon/react';
16
+ import { Add, Folder } from '@carbon/icons-react';
17
+ import { useTranslation } from 'react-i18next';
18
+
19
+ import Header from '../shared/header/header.component';
20
+ import CreateSectionModal from './create-report-section-modal.component';
21
+ import type { CreateSectionPayload, SectionIndicatorRef } from './section-types';
22
+ import SectionPreviewModal from './report-section-preview-modal.component';
23
+ import styles from './sections-page.scss';
24
+
25
+ import { useLocation } from 'react-router-dom';
26
+
27
+ import { listIndicators } from '../../resources/indicator/indicators.api';
28
+ import { createSection, getSection, listSections, updateSection, type ReportSectionDto } from '../../resources/report-section/report-sections.api';
29
+
30
+ function countIndicatorsFromConfig(section: ReportSectionDto): number {
31
+ try {
32
+ if (!section?.configJson) return 0;
33
+ const obj = JSON.parse(section.configJson);
34
+ const list = Array.isArray(obj?.indicators) ? obj.indicators : [];
35
+ return list.length;
36
+ } catch {
37
+ return 0;
38
+ }
39
+ }
40
+
41
+ const SectionsPage: React.FC = () => {
42
+ const { t } = useTranslation();
43
+
44
+ const [q, setQ] = React.useState('');
45
+ const [openCreate, setOpenCreate] = React.useState(false);
46
+
47
+ const [sections, setSections] = React.useState<ReportSectionDto[]>([]);
48
+ const [, setSectionsLoading] = React.useState(false);
49
+ const [sectionsError, setSectionsError] = React.useState<string | null>(null);
50
+
51
+ const [allIndicators, setAllIndicators] = React.useState<SectionIndicatorRef[]>([]);
52
+ const [, setIndicatorsLoading] = React.useState(false);
53
+ const [indicatorsError, setIndicatorsError] = React.useState<string | null>(null);
54
+
55
+ const [previewOpen, setPreviewOpen] = React.useState(false);
56
+ const [previewSection, setPreviewSection] = React.useState<ReportSectionDto | null>(null);
57
+
58
+ // NEW: edit state
59
+ const [editOpen, setEditOpen] = React.useState(false);
60
+ const [editSection, setEditSection] = React.useState<ReportSectionDto | null>(null);
61
+
62
+ const filtered = React.useMemo(() => {
63
+ const s = q.trim().toLowerCase();
64
+ return sections
65
+ .filter((x) => !x.retired)
66
+ .filter((x) => !s || (x.name ?? '').toLowerCase().includes(s) || (x.code ?? '').toLowerCase().includes(s));
67
+ }, [q, sections]);
68
+
69
+ const headers = React.useMemo(
70
+ () => [
71
+ { key: 'name', header: t('name', 'Name') },
72
+ { key: 'indicators', header: t('indicators', 'Indicators') },
73
+ { key: 'actions', header: t('actions', 'Actions') },
74
+ ],
75
+ [t],
76
+ );
77
+
78
+ const rows = React.useMemo(
79
+ () =>
80
+ filtered.map((x) => ({
81
+ id: x.uuid,
82
+ name: x.name,
83
+ indicators: String(countIndicatorsFromConfig(x)),
84
+ actions: '',
85
+ })),
86
+ [filtered],
87
+ );
88
+
89
+ const refreshSections = React.useCallback(async () => {
90
+ const ac = new AbortController();
91
+ setSectionsLoading(true);
92
+ setSectionsError(null);
93
+ try {
94
+ const data = await listSections({ v: 'default' }, ac.signal);
95
+ setSections(data);
96
+ } catch (e: any) {
97
+ setSectionsError(e?.message ?? 'Failed to load sections');
98
+ } finally {
99
+ setSectionsLoading(false);
100
+ }
101
+ return () => ac.abort();
102
+ }, []);
103
+
104
+ React.useEffect(() => {
105
+ refreshSections();
106
+ }, [refreshSections]);
107
+
108
+ React.useEffect(() => {
109
+ const ac = new AbortController();
110
+ setIndicatorsLoading(true);
111
+ setIndicatorsError(null);
112
+ listIndicators({ v: 'default', includeRetired: false }, ac.signal)
113
+ .then((inds) => {
114
+ setAllIndicators(
115
+ inds.map((i) => ({
116
+ id: i.uuid,
117
+ type: i.kind,
118
+ code: i.code ?? '',
119
+ name: i.name,
120
+ })),
121
+ );
122
+ })
123
+ .catch((e: any) => setIndicatorsError(e?.message ?? 'Failed to load indicators'))
124
+ .finally(() => setIndicatorsLoading(false));
125
+ return () => ac.abort();
126
+ }, []);
127
+
128
+ const onSaveSection = async (payload: CreateSectionPayload) => {
129
+ try {
130
+ if (payload.id) {
131
+ await updateSection(payload.id, {
132
+ name: payload.name,
133
+ description: payload.description,
134
+ configJson: payload.configJson,
135
+ });
136
+ setEditOpen(false);
137
+ setEditSection(null);
138
+ } else {
139
+ await createSection({
140
+ name: payload.name,
141
+ description: payload.description,
142
+ code: payload.name?.trim?.() ? payload.name.trim().toUpperCase().replace(/[^A-Z0-9]+/g, '_').slice(0, 60) : undefined,
143
+ configJson: payload.configJson,
144
+ });
145
+ setOpenCreate(false);
146
+ }
147
+ await refreshSections();
148
+ } catch (e: any) {
149
+ // eslint-disable-next-line no-console
150
+ console.error('Failed to save section', e);
151
+ }
152
+ };
153
+
154
+ const openPreview = async (uuid: string) => {
155
+ try {
156
+ const sec = await getSection(uuid, undefined, 'full');
157
+ setPreviewSection(sec);
158
+ setPreviewOpen(true);
159
+ } catch (e: any) {
160
+ // eslint-disable-next-line no-console
161
+ console.error('Failed to load section', e);
162
+ }
163
+ };
164
+
165
+ const openEdit = async (uuid: string) => {
166
+ try {
167
+ const sec = await getSection(uuid, undefined, 'full');
168
+ setEditSection(sec);
169
+ setEditOpen(true);
170
+ } catch (e: any) {
171
+ // eslint-disable-next-line no-console
172
+ console.error('Failed to load section for edit', e);
173
+ }
174
+ };
175
+
176
+ const location = useLocation();
177
+
178
+ React.useEffect(() => {
179
+ const params = new URLSearchParams(location.search);
180
+ if (params.get('create') === '1') {
181
+ setOpenCreate(true);
182
+ }
183
+ }, [location.search]);
184
+
185
+ return (
186
+ <div className={styles.page}>
187
+ <Header
188
+ title={t('reportSection', 'Manage Report Sections')}
189
+ subtitle={t('sectionsSubtitle', 'Draft • Define sections and reuse them in reports')}
190
+ status={{ label: t('draft', 'Draft'), kind: 'warning' }}
191
+ />
192
+
193
+ <div className={styles.sectionsPage}>
194
+ <div className={styles.headerRow}>
195
+ <h3 className={styles.title}>{t('sections', 'Sections')}</h3>
196
+ <Button size="sm" kind="primary" renderIcon={Add} onClick={() => setOpenCreate(true)}>
197
+ {t('createSection', 'Create Section')}
198
+ </Button>
199
+ </div>
200
+
201
+ <div className={styles.surface}>
202
+ <div className={styles.toolbarRow}>
203
+ <Search
204
+ size="lg"
205
+ labelText={t('search', 'Search')}
206
+ placeholder={t('search', 'Search')}
207
+ value={q}
208
+ onChange={(e) => setQ((e.target as HTMLInputElement).value)}
209
+ />
210
+ </div>
211
+
212
+ {sectionsError ? <div style={{ padding: '0.75rem' }}>{sectionsError}</div> : null}
213
+ {indicatorsError ? <div style={{ padding: '0.75rem' }}>{indicatorsError}</div> : null}
214
+
215
+ <DataTable rows={rows} headers={headers} size="lg" useZebraStyles>
216
+ {({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => (
217
+ <Table {...getTableProps()}>
218
+ <TableHead>
219
+ <TableRow>
220
+ <TableHeader key="_hash" style={{ width: '3rem' }} />
221
+ {headers.map((h) => (
222
+ <TableHeader key={h.key} {...getHeaderProps({ header: h })}>
223
+ {h.header}
224
+ </TableHeader>
225
+ ))}
226
+ </TableRow>
227
+ </TableHead>
228
+
229
+ <TableBody>
230
+ {rows.map((row) => (
231
+ <TableRow key={row.id} data-testid={`section-row-${String(filtered.find((x) => x.uuid === row.id)?.name ?? row.id).replace(/\s+/g, '-')}`} {...getRowProps({ row })}>
232
+ <TableCell>
233
+ <Folder size={16} />
234
+ </TableCell>
235
+
236
+ {row.cells.map((cell) => {
237
+ if (cell.info.header === 'actions') {
238
+ return (
239
+ <TableCell key={cell.id}>
240
+ <OverflowMenu data-testid={`section-actions-${row.id}`} size="sm" ariaLabel="Actions" flipped>
241
+ <OverflowMenuItem itemText="Preview" onClick={() => openPreview(row.id)} />
242
+ <OverflowMenuItem itemText="Edit" onClick={() => openEdit(row.id)} />
243
+ </OverflowMenu>
244
+ </TableCell>
245
+ );
246
+ }
247
+
248
+ if (cell.info.header === 'name') {
249
+ return (
250
+ <TableCell key={cell.id}>
251
+ <div style={{ fontWeight: 600 }}>{cell.value}</div>
252
+ </TableCell>
253
+ );
254
+ }
255
+
256
+ if (cell.info.header === 'indicators') {
257
+ return (
258
+ <TableCell key={cell.id}>
259
+ <Tag type="gray" size="sm">
260
+ {cell.value}
261
+ </Tag>
262
+ </TableCell>
263
+ );
264
+ }
265
+
266
+ return <TableCell key={cell.id}>{cell.value as any}</TableCell>;
267
+ })}
268
+ </TableRow>
269
+ ))}
270
+ </TableBody>
271
+ </Table>
272
+ )}
273
+ </DataTable>
274
+ </div>
275
+ </div>
276
+
277
+ {/* Create */}
278
+ <CreateSectionModal
279
+ open={openCreate}
280
+ onClose={() => setOpenCreate(false)}
281
+ onSubmit={onSaveSection}
282
+ indicators={allIndicators}
283
+ mode="create"
284
+ initialSection={null}
285
+ />
286
+
287
+ {/* Edit */}
288
+ <CreateSectionModal
289
+ open={editOpen}
290
+ onClose={() => {
291
+ setEditOpen(false);
292
+ setEditSection(null);
293
+ }}
294
+ onSubmit={onSaveSection}
295
+ indicators={allIndicators}
296
+ mode="edit"
297
+ initialSection={editSection}
298
+ />
299
+
300
+ {/* Preview */}
301
+ <SectionPreviewModal
302
+ open={previewOpen}
303
+ onClose={() => {
304
+ setPreviewOpen(false);
305
+ setPreviewSection(null);
306
+ }}
307
+ section={previewSection}
308
+ />
309
+ </div>
310
+ );
311
+ };
312
+
313
+ export default SectionsPage;
@@ -0,0 +1,55 @@
1
+ import type { ReportSectionDto } from '../../resources/report-section/report-sections.api';
2
+
3
+ export type SectionIndicatorType = 'BASE' | 'COMPOSITE' | 'FINAL';
4
+
5
+ export type SectionIndicatorRef = {
6
+ id: string;
7
+ type: SectionIndicatorType;
8
+ code: string;
9
+ name: string;
10
+ };
11
+
12
+ export type CreateSectionPayload = {
13
+ id?: string; // section uuid when editing
14
+ name: string;
15
+ description?: string;
16
+
17
+ disaggregationEnabled: boolean;
18
+ ageCategoryUuid: string | null;
19
+ ageCategoryCode?: string | null;
20
+ genders: Array<'F' | 'M'>;
21
+
22
+ indicators: Array<{ id: string; type: SectionIndicatorType; sortOrder: number; finalDisagg?: any }>;
23
+
24
+ /**
25
+ * JSON config that includes compiled SQL + strategy metadata.
26
+ * This is the payload you can persist as-is on the backend.
27
+ */
28
+ configJson: string;
29
+ };
30
+
31
+ export type Dhis2MappingV1 = {
32
+ version: 1;
33
+ enabled: boolean;
34
+ datasetId?: string;
35
+ periodType?: string;
36
+ orgUnitStrategy?: 'location' | 'fixed';
37
+ mappingMode?: 'dataElement-categoryOptionCombo';
38
+ indicatorMappings: Array<{
39
+ indicatorUuid: string;
40
+ dataElementId?: string;
41
+ categoryOptionComboByDisagg?: Record<string, string>;
42
+ }>;
43
+ };
44
+
45
+ export type ReportSectionEditorMode = 'create' | 'edit';
46
+
47
+ export type ReportSectionEditorProps = {
48
+ open: boolean;
49
+ onClose: () => void;
50
+ onSubmit: (payload: CreateSectionPayload) => void;
51
+ indicators: SectionIndicatorRef[];
52
+
53
+ mode?: ReportSectionEditorMode;
54
+ initialSection?: ReportSectionDto | null;
55
+ };
@@ -0,0 +1,46 @@
1
+ import type { IndicatorDto } from '../../resources/indicator/indicators.api';
2
+ import type { FinalIndicatorAuthoringV1 } from '../indicators/utils/final-indicator-sql.utils';
3
+
4
+ export function safeTrim(s: any) {
5
+ const v = (s ?? '').toString().trim();
6
+ return v.length ? v : '';
7
+ }
8
+
9
+ export function makeDisaggKey(ageGroupLabel: string, gender: string) {
10
+ return `${ageGroupLabel}__${gender}`;
11
+ }
12
+
13
+ export function safeParseJson(s?: string) {
14
+ try {
15
+ return s ? JSON.parse(s) : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function normalizeCompiledSql(sql: string) {
22
+ if (!sql) return sql;
23
+
24
+ let out = sql
25
+ .replace(/&amp;/g, '&')
26
+ .replace(/&lt;/g, '<')
27
+ .replace(/&gt;/g, '>')
28
+ .replace(/&quot;/g, '"')
29
+ .replace(/&#39;/g, "'");
30
+
31
+ // convert quoted placeholders into named params
32
+ out = out.replace(/':startDate'/g, ':startDate').replace(/':endDate'/g, ':endDate');
33
+
34
+ return out;
35
+ }
36
+
37
+ export function parseFinalAuthoring(ind: IndicatorDto): FinalIndicatorAuthoringV1 | null {
38
+ try {
39
+ if (!ind?.configJson) return null;
40
+ const obj = JSON.parse(ind.configJson);
41
+ if (obj && obj.version === 1 && (obj.ageCategoryCode || obj.ageGroupSetCode)) return obj as FinalIndicatorAuthoringV1;
42
+ return null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
@@ -0,0 +1,79 @@
1
+ .page {
2
+ width: 100%;
3
+ margin: 0 auto;
4
+ padding: 1.5rem 0 2rem;
5
+ }
6
+
7
+ .sectionsPage {
8
+ width: 100%;
9
+ }
10
+
11
+ .headerRow {
12
+ display: flex;
13
+ align-items: flex-end;
14
+ justify-content: space-between;
15
+ gap: 1rem;
16
+
17
+ padding: 1rem 1.5rem;
18
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
19
+ background: var(--cds-layer, #ffffff);
20
+ }
21
+
22
+ .title {
23
+ font-size: 1.5rem;
24
+ font-weight: 600;
25
+ line-height: 1.2;
26
+ margin: 0;
27
+ }
28
+
29
+ .surface {
30
+ margin: 0 1.5rem 1.5rem;
31
+ background: var(--cds-layer, #ffffff);
32
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
33
+ border-radius: 0.25rem;
34
+ overflow: hidden;
35
+ }
36
+
37
+ .toolbarRow {
38
+ padding: 0.75rem;
39
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
40
+ }
41
+
42
+ .footerRow {
43
+ padding: 0.75rem;
44
+ border-top: 1px solid var(--cds-border-subtle, #e0e0e0);
45
+ background: var(--cds-layer-accent, #f4f4f4);
46
+ }
47
+
48
+ .footerText {
49
+ opacity: 0.85;
50
+ }
51
+
52
+ .rowIconWrap {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 0.5rem;
56
+ }
57
+
58
+ .rowIndexTag {
59
+ margin-left: 0.25rem;
60
+ }
61
+
62
+ .nameCell {
63
+ display: flex;
64
+ align-items: center;
65
+ }
66
+
67
+ .nameText {
68
+ font-weight: 600;
69
+ }
70
+
71
+ .countCell {
72
+ font-weight: 600;
73
+ opacity: 0.85;
74
+ }
75
+
76
+ .actionsCell {
77
+ display: flex;
78
+ justify-content: flex-end;
79
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+
4
+ import Header from '../shared/header/header.component';
5
+
6
+ const RunReportsPage: React.FC = () => {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <div>
11
+ <Header
12
+ title={t('runReports', 'Run Reports')}
13
+ subtitle={t('runReportsHint', 'Choose a report and run it for specific time periods and locations.')}
14
+ />
15
+
16
+ <div style={{ padding: '1rem' }}>
17
+ {t('runReportsComingSoon', 'Coming soon: run report wizard, filters, and results viewer.')}
18
+ </div>
19
+ </div>
20
+ );
21
+ };
22
+
23
+ export default RunReportsPage;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import {Button, ButtonSet} from '@carbon/react';
3
+ import {Download, Save, View} from '@carbon/icons-react';
4
+ import {useTranslation} from 'react-i18next';
5
+
6
+ type Props = {
7
+ onSave?: () => void;
8
+ onPreview?: () => void;
9
+ onExport?: () => void;
10
+ saving?: boolean;
11
+ disabled?: boolean;
12
+ };
13
+
14
+ export default function BuilderHeaderActions({
15
+ onSave,
16
+ onPreview,
17
+ onExport,
18
+ saving = false,
19
+ disabled = false,
20
+ }: Props) {
21
+ const {t} = useTranslation();
22
+
23
+ return (
24
+ <ButtonSet>
25
+ <Button kind="secondary" size="md" renderIcon={Save} onClick={onSave} disabled={disabled || saving}>
26
+ {saving ? t('saving', 'Saving…') : t('save', 'Save')}
27
+ </Button>
28
+
29
+ <Button kind="secondary" size="md" renderIcon={View} onClick={onPreview} disabled={disabled}>
30
+ {t('preview', 'Preview')}
31
+ </Button>
32
+
33
+ <Button kind="primary" size="md" renderIcon={Download} onClick={onExport} disabled={disabled}>
34
+ {t('export', 'Export')}
35
+ </Button>
36
+ </ButtonSet>
37
+ );
38
+ }
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar, ChevronDown, Location, UserFollow } from '@carbon/react/icons';
4
+ import { formatDate, useSession } from '@openmrs/esm-framework';
5
+
6
+ import Illustration from './illustration.component';
7
+ import styles from './header.scss';
8
+ import AiAssistButton from '../../ai-support/ai-assist-button.component';
9
+
10
+ export interface HeaderStatus {
11
+ label: string;
12
+ kind?: 'neutral' | 'info' | 'success' | 'warning'; // kept for future, not styled as pill in this design
13
+ }
14
+
15
+ export interface HeaderProps {
16
+ title: string;
17
+ subtitle?: string;
18
+ status?: HeaderStatus;
19
+ actions?: React.ReactNode;
20
+
21
+ showAiAssist?: boolean; // NEW
22
+ }
23
+
24
+ const Header: React.FC<HeaderProps> = ({ title, subtitle, status, actions, showAiAssist = true }) => {
25
+ const { t } = useTranslation();
26
+ const session = useSession();
27
+ const location = session?.sessionLocation?.display;
28
+
29
+ const metaLeft = status?.label;
30
+ const metaRight = subtitle;
31
+
32
+ return (
33
+ <div className={styles.header}>
34
+ {/* LEFT */}
35
+ <div className={styles.left}>
36
+ <div className={styles.illustrationWrap}>
37
+ <Illustration />
38
+ </div>
39
+
40
+ <div className={styles.pageLabels}>
41
+ <div className={styles.appName}>{t('reportBuilder', 'Report builder')}</div>
42
+ <div className={styles.pageTitle}>{title}</div>
43
+
44
+ {(metaLeft || metaRight) && (
45
+ <div className={styles.metaLine}>
46
+ {metaLeft ? <span className={styles.metaItem}>{metaLeft}</span> : null}
47
+ {metaLeft && metaRight ? <span className={styles.metaDot}>•</span> : null}
48
+ {metaRight ? <span className={styles.metaItem}>{metaRight}</span> : null}
49
+ </div>
50
+ )}
51
+ </div>
52
+ </div>
53
+
54
+ {/* RIGHT */}
55
+ <div className={styles.right}>
56
+ <div className={styles.rightTop}>
57
+ <div className={styles.userRow}>
58
+ <span className={styles.userName}>{session?.user?.person?.display}</span>
59
+ <UserFollow size={18} className={styles.userIcon} />
60
+ </div>
61
+
62
+ <div className={styles.contextRow}>
63
+ <Location size={16} />
64
+ <span className={styles.value}>{location}</span>
65
+ <span className={styles.metaDot}>•</span>
66
+ <Calendar size={16} />
67
+ <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
68
+ <ChevronDown size={16} className={styles.chevron} />
69
+ </div>
70
+ </div>
71
+
72
+ {showAiAssist || actions ? (
73
+ <div className={styles.actions}>
74
+ {showAiAssist ? <AiAssistButton context={{ page: title }} /> : null}
75
+ {actions}
76
+ </div>
77
+ ) : null}
78
+ </div>
79
+
80
+
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default Header;