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

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 (551) 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/162.js +1 -0
  9. package/dist/162.js.map +1 -0
  10. package/dist/184.js +2 -0
  11. package/dist/184.js.LICENSE.txt +14 -0
  12. package/dist/184.js.map +1 -0
  13. package/dist/242.js +2 -0
  14. package/dist/242.js.LICENSE.txt +21 -0
  15. package/dist/242.js.map +1 -0
  16. package/dist/253.js +1 -0
  17. package/dist/253.js.map +1 -0
  18. package/dist/300.js +1 -0
  19. package/dist/311.js +1 -0
  20. package/dist/311.js.map +1 -0
  21. package/dist/469.js +2 -0
  22. package/dist/469.js.LICENSE.txt +45 -0
  23. package/dist/469.js.map +1 -0
  24. package/dist/478.js +2 -0
  25. package/dist/478.js.LICENSE.txt +9 -0
  26. package/dist/478.js.map +1 -0
  27. package/dist/539.js +1 -0
  28. package/dist/539.js.map +1 -0
  29. package/dist/540.js +2 -0
  30. package/dist/540.js.LICENSE.txt +9 -0
  31. package/dist/540.js.map +1 -0
  32. package/dist/669.js +1 -0
  33. package/dist/669.js.map +1 -0
  34. package/dist/710.js +1 -0
  35. package/dist/710.js.map +1 -0
  36. package/dist/729.js +1 -0
  37. package/dist/729.js.map +1 -0
  38. package/dist/838.js +2 -0
  39. package/dist/838.js.LICENSE.txt +9 -0
  40. package/dist/838.js.map +1 -0
  41. package/dist/917.js +1 -0
  42. package/dist/917.js.map +1 -0
  43. package/dist/961.js +2 -0
  44. package/dist/961.js.LICENSE.txt +19 -0
  45. package/dist/961.js.map +1 -0
  46. package/dist/main.js +1 -0
  47. package/dist/main.js.map +1 -0
  48. package/dist/openmrs-esm-report-builder.js +1 -0
  49. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +530 -0
  50. package/dist/openmrs-esm-report-builder.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/e2e/core/env.ts +5 -0
  53. package/e2e/core/global-setup.ts +34 -0
  54. package/e2e/core/test.ts +21 -0
  55. package/e2e/pages/app-shell.page.ts +34 -0
  56. package/e2e/pages/etl-sources.page.ts +35 -0
  57. package/e2e/pages/report-sections.page.ts +20 -0
  58. package/e2e/pages/section-preview.modal.ts +29 -0
  59. package/e2e/specs/etl-sources.spec.ts +51 -0
  60. package/e2e/specs/navigation.spec.ts +13 -0
  61. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  62. package/e2e/specs/section-preview.spec.ts +94 -0
  63. package/e2e/support/github/Dockerfile +22 -0
  64. package/e2e/support/github/Dockerfile.dockerignore +13 -0
  65. package/e2e/support/github/docker-compose.yml +31 -0
  66. package/e2e/support/github/run-e2e-docker-env.sh +131 -0
  67. package/e2e/support/github/spa-assemble-config.json +9 -0
  68. package/e2e/tsconfig.json +11 -0
  69. package/example.env +5 -0
  70. package/jest.config.js +28 -0
  71. package/linelist_report_builder_design_spec.md +816 -0
  72. package/package.json +111 -0
  73. package/playwright.config.ts +28 -0
  74. package/routes.json +22 -0
  75. package/src/__debug__/all-designs.test.tsx +30 -0
  76. package/src/admin-card-link.extension.module.scss +55 -0
  77. package/src/admin-card-link.extension.tsx +24 -0
  78. package/src/components/admin/admin-page.component.tsx +75 -0
  79. package/src/components/admin/age-categories-page.component.tsx +109 -0
  80. package/src/components/admin/age-groups-page.component.tsx +146 -0
  81. package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
  82. package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
  83. package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
  84. package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
  85. package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
  86. package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
  87. package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
  88. package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
  89. package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
  90. package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
  91. package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
  92. package/src/components/admin/etl-sources-page.component.tsx +355 -0
  93. package/src/components/admin/etl-tasks-page.component.scss +370 -0
  94. package/src/components/admin/etl-tasks-page.component.tsx +768 -0
  95. package/src/components/admin/report-categories-page.component.tsx +267 -0
  96. package/src/components/admin/report-library-page.component.tsx +551 -0
  97. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  98. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  99. package/src/components/ai-support/ai.service.ts +21 -0
  100. package/src/components/ai-support/ai.types.ts +12 -0
  101. package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
  102. package/src/components/app-shell/report-builder-shell.scss +18 -0
  103. package/src/components/dashboards/dashboard-config.schema.ts +97 -0
  104. package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
  105. package/src/components/dashboards/dashboards-admin.scss +62 -0
  106. package/src/components/dashboards/dashboards-page.component.tsx +153 -0
  107. package/src/components/dashboards/dashboards-table.component.tsx +130 -0
  108. package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
  109. package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
  110. package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
  111. package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
  112. package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
  113. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  114. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  115. package/src/components/data-themes/data-themes-page.component.tsx +139 -0
  116. package/src/components/data-themes/data-themes-table.component.tsx +98 -0
  117. package/src/components/data-themes/icon-registry.ts +35 -0
  118. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  119. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  120. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  121. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  122. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  123. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  124. package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
  125. package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
  126. package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
  127. package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
  128. package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
  129. package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
  130. package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
  131. package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
  132. package/src/components/data-visualizer/components/header/header.scss +71 -0
  133. package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
  134. package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
  135. package/src/components/data-visualizer/components/panel/panel.scss +25 -0
  136. package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
  137. package/src/components/data-visualizer/constants.ts +445 -0
  138. package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
  139. package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
  140. package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
  141. package/src/components/data-visualizer/data-visualizer.scss +449 -0
  142. package/src/components/data-visualizer/declarations.d.ts +131 -0
  143. package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
  144. package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
  145. package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
  146. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
  147. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
  148. package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
  149. package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
  150. package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
  151. package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
  152. package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
  153. package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
  154. package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
  155. package/src/components/data-visualizer/types.ts +206 -0
  156. package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
  157. package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
  158. package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
  159. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
  160. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
  161. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
  162. package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
  163. package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
  164. package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
  165. package/src/components/etl-monitor/builder/index.ts +5 -0
  166. package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
  167. package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
  168. package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
  169. package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
  170. package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
  171. package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
  172. package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
  173. package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
  174. package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
  175. package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
  176. package/src/components/etl-monitor/builder/steps/index.ts +5 -0
  177. package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
  178. package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
  179. package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
  180. package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
  181. package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
  182. package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
  183. package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
  184. package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
  185. package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
  186. package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
  187. package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
  188. package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
  189. package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
  190. package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
  191. package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
  192. package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
  193. package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
  194. package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
  195. package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
  196. package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
  197. package/src/components/etl-monitor/renderers/components/index.ts +6 -0
  198. package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
  199. package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
  200. package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
  201. package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
  202. package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
  203. package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
  204. package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
  205. package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
  206. package/src/components/etl-monitor/renderers/index.ts +11 -0
  207. package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
  208. package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
  209. package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
  210. package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
  211. package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
  212. package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
  213. package/src/components/import-export/artifact-groups.ts +153 -0
  214. package/src/components/import-export/export-report-modal.component.tsx +265 -0
  215. package/src/components/import-export/import-export-page.component.tsx +610 -0
  216. package/src/components/import-export/import-export.styles.scss +825 -0
  217. package/src/components/import-export/import-package-modal.component.tsx +401 -0
  218. package/src/components/import-export/index.ts +3 -0
  219. package/src/components/indicators/README.md +24 -0
  220. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  221. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  222. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  223. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  224. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  225. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  226. package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
  227. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
  228. package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
  229. package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
  230. package/src/components/indicators/custom-condition-input.component.tsx +310 -0
  231. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  232. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  233. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  234. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  235. package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
  236. package/src/components/indicators/indicator-search-select.component.tsx +297 -0
  237. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  238. package/src/components/indicators/indicators-page.component.tsx +673 -0
  239. package/src/components/indicators/indicators-page.scss +121 -0
  240. package/src/components/indicators/indicators-table.component.tsx +188 -0
  241. package/src/components/indicators/preview-table.component.tsx +31 -0
  242. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  243. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
  244. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  245. package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
  246. package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
  247. package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
  248. package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
  249. package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
  250. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  251. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  252. package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
  253. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  254. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  255. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  256. package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
  257. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
  258. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  259. package/src/components/indicators/sql-preview.component.tsx +19 -0
  260. package/src/components/indicators/tag-input.component.tsx +177 -0
  261. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  262. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  263. package/src/components/indicators/types/composite-indicator.types.ts +27 -0
  264. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  265. package/src/components/indicators/types/custom-indicator.types.ts +161 -0
  266. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  267. package/src/components/indicators/types/indicator-types.ts +57 -0
  268. package/src/components/indicators/types/sql-builders.ts +43 -0
  269. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
  270. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
  271. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  272. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
  273. package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
  274. package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
  275. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  276. package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
  277. package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
  278. package/src/components/indicators/utils/population-sql.compiler.test.ts +999 -0
  279. package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
  280. package/src/components/landing/landing-action-item.component.tsx +50 -0
  281. package/src/components/landing/landing-action-list.component.tsx +12 -0
  282. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  283. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  284. package/src/components/landing/landing-hero.component.tsx +18 -0
  285. package/src/components/landing/landing-section-card.component.tsx +33 -0
  286. package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
  287. package/src/components/landing/report-builder-landing-page.scss +131 -0
  288. package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
  289. package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
  290. package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
  291. package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
  292. package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
  293. package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
  294. package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
  295. package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
  296. package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
  297. package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
  298. package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
  299. package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
  300. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
  301. package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
  302. package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
  303. package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
  304. package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
  305. package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
  306. package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
  307. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
  308. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
  309. package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
  310. package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
  311. package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
  312. package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
  313. package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
  314. package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
  315. package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
  316. package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
  317. package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
  318. package/src/components/linelist/design/panels/column-category-selector.scss +329 -0
  319. package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
  320. package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
  321. package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
  322. package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
  323. package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
  324. package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
  325. package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
  326. package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
  327. package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
  328. package/src/components/linelist/design/panels/json-preview.scss +85 -0
  329. package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
  330. package/src/components/linelist/design/panels/query-config-panel.scss +342 -0
  331. package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
  332. package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
  333. package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
  334. package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
  335. package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
  336. package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
  337. package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
  338. package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
  339. package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
  340. package/src/components/linelist/linelist-dashboard.page.scss +240 -0
  341. package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
  342. package/src/components/linelist/linelist-editor.page.scss +28 -0
  343. package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
  344. package/src/components/linelist/linelist-run-report.page.scss +136 -0
  345. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
  346. package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
  347. package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
  348. package/src/components/report/dashboard/reports-table.component.tsx +186 -0
  349. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  350. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  351. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  352. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  353. package/src/components/report/definition/report-definition.types.ts +66 -0
  354. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  355. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  356. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  357. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  358. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  359. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  360. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  361. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  362. package/src/components/report/design/report-design-editor.component.tsx +1170 -0
  363. package/src/components/report/design/report-design.types.ts +48 -0
  364. package/src/components/report/design/report-design.utils.ts +86 -0
  365. package/src/components/report/panels/report-detail.component.tsx +71 -0
  366. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  367. package/src/components/report/report-dashboard.page.component.tsx +554 -0
  368. package/src/components/report/report-editor.page.component.tsx +590 -0
  369. package/src/components/report/report.routes.tsx +32 -0
  370. package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
  371. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  372. package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
  373. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  374. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  375. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
  376. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  377. package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
  378. package/src/components/report-sections/report-sections-page.component.tsx +325 -0
  379. package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
  380. package/src/components/report-sections/section-types.ts +55 -0
  381. package/src/components/report-sections/section-utils.ts +50 -0
  382. package/src/components/report-sections/sections-page.scss +79 -0
  383. package/src/components/run-reports/run-reports-page.component.tsx +8 -0
  384. package/src/components/security/access-denied-page.component.tsx +69 -0
  385. package/src/components/security/access-denied-page.scss +39 -0
  386. package/src/components/security/privilege-route.component.test.tsx +79 -0
  387. package/src/components/security/privilege-route.component.tsx +39 -0
  388. package/src/components/shared/compile-setup-modal.component.tsx +182 -0
  389. package/src/components/shared/concept-selector.component.tsx +245 -0
  390. package/src/components/shared/concept-selector.scss +121 -0
  391. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  392. package/src/components/shared/header/header.component.tsx +85 -0
  393. package/src/components/shared/header/header.scss +150 -0
  394. package/src/components/shared/header/illustration.component.tsx +34 -0
  395. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  396. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  397. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  398. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  399. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  400. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  401. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  402. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  403. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  404. package/src/config-schema.ts +43 -0
  405. package/src/constants/privileges.test.ts +53 -0
  406. package/src/constants/privileges.ts +118 -0
  407. package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
  408. package/src/features/dashboards/DashboardsPage.tsx +164 -0
  409. package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
  410. package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
  411. package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
  412. package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
  413. package/src/features/dashboards/components/DashboardSection.tsx +46 -0
  414. package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
  415. package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
  416. package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
  417. package/src/features/dashboards/dashboards.scss +196 -0
  418. package/src/features/dashboards/hooks/useDashboard.ts +61 -0
  419. package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
  420. package/src/features/dashboards/hooks/useDashboards.ts +56 -0
  421. package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
  422. package/src/features/dashboards/index.ts +11 -0
  423. package/src/features/dashboards/utils/dashboard-config.util.test.ts +255 -0
  424. package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
  425. package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
  426. package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
  427. package/src/features/report-visualizer/components/Export/index.ts +2 -0
  428. package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
  429. package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
  430. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
  431. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
  432. package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
  433. package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
  434. package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
  435. package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
  436. package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
  437. package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
  438. package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
  439. package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
  440. package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
  441. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
  442. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
  443. package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
  444. package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
  445. package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
  446. package/src/features/report-visualizer/components/TableView/index.ts +2 -0
  447. package/src/features/report-visualizer/hooks/index.ts +4 -0
  448. package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
  449. package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
  450. package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
  451. package/src/features/report-visualizer/hooks/useReports.ts +135 -0
  452. package/src/features/report-visualizer/index.ts +6 -0
  453. package/src/features/report-visualizer/report-visualizer.scss +593 -0
  454. package/src/features/report-visualizer/types/index.ts +1 -0
  455. package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
  456. package/src/features/report-visualizer/utils/status-colors.ts +117 -0
  457. package/src/globals.d.ts +14 -0
  458. package/src/hooks/address-template.ts +86 -0
  459. package/src/hooks/concept.ts +112 -0
  460. package/src/hooks/indicator/index.ts +1 -0
  461. package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
  462. package/src/hooks/openmrs-reference-data.ts +432 -0
  463. package/src/hooks/theme/index.ts +4 -0
  464. package/src/hooks/theme/useDataTheme.ts +61 -0
  465. package/src/hooks/theme/useDataThemes.ts +28 -0
  466. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  467. package/src/hooks/theme/useMambaTables.ts +95 -0
  468. package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
  469. package/src/hooks/use-report-builder-privileges.ts +58 -0
  470. package/src/index.ts +29 -0
  471. package/src/resources/address-template/address-template.api.ts +180 -0
  472. package/src/resources/agegroup/agegroups.api.ts +58 -0
  473. package/src/resources/concept/concept.api.ts +116 -0
  474. package/src/resources/concepts/concept-mapper.ts +21 -0
  475. package/src/resources/concepts/concept-types.ts +31 -0
  476. package/src/resources/concepts/concepts.resource.ts +44 -0
  477. package/src/resources/concepts/useConceptSearch.ts +41 -0
  478. package/src/resources/dashboard/dashboard.api.ts +107 -0
  479. package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
  480. package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
  481. package/src/resources/etl-source/etl-source.api.ts +65 -0
  482. package/src/resources/etl-task/etl-task.api.ts +96 -0
  483. package/src/resources/etl-task/etl-task.types.ts +31 -0
  484. package/src/resources/indicator/indicators.api.ts +125 -0
  485. package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
  486. package/src/resources/linelist/linelist-reports.api.ts +616 -0
  487. package/src/resources/location/location.api.ts +135 -0
  488. package/src/resources/openmrs-api.ts +40 -0
  489. package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
  490. package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
  491. package/src/resources/preview/sql-preview.api.ts +54 -0
  492. package/src/resources/report/reports.api.ts +140 -0
  493. package/src/resources/report-category/report-category.api.ts +41 -0
  494. package/src/resources/report-import-export/import-export-api.ts +164 -0
  495. package/src/resources/report-library/report-library.api.ts +63 -0
  496. package/src/resources/report-section/report-sections.api.ts +73 -0
  497. package/src/resources/report-section/section-preview.api.ts +25 -0
  498. package/src/resources/theme/data-theme.api.ts +88 -0
  499. package/src/resources/theme/etl-schema.api.ts +101 -0
  500. package/src/resources/theme/etl-table-meta.api.ts +45 -0
  501. package/src/root.component.tsx +20 -0
  502. package/src/routes/report-builder.routes.tsx +160 -0
  503. package/src/routes/report-builder.scss +56 -0
  504. package/src/routes.json +28 -0
  505. package/src/sample-template.ts +121 -0
  506. package/src/template-utils.ts +110 -0
  507. package/src/types/carbon-react-inert.d.ts +13 -0
  508. package/src/types/condition-operators.ts +63 -0
  509. package/src/types/dashboard/dashboard.types.ts +137 -0
  510. package/src/types/dashboard/index.ts +4 -0
  511. package/src/types/etl/etl-types.ts +238 -0
  512. package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
  513. package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
  514. package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
  515. package/src/types/etl-monitor/index.ts +6 -0
  516. package/src/types/linelist/compile-config.ts +455 -0
  517. package/src/types/linelist-types.ts +2059 -0
  518. package/src/types/theme/data-theme.types.ts +88 -0
  519. package/src/utils/api-error.utils.test.ts +151 -0
  520. package/src/utils/api-error.utils.ts +131 -0
  521. package/src/utils/config-enhancer.ts +62 -0
  522. package/src/utils/etl-browser/etl-browser.util.test.ts +145 -0
  523. package/src/utils/etl-browser/export-csv.util.ts +43 -0
  524. package/src/utils/etl-browser/group-tables.util.ts +84 -0
  525. package/src/utils/etl-browser/index.ts +4 -0
  526. package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
  527. package/src/utils/etl-browser/table-sql.util.ts +20 -0
  528. package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
  529. package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
  530. package/src/utils/etl-monitor/index.ts +10 -0
  531. package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
  532. package/src/utils/etl-monitor/normalization-examples.ts +0 -0
  533. package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
  534. package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
  535. package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
  536. package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
  537. package/src/utils/etl-task/etl-task-card.util.test.ts +239 -0
  538. package/src/utils/etl-task/etl-task-card.util.ts +276 -0
  539. package/src/utils/etl-task/index.ts +1 -0
  540. package/src/utils/html-entities.utils.ts +34 -0
  541. package/src/utils/parameter-resolution.ts +218 -0
  542. package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
  543. package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
  544. package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
  545. package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
  546. package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
  547. package/tools/scss-stub.js +12 -0
  548. package/tools/setup-tests.ts +1 -0
  549. package/translations/en.json +65 -0
  550. package/tsconfig.json +43 -0
  551. package/webpack.config.js +30 -0
