@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,389 @@
1
+ import React from 'react';
2
+ import {
3
+ Tabs,
4
+ Tab,
5
+ TabList,
6
+ TabPanels,
7
+ TabPanel,
8
+ TextInput,
9
+ Dropdown,
10
+ Button,
11
+ Tile,
12
+ ComboBox,
13
+ InlineLoading,
14
+ InlineNotification,
15
+ Tag,
16
+ } from '@carbon/react';
17
+ import { useTranslation } from 'react-i18next';
18
+
19
+ import styles from './IndicatorPropertiesPanel.scss';
20
+
21
+ type DropdownItem = { id: string; text: string };
22
+
23
+ export type SelectedNode = {
24
+ id: string;
25
+ label?: string;
26
+ code?: string;
27
+
28
+ // Optional “domain” fields you can persist on your indicator node
29
+ conditionConceptUuid?: string;
30
+ conditionConceptLabel?: string;
31
+ };
32
+
33
+ export type ConceptSummary = {
34
+ uuid: string;
35
+ display: string;
36
+
37
+ // Optional fields if you request v=full (or v=custom)
38
+ conceptClass?: { display?: string };
39
+ datatype?: { display?: string };
40
+ mappings?: Array<{
41
+ display?: string;
42
+ conceptReferenceTerm?: { display?: string };
43
+ conceptMapType?: { display?: string };
44
+ }>;
45
+ };
46
+
47
+ type Props = {
48
+ selected?: SelectedNode | null;
49
+
50
+ onChangeSelectedLabel: (value: string) => void;
51
+ onChangeSelectedCode: (value: string) => void;
52
+
53
+ // NEW: concept selection (persist onto selected node in your tree)
54
+ onChangeSelectedConditionConcept?: (payload: { uuid: string; label: string } | null) => void;
55
+
56
+ mappingCategoryOptions: DropdownItem[];
57
+ mappingAttrOptions: DropdownItem[];
58
+
59
+ selectedCoc?: DropdownItem | null;
60
+ selectedAoc?: DropdownItem | null;
61
+ onChangeCoc?: (item: DropdownItem) => void;
62
+ onChangeAoc?: (item: DropdownItem) => void;
63
+
64
+ onRemoveOverride?: () => void;
65
+
66
+ /**
67
+ * ✅ API hooks (pass from container)
68
+ * - searchConcepts: queries OpenMRS concept endpoint (q=..., v=custom/full)
69
+ * - previewIndicator: optional “test run” endpoint for the selected indicator node
70
+ */
71
+ searchConcepts?: (query: string) => Promise<ConceptSummary[]>;
72
+ previewIndicator?: (nodeId: string) => Promise<{ rows: number; sample?: any[] }>;
73
+ };
74
+
75
+ function useDebouncedValue<T>(value: T, delayMs: number) {
76
+ const [debounced, setDebounced] = React.useState(value);
77
+ React.useEffect(() => {
78
+ const t = window.setTimeout(() => setDebounced(value), delayMs);
79
+ return () => window.clearTimeout(t);
80
+ }, [value, delayMs]);
81
+ return debounced;
82
+ }
83
+
84
+ export default function IndicatorPropertiesPanel({
85
+ selected,
86
+ onChangeSelectedLabel,
87
+ onChangeSelectedCode,
88
+ onChangeSelectedConditionConcept,
89
+ mappingCategoryOptions,
90
+ mappingAttrOptions,
91
+ selectedCoc,
92
+ selectedAoc,
93
+ onChangeCoc,
94
+ onChangeAoc,
95
+ onRemoveOverride,
96
+ searchConcepts,
97
+ previewIndicator,
98
+ }: Props) {
99
+ const { t } = useTranslation();
100
+
101
+ // ----------------------------
102
+ // Concept search (API-backed)
103
+ // ----------------------------
104
+ const [conceptQuery, setConceptQuery] = React.useState('');
105
+ const debouncedQuery = useDebouncedValue(conceptQuery, 300);
106
+
107
+ const [conceptItems, setConceptItems] = React.useState<ConceptSummary[]>([]);
108
+ const [conceptLoading, setConceptLoading] = React.useState(false);
109
+ const [conceptError, setConceptError] = React.useState<string | null>(null);
110
+
111
+ const selectedConceptUuid = selected?.conditionConceptUuid ?? '';
112
+ const selectedConceptLabel = selected?.conditionConceptLabel ?? '';
113
+
114
+ // track selected concept object if it exists in list
115
+ const selectedConceptItem = React.useMemo(() => {
116
+ if (!selectedConceptUuid) return null;
117
+ return (
118
+ conceptItems.find((c) => c.uuid === selectedConceptUuid) ?? {
119
+ uuid: selectedConceptUuid,
120
+ display: selectedConceptLabel || selectedConceptUuid,
121
+ }
122
+ );
123
+ }, [conceptItems, selectedConceptUuid, selectedConceptLabel]);
124
+
125
+ React.useEffect(() => {
126
+ let cancelled = false;
127
+
128
+ async function run() {
129
+ if (!searchConcepts) return; // API not wired yet
130
+ const q = (debouncedQuery ?? '').trim();
131
+ if (!q || q.length < 2) {
132
+ setConceptItems([]);
133
+ setConceptError(null);
134
+ return;
135
+ }
136
+
137
+ try {
138
+ setConceptLoading(true);
139
+ setConceptError(null);
140
+ const results = await searchConcepts(q);
141
+ if (!cancelled) setConceptItems(results ?? []);
142
+ } catch (e: any) {
143
+ if (!cancelled) setConceptError(e?.message ?? 'Failed to search concepts');
144
+ } finally {
145
+ if (!cancelled) setConceptLoading(false);
146
+ }
147
+ }
148
+
149
+ run();
150
+ return () => {
151
+ cancelled = true;
152
+ };
153
+ }, [debouncedQuery, searchConcepts]);
154
+
155
+ // ----------------------------
156
+ // Preview / Test (API-backed)
157
+ // ----------------------------
158
+ const [previewLoading, setPreviewLoading] = React.useState(false);
159
+ const [previewError, setPreviewError] = React.useState<string | null>(null);
160
+ const [previewInfo, setPreviewInfo] = React.useState<{ rows: number; sample?: any[] } | null>(null);
161
+
162
+ const runPreview = async () => {
163
+ if (!previewIndicator || !selected?.id) return;
164
+ try {
165
+ setPreviewLoading(true);
166
+ setPreviewError(null);
167
+ const res = await previewIndicator(selected.id);
168
+ setPreviewInfo(res);
169
+ } catch (e: any) {
170
+ setPreviewError(e?.message ?? 'Failed to preview indicator');
171
+ setPreviewInfo(null);
172
+ } finally {
173
+ setPreviewLoading(false);
174
+ }
175
+ };
176
+
177
+ const isEmpty = !selected;
178
+
179
+ return (
180
+ <Tile className={styles.tile}>
181
+ <div className={styles.tileHeader}>
182
+ <h3 className={styles.tileTitle}>{t('indicatorProperties', 'Indicator Properties')}</h3>
183
+ {isEmpty ? (
184
+ <div className={styles.tileHint}>{t('selectNodeHint', 'Select an indicator/row to edit properties')}</div>
185
+ ) : null}
186
+ </div>
187
+
188
+ <Tabs>
189
+ <TabList aria-label={t('propertiesTabs', 'Properties tabs')}>
190
+ <Tab>{t('details', 'Details')}</Tab>
191
+ <Tab>{t('disaggregation', 'Disaggregation')}</Tab>
192
+ <Tab>{t('mapping', 'Mapping')}</Tab>
193
+ <Tab>{t('api', 'API')}</Tab>
194
+ </TabList>
195
+
196
+ <TabPanels>
197
+ {/* ---------------- Details ---------------- */}
198
+ <TabPanel>
199
+ <div className={styles.formGrid}>
200
+ <TextInput
201
+ id="nodeLabel"
202
+ labelText={t('label', 'Label')}
203
+ value={selected?.label ?? ''}
204
+ onChange={(e) => onChangeSelectedLabel(e.target.value)}
205
+ placeholder={t('selectNode', 'Select a node to edit')}
206
+ disabled={isEmpty}
207
+ />
208
+
209
+ <TextInput
210
+ id="nodeCode"
211
+ labelText={t('codeOptional', 'Code (optional for grouping nodes)')}
212
+ value={selected?.code ?? ''}
213
+ onChange={(e) => onChangeSelectedCode(e.target.value)}
214
+ disabled={isEmpty}
215
+ />
216
+ </div>
217
+
218
+ <div className={styles.note}>
219
+ {t(
220
+ 'noteGroups',
221
+ 'Note: Nodes with children act as groups. Leaf nodes act as indicators and will appear as rows in the output tables.',
222
+ )}
223
+ </div>
224
+ </TabPanel>
225
+
226
+ {/* ---------------- Disaggregation ---------------- */}
227
+ <TabPanel>
228
+ <div className={styles.placeholder}>
229
+ {t('disaggPlaceholder', 'Disaggregation settings will go here.')}
230
+ </div>
231
+ </TabPanel>
232
+
233
+ {/* ---------------- Mapping ---------------- */}
234
+ <TabPanel>
235
+ <div className={styles.formGrid}>
236
+ <TextInput
237
+ id="indicatorCode"
238
+ labelText={t('code', 'Code')}
239
+ value={selected?.code ?? ''}
240
+ onChange={(e) => onChangeSelectedCode(e.target.value)}
241
+ disabled={isEmpty}
242
+ />
243
+
244
+ <TextInput
245
+ id="dataElementId"
246
+ labelText={t('dataElementId', 'Data Element ID')}
247
+ value={selected?.code ? `${selected.code}_Cases` : ''}
248
+ readOnly
249
+ />
250
+
251
+ <Dropdown
252
+ id="coc"
253
+ label=""
254
+ titleText={t('categoryOptionCombo', 'Category Option Combo')}
255
+ items={mappingCategoryOptions}
256
+ itemToString={(item) => (item ? item.text : '')}
257
+ selectedItem={selectedCoc ?? null}
258
+ onChange={(e) => onChangeCoc?.(e.selectedItem as DropdownItem)}
259
+ />
260
+
261
+ <Dropdown
262
+ id="aoc"
263
+ label=""
264
+ titleText={t('attributeOptionCombo', 'Attribute Option Combo')}
265
+ items={mappingAttrOptions}
266
+ itemToString={(item) => (item ? item.text : '')}
267
+ selectedItem={selectedAoc ?? null}
268
+ onChange={(e) => onChangeAoc?.(e.selectedItem as DropdownItem)}
269
+ />
270
+ </div>
271
+
272
+ <div className={styles.mappingActions}>
273
+ <Button size="sm" kind="secondary" onClick={() => onRemoveOverride?.()}>
274
+ {t('removeOverride', 'Remove Override')}
275
+ </Button>
276
+ </div>
277
+ </TabPanel>
278
+
279
+ {/* ---------------- API ---------------- */}
280
+ <TabPanel>
281
+ {!searchConcepts ? (
282
+ <InlineNotification
283
+ kind="info"
284
+ lowContrast
285
+ title={t('apiNotWired', 'API not wired yet')}
286
+ subtitle={t(
287
+ 'apiNotWiredHint',
288
+ 'Pass searchConcepts() from your container using OpenMRS REST (ws/rest/v1/concept?q=...).',
289
+ )}
290
+ />
291
+ ) : null}
292
+
293
+ <div className={styles.apiBlock}>
294
+ <ComboBox
295
+ id="conceptSearch"
296
+ titleText={t('conditionConcept', 'Condition concept')}
297
+ placeholder={t('searchConcept', 'Search concept...')}
298
+ items={conceptItems}
299
+ itemToString={(item) => (item ? item.display : '')}
300
+ selectedItem={selectedConceptItem as any}
301
+ onInputChange={(text) => setConceptQuery(text ?? '')}
302
+ onChange={(e) => {
303
+ const item = e.selectedItem as ConceptSummary | null;
304
+ if (!onChangeSelectedConditionConcept) return;
305
+ if (!item) onChangeSelectedConditionConcept(null);
306
+ else onChangeSelectedConditionConcept({ uuid: item.uuid, label: item.display });
307
+ }}
308
+ disabled={isEmpty}
309
+ />
310
+
311
+ {conceptLoading ? (
312
+ <div style={{ marginTop: '0.75rem' }}>
313
+ <InlineLoading description={t('searching', 'Searching...')} />
314
+ </div>
315
+ ) : null}
316
+
317
+ {conceptError ? (
318
+ <div style={{ marginTop: '0.75rem' }}>
319
+ <InlineNotification kind="error" lowContrast title={t('error', 'Error')} subtitle={conceptError} />
320
+ </div>
321
+ ) : null}
322
+
323
+ {selectedConceptItem ? (
324
+ <div className={styles.conceptSummary}>
325
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
326
+ <strong>{selectedConceptItem.display}</strong>
327
+ <Tag size="sm" type="gray">
328
+ {selectedConceptItem.uuid}
329
+ </Tag>
330
+ {selectedConceptItem.conceptClass?.display ? (
331
+ <Tag size="sm" type="blue">
332
+ {selectedConceptItem.conceptClass.display}
333
+ </Tag>
334
+ ) : null}
335
+ {selectedConceptItem.datatype?.display ? (
336
+ <Tag size="sm" type="teal">
337
+ {selectedConceptItem.datatype.display}
338
+ </Tag>
339
+ ) : null}
340
+ </div>
341
+
342
+ {selectedConceptItem.mappings?.length ? (
343
+ <div style={{ marginTop: '0.5rem', fontSize: '0.875rem', opacity: 0.85 }}>
344
+ {t('mappings', 'Mappings')}: {selectedConceptItem.mappings.slice(0, 3).map((m) => m.display).filter(Boolean).join(' • ')}
345
+ {selectedConceptItem.mappings.length > 3 ? ' …' : ''}
346
+ </div>
347
+ ) : null}
348
+ </div>
349
+ ) : null}
350
+ </div>
351
+
352
+ <div className={styles.apiActions}>
353
+ <Button
354
+ size="sm"
355
+ kind="primary"
356
+ onClick={runPreview}
357
+ disabled={!previewIndicator || !selected?.id}
358
+ >
359
+ {t('preview', 'Preview')}
360
+ </Button>
361
+
362
+ {previewLoading ? <InlineLoading description={t('running', 'Running...')} /> : null}
363
+ </div>
364
+
365
+ {previewError ? (
366
+ <div style={{ marginTop: '0.75rem' }}>
367
+ <InlineNotification kind="error" lowContrast title={t('previewFailed', 'Preview failed')} subtitle={previewError} />
368
+ </div>
369
+ ) : null}
370
+
371
+ {previewInfo ? (
372
+ <div className={styles.previewSummary}>
373
+ <div style={{ fontWeight: 600 }}>{t('previewResult', 'Preview result')}</div>
374
+ <div style={{ marginTop: '0.25rem' }}>
375
+ {t('rows', 'Rows')}: <strong>{previewInfo.rows}</strong>
376
+ </div>
377
+ {previewInfo.sample?.length ? (
378
+ <pre style={{ marginTop: '0.5rem', overflow: 'auto' }}>
379
+ {JSON.stringify(previewInfo.sample.slice(0, 5), null, 2)}
380
+ </pre>
381
+ ) : null}
382
+ </div>
383
+ ) : null}
384
+ </TabPanel>
385
+ </TabPanels>
386
+ </Tabs>
387
+ </Tile>
388
+ );
389
+ }
@@ -0,0 +1,32 @@
1
+ @use '@carbon/layout';
2
+
3
+ .headerRow {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ margin-bottom: layout.$spacing-03;
8
+ }
9
+
10
+ .title {
11
+ margin: 0;
12
+ }
13
+
14
+ .tools {
15
+ display: flex;
16
+ gap: layout.$spacing-02;
17
+ align-items: center;
18
+ }
19
+
20
+ .editorContainer {
21
+ padding: layout.$spacing-03;
22
+
23
+ :global(.ace_editor) {
24
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
25
+ border-radius: 4px;
26
+ }
27
+ }
28
+
29
+ .maximized {
30
+ position: relative;
31
+ z-index: 5;
32
+ }
@@ -0,0 +1,104 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Button, CopyButton, IconButton } from '@carbon/react';
3
+ import { Download, Maximize, Minimize, Renew } from '@carbon/icons-react';
4
+ import AceEditor from 'react-ace';
5
+
6
+ import 'ace-builds/src-noconflict/mode-json';
7
+ import 'ace-builds/src-noconflict/theme-github';
8
+
9
+ import styles from './JsonPreviewPanel.scss';
10
+
11
+ type Props = {
12
+ /** Draft JSON shown in the editor (controlled) */
13
+ jsonString: string;
14
+
15
+ /** Called when user edits the JSON */
16
+ onChangeJson?: (value: string) => void;
17
+
18
+ /** Refresh Template Structure from JSON draft */
19
+ onRefreshTemplate?: () => void;
20
+
21
+ fileName?: string;
22
+
23
+ /** Defaults to true to preserve current behaviour */
24
+ readOnly?: boolean;
25
+ };
26
+
27
+ export default function JsonPreviewPanel({
28
+ jsonString,
29
+ onChangeJson,
30
+ onRefreshTemplate,
31
+ fileName = 'report-template.json',
32
+ readOnly = true,
33
+ }: Props) {
34
+ const [isMaximized, setIsMaximized] = useState(false);
35
+
36
+ const downloadable = useMemo(() => new Blob([jsonString], { type: 'application/json' }), [jsonString]);
37
+ const downloadUrl = useMemo(() => window.URL.createObjectURL(downloadable), [downloadable]);
38
+
39
+ const canEdit = !readOnly && typeof onChangeJson === 'function';
40
+ const canRefreshTemplate = typeof onRefreshTemplate === 'function';
41
+
42
+ return (
43
+ <div className={isMaximized ? styles.maximized : ''}>
44
+ <div className={styles.headerRow}>
45
+ <h3 className={styles.title}>JSON Preview</h3>
46
+
47
+ <div className={styles.tools}>
48
+ {/* ✅ Refresh Template (JSON -> Template Structure) */}
49
+ {canRefreshTemplate ? (
50
+ <Button
51
+ size="sm"
52
+ kind="ghost"
53
+ renderIcon={Renew}
54
+ onClick={onRefreshTemplate}
55
+ className={styles.refreshBtn}
56
+ />
57
+ ) : null}
58
+
59
+ <CopyButton
60
+ align="top"
61
+ className="cds--btn--sm"
62
+ iconDescription="Copy JSON"
63
+ kind="ghost"
64
+ onClick={() => navigator.clipboard.writeText(jsonString)}
65
+ />
66
+
67
+ <a download={fileName} href={downloadUrl}>
68
+ <IconButton kind="ghost" label="Download JSON" size="sm">
69
+ <Download />
70
+ </IconButton>
71
+ </a>
72
+
73
+ <IconButton
74
+ kind="ghost"
75
+ label={isMaximized ? 'Minimize' : 'Maximize'}
76
+ size="sm"
77
+ onClick={() => setIsMaximized((v) => !v)}
78
+ >
79
+ {isMaximized ? <Minimize /> : <Maximize />}
80
+ </IconButton>
81
+ </div>
82
+ </div>
83
+
84
+ <div className={styles.editorContainer}>
85
+ <AceEditor
86
+ mode="json"
87
+ theme="github"
88
+ value={jsonString}
89
+ readOnly={!canEdit}
90
+ onChange={(value) => onChangeJson?.(value)}
91
+ width="100%"
92
+ height={isMaximized ? '70vh' : '320px'}
93
+ fontSize={12}
94
+ setOptions={{
95
+ useWorker: false,
96
+ showPrintMargin: false,
97
+ tabSize: 2,
98
+ wrap: true,
99
+ }}
100
+ />
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { Tile, Toggle } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import styles from './mapping-preview-panel.scss';
6
+
7
+ type Props = {
8
+ title?: string;
9
+
10
+ // Controlled toggle state
11
+ zerosMode: boolean;
12
+ onToggleZerosMode: (value: boolean) => void;
13
+
14
+ // Row label (selected indicator)
15
+ rowLabel?: string;
16
+
17
+ // Column headers (disaggregation preview)
18
+ headers?: string[];
19
+
20
+ // Sample values (same length as headers)
21
+ sampleValues?: number[];
22
+
23
+ // Optional: allow parent to pass className for sizing
24
+ className?: string;
25
+ };
26
+
27
+ export default function MappingPreviewPanel({
28
+ title,
29
+ zerosMode,
30
+ onToggleZerosMode,
31
+ rowLabel,
32
+ headers = ['Age <5 | Male', 'Age <5 | Female', '5-14 | Male', '5-14 | Female'],
33
+ sampleValues = [120, 80, 75, 60],
34
+ className,
35
+ }: Props) {
36
+ const { t } = useTranslation();
37
+
38
+ const safeValues = headers.map((_, i) => sampleValues[i] ?? 0);
39
+
40
+ return (
41
+ <Tile className={`${styles.tile} ${className ?? ''}`}>
42
+ <div className={styles.tileHeaderRow}>
43
+ <h3 className={styles.tileTitle}>{title ?? t('mappingPreview', 'Mapping Preview')}</h3>
44
+
45
+ <div className={styles.previewControls}>
46
+ <Toggle
47
+ id="zerosMode"
48
+ labelText=""
49
+ hideLabel
50
+ labelA={t('sampleValues', 'Sample Values')}
51
+ labelB={t('zerosMode', 'Zeros Mode')}
52
+ toggled={zerosMode}
53
+ onToggle={onToggleZerosMode}
54
+ size="sm"
55
+ />
56
+ </div>
57
+ </div>
58
+
59
+ <div className={styles.previewTableWrap}>
60
+ <table className={styles.previewTable}>
61
+ <thead>
62
+ <tr>
63
+ <th />
64
+ {headers.map((h) => (
65
+ <th key={h}>{h}</th>
66
+ ))}
67
+ </tr>
68
+ </thead>
69
+
70
+ <tbody>
71
+ <tr>
72
+ <td className={styles.previewRowLabel}>{rowLabel ?? 'EP01A. Malaria Cases'}</td>
73
+ {safeValues.map((v, i) => (
74
+ <td key={i}>{zerosMode ? 0 : v}</td>
75
+ ))}
76
+ </tr>
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ </Tile>
81
+ );
82
+ }
@@ -0,0 +1,55 @@
1
+ .tile {
2
+ padding: 1rem;
3
+ border-radius: 0.5rem;
4
+ display: flex;
5
+ flex-direction: column;
6
+ min-height: 0;
7
+ }
8
+
9
+ .tileHeaderRow {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: 0.75rem;
14
+ margin-bottom: 0.75rem;
15
+ }
16
+
17
+ .tileTitle {
18
+ margin: 0;
19
+ font-weight: 600;
20
+ }
21
+
22
+ .previewControls {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 0.75rem;
26
+ }
27
+
28
+ .previewTableWrap {
29
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
30
+ border-radius: 0.375rem;
31
+ overflow: hidden;
32
+ }
33
+
34
+ .previewTable {
35
+ width: 100%;
36
+ border-collapse: collapse;
37
+ }
38
+
39
+ .previewTable thead th {
40
+ background: var(--cds-layer-02, #f4f4f4);
41
+ font-weight: 600;
42
+ text-align: left;
43
+ padding: 0.75rem;
44
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
45
+ }
46
+
47
+ .previewTable tbody td {
48
+ padding: 0.75rem;
49
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
50
+ }
51
+
52
+ .previewRowLabel {
53
+ font-weight: 600;
54
+ white-space: nowrap;
55
+ }