@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,88 @@
1
+ import type { DataTheme } from '../../types/theme/data-theme.types';
2
+ import { omrsDelete, omrsGet, omrsPost } from '../openmrs-api';
3
+
4
+ const RESOURCE = '/reportbuilder/datatheme';
5
+
6
+ type RestList<T> = {
7
+ results?: T[];
8
+ } & Record<string, any>;
9
+
10
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
11
+ if (!data) return [];
12
+ if (Array.isArray(data)) return data;
13
+ return Array.isArray(data.results) ? data.results : [];
14
+ }
15
+
16
+ export async function listThemes(q?: string, signal?: AbortSignal): Promise<DataTheme[]> {
17
+ const trimmed = q?.trim();
18
+ const path = trimmed
19
+ ? `${RESOURCE}?q=${encodeURIComponent(trimmed)}&v=full`
20
+ : `${RESOURCE}?v=full`;
21
+
22
+ const data = await omrsGet<RestList<DataTheme> | DataTheme[] | undefined>(path, signal);
23
+ return unwrapRestList(data);
24
+ }
25
+
26
+ export async function getTheme(uuid: string, signal?: AbortSignal): Promise<DataTheme> {
27
+ return omrsGet<DataTheme>(`${RESOURCE}/${encodeURIComponent(uuid)}?v=full`, signal);
28
+ }
29
+
30
+ export async function createTheme(
31
+ payload: Partial<DataTheme>,
32
+ signal?: AbortSignal
33
+ ): Promise<DataTheme> {
34
+ return omrsPost<DataTheme>(RESOURCE, payload, signal);
35
+ }
36
+
37
+ export async function updateTheme(
38
+ uuid: string,
39
+ payload: Partial<DataTheme>,
40
+ signal?: AbortSignal
41
+ ): Promise<DataTheme> {
42
+ // RESTWS often uses POST for update
43
+ return omrsPost<DataTheme>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
44
+ }
45
+
46
+ export async function deleteTheme(
47
+ uuid: string,
48
+ purge = false,
49
+ reason = 'Retired via UI',
50
+ signal?: AbortSignal
51
+ ): Promise<void> {
52
+ const qs = purge
53
+ ? `purge=true`
54
+ : `purge=false&reason=${encodeURIComponent(reason)}`;
55
+
56
+ await omrsDelete<void>(`${RESOURCE}/${encodeURIComponent(uuid)}?${qs}`, signal);
57
+ }
58
+
59
+ export type DataThemeDto = {
60
+ uuid: string;
61
+ name: string;
62
+ description?: string;
63
+ code?: string;
64
+ domain?: string;
65
+ configJson?: string; // stringified JSON
66
+ metaJson?: string; // stringified JSON
67
+ retired?: boolean;
68
+ };
69
+
70
+ type ListResponse = { results: DataThemeDto[] } | DataThemeDto[];
71
+
72
+ function normalizeList(payload: ListResponse): DataThemeDto[] {
73
+ if (Array.isArray(payload)) return payload;
74
+ return payload?.results ?? [];
75
+ }
76
+
77
+ export async function listDataThemes(q?: string, signal?: AbortSignal): Promise<DataThemeDto[]> {
78
+ const trimmed = q?.trim();
79
+ const path = trimmed
80
+ ? `${RESOURCE}?q=${encodeURIComponent(trimmed)}&v=full`
81
+ : `${RESOURCE}?v=full`;
82
+ const data = await omrsGet<ListResponse>(path, signal);
83
+ return normalizeList(data);
84
+ }
85
+
86
+ export async function getDataTheme(uuid: string, signal?: AbortSignal): Promise<DataThemeDto> {
87
+ return omrsGet<DataThemeDto>(`${RESOURCE}/${uuid}?v=full`, signal);
88
+ }
@@ -0,0 +1,31 @@
1
+ import { omrsGet } from '../openmrs-api';
2
+
3
+ export type SchemaTable = { name: string; type?: string };
4
+
5
+ type ListTablesResponse =
6
+ | { results: SchemaTable[] }
7
+ | { tables: SchemaTable[] }
8
+ | SchemaTable[]
9
+ | string[];
10
+
11
+ function normalizeTables(payload: ListTablesResponse | null | undefined): SchemaTable[] {
12
+ if (!payload) return [];
13
+
14
+ if (Array.isArray(payload)) {
15
+ if (payload.length && typeof payload[0] === 'string') {
16
+ return (payload as string[]).map((name) => ({ name }));
17
+ }
18
+ return payload as SchemaTable[];
19
+ }
20
+
21
+ const anyPayload: any = payload;
22
+ if (Array.isArray(anyPayload?.results)) return anyPayload.results;
23
+ if (Array.isArray(anyPayload?.tables)) return anyPayload.tables;
24
+
25
+ return [];
26
+ }
27
+
28
+ export async function getSchemaTables(signal?: AbortSignal): Promise<SchemaTable[]> {
29
+ const data = await omrsGet<ListTablesResponse>('/reportbuilder/schema', signal);
30
+ return normalizeTables(data);
31
+ }
@@ -0,0 +1,45 @@
1
+ import { omrsGet } from '../openmrs-api';
2
+
3
+ export type TableColumn = {
4
+ name: string;
5
+ type?: string;
6
+ nullable?: boolean;
7
+ };
8
+
9
+ type RawColumn = {
10
+ columnName?: string;
11
+ dataType?: string;
12
+ name?: string; // in case backend changes later
13
+ type?: string;
14
+ nullable?: boolean;
15
+ };
16
+
17
+ type RawMetaResponse =
18
+ | { results?: RawColumn[] }
19
+ | { columns?: RawColumn[] }
20
+ | RawColumn[];
21
+
22
+ function normalizeColumns(input: RawColumn[] = []): TableColumn[] {
23
+ return input
24
+ .map((c) => ({
25
+ name: c.name ?? c.columnName ?? '',
26
+ type: c.type ?? c.dataType,
27
+ nullable: c.nullable,
28
+ }))
29
+ .filter((c) => c.name.length > 0);
30
+ }
31
+
32
+ export async function getMambaTableMeta(
33
+ table: string,
34
+ signal?: AbortSignal
35
+ ): Promise<TableColumn[]> {
36
+ const data = await omrsGet<RawMetaResponse>(
37
+ `/mambatablecolumn?table=${encodeURIComponent(table)}`,
38
+ signal
39
+ );
40
+
41
+ if (Array.isArray(data)) return normalizeColumns(data);
42
+
43
+ const anyData = data as any;
44
+ return normalizeColumns(anyData?.columns ?? anyData?.results ?? []);
45
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
3
+
4
+ import ReportBuilderRoutes from './routes/report-builder.routes';
5
+
6
+ const RootComponent: React.FC = () => {
7
+ return (
8
+ <BrowserRouter basename={`${window.spaBase}/report-builder`}>
9
+ <Routes>
10
+ {/* All report-builder routes live under the shell (left nav + content frame) */}
11
+ <Route path="/*" element={<ReportBuilderRoutes />} />
12
+
13
+ {/* Safety fallback */}
14
+ <Route path="*" element={<Navigate to="/" replace />} />
15
+ </Routes>
16
+ </BrowserRouter>
17
+ );
18
+ };
19
+
20
+ export default RootComponent;
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Routes, Route, Navigate } from 'react-router-dom';
3
+
4
+ import ReportBuilderShell from '../components/app-shell/report-builder-shell.component';
5
+ import ReportBuilderLandingPage from '../components/landing/report-builder-landing-page.component';
6
+
7
+ import ReportDashboardPage from '../components/report/report-dashboard.page.component';
8
+ import ReportEditorPage from '../components/report/report-editor.page.component';
9
+
10
+ import IndicatorsPage from '../components/indicators/indicators-page.component';
11
+ import SectionsPage from '../components/report-sections/report-sections-page.component';
12
+ import RunReportsPage from '../components/run-reports/run-reports-page.component';
13
+ import DataThemesPage from '../components/data-themes/data-themes-page.component';
14
+
15
+ import AdminPage from '../components/admin/admin-page.component';
16
+ import ReportCategoriesPage from '../components/admin/report-categories-page.component';
17
+ import AgeCategoriesPage from '../components/admin/age-categories-page.component';
18
+ import AgeGroupsPage from '../components/admin/age-groups-page.component';
19
+ import ReportLibraryPage from '../components/admin/report-library-page.component';
20
+ import ETLSourcesPage from '../components/admin/etl-sources-page.component';
21
+
22
+ const ReportBuilderRoutes: React.FC = () => {
23
+ return (
24
+ <Routes>
25
+ <Route element={<ReportBuilderShell />}>
26
+ {/* Home / landing */}
27
+ <Route path="/" element={<ReportBuilderLandingPage />} />
28
+
29
+ {/* Reports */}
30
+ <Route path="/reports" element={<ReportDashboardPage />} />
31
+
32
+ {/* Create / edit report */}
33
+ <Route path="/new" element={<ReportEditorPage />} />
34
+ <Route path="/edit/:reportId" element={<ReportEditorPage />} />
35
+
36
+ {/* Indicators / sections */}
37
+ <Route path="/indicators" element={<IndicatorsPage />} />
38
+ <Route path="/sections" element={<SectionsPage />} />
39
+
40
+ {/* Run */}
41
+ <Route path="/run" element={<RunReportsPage />} />
42
+
43
+
44
+ {/* Admin */}
45
+ <Route path="/admin" element={<AdminPage />} />
46
+ <Route path="/admin/report-categories" element={<ReportCategoriesPage />} />
47
+ <Route path="/admin/age-categories" element={<AgeCategoriesPage />} />
48
+ <Route path="/admin/age-groups" element={<AgeGroupsPage />} />
49
+ <Route path="/admin/report-library" element={<ReportLibraryPage />} />
50
+ <Route path="/admin/etl-sources" element={<ETLSourcesPage />} />
51
+
52
+ {/* Data themes now accessed under Admin */}
53
+ <Route path="/admin/themes" element={<DataThemesPage />} />
54
+
55
+ {/* Optional backward compatibility */}
56
+ <Route path="/themes" element={<Navigate to="/admin/themes" replace />} />
57
+
58
+
59
+ {/* Fallback */}
60
+ <Route path="*" element={<Navigate to="/" replace />} />
61
+ </Route>
62
+ </Routes>
63
+ );
64
+ };
65
+
66
+ export default ReportBuilderRoutes;
@@ -0,0 +1,56 @@
1
+ .page {
2
+ padding: 1rem;
3
+ height: 100vh;
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
7
+
8
+ .content {
9
+ margin-top: 1rem;
10
+ }
11
+
12
+ .designWorkspace {
13
+ padding-top: 1rem;
14
+ }
15
+
16
+ .workspaceTitle {
17
+ margin: 0 0 0.25rem 0;
18
+ font-weight: 600;
19
+ }
20
+
21
+ .workspaceHint {
22
+ margin: 0 0 1rem 0;
23
+ opacity: 0.75;
24
+ }
25
+
26
+ .designGrid {
27
+ display: grid;
28
+ grid-template-columns: 2fr 3fr;
29
+ gap: 1rem;
30
+ }
31
+
32
+ .designLeft,
33
+ .designRight {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 1rem;
37
+ min-width: 0;
38
+ }
39
+
40
+ .panel {
41
+ background: var(--cds-layer-01, #fff);
42
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
43
+ border-radius: 0.5rem;
44
+ padding: 1rem;
45
+ }
46
+
47
+ .panelTitle {
48
+ margin: 0 0 0.25rem 0;
49
+ font-weight: 600;
50
+ }
51
+
52
+ .panelHint {
53
+ margin: 0;
54
+ opacity: 0.75;
55
+ font-size: 0.875rem;
56
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "pages": [
4
+ {
5
+ "component": "root",
6
+ "route": "report-builder",
7
+ "online": true,
8
+ "offline": false,
9
+ "order": 1
10
+ }
11
+ ],
12
+ "extensions": [
13
+ {
14
+ "name": "system-administration-report-builder-card-link",
15
+ "slot": "system-admin-page-card-link-slot",
16
+ "component": "reportBuilderAdminLink",
17
+ "online": true,
18
+ "offline": false
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,121 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ export type DisaggregationDimension = {
4
+ name: string;
5
+ items: Array<{ id: string; label: string }>;
6
+ };
7
+
8
+ export type IndicatorNode = {
9
+ id: string;
10
+ label: string;
11
+ code?: string;
12
+ type?: 'group' | 'indicator';
13
+ children?: IndicatorNode[];
14
+ };
15
+
16
+ export type MappingGroup = {
17
+ id: string;
18
+ title: string;
19
+ keyPattern?: string;
20
+ dims?: Record<string, string>;
21
+ indicatorTree: IndicatorNode[];
22
+ };
23
+
24
+ export type TemplateModel = {
25
+ version: number;
26
+ title: string;
27
+ dimensions: Record<string, Array<{ id: string; label: string }>>;
28
+ mapping: {
29
+ arrayName: string;
30
+ defaultValue: number;
31
+ groups: MappingGroup[];
32
+ };
33
+ };
34
+
35
+ export function buildDefaultTemplate(): TemplateModel {
36
+ const age = [
37
+ { id: '0-28d', label: '0-28d' },
38
+ { id: '29d-4y', label: '29d-4y' },
39
+ { id: '5-9y', label: '5-9y' },
40
+ { id: '10-19y', label: '10-19y' },
41
+ { id: '20yrs+', label: '20yrs+' },
42
+ ];
43
+
44
+ const sex = [
45
+ { id: 'M', label: 'M' },
46
+ { id: 'F', label: 'F' },
47
+ ];
48
+
49
+ const makeGroup = (title: string, indicatorTree: IndicatorNode[]): MappingGroup => ({
50
+ id: nanoid(),
51
+ title,
52
+ keyPattern: '{code}_{age}_{sex}',
53
+ dims: { age: 'age', sex: 'sex' },
54
+ indicatorTree,
55
+ });
56
+
57
+ return {
58
+ version: 4,
59
+ title: '1.0 OPD ATTENDANCES, REFERRALS AND DIAGNOSES (Sample)',
60
+ dimensions: { age, sex },
61
+ mapping: {
62
+ arrayName: 'dataValues',
63
+ defaultValue: 0,
64
+ groups: [
65
+ makeGroup('1.1 OUTPATIENT ATTENDANCE', [
66
+ { id: nanoid(), label: 'OA01. New attendance', code: 'OA01' },
67
+ { id: nanoid(), label: 'OA02. Reattendance', code: 'OA02' },
68
+ ]),
69
+ makeGroup('1.2 OUTPATIENT REFERRALS', [
70
+ { id: nanoid(), label: 'OR01. Referrals to unit', code: 'OR01' },
71
+ { id: nanoid(), label: 'OR02. Referrals from unit', code: 'OR02' },
72
+ ]),
73
+ makeGroup('1.3.1 Epidemic-Prone Diseases', [
74
+ {
75
+ id: nanoid(),
76
+ label: 'EP01. Malaria',
77
+ code: 'EP01',
78
+ type: 'group',
79
+ children: [
80
+ { id: nanoid(), label: 'EP01a. Suspected Malaria (fever)', code: 'EP01a' },
81
+ { id: nanoid(), label: 'EP01b. Malaria Tested (B/s & RDT )', code: 'EP01b' },
82
+ { id: nanoid(), label: 'EP01c. Malaria confirmed (B/s & RDT)', code: 'EP01c' },
83
+ { id: nanoid(), label: 'EP01d. Confirmed Malaria cases treated', code: 'EP01d' },
84
+ { id: nanoid(), label: 'EP01e. Total malaria cases treated', code: 'EP01e' },
85
+ ],
86
+ },
87
+ { id: nanoid(), label: 'EP02. Acute Flaccid Paralysis', code: 'EP02' },
88
+ { id: nanoid(), label: 'EP03. Animal Bites (suspected rabies)', code: 'EP03' },
89
+ { id: nanoid(), label: 'EP04. Cholera', code: 'EP04' },
90
+ { id: nanoid(), label: 'EP05. Dysentery', code: 'EP05' },
91
+ { id: nanoid(), label: 'EP06. Guinea Worm', code: 'EP06' },
92
+ { id: nanoid(), label: 'EP07. Measles', code: 'EP07' },
93
+ { id: nanoid(), label: 'EP08. Bacterial Meningitis', code: 'EP08' },
94
+ { id: nanoid(), label: 'EP09. Neonatal tetanus', code: 'EP09' },
95
+ { id: nanoid(), label: 'EP10. Plague', code: 'EP10' },
96
+ { id: nanoid(), label: 'EP11. Yellow Fever', code: 'EP11' },
97
+ { id: nanoid(), label: 'EP12. Other Viral Haemorrhagic Fevers', code: 'EP12' },
98
+ { id: nanoid(), label: 'EP13. Severe Acute Respiratory Infection (SARI)', code: 'EP13' },
99
+ {
100
+ id: nanoid(),
101
+ label: 'EP14. Adverse Events Following Immunization (AEFI)',
102
+ code: 'EP14',
103
+ type: 'group',
104
+ children: [
105
+ { id: nanoid(), label: 'Serious', code: 'EP14_Serious' },
106
+ { id: nanoid(), label: 'Non Serious', code: 'EP14_NonSerious' },
107
+ ],
108
+ },
109
+ { id: nanoid(), label: 'EP15. Typhoid Fever', code: 'EP15' },
110
+ { id: nanoid(), label: 'EP16. Refampicin resistant TB cases', code: 'EP16' },
111
+ {
112
+ id: nanoid(),
113
+ label: 'EP17. Other Emerging infectious Diseases e.g. Influenza like illness (ILI), SARS',
114
+ code: 'EP17',
115
+ },
116
+ { id: nanoid(), label: 'EP18. Covid-19', code: 'EP18' },
117
+ ]),
118
+ ],
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,110 @@
1
+ import type { IndicatorNode, MappingGroup, TemplateModel } from './sample-template';
2
+
3
+ export function isGroupNode(n: IndicatorNode): boolean {
4
+ return Boolean(n.children && n.children.length);
5
+ }
6
+
7
+ export function collectLeafIndicatorCodes(nodes: IndicatorNode[]): string[] {
8
+ const out: string[] = [];
9
+ const walk = (arr: IndicatorNode[]) => {
10
+ for (const n of arr) {
11
+ if (n.children && n.children.length) {
12
+ walk(n.children);
13
+ } else if (n.code) {
14
+ out.push(n.code);
15
+ }
16
+ }
17
+ };
18
+ walk(nodes);
19
+ return out;
20
+ }
21
+
22
+ /**
23
+ * ✅ EXISTING in your repo (keep it)
24
+ * Converts the in-app model into the JSON template format.
25
+ */
26
+ export function toJsonTemplate(tpl: TemplateModel) {
27
+ return {
28
+ version: tpl.version,
29
+ title: tpl.title,
30
+ dimensions: tpl.dimensions,
31
+ mapping: {
32
+ arrayName: tpl.mapping.arrayName,
33
+ defaultValue: tpl.mapping.defaultValue,
34
+ groups: tpl.mapping.groups.map((g: MappingGroup) => ({
35
+ id: g.id,
36
+ title: g.title,
37
+ keyPattern: g.keyPattern ?? '{code}_{age}_{sex}',
38
+ dims: g.dims ?? { age: 'age', sex: 'sex' },
39
+ indicatorTree: g.indicatorTree,
40
+ indicatorCodes: collectLeafIndicatorCodes(g.indicatorTree),
41
+ })),
42
+ },
43
+ };
44
+ }
45
+
46
+ /**
47
+ * ✅ NEW
48
+ * Convert JSON template back into TemplateModel.
49
+ * We overlay onto defaultsBase so any missing fields are filled from defaults.
50
+ */
51
+ export function fromJsonTemplate(json: any, defaultsBase: TemplateModel): TemplateModel {
52
+ if (!json || typeof json !== 'object') {
53
+ throw new Error('JSON template must be an object');
54
+ }
55
+
56
+ const model = structuredClone(defaultsBase);
57
+
58
+ // Validate & map groups (this matches your toJsonTemplate output)
59
+ const incomingGroups = json?.mapping?.groups;
60
+ if (!Array.isArray(incomingGroups)) {
61
+ throw new Error('Invalid JSON: expected mapping.groups to be an array');
62
+ }
63
+
64
+ // Overlay groups onto existing defaults by id when possible
65
+ model.mapping.groups = incomingGroups.map((g: any) => {
66
+ if (!g?.id) throw new Error('Invalid JSON: group missing id');
67
+
68
+ return {
69
+ // keep defaults if present, but overlay JSON
70
+ id: String(g.id),
71
+ title: String(g.title ?? 'Group'),
72
+ keyPattern: g.keyPattern ? String(g.keyPattern) : '{code}_{age}_{sex}',
73
+ dims: g.dims ?? { age: 'age', sex: 'sex' },
74
+
75
+ // ✅ the important part
76
+ indicatorTree: Array.isArray(g.indicatorTree) ? g.indicatorTree.map(normalizeNode) : [],
77
+ } as MappingGroup;
78
+ });
79
+
80
+ // Optional overlays (safe)
81
+ if (typeof json.version === 'string') model.version = json.version;
82
+ if (typeof json.title === 'string') model.title = json.title;
83
+ if (json.dimensions && typeof json.dimensions === 'object') model.dimensions = json.dimensions;
84
+
85
+ if (json?.mapping?.arrayName) model.mapping.arrayName = String(json.mapping.arrayName);
86
+ if (json?.mapping?.defaultValue != null) model.mapping.defaultValue = json.mapping.defaultValue;
87
+
88
+ return model;
89
+ }
90
+
91
+ function normalizeNode(n: any): IndicatorNode {
92
+ if (!n || typeof n !== 'object') {
93
+ throw new Error('Invalid node: node must be an object');
94
+ }
95
+ if (!n.id) {
96
+ throw new Error('Invalid node: missing id');
97
+ }
98
+
99
+ const node: IndicatorNode = {
100
+ id: String(n.id),
101
+ label: String(n.label ?? ''),
102
+ code: n.code != null ? String(n.code) : '',
103
+ };
104
+
105
+ if (Array.isArray(n.children) && n.children.length) {
106
+ node.children = n.children.map(normalizeNode);
107
+ }
108
+
109
+ return node;
110
+ }
@@ -0,0 +1,13 @@
1
+ // Workaround for TS type mismatch between Carbon SideNavProps and the `inert` prop
2
+ // used by @openmrs/esm-styleguide's LeftNavMenu.
3
+ // This avoids type-check failures during local builds.
4
+
5
+ import '@carbon/react';
6
+
7
+ declare module '@carbon/react' {
8
+ // `inert` is a valid HTML attribute supported by browsers, but may be missing from
9
+ // the Carbon SideNavProps typing in some versions.
10
+ interface SideNavProps {
11
+ inert?: boolean;
12
+ }
13
+ }
@@ -0,0 +1,49 @@
1
+ // src/report-builder/types/condition-operators.ts
2
+
3
+ /**
4
+ * Central source of truth for operators used across:
5
+ * - Theme definition UI
6
+ * - Indicator condition saving
7
+ * - SQL rendering
8
+ */
9
+
10
+ /** Operators shown/allowed in Theme editor UI */
11
+ export const THEME_OPERATOR_OPTIONS = ['EQUALS', 'IN', 'NOT_IN', 'LIKE', 'BETWEEN', 'GTE', 'LTE'] as const;
12
+ export type ThemeOperator = (typeof THEME_OPERATOR_OPTIONS)[number];
13
+
14
+ /** SQL operators after normalization */
15
+ export type NormalizedSqlOperator =
16
+ | '='
17
+ | '!='
18
+ | '>'
19
+ | '>='
20
+ | '<'
21
+ | '<='
22
+ | 'IN'
23
+ | 'NOT IN'
24
+ | 'LIKE'
25
+ | 'BETWEEN';
26
+
27
+ export function normalizeOperator(op?: string | null): NormalizedSqlOperator {
28
+ const raw = String(op ?? '').trim().toUpperCase();
29
+
30
+ if (raw === 'EQUALS') return '=';
31
+ if (raw === 'GTE') return '>=';
32
+ if (raw === 'LTE') return '<=';
33
+ if (raw === 'NOT_IN') return 'NOT IN';
34
+ if (raw === 'IN') return 'IN';
35
+ if (raw === 'LIKE') return 'LIKE';
36
+ if (raw === 'BETWEEN') return 'BETWEEN';
37
+
38
+ // allow already-normalized SQL ops
39
+ if (raw === '=' || raw === '!=' || raw === '>' || raw === '>=' || raw === '<' || raw === '<=') {
40
+ return raw as NormalizedSqlOperator;
41
+ }
42
+
43
+ // safest default
44
+ return 'IN';
45
+ }
46
+
47
+ export function isInOperator(op: NormalizedSqlOperator) {
48
+ return op === 'IN' || op === 'NOT IN';
49
+ }