@openmrs/esm-report-builder 0.1.1-pre.101

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 (670) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/.tx/config +12 -0
  5. package/LICENSE +401 -0
  6. package/README.md +336 -0
  7. package/__mocks__/react-i18next.js +12 -0
  8. package/dist/128.css +1 -0
  9. package/dist/128.css.map +1 -0
  10. package/dist/128.js +338 -0
  11. package/dist/128.js.map +1 -0
  12. package/dist/1480.js +1 -0
  13. package/dist/1480.js.map +1 -0
  14. package/dist/1646.js +1 -0
  15. package/dist/1646.js.map +1 -0
  16. package/dist/1869.js +1 -0
  17. package/dist/1869.js.map +1 -0
  18. package/dist/1877.js +1 -0
  19. package/dist/1877.js.map +1 -0
  20. package/dist/2131.js +1 -0
  21. package/dist/2131.js.map +1 -0
  22. package/dist/2317.js +1 -0
  23. package/dist/2317.js.map +1 -0
  24. package/dist/2408.js +1 -0
  25. package/dist/2408.js.map +1 -0
  26. package/dist/2416.js +1 -0
  27. package/dist/2416.js.map +1 -0
  28. package/dist/282.js +1 -0
  29. package/dist/282.js.map +1 -0
  30. package/dist/2881.js +1 -0
  31. package/dist/2881.js.map +1 -0
  32. package/dist/2997.js +1 -0
  33. package/dist/2997.js.map +1 -0
  34. package/dist/3378.js +1 -0
  35. package/dist/3378.js.map +1 -0
  36. package/dist/3963.js +1 -0
  37. package/dist/3963.js.map +1 -0
  38. package/dist/4106.js +1 -0
  39. package/dist/4106.js.map +1 -0
  40. package/dist/4111.js +1 -0
  41. package/dist/4111.js.map +1 -0
  42. package/dist/4171.css +1 -0
  43. package/dist/4171.css.map +1 -0
  44. package/dist/4171.js +43 -0
  45. package/dist/4171.js.map +1 -0
  46. package/dist/434.js +1 -0
  47. package/dist/434.js.map +1 -0
  48. package/dist/4348.js +1 -0
  49. package/dist/4348.js.map +1 -0
  50. package/dist/4383.js +1 -0
  51. package/dist/4383.js.map +1 -0
  52. package/dist/4658.js +1 -0
  53. package/dist/4658.js.map +1 -0
  54. package/dist/4668.js +1 -0
  55. package/dist/4668.js.map +1 -0
  56. package/dist/4928.js +1 -0
  57. package/dist/4928.js.map +1 -0
  58. package/dist/5098.js +1 -0
  59. package/dist/5098.js.map +1 -0
  60. package/dist/5117.js +1 -0
  61. package/dist/5117.js.map +1 -0
  62. package/dist/5132.js +1 -0
  63. package/dist/5132.js.map +1 -0
  64. package/dist/5145.js +1 -0
  65. package/dist/5145.js.map +1 -0
  66. package/dist/550.js +1 -0
  67. package/dist/550.js.map +1 -0
  68. package/dist/5503.js +1 -0
  69. package/dist/5503.js.map +1 -0
  70. package/dist/556.js +1 -0
  71. package/dist/556.js.map +1 -0
  72. package/dist/5644.js +1 -0
  73. package/dist/5644.js.map +1 -0
  74. package/dist/5940.js +1 -0
  75. package/dist/5940.js.map +1 -0
  76. package/dist/6047.js +1 -0
  77. package/dist/6047.js.map +1 -0
  78. package/dist/6237.js +1 -0
  79. package/dist/6237.js.map +1 -0
  80. package/dist/6371.js +1 -0
  81. package/dist/6371.js.map +1 -0
  82. package/dist/6377.js +1 -0
  83. package/dist/6377.js.map +1 -0
  84. package/dist/6444.js +1 -0
  85. package/dist/6444.js.map +1 -0
  86. package/dist/6508.js +1 -0
  87. package/dist/6508.js.map +1 -0
  88. package/dist/6652.js +1 -0
  89. package/dist/6652.js.map +1 -0
  90. package/dist/6686.js +1 -0
  91. package/dist/6686.js.map +1 -0
  92. package/dist/6724.js +1 -0
  93. package/dist/6724.js.map +1 -0
  94. package/dist/6867.js +19 -0
  95. package/dist/6867.js.map +1 -0
  96. package/dist/6904.js +1 -0
  97. package/dist/6904.js.map +1 -0
  98. package/dist/7045.js +1 -0
  99. package/dist/7045.js.map +1 -0
  100. package/dist/7175.js +1 -0
  101. package/dist/7175.js.map +1 -0
  102. package/dist/7182.js +1 -0
  103. package/dist/7182.js.map +1 -0
  104. package/dist/7475.js +1 -0
  105. package/dist/7475.js.map +1 -0
  106. package/dist/7742.js +1 -0
  107. package/dist/7742.js.map +1 -0
  108. package/dist/7912.js +1 -0
  109. package/dist/7912.js.map +1 -0
  110. package/dist/8251.js +1 -0
  111. package/dist/8251.js.map +1 -0
  112. package/dist/8358.js +1 -0
  113. package/dist/8358.js.map +1 -0
  114. package/dist/8359.js +1 -0
  115. package/dist/8359.js.map +1 -0
  116. package/dist/8695.js +1 -0
  117. package/dist/8695.js.map +1 -0
  118. package/dist/903.js +1 -0
  119. package/dist/903.js.map +1 -0
  120. package/dist/9072.js +1 -0
  121. package/dist/9072.js.map +1 -0
  122. package/dist/9806.js +1 -0
  123. package/dist/9806.js.map +1 -0
  124. package/dist/997.js +1 -0
  125. package/dist/997.js.map +1 -0
  126. package/dist/openmrs-esm-report-builder.js +3 -0
  127. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +1433 -0
  128. package/dist/openmrs-esm-report-builder.js.map +1 -0
  129. package/dist/routes.json +1 -0
  130. package/e2e/core/env.ts +5 -0
  131. package/e2e/core/global-setup.ts +34 -0
  132. package/e2e/core/test.ts +21 -0
  133. package/e2e/pages/app-shell.page.ts +35 -0
  134. package/e2e/pages/etl-sources.page.ts +36 -0
  135. package/e2e/pages/report-sections.page.ts +21 -0
  136. package/e2e/pages/section-preview.modal.ts +29 -0
  137. package/e2e/specs/etl-sources.spec.ts +51 -0
  138. package/e2e/specs/navigation.spec.ts +13 -0
  139. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  140. package/e2e/specs/section-preview.spec.ts +94 -0
  141. package/e2e/support/github/Dockerfile +32 -0
  142. package/e2e/support/github/Dockerfile.dockerignore +13 -0
  143. package/e2e/support/github/docker-compose.yml +31 -0
  144. package/e2e/support/github/run-e2e-docker-env.sh +131 -0
  145. package/e2e/support/github/spa-assemble-config.json +6 -0
  146. package/e2e/tsconfig.json +11 -0
  147. package/example.env +5 -0
  148. package/linelist_report_builder_design_spec.md +816 -0
  149. package/package.json +98 -0
  150. package/playwright.config.ts +28 -0
  151. package/rspack.config.js +2 -0
  152. package/src/__debug__/all-designs.test.tsx +33 -0
  153. package/src/admin-card-link.extension.module.scss +55 -0
  154. package/src/admin-card-link.extension.tsx +24 -0
  155. package/src/components/admin/admin-page.component.tsx +75 -0
  156. package/src/components/admin/age-categories-page.component.tsx +109 -0
  157. package/src/components/admin/age-groups-page.component.tsx +146 -0
  158. package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
  159. package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
  160. package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
  161. package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
  162. package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
  163. package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
  164. package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
  165. package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
  166. package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
  167. package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
  168. package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
  169. package/src/components/admin/etl-sources-page.component.tsx +355 -0
  170. package/src/components/admin/etl-tasks-page.component.scss +370 -0
  171. package/src/components/admin/etl-tasks-page.component.tsx +768 -0
  172. package/src/components/admin/report-categories-page.component.tsx +267 -0
  173. package/src/components/admin/report-library-page.component.tsx +551 -0
  174. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  175. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  176. package/src/components/ai-support/ai.service.ts +21 -0
  177. package/src/components/ai-support/ai.types.ts +12 -0
  178. package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
  179. package/src/components/app-shell/report-builder-shell.scss +18 -0
  180. package/src/components/dashboards/dashboard-config.schema.ts +97 -0
  181. package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
  182. package/src/components/dashboards/dashboards-admin.scss +62 -0
  183. package/src/components/dashboards/dashboards-page.component.tsx +153 -0
  184. package/src/components/dashboards/dashboards-table.component.tsx +130 -0
  185. package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
  186. package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
  187. package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
  188. package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
  189. package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
  190. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  191. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  192. package/src/components/data-themes/data-themes-page.component.tsx +139 -0
  193. package/src/components/data-themes/data-themes-table.component.tsx +98 -0
  194. package/src/components/data-themes/icon-registry.ts +35 -0
  195. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  196. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  197. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  198. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  199. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  200. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  201. package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
  202. package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
  203. package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
  204. package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
  205. package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
  206. package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
  207. package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
  208. package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
  209. package/src/components/data-visualizer/components/header/header.scss +71 -0
  210. package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
  211. package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
  212. package/src/components/data-visualizer/components/panel/panel.scss +25 -0
  213. package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
  214. package/src/components/data-visualizer/constants.ts +445 -0
  215. package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
  216. package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
  217. package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
  218. package/src/components/data-visualizer/data-visualizer.scss +448 -0
  219. package/src/components/data-visualizer/declarations.d.ts +131 -0
  220. package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
  221. package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
  222. package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
  223. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
  224. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
  225. package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
  226. package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
  227. package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
  228. package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
  229. package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
  230. package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
  231. package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
  232. package/src/components/data-visualizer/types.ts +206 -0
  233. package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
  234. package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
  235. package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
  236. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
  237. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
  238. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
  239. package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
  240. package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
  241. package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
  242. package/src/components/etl-monitor/builder/index.ts +5 -0
  243. package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
  244. package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
  245. package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
  246. package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
  247. package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
  248. package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
  249. package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
  250. package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
  251. package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
  252. package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
  253. package/src/components/etl-monitor/builder/steps/index.ts +5 -0
  254. package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
  255. package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
  256. package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
  257. package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
  258. package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
  259. package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
  260. package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
  261. package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
  262. package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
  263. package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
  264. package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
  265. package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
  266. package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
  267. package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
  268. package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
  269. package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
  270. package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
  271. package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
  272. package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
  273. package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
  274. package/src/components/etl-monitor/renderers/components/index.ts +6 -0
  275. package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
  276. package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
  277. package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
  278. package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
  279. package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
  280. package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
  281. package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
  282. package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
  283. package/src/components/etl-monitor/renderers/index.ts +11 -0
  284. package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
  285. package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
  286. package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
  287. package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
  288. package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
  289. package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
  290. package/src/components/import-export/artifact-groups.ts +153 -0
  291. package/src/components/import-export/export-report-modal.component.tsx +265 -0
  292. package/src/components/import-export/import-export-page.component.tsx +610 -0
  293. package/src/components/import-export/import-export.styles.scss +825 -0
  294. package/src/components/import-export/import-package-modal.component.tsx +401 -0
  295. package/src/components/import-export/index.ts +3 -0
  296. package/src/components/indicators/README.md +24 -0
  297. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  298. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  299. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  300. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  301. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  302. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  303. package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
  304. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
  305. package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
  306. package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
  307. package/src/components/indicators/custom-condition-input.component.tsx +310 -0
  308. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  309. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  310. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  311. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  312. package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
  313. package/src/components/indicators/indicator-search-select.component.tsx +297 -0
  314. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  315. package/src/components/indicators/indicators-page.component.tsx +673 -0
  316. package/src/components/indicators/indicators-page.scss +121 -0
  317. package/src/components/indicators/indicators-table.component.tsx +188 -0
  318. package/src/components/indicators/preview-table.component.tsx +31 -0
  319. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  320. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
  321. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  322. package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
  323. package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
  324. package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
  325. package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
  326. package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
  327. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  328. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  329. package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
  330. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  331. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  332. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  333. package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
  334. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
  335. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  336. package/src/components/indicators/sql-preview.component.tsx +19 -0
  337. package/src/components/indicators/tag-input.component.tsx +177 -0
  338. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  339. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  340. package/src/components/indicators/types/composite-indicator.types.ts +27 -0
  341. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  342. package/src/components/indicators/types/custom-indicator.types.ts +161 -0
  343. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  344. package/src/components/indicators/types/indicator-types.ts +57 -0
  345. package/src/components/indicators/types/sql-builders.ts +43 -0
  346. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
  347. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
  348. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  349. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
  350. package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
  351. package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
  352. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  353. package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
  354. package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
  355. package/src/components/indicators/utils/population-sql.compiler.test.ts +985 -0
  356. package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
  357. package/src/components/landing/landing-action-item.component.tsx +50 -0
  358. package/src/components/landing/landing-action-list.component.tsx +12 -0
  359. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  360. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  361. package/src/components/landing/landing-hero.component.tsx +18 -0
  362. package/src/components/landing/landing-section-card.component.tsx +33 -0
  363. package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
  364. package/src/components/landing/report-builder-landing-page.scss +131 -0
  365. package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
  366. package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
  367. package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
  368. package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
  369. package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
  370. package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
  371. package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
  372. package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
  373. package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
  374. package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
  375. package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
  376. package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
  377. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
  378. package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
  379. package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
  380. package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
  381. package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
  382. package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
  383. package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
  384. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
  385. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
  386. package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
  387. package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
  388. package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
  389. package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
  390. package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
  391. package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
  392. package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
  393. package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
  394. package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
  395. package/src/components/linelist/design/panels/column-category-selector.scss +324 -0
  396. package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
  397. package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
  398. package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
  399. package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
  400. package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
  401. package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
  402. package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
  403. package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
  404. package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
  405. package/src/components/linelist/design/panels/json-preview.scss +85 -0
  406. package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
  407. package/src/components/linelist/design/panels/query-config-panel.scss +341 -0
  408. package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
  409. package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
  410. package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
  411. package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
  412. package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
  413. package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
  414. package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
  415. package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
  416. package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
  417. package/src/components/linelist/linelist-dashboard.page.scss +240 -0
  418. package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
  419. package/src/components/linelist/linelist-editor.page.scss +28 -0
  420. package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
  421. package/src/components/linelist/linelist-run-report.page.scss +136 -0
  422. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
  423. package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
  424. package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
  425. package/src/components/report/dashboard/reports-table.component.tsx +186 -0
  426. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  427. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  428. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  429. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  430. package/src/components/report/definition/report-definition.types.ts +66 -0
  431. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  432. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  433. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  434. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  435. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  436. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  437. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  438. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  439. package/src/components/report/design/report-design-editor.component.tsx +1170 -0
  440. package/src/components/report/design/report-design.types.ts +48 -0
  441. package/src/components/report/design/report-design.utils.ts +86 -0
  442. package/src/components/report/panels/report-detail.component.tsx +71 -0
  443. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  444. package/src/components/report/report-dashboard.page.component.tsx +554 -0
  445. package/src/components/report/report-editor.page.component.tsx +590 -0
  446. package/src/components/report/report.routes.tsx +32 -0
  447. package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
  448. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  449. package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
  450. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  451. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  452. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
  453. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  454. package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
  455. package/src/components/report-sections/report-sections-page.component.tsx +325 -0
  456. package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
  457. package/src/components/report-sections/section-types.ts +55 -0
  458. package/src/components/report-sections/section-utils.ts +50 -0
  459. package/src/components/report-sections/sections-page.scss +79 -0
  460. package/src/components/run-reports/run-reports-page.component.tsx +8 -0
  461. package/src/components/security/access-denied-page.component.tsx +64 -0
  462. package/src/components/security/access-denied-page.scss +39 -0
  463. package/src/components/security/privilege-route.component.test.tsx +76 -0
  464. package/src/components/security/privilege-route.component.tsx +39 -0
  465. package/src/components/shared/compile-setup-modal.component.tsx +182 -0
  466. package/src/components/shared/concept-selector.component.tsx +245 -0
  467. package/src/components/shared/concept-selector.scss +121 -0
  468. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  469. package/src/components/shared/header/header.component.tsx +85 -0
  470. package/src/components/shared/header/header.scss +151 -0
  471. package/src/components/shared/header/illustration.component.tsx +34 -0
  472. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  473. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  474. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  475. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  476. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  477. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  478. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  479. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  480. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  481. package/src/config-schema.ts +43 -0
  482. package/src/constants/privileges.test.ts +54 -0
  483. package/src/constants/privileges.ts +118 -0
  484. package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
  485. package/src/features/dashboards/DashboardsPage.tsx +164 -0
  486. package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
  487. package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
  488. package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
  489. package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
  490. package/src/features/dashboards/components/DashboardSection.tsx +46 -0
  491. package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
  492. package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
  493. package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
  494. package/src/features/dashboards/dashboards.scss +196 -0
  495. package/src/features/dashboards/hooks/useDashboard.ts +61 -0
  496. package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
  497. package/src/features/dashboards/hooks/useDashboards.ts +56 -0
  498. package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
  499. package/src/features/dashboards/index.ts +11 -0
  500. package/src/features/dashboards/utils/dashboard-config.util.test.ts +286 -0
  501. package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
  502. package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
  503. package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
  504. package/src/features/report-visualizer/components/Export/index.ts +2 -0
  505. package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
  506. package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
  507. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
  508. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
  509. package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
  510. package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
  511. package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
  512. package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
  513. package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
  514. package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
  515. package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
  516. package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
  517. package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
  518. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
  519. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
  520. package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
  521. package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
  522. package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
  523. package/src/features/report-visualizer/components/TableView/index.ts +2 -0
  524. package/src/features/report-visualizer/hooks/index.ts +4 -0
  525. package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
  526. package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
  527. package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
  528. package/src/features/report-visualizer/hooks/useReports.ts +135 -0
  529. package/src/features/report-visualizer/index.ts +6 -0
  530. package/src/features/report-visualizer/report-visualizer.scss +593 -0
  531. package/src/features/report-visualizer/types/index.ts +1 -0
  532. package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
  533. package/src/features/report-visualizer/utils/status-colors.ts +117 -0
  534. package/src/globals.d.ts +9 -0
  535. package/src/hooks/address-template.ts +86 -0
  536. package/src/hooks/concept.ts +112 -0
  537. package/src/hooks/indicator/index.ts +1 -0
  538. package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
  539. package/src/hooks/openmrs-reference-data.ts +432 -0
  540. package/src/hooks/theme/index.ts +4 -0
  541. package/src/hooks/theme/useDataTheme.ts +61 -0
  542. package/src/hooks/theme/useDataThemes.ts +28 -0
  543. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  544. package/src/hooks/theme/useMambaTables.ts +95 -0
  545. package/src/hooks/use-report-builder-privileges.test.tsx +92 -0
  546. package/src/hooks/use-report-builder-privileges.ts +58 -0
  547. package/src/index.ts +29 -0
  548. package/src/resources/address-template/address-template.api.ts +180 -0
  549. package/src/resources/agegroup/agegroups.api.ts +58 -0
  550. package/src/resources/concept/concept.api.ts +116 -0
  551. package/src/resources/concepts/concept-mapper.ts +21 -0
  552. package/src/resources/concepts/concept-types.ts +31 -0
  553. package/src/resources/concepts/concepts.resource.ts +44 -0
  554. package/src/resources/concepts/useConceptSearch.ts +41 -0
  555. package/src/resources/dashboard/dashboard.api.ts +107 -0
  556. package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
  557. package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
  558. package/src/resources/etl-source/etl-source.api.ts +65 -0
  559. package/src/resources/etl-task/etl-task.api.ts +96 -0
  560. package/src/resources/etl-task/etl-task.types.ts +31 -0
  561. package/src/resources/indicator/indicators.api.ts +125 -0
  562. package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
  563. package/src/resources/linelist/linelist-reports.api.ts +616 -0
  564. package/src/resources/location/location.api.ts +135 -0
  565. package/src/resources/openmrs-api.ts +40 -0
  566. package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
  567. package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
  568. package/src/resources/preview/sql-preview.api.ts +54 -0
  569. package/src/resources/report/reports.api.ts +140 -0
  570. package/src/resources/report-category/report-category.api.ts +41 -0
  571. package/src/resources/report-import-export/import-export-api.ts +164 -0
  572. package/src/resources/report-library/report-library.api.ts +63 -0
  573. package/src/resources/report-section/report-sections.api.ts +73 -0
  574. package/src/resources/report-section/section-preview.api.ts +25 -0
  575. package/src/resources/theme/data-theme.api.ts +88 -0
  576. package/src/resources/theme/etl-schema.api.ts +101 -0
  577. package/src/resources/theme/etl-table-meta.api.ts +45 -0
  578. package/src/root.component.tsx +20 -0
  579. package/src/routes/report-builder.routes.tsx +160 -0
  580. package/src/routes/report-builder.scss +56 -0
  581. package/src/routes.json +31 -0
  582. package/src/sample-template.ts +121 -0
  583. package/src/template-utils.ts +110 -0
  584. package/src/types/carbon-react-inert.d.ts +13 -0
  585. package/src/types/condition-operators.ts +63 -0
  586. package/src/types/dashboard/dashboard.types.ts +137 -0
  587. package/src/types/dashboard/index.ts +4 -0
  588. package/src/types/etl/etl-types.ts +238 -0
  589. package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
  590. package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
  591. package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
  592. package/src/types/etl-monitor/index.ts +6 -0
  593. package/src/types/linelist/compile-config.ts +455 -0
  594. package/src/types/linelist-types.ts +2059 -0
  595. package/src/types/theme/data-theme.types.ts +88 -0
  596. package/src/utils/api-error.utils.test.ts +147 -0
  597. package/src/utils/api-error.utils.ts +131 -0
  598. package/src/utils/config-enhancer.ts +62 -0
  599. package/src/utils/etl-browser/etl-browser.util.test.ts +151 -0
  600. package/src/utils/etl-browser/export-csv.util.ts +43 -0
  601. package/src/utils/etl-browser/group-tables.util.ts +84 -0
  602. package/src/utils/etl-browser/index.ts +4 -0
  603. package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
  604. package/src/utils/etl-browser/table-sql.util.ts +20 -0
  605. package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
  606. package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
  607. package/src/utils/etl-monitor/index.ts +10 -0
  608. package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
  609. package/src/utils/etl-monitor/normalization-examples.ts +0 -0
  610. package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
  611. package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
  612. package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
  613. package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
  614. package/src/utils/etl-task/etl-task-card.util.test.ts +240 -0
  615. package/src/utils/etl-task/etl-task-card.util.ts +276 -0
  616. package/src/utils/etl-task/index.ts +1 -0
  617. package/src/utils/html-entities.utils.ts +34 -0
  618. package/src/utils/parameter-resolution.ts +218 -0
  619. package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
  620. package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
  621. package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
  622. package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
  623. package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
  624. package/tools/i18next-parser.config.js +0 -0
  625. package/tools/setup-tests.ts +1 -0
  626. package/tools/update-openmrs-deps.mjs +42 -0
  627. package/translations/am.json +65 -0
  628. package/translations/ar.json +65 -0
  629. package/translations/ar_SY.json +65 -0
  630. package/translations/bn.json +65 -0
  631. package/translations/cs.json +65 -0
  632. package/translations/de.json +65 -0
  633. package/translations/en.json +65 -0
  634. package/translations/en_US.json +65 -0
  635. package/translations/es.json +65 -0
  636. package/translations/es_MX.json +65 -0
  637. package/translations/fr.json +65 -0
  638. package/translations/he.json +65 -0
  639. package/translations/hi.json +65 -0
  640. package/translations/hi_IN.json +65 -0
  641. package/translations/id.json +65 -0
  642. package/translations/it.json +65 -0
  643. package/translations/ka.json +65 -0
  644. package/translations/km.json +65 -0
  645. package/translations/ku.json +65 -0
  646. package/translations/ky.json +65 -0
  647. package/translations/lg.json +65 -0
  648. package/translations/ne.json +65 -0
  649. package/translations/pl.json +65 -0
  650. package/translations/pt.json +65 -0
  651. package/translations/pt_BR.json +65 -0
  652. package/translations/qu.json +65 -0
  653. package/translations/ro_RO.json +65 -0
  654. package/translations/ru_RU.json +65 -0
  655. package/translations/si.json +65 -0
  656. package/translations/sq.json +65 -0
  657. package/translations/sw.json +65 -0
  658. package/translations/sw_KE.json +65 -0
  659. package/translations/tr.json +65 -0
  660. package/translations/tr_TR.json +65 -0
  661. package/translations/uk.json +65 -0
  662. package/translations/uz.json +65 -0
  663. package/translations/uz@Latn.json +65 -0
  664. package/translations/uz_UZ.json +65 -0
  665. package/translations/vi.json +65 -0
  666. package/translations/zh.json +65 -0
  667. package/translations/zh_CN.json +65 -0
  668. package/translations/zh_TW.json +65 -0
  669. package/tsconfig.json +24 -0
  670. package/vitest.config.mts +42 -0
@@ -0,0 +1,1062 @@
1
+ /**
2
+ * Data Catalogue Component for Linelist Reports
3
+ *
4
+ * Left panel component that provides searchable, grouped access to available fields.
5
+ * Integrates with existing ETL metadata infrastructure.
6
+ *
7
+ * Based on design spec section 7: Left panel — Data catalogue
8
+ */
9
+
10
+ import React, { useState, useCallback, useMemo, useEffect } from 'react';
11
+ import {
12
+ TextInput,
13
+ Button,
14
+ Tag,
15
+ Accordion,
16
+ AccordionItem,
17
+ InlineNotification,
18
+ Toggle,
19
+ } from '@carbon/react';
20
+ import { User, Tag as TagIcon, Function, Hashtag } from '@carbon/react/icons';
21
+
22
+ // import { useETLTables } from '../../../../hooks/theme';
23
+ import { useETLTableMeta } from '../../../../hooks/theme';
24
+ // import { useDataThemes } from '../../../../hooks/theme';
25
+ // TableColumn type imported as needed
26
+ import type { FilterFieldType, DataSourceInfo, PopulationSource, LinelistColumnDraft } from '../../../../types/linelist-types';
27
+ import type {
28
+ EtlStructure,
29
+ } from '../../../../types/etl/etl-types';
30
+ import { listPersonAttributeTypes, type PersonAttributeTypeDto } from '../../../../resources/person-attribute-type/person-attribute-type.api';
31
+ import { listPatientIdentifierTypes, type PatientIdentifierTypeDto } from '../../../../resources/patient-identifier-type/patient-identifier-type.api';
32
+ import { useAddressFields } from '../../../../hooks/address-template';
33
+ import CustomSqlColumnModal, { type CustomSqlColumnConfig } from '../../definition/panels/custom-sql-column-modal.component';
34
+ import PopulationSourceSelector from '../../definition/panels/population-source-selector.component';
35
+ import ObservationColumnModal from '../../definition/panels/observation-column-modal.component';
36
+ import EncounterDiagnosisColumnModal from '../../definition/panels/encounter-diagnosis-column-modal.component';
37
+ import styles from './data-catalogue.scss';
38
+
39
+ /**
40
+ * Field group definitions based on clinical domain
41
+ * Maps field patterns to display groups
42
+ */
43
+ const FIELD_GROUPS = [
44
+ {
45
+ id: 'demographics',
46
+ name: 'Demographics',
47
+ description: 'Patient demographic information',
48
+ fieldPatterns: ['given_name', 'family_name', 'person_name', 'sex', 'gender', 'birth_date', 'age', 'death_date'],
49
+ icon: User,
50
+ },
51
+ {
52
+ id: 'api-person-attributes',
53
+ name: 'Person Attributes',
54
+ description: 'Custom person attributes from OpenMRS (Telephone, Civil Status, etc.)',
55
+ fieldPatterns: [], // No pattern matching - populated via API
56
+ icon: TagIcon,
57
+ isApiDriven: true, // Flag for API-driven groups
58
+ },
59
+ {
60
+ id: 'openmrs-calculated',
61
+ name: 'Calculated Fields',
62
+ description: 'Fields calculated from other data',
63
+ fieldPatterns: ['age', 'bmi'],
64
+ icon: Function,
65
+ },
66
+ {
67
+ id: 'api-patient-identifiers',
68
+ name: 'Patient Identifiers',
69
+ description: 'Patient identification numbers from OpenMRS (Clinic No, National ID, etc.)',
70
+ fieldPatterns: [], // No pattern matching - populated via API
71
+ icon: Hashtag,
72
+ isApiDriven: true, // Flag for API-driven groups
73
+ },
74
+ {
75
+ id: 'api-observations',
76
+ name: 'Observations',
77
+ description: 'Clinical observations from OpenMRS based on concepts (e.g., Weight, Height, Temperature)',
78
+ fieldPatterns: [], // No pattern matching - populated via API
79
+ icon: '📊',
80
+ isApiDriven: true, // Flag for API-driven groups
81
+ isConceptBased: true, // New flag for concept-based groups
82
+ },
83
+ {
84
+ id: 'api-encounter-diagnoses',
85
+ name: 'Encounter Diagnoses',
86
+ description: 'Diagnoses from OpenMRS encounters based on concepts (ICD coded conditions)',
87
+ fieldPatterns: [], // No pattern matching - populated via API
88
+ icon: '🏥',
89
+ isApiDriven: true, // Flag for API-driven groups
90
+ isConceptBased: true, // New flag for concept-based groups
91
+ },
92
+ {
93
+ id: 'openmrs-person-name',
94
+ name: 'Person Name',
95
+ description: 'Patient name components',
96
+ fieldPatterns: ['given_name', 'family_name', 'middle_name', 'person_name'],
97
+ icon: User,
98
+ },
99
+ {
100
+ id: 'openmrs-address',
101
+ name: 'Address & Contact',
102
+ description: 'Address and contact information',
103
+ fieldPatterns: ['address', 'address1', 'address2', 'address3', 'address4', 'address5', 'village', 'parish', 'subcounty', 'county', 'district', 'state', 'stateProvince', 'countyDistrict', 'city', 'cityVillage', 'phone', 'telephone'],
104
+ icon: '📍',
105
+ },
106
+ {
107
+ id: 'addresses',
108
+ name: 'Addresses & Contacts',
109
+ description: 'Patient address and contact information',
110
+ fieldPatterns: ['address', 'address1', 'address2', 'address3', 'address4', 'address5', 'city', 'village', 'parish', 'subcounty', 'county', 'district', 'state', 'stateProvince', 'countyDistrict', 'country', 'phone', 'telephone', 'email'],
111
+ icon: '📍',
112
+ },
113
+ {
114
+ id: 'encounters',
115
+ name: 'Encounters & Visits',
116
+ description: 'Encounter and visit information',
117
+ fieldPatterns: ['encounter', 'visit', 'visit_date', 'encounter_date', 'encounter_type', 'location'],
118
+ icon: '🏥',
119
+ },
120
+ {
121
+ id: 'observations',
122
+ name: 'Observations',
123
+ description: 'Clinical observations and vitals',
124
+ fieldPatterns: ['obs', 'observation', 'value', 'concept', 'weight', 'height', 'bmi', 'temperature', 'blood_pressure'],
125
+ icon: '📊',
126
+ },
127
+ {
128
+ id: 'programs',
129
+ name: 'Programs & Enrollments',
130
+ description: 'Program enrollment and status',
131
+ fieldPatterns: ['program', 'enrollment', 'enrol', 'program_enrollment', 'regimen', 'art', 'hiv', 'tb'],
132
+ icon: '📋',
133
+ },
134
+ {
135
+ id: 'appointments',
136
+ name: 'Appointments',
137
+ description: 'Scheduled appointments',
138
+ fieldPatterns: ['appointment', 'return_visit', 'appointment_date', 'fulfillment', 'scheduled'],
139
+ icon: '📅',
140
+ },
141
+ {
142
+ id: 'laboratory',
143
+ name: 'Laboratory',
144
+ description: 'Lab results and orders',
145
+ fieldPatterns: ['lab', 'laboratory', 'test', 'result', 'specimen', 'order_date', 'result_date'],
146
+ icon: '🔬',
147
+ },
148
+ {
149
+ id: 'medications',
150
+ name: 'Medications',
151
+ description: 'Drug prescriptions and dispenses',
152
+ fieldPatterns: ['drug', 'medication', 'prescription', 'dispense', 'regimen', 'dose'],
153
+ icon: '💊',
154
+ },
155
+ ];
156
+
157
+ /**
158
+ * Common OpenMRS Calculated Fields
159
+ * These are always available regardless of selected ETL table
160
+ */
161
+ const CALCULATED_FIELDS: CatalogueField[] = [
162
+ {
163
+ id: 'openmrs.calc.age',
164
+ name: 'age',
165
+ label: 'Age',
166
+ type: 'NUMBER',
167
+ source: 'CALCULATION',
168
+ table: 'person',
169
+ isRepeated: false,
170
+ description: 'Age in years (as of report end date)',
171
+ },
172
+ {
173
+ id: 'openmrs.calc.age_months',
174
+ name: 'age_months',
175
+ label: 'Age (Months)',
176
+ type: 'NUMBER',
177
+ source: 'CALCULATION',
178
+ table: 'person',
179
+ isRepeated: false,
180
+ description: 'Age in months (as of report end date)',
181
+ },
182
+ {
183
+ id: 'openmrs.calc.age_years',
184
+ name: 'age_years',
185
+ label: 'Age (Years)',
186
+ type: 'NUMBER',
187
+ source: 'CALCULATION',
188
+ table: 'person',
189
+ isRepeated: false,
190
+ description: 'Age in whole years (as of report end date)',
191
+ },
192
+ {
193
+ id: 'openmrs.calc.bmi',
194
+ name: 'bmi',
195
+ label: 'BMI',
196
+ type: 'NUMBER',
197
+ source: 'CALCULATION',
198
+ table: 'person',
199
+ isRepeated: false,
200
+ description: 'Body Mass Index',
201
+ },
202
+ ];
203
+
204
+ /**
205
+ * Common OpenMRS Person/Patient fields
206
+ * These are always available regardless of selected ETL table
207
+ */
208
+ const DEMOGRAPHIC_FIELDS: CatalogueField[] = [
209
+ // Person Name
210
+ {
211
+ id: 'openmrs.person.given_name',
212
+ name: 'given_name',
213
+ label: 'Given Name',
214
+ type: 'TEXT',
215
+ source: 'CORE',
216
+ table: 'person',
217
+ isRepeated: false,
218
+ description: 'Patient given name',
219
+ },
220
+ {
221
+ id: 'openmrs.person.family_name',
222
+ name: 'family_name',
223
+ label: 'Family Name',
224
+ type: 'TEXT',
225
+ source: 'CORE',
226
+ table: 'person',
227
+ isRepeated: false,
228
+ description: 'Patient family name/surname',
229
+ },
230
+ {
231
+ id: 'openmrs.person.full_name',
232
+ name: 'full_name',
233
+ label: 'Full Name',
234
+ type: 'TEXT',
235
+ source: 'CORE',
236
+ table: 'person',
237
+ isRepeated: false,
238
+ description: 'Complete patient name',
239
+ },
240
+ // Person Attributes
241
+ {
242
+ id: 'openmrs.person.gender',
243
+ name: 'gender',
244
+ label: 'Gender',
245
+ type: 'CODED',
246
+ source: 'CORE',
247
+ table: 'person',
248
+ isRepeated: false,
249
+ description: 'Patient gender',
250
+ },
251
+ {
252
+ id: 'openmrs.person.birthdate',
253
+ name: 'birthdate',
254
+ label: 'Birth Date',
255
+ type: 'DATE',
256
+ source: 'CORE',
257
+ table: 'person',
258
+ isRepeated: false,
259
+ description: 'Patient birth date',
260
+ },
261
+ {
262
+ id: 'openmrs.person.death_date',
263
+ name: 'death_date',
264
+ label: 'Death Date',
265
+ type: 'DATE',
266
+ source: 'CORE',
267
+ table: 'person',
268
+ isRepeated: false,
269
+ description: 'Patient death date',
270
+ },
271
+ ];
272
+
273
+ /**
274
+ * Map SQL column types to FilterFieldType
275
+ */
276
+ function mapColumnTypeToFilterFieldType(sqlType?: string): FilterFieldType {
277
+ if (!sqlType) return 'TEXT';
278
+
279
+ const type = sqlType.toLowerCase();
280
+
281
+ // Integer types
282
+ if (type.includes('int') || type.includes('serial') || type.includes('bigint')) {
283
+ return 'NUMBER';
284
+ }
285
+
286
+ // Decimal/numeric types
287
+ if (type.includes('decimal') || type.includes('numeric') || type.includes('float') || type.includes('double')) {
288
+ return 'NUMBER';
289
+ }
290
+
291
+ // Date/time types
292
+ if (type.includes('date') || type.includes('time') || type.includes('timestamp')) {
293
+ return 'DATE';
294
+ }
295
+
296
+ // Boolean type
297
+ if (type.includes('bool') || type.includes('bit')) {
298
+ return 'BOOLEAN';
299
+ }
300
+
301
+ // Default to text
302
+ return 'TEXT';
303
+ }
304
+
305
+ /**
306
+ * Get field source type
307
+ */
308
+ function getFieldType(columnName: string, table: string): 'CORE' | 'ETL' | 'CALCULATION' {
309
+ // Add calculation detection logic here
310
+ if (columnName.includes('age') || columnName.includes('bmi')) {
311
+ return 'CALCULATION';
312
+ }
313
+
314
+ // ETL tables typically have specific prefixes
315
+ if (table.includes('_etl') || table.includes('_fact') || table.includes('_dim')) {
316
+ return 'ETL';
317
+ }
318
+
319
+ return 'CORE';
320
+ }
321
+
322
+ /**
323
+ * Categorize a field into a group
324
+ */
325
+ function categorizeField(fieldName: string): string | null {
326
+ const lowerField = fieldName.toLowerCase();
327
+
328
+ for (const group of FIELD_GROUPS) {
329
+ for (const pattern of group.fieldPatterns) {
330
+ if (lowerField.includes(pattern.toLowerCase())) {
331
+ return group.id;
332
+ }
333
+ }
334
+ }
335
+
336
+ return null;
337
+ }
338
+
339
+ /**
340
+ * Check if a field can have multiple values per patient
341
+ */
342
+ function isPotentiallyRepeated(fieldName: string): boolean {
343
+ const lowerField = fieldName.toLowerCase();
344
+ const repeatedKeywords = [
345
+ 'appointment', 'visit', 'encounter', 'obs', 'observation',
346
+ 'lab', 'test', 'result', 'program', 'enrollment', 'drug',
347
+ 'medication', 'regimen'
348
+ ];
349
+
350
+ return repeatedKeywords.some(keyword => lowerField.includes(keyword));
351
+ }
352
+
353
+ type Props = {
354
+ /** Legacy: Single table name (deprecated, use dataSources) */
355
+ table?: string;
356
+ /** Multiple datasources to browse columns from */
357
+ dataSources?: DataSourceInfo[];
358
+ /** Population sources for cohort definition */
359
+ populationSources?: PopulationSource[];
360
+ /** Callback when population sources change */
361
+ onPopulationSourcesChange?: (populationSources: PopulationSource[]) => void;
362
+ /** Show population sources selector (for SQL/Hybrid modes) */
363
+ showPopulationSelector?: boolean;
364
+ onAddToColumns: (field: CatalogueField) => void;
365
+ onAddToFilters: (field: CatalogueField) => void;
366
+ onTableChange?: (table: string) => void;
367
+ onDataSourcesChange?: (dataSources: DataSourceInfo[]) => void;
368
+ onFieldsAvailable?: (fields: CatalogueField[]) => void; // Callback to expose available fields to parent
369
+ onEtlStructureDetected?: (structure: EtlStructure) => void; // Callback when ETL structure is detected
370
+ selectedFields?: string[]; // Field IDs that are already selected
371
+ disabled?: boolean;
372
+ /** Callback to add a custom SQL column */
373
+ onAddCustomSqlColumn?: (config: CustomSqlColumnConfig) => void;
374
+ /** Callback to add a draft column (for observation/diagnosis columns) */
375
+ onAddDraftColumn?: (column: LinelistColumnDraft) => void;
376
+ /** The patient/client ID column alias used by the base cohort SQL */
377
+ idColumnAlias?: 'patient_id' | 'patient_id';
378
+ };
379
+
380
+ /**
381
+ * Field in the catalogue
382
+ */
383
+ export type CatalogueField = {
384
+ id: string;
385
+ name: string;
386
+ label: string;
387
+ type: FilterFieldType;
388
+ source: 'CORE' | 'ETL' | 'CALCULATION';
389
+ table: string;
390
+ isRepeated: boolean;
391
+ description?: string;
392
+ /** Which datasource this field comes from */
393
+ dataSourceUuid?: string;
394
+ /** Display name of the datasource */
395
+ dataSourceName?: string;
396
+ };
397
+
398
+ const DataCatalogue: React.FC<Props> = ({
399
+ table: legacyTable,
400
+ dataSources: propDataSources,
401
+ populationSources: propPopulationSources,
402
+ onPopulationSourcesChange,
403
+ showPopulationSelector = true,
404
+ onAddToColumns,
405
+ onAddToFilters,
406
+ onFieldsAvailable,
407
+ onEtlStructureDetected,
408
+ selectedFields = [],
409
+ disabled = false,
410
+ onAddCustomSqlColumn,
411
+ onAddDraftColumn,
412
+ idColumnAlias = 'patient_id',
413
+ }) => {
414
+ const [searchTerm, setSearchTerm] = useState('');
415
+ const [showSelectedOnly, setShowSelectedOnly] = useState(false);
416
+ const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
417
+ const [etlStructure, setEtlStructure] = useState<EtlStructure | null>(null);
418
+ const [customSqlModalOpen, setCustomSqlModalOpen] = useState(false);
419
+ const [observationModalOpen, setObservationModalOpen] = useState(false);
420
+ const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false);
421
+
422
+ // State for API-fetched data
423
+ const [personAttributeTypes, setPersonAttributeTypes] = useState<Array<PersonAttributeTypeDto>>([]);
424
+ const [patientIdentifierTypes, setPatientIdentifierTypes] = useState<Array<PatientIdentifierTypeDto>>([]);
425
+ const [apiDataLoading, setApiDataLoading] = useState(true);
426
+ const [apiDataError, setApiDataError] = useState<string | null>(null);
427
+
428
+ // Internal state for population sources (if not controlled)
429
+ const [internalPopulationSources, setInternalPopulationSources] = useState<PopulationSource[]>([]);
430
+ const populationSources = propPopulationSources ?? internalPopulationSources;
431
+
432
+ // Handle population sources change
433
+ const handlePopulationSourcesChange = (newSources: PopulationSource[]) => {
434
+ setInternalPopulationSources(newSources);
435
+ if (onPopulationSourcesChange) {
436
+ onPopulationSourcesChange(newSources);
437
+ }
438
+ };
439
+
440
+ // Fetch dynamic address fields from OpenMRS address template
441
+ const { addressFields } = useAddressFields();
442
+
443
+ // Get available tables
444
+ // const { tables, loading: tablesLoading, error: tablesError } = useETLTables(true);
445
+
446
+ // Use new dataSources prop or fall back to legacy single table
447
+ const dataSources = useMemo(() => {
448
+ return propDataSources || (legacyTable ? [{
449
+ uuid: legacyTable,
450
+ name: legacyTable,
451
+ type: 'ETL',
452
+ role: 'PRIMARY',
453
+ tables: [],
454
+ }] : []);
455
+ }, [propDataSources, legacyTable]);
456
+
457
+ // Get the primary datasource for single-table mode compatibility
458
+ const primaryDataSource = dataSources.find(ds => ds.role === 'PRIMARY');
459
+ const primaryTable = primaryDataSource?.uuid || legacyTable || '';
460
+ const primaryDatasourceName = primaryDataSource?.name || '';
461
+
462
+ // Get columns for selected table (primary datasource)
463
+ const {
464
+ columns,
465
+ loading: columnsLoading,
466
+ error: columnsError,
467
+ } = useETLTableMeta(primaryTable, Boolean(primaryTable));
468
+
469
+ // Fetch person attribute types and patient identifier types on mount
470
+ useEffect(() => {
471
+ const fetchApiData = async () => {
472
+ try {
473
+ setApiDataLoading(true);
474
+ setApiDataError(null);
475
+
476
+ const [attributes, identifiers] = await Promise.all([
477
+ listPersonAttributeTypes(true),
478
+ listPatientIdentifierTypes(true),
479
+ ]);
480
+
481
+ setPersonAttributeTypes(attributes);
482
+ setPatientIdentifierTypes(identifiers);
483
+ } catch (error) {
484
+ console.error('Failed to fetch person attributes/identifiers:', error);
485
+ setApiDataError('Failed to load person attributes and identifiers from server');
486
+ } finally {
487
+ setApiDataLoading(false);
488
+ }
489
+ };
490
+
491
+ fetchApiData();
492
+ }, []);
493
+
494
+ /**
495
+ * Organize columns into field groups
496
+ */
497
+ const groupedFields = useMemo(() => {
498
+ const groups: Record<string, CatalogueField[]> = {};
499
+
500
+ // Initialize empty arrays for all groups
501
+ FIELD_GROUPS.forEach((group) => {
502
+ groups[group.id] = [];
503
+ });
504
+
505
+ // Add "uncategorized" group for fields that don't match any group
506
+ groups['uncategorized'] = [];
507
+
508
+ // Add calculated fields (always available)
509
+ groups['openmrs-calculated'] = CALCULATED_FIELDS;
510
+
511
+ // Add demographic fields (always available)
512
+ groups['demographics'] = DEMOGRAPHIC_FIELDS;
513
+
514
+ // Add dynamic address fields from OpenMRS address template
515
+ groups['openmrs-address'] = addressFields;
516
+
517
+ // Add API-fetched person attribute types
518
+ groups['api-person-attributes'] = personAttributeTypes
519
+ .filter((attr) => !attr.retired) // Filter out retired attributes
520
+ .map((attr) => ({
521
+ id: `person-attribute-${attr.uuid}`,
522
+ name: attr.uuid,
523
+ label: attr.display || attr.name || attr.uuid,
524
+ type: 'TEXT' as FilterFieldType,
525
+ source: 'CORE' as const,
526
+ table: 'person_attribute',
527
+ isRepeated: false,
528
+ description: attr.description || 'Custom person attribute',
529
+ }));
530
+
531
+ // Add API-fetched patient identifier types
532
+ groups['api-patient-identifiers'] = patientIdentifierTypes
533
+ .filter((id) => !id.retired) // Filter out retired identifiers
534
+ .map((idType) => ({
535
+ id: `patient-identifier-${idType.uuid}`,
536
+ name: idType.uuid,
537
+ label: idType.display || idType.name || idType.uuid,
538
+ type: 'TEXT' as FilterFieldType,
539
+ source: 'CORE' as const,
540
+ table: 'patient_identifier',
541
+ isRepeated: true, // Patients can have multiple identifiers of the same type
542
+ description: idType.description || 'Patient identifier',
543
+ }));
544
+
545
+ // Add placeholder for concept-based observations
546
+ // These will be dynamically added by users via concept search
547
+ groups['api-observations'] = [];
548
+
549
+ // Add placeholder for concept-based encounter diagnoses
550
+ // These will be dynamically added by users via concept search
551
+ groups['api-encounter-diagnoses'] = [];
552
+
553
+ // Categorize each column from the selected table
554
+ if (columns && columns.length > 0) {
555
+ columns.forEach((column) => {
556
+ const groupId = categorizeField(column.name) || 'uncategorized';
557
+
558
+ // Skip groups that are pre-populated with static/dynamic fields
559
+ // to avoid duplicates with ETL table columns
560
+ if (groupId === 'demographics' || groupId === 'openmrs-calculated') {
561
+ return;
562
+ }
563
+
564
+ const field: CatalogueField = {
565
+ id: `${primaryTable}.${column.name}`,
566
+ name: column.name,
567
+ label: formatFieldLabel(column.name),
568
+ type: mapColumnTypeToFilterFieldType(column.type),
569
+ source: getFieldType(column.name, primaryTable),
570
+ table: primaryTable,
571
+ isRepeated: isPotentiallyRepeated(column.name),
572
+ dataSourceUuid: dataSources.find(ds => ds.uuid === primaryTable)?.uuid || primaryTable,
573
+ dataSourceName: dataSources.find(ds => ds.uuid === primaryTable)?.name || primaryTable,
574
+ };
575
+
576
+ groups[groupId].push(field);
577
+ });
578
+ }
579
+
580
+ return groups;
581
+ }, [columns, primaryTable, personAttributeTypes, patientIdentifierTypes, dataSources, addressFields]);
582
+
583
+ /**
584
+ * Expose all available fields to parent component
585
+ */
586
+ useEffect(() => {
587
+ if (onFieldsAvailable) {
588
+ const allFields: CatalogueField[] = Object.values(groupedFields).flat();
589
+ onFieldsAvailable(allFields);
590
+ }
591
+ }, [groupedFields, onFieldsAvailable]);
592
+
593
+ /**
594
+ * Detect ETL structure when table/columns change
595
+ */
596
+ useEffect(() => {
597
+ if (!primaryTable || !columns || columns.length === 0) {
598
+ return;
599
+ }
600
+
601
+ // Import the detection function dynamically to avoid circular deps
602
+ import('../../../../types/etl/etl-types').then(({ detectEtlStructure }) => {
603
+ const columnNames = columns.map((c) => c.name);
604
+ const detected = detectEtlStructure(primaryTable, columnNames);
605
+ setEtlStructure(detected);
606
+
607
+ if (onEtlStructureDetected) {
608
+ onEtlStructureDetected(detected);
609
+ }
610
+ });
611
+ }, [primaryTable, columns, onEtlStructureDetected]);
612
+
613
+ /**
614
+ * Filter fields based on search term and selection toggle
615
+ */
616
+ const filteredGroupedFields = useMemo(() => {
617
+ let result = groupedFields;
618
+ const selectedSet = new Set(selectedFields);
619
+
620
+ // By default, exclude selected fields from the available options
621
+ // (unless showSelectedOnly is enabled, then show ONLY selected fields)
622
+ if (!showSelectedOnly && selectedSet.size > 0) {
623
+ const filtered: Record<string, CatalogueField[]> = {};
624
+ Object.entries(groupedFields).forEach(([groupId, fields]) => {
625
+ const availableFields = fields.filter((field) => !selectedSet.has(field.id));
626
+ if (availableFields.length > 0) {
627
+ filtered[groupId] = availableFields;
628
+ }
629
+ });
630
+ result = filtered;
631
+ } else if (showSelectedOnly) {
632
+ // Show only selected fields
633
+ const filtered: Record<string, CatalogueField[]> = {};
634
+ Object.entries(groupedFields).forEach(([groupId, fields]) => {
635
+ const matchingFields = fields.filter((field) => selectedSet.has(field.id));
636
+ if (matchingFields.length > 0) {
637
+ filtered[groupId] = matchingFields;
638
+ }
639
+ });
640
+ result = filtered;
641
+ }
642
+
643
+ // Apply search filter
644
+ if (searchTerm) {
645
+ const lowerSearch = searchTerm.toLowerCase();
646
+ const filtered: Record<string, CatalogueField[]> = {};
647
+ Object.entries(result).forEach(([groupId, fields]) => {
648
+ const matchingFields = fields.filter(
649
+ (field) =>
650
+ field.name.toLowerCase().includes(lowerSearch) ||
651
+ field.label.toLowerCase().includes(lowerSearch)
652
+ );
653
+ if (matchingFields.length > 0) {
654
+ filtered[groupId] = matchingFields;
655
+ }
656
+ });
657
+ result = filtered;
658
+ }
659
+
660
+ return result;
661
+ }, [groupedFields, searchTerm, showSelectedOnly, selectedFields]);
662
+
663
+ /**
664
+ * Toggle group expansion
665
+ */
666
+ const toggleGroup = useCallback((groupId: string, event?: React.MouseEvent) => {
667
+ event?.stopPropagation();
668
+ setExpandedGroups((prev) => {
669
+ const next = new Set(prev);
670
+ if (next.has(groupId)) {
671
+ next.delete(groupId);
672
+ } else {
673
+ next.add(groupId);
674
+ }
675
+ return next;
676
+ });
677
+ }, []);
678
+
679
+ /**
680
+ * Expand all groups
681
+ */
682
+ const expandAll = useCallback(() => {
683
+ setExpandedGroups(new Set(Object.keys(filteredGroupedFields)));
684
+ }, [filteredGroupedFields]);
685
+
686
+ /**
687
+ * Collapse all groups
688
+ */
689
+ const collapseAll = useCallback(() => {
690
+ setExpandedGroups(new Set());
691
+ }, []);
692
+
693
+ const isLoading = columnsLoading || apiDataLoading;
694
+ const error = columnsError || apiDataError;
695
+ const totalFields = Object.values(filteredGroupedFields).reduce((sum, fields) => sum + fields.length, 0);
696
+
697
+ return (
698
+ <div className={styles.catalogue}>
699
+ {/* Population Sources Selector */}
700
+ {showPopulationSelector && onPopulationSourcesChange && (
701
+ <div className={styles.populationSourcesSection}>
702
+ <PopulationSourceSelector
703
+ populationSources={populationSources}
704
+ onChange={handlePopulationSourcesChange}
705
+ disabled={disabled}
706
+ showAdvanced={false}
707
+ />
708
+ </div>
709
+ )}
710
+
711
+ {/* ETL Structure Indicator */}
712
+ {etlStructure && (
713
+ <div className={styles.etlIndicator}>
714
+ <div className={styles.etlTypeBadge}>
715
+ <span className={styles.etlTypeIcon}>
716
+ {etlStructure.type === 'VERTICAL' ? '📊' : '📋'}
717
+ </span>
718
+ <span className={styles.etlTypeLabel}>
719
+ {etlStructure.type === 'VERTICAL' ? 'Vertical ETL' : 'Horizontal ETL'}
720
+ </span>
721
+ </div>
722
+ <div className={styles.etlDescription}>
723
+ {etlStructure.type === 'VERTICAL' ? (
724
+ <>
725
+ Data stored in long format. Filter by{' '}
726
+ <strong>{etlStructure.dataIdentifierColumn || 'concept'}</strong> to select specific data points.
727
+ </>
728
+ ) : (
729
+ <>
730
+ Data stored in wide format. Columns are directly selectable data points.
731
+ </>
732
+ )}
733
+ </div>
734
+ {etlStructure.type === 'VERTICAL' && etlStructure.requiredFilters && (
735
+ <div className={styles.etlHint}>
736
+ <strong>Required filters:</strong> {etlStructure.requiredFilters.map(f => f.column).join(', ')}
737
+ </div>
738
+ )}
739
+ </div>
740
+ )}
741
+
742
+ {/* Search and controls */}
743
+ <div className={styles.controls}>
744
+ <TextInput
745
+ id="field-search"
746
+ labelText=""
747
+ placeholder="Search fields..."
748
+ size="sm"
749
+ value={searchTerm}
750
+ onChange={(e) => setSearchTerm(e.target.value)}
751
+ disabled={disabled || isLoading}
752
+ />
753
+ <div className={styles.actionButtons}>
754
+ <Button
755
+ kind="ghost"
756
+ size="sm"
757
+ onClick={expandAll}
758
+ disabled={disabled || isLoading}
759
+ >
760
+ Expand All
761
+ </Button>
762
+ <Button
763
+ kind="ghost"
764
+ size="sm"
765
+ onClick={collapseAll}
766
+ disabled={disabled || isLoading}
767
+ >
768
+ Collapse All
769
+ </Button>
770
+ </div>
771
+ </div>
772
+
773
+ {/* Add Custom SQL Column */}
774
+ {onAddCustomSqlColumn && (
775
+ <div className={styles.customColumnSection}>
776
+ <Button
777
+ kind="secondary"
778
+ size="sm"
779
+ onClick={() => setCustomSqlModalOpen(true)}
780
+ disabled={disabled}
781
+ >
782
+ + Custom SQL Column
783
+ </Button>
784
+ </div>
785
+ )}
786
+
787
+ {/* Available/Selected toggle */}
788
+ {selectedFields.length > 0 && (
789
+ <div className={styles.toggleContainer}>
790
+ <Toggle
791
+ id="show-selected-toggle"
792
+ labelText="Show selected only"
793
+ labelA="All fields"
794
+ labelB="Selected only"
795
+ toggled={showSelectedOnly}
796
+ onToggle={() => setShowSelectedOnly(!showSelectedOnly)}
797
+ disabled={disabled || isLoading}
798
+ />
799
+ <Tag type="blue">{selectedFields.length} selected</Tag>
800
+ </div>
801
+ )}
802
+
803
+ {/* Error state */}
804
+ {error && (
805
+ <InlineNotification
806
+ kind="error"
807
+ title="Error"
808
+ subtitle={error}
809
+ hideCloseButton
810
+ />
811
+ )}
812
+
813
+ {/* Empty state */}
814
+ {!isLoading && !error && totalFields === 0 && (
815
+ <div className={styles.empty}>
816
+ {primaryTable ? (
817
+ <p>No fields found for table: {primaryTable}</p>
818
+ ) : (
819
+ <p>Select a data source to view available fields</p>
820
+ )}
821
+ </div>
822
+ )}
823
+
824
+ {/* Loading state */}
825
+ {isLoading && (
826
+ <div className={styles.loading}>Loading fields...</div>
827
+ )}
828
+
829
+ {/* Field groups */}
830
+ {!isLoading && totalFields > 0 && (
831
+ <div className={styles.fieldGroups}>
832
+ <Accordion className={styles.accordion}>
833
+ {FIELD_GROUPS.map((group) => {
834
+ const groupFields = filteredGroupedFields[group.id] || [];
835
+ const isExpanded = expandedGroups.has(group.id);
836
+
837
+ // Allow concept-based groups to show even with no fields
838
+ if (groupFields.length === 0 && !group.isConceptBased) return null;
839
+
840
+ return (
841
+ <AccordionItem
842
+ key={group.id}
843
+ title={
844
+ <div className={styles.groupHeader}>
845
+ <span className={styles.groupIcon}>{group.icon && React.createElement(group.icon, { size: 16 })}</span>
846
+ <span className={styles.groupName}>{group.name}</span>
847
+ {group.isConceptBased ? (
848
+ <Tag size="sm" type="blue">+</Tag>
849
+ ) : (
850
+ <Tag size="sm" type="gray">{groupFields.length}</Tag>
851
+ )}
852
+ </div>
853
+ }
854
+ open={isExpanded}
855
+ onClick={(e) => toggleGroup(group.id, e)}
856
+ >
857
+ {/* Concept-based groups show an "Add Column" button instead of predefined fields */}
858
+ {group.isConceptBased ? (
859
+ <div className={styles.conceptBasedGroupContent}>
860
+ <p className={styles.conceptBasedDescription}>
861
+ {group.description}
862
+ </p>
863
+ <div className={styles.conceptBasedActions}>
864
+ {group.id === 'api-observations' && (
865
+ <Button
866
+ kind="primary"
867
+ size="sm"
868
+ onClick={() => setObservationModalOpen(true)}
869
+ disabled={disabled}
870
+ renderIcon={null}
871
+ >
872
+ + Add Observation Column
873
+ </Button>
874
+ )}
875
+ {group.id === 'api-encounter-diagnoses' && (
876
+ <Button
877
+ kind="primary"
878
+ size="sm"
879
+ onClick={() => setDiagnosisModalOpen(true)}
880
+ disabled={disabled}
881
+ renderIcon={null}
882
+ >
883
+ + Add Diagnosis Column
884
+ </Button>
885
+ )}
886
+ </div>
887
+ </div>
888
+ ) : (
889
+ <div
890
+ className={styles.fieldList}
891
+ onClick={(e) => {
892
+ // Prevent accordion toggle when clicking on field items
893
+ e.stopPropagation();
894
+ }}
895
+ >
896
+ {groupFields.map((field) => {
897
+ const isSelected = selectedFields.includes(field.id);
898
+ return (
899
+ <div key={field.id} className={styles.fieldItem} data-selected={isSelected}>
900
+ <div className={styles.fieldInfo}>
901
+ <span className={styles.fieldName}>{field.label}</span>
902
+ <div className={styles.fieldMeta}>
903
+ {isSelected && (
904
+ <Tag size="sm" type="green">Selected</Tag>
905
+ )}
906
+ {field.dataSourceName && (
907
+ <Tag size="sm" type="cool-gray">{field.dataSourceName}</Tag>
908
+ )}
909
+ <Tag size="sm" type="blue">{field.type}</Tag>
910
+ <Tag size="sm" type="gray">{field.source}</Tag>
911
+ {field.isRepeated && (
912
+ <Tag size="sm" type="purple">Repeated</Tag>
913
+ )}
914
+ </div>
915
+ <div className={styles.fieldName}>{field.name}</div>
916
+ </div>
917
+ <div className={styles.fieldActions}>
918
+ <Button
919
+ kind="ghost"
920
+ size="sm"
921
+ onClick={(e) => {
922
+ e.stopPropagation();
923
+ onAddToColumns(field);
924
+ }}
925
+ disabled={disabled}
926
+ >
927
+ + Column
928
+ </Button>
929
+ <Button
930
+ kind="ghost"
931
+ size="sm"
932
+ onClick={(e) => {
933
+ e.stopPropagation();
934
+ onAddToFilters(field);
935
+ }}
936
+ disabled={disabled}
937
+ >
938
+ + Filter
939
+ </Button>
940
+ </div>
941
+ </div>
942
+ );
943
+ })}
944
+ </div>
945
+ )}
946
+ </AccordionItem>
947
+ );
948
+ })}
949
+
950
+ {/* Uncategorized fields */}
951
+ {filteredGroupedFields['uncategorized'] && filteredGroupedFields['uncategorized'].length > 0 && (
952
+ <AccordionItem
953
+ key="uncategorized"
954
+ title={
955
+ <div className={styles.groupHeader}>
956
+ <span className={styles.groupIcon}>📁</span>
957
+ <span className={styles.groupName}>Other Fields</span>
958
+ <Tag size="sm" type="gray">{filteredGroupedFields['uncategorized'].length}</Tag>
959
+ </div>
960
+ }
961
+ open={expandedGroups.has('uncategorized')}
962
+ onClick={() => toggleGroup('uncategorized')}
963
+ >
964
+ <div className={styles.fieldList}>
965
+ {filteredGroupedFields['uncategorized']?.map((field) => {
966
+ const isSelected = selectedFields.includes(field.id);
967
+ return (
968
+ <div key={field.id} className={styles.fieldItem} data-selected={isSelected}>
969
+ <div className={styles.fieldInfo}>
970
+ <span className={styles.fieldName}>{field.label}</span>
971
+ <div className={styles.fieldMeta}>
972
+ {isSelected && (
973
+ <Tag size="sm" type="green">Selected</Tag>
974
+ )}
975
+ {field.dataSourceName && (
976
+ <Tag size="sm" type="cool-gray">{field.dataSourceName}</Tag>
977
+ )}
978
+ <Tag size="sm" type="blue">{field.type}</Tag>
979
+ <Tag size="sm" type="gray">{field.source}</Tag>
980
+ {field.isRepeated && (
981
+ <Tag size="sm" type="purple">Repeated</Tag>
982
+ )}
983
+ </div>
984
+ <div className={styles.fieldName}>{field.name}</div>
985
+ </div>
986
+ <div className={styles.fieldActions}>
987
+ <Button
988
+ kind="ghost"
989
+ size="sm"
990
+ onClick={() => onAddToColumns(field)}
991
+ disabled={disabled}
992
+ >
993
+ + Column
994
+ </Button>
995
+ <Button
996
+ kind="ghost"
997
+ size="sm"
998
+ onClick={() => onAddToFilters(field)}
999
+ disabled={disabled}
1000
+ >
1001
+ + Filter
1002
+ </Button>
1003
+ </div>
1004
+ </div>
1005
+ );
1006
+ })}
1007
+ </div>
1008
+ </AccordionItem>
1009
+ )}
1010
+ </Accordion>
1011
+ </div>
1012
+ )}
1013
+
1014
+ {/* Summary */}
1015
+ {!isLoading && totalFields > 0 && (
1016
+ <div className={styles.summary}>
1017
+ {totalFields} field{totalFields !== 1 ? 's' : ''} available
1018
+ </div>
1019
+ )}
1020
+
1021
+ {/* Custom SQL Column Modal */}
1022
+ {onAddCustomSqlColumn && (
1023
+ <CustomSqlColumnModal
1024
+ open={customSqlModalOpen}
1025
+ onClose={() => setCustomSqlModalOpen(false)}
1026
+ onSave={onAddCustomSqlColumn}
1027
+ existingColumnNames={[]}
1028
+ idColumnAlias={idColumnAlias}
1029
+ primaryDatasourceName={primaryDatasourceName}
1030
+ />
1031
+ )}
1032
+
1033
+ {/* Observation Column Modal */}
1034
+ <ObservationColumnModal
1035
+ open={observationModalOpen}
1036
+ onClose={() => setObservationModalOpen(false)}
1037
+ onAddColumn={onAddDraftColumn || (() => {})}
1038
+ existingColumns={[]}
1039
+ />
1040
+
1041
+ {/* Encounter Diagnosis Column Modal */}
1042
+ <EncounterDiagnosisColumnModal
1043
+ open={diagnosisModalOpen}
1044
+ onClose={() => setDiagnosisModalOpen(false)}
1045
+ onAddColumn={onAddDraftColumn || (() => {})}
1046
+ existingColumns={[]}
1047
+ />
1048
+ </div>
1049
+ );
1050
+ };
1051
+
1052
+ /**
1053
+ * Format field name into a readable label
1054
+ */
1055
+ function formatFieldLabel(fieldName: string): string {
1056
+ return fieldName
1057
+ .toLowerCase()
1058
+ .replace(/[_\s]+/g, ' ')
1059
+ .replace(/\b\w/g, (c) => c.toUpperCase());
1060
+ }
1061
+
1062
+ export default DataCatalogue;