@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,296 @@
1
+ import React, { useRef } from 'react';
2
+ import { Button, Tile, TreeView, TreeNode } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { nanoid } from 'nanoid';
5
+
6
+ import styles from './template-structure-panel.scss';
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 SelectedNode = {
17
+ id: string;
18
+ label?: string;
19
+ code?: string;
20
+ hasChildren?: boolean;
21
+ };
22
+
23
+ type Props = {
24
+ tree: IndicatorNode[];
25
+ selected?: SelectedNode | null;
26
+ onSelectNode: (node: IndicatorNode) => void;
27
+ onChangeTree: (updater: (prevTree: IndicatorNode[]) => IndicatorNode[]) => void;
28
+
29
+ // ✅ NEW: refresh JSON from template draft
30
+ onRefreshJson: () => void;
31
+ };
32
+
33
+ type IndexPath = number[];
34
+
35
+ function findIndexPath(nodes: IndicatorNode[], id: string): IndexPath | null {
36
+ for (let i = 0; i < nodes.length; i++) {
37
+ const n = nodes[i];
38
+ if (n.id === id) return [i];
39
+ if (n.children?.length) {
40
+ const childPath = findIndexPath(n.children, id);
41
+ if (childPath) return [i, ...childPath];
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function getNodeAtPath(root: IndicatorNode[], path: IndexPath): IndicatorNode {
48
+ let node = root[path[0]];
49
+ for (let i = 1; i < path.length; i++) node = (node.children ?? [])[path[i]];
50
+ return node;
51
+ }
52
+
53
+ function getSiblingsAtPath(root: IndicatorNode[], path: IndexPath): { siblings: IndicatorNode[]; index: number } {
54
+ let siblings = root;
55
+ for (let depth = 0; depth < path.length - 1; depth++) {
56
+ const idx = path[depth];
57
+ siblings = siblings[idx].children ?? (siblings[idx].children = []);
58
+ }
59
+ return { siblings, index: path[path.length - 1] };
60
+ }
61
+
62
+ function getSiblingsAtDepth(root: IndicatorNode[], pathToLevel: number[]): IndicatorNode[] {
63
+ let siblings = root;
64
+ for (let i = 0; i < pathToLevel.length; i++) {
65
+ const idx = pathToLevel[i];
66
+ siblings = siblings[idx].children ?? [];
67
+ }
68
+ return siblings;
69
+ }
70
+
71
+ function containsId(node: IndicatorNode, id: string): boolean {
72
+ if (node.id === id) return true;
73
+ for (const c of node.children ?? []) {
74
+ if (containsId(c, id)) return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ function removeNodeById(nodes: IndicatorNode[], id: string): IndicatorNode | null {
80
+ for (let i = 0; i < nodes.length; i++) {
81
+ const n = nodes[i];
82
+ if (n.id === id) return nodes.splice(i, 1)[0];
83
+
84
+ if (n.children?.length) {
85
+ const removed = removeNodeById(n.children, id);
86
+ if (removed) {
87
+ if (n.children.length === 0) delete n.children;
88
+ return removed;
89
+ }
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ export default function TemplateStructurePanel({ tree, selected, onSelectNode, onChangeTree, onRefreshJson }: Props) {
96
+ const { t } = useTranslation();
97
+ const draggingIdRef = useRef<string | null>(null);
98
+
99
+ const addNode = () => {
100
+ onChangeTree((prevTree) => {
101
+ const root = structuredClone(prevTree);
102
+
103
+ const newNode: IndicatorNode = {
104
+ id: nanoid(),
105
+ label: t('newNode', 'New node'),
106
+ code: '',
107
+ };
108
+
109
+ if (!selected?.id) {
110
+ root.push(newNode);
111
+ onSelectNode(newNode);
112
+ return root;
113
+ }
114
+
115
+ const path = findIndexPath(root, selected.id);
116
+ if (!path) return root;
117
+
118
+ const selNode = getNodeAtPath(root, path);
119
+ const isGroup = Boolean(selNode.children?.length) || selNode.type === 'group';
120
+
121
+ if (isGroup) {
122
+ selNode.children = selNode.children ?? [];
123
+ selNode.children.push(newNode);
124
+ } else {
125
+ const { siblings, index } = getSiblingsAtPath(root, path);
126
+ siblings.splice(index + 1, 0, newNode);
127
+ }
128
+
129
+ onSelectNode(newNode);
130
+ return root;
131
+ });
132
+ };
133
+
134
+ const moveUp = () => {
135
+ if (!selected?.id) return;
136
+
137
+ onChangeTree((prevTree) => {
138
+ const root = structuredClone(prevTree);
139
+ const path = findIndexPath(root, selected.id);
140
+ if (!path) return root;
141
+
142
+ const { siblings, index } = getSiblingsAtPath(root, path);
143
+ if (index <= 0) return root;
144
+
145
+ [siblings[index - 1], siblings[index]] = [siblings[index], siblings[index - 1]];
146
+ return root;
147
+ });
148
+ };
149
+
150
+ const moveDown = () => {
151
+ if (!selected?.id) return;
152
+
153
+ onChangeTree((prevTree) => {
154
+ const root = structuredClone(prevTree);
155
+ const path = findIndexPath(root, selected.id);
156
+ if (!path) return root;
157
+
158
+ const { siblings, index } = getSiblingsAtPath(root, path);
159
+ if (index >= siblings.length - 1) return root;
160
+
161
+ [siblings[index + 1], siblings[index]] = [siblings[index], siblings[index + 1]];
162
+ return root;
163
+ });
164
+ };
165
+
166
+ const outdent = () => {
167
+ if (!selected?.id) return;
168
+
169
+ onChangeTree((prevTree) => {
170
+ const root = structuredClone(prevTree);
171
+ const path = findIndexPath(root, selected.id);
172
+ if (!path || path.length < 2) return root;
173
+
174
+ const { siblings: currentSiblings, index: currentIndex } = getSiblingsAtPath(root, path);
175
+ const movedNode = currentSiblings.splice(currentIndex, 1)[0];
176
+
177
+ const parentPath = path.slice(0, -1);
178
+ const parentIndexInGrand = parentPath[parentPath.length - 1];
179
+
180
+ const grandParentPath = path.slice(0, -2);
181
+ const grandSiblings = grandParentPath.length ? getSiblingsAtDepth(root, grandParentPath) : root;
182
+
183
+ grandSiblings.splice(parentIndexInGrand + 1, 0, movedNode);
184
+
185
+ const parentNode = getNodeAtPath(root, parentPath);
186
+ if (parentNode.children && parentNode.children.length === 0) delete parentNode.children;
187
+
188
+ return root;
189
+ });
190
+ };
191
+
192
+ const onDragStart = (id: string) => (e: React.DragEvent) => {
193
+ draggingIdRef.current = id;
194
+ e.dataTransfer.setData('application/x-indicator-node-id', id);
195
+ e.dataTransfer.effectAllowed = 'move';
196
+ };
197
+
198
+ const onDragOver = (e: React.DragEvent) => {
199
+ e.preventDefault();
200
+ e.dataTransfer.dropEffect = 'move';
201
+ };
202
+
203
+ const onDropIndent = (targetId: string) => (e: React.DragEvent) => {
204
+ e.preventDefault();
205
+
206
+ const draggedId = e.dataTransfer.getData('application/x-indicator-node-id') || draggingIdRef.current;
207
+ if (!draggedId || draggedId === targetId) return;
208
+
209
+ onChangeTree((prevTree) => {
210
+ const root = structuredClone(prevTree);
211
+
212
+ const draggedNode = removeNodeById(root, draggedId);
213
+ if (!draggedNode) return root;
214
+
215
+ if (containsId(draggedNode, targetId)) {
216
+ root.push(draggedNode);
217
+ return root;
218
+ }
219
+
220
+ const targetPath = findIndexPath(root, targetId);
221
+ if (!targetPath) {
222
+ root.push(draggedNode);
223
+ return root;
224
+ }
225
+
226
+ const targetNode = getNodeAtPath(root, targetPath);
227
+ targetNode.children = targetNode.children ?? [];
228
+ targetNode.children.push(draggedNode);
229
+
230
+ return root;
231
+ });
232
+ };
233
+
234
+ const renderTree = (nodes: IndicatorNode[]) =>
235
+ nodes.map((node) => (
236
+ <TreeNode
237
+ key={node.id}
238
+ id={node.id}
239
+ label={
240
+ <div
241
+ className={styles.draggableLabel}
242
+ draggable
243
+ onDragStart={onDragStart(node.id)}
244
+ onDragOver={onDragOver}
245
+ onDrop={onDropIndent(node.id)}
246
+ onClick={() => onSelectNode(node)}
247
+ role="button"
248
+ tabIndex={0}
249
+ >
250
+ <span className={styles.nodeText}>{node.label}</span>
251
+ {node.code ? <span className={styles.nodeCode}>{node.code}</span> : null}
252
+ </div>
253
+ }
254
+ >
255
+ {node.children?.length ? renderTree(node.children) : null}
256
+ </TreeNode>
257
+ ));
258
+
259
+ return (
260
+ <div className={styles.root}>
261
+ <Tile className={styles.tile}>
262
+ <div className={styles.tileHeaderRow}>
263
+ <h3 className={styles.tileTitle}>{t('templateStructure', 'Template Structure')}</h3>
264
+
265
+ <Button size="sm" kind="ghost" onClick={onRefreshJson}>
266
+ {t('refreshJson', 'Refresh JSON')}
267
+ </Button>
268
+ </div>
269
+
270
+ <div className={styles.treeWrap}>
271
+ <TreeView label={t('templateStructure', 'Template Structure')} hideLabel selected={selected?.id ? [selected.id] : []}>
272
+ {renderTree(tree)}
273
+ </TreeView>
274
+ </div>
275
+
276
+ <div className={styles.structureActions}>
277
+ <Button size="sm" kind="primary" onClick={addNode}>
278
+ {t('addNode', 'Add Node')}
279
+ </Button>
280
+
281
+ <Button size="sm" kind="secondary" onClick={moveUp} disabled={!selected?.id}>
282
+ {t('moveUp', 'Move Up')}
283
+ </Button>
284
+
285
+ <Button size="sm" kind="secondary" onClick={moveDown} disabled={!selected?.id}>
286
+ {t('moveDown', 'Move Down')}
287
+ </Button>
288
+
289
+ <Button size="sm" kind="tertiary" onClick={outdent} disabled={!selected?.id}>
290
+ {t('outdent', 'Outdent')}
291
+ </Button>
292
+ </div>
293
+ </Tile>
294
+ </div>
295
+ );
296
+ }
@@ -0,0 +1,81 @@
1
+ .root {
2
+ flex: 1;
3
+ min-height: 0;
4
+ display: flex;
5
+ }
6
+
7
+ .tile {
8
+ flex: 1;
9
+ min-height: 0;
10
+ padding: 1rem;
11
+ border-radius: 0.5rem;
12
+
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
16
+
17
+ .tileHeaderRow {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ gap: 0.75rem;
22
+ margin-bottom: 0.75rem;
23
+ flex-shrink: 0;
24
+ }
25
+
26
+ .tileTitle {
27
+ margin: 0;
28
+ font-weight: 600;
29
+ }
30
+
31
+ .treeWrap {
32
+ flex: 1;
33
+ min-height: 0;
34
+ overflow: auto;
35
+
36
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
37
+ border-radius: 0.375rem;
38
+ padding: 0.5rem;
39
+ background: var(--cds-layer-01, #fff);
40
+ }
41
+
42
+ .structureActions {
43
+ display: flex;
44
+ gap: 0.5rem;
45
+ margin-top: 0.75rem;
46
+ flex-wrap: wrap;
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .draggableLabel {
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: space-between;
54
+ gap: 0.75rem;
55
+ width: 100%;
56
+ cursor: grab;
57
+ padding: 0.125rem 0.25rem;
58
+ border-radius: 0.25rem;
59
+ }
60
+
61
+ .draggableLabel:active {
62
+ cursor: grabbing;
63
+ }
64
+
65
+ .draggableLabel:hover {
66
+ background: var(--cds-layer-hover-01, #f4f4f4);
67
+ }
68
+
69
+ .nodeText {
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ white-space: nowrap;
73
+ }
74
+
75
+ .nodeCode {
76
+ font-size: 0.75rem;
77
+ opacity: 0.75;
78
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
79
+ padding: 0.1rem 0.35rem;
80
+ border-radius: 0.375rem;
81
+ }
@@ -0,0 +1,23 @@
1
+ import { Type } from '@openmrs/esm-framework';
2
+
3
+ export const configSchema = {
4
+ defaultMapping: {
5
+ _description: 'Defaults used by the report builder.',
6
+ keyPattern: {
7
+ _default: '{code}_{age}_{sex}',
8
+ _description: 'Default key pattern for flat values.',
9
+ _type: Type.String,
10
+ },
11
+ defaultDimensions: {
12
+ _description: 'Default dimension names used in templates.',
13
+ age: {
14
+ _default: 'age',
15
+ _type: Type.String,
16
+ },
17
+ sex: {
18
+ _default: 'sex',
19
+ _type: Type.String,
20
+ },
21
+ },
22
+ },
23
+ };
@@ -0,0 +1,14 @@
1
+ declare module '*.scss' {
2
+ const content: { [className: string]: string };
3
+ export default content;
4
+ }
5
+
6
+ declare module '*.module.scss' {
7
+ const classes: { [className: string]: string };
8
+ export default classes;
9
+ }
10
+
11
+ // Webpack-specific typing used by OpenMRS translations loading
12
+ interface Require {
13
+ context(directory: string, useSubdirectories: boolean, regExp: RegExp, mode?: string): any;
14
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import type { DataTheme } from '../../types/theme/data-theme.types';
3
+ import { listThemes } from '../../resources/theme/data-theme.api';
4
+
5
+ export function useDataThemes(query: string) {
6
+ const [loading, setLoading] = React.useState(false);
7
+ const [error, setError] = React.useState<string | null>(null);
8
+ const [themes, setThemes] = React.useState<DataTheme[]>([]);
9
+
10
+ const reload = React.useCallback(async () => {
11
+ setLoading(true);
12
+ setError(null);
13
+ try {
14
+ const data = await listThemes(query?.trim() || undefined);
15
+ setThemes(data);
16
+ } catch (e: any) {
17
+ setError(e?.message ?? String(e));
18
+ } finally {
19
+ setLoading(false);
20
+ }
21
+ }, [query]);
22
+
23
+ React.useEffect(() => {
24
+ reload();
25
+ }, [reload]);
26
+
27
+ return { themes, loading, error, reload, setThemes };
28
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { getMambaTableMeta } from '../../resources/theme/mamba-table-meta.api';
3
+ import type { TableColumn } from '../../resources/theme/mamba-table-meta.api';
4
+
5
+ export function useMambaTableMeta(table?: string, enabled: boolean = true) {
6
+ const [columns, setColumns] = React.useState<TableColumn[]>([]);
7
+ const [loading, setLoading] = React.useState(false);
8
+ const [error, setError] = React.useState<string | null>(null);
9
+
10
+ React.useEffect(() => {
11
+ // ✅ don’t load until enabled AND a table is selected
12
+ if (!enabled || !table) {
13
+ setColumns([]);
14
+ setLoading(false);
15
+ setError(null);
16
+ return;
17
+ }
18
+
19
+ const ac = new AbortController();
20
+ setLoading(true);
21
+ setError(null);
22
+
23
+ getMambaTableMeta(table, ac.signal)
24
+ .then((cols) => setColumns(Array.isArray(cols) ? cols : []))
25
+ .catch((e) => {
26
+ if (e?.name !== 'AbortError') setError(e?.message ?? 'Failed to load columns');
27
+ })
28
+ .finally(() => setLoading(false));
29
+
30
+ return () => ac.abort();
31
+ }, [table, enabled]);
32
+
33
+ return { columns, loading, error };
34
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { getSchemaTables, type SchemaTable } from '../../resources/theme/mamba-schema.api';
3
+
4
+ function toTableName(t: SchemaTable | any): string {
5
+ // Try common backend shapes
6
+ return (
7
+ t?.name ??
8
+ t?.table ??
9
+ t?.tableName ??
10
+ t?.view ??
11
+ t?.value ?? // fallback (some APIs)
12
+ ''
13
+ );
14
+ }
15
+
16
+ export function useMambaTables(enabled: boolean) {
17
+ const [tables, setTables] = React.useState<string[]>([]);
18
+ const [loading, setLoading] = React.useState(false);
19
+ const [error, setError] = React.useState<string | null>(null);
20
+
21
+ React.useEffect(() => {
22
+ if (!enabled) {
23
+ setTables([]);
24
+ setLoading(false);
25
+ setError(null);
26
+ return;
27
+ }
28
+
29
+ const ac = new AbortController();
30
+ setLoading(true);
31
+ setError(null);
32
+
33
+ getSchemaTables(ac.signal).then((data) => {
34
+ // data is expected to be SchemaTable[] or something close
35
+
36
+ console.log("I am here trying to see if data is loaded")
37
+ const names = (data ?? [])
38
+ .map((t) => toTableName(t))
39
+ .map((x) => x.trim())
40
+ .filter(Boolean);
41
+
42
+ // de-dupe while preserving order
43
+ const uniq: string[] = [];
44
+ const seen = new Set<string>();
45
+ for (const n of names) {
46
+ if (!seen.has(n)) {
47
+ seen.add(n);
48
+ uniq.push(n);
49
+ }
50
+ }
51
+
52
+ setTables(uniq);
53
+ })
54
+ .catch((e) => {
55
+ if (e?.name !== 'AbortError') setError(e?.message ?? 'Failed to load tables');
56
+ })
57
+ .finally(() => setLoading(false));
58
+
59
+ return () => ac.abort();
60
+ }, [enabled]);
61
+
62
+ return { tables, loading, error };
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { defineConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
2
+ import { configSchema } from './config-schema';
3
+
4
+ const moduleName = '@epcare/esm-report-builder';
5
+
6
+ const options = {
7
+ featureName: 'report-builder',
8
+ moduleName,
9
+ };
10
+
11
+ export const reportBuilderAdminLink = getAsyncLifecycle(
12
+ () => import('./admin-card-link.extension'),
13
+ options,
14
+ );
15
+
16
+ // Optional but recommended if you add translations folder
17
+ export const importTranslation = require.context('../translations', true, /.json$/, 'lazy');
18
+
19
+ export const root = getAsyncLifecycle(() => import('./root.component'), options);
20
+
21
+ export function startupApp() {
22
+ defineConfigSchema(moduleName, configSchema);
23
+ }
@@ -0,0 +1,58 @@
1
+ import { omrsGet } from '../openmrs-api';
2
+
3
+ type RestList<T> = { results?: T[] };
4
+
5
+ export type AgeGroupRef = {
6
+ id: number;
7
+ display: string;
8
+ code?: string;
9
+ label: string;
10
+ minAgeDays: number;
11
+ maxAgeDays: number;
12
+ sortOrder: number;
13
+ active: boolean;
14
+ };
15
+
16
+ export type AgeCategoryDto = {
17
+ uuid: string;
18
+ display: string;
19
+ name: string;
20
+ description?: string;
21
+ code: string;
22
+ version?: string;
23
+ effectiveFrom?: string;
24
+ effectiveTo?: string | null;
25
+ active: boolean;
26
+ ageGroups?: AgeGroupRef[];
27
+ retired?: boolean;
28
+ };
29
+
30
+ export type AgeCategoryOption = {
31
+ uuid: string;
32
+ code: string;
33
+ label: string; // what we show in dropdown
34
+ name: string;
35
+ description?: string;
36
+ ageGroups: AgeGroupRef[];
37
+ };
38
+
39
+ export async function listAgeCategoriesWithGroups(signal?: AbortSignal): Promise<AgeCategoryOption[]> {
40
+ // ✅ categories endpoint
41
+ const data = await omrsGet<RestList<AgeCategoryDto>>('/reportbuilder/agecategory?v=full', signal);
42
+
43
+ const results = Array.isArray(data?.results) ? data.results : [];
44
+
45
+ return results
46
+ .filter((c) => !c.retired)
47
+ .map((c) => ({
48
+ uuid: c.uuid,
49
+ code: c.code,
50
+ name: c.name ?? c.display ?? c.code,
51
+ description: c.description,
52
+ label: `${c.name ?? c.display ?? c.code} (${c.code})`,
53
+ ageGroups: Array.isArray(c.ageGroups)
54
+ ? c.ageGroups.slice().sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
55
+ : [],
56
+ }))
57
+ .sort((a, b) => a.label.localeCompare(b.label));
58
+ }
@@ -0,0 +1,21 @@
1
+ export type ConceptMappingSummary = {
2
+ source: string; // e.g. "CIEL", "ICD-10-WHO", "SNOMED CT"
3
+ code: string; // e.g. "116128", "B54", "61462000"
4
+ mapType?: string; // e.g. "SAME-AS"
5
+ display: string; // original display string e.g. "CIEL: 116128"
6
+ };
7
+
8
+ export type ConceptSummary = {
9
+ uuid: string;
10
+ display: string;
11
+
12
+ conceptClass?: string; // e.g. "Diagnosis"
13
+ datatype?: string; // e.g. "Coded", "Numeric", "N/A"
14
+
15
+ // “best effort” external codes for showing + decisioning
16
+ mappings: ConceptMappingSummary[];
17
+
18
+ // convenience fields
19
+ cielCode?: string; // if present
20
+ icd10Code?: string; // if present
21
+ };
@@ -0,0 +1,31 @@
1
+ export type ConceptMapping = {
2
+ display: string;
3
+ uuid: string;
4
+ conceptMapType?: {
5
+ display?: string;
6
+ };
7
+ };
8
+
9
+ export type ConceptSummary = {
10
+ id: number;
11
+ uuid: string;
12
+ display: string;
13
+
14
+ datatype?: {
15
+ uuid: string;
16
+ name: string;
17
+ };
18
+
19
+ conceptClass?: {
20
+ uuid: string;
21
+ name: string;
22
+ };
23
+
24
+ answers?: Array<{
25
+ id: number;
26
+ uuid: string;
27
+ display: string;
28
+ }>;
29
+
30
+ mappings?: ConceptMapping[];
31
+ };