@@ -0,0 +1,1676 @@
1
+ /**
2
+ * Linelist Builder Workspace - Three-Panel Layout
3
+ *
4
+ * Full-width, three-panel workspace for creating and editing linelist reports.
5
+ * Follows design spec section 6: Screen 3 — Builder workspace.
6
+ *
7
+ * Layout (Desktop 1440px+):
8
+ * - Left: Data Catalogue (280px, resizable)
9
+ * - Middle: Query Configuration (400px, resizable)
10
+ * - Right: Preview (remaining width, minimum 560px)
11
+ *
12
+ * Panels can collapse at smaller desktop widths.
13
+ */
14
+
15
+ import React, { useState, useCallback, useEffect, useMemo } from 'react';
16
+ import {
17
+ Button,
18
+ ButtonSet,
19
+ InlineNotification,
20
+ Tag,
21
+ TextInput,
22
+ OverflowMenu,
23
+ OverflowMenuItem,
24
+ Table,
25
+ TableBody,
26
+ TableCell,
27
+ TableContainer,
28
+ TableHead,
29
+ TableHeader,
30
+ TableRow,
31
+ } from '@carbon/react';
32
+ import {
33
+ Save,
34
+ Send,
35
+ Document,
36
+ OverflowMenuHorizontal,
37
+ ChevronLeft,
38
+ ChevronRight,
39
+ } from '@carbon/react/icons';
40
+ import { useNavigate, useParams } from 'react-router-dom';
41
+
42
+ import type {
43
+ LinelistReportDraft,
44
+ LinelistColumnDraft,
45
+ FilterFieldType,
46
+ FilterOperator,
47
+ DataSourceInfo,
48
+ ColumnSource,
49
+ LinelistSortConfig,
50
+ LinelistDataDefinitionMap,
51
+ PopulationDefinition,
52
+ PopulationSource,
53
+ LinelistParameterConfig,
54
+ } from '../../../types/linelist-types';
55
+ import type { EtlStructure } from '../../../types/etl/etl-types';
56
+ import {
57
+ draftToConfig,
58
+ // isLinelistDraftReadyToCompile,
59
+ // isLinelistDraftReadyToPublish,
60
+ createEmptyDraft,
61
+ // isLinelistDraftValid,
62
+ validateLinelistDraft,
63
+ } from '../../../types/linelist-types';
64
+
65
+ import {
66
+ createLinelistReport,
67
+ updateLinelistReport,
68
+ configToSavePayload,
69
+ compileLinelistReport,
70
+ previewLinelistReport,
71
+ } from '../../../resources/linelist/linelist-reports.api';
72
+ import type { LinelistBuilderMeta } from '../../../resources/linelist/linelist-reports.api';
73
+ import type { LinelistReportDto } from '../../../types/linelist-types';
74
+ import { listReportCategories, type ReportCategoryDto } from '../../../resources/report-category/report-category.api';
75
+ import { useIndicatorsByTheme } from '../../../hooks/indicator';
76
+
77
+ import DataCatalogue from '../design/panels/data-catalogue.component';
78
+ import QueryConfigPanel from '../design/panels/query-config-panel.component';
79
+ import MultiDatasourceSelector from './panels/multi-datasource-selector.component';
80
+ import PopulationSourceSelector from './panels/population-source-selector.component';
81
+ import type { CatalogueField } from '../design/panels/data-catalogue.component';
82
+ import type { CustomSqlColumnConfig } from './panels/custom-sql-column-modal.component';
83
+ import PreviewParameterModal from '../modals/preview-parameter-modal.component';
84
+ import styles from './linelist-definition-editor.scss';
85
+ import type { LinelistReportDefinitionConfig } from '../../../types/linelist-types';
86
+ import CompileSetupModal, { type CompileSetupResult } from '../../shared/compile-setup-modal.component';
87
+ import { RB } from '../../../constants/privileges';
88
+ import { useReportBuilderPrivileges } from '../../../hooks/use-report-builder-privileges';
89
+
90
+ type Props = {};
91
+
92
+ /**
93
+ * Convert LinelistReportDto to LinelistReportDraft for editing (v2)
94
+ * This function populates the draft with all builder details from the saved report
95
+ */
96
+ function reportToDraft(report: LinelistReportDto): LinelistReportDraft {
97
+ let config: LinelistReportDefinitionConfig | null = null;
98
+
99
+ try {
100
+ config = report.configJson ? JSON.parse(report.configJson) : null;
101
+ } catch (e) {
102
+ console.error('Failed to parse report config:', e);
103
+ }
104
+
105
+ // Check for valid LINE_LIST config (handle both type and reportType formats)
106
+ const isValidLinelist = config && (
107
+ config.type === 'LINE_LIST' ||
108
+ (config as any).reportType === 'LINE_LIST' ||
109
+ (config as any).reportType === 'LINELIST'
110
+ );
111
+
112
+ if (!config || !isValidLinelist) {
113
+ console.warn('Invalid config or type, returning empty draft');
114
+ return createEmptyDraft();
115
+ }
116
+
117
+ const now = new Date().toISOString();
118
+ const dataSet = config.dataSetDefinitions?.[0];
119
+
120
+ // The compiled backend format stores builder state under a `_builder` key.
121
+ // Fall back to top-level config fields for older/intermediate formats.
122
+ const builderState = (config as any)._builder;
123
+
124
+ // === BUILD DATA SOURCES ARRAY ===
125
+ const dataSources: DataSourceInfo[] = [];
126
+ const configDataSources = builderState?.dataSources || config.dataSources;
127
+
128
+ if (configDataSources && configDataSources.length > 0) {
129
+ // V2 format (or compiled _builder) - use dataSources array directly
130
+ configDataSources.forEach((ds: any) => {
131
+ dataSources.push({
132
+ uuid: ds.uuid,
133
+ name: ds.name,
134
+ type: ds.type as any,
135
+ role: ds.role as any,
136
+ tables: ds.columns ? Object.keys(ds.columns) : [],
137
+ });
138
+ });
139
+ } else if (config.version === 1 || !config.dataSources) {
140
+ // V1 format - migrate from dataSourceUuid
141
+ const primaryDataSourceUuid = config.dataSourceUuid || '';
142
+ if (primaryDataSourceUuid) {
143
+ dataSources.push({
144
+ uuid: primaryDataSourceUuid,
145
+ name: primaryDataSourceUuid, // Will be resolved by data catalogue
146
+ type: 'ETL',
147
+ role: 'PRIMARY',
148
+ tables: [],
149
+ });
150
+ }
151
+
152
+ // Add CORE datasources if columns use them
153
+ if (dataSet?.columns) {
154
+ const hasPersonAttributes = dataSet.columns.some(col =>
155
+ (col.dataDefinition as any)?.type === 'PERSON_ATTRIBUTE'
156
+ );
157
+ const hasIdentifiers = dataSet.columns.some(col =>
158
+ (col.dataDefinition as any)?.type === 'IDENTIFIER'
159
+ );
160
+
161
+ if (hasPersonAttributes) {
162
+ dataSources.push({
163
+ uuid: 'person_attributes',
164
+ name: 'Person Attributes',
165
+ type: 'CORE',
166
+ role: 'REFERENCE',
167
+ });
168
+ }
169
+
170
+ if (hasIdentifiers) {
171
+ dataSources.push({
172
+ uuid: 'patient_identifiers',
173
+ name: 'Patient Identifiers',
174
+ type: 'CORE',
175
+ role: 'REFERENCE',
176
+ });
177
+ }
178
+ }
179
+ } else {
180
+ // V2 format - use dataSources array directly
181
+ if (config.dataSources && config.dataSources.length > 0) {
182
+ config.dataSources.forEach(ds => {
183
+ dataSources.push({
184
+ uuid: ds.uuid,
185
+ name: ds.name,
186
+ type: ds.type as any,
187
+ role: ds.role as any,
188
+ tables: ds.columns ? Object.keys(ds.columns) : [],
189
+ });
190
+ });
191
+ }
192
+ }
193
+
194
+ // Get primary datasource UUID from the migrated dataSources
195
+ const primaryDataSourceUuid = dataSources.find(ds => ds.role === 'PRIMARY')?.uuid || '';
196
+
197
+ // === BUILD COLUMN → SOURCE LOOKUP FROM dataSources METADATA ===
198
+ // The V2 contract stores per-column source info inside dataSources[].columns.
199
+ // Build a map: columnName (lowercase) → { dataSource, table, fieldType }
200
+ type SourceLookup = {
201
+ dataSourceUuid: string;
202
+ dataSourceName: string;
203
+ table: string;
204
+ fieldType: string;
205
+ };
206
+ const columnSourceLookup = new Map<string, SourceLookup>();
207
+ (configDataSources as any[]).forEach((ds) => {
208
+ const dsColumns = ds.columns || {};
209
+ Object.values(dsColumns).forEach((colInfo: any) => {
210
+ const lookupKey = (colInfo?.name || '').toLowerCase();
211
+ if (lookupKey) {
212
+ columnSourceLookup.set(lookupKey, {
213
+ dataSourceUuid: ds.uuid,
214
+ dataSourceName: ds.name,
215
+ table: colInfo?.sourceTable || ds.uuid,
216
+ fieldType: colInfo?.type || 'UNKNOWN',
217
+ });
218
+ }
219
+ });
220
+ });
221
+
222
+ // Track unique tables used
223
+ const tablesUsed = new Set<string>();
224
+
225
+ // === EXTRACT COLUMNS WITH SOURCE INFO ===
226
+ const columns: LinelistColumnDraft[] = [];
227
+
228
+ if (dataSet?.columns) {
229
+ dataSet.columns.forEach((col, idx) => {
230
+ const defType = (col.dataDefinition as any)?.type;
231
+ const defConfig = (col.dataDefinition as any)?.config;
232
+ const colNameLower = col.name.toLowerCase();
233
+ const sqlStr = defConfig?.sql || '';
234
+ // Detect custom SQL (a full query, not a simple table.column reference)
235
+ const isCustomSql = /^\s*SELECT\s/i.test(sqlStr) || /:(patientId|client_id)\b/i.test(sqlStr);
236
+
237
+ // Determine source based on data definition type
238
+ let source: ColumnSource;
239
+
240
+ if (defType === 'PERSON_ATTRIBUTE') {
241
+ source = {
242
+ dataSourceUuid: 'person_attributes',
243
+ dataSourceName: 'Person Attributes',
244
+ table: 'person_attribute',
245
+ field: col.name,
246
+ fieldType: 'PERSON_ATTRIBUTE',
247
+ attributeTypeUuid: defConfig?.attributeTypeUuid,
248
+ };
249
+ } else if (defType === 'IDENTIFIER') {
250
+ source = {
251
+ dataSourceUuid: 'patient_identifiers',
252
+ dataSourceName: 'Patient Identifiers',
253
+ table: 'patient_identifier',
254
+ field: col.name,
255
+ fieldType: 'IDENTIFIER',
256
+ identifierTypeUuid: defConfig?.identifierTypeUuid,
257
+ };
258
+ } else if (defType === 'CALCULATION') {
259
+ // Calculated field (e.g., Age)
260
+ source = {
261
+ dataSourceUuid: primaryDataSourceUuid,
262
+ dataSourceName: primaryDataSourceUuid,
263
+ table: 'calculated',
264
+ field: col.name,
265
+ fieldType: 'CALCULATED',
266
+ };
267
+ } else {
268
+ // SQL column - use the dataSources metadata lookup first, then fall back
269
+ const lookup = columnSourceLookup.get(colNameLower);
270
+
271
+ if (lookup && !isCustomSql) {
272
+ // Found in the dataSources metadata
273
+ source = {
274
+ dataSourceUuid: lookup.dataSourceUuid,
275
+ dataSourceName: lookup.dataSourceName,
276
+ table: lookup.table,
277
+ field: col.name,
278
+ fieldType: lookup.fieldType,
279
+ };
280
+ tablesUsed.add(lookup.table);
281
+ } else if (isCustomSql) {
282
+ // Custom SQL column - per-row query with :patientId / :client_id
283
+ source = {
284
+ dataSourceUuid: lookup?.dataSourceUuid || primaryDataSourceUuid || 'custom_sql',
285
+ dataSourceName: lookup?.dataSourceName || 'Custom SQL',
286
+ table: 'custom_sql',
287
+ field: col.name,
288
+ fieldType: 'SQL',
289
+ };
290
+ } else {
291
+ // Simple SQL reference like `table`.`column` - parse it
292
+ const sqlMatch = sqlStr.match(/`?(\w+)`?\.\s*`?(\w+)`?/);
293
+ if (sqlMatch) {
294
+ source = {
295
+ dataSourceUuid: primaryDataSourceUuid,
296
+ dataSourceName: primaryDataSourceUuid,
297
+ table: sqlMatch[1],
298
+ field: sqlMatch[2],
299
+ fieldType: 'DERIVED',
300
+ };
301
+ tablesUsed.add(sqlMatch[1]);
302
+ } else {
303
+ // Fallback
304
+ source = {
305
+ dataSourceUuid: primaryDataSourceUuid,
306
+ dataSourceName: primaryDataSourceUuid,
307
+ table: 'unknown',
308
+ field: col.name,
309
+ fieldType: 'UNKNOWN',
310
+ };
311
+ }
312
+ }
313
+ }
314
+
315
+ const columnDraft: LinelistColumnDraft = {
316
+ id: `col-${idx}`,
317
+ name: col.name,
318
+ description: '',
319
+ source,
320
+ dataDefinitionType: (defType || 'SQL') as keyof LinelistDataDefinitionMap,
321
+ dataDefinitionConfig: defConfig || {},
322
+ additionInfo: {
323
+ addedVia: isCustomSql ? 'SQL_BUILDER' : 'IMPORT',
324
+ addedAt: now,
325
+ orderAdded: idx,
326
+ },
327
+ display: {
328
+ width: 150,
329
+ align: defType === 'CALCULATION' ? 'center' : 'left',
330
+ sortable: true,
331
+ filterable: defType !== 'CALCULATION',
332
+ format: defType === 'CALCULATION' ? 'number' : 'text',
333
+ },
334
+ sortOrder: idx,
335
+ repeatResolution: (col as any).repeatResolution,
336
+ transformations: (col as any).transformations || [],
337
+ };
338
+
339
+ columns.push(columnDraft);
340
+ });
341
+ }
342
+
343
+ // Update data sources with tables used
344
+ dataSources.forEach(ds => {
345
+ if (ds.uuid === primaryDataSourceUuid) {
346
+ ds.tables = Array.from(tablesUsed);
347
+ }
348
+ });
349
+
350
+ // === EXTRACT PARAMETERS ===
351
+ let parameters = config.parameters?.map((param, idx) => ({
352
+ id: `param-${idx}`,
353
+ name: param.name,
354
+ label: param.label,
355
+ type: param.type,
356
+ required: param.required || false,
357
+ defaultValue: param.defaultValue || '',
358
+ displayOrder: param.displayOrder || idx,
359
+ config: (param.config || { type: param.type }) as LinelistParameterConfig,
360
+ })) || [];
361
+
362
+ // === EXTRACT BUILDER METADATA (build method, indicators, visualFilter, filterMap) ===
363
+ // Builder state lives under `_builder` (compiled format) or at the top level
364
+ // (intermediate format). Read from _builder first, then fall back.
365
+ let buildMethod: PopulationDefinition['buildMethod'] = builderState?.buildMethod || (config as any).buildMethod || 'SQL_BUILDER';
366
+ let indicatorRules: PopulationDefinition['indicatorRules'] = undefined;
367
+ let loadedVisualFilter: any = undefined;
368
+ let loadedFilterMap: any = undefined;
369
+
370
+ // Try to load visualFilter from config._builder or top-level config
371
+ const configVisualFilter = builderState?.visualFilter || (config as any).visualFilter;
372
+ if (configVisualFilter) {
373
+ loadedVisualFilter = configVisualFilter;
374
+ }
375
+
376
+ // Try to load filterMap from config._builder or config.baseCohortDefinition.config
377
+ const configFilterMap = builderState?.filterMap || config.baseCohortDefinition?.config?.filterMap;
378
+ if (configFilterMap) {
379
+ loadedFilterMap = configFilterMap;
380
+ }
381
+
382
+ const configIndicatorRules = builderState?.indicatorRules || (config as any).indicatorRules;
383
+ if (Array.isArray(configIndicatorRules) && configIndicatorRules.length > 0) {
384
+ indicatorRules = configIndicatorRules.map((rule: any, idx: number) => ({
385
+ id: rule.id || `rule-${idx}`,
386
+ indicatorUuid: rule.indicatorUuid,
387
+ name: rule.name || '',
388
+ conditions: rule.conditions,
389
+ logicalOperator: rule.logicalOperator || 'AND',
390
+ negate: rule.negate || false,
391
+ }));
392
+ }
393
+
394
+ // Fall back to metaJson for buildMethod, indicator rules, parameters if not in config
395
+ // Load parameters from metaJson if available (for dynamic parameter options like LIST options)
396
+ if (report.metaJson) {
397
+ try {
398
+ const meta = JSON.parse(report.metaJson);
399
+ // Try to get buildMethod from metaJson as fallback
400
+ if (meta.buildMethod && (meta.buildMethod === 'INDICATOR_BASED' || meta.buildMethod === 'VISUAL_FILTER' || meta.buildMethod === 'SQL_BUILDER')) {
401
+ buildMethod = meta.buildMethod;
402
+ }
403
+ // Also try to get indicator rules from metaJson as fallback
404
+ if (!indicatorRules && Array.isArray(meta.indicatorRules) && meta.indicatorRules.length > 0) {
405
+ indicatorRules = meta.indicatorRules.map((rule: any, idx: number) => ({
406
+ id: rule.id || `rule-${idx}`,
407
+ indicatorUuid: rule.indicatorUuid,
408
+ name: rule.name || '',
409
+ conditions: rule.conditions,
410
+ logicalOperator: rule.logicalOperator || 'AND',
411
+ negate: rule.negate || false,
412
+ }));
413
+ }
414
+ // Load parameters from metaJson if available (for dynamic parameter options)
415
+ // This takes precedence over config parameters since metaJson may have updated options
416
+ if (meta.parameters && Array.isArray(meta.parameters) && meta.parameters.length > 0) {
417
+ parameters = meta.parameters.map((param: any, idx: number) => ({
418
+ id: param.id || `param-${idx}`,
419
+ name: param.name,
420
+ label: param.label,
421
+ type: param.type,
422
+ required: param.required || false,
423
+ defaultValue: param.defaultValue || '',
424
+ displayOrder: param.displayOrder !== undefined ? param.displayOrder : idx,
425
+ config: (param.config || { type: param.type }) as LinelistParameterConfig,
426
+ }));
427
+ }
428
+ } catch (e) {
429
+ console.warn('Failed to parse metaJson:', e);
430
+ }
431
+ }
432
+
433
+ // === BUILD SORT CONFIG ===
434
+ const sortConfig: LinelistSortConfig[] = [];
435
+ if (config.orderBy) {
436
+ sortConfig.push({
437
+ id: `sort-0`,
438
+ columnId: config.orderBy,
439
+ columnName: config.orderBy,
440
+ direction: config.orderDirection || 'ASC',
441
+ nulls: 'LAST',
442
+ sortOrder: 0,
443
+ });
444
+ }
445
+
446
+ // === BUILD POPULATION DEFINITION ===
447
+ const cohortSql = config.baseCohortDefinition?.config?.sql || '';
448
+ const baseDsUuid = dataSources.find(ds => ds.role === 'PRIMARY')?.uuid || primaryDataSourceUuid;
449
+
450
+ const population: PopulationDefinition = {
451
+ baseDataSourceUuid: baseDsUuid,
452
+ buildMethod,
453
+ sqlTemplate: cohortSql,
454
+ parameterReferences: extractParameterReferences(cohortSql),
455
+ // Restore visual filter from metaJson if available, otherwise use empty default
456
+ visualFilter: loadedVisualFilter || {
457
+ rootGroup: {
458
+ id: 'root',
459
+ logicalOperator: 'AND',
460
+ conditions: [],
461
+ nestedGroups: [],
462
+ },
463
+ useVisualBuilder: false,
464
+ },
465
+ // Restore filter map from metaJson or config, otherwise undefined
466
+ filterMap: loadedFilterMap || undefined,
467
+ // Reconstruct indicator rules so they show as selected on edit
468
+ indicatorRules,
469
+ buildHistory: [
470
+ {
471
+ timestamp: now,
472
+ action: 'IMPORTED_FROM_EXISTING',
473
+ description: config.version === 1 ? 'Migrated from V1 format (single datasource)' : 'Imported from existing report definition',
474
+ },
475
+ ],
476
+ };
477
+
478
+ // === CREATE THE DRAFT ===
479
+ // Build population sources from primary datasource
480
+ const populationSources: PopulationSource[] = [];
481
+ const primaryDs = dataSources.find(ds => ds.role === 'PRIMARY');
482
+ if (primaryDs) {
483
+ populationSources.push({
484
+ uuid: primaryDs.uuid,
485
+ name: primaryDs.name,
486
+ type: primaryDs.type,
487
+ joinType: 'JOIN',
488
+ enabled: true,
489
+ order: 0,
490
+ });
491
+ }
492
+
493
+ const draft: LinelistReportDraft = {
494
+ version: 3,
495
+ name: report.name || '',
496
+ description: report.description || '',
497
+ code: report.code || '',
498
+ currentPanel: 'basics',
499
+ categoryUuid: config.categoryUuid || '',
500
+ rowGrain: config.rowGrain || 'PATIENT',
501
+ templateId: config.templateId || '',
502
+ populationSources,
503
+ dataSources,
504
+ population,
505
+ columns,
506
+ parameters,
507
+ sortConfig,
508
+ limit: config.limit,
509
+ // Legacy top-level fields used by the indicator UI and getCohortSql().
510
+ // Keep them in sync with population.* so the existing code paths work.
511
+ populationMode: buildMethod === 'INDICATOR_BASED' ? 'INDICATOR' : 'SQL',
512
+ indicatorRules,
513
+ displaySettings: {
514
+ defaultPageSize: 25,
515
+ freezeFirstColumn: true,
516
+ freezeHeader: true,
517
+ dateDisplayFormat: 'dd MMM yyyy',
518
+ nullDisplayValue: '—',
519
+ maxInteractiveRows: 500,
520
+ maxExportRows: 100000,
521
+ allowedExports: ['CSV', 'XLSX', 'PDF'],
522
+ includeParametersInExportHeader: true,
523
+ includeGeneratedTimestamp: true,
524
+ },
525
+ validation: {
526
+ errors: {},
527
+ warnings: {},
528
+ lastValidated: now,
529
+ },
530
+ metadata: {
531
+ createdAt: now,
532
+ lastModified: now,
533
+ buildMethod: 'EDIT',
534
+ sourceReportUuid: report.uuid,
535
+ version: 3,
536
+ status: 'DRAFT',
537
+ },
538
+ };
539
+
540
+ return draft;
541
+ }
542
+
543
+ /**
544
+ * Extract parameter references from SQL
545
+ * Finds patterns like :paramName
546
+ */
547
+ function extractParameterReferences(sql: string): string[] {
548
+ const paramRegex = /:(\w+)/g;
549
+ const params = new Set<string>();
550
+ let match;
551
+ while ((match = paramRegex.exec(sql)) !== null) {
552
+ params.add(match[1]);
553
+ }
554
+ return Array.from(params);
555
+ }
556
+
557
+ const LinelistBuilderWorkspace: React.FC<Props> = () => {
558
+ const navigate = useNavigate();
559
+ const { reportId } = useParams();
560
+ const { has: hasPrivilege } = useReportBuilderPrivileges();
561
+
562
+ // Report state
563
+ const [draft, setDraft] = useState<LinelistReportDraft>(createEmptyDraft());
564
+ const [initialReport, setInitialReport] = useState<LinelistReportDto | null>(null);
565
+ const [loading, setLoading] = useState(false);
566
+ const [saving, setSaving] = useState(false);
567
+ const [compiling, setCompiling] = useState(false);
568
+ const [error, setError] = useState<string | null>(null);
569
+ const [saveError, setSaveError] = useState<string | null>(null);
570
+ const [compileError, setCompileError] = useState<string | null>(null);
571
+ const [compileSuccess, setCompileSuccess] = useState<string | null>(null);
572
+ const [mode, setMode] = useState<'create' | 'edit'>('create');
573
+
574
+ // Reference data
575
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
576
+ const [categories, setCategories] = useState<ReportCategoryDto[]>([]);
577
+
578
+ // Panel state
579
+ const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(false);
580
+ const [middlePanelCollapsed, setMiddlePanelCollapsed] = useState(false);
581
+ const [leftPanelWidth, setLeftPanelWidth] = useState(500);
582
+ const [middlePanelWidth, setMiddlePanelWidth] = useState(500);
583
+
584
+ // Resizable state
585
+ const [isResizingLeft, setIsResizingLeft] = useState(false);
586
+ const [isResizingMiddle, setIsResizingMiddle] = useState(false);
587
+
588
+ // Preview state
589
+ const [previewRunning, setPreviewRunning] = useState(false);
590
+ const [cachedReportDefinitionUuid, setCachedReportDefinitionUuid] = useState<string | null>(null);
591
+ const [previewData, setPreviewData] = useState<{
592
+ columns: string[];
593
+ rows: Record<string, any>[];
594
+ rowCount: number;
595
+ html?: string;
596
+ } | null>(null);
597
+ const [previewError, setPreviewError] = useState<string | null>(null);
598
+ const [previewParamModalOpen, setPreviewParamModalOpen] = useState(false);
599
+
600
+ // Available fields from data catalogue (for visual filter builder)
601
+ const [availableFields, setAvailableFields] = useState<Array<{ name: string; label: string; type: FilterFieldType }>>([]);
602
+
603
+ // ETL structure for selected data source (for future use)
604
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
605
+ const [etlStructure, setEtlStructure] = useState<EtlStructure | null>(null);
606
+
607
+ // Indicators for population (load all available indicators)
608
+ const { indicators } = useIndicatorsByTheme(''); // Empty string to load all indicators
609
+
610
+ // Compile setup modal state
611
+ const [showCompileSetupModal, setShowCompileSetupModal] = useState(false);
612
+
613
+
614
+ /**
615
+ * Load report for editing
616
+ */
617
+ useEffect(() => {
618
+ if (reportId && reportId !== 'new') {
619
+ setLoading(true);
620
+ setError(null);
621
+
622
+ import('../../../resources/linelist/linelist-reports.api')
623
+ .then(({ getLinelistReport }) => getLinelistReport(reportId))
624
+ .then((report: LinelistReportDto) => {
625
+ setInitialReport(report);
626
+ setMode('edit');
627
+ // Convert report to draft
628
+ const draft = reportToDraft(report);
629
+ setDraft(draft);
630
+ // Cache the compiledReportDefinitionUuid from the loaded report for preview
631
+ if (report.compiledReportDefinitionUuid) {
632
+ setCachedReportDefinitionUuid(report.compiledReportDefinitionUuid);
633
+ }
634
+ })
635
+ .catch((err) => {
636
+ console.error('Failed to load report:', err);
637
+ setError(err instanceof Error ? err.message : 'Failed to load report');
638
+ })
639
+ .finally(() => setLoading(false));
640
+ } else {
641
+ setMode('create');
642
+ setDraft(createEmptyDraft());
643
+ setLoading(false);
644
+ }
645
+ }, [reportId]);
646
+
647
+ /**
648
+ * Load reference data
649
+ */
650
+ useEffect(() => {
651
+ const loadData = async () => {
652
+ try {
653
+ const cats = await listReportCategories();
654
+ setCategories(cats);
655
+ } catch (err) {
656
+ console.error('Failed to load reference data:', err);
657
+ }
658
+ };
659
+ loadData();
660
+ }, []);
661
+
662
+ /**
663
+ * Invalidate compiled cache when draft structure changes
664
+ * This ensures we recompile when columns, data sources, or SQL changes
665
+ */
666
+ useEffect(() => {
667
+ // Clear cache when draft structure changes
668
+ setCachedReportDefinitionUuid(null);
669
+ }, [draft.columns.length, draft.dataSources.length, draft.population.sqlTemplate, draft.rowGrain]);
670
+
671
+ /**
672
+ * Handle save draft
673
+ */
674
+ const handleSave = useCallback(async () => {
675
+ setSaving(true);
676
+ setSaveError(null);
677
+
678
+ try {
679
+ // Convert indicators array to Map for draftToConfig
680
+ const indicatorsMap = new Map(
681
+ indicators.map(ind => [ind.uuid, ind])
682
+ );
683
+ // Pass indicators map so draftToConfig can generate SQL from indicator rules
684
+ const config = draftToConfig(draft, indicatorsMap);
685
+ // Preserve builder state (indicators, build method, population sources, visual filter, filter map) in metaJson so the
686
+ // editor can reconstruct the draft when re-opening for edit.
687
+ const builderMeta: LinelistBuilderMeta = {
688
+ buildMethod: draft.population.buildMethod,
689
+ indicatorRules: draft.population.indicatorRules,
690
+ populationSources: draft.populationSources,
691
+ // Preserve visual filter state for reconstructing the filter UI
692
+ visualFilter: draft.population.visualFilter,
693
+ // Preserve filter map for parameter-to-column mapping
694
+ filterMap: draft.population.filterMap,
695
+ };
696
+ const payload = configToSavePayload(config, draft, builderMeta);
697
+
698
+ let result;
699
+ if (mode === 'create') {
700
+ result = await createLinelistReport(payload);
701
+ // Navigate to edit mode after creating a new report
702
+ navigate(`/linelist/edit/${result.uuid}`, { replace: true });
703
+ } else {
704
+ result = await updateLinelistReport(initialReport?.uuid || '', payload);
705
+ }
706
+
707
+ setInitialReport(result);
708
+ setDraft(prev => ({ ...prev, unsavedChanges: false }));
709
+ } catch (err) {
710
+ setSaveError(err instanceof Error ? err.message : 'Failed to save report');
711
+ } finally {
712
+ setSaving(false);
713
+ }
714
+ }, [draft, mode, initialReport, indicators, navigate]);
715
+
716
+ /**
717
+ * Handle compile
718
+ * Always shows the compile setup modal for category confirmation/selection
719
+ */
720
+ const handleCompile = useCallback(async () => {
721
+ setCompileError(null);
722
+ setSaveError(null);
723
+
724
+ // Always show the modal to allow category confirmation/selection
725
+ setShowCompileSetupModal(true);
726
+ }, []);
727
+
728
+ /**
729
+ * Handle navigate back
730
+ */
731
+ const handleBack = useCallback(() => {
732
+ navigate('/linelist');
733
+ }, [navigate]);
734
+
735
+ /**
736
+ * Update draft field
737
+ */
738
+ const updateDraft = useCallback((updates: Partial<LinelistReportDraft>) => {
739
+ setDraft(prev => ({ ...prev, ...updates, unsavedChanges: true }));
740
+ }, []);
741
+
742
+ /**
743
+ * Handle compile setup modal cancel
744
+ */
745
+ const handleCompileSetupCancel = useCallback(() => {
746
+ setShowCompileSetupModal(false);
747
+ }, []);
748
+
749
+ /**
750
+ * Handle compile setup modal confirm
751
+ * This is called after the user selects category in the modal
752
+ */
753
+ const handleCompileSetupConfirm = useCallback(async (result: CompileSetupResult) => {
754
+ setShowCompileSetupModal(false);
755
+
756
+ // Create updated draft with new category for validation
757
+ const updatedDraft = { ...draft, categoryUuid: result.categoryUuid };
758
+
759
+ // Check validation before saving/compiling
760
+ const errors = validateLinelistDraft(updatedDraft);
761
+ if (Object.keys(errors).length > 0) {
762
+ // Show specific validation errors
763
+ const errorMessages = Object.values(errors).join(', ');
764
+ setSaveError(`Cannot compile: ${errorMessages}`);
765
+ return;
766
+ }
767
+
768
+ // Update the draft with selected category
769
+ updateDraft({
770
+ categoryUuid: result.categoryUuid,
771
+ });
772
+
773
+ // Save if there are unsaved changes or no initial report
774
+ if (draft.unsavedChanges || !initialReport?.uuid) {
775
+ await handleSave();
776
+ }
777
+
778
+ if (!initialReport?.uuid) {
779
+ setCompileError('Cannot compile: Report must be saved first');
780
+ return;
781
+ }
782
+
783
+ setCompiling(true);
784
+ setCompileError(null);
785
+ setCompileSuccess(null);
786
+
787
+ try {
788
+ const compiledResult = await compileLinelistReport(initialReport.uuid, draft.categoryUuid);
789
+
790
+ // Cache the reportDefinitionUuid for subsequent previews
791
+ if (compiledResult?.reportDefinitionUuid) {
792
+ setCachedReportDefinitionUuid(compiledResult.reportDefinitionUuid);
793
+ }
794
+
795
+ setCompileSuccess(
796
+ compiledResult?.reportDefinitionUuid
797
+ ? `Compiled successfully. Runtime report UUID: ${compiledResult.reportDefinitionUuid}`
798
+ : 'Compiled successfully.'
799
+ );
800
+ } catch (err) {
801
+ setCompileError(err instanceof Error ? err.message : 'Failed to compile report');
802
+ } finally {
803
+ setCompiling(false);
804
+ }
805
+ }, [draft, initialReport?.uuid, handleSave, updateDraft]);
806
+
807
+ /**
808
+ * Handle datasources change
809
+ * Updates the dataSources array in the draft
810
+ */
811
+ const handleDataSourcesChange = useCallback((dataSources: DataSourceInfo[]) => {
812
+ updateDraft({ dataSources });
813
+ }, [updateDraft]);
814
+
815
+ /**
816
+ * Handle data source table change
817
+ * Updates the primary data source in the dataSources array
818
+ */
819
+ const handleTableChange = useCallback((table: string) => {
820
+ // For now, update the first data source or create a new primary one
821
+ const newDataSources = [...draft.dataSources];
822
+ if (newDataSources.length > 0) {
823
+ newDataSources[0].uuid = table;
824
+ newDataSources[0].name = table;
825
+ } else {
826
+ newDataSources.push({
827
+ uuid: table,
828
+ name: table,
829
+ type: 'ETL',
830
+ role: 'PRIMARY',
831
+ });
832
+ }
833
+ updateDraft({ dataSources: newDataSources });
834
+ }, [updateDraft, draft.dataSources]);
835
+
836
+ /**
837
+ * Get list of selected field IDs for highlighting in catalogue
838
+ * Constructs field IDs based on column type to match catalogue formats:
839
+ * - Person attributes: person-attribute-${uuid}
840
+ * - Patient identifiers: patient-identifier-${uuid}
841
+ * - Calculated fields: openmrs.calc.${name}
842
+ * - CORE/Demographic: openmrs.${table}.${name}
843
+ * - ETL columns: ${table}.${name}
844
+ */
845
+ const selectedFieldIds = useMemo(() => {
846
+ return draft.columns.map((col) => {
847
+ // Person attributes
848
+ if (col.dataDefinitionType === 'PERSON_ATTRIBUTE') {
849
+ return `person-attribute-${col.source.attributeTypeUuid || col.name}`;
850
+ }
851
+ // Patient identifiers
852
+ if (col.dataDefinitionType === 'IDENTIFIER') {
853
+ return `patient-identifier-${col.source.identifierTypeUuid || col.name}`;
854
+ }
855
+ // Calculated fields
856
+ if (col.dataDefinitionType === 'CALCULATION') {
857
+ const calcName = (col.dataDefinitionConfig.calculation || col.name).toLowerCase();
858
+ return `openmrs.calc.${calcName}`;
859
+ }
860
+ // CORE/Demographic fields (person, person_address tables)
861
+ if (col.source.table === 'person' || col.source.table === 'person_address') {
862
+ return `openmrs.${col.source.table}.${col.source.field || col.name}`;
863
+ }
864
+ // Person name fields
865
+ if (col.dataDefinitionType === 'PERSON_NAME') {
866
+ return `openmrs.person.${col.source.field || col.name}`;
867
+ }
868
+ // ETL columns - use data source UUID or table name
869
+ const table = col.source.table || col.source.dataSourceUuid || 'unknown';
870
+ return `${table}.${col.source.field || col.name}`;
871
+ });
872
+ }, [draft.columns]);
873
+
874
+ /**
875
+ * Handle removing a column
876
+ */
877
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
878
+ const handleRemoveColumn = useCallback((columnId: string) => {
879
+ // Clear any error message
880
+ setSaveError(null);
881
+ }, []);
882
+
883
+ /**
884
+ * Open preview parameter modal
885
+ */
886
+ const handleRunPreview = useCallback(() => {
887
+ // Check if draft has required fields
888
+ if (!draft.population?.sqlTemplate || draft.columns.length === 0) {
889
+ setPreviewError('Please add a base cohort definition and at least one column before running preview.');
890
+ return;
891
+ }
892
+
893
+ // Check if report needs to be saved first (for new reports)
894
+ if (!initialReport?.uuid) {
895
+ setPreviewError('Please save the report first before running preview.');
896
+ return;
897
+ }
898
+
899
+ // Open the parameter modal
900
+ setPreviewParamModalOpen(true);
901
+ setPreviewError(null);
902
+ }, [draft.population?.sqlTemplate, draft.columns.length, initialReport?.uuid]);
903
+
904
+ /**
905
+ * Run preview with parameters
906
+ */
907
+ const handleRunPreviewWithParams = useCallback(async (parameterValues: Record<string, any>) => {
908
+ // Close the modal
909
+ setPreviewParamModalOpen(false);
910
+ setPreviewRunning(true);
911
+ setPreviewError(null);
912
+
913
+ try {
914
+ // Convert indicators array to Map for draftToConfig
915
+ const indicatorRules = draft.indicatorRules || draft.population?.indicatorRules || [];
916
+ const indicatorsMap = new Map(
917
+ indicatorRules.map(rule => [
918
+ rule.indicatorUuid,
919
+ { sqlTemplate: rule.sqlTemplate, configJson: rule.configJson }
920
+ ])
921
+ );
922
+
923
+ // Convert draft to config
924
+ const config = draftToConfig(draft, indicatorsMap);
925
+
926
+ // Call preview API with report UUID and parameters
927
+ // Only works with saved reports (not new drafts)
928
+ const result = await previewLinelistReport({
929
+ reportUuid: initialReport?.uuid || '',
930
+ config,
931
+ parameters: parameterValues,
932
+ maxRows: 100, // Limit preview to 100 rows
933
+ cachedReportDefinitionUuid: cachedReportDefinitionUuid || undefined,
934
+ });
935
+
936
+ if (!result.success) {
937
+ setPreviewError(result.error || 'Preview failed');
938
+ return;
939
+ }
940
+
941
+ if (result.data) {
942
+ // Store the full result with metadata
943
+ setPreviewData({
944
+ columns: result.data.columns,
945
+ rows: result.data.rows,
946
+ rowCount: result.data.rowCount,
947
+ html: result.data.html,
948
+ });
949
+ }
950
+ } catch (err) {
951
+ setPreviewError(err instanceof Error ? err.message : 'Preview failed');
952
+ } finally {
953
+ setPreviewRunning(false);
954
+ }
955
+ }, [draft]); // eslint-disable-line react-hooks/exhaustive-deps
956
+
957
+ /**
958
+ * Handle adding field from catalogue as column
959
+ */
960
+ const handleAddFieldAsColumn = useCallback((field: CatalogueField) => {
961
+ // Check if column already exists
962
+ if (draft.columns.some((col) => col.name === field.label || col.name === field.name)) {
963
+ setSaveError('This field is already added as a column');
964
+ return;
965
+ }
966
+
967
+ // Handle special cases for person attributes and patient identifiers
968
+ const isPersonAttribute = field.id.startsWith('person-attribute-');
969
+ const isPatientIdentifier = field.id.startsWith('patient-identifier-');
970
+ const uuid = field.name; // For API fields, we store the UUID in the name field
971
+ const now = new Date().toISOString();
972
+
973
+ let newColumn: LinelistColumnDraft;
974
+ let source: ColumnSource;
975
+
976
+ if (isPersonAttribute) {
977
+ // Person Attribute column
978
+ source = {
979
+ dataSourceUuid: 'person_attributes',
980
+ dataSourceName: 'Person Attributes',
981
+ table: 'person_attribute',
982
+ field: field.label,
983
+ fieldType: 'PERSON_ATTRIBUTE',
984
+ attributeTypeUuid: uuid,
985
+ };
986
+ newColumn = {
987
+ id: `col-${Date.now()}`,
988
+ name: field.label,
989
+ description: '',
990
+ source,
991
+ dataDefinitionType: 'PERSON_ATTRIBUTE',
992
+ dataDefinitionConfig: { attributeTypeUuid: uuid },
993
+ additionInfo: {
994
+ addedVia: 'DRAG_DROP',
995
+ addedAt: now,
996
+ orderAdded: draft.columns.length,
997
+ },
998
+ display: {
999
+ width: 150,
1000
+ align: 'left',
1001
+ sortable: true,
1002
+ filterable: true,
1003
+ format: 'text',
1004
+ },
1005
+ sortOrder: draft.columns.length,
1006
+ transformations: [],
1007
+ };
1008
+ } else if (isPatientIdentifier) {
1009
+ // Patient Identifier column
1010
+ source = {
1011
+ dataSourceUuid: 'patient_identifiers',
1012
+ dataSourceName: 'Patient Identifiers',
1013
+ table: 'patient_identifier',
1014
+ field: field.label,
1015
+ fieldType: 'IDENTIFIER',
1016
+ identifierTypeUuid: uuid,
1017
+ };
1018
+ newColumn = {
1019
+ id: `col-${Date.now()}`,
1020
+ name: field.label,
1021
+ description: '',
1022
+ source,
1023
+ dataDefinitionType: 'IDENTIFIER',
1024
+ dataDefinitionConfig: { identifierTypeUuid: uuid, preferred: false },
1025
+ additionInfo: {
1026
+ addedVia: 'DRAG_DROP',
1027
+ addedAt: now,
1028
+ orderAdded: draft.columns.length,
1029
+ },
1030
+ display: {
1031
+ width: 150,
1032
+ align: 'left',
1033
+ sortable: true,
1034
+ filterable: true,
1035
+ format: 'text',
1036
+ },
1037
+ sortOrder: draft.columns.length,
1038
+ repeatResolution: {
1039
+ strategy: 'LATEST',
1040
+ orderBy: undefined,
1041
+ restrictToPeriod: false,
1042
+ ignoreVoided: true,
1043
+ },
1044
+ transformations: [],
1045
+ };
1046
+ } else if (field.source === 'CALCULATION') {
1047
+ // Calculated field
1048
+ const primaryDataSource = draft.dataSources.find(ds => ds.role === 'PRIMARY');
1049
+ source = {
1050
+ dataSourceUuid: primaryDataSource?.uuid || '',
1051
+ dataSourceName: primaryDataSource?.name || '',
1052
+ table: 'calculated',
1053
+ field: field.name,
1054
+ fieldType: 'CALCULATED',
1055
+ };
1056
+ newColumn = {
1057
+ id: `col-${Date.now()}`,
1058
+ name: field.label || field.name,
1059
+ description: '',
1060
+ source,
1061
+ dataDefinitionType: 'CALCULATION',
1062
+ dataDefinitionConfig: { calculation: field.name.toUpperCase(), onDate: ':startDate' },
1063
+ additionInfo: {
1064
+ addedVia: 'DRAG_DROP',
1065
+ addedAt: now,
1066
+ orderAdded: draft.columns.length,
1067
+ },
1068
+ display: {
1069
+ width: 120,
1070
+ align: 'center',
1071
+ sortable: true,
1072
+ filterable: false,
1073
+ format: 'number',
1074
+ },
1075
+ sortOrder: draft.columns.length,
1076
+ transformations: [],
1077
+ };
1078
+ } else if (field.id.startsWith('openmrs.person_address.') || field.table === 'person_address') {
1079
+ // Person Address field
1080
+ source = {
1081
+ dataSourceUuid: 'person_address',
1082
+ dataSourceName: 'Person Address',
1083
+ table: 'person_address',
1084
+ field: field.name,
1085
+ fieldType: 'TEXT',
1086
+ };
1087
+ newColumn = {
1088
+ id: `col-${Date.now()}`,
1089
+ name: field.label || field.name,
1090
+ description: '',
1091
+ source,
1092
+ dataDefinitionType: 'PERSON_ADDRESS',
1093
+ dataDefinitionConfig: {
1094
+ type: 'ADDRESS_FIELD',
1095
+ field: field.name, // e.g., 'address5', 'address4', etc.
1096
+ },
1097
+ additionInfo: {
1098
+ addedVia: 'DRAG_DROP',
1099
+ addedAt: now,
1100
+ orderAdded: draft.columns.length,
1101
+ },
1102
+ display: {
1103
+ width: 150,
1104
+ align: 'left',
1105
+ sortable: true,
1106
+ filterable: true,
1107
+ format: 'text',
1108
+ },
1109
+ sortOrder: draft.columns.length,
1110
+ transformations: [],
1111
+ };
1112
+ } else {
1113
+ // SQL/ETL column
1114
+ source = {
1115
+ dataSourceUuid: field.dataSourceUuid || field.table,
1116
+ dataSourceName: field.dataSourceName || field.table,
1117
+ table: field.table,
1118
+ field: field.name,
1119
+ fieldType: field.type || 'UNKNOWN',
1120
+ };
1121
+ newColumn = {
1122
+ id: `col-${Date.now()}`,
1123
+ name: field.label || field.name,
1124
+ description: '',
1125
+ source,
1126
+ dataDefinitionType: 'SQL',
1127
+ dataDefinitionConfig: {
1128
+ sql: `\`${field.table}\`.\`${field.name}\``, // Default SQL - table.column
1129
+ },
1130
+ additionInfo: {
1131
+ addedVia: 'DRAG_DROP',
1132
+ addedAt: now,
1133
+ orderAdded: draft.columns.length,
1134
+ },
1135
+ display: {
1136
+ width: 150,
1137
+ align: 'left',
1138
+ sortable: true,
1139
+ filterable: true,
1140
+ format: 'text',
1141
+ },
1142
+ sortOrder: draft.columns.length,
1143
+ repeatResolution: field.isRepeated ? {
1144
+ strategy: 'LATEST',
1145
+ orderBy: undefined,
1146
+ restrictToPeriod: false,
1147
+ ignoreVoided: true,
1148
+ } : undefined,
1149
+ transformations: [],
1150
+ };
1151
+ }
1152
+
1153
+ const updatedColumns = [...draft.columns, newColumn];
1154
+ updateDraft({ columns: updatedColumns });
1155
+ }, [draft.columns, draft.dataSources, updateDraft]);
1156
+
1157
+ /**
1158
+ * Handle adding a custom SQL column from the modal
1159
+ * The SQL runs per-row and can reference :client_id / :patient_id
1160
+ */
1161
+ const handleAddCustomSqlColumn = useCallback((config: CustomSqlColumnConfig) => {
1162
+ // Prevent duplicate column names
1163
+ if (draft.columns.some((col) => col.name.toLowerCase() === config.name.toLowerCase())) {
1164
+ setSaveError('A column with this name already exists');
1165
+ return;
1166
+ }
1167
+
1168
+ const now = new Date().toISOString();
1169
+ const primaryDataSource = draft.dataSources.find(ds => ds.role === 'PRIMARY');
1170
+
1171
+ const newColumn: LinelistColumnDraft = {
1172
+ id: `col-${Date.now()}`,
1173
+ name: config.name,
1174
+ description: config.description || '',
1175
+ source: {
1176
+ dataSourceUuid: primaryDataSource?.uuid || 'custom_sql',
1177
+ dataSourceName: primaryDataSource?.name || 'Custom SQL',
1178
+ table: 'custom_sql',
1179
+ field: config.name,
1180
+ fieldType: 'SQL',
1181
+ },
1182
+ dataDefinitionType: 'SQL',
1183
+ dataDefinitionConfig: {
1184
+ sql: config.sql,
1185
+ },
1186
+ additionInfo: {
1187
+ addedVia: 'SQL_BUILDER',
1188
+ addedAt: now,
1189
+ orderAdded: draft.columns.length,
1190
+ },
1191
+ display: {
1192
+ width: 150,
1193
+ align: 'left',
1194
+ sortable: true,
1195
+ filterable: true,
1196
+ format: 'text',
1197
+ },
1198
+ sortOrder: draft.columns.length,
1199
+ repeatResolution: config.repeatResolution ? {
1200
+ strategy: 'LATEST',
1201
+ orderBy: undefined,
1202
+ restrictToPeriod: false,
1203
+ ignoreVoided: true,
1204
+ } : undefined,
1205
+ transformations: [],
1206
+ };
1207
+
1208
+ const updatedColumns = [...draft.columns, newColumn];
1209
+ updateDraft({ columns: updatedColumns });
1210
+ }, [draft.columns, draft.dataSources, updateDraft]);
1211
+
1212
+ /**
1213
+ * Handle adding draft column (from observation/diagnosis modals)
1214
+ */
1215
+ const handleAddDraftColumn = useCallback((column: LinelistColumnDraft) => {
1216
+ // Prevent duplicate column names
1217
+ if (draft.columns.some((col) => col.name.toLowerCase() === column.name.toLowerCase())) {
1218
+ setSaveError('A column with this name already exists');
1219
+ return;
1220
+ }
1221
+
1222
+ const updatedColumns = [...draft.columns, column];
1223
+ updateDraft({ columns: updatedColumns });
1224
+ }, [draft.columns, updateDraft]);
1225
+
1226
+ /**
1227
+ * Handle adding field from catalogue as filter
1228
+ */
1229
+ const handleAddFieldAsFilter = useCallback((field: CatalogueField) => {
1230
+ const newCondition = {
1231
+ id: `cond-${Date.now()}`,
1232
+ field: field.name,
1233
+ fieldLabel: field.label,
1234
+ fieldType: field.type,
1235
+ operator: getDefaultOperator(field.type),
1236
+ value: '',
1237
+ };
1238
+
1239
+ const updatedFilter = {
1240
+ ...draft.population.visualFilter,
1241
+ rootGroup: {
1242
+ ...draft.population.visualFilter?.rootGroup,
1243
+ conditions: [...(draft.population.visualFilter?.rootGroup?.conditions || []), newCondition],
1244
+ },
1245
+ };
1246
+
1247
+ updateDraft({ population: { ...draft.population, visualFilter: updatedFilter } });
1248
+ }, [draft.population, updateDraft]);
1249
+
1250
+ /**
1251
+ * Handle fields available from data catalogue
1252
+ * Convert CatalogueField[] to format expected by visual filter builder
1253
+ */
1254
+ const handleFieldsAvailable = useCallback((fields: CatalogueField[]) => {
1255
+ const filterFields = fields.map((f) => ({
1256
+ name: f.name,
1257
+ label: f.label,
1258
+ type: f.type,
1259
+ }));
1260
+ setAvailableFields(filterFields);
1261
+ }, []);
1262
+
1263
+ /**
1264
+ * Handle ETL structure detection from data catalogue
1265
+ */
1266
+ const handleEtlStructureDetected = useCallback((structure: EtlStructure) => {
1267
+ setEtlStructure(structure);
1268
+ }, []);
1269
+
1270
+ /**
1271
+ * Handle panel resize with mouse
1272
+ */
1273
+ useEffect(() => {
1274
+ const handleMouseMove = (e: MouseEvent) => {
1275
+ if (isResizingLeft) {
1276
+ const newWidth = Math.max(200, Math.min(500, e.clientX));
1277
+ setLeftPanelWidth(newWidth);
1278
+ }
1279
+ if (isResizingMiddle) {
1280
+ const workspaceRect = document.querySelector(`.${styles.workspace}`)?.getBoundingClientRect();
1281
+ if (workspaceRect) {
1282
+ const leftPanelEnd = leftPanelCollapsed ? 48 : leftPanelWidth;
1283
+ const newWidth = Math.max(400, Math.min(700, e.clientX - workspaceRect.left - leftPanelEnd));
1284
+ setMiddlePanelWidth(newWidth);
1285
+ }
1286
+ }
1287
+ };
1288
+
1289
+ const handleMouseUp = () => {
1290
+ setIsResizingLeft(false);
1291
+ setIsResizingMiddle(false);
1292
+ document.body.style.cursor = 'default';
1293
+ document.body.style.userSelect = '';
1294
+ };
1295
+
1296
+ if (isResizingLeft || isResizingMiddle) {
1297
+ document.addEventListener('mousemove', handleMouseMove);
1298
+ document.addEventListener('mouseup', handleMouseUp);
1299
+ document.body.style.cursor = 'col-resize';
1300
+ document.body.style.userSelect = 'none';
1301
+
1302
+ return () => {
1303
+ document.removeEventListener('mousemove', handleMouseMove);
1304
+ document.removeEventListener('mouseup', handleMouseUp);
1305
+ document.body.style.cursor = 'default';
1306
+ document.body.style.userSelect = '';
1307
+ };
1308
+ }
1309
+ }, [isResizingLeft, isResizingMiddle, leftPanelWidth, leftPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
1310
+
1311
+ /**
1312
+ * Start resizing left panel
1313
+ */
1314
+ const startResizeLeft = useCallback((e: React.MouseEvent) => {
1315
+ e.preventDefault();
1316
+ setIsResizingLeft(true);
1317
+ }, []);
1318
+
1319
+ /**
1320
+ * Start resizing middle panel
1321
+ */
1322
+ const startResizeMiddle = useCallback((e: React.MouseEvent) => {
1323
+ e.preventDefault();
1324
+ setIsResizingMiddle(true);
1325
+ }, []);
1326
+
1327
+ /**
1328
+ * Get default operator for field type
1329
+ */
1330
+ function getDefaultOperator(fieldType: FilterFieldType): FilterOperator {
1331
+ switch (fieldType) {
1332
+ case 'TEXT':
1333
+ return 'CONTAINS';
1334
+ case 'NUMBER':
1335
+ return 'EQUALS';
1336
+ case 'DATE':
1337
+ return 'BETWEEN_DATES';
1338
+ case 'CODED':
1339
+ return 'IS_ONE_OF';
1340
+ case 'BOOLEAN':
1341
+ return 'IS_TRUE';
1342
+ case 'LOCATION':
1343
+ return 'IN_LOCATION';
1344
+ default:
1345
+ return 'EQUALS';
1346
+ }
1347
+ }
1348
+
1349
+ if (loading) {
1350
+ return (
1351
+ <div className={styles.workspace}>
1352
+ <div className={styles.loading}>Loading report...</div>
1353
+ </div>
1354
+ );
1355
+ }
1356
+
1357
+ return (
1358
+ <div className={styles.workspace}>
1359
+ {/* Header */}
1360
+ <header className={styles.header}>
1361
+ <div className={styles.headerLeft}>
1362
+ <Button
1363
+ kind="ghost"
1364
+ size="sm"
1365
+ hasIconOnly
1366
+ renderIcon={ChevronLeft}
1367
+ iconDescription="Back"
1368
+ onClick={handleBack}
1369
+ />
1370
+ </div>
1371
+ <div className={styles.headerCenter}>
1372
+ <TextInput
1373
+ id="report-name"
1374
+ labelText=""
1375
+ value={draft.name}
1376
+ onChange={(e) => updateDraft({ name: e.target.value })}
1377
+ placeholder="Report name"
1378
+ className={styles.nameInput}
1379
+ size="sm"
1380
+ />
1381
+ <Tag size="sm" type={compileSuccess ? 'green' : 'gray'}>
1382
+ {compileSuccess ? 'Compiled' : 'Draft'}
1383
+ </Tag>
1384
+ {draft.unsavedChanges && (
1385
+ <Tag size="sm" type="blue">Unsaved</Tag>
1386
+ )}
1387
+ </div>
1388
+ <div className={styles.headerRight} >
1389
+ <ButtonSet>
1390
+ <Button
1391
+ kind="ghost"
1392
+ size="sm"
1393
+ renderIcon={Save}
1394
+ onClick={handleSave}
1395
+ disabled={saving || !draft.unsavedChanges}
1396
+ >
1397
+ {saving ? 'Saving...' : 'Save'}
1398
+ </Button>
1399
+ <Button
1400
+ kind="primary"
1401
+ size="sm"
1402
+ renderIcon={Send}
1403
+ onClick={handleCompile}
1404
+ disabled={compiling || !initialReport?.uuid || !hasPrivilege(RB.REPORT_COMPILE)}
1405
+ title={
1406
+ hasPrivilege(RB.REPORT_COMPILE)
1407
+ ? undefined
1408
+ : 'Requires the report compile privilege'
1409
+ }
1410
+ >
1411
+ {compiling ? 'Compiling...' : 'Compile'}
1412
+ </Button>
1413
+ </ButtonSet>
1414
+ <OverflowMenu size="sm" renderIcon={OverflowMenuHorizontal} align="right">
1415
+ <OverflowMenuItem itemText="Save as" />
1416
+ <OverflowMenuItem itemText="Export JSON" />
1417
+ <OverflowMenuItem itemText="Import JSON" />
1418
+ <OverflowMenuItem itemText="Validate" />
1419
+ <OverflowMenuItem itemText="View history" />
1420
+ <OverflowMenuItem itemText="Retire" />
1421
+ </OverflowMenu>
1422
+ </div>
1423
+ </header>
1424
+
1425
+ {/* Error notifications */}
1426
+ {error && (
1427
+ <div className={styles.notification}>
1428
+ <InlineNotification kind="error" title="Error" subtitle={error} />
1429
+ </div>
1430
+ )}
1431
+ {saveError && (
1432
+ <div className={styles.notification}>
1433
+ <InlineNotification kind="error" title="Save Error" subtitle={saveError} onClose={() => setSaveError(null)} />
1434
+ </div>
1435
+ )}
1436
+ {compileError && (
1437
+ <div className={styles.notification}>
1438
+ <InlineNotification kind="error" title="Compile Error" subtitle={compileError} onClose={() => setCompileError(null)} />
1439
+ </div>
1440
+ )}
1441
+ {compileSuccess && (
1442
+ <div className={styles.notification}>
1443
+ <InlineNotification kind="success" title="Compiled" subtitle={compileSuccess} onClose={() => setCompileSuccess(null)} />
1444
+ </div>
1445
+ )}
1446
+
1447
+ {/* Compile Setup Modal */}
1448
+ <CompileSetupModal
1449
+ open={showCompileSetupModal}
1450
+ currentCategoryUuid={draft.categoryUuid}
1451
+ onConfirm={handleCompileSetupConfirm}
1452
+ onClose={handleCompileSetupCancel}
1453
+ reportType="linelist"
1454
+ />
1455
+
1456
+ {/* Preview Parameter Modal */}
1457
+ <PreviewParameterModal
1458
+ open={previewParamModalOpen}
1459
+ onClose={() => setPreviewParamModalOpen(false)}
1460
+ onRun={handleRunPreviewWithParams}
1461
+ parameters={draft.parameters}
1462
+ loading={previewRunning}
1463
+ />
1464
+
1465
+ {/* Three-Panel Layout */}
1466
+ <div className={styles.panels}>
1467
+ {/* Left Panel - Data Catalogue */}
1468
+ <aside
1469
+ className={styles.leftPanel}
1470
+ style={{
1471
+ width: leftPanelCollapsed ? 48 : leftPanelWidth,
1472
+ }}
1473
+ >
1474
+ <div className={styles.panelHeader}>
1475
+ {!leftPanelCollapsed && (
1476
+ <>
1477
+ <h3 className={styles.panelTitle}>Data Catalogue</h3>
1478
+ <Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronLeft} iconDescription="Collapse left panel" onClick={() => setLeftPanelCollapsed(true)} />
1479
+ </>
1480
+ )}
1481
+ {leftPanelCollapsed && (
1482
+ <Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronRight} iconDescription="Expand left panel" onClick={() => setLeftPanelCollapsed(false)} />
1483
+ )}
1484
+ </div>
1485
+
1486
+ {!leftPanelCollapsed && (
1487
+ <div className={styles.panelContent}>
1488
+ {/* Column Sources Section */}
1489
+ <div className={styles.columnSourcesSection}>
1490
+ {/* Population Sources Section */}
1491
+ <div className={styles.populationCard}>
1492
+ <PopulationSourceSelector
1493
+ populationSources={draft.populationSources}
1494
+ onChange={(sources) => updateDraft({ populationSources: sources })}
1495
+ disabled={loading}
1496
+ />
1497
+ </div>
1498
+
1499
+ {/* Column Sources Section */}
1500
+ <div className={styles.columnSourcesCard}>
1501
+ <div className={styles.columnSourcesHeader}>
1502
+ <h4 className={styles.columnSourcesTitle}>
1503
+ <span className={styles.columnSourcesIcon}>📊</span>
1504
+ Column Sources
1505
+ </h4>
1506
+ <span className={styles.columnSourcesSubtitle}>
1507
+ Select columns for output
1508
+ </span>
1509
+ </div>
1510
+ <MultiDatasourceSelector
1511
+ dataSources={draft.dataSources}
1512
+ onChange={handleDataSourcesChange}
1513
+ disabled={loading}
1514
+ />
1515
+ <DataCatalogue
1516
+ dataSources={draft.dataSources}
1517
+ populationSources={draft.populationSources}
1518
+ onPopulationSourcesChange={undefined}
1519
+ onAddToColumns={handleAddFieldAsColumn}
1520
+ onAddToFilters={handleAddFieldAsFilter}
1521
+ onTableChange={handleTableChange}
1522
+ onFieldsAvailable={handleFieldsAvailable}
1523
+ onEtlStructureDetected={handleEtlStructureDetected}
1524
+ selectedFields={selectedFieldIds}
1525
+ onAddCustomSqlColumn={handleAddCustomSqlColumn}
1526
+ onAddDraftColumn={handleAddDraftColumn}
1527
+ idColumnAlias="client_id"
1528
+ showPopulationSelector={false}
1529
+ />
1530
+ </div>
1531
+ </div>
1532
+ </div>
1533
+ )}
1534
+ </aside>
1535
+
1536
+ {/* Resize Handle - Left/Middle */}
1537
+ <div
1538
+ className={`${styles.resizeHandle} ${styles.resizeHandleLeftMiddle}`}
1539
+ onMouseDown={startResizeLeft}
1540
+ style={{ cursor: isResizingLeft ? 'col-resize' : 'col-resize' }}
1541
+ />
1542
+
1543
+ {/* Middle Panel - Query Configuration */}
1544
+ <aside
1545
+ className={styles.middlePanel}
1546
+ style={{
1547
+ width: middlePanelCollapsed ? 48 : middlePanelWidth,
1548
+ }}
1549
+ >
1550
+ <div className={styles.panelHeader}>
1551
+ {!middlePanelCollapsed && (
1552
+ <>
1553
+ <h3 className={styles.panelTitle}>Query Configuration</h3>
1554
+ <Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronLeft} iconDescription="Collapse middle panel" onClick={() => setMiddlePanelCollapsed(true)} />
1555
+ </>
1556
+ )}
1557
+ {middlePanelCollapsed && (
1558
+ <Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronRight} iconDescription="Expand middle panel" onClick={() => setMiddlePanelCollapsed(false)} />
1559
+ )}
1560
+ </div>
1561
+
1562
+ {!middlePanelCollapsed && (
1563
+ <div className={styles.panelContent}>
1564
+ <QueryConfigPanel
1565
+ draft={draft}
1566
+ onDraftChange={updateDraft}
1567
+ availableFields={availableFields}
1568
+ indicators={indicators}
1569
+ onRemoveColumn={handleRemoveColumn}
1570
+ />
1571
+ </div>
1572
+ )}
1573
+ </aside>
1574
+
1575
+ {/* Resize Handle - Middle/Right */}
1576
+ <div
1577
+ className={`${styles.resizeHandle} ${styles.resizeHandleMiddleRight}`}
1578
+ onMouseDown={startResizeMiddle}
1579
+ style={{ cursor: isResizingMiddle ? 'col-resize' : 'col-resize' }}
1580
+ />
1581
+
1582
+ {/* Right Panel - Preview */}
1583
+ <main className={styles.rightPanel}>
1584
+ <div className={styles.panelHeader}>
1585
+ <h3 className={styles.panelTitle}>Preview</h3>
1586
+ <ButtonSet>
1587
+ <Button
1588
+ kind="primary"
1589
+ size="sm"
1590
+ onClick={handleRunPreview}
1591
+ disabled={
1592
+ previewRunning ||
1593
+ !initialReport?.compiledReportDefinitionUuid ||
1594
+ !hasPrivilege(RB.SQL_EXECUTE)
1595
+ }
1596
+ tooltipAlignment="end"
1597
+ tooltipPosition="bottom"
1598
+ >
1599
+ {previewRunning ? 'Loading...' : 'Run Preview'}
1600
+ </Button>
1601
+ </ButtonSet>
1602
+ </div>
1603
+
1604
+ <div className={styles.panelContent}>
1605
+ {!initialReport?.compiledReportDefinitionUuid && mode === 'edit' && (
1606
+ <InlineNotification
1607
+ kind="info"
1608
+ title="Report Not Compiled"
1609
+ subtitle="Please compile the report first before running preview. Click the 'Compile' button above to generate the report definition."
1610
+ lowContrast
1611
+ />
1612
+ )}
1613
+
1614
+ {previewError && (
1615
+ <InlineNotification kind="error" title="Preview Error" subtitle={previewError} />
1616
+ )}
1617
+
1618
+ {!previewData && !previewError && initialReport?.compiledReportDefinitionUuid && (
1619
+ <div className={styles.emptyPreview}>
1620
+ <Document size={32} />
1621
+ <p>Run a preview to see sample data</p>
1622
+ </div>
1623
+ )}
1624
+
1625
+ {previewData && previewData.rowCount > 0 && (
1626
+ <div className={styles.previewResults}>
1627
+ <div className={styles.previewStatus}>
1628
+ <span>Preview rows: {previewData.rowCount}</span>
1629
+ <span>Query duration: —</span>
1630
+ </div>
1631
+ {previewData.html ? (
1632
+ <div
1633
+ className={styles.htmlPreview}
1634
+ dangerouslySetInnerHTML={{ __html: previewData.html }}
1635
+ />
1636
+ ) : (
1637
+ <TableContainer>
1638
+ <Table>
1639
+ <TableHead>
1640
+ <TableRow>
1641
+ {previewData.columns.map((column) => (
1642
+ <TableHeader key={column}>{column}</TableHeader>
1643
+ ))}
1644
+ </TableRow>
1645
+ </TableHead>
1646
+ <TableBody>
1647
+ {previewData.rows.map((row, index) => (
1648
+ <TableRow key={index}>
1649
+ {previewData.columns.map((column) => (
1650
+ <TableCell key={`${index}-${column}`}>
1651
+ {row[column]?.toString() ?? ''}
1652
+ </TableCell>
1653
+ ))}
1654
+ </TableRow>
1655
+ ))}
1656
+ </TableBody>
1657
+ </Table>
1658
+ </TableContainer>
1659
+ )}
1660
+ </div>
1661
+ )}
1662
+
1663
+ {previewData && previewData.rowCount === 0 && (
1664
+ <div className={styles.emptyPreview}>
1665
+ <Document size={32} />
1666
+ <p>No data found for the current configuration</p>
1667
+ </div>
1668
+ )}
1669
+ </div>
1670
+ </main>
1671
+ </div>
1672
+ </div>
1673
+ );
1674
+ };
1675
+
1676
+ export default LinelistBuilderWorkspace;