@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,150 @@
1
+ @use '@carbon/layout' as layout;
2
+ @use '@carbon/type' as type;
3
+
4
+ .page {
5
+ width: 100%;
6
+ margin: 0 auto;
7
+ padding: 1.5rem 0 2rem;
8
+ }
9
+
10
+ .header {
11
+ background: var(--cds-layer-01, #ffffff);
12
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
13
+ padding: layout.$spacing-06 layout.$spacing-06;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: layout.$spacing-07;
18
+ }
19
+
20
+ .left {
21
+ display: flex;
22
+ align-items: flex-start;
23
+ gap: layout.$spacing-06;
24
+ min-width: 0;
25
+ }
26
+
27
+ .illustrationWrap {
28
+ flex: 0 0 auto;
29
+ }
30
+
31
+ .pageLabels {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: layout.$spacing-03;
35
+ min-width: 0;
36
+ }
37
+
38
+ .appName {
39
+ @include type.type-style('body-compact-02');
40
+ color: var(--cds-text-secondary, #525252);
41
+ }
42
+
43
+ .pageTitle {
44
+ @include type.type-style('heading-05');
45
+ color: var(--cds-text-primary, #161616);
46
+ line-height: 1.15;
47
+ }
48
+
49
+ .metaLine {
50
+ @include type.type-style('body-compact-02');
51
+ color: var(--cds-text-secondary, #525252);
52
+ display: flex;
53
+ align-items: center;
54
+ gap: layout.$spacing-03;
55
+ }
56
+
57
+ .metaItem {
58
+ white-space: nowrap;
59
+ }
60
+
61
+ .metaDot {
62
+ opacity: 0.7;
63
+ }
64
+
65
+ .right {
66
+ display: flex;
67
+ flex-direction: column;
68
+ align-items: flex-end;
69
+ gap: layout.$spacing-05;
70
+ }
71
+
72
+ .rightTop {
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: flex-end;
76
+ gap: layout.$spacing-03;
77
+ }
78
+
79
+ .userRow {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: layout.$spacing-03;
83
+ }
84
+
85
+ .userName {
86
+ @include type.type-style('body-compact-02');
87
+ color: var(--cds-text-primary, #161616);
88
+ }
89
+
90
+ .userIcon {
91
+ color: var(--cds-text-secondary, #525252);
92
+ }
93
+
94
+ .contextRow {
95
+ @include type.type-style('body-compact-02');
96
+ color: var(--cds-text-secondary, #525252);
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: flex-end;
100
+ gap: layout.$spacing-03;
101
+ }
102
+
103
+ .value {
104
+ margin-left: layout.$spacing-02;
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .chevron {
109
+ opacity: 0.7;
110
+ }
111
+
112
+ .actions {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: flex-end;
116
+ }
117
+
118
+
119
+ .illustration {
120
+ position: relative;
121
+ width: 44px;
122
+ height: 44px;
123
+ flex: 0 0 auto;
124
+ }
125
+
126
+ .sheetBack,
127
+ .sheetMid,
128
+ .sheetFront {
129
+ position: absolute;
130
+ inset: 0;
131
+ border-radius: 2px;
132
+ background: #a7d6d4; /* example */
133
+ border: 1px solid rgba(0, 0, 0, 0.08);
134
+ }
135
+
136
+ /* ✅ overlap should trail LEFT */
137
+ .sheetBack {
138
+ transform: translateX(-8px) translateY(2px);
139
+ opacity: 0.6;
140
+ }
141
+
142
+ .sheetMid {
143
+ transform: translateX(-4px) translateY(1px);
144
+ opacity: 0.8;
145
+ }
146
+
147
+ .sheetFront {
148
+ transform: translateX(0) translateY(0);
149
+ opacity: 1;
150
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+
3
+ const Illustration: React.FC = () => {
4
+ return (
5
+ <svg
6
+ height="64"
7
+ width="64"
8
+ viewBox="0 0 32 32"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ xmlSpace="preserve"
11
+ fillRule="evenodd"
12
+ clipRule="evenodd"
13
+ strokeLinejoin="round"
14
+ strokeMiterlimit="2"
15
+ >
16
+ <path
17
+ d="M27 31.36H8a.36.36 0 0 1-.36-.36v-1.64H6a.36.36 0 0 1-.36-.36v-1.64H5a.36.36 0 0 1-.36-.36V3A.36.36 0 0 1 5 2.64h4.64V2a.36.36 0 0 1 .36-.36h1.64V1A.36.36 0 0 1 12 .64h4a.36.36 0 0 1 .36.36v.64H18a.36.36 0 0 1 .36.36v.64H23a.36.36 0 0 1 .36.36v1.64H25a.36.36 0 0 1 .36.36v1.64H27a.36.36 0 0 1 .36.36v24a.36.36 0 0 1-.36.36Z"
18
+ fill="#d2e5e5"
19
+ />
20
+ <path d="M8.36 30.64h18.28V7.36h-1.28V29a.36.36 0 0 1-.36.36H8.36v1.28Z" fill="#8abab8" />
21
+ <path
22
+ d="M5.36 26.64h17.28V3.36h-4.28V4a.36.36 0 0 1-.36.36h-8A.36.36 0 0 1 9.64 4v-.64H5.36v23.28Z"
23
+ fill="#8abab8"
24
+ />
25
+ <path fill="#fff" d="M7.5 12.64h13v.72h-13zM7.5 8.64h13v.72h-13zM7.5 20.64h13v.72h-13zM7.5 16.64h13v.72h-13z" />
26
+ <path
27
+ d="M10.36 3.64h7.28V2.36H16a.36.36 0 0 1-.36-.36v-.64h-3.28V2a.36.36 0 0 1-.36.36h-1.64v1.28ZM6.36 28.64h18.28V5.36h-1.28V27a.36.36 0 0 1-.36.36H6.36v1.28Z"
28
+ fill="#8abab8"
29
+ />
30
+ </svg>
31
+ );
32
+ };
33
+
34
+ export default Illustration;
@@ -0,0 +1,33 @@
1
+ .tileHeader {
2
+ margin-bottom: 0.75rem;
3
+ }
4
+
5
+ .tileTitle {
6
+ margin: 0;
7
+ font-weight: 600;
8
+ }
9
+
10
+ .formGrid {
11
+ display: grid;
12
+ grid-template-columns: 1fr;
13
+ gap: 0.75rem;
14
+ margin-top: 0.75rem;
15
+ }
16
+
17
+ .note {
18
+ margin-top: 0.75rem;
19
+ padding: 0.75rem;
20
+ border-left: 4px solid var(--cds-support-info, #0f62fe);
21
+ background: var(--cds-layer-accent-01, #edf5ff);
22
+ border-radius: 0.25rem;
23
+ font-size: 0.875rem;
24
+ }
25
+
26
+ .mappingActions {
27
+ margin-top: 0.75rem;
28
+ }
29
+
30
+ .placeholder {
31
+ padding: 1rem 0;
32
+ opacity: 0.8;
33
+ }
@@ -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
+ }