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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (670) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/.tx/config +12 -0
  5. package/LICENSE +401 -0
  6. package/README.md +336 -0
  7. package/__mocks__/react-i18next.js +12 -0
  8. package/dist/128.css +1 -0
  9. package/dist/128.css.map +1 -0
  10. package/dist/128.js +338 -0
  11. package/dist/128.js.map +1 -0
  12. package/dist/1480.js +1 -0
  13. package/dist/1480.js.map +1 -0
  14. package/dist/1646.js +1 -0
  15. package/dist/1646.js.map +1 -0
  16. package/dist/1869.js +1 -0
  17. package/dist/1869.js.map +1 -0
  18. package/dist/1877.js +1 -0
  19. package/dist/1877.js.map +1 -0
  20. package/dist/2131.js +1 -0
  21. package/dist/2131.js.map +1 -0
  22. package/dist/2317.js +1 -0
  23. package/dist/2317.js.map +1 -0
  24. package/dist/2408.js +1 -0
  25. package/dist/2408.js.map +1 -0
  26. package/dist/2416.js +1 -0
  27. package/dist/2416.js.map +1 -0
  28. package/dist/282.js +1 -0
  29. package/dist/282.js.map +1 -0
  30. package/dist/2881.js +1 -0
  31. package/dist/2881.js.map +1 -0
  32. package/dist/2997.js +1 -0
  33. package/dist/2997.js.map +1 -0
  34. package/dist/3378.js +1 -0
  35. package/dist/3378.js.map +1 -0
  36. package/dist/3963.js +1 -0
  37. package/dist/3963.js.map +1 -0
  38. package/dist/4106.js +1 -0
  39. package/dist/4106.js.map +1 -0
  40. package/dist/4111.js +1 -0
  41. package/dist/4111.js.map +1 -0
  42. package/dist/4171.css +1 -0
  43. package/dist/4171.css.map +1 -0
  44. package/dist/4171.js +43 -0
  45. package/dist/4171.js.map +1 -0
  46. package/dist/434.js +1 -0
  47. package/dist/434.js.map +1 -0
  48. package/dist/4348.js +1 -0
  49. package/dist/4348.js.map +1 -0
  50. package/dist/4383.js +1 -0
  51. package/dist/4383.js.map +1 -0
  52. package/dist/4658.js +1 -0
  53. package/dist/4658.js.map +1 -0
  54. package/dist/4668.js +1 -0
  55. package/dist/4668.js.map +1 -0
  56. package/dist/4928.js +1 -0
  57. package/dist/4928.js.map +1 -0
  58. package/dist/5098.js +1 -0
  59. package/dist/5098.js.map +1 -0
  60. package/dist/5117.js +1 -0
  61. package/dist/5117.js.map +1 -0
  62. package/dist/5132.js +1 -0
  63. package/dist/5132.js.map +1 -0
  64. package/dist/5145.js +1 -0
  65. package/dist/5145.js.map +1 -0
  66. package/dist/550.js +1 -0
  67. package/dist/550.js.map +1 -0
  68. package/dist/5503.js +1 -0
  69. package/dist/5503.js.map +1 -0
  70. package/dist/556.js +1 -0
  71. package/dist/556.js.map +1 -0
  72. package/dist/5644.js +1 -0
  73. package/dist/5644.js.map +1 -0
  74. package/dist/5940.js +1 -0
  75. package/dist/5940.js.map +1 -0
  76. package/dist/6047.js +1 -0
  77. package/dist/6047.js.map +1 -0
  78. package/dist/6237.js +1 -0
  79. package/dist/6237.js.map +1 -0
  80. package/dist/6371.js +1 -0
  81. package/dist/6371.js.map +1 -0
  82. package/dist/6377.js +1 -0
  83. package/dist/6377.js.map +1 -0
  84. package/dist/6444.js +1 -0
  85. package/dist/6444.js.map +1 -0
  86. package/dist/6508.js +1 -0
  87. package/dist/6508.js.map +1 -0
  88. package/dist/6652.js +1 -0
  89. package/dist/6652.js.map +1 -0
  90. package/dist/6686.js +1 -0
  91. package/dist/6686.js.map +1 -0
  92. package/dist/6724.js +1 -0
  93. package/dist/6724.js.map +1 -0
  94. package/dist/6867.js +19 -0
  95. package/dist/6867.js.map +1 -0
  96. package/dist/6904.js +1 -0
  97. package/dist/6904.js.map +1 -0
  98. package/dist/7045.js +1 -0
  99. package/dist/7045.js.map +1 -0
  100. package/dist/7175.js +1 -0
  101. package/dist/7175.js.map +1 -0
  102. package/dist/7182.js +1 -0
  103. package/dist/7182.js.map +1 -0
  104. package/dist/7475.js +1 -0
  105. package/dist/7475.js.map +1 -0
  106. package/dist/7742.js +1 -0
  107. package/dist/7742.js.map +1 -0
  108. package/dist/7912.js +1 -0
  109. package/dist/7912.js.map +1 -0
  110. package/dist/8251.js +1 -0
  111. package/dist/8251.js.map +1 -0
  112. package/dist/8358.js +1 -0
  113. package/dist/8358.js.map +1 -0
  114. package/dist/8359.js +1 -0
  115. package/dist/8359.js.map +1 -0
  116. package/dist/8695.js +1 -0
  117. package/dist/8695.js.map +1 -0
  118. package/dist/903.js +1 -0
  119. package/dist/903.js.map +1 -0
  120. package/dist/9072.js +1 -0
  121. package/dist/9072.js.map +1 -0
  122. package/dist/9806.js +1 -0
  123. package/dist/9806.js.map +1 -0
  124. package/dist/997.js +1 -0
  125. package/dist/997.js.map +1 -0
  126. package/dist/openmrs-esm-report-builder.js +3 -0
  127. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +1433 -0
  128. package/dist/openmrs-esm-report-builder.js.map +1 -0
  129. package/dist/routes.json +1 -0
  130. package/e2e/core/env.ts +5 -0
  131. package/e2e/core/global-setup.ts +34 -0
  132. package/e2e/core/test.ts +21 -0
  133. package/e2e/pages/app-shell.page.ts +35 -0
  134. package/e2e/pages/etl-sources.page.ts +36 -0
  135. package/e2e/pages/report-sections.page.ts +21 -0
  136. package/e2e/pages/section-preview.modal.ts +29 -0
  137. package/e2e/specs/etl-sources.spec.ts +51 -0
  138. package/e2e/specs/navigation.spec.ts +13 -0
  139. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  140. package/e2e/specs/section-preview.spec.ts +94 -0
  141. package/e2e/support/github/Dockerfile +32 -0
  142. package/e2e/support/github/Dockerfile.dockerignore +13 -0
  143. package/e2e/support/github/docker-compose.yml +31 -0
  144. package/e2e/support/github/run-e2e-docker-env.sh +131 -0
  145. package/e2e/support/github/spa-assemble-config.json +6 -0
  146. package/e2e/tsconfig.json +11 -0
  147. package/example.env +5 -0
  148. package/linelist_report_builder_design_spec.md +816 -0
  149. package/package.json +98 -0
  150. package/playwright.config.ts +28 -0
  151. package/rspack.config.js +2 -0
  152. package/src/__debug__/all-designs.test.tsx +33 -0
  153. package/src/admin-card-link.extension.module.scss +55 -0
  154. package/src/admin-card-link.extension.tsx +24 -0
  155. package/src/components/admin/admin-page.component.tsx +75 -0
  156. package/src/components/admin/age-categories-page.component.tsx +109 -0
  157. package/src/components/admin/age-groups-page.component.tsx +146 -0
  158. package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
  159. package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
  160. package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
  161. package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
  162. package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
  163. package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
  164. package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
  165. package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
  166. package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
  167. package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
  168. package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
  169. package/src/components/admin/etl-sources-page.component.tsx +355 -0
  170. package/src/components/admin/etl-tasks-page.component.scss +370 -0
  171. package/src/components/admin/etl-tasks-page.component.tsx +768 -0
  172. package/src/components/admin/report-categories-page.component.tsx +267 -0
  173. package/src/components/admin/report-library-page.component.tsx +551 -0
  174. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  175. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  176. package/src/components/ai-support/ai.service.ts +21 -0
  177. package/src/components/ai-support/ai.types.ts +12 -0
  178. package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
  179. package/src/components/app-shell/report-builder-shell.scss +18 -0
  180. package/src/components/dashboards/dashboard-config.schema.ts +97 -0
  181. package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
  182. package/src/components/dashboards/dashboards-admin.scss +62 -0
  183. package/src/components/dashboards/dashboards-page.component.tsx +153 -0
  184. package/src/components/dashboards/dashboards-table.component.tsx +130 -0
  185. package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
  186. package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
  187. package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
  188. package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
  189. package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
  190. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  191. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  192. package/src/components/data-themes/data-themes-page.component.tsx +139 -0
  193. package/src/components/data-themes/data-themes-table.component.tsx +98 -0
  194. package/src/components/data-themes/icon-registry.ts +35 -0
  195. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  196. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  197. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  198. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  199. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  200. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  201. package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
  202. package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
  203. package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
  204. package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
  205. package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
  206. package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
  207. package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
  208. package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
  209. package/src/components/data-visualizer/components/header/header.scss +71 -0
  210. package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
  211. package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
  212. package/src/components/data-visualizer/components/panel/panel.scss +25 -0
  213. package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
  214. package/src/components/data-visualizer/constants.ts +445 -0
  215. package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
  216. package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
  217. package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
  218. package/src/components/data-visualizer/data-visualizer.scss +448 -0
  219. package/src/components/data-visualizer/declarations.d.ts +131 -0
  220. package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
  221. package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
  222. package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
  223. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
  224. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
  225. package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
  226. package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
  227. package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
  228. package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
  229. package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
  230. package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
  231. package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
  232. package/src/components/data-visualizer/types.ts +206 -0
  233. package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
  234. package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
  235. package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
  236. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
  237. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
  238. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
  239. package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
  240. package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
  241. package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
  242. package/src/components/etl-monitor/builder/index.ts +5 -0
  243. package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
  244. package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
  245. package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
  246. package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
  247. package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
  248. package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
  249. package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
  250. package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
  251. package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
  252. package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
  253. package/src/components/etl-monitor/builder/steps/index.ts +5 -0
  254. package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
  255. package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
  256. package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
  257. package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
  258. package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
  259. package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
  260. package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
  261. package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
  262. package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
  263. package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
  264. package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
  265. package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
  266. package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
  267. package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
  268. package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
  269. package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
  270. package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
  271. package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
  272. package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
  273. package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
  274. package/src/components/etl-monitor/renderers/components/index.ts +6 -0
  275. package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
  276. package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
  277. package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
  278. package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
  279. package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
  280. package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
  281. package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
  282. package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
  283. package/src/components/etl-monitor/renderers/index.ts +11 -0
  284. package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
  285. package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
  286. package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
  287. package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
  288. package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
  289. package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
  290. package/src/components/import-export/artifact-groups.ts +153 -0
  291. package/src/components/import-export/export-report-modal.component.tsx +265 -0
  292. package/src/components/import-export/import-export-page.component.tsx +610 -0
  293. package/src/components/import-export/import-export.styles.scss +825 -0
  294. package/src/components/import-export/import-package-modal.component.tsx +401 -0
  295. package/src/components/import-export/index.ts +3 -0
  296. package/src/components/indicators/README.md +24 -0
  297. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  298. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  299. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  300. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  301. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  302. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  303. package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
  304. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
  305. package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
  306. package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
  307. package/src/components/indicators/custom-condition-input.component.tsx +310 -0
  308. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  309. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  310. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  311. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  312. package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
  313. package/src/components/indicators/indicator-search-select.component.tsx +297 -0
  314. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  315. package/src/components/indicators/indicators-page.component.tsx +673 -0
  316. package/src/components/indicators/indicators-page.scss +121 -0
  317. package/src/components/indicators/indicators-table.component.tsx +188 -0
  318. package/src/components/indicators/preview-table.component.tsx +31 -0
  319. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  320. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
  321. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  322. package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
  323. package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
  324. package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
  325. package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
  326. package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
  327. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  328. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  329. package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
  330. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  331. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  332. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  333. package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
  334. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
  335. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  336. package/src/components/indicators/sql-preview.component.tsx +19 -0
  337. package/src/components/indicators/tag-input.component.tsx +177 -0
  338. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  339. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  340. package/src/components/indicators/types/composite-indicator.types.ts +27 -0
  341. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  342. package/src/components/indicators/types/custom-indicator.types.ts +161 -0
  343. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  344. package/src/components/indicators/types/indicator-types.ts +57 -0
  345. package/src/components/indicators/types/sql-builders.ts +43 -0
  346. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
  347. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
  348. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  349. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
  350. package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
  351. package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
  352. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  353. package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
  354. package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
  355. package/src/components/indicators/utils/population-sql.compiler.test.ts +985 -0
  356. package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
  357. package/src/components/landing/landing-action-item.component.tsx +50 -0
  358. package/src/components/landing/landing-action-list.component.tsx +12 -0
  359. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  360. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  361. package/src/components/landing/landing-hero.component.tsx +18 -0
  362. package/src/components/landing/landing-section-card.component.tsx +33 -0
  363. package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
  364. package/src/components/landing/report-builder-landing-page.scss +131 -0
  365. package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
  366. package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
  367. package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
  368. package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
  369. package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
  370. package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
  371. package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
  372. package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
  373. package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
  374. package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
  375. package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
  376. package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
  377. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
  378. package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
  379. package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
  380. package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
  381. package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
  382. package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
  383. package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
  384. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
  385. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
  386. package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
  387. package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
  388. package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
  389. package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
  390. package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
  391. package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
  392. package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
  393. package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
  394. package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
  395. package/src/components/linelist/design/panels/column-category-selector.scss +324 -0
  396. package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
  397. package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
  398. package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
  399. package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
  400. package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
  401. package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
  402. package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
  403. package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
  404. package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
  405. package/src/components/linelist/design/panels/json-preview.scss +85 -0
  406. package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
  407. package/src/components/linelist/design/panels/query-config-panel.scss +341 -0
  408. package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
  409. package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
  410. package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
  411. package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
  412. package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
  413. package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
  414. package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
  415. package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
  416. package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
  417. package/src/components/linelist/linelist-dashboard.page.scss +240 -0
  418. package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
  419. package/src/components/linelist/linelist-editor.page.scss +28 -0
  420. package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
  421. package/src/components/linelist/linelist-run-report.page.scss +136 -0
  422. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
  423. package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
  424. package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
  425. package/src/components/report/dashboard/reports-table.component.tsx +186 -0
  426. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  427. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  428. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  429. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  430. package/src/components/report/definition/report-definition.types.ts +66 -0
  431. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  432. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  433. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  434. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  435. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  436. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  437. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  438. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  439. package/src/components/report/design/report-design-editor.component.tsx +1170 -0
  440. package/src/components/report/design/report-design.types.ts +48 -0
  441. package/src/components/report/design/report-design.utils.ts +86 -0
  442. package/src/components/report/panels/report-detail.component.tsx +71 -0
  443. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  444. package/src/components/report/report-dashboard.page.component.tsx +554 -0
  445. package/src/components/report/report-editor.page.component.tsx +590 -0
  446. package/src/components/report/report.routes.tsx +32 -0
  447. package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
  448. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  449. package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
  450. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  451. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  452. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
  453. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  454. package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
  455. package/src/components/report-sections/report-sections-page.component.tsx +325 -0
  456. package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
  457. package/src/components/report-sections/section-types.ts +55 -0
  458. package/src/components/report-sections/section-utils.ts +50 -0
  459. package/src/components/report-sections/sections-page.scss +79 -0
  460. package/src/components/run-reports/run-reports-page.component.tsx +8 -0
  461. package/src/components/security/access-denied-page.component.tsx +64 -0
  462. package/src/components/security/access-denied-page.scss +39 -0
  463. package/src/components/security/privilege-route.component.test.tsx +76 -0
  464. package/src/components/security/privilege-route.component.tsx +39 -0
  465. package/src/components/shared/compile-setup-modal.component.tsx +182 -0
  466. package/src/components/shared/concept-selector.component.tsx +245 -0
  467. package/src/components/shared/concept-selector.scss +121 -0
  468. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  469. package/src/components/shared/header/header.component.tsx +85 -0
  470. package/src/components/shared/header/header.scss +151 -0
  471. package/src/components/shared/header/illustration.component.tsx +34 -0
  472. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  473. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  474. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  475. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  476. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  477. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  478. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  479. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  480. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  481. package/src/config-schema.ts +43 -0
  482. package/src/constants/privileges.test.ts +54 -0
  483. package/src/constants/privileges.ts +118 -0
  484. package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
  485. package/src/features/dashboards/DashboardsPage.tsx +164 -0
  486. package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
  487. package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
  488. package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
  489. package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
  490. package/src/features/dashboards/components/DashboardSection.tsx +46 -0
  491. package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
  492. package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
  493. package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
  494. package/src/features/dashboards/dashboards.scss +196 -0
  495. package/src/features/dashboards/hooks/useDashboard.ts +61 -0
  496. package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
  497. package/src/features/dashboards/hooks/useDashboards.ts +56 -0
  498. package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
  499. package/src/features/dashboards/index.ts +11 -0
  500. package/src/features/dashboards/utils/dashboard-config.util.test.ts +286 -0
  501. package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
  502. package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
  503. package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
  504. package/src/features/report-visualizer/components/Export/index.ts +2 -0
  505. package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
  506. package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
  507. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
  508. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
  509. package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
  510. package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
  511. package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
  512. package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
  513. package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
  514. package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
  515. package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
  516. package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
  517. package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
  518. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
  519. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
  520. package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
  521. package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
  522. package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
  523. package/src/features/report-visualizer/components/TableView/index.ts +2 -0
  524. package/src/features/report-visualizer/hooks/index.ts +4 -0
  525. package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
  526. package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
  527. package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
  528. package/src/features/report-visualizer/hooks/useReports.ts +135 -0
  529. package/src/features/report-visualizer/index.ts +6 -0
  530. package/src/features/report-visualizer/report-visualizer.scss +593 -0
  531. package/src/features/report-visualizer/types/index.ts +1 -0
  532. package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
  533. package/src/features/report-visualizer/utils/status-colors.ts +117 -0
  534. package/src/globals.d.ts +9 -0
  535. package/src/hooks/address-template.ts +86 -0
  536. package/src/hooks/concept.ts +112 -0
  537. package/src/hooks/indicator/index.ts +1 -0
  538. package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
  539. package/src/hooks/openmrs-reference-data.ts +432 -0
  540. package/src/hooks/theme/index.ts +4 -0
  541. package/src/hooks/theme/useDataTheme.ts +61 -0
  542. package/src/hooks/theme/useDataThemes.ts +28 -0
  543. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  544. package/src/hooks/theme/useMambaTables.ts +95 -0
  545. package/src/hooks/use-report-builder-privileges.test.tsx +92 -0
  546. package/src/hooks/use-report-builder-privileges.ts +58 -0
  547. package/src/index.ts +29 -0
  548. package/src/resources/address-template/address-template.api.ts +180 -0
  549. package/src/resources/agegroup/agegroups.api.ts +58 -0
  550. package/src/resources/concept/concept.api.ts +116 -0
  551. package/src/resources/concepts/concept-mapper.ts +21 -0
  552. package/src/resources/concepts/concept-types.ts +31 -0
  553. package/src/resources/concepts/concepts.resource.ts +44 -0
  554. package/src/resources/concepts/useConceptSearch.ts +41 -0
  555. package/src/resources/dashboard/dashboard.api.ts +107 -0
  556. package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
  557. package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
  558. package/src/resources/etl-source/etl-source.api.ts +65 -0
  559. package/src/resources/etl-task/etl-task.api.ts +96 -0
  560. package/src/resources/etl-task/etl-task.types.ts +31 -0
  561. package/src/resources/indicator/indicators.api.ts +125 -0
  562. package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
  563. package/src/resources/linelist/linelist-reports.api.ts +616 -0
  564. package/src/resources/location/location.api.ts +135 -0
  565. package/src/resources/openmrs-api.ts +40 -0
  566. package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
  567. package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
  568. package/src/resources/preview/sql-preview.api.ts +54 -0
  569. package/src/resources/report/reports.api.ts +140 -0
  570. package/src/resources/report-category/report-category.api.ts +41 -0
  571. package/src/resources/report-import-export/import-export-api.ts +164 -0
  572. package/src/resources/report-library/report-library.api.ts +63 -0
  573. package/src/resources/report-section/report-sections.api.ts +73 -0
  574. package/src/resources/report-section/section-preview.api.ts +25 -0
  575. package/src/resources/theme/data-theme.api.ts +88 -0
  576. package/src/resources/theme/etl-schema.api.ts +101 -0
  577. package/src/resources/theme/etl-table-meta.api.ts +45 -0
  578. package/src/root.component.tsx +20 -0
  579. package/src/routes/report-builder.routes.tsx +160 -0
  580. package/src/routes/report-builder.scss +56 -0
  581. package/src/routes.json +31 -0
  582. package/src/sample-template.ts +121 -0
  583. package/src/template-utils.ts +110 -0
  584. package/src/types/carbon-react-inert.d.ts +13 -0
  585. package/src/types/condition-operators.ts +63 -0
  586. package/src/types/dashboard/dashboard.types.ts +137 -0
  587. package/src/types/dashboard/index.ts +4 -0
  588. package/src/types/etl/etl-types.ts +238 -0
  589. package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
  590. package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
  591. package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
  592. package/src/types/etl-monitor/index.ts +6 -0
  593. package/src/types/linelist/compile-config.ts +455 -0
  594. package/src/types/linelist-types.ts +2059 -0
  595. package/src/types/theme/data-theme.types.ts +88 -0
  596. package/src/utils/api-error.utils.test.ts +147 -0
  597. package/src/utils/api-error.utils.ts +131 -0
  598. package/src/utils/config-enhancer.ts +62 -0
  599. package/src/utils/etl-browser/etl-browser.util.test.ts +151 -0
  600. package/src/utils/etl-browser/export-csv.util.ts +43 -0
  601. package/src/utils/etl-browser/group-tables.util.ts +84 -0
  602. package/src/utils/etl-browser/index.ts +4 -0
  603. package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
  604. package/src/utils/etl-browser/table-sql.util.ts +20 -0
  605. package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
  606. package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
  607. package/src/utils/etl-monitor/index.ts +10 -0
  608. package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
  609. package/src/utils/etl-monitor/normalization-examples.ts +0 -0
  610. package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
  611. package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
  612. package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
  613. package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
  614. package/src/utils/etl-task/etl-task-card.util.test.ts +240 -0
  615. package/src/utils/etl-task/etl-task-card.util.ts +276 -0
  616. package/src/utils/etl-task/index.ts +1 -0
  617. package/src/utils/html-entities.utils.ts +34 -0
  618. package/src/utils/parameter-resolution.ts +218 -0
  619. package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
  620. package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
  621. package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
  622. package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
  623. package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
  624. package/tools/i18next-parser.config.js +0 -0
  625. package/tools/setup-tests.ts +1 -0
  626. package/tools/update-openmrs-deps.mjs +42 -0
  627. package/translations/am.json +65 -0
  628. package/translations/ar.json +65 -0
  629. package/translations/ar_SY.json +65 -0
  630. package/translations/bn.json +65 -0
  631. package/translations/cs.json +65 -0
  632. package/translations/de.json +65 -0
  633. package/translations/en.json +65 -0
  634. package/translations/en_US.json +65 -0
  635. package/translations/es.json +65 -0
  636. package/translations/es_MX.json +65 -0
  637. package/translations/fr.json +65 -0
  638. package/translations/he.json +65 -0
  639. package/translations/hi.json +65 -0
  640. package/translations/hi_IN.json +65 -0
  641. package/translations/id.json +65 -0
  642. package/translations/it.json +65 -0
  643. package/translations/ka.json +65 -0
  644. package/translations/km.json +65 -0
  645. package/translations/ku.json +65 -0
  646. package/translations/ky.json +65 -0
  647. package/translations/lg.json +65 -0
  648. package/translations/ne.json +65 -0
  649. package/translations/pl.json +65 -0
  650. package/translations/pt.json +65 -0
  651. package/translations/pt_BR.json +65 -0
  652. package/translations/qu.json +65 -0
  653. package/translations/ro_RO.json +65 -0
  654. package/translations/ru_RU.json +65 -0
  655. package/translations/si.json +65 -0
  656. package/translations/sq.json +65 -0
  657. package/translations/sw.json +65 -0
  658. package/translations/sw_KE.json +65 -0
  659. package/translations/tr.json +65 -0
  660. package/translations/tr_TR.json +65 -0
  661. package/translations/uk.json +65 -0
  662. package/translations/uz.json +65 -0
  663. package/translations/uz@Latn.json +65 -0
  664. package/translations/uz_UZ.json +65 -0
  665. package/translations/vi.json +65 -0
  666. package/translations/zh.json +65 -0
  667. package/translations/zh_CN.json +65 -0
  668. package/translations/zh_TW.json +65 -0
  669. package/tsconfig.json +24 -0
  670. package/vitest.config.mts +42 -0
@@ -0,0 +1,2059 @@
1
+ /**
2
+ * Linelist Report Type Definitions
3
+ *
4
+ * These types define the structure for linelist (patient list) report definitions.
5
+ * Linelists are standalone reports that return individual patient records,
6
+ * as opposed to aggregate reports which return counts with disaggregation.
7
+ *
8
+ * Based on backend linelist report definition structure from:
9
+ * /openmrs-module-report-builder/omod/src/main/resources/reports/etl/linelist/
10
+ */
11
+
12
+ /**
13
+ * Population source join types
14
+ * How multiple population sources combine
15
+ */
16
+ export type PopulationJoinType =
17
+ | 'JOIN' // INNER JOIN on patient_id
18
+ | 'LEFT_JOIN' // LEFT JOIN on patient_id
19
+ | 'INTERSECT' // Patients in ALL sources (AND logic)
20
+ | 'UNION' // Patients from ANY source (OR logic)
21
+ | 'EXCEPT'; // Patients in source A but NOT in source B
22
+
23
+ /**
24
+ * Population source definition
25
+ * Represents a datasource used for patient population selection
26
+ */
27
+ export type PopulationSource = {
28
+ uuid: string; // Table name (e.g., "mamba_fact_patients_latest")
29
+ name: string; // Display name
30
+ type: 'ETL' | 'CORE' | 'REFERENCE' | 'SQL' | 'INDICATOR';
31
+ joinType: PopulationJoinType; // How this source combines with others
32
+ joinCondition?: string; // Custom ON clause for JOINs
33
+ enabled: boolean; // Whether this source is active
34
+ order: number; // Display order for UI
35
+ };
36
+
37
+ /**
38
+ * Data source configuration in report definition
39
+ * Defines which datasources are used and their roles
40
+ */
41
+ export type DataSourceConfig = {
42
+ uuid: string; // Data source/table UUID or name
43
+ name: string; // Display name
44
+ type: 'ETL' | 'SQL' | 'REFERENCE' | 'INDICATOR' | 'CORE'; // Source type
45
+ role: 'PRIMARY' | 'SECONDARY' | 'REFERENCE'; // Role in the report
46
+
47
+ /**
48
+ * Columns selected from this datasource
49
+ * Maps column names to their definitions
50
+ */
51
+ columns?: Record<string, {
52
+ name: string;
53
+ type: string;
54
+ sourceTable: string;
55
+ }>;
56
+ };
57
+
58
+ /**
59
+ * Linelist report definition configuration
60
+ * This is stored in the configJson field of a report definition
61
+ *
62
+ * Version 2: Supports multiple data sources
63
+ * Version 3: Supports multiple population sources with join types
64
+ */
65
+ export type LinelistReportDefinitionConfig = {
66
+ version: 1 | 2 | 3; // Version for migration support
67
+ type: 'LINE_LIST';
68
+
69
+ /**
70
+ * Report metadata
71
+ */
72
+ categoryUuid?: string;
73
+ themeUuid?: string;
74
+
75
+ /**
76
+ * Version 1: Single data source (legacy)
77
+ * Version 2: Multiple data sources
78
+ */
79
+ dataSourceUuid?: string; // V1 only - deprecated, use dataSources
80
+ dataSources?: DataSourceConfig[]; // V2 - array of data sources
81
+
82
+ rowGrain?: LinelistRowGrain;
83
+ templateId?: string;
84
+
85
+ /**
86
+ * Parameters supported by this linelist report
87
+ * Typically startDate and endDate for time-based filtering
88
+ */
89
+ parameters?: Array<LinelistParameter>;
90
+
91
+ /**
92
+ * Base cohort definition - SQL query that selects patients
93
+ * This query should return distinct patient IDs
94
+ */
95
+ baseCohortDefinition: LinelistCohortDefinition;
96
+
97
+ /**
98
+ * Dataset definitions define the output columns and their sources
99
+ * Each dataset becomes a tab/key in the output JSON
100
+ */
101
+ dataSetDefinitions: LinelistDataSetDefinition[];
102
+
103
+ /**
104
+ * Optional sorting configuration
105
+ */
106
+ orderBy?: string;
107
+ orderDirection?: 'ASC' | 'DESC';
108
+
109
+ /**
110
+ * Builder metadata - persisted so the editor can reconstruct the draft
111
+ * when re-opening for edit. Ignored by the backend at run time.
112
+ */
113
+ buildMethod?: 'SQL_BUILDER' | 'VISUAL_FILTER' | 'INDICATOR_BASED';
114
+ indicatorRules?: Array<{
115
+ id: string;
116
+ indicatorUuid: string;
117
+ name: string;
118
+ conditions?: Array<{ key: string; operator?: string; value: any }>;
119
+ logicalOperator?: 'AND' | 'OR';
120
+ negate?: boolean;
121
+ }>;
122
+
123
+ /**
124
+ * Optional row limit for safety
125
+ */
126
+ limit?: number;
127
+ };
128
+
129
+ /**
130
+ * Parameter definition for linelist reports
131
+ */
132
+ export type LinelistParameter = {
133
+ name: string;
134
+ label: string;
135
+ type: LinelistParameterType;
136
+ required?: boolean;
137
+ defaultValue?: string;
138
+ displayOrder?: number;
139
+ /**
140
+ * Type-specific configuration
141
+ */
142
+ config?: LinelistParameterConfig;
143
+ };
144
+
145
+ /**
146
+ * Available parameter types
147
+ */
148
+ export type LinelistParameterType =
149
+ | 'DATE'
150
+ | 'DATETIME'
151
+ | 'LOCATION'
152
+ | 'PROGRAM'
153
+ | 'PROVIDER'
154
+ | 'CONCEPT'
155
+ | 'CODED_VALUE'
156
+ | 'IDENTIFIER_TYPE' // Patient identifier types
157
+ | 'PERSON_ATTRIBUTE' // Person attribute types
158
+ | 'BOOLEAN'
159
+ | 'NUMBER'
160
+ | 'TEXT'
161
+ | 'LIST'; // Configurable list with predefined options
162
+
163
+ /**
164
+ * Type-specific configuration for parameters
165
+ */
166
+ export type LinelistParameterConfig =
167
+ // DATE/DATETIME configuration
168
+ | {
169
+ type: 'DATE' | 'DATETIME';
170
+ includeTime?: boolean;
171
+ minDate?: string; // ISO date string
172
+ maxDate?: string; // ISO date string
173
+ }
174
+ // LOCATION configuration
175
+ | {
176
+ type: 'LOCATION';
177
+ locationUuids?: string[]; // Restrict to specific locations
178
+ userDefaultToSessionLocation?: boolean;
179
+ tagUuid?: string; // Filter by location tag
180
+ }
181
+ // CONCEPT configuration
182
+ | {
183
+ type: 'CONCEPT';
184
+ conceptClassUuids?: string[]; // Restrict to concept classes
185
+ sourceUuid?: string; // Concept source UUID
186
+ allowMultiSelect?: boolean;
187
+ }
188
+ // CODED_VALUE configuration
189
+ | {
190
+ type: 'CODED_VALUE';
191
+ sourceUuid?: string; // Data source for coded values
192
+ options?: Array<{ uuid: string; label: string; }>;
193
+ allowMultiSelect?: boolean;
194
+ }
195
+ // IDENTIFIER_TYPE configuration
196
+ | {
197
+ type: 'IDENTIFIER_TYPE';
198
+ identifierTypeUuids?: string[]; // Restrict to specific types
199
+ }
200
+ // PERSON_ATTRIBUTE configuration
201
+ | {
202
+ type: 'PERSON_ATTRIBUTE';
203
+ attributeTypeUuids?: string[]; // Restrict to specific types
204
+ format?: string; // Filter by format (e.g., 'org.openmrs.Concept')
205
+ }
206
+ // LIST configuration (predefined options)
207
+ | {
208
+ type: 'LIST';
209
+ options: Array<{
210
+ value: string;
211
+ label: string;
212
+ }>;
213
+ allowMultiSelect?: boolean;
214
+ }
215
+ // TEXT configuration
216
+ | {
217
+ type: 'TEXT';
218
+ minLength?: number;
219
+ maxLength?: number;
220
+ pattern?: string; // Regex pattern for validation
221
+ }
222
+ // NUMBER configuration
223
+ | {
224
+ type: 'NUMBER';
225
+ min?: number;
226
+ max?: number;
227
+ step?: number;
228
+ integerOnly?: boolean;
229
+ }
230
+ // BOOLEAN configuration
231
+ | {
232
+ type: 'BOOLEAN';
233
+ trueLabel?: string; // Custom label for true value
234
+ falseLabel?: string; // Custom label for false value
235
+ }
236
+ // PROGRAM configuration
237
+ | {
238
+ type: 'PROGRAM';
239
+ programUuids?: string[]; // Restrict to specific programs
240
+ }
241
+ // PROVIDER configuration
242
+ | {
243
+ type: 'PROVIDER';
244
+ providerRole?: string; // Filter by provider role
245
+ };
246
+
247
+ /**
248
+ * Parameter draft for builder UI
249
+ */
250
+ export type LinelistParameterDraft = {
251
+ id: string; // Unique ID for React keys
252
+ name: string;
253
+ label: string;
254
+ type: LinelistParameterType;
255
+ required: boolean;
256
+ defaultValue: string;
257
+ displayOrder: number;
258
+ config?: LinelistParameterConfig;
259
+ };
260
+
261
+ /**
262
+ * Sort configuration for linelist reports
263
+ * Defines ordered multi-column sorting
264
+ */
265
+ export type LinelistSortConfig = {
266
+ id: string; // Unique ID for React keys
267
+ columnId: string; // ID of the column to sort by
268
+ columnName: string; // Display name of the column
269
+ direction: 'ASC' | 'DESC'; // Sort direction
270
+ nulls: 'FIRST' | 'LAST'; // Where to place null values
271
+ sortOrder: number; // Priority in sort order (0 = highest priority)
272
+ };
273
+
274
+ /**
275
+ * Visual filter condition types
276
+ * Used for building SQL queries visually
277
+ */
278
+
279
+ /**
280
+ * Field type for filter conditions
281
+ */
282
+ export type FilterFieldType = 'TEXT' | 'NUMBER' | 'DATE' | 'CODED' | 'BOOLEAN' | 'LOCATION';
283
+
284
+ /**
285
+ * Available operators by field type
286
+ */
287
+ export type FilterOperator =
288
+ // Text operators
289
+ | 'EQUALS' | 'NOT_EQUALS' | 'CONTAINS' | 'STARTS_WITH' | 'ENDS_WITH' | 'IS_BLANK' | 'IS_NOT_BLANK'
290
+ // Number operators
291
+ | 'GREATER_THAN' | 'LESS_THAN' | 'GREATER_EQUAL' | 'LESS_EQUAL' | 'BETWEEN'
292
+ // Date operators
293
+ | 'ON' | 'BEFORE' | 'AFTER' | 'BETWEEN_DATES' | 'IN_PREVIOUS_PERIOD' | 'IN_NEXT_PERIOD'
294
+ // Coded operators
295
+ | 'IS_ONE_OF' | 'IS_NOT_ONE_OF'
296
+ // Boolean operators
297
+ | 'IS_TRUE' | 'IS_FALSE' | 'NOT_RECORDED'
298
+ // Location operators
299
+ | 'WITHIN_HIERARCHY' | 'IN_LOCATION';
300
+
301
+ /**
302
+ * Logical operator for combining conditions
303
+ */
304
+ export type LogicalOperator = 'AND' | 'OR';
305
+
306
+ /**
307
+ * Filter condition definition
308
+ */
309
+ export type FilterCondition = {
310
+ id: string;
311
+ field: string; // Field name (e.g., "appointment_date", "gender")
312
+ fieldLabel: string; // Display name for the field
313
+ fieldType: FilterFieldType;
314
+ operator: FilterOperator;
315
+ value?: string | string[]; // Single value or array of values
316
+ value2?: string; // Second value for "between" operators
317
+ negate?: boolean; // Whether to negate the condition
318
+ };
319
+
320
+ /**
321
+ * Filter group - contains conditions and nested groups
322
+ */
323
+ export type FilterGroup = {
324
+ id: string;
325
+ logicalOperator: LogicalOperator; // How to combine items in this group
326
+ conditions: FilterCondition[];
327
+ nestedGroups?: FilterGroup[]; // Nested groups for complex logic
328
+ };
329
+
330
+ /**
331
+ * Visual filter builder state
332
+ */
333
+ export type VisualFilterState = {
334
+ rootGroup: FilterGroup;
335
+ useVisualBuilder: boolean; // Toggle between visual and SQL mode
336
+ };
337
+
338
+ /**
339
+ * Cohort definition for patient selection
340
+ */
341
+ /**
342
+ * FilterMap configuration for base cohort
343
+ * Maps parameter names to column references for use in column SQL queries
344
+ * Enables column queries to filter to the exact row that matched the base cohort criteria
345
+ */
346
+ export type FilterMap = {
347
+ /**
348
+ * Map of parameter names to column references
349
+ * Example: { "patientId": "vl.patient_id", "orderId": "vl.native_order_id", "episodeId": "vl.episode_id" }
350
+ * Column SQL can then use :patientId, :orderId, :episodeId as parameters
351
+ */
352
+ [parameterName: string]: string;
353
+ };
354
+
355
+ /**
356
+ * Cohort definition for patient selection
357
+ */
358
+ export type LinelistCohortDefinition = {
359
+ type: 'SQL';
360
+ name: string;
361
+ config: {
362
+ /**
363
+ * SQL query that returns patient IDs
364
+ * Should use :startDate and :endDate parameters for time-based filtering
365
+ * Example: SELECT DISTINCT patient_id FROM mamba_fact_patients_latest WHERE ...
366
+ */
367
+ sql: string;
368
+ /**
369
+ * Optional filterMap for column parameter binding
370
+ * When column queries reference the same table as the base cohort,
371
+ * filterMap enables filtering to the exact row that matched cohort criteria
372
+ */
373
+ filterMap?: FilterMap;
374
+ };
375
+ };
376
+
377
+ /**
378
+ * Dataset definition for output columns
379
+ * Each dataset becomes a separate section in the report output
380
+ */
381
+ export type LinelistDataSetDefinition = {
382
+ name: string;
383
+ type: 'PATIENT_DATA_SET';
384
+
385
+ /**
386
+ * Row filter defines which patients are included
387
+ * Often the same as the base cohort SQL
388
+ */
389
+ rowFilter?: {
390
+ type: 'SQL';
391
+ name: string;
392
+ config: {
393
+ sql: string;
394
+ filterMap?: FilterMap;
395
+ };
396
+ };
397
+
398
+ /**
399
+ * Column definitions for the output
400
+ */
401
+ columns: LinelistColumnDefinition[];
402
+ };
403
+
404
+ /**
405
+ * Column definition for linelist output
406
+ * Each column defines one field in the patient list
407
+ */
408
+ export type LinelistColumnDefinition = {
409
+ /**
410
+ * Display name for the column (used as header in output)
411
+ */
412
+ name: string;
413
+
414
+ /**
415
+ * Data definition specifies how to get the column value
416
+ */
417
+ dataDefinition: LinelistDataDefinition;
418
+
419
+ /**
420
+ * Optional repeat resolution for fields that may return multiple values
421
+ */
422
+ repeatResolution?: LinelistRepeatResolution;
423
+ };
424
+
425
+ /**
426
+ * Data definition types for linelist columns
427
+ * Based on backend-supported column types
428
+ */
429
+ export type LinelistDataDefinition =
430
+ | LinelistIdentifierDefinition
431
+ | LinelistPersonNameDefinition
432
+ | LinelistPersonAttributeDefinition
433
+ | LinelistCalculationDefinition
434
+ | LinelistSqlDefinition
435
+ | LinelistPersonAddressDefinition
436
+ | LinelistConverterDefinition
437
+ | LinelistObservationDefinition
438
+ | LinelistEncounterDiagnosisDefinition;
439
+
440
+ /**
441
+ * Base data definition structure
442
+ */
443
+ type BaseDataDefinition<T extends string = string> = {
444
+ type: T;
445
+ };
446
+
447
+ /**
448
+ * Identifier column (e.g., Clinic Number, EID Number)
449
+ * Uses OpenMRS patient identifier types
450
+ */
451
+ export type LinelistIdentifierDefinition = BaseDataDefinition<'IDENTIFIER'> & {
452
+ config: {
453
+ identifierTypeUuid: string;
454
+ preferred?: boolean;
455
+ };
456
+ };
457
+
458
+ /**
459
+ * Person name column
460
+ * Formats person name according to specified type
461
+ */
462
+ export type LinelistPersonNameDefinition = BaseDataDefinition<'PERSON_NAME'> & {
463
+ config: {
464
+ type?: 'FULL_NAME' | 'GIVEN_NAME' | 'MIDDLE_NAME' | 'FAMILY_NAME';
465
+ preferred?: boolean;
466
+ };
467
+ };
468
+
469
+ /**
470
+ * Person attribute column
471
+ * Uses OpenMRS person attribute types
472
+ */
473
+ export type LinelistPersonAttributeDefinition = BaseDataDefinition<'PERSON_ATTRIBUTE'> & {
474
+ config: {
475
+ attributeTypeUuid: string;
476
+ };
477
+ };
478
+
479
+ /**
480
+ * Calculation column
481
+ * Performs calculations on patient data (e.g., age)
482
+ */
483
+ export type LinelistCalculationDefinition = BaseDataDefinition<'CALCULATION'> & {
484
+ config: {
485
+ calculation: 'AGE' | 'AGE_IN_MONTHS' | 'AGE_IN_YEARS' | 'BMI' | string;
486
+ onDate?: boolean; // Whether to calculate as of the report end date
487
+ };
488
+ };
489
+
490
+ /**
491
+ * SQL column
492
+ * Custom SQL expression for complex column values
493
+ * The :patientId placeholder will be replaced with the patient ID
494
+ */
495
+ export type LinelistSqlDefinition = BaseDataDefinition<'SQL'> & {
496
+ config: {
497
+ sql: string;
498
+ };
499
+ };
500
+
501
+ /**
502
+ * Person address column
503
+ * Retrieves address information
504
+ *
505
+ * UgandaEMR Address Template Mappings:
506
+ * - ADDRESS1: Address Line 1
507
+ * - ADDRESS2: Address Line 2
508
+ * - ADDRESS3: Subcounty
509
+ * - ADDRESS4: Parish
510
+ * - ADDRESS5: Village
511
+ * - STATE_PROVINCE: County
512
+ * - COUNTY_DISTRICT: District
513
+ * - COUNTRY: Country
514
+ */
515
+ export type LinelistPersonAddressDefinition = BaseDataDefinition<'PERSON_ADDRESS'> & {
516
+ config: {
517
+ type?: 'PERSON_ADDRESS' | string;
518
+ field?: 'ADDRESS1' | 'ADDRESS2' | 'ADDRESS3' | 'ADDRESS4' | 'ADDRESS5'
519
+ | 'CITY_VILLAGE' | 'STATE_PROVINCE' | 'COUNTY_DISTRICT' | 'COUNTRY'
520
+ | 'LATITUDE' | 'LONGITUDE' | 'POSTAL_CODE' | string;
521
+ };
522
+ };
523
+
524
+ /**
525
+ * Converter column
526
+ * Converts/transforms values from another source
527
+ */
528
+ export type LinelistConverterDefinition = BaseDataDefinition<'CONVERTER'> & {
529
+ config: {
530
+ type: string;
531
+ format?: string;
532
+ };
533
+ };
534
+
535
+ /**
536
+ * Observation column
537
+ * Uses OpenMRS observations based on concepts
538
+ * Returns the most recent observation value for each patient
539
+ */
540
+ export type LinelistObservationDefinition = BaseDataDefinition<'OBSERVATION'> & {
541
+ config: {
542
+ conceptUuid: string;
543
+ conceptName?: string;
544
+ /**
545
+ * Strategy for handling multiple observations
546
+ * LATEST: Most recent observation by date
547
+ * EARLIEST: Oldest observation by date
548
+ * CLOSEST_TO_START: Value closest to report start date
549
+ * CLOSEST_TO_END: Value closest to report end date
550
+ * FIRST_WITHIN_PERIOD: First observation within reporting period
551
+ * LAST_WITHIN_PERIOD: Last observation within reporting period
552
+ */
553
+ strategy?: 'LATEST' | 'EARLIEST' | 'CLOSEST_TO_START' | 'CLOSEST_TO_END' | 'FIRST_WITHIN_PERIOD' | 'LAST_WITHIN_PERIOD';
554
+ /**
555
+ * Whether to return the concept's display name instead of UUID
556
+ */
557
+ returnDisplay?: boolean;
558
+ /**
559
+ * For coded concepts, optionally return a specific answer
560
+ */
561
+ answerConceptUuid?: string;
562
+ };
563
+ };
564
+
565
+ /**
566
+ * Encounter Diagnosis column
567
+ * Uses OpenMRS encounter diagnoses based on concepts
568
+ * Returns diagnoses for each patient within the reporting period
569
+ */
570
+ export type LinelistEncounterDiagnosisDefinition = BaseDataDefinition<'ENCOUNTER_DIAGNOSIS'> & {
571
+ config: {
572
+ /**
573
+ * Optional concept UUID to filter diagnoses by specific condition
574
+ * If not provided, returns all diagnoses
575
+ */
576
+ conceptUuid?: string;
577
+ conceptName?: string;
578
+ /**
579
+ * Rank of diagnosis (PRIMARY/SECONDARY)
580
+ */
581
+ rank?: 'PRIMARY' | 'SECONDARY' | 'ANY';
582
+ /**
583
+ * Whether to return only confirmed diagnoses
584
+ */
585
+ confirmedOnly?: boolean;
586
+ /**
587
+ * Strategy for handling multiple diagnoses
588
+ * ALL_VALUES: Return all diagnoses as comma-separated list
589
+ * LATEST: Most recent diagnosis
590
+ * FIRST_WITHIN_PERIOD: First diagnosis within reporting period
591
+ * LAST_WITHIN_PERIOD: Last diagnosis within reporting period
592
+ */
593
+ strategy?: 'ALL_VALUES' | 'LATEST' | 'FIRST_WITHIN_PERIOD' | 'LAST_WITHIN_PERIOD';
594
+ /**
595
+ * Whether to return the concept's display name instead of UUID
596
+ */
597
+ returnDisplay?: boolean;
598
+ };
599
+ };
600
+
601
+ /**
602
+ * Population configuration mode
603
+ * Indicates how the cohort/patient selection is defined
604
+ */
605
+ export type PopulationMode = 'SQL' | 'INDICATOR' | 'HYBRID';
606
+
607
+ /**
608
+ * Draft state for linelist report builder UI
609
+ * Version 2.0 - Supports multiple data sources and full builder tracking
610
+ * Version 3.0 - Supports multiple population sources with join types
611
+ */
612
+ export type LinelistReportDraft = {
613
+ // === VERSION ===
614
+ version: 2 | 3; // Contract version for migration support
615
+
616
+ // === BASIC INFO ===
617
+ name: string;
618
+ description?: string;
619
+ code?: string;
620
+
621
+ // Current panel in the builder UI
622
+ currentPanel: LinelistBuilderPanel;
623
+
624
+ // Report categorization
625
+ categoryUuid?: string; // Optional - report category
626
+ rowGrain?: LinelistRowGrain; // Required - what one row represents
627
+ templateId?: string; // Optional - starting template
628
+
629
+ // === POPULATION SOURCES (v3 - Multiple sources with join types) ===
630
+ populationSources: PopulationSource[];
631
+
632
+ // === DATA SOURCES (v2 - Multiple sources supported for columns) ===
633
+ dataSources: DataSourceInfo[];
634
+
635
+ // === LEGACY PROPERTIES (for backward compatibility during migration) ===
636
+ /** @deprecated Use dataSources array instead */
637
+ dataSourceUuid?: string;
638
+ /** @deprecated Use population.sqlTemplate instead */
639
+ cohortSql?: string;
640
+ /** @deprecated Use population.indicatorRules instead */
641
+ indicatorRules?: any[];
642
+ /** @deprecated Use population.visualFilter instead */
643
+ visualFilter?: any;
644
+ /** @deprecated Not used in V2 */
645
+ populationMode?: string;
646
+ /** @deprecated Not used in V2 */
647
+ dataSetName?: string;
648
+ /** UI state flag for unsaved changes */
649
+ unsavedChanges?: boolean;
650
+ /** Convenience property - aliases to metadata.status */
651
+ status?: 'DRAFT' | 'PUBLISHED' | 'RETIRED';
652
+ /** Embedded validation errors (for UI display convenience) */
653
+ errors?: any;
654
+
655
+ // === POPULATION (COHORT) DEFINITION ===
656
+ population: PopulationDefinition;
657
+
658
+ // === COLUMNS ===
659
+ columns: LinelistColumnDraft[];
660
+
661
+ // === PARAMETERS ===
662
+ parameters: LinelistParameterDraft[];
663
+
664
+ // === SORTING ===
665
+ sortConfig: LinelistSortConfig[];
666
+
667
+ // === ROW LIMIT ===
668
+ limit?: number;
669
+
670
+ // === DISPLAY SETTINGS ===
671
+ displaySettings: DisplaySettings;
672
+
673
+ // === VALIDATION STATE ===
674
+ validation: ValidationState;
675
+
676
+ // === METADATA ===
677
+ metadata: BuilderMetadata;
678
+ };
679
+
680
+ /**
681
+ * Data source information with role and configuration
682
+ */
683
+ export type DataSourceInfo = {
684
+ uuid: string; // Data source UUID
685
+ name: string; // Display name
686
+ type: 'ETL' | 'SQL' | 'REFERENCE' | 'INDICATOR' | 'CORE'; // Source type
687
+ role: 'PRIMARY' | 'SECONDARY' | 'REFERENCE'; // Role in the report
688
+ tables?: string[]; // Tables used from this source
689
+ joinConfig?: DataSourceJoinConfig; // How this source joins to primary
690
+ };
691
+
692
+ /**
693
+ * Data source join configuration
694
+ */
695
+ export type DataSourceJoinConfig = {
696
+ joinType: 'LEFT_JOIN' | 'INNER_JOIN' | 'FULL_JOIN';
697
+ joinCondition: string; // Field name to join on
698
+ joinSql?: string; // Custom join SQL if needed
699
+ };
700
+
701
+ /**
702
+ * Population (cohort) definition
703
+ * Captures both the result and the build process
704
+ */
705
+ export type PopulationDefinition = {
706
+ // Which data source is the base
707
+ baseDataSourceUuid: string;
708
+
709
+ // How the population was built
710
+ buildMethod: 'SQL_BUILDER' | 'VISUAL_FILTER' | 'INDICATOR_BASED';
711
+
712
+ // SQL Builder state
713
+ sqlTemplate: string;
714
+ parameterReferences: string[];
715
+
716
+ // Visual Filter (if used)
717
+ visualFilter?: VisualFilterState;
718
+
719
+ // Indicator-based (if used)
720
+ indicatorRules?: Array<{
721
+ id: string;
722
+ indicatorUuid: string;
723
+ name: string;
724
+ conditions?: Array<{
725
+ key: string;
726
+ operator?: string;
727
+ value: any;
728
+ }>;
729
+ logicalOperator?: 'AND' | 'OR';
730
+ negate?: boolean;
731
+ }>;
732
+
733
+ // Build history for reproducibility
734
+ buildHistory?: PopulationBuildStep[];
735
+
736
+ // FilterMap for column parameter binding
737
+ filterMap?: FilterMap;
738
+ };
739
+
740
+ /**
741
+ * Population build step for tracking construction history
742
+ */
743
+ export type PopulationBuildStep = {
744
+ timestamp: string; // ISO timestamp
745
+ action: string; // What was done
746
+ description: string; // Human-readable description
747
+ configSnapshot?: any; // Optional state at this point
748
+ };
749
+
750
+ /**
751
+ * Display settings configuration
752
+ */
753
+ export type DisplaySettings = {
754
+ defaultPageSize: number;
755
+ freezeFirstColumn: boolean;
756
+ freezeHeader: boolean;
757
+ dateDisplayFormat: string;
758
+ nullDisplayValue: string;
759
+ maxInteractiveRows: number;
760
+ maxExportRows: number;
761
+ allowedExports: Array<'CSV' | 'XLSX' | 'PDF' | 'HTML'>;
762
+ includeParametersInExportHeader: boolean;
763
+ includeGeneratedTimestamp: boolean;
764
+ };
765
+
766
+ /**
767
+ * Validation state
768
+ */
769
+ export type ValidationState = {
770
+ errors: LinelistValidationErrors;
771
+ warnings: LinelistValidationWarnings;
772
+ lastValidated: string; // ISO timestamp
773
+ };
774
+
775
+ /**
776
+ * Builder metadata
777
+ */
778
+ export type BuilderMetadata = {
779
+ createdAt: string; // ISO timestamp when draft was created
780
+ lastModified: string; // ISO timestamp of last change
781
+ lastModifiedBy?: string; // User who last modified
782
+ buildMethod: 'NEW' | 'TEMPLATE' | 'DUPLICATE' | 'IMPORT' | 'EDIT';
783
+ sourceReportUuid?: string; // If duplicated/imported, the source
784
+ version: number; // For migration support
785
+ status: 'DRAFT' | 'PUBLISHED' | 'RETIRED';
786
+ };
787
+
788
+ /**
789
+ * Column draft for builder UI
790
+ * Version 2.0 - With full source tracking
791
+ */
792
+ export type LinelistColumnDraft = {
793
+ id: string; // Unique ID for React keys
794
+ name: string; // Display name
795
+ description?: string; // Optional description
796
+
797
+ // === SOURCE INFORMATION ===
798
+ source: ColumnSource;
799
+
800
+ // === DATA DEFINITION ===
801
+ dataDefinitionType: keyof LinelistDataDefinitionMap;
802
+ dataDefinitionConfig: Record<string, any>;
803
+
804
+ /** @deprecated Use dataDefinitionConfig instead */
805
+ config?: Record<string, any>;
806
+
807
+ // === ADDITION INFO ===
808
+ additionInfo: ColumnAdditionInfo;
809
+
810
+ // === DISPLAY ===
811
+ display: ColumnDisplaySettings;
812
+
813
+ // === SORT ORDER ===
814
+ sortOrder: number;
815
+
816
+ // === REPEAT RESOLUTION ===
817
+ repeatResolution?: LinelistRepeatResolution;
818
+
819
+ // === TRANSFORMATIONS ===
820
+ transformations?: LinelistColumnTransformation[];
821
+ };
822
+
823
+ /**
824
+ * Column source information
825
+ */
826
+ export type ColumnSource = {
827
+ dataSourceUuid: string; // Which data source
828
+ dataSourceName: string; // Display name
829
+ table: string; // Source table
830
+ field: string; // Source field
831
+ fieldType: string; // Original field type
832
+ attributeTypeUuid?: string; // For person attributes
833
+ identifierTypeUuid?: string; // For identifiers
834
+ conceptUuid?: string; // For observations and encounter diagnoses
835
+ };
836
+
837
+ /**
838
+ * Column addition information
839
+ */
840
+ export type ColumnAdditionInfo = {
841
+ addedVia: 'DRAG_DROP' | 'SQL_BUILDER' | 'VISUAL_SELECTOR' | 'IMPORT' | 'CALCULATED';
842
+ addedAt: string; // ISO timestamp
843
+ addedBy?: string; // User who added
844
+ orderAdded: number; // Sequence in which this was added
845
+ };
846
+
847
+ /**
848
+ * Column display settings
849
+ */
850
+ export type ColumnDisplaySettings = {
851
+ width: number;
852
+ align: 'left' | 'center' | 'right';
853
+ sortable: boolean;
854
+ filterable: boolean;
855
+ format?: 'text' | 'date' | 'number' | 'coded' | 'boolean';
856
+ formatConfig?: Record<string, any>;
857
+ };
858
+
859
+ /**
860
+ * Enhanced column draft with full builder details
861
+ * Extends the base column draft with construction metadata
862
+ */
863
+ export type LinelistColumnDraftExtended = LinelistColumnDraft & {
864
+ /**
865
+ * Source tracking - where this column came from
866
+ */
867
+ sourceInfo?: {
868
+ dataSourceUuid: string; // Which data source
869
+ dataSourceName: string; // Display name of data source
870
+ table: string; // Source table (e.g., "mamba_fact_patients_latest")
871
+ field: string; // Source field name in the table
872
+ fieldPath?: string; // Full path if nested (e.g., "address.city")
873
+ fieldType: string; // Original field type (TEXT, NUMBER, DATE, etc.)
874
+ };
875
+
876
+ /**
877
+ * How this column was added to the report
878
+ */
879
+ additionInfo?: {
880
+ addedVia: 'DRAG_DROP' | 'SQL_BUILDER' | 'VISUAL_SELECTOR' | 'IMPORT';
881
+ addedAt: string; // ISO timestamp
882
+ addedBy?: string; // User who added
883
+ orderAdded: number; // Sequence in which this was added
884
+ };
885
+
886
+ /**
887
+ * Transformation pipeline - all transformations applied
888
+ * Preserves the order and configuration of each transformation
889
+ */
890
+ transformationPipeline?: Array<{
891
+ type: string; // Transformation type
892
+ config: Record<string, any>; // Transformation configuration
893
+ enabled: boolean; // Whether this transformation is active
894
+ order: number; // Order in pipeline
895
+ }>;
896
+
897
+ /**
898
+ * Preview/sample data for this column (for UX)
899
+ */
900
+ previewData?: {
901
+ sampleValues: string[]; // Example values from actual data
902
+ nullCount?: number; // How many nulls in sample
903
+ uniqueCount?: number; // How many unique values
904
+ };
905
+ };
906
+
907
+ /**
908
+ * Row grain options - what one row represents in the linelist
909
+ */
910
+ export type LinelistRowGrain =
911
+ | 'PATIENT'
912
+ | 'ENCOUNTER'
913
+ | 'OBSERVATION'
914
+ | 'PROGRAM_ENROLLMENT'
915
+ | 'APPOINTMENT'
916
+ | 'ORDER';
917
+
918
+ /**
919
+ * Repeat resolution strategy for fields that can return multiple values
920
+ */
921
+ export type LinelistRepeatResolution = {
922
+ strategy: RepeatResolutionStrategy;
923
+ orderBy?: string; // Field to use for ordering (e.g., return_visit_date)
924
+ restrictToPeriod?: boolean; // Only use values within reporting period
925
+ ignoreVoided?: boolean; // Ignore voided records
926
+ tieBreakField?: string; // Field to use as tie-breaker
927
+ };
928
+
929
+ /**
930
+ * Available repeat resolution strategies
931
+ */
932
+ export type RepeatResolutionStrategy =
933
+ | 'LATEST' // Most recent value based on order by field
934
+ | 'EARLIEST' // Oldest value based on order by field
935
+ | 'HIGHEST' // Maximum value
936
+ | 'LOWEST' // Minimum value
937
+ | 'CLOSEST_TO_START' // Value closest to report start date
938
+ | 'CLOSEST_TO_END' // Value closest to report end date
939
+ | 'FIRST_WITHIN_PERIOD' // First value within reporting period
940
+ | 'LAST_WITHIN_PERIOD' // Last value within reporting period
941
+ | 'CONCATENATE' // Concatenate all values with separator
942
+ | 'ALL_VALUES' // Return all values and change row grain (one row per value)
943
+ | 'NONE'; // Explicitly choose not to resolve (advanced users only)
944
+
945
+ /**
946
+ * Column transformation types
947
+ * Transformations are applied in sequence to column values
948
+ */
949
+ export type LinelistColumnTransformation =
950
+ | LinelistDateFormatTransformation
951
+ | LinelistAgeCalculationTransformation
952
+ | LinelistCodedToDisplayTransformation
953
+ | LinelistBooleanMappingTransformation
954
+ | LinelistReplaceNullTransformation
955
+ | LinelistNumericRoundingTransformation
956
+ | LinelistCategorizeNumericTransformation
957
+ | LinelistConcatenateTextTransformation
958
+ | LinelistMaskIdentifierTransformation;
959
+
960
+ /**
961
+ * Base transformation type
962
+ */
963
+ type BaseTransformation<T extends string = string> = {
964
+ type: T;
965
+ enabled?: boolean;
966
+ };
967
+
968
+ /**
969
+ * Date format transformation
970
+ * Converts date values to a specified format
971
+ */
972
+ export type LinelistDateFormatTransformation = BaseTransformation<'DATE_FORMAT'> & {
973
+ format: string; // e.g., "dd/MM/yyyy", "yyyy-MM-dd", "MMM dd, yyyy"
974
+ inputFormat?: string; // If input needs parsing
975
+ };
976
+
977
+ /**
978
+ * Age calculation transformation
979
+ * Calculates age as of a specific date
980
+ */
981
+ export type LinelistAgeCalculationTransformation = BaseTransformation<'AGE_ON_DATE'> & {
982
+ dateField?: string; // Field to calculate age as of (default: endDate)
983
+ years?: boolean; // Include years
984
+ months?: boolean; // Include months
985
+ };
986
+
987
+ /**
988
+ * Coded to display name transformation
989
+ * Converts coded values (UUIDs) to their display names
990
+ */
991
+ export type LinelistCodedToDisplayTransformation = BaseTransformation<'CODED_TO_DISPLAY'> & {
992
+ fallback?: string; // Value to use if lookup fails
993
+ };
994
+
995
+ /**
996
+ * Boolean mapping transformation
997
+ * Maps boolean values to custom display strings
998
+ */
999
+ export type LinelistBooleanMappingTransformation = BaseTransformation<'BOOLEAN_MAPPING'> & {
1000
+ trueValue: string; // Display value for true (e.g., "Yes")
1001
+ falseValue: string; // Display value for false (e.g., "No")
1002
+ nullValue?: string; // Display value for null (e.g., "Unknown")
1003
+ };
1004
+
1005
+ /**
1006
+ * Replace null transformation
1007
+ * Replaces null/empty values with a specified value
1008
+ */
1009
+ export type LinelistReplaceNullTransformation = BaseTransformation<'REPLACE_NULL'> & {
1010
+ replacementValue: string; // Value to use when null
1011
+ };
1012
+
1013
+ /**
1014
+ * Numeric rounding transformation
1015
+ * Rounds numeric values to specified precision
1016
+ */
1017
+ export type LinelistNumericRoundingTransformation = BaseTransformation<'NUMERIC_ROUNDING'> & {
1018
+ decimalPlaces: number; // Number of decimal places (0-10)
1019
+ roundingMode?: 'UP' | 'DOWN' | 'HALF_UP' | 'HALF_DOWN'; // Rounding strategy
1020
+ };
1021
+
1022
+ /**
1023
+ * Categorize numeric transformation
1024
+ * Categorizes numeric values into ranges
1025
+ */
1026
+ export type LinelistCategorizeNumericTransformation = BaseTransformation<'CATEGORIZE_NUMERIC'> & {
1027
+ ranges: Array<{
1028
+ min: number;
1029
+ max: number;
1030
+ label: string;
1031
+ }>;
1032
+ defaultValue?: string; // Label for values not in any range
1033
+ };
1034
+
1035
+ /**
1036
+ * Concatenate text transformation
1037
+ * Concatenates multiple field values
1038
+ */
1039
+ export type LinelistConcatenateTextTransformation = BaseTransformation<'CONCATENATE_TEXT'> & {
1040
+ fields: string[]; // Field names to concatenate
1041
+ separator?: string; // Separator between values (default: ", ")
1042
+ includeNulls?: boolean; // Whether to include null values
1043
+ };
1044
+
1045
+ /**
1046
+ * Mask identifier transformation
1047
+ * Masks sensitive identifiers for privacy
1048
+ */
1049
+ export type LinelistMaskIdentifierTransformation = BaseTransformation<'MASK_IDENTIFIER'> & {
1050
+ maskChar?: string; // Character to use for masking (default: "*")
1051
+ visibleStart?: number; // Number of characters to show at start
1052
+ visibleEnd?: number; // Number of characters to show at end
1053
+ };
1054
+
1055
+ /**
1056
+ * Map of data definition type keys to their full types
1057
+ */
1058
+ export type LinelistDataDefinitionMap = {
1059
+ IDENTIFIER: LinelistIdentifierDefinition;
1060
+ PERSON_NAME: LinelistPersonNameDefinition;
1061
+ PERSON_ATTRIBUTE: LinelistPersonAttributeDefinition;
1062
+ CALCULATION: LinelistCalculationDefinition;
1063
+ SQL: LinelistSqlDefinition;
1064
+ PERSON_ADDRESS: LinelistPersonAddressDefinition;
1065
+ CONVERTER: LinelistConverterDefinition;
1066
+ OBSERVATION: LinelistObservationDefinition;
1067
+ ENCOUNTER_DIAGNOSIS: LinelistEncounterDiagnosisDefinition;
1068
+ };
1069
+
1070
+ /**
1071
+ * Builder panel options
1072
+ */
1073
+ export type LinelistBuilderPanel =
1074
+ | 'basics'
1075
+ | 'cohort'
1076
+ | 'columns'
1077
+ | 'sort'
1078
+ | 'parameters'
1079
+ | 'display-export'
1080
+ | 'preview'
1081
+ | 'review';
1082
+
1083
+ /**
1084
+ * Validation errors for linelist draft
1085
+ */
1086
+ export type LinelistValidationErrors = {
1087
+ name?: string;
1088
+ categoryUuid?: string;
1089
+ themeUuid?: string;
1090
+ dataSources?: string;
1091
+ rowGrain?: string;
1092
+ population?: string;
1093
+ columns?: string;
1094
+ general?: string;
1095
+ };
1096
+
1097
+ /**
1098
+ * Validation warnings for linelist draft (non-blocking)
1099
+ */
1100
+ export type LinelistValidationWarnings = {
1101
+ population?: string;
1102
+ columns?: string;
1103
+ performance?: string;
1104
+ category?: string;
1105
+ theme?: string;
1106
+ };
1107
+
1108
+ /**
1109
+ * Generate warnings for a linelist draft (non-blocking issues)
1110
+ */
1111
+ export function generateLinelistWarnings(draft: LinelistReportDraft): LinelistValidationWarnings {
1112
+ const warnings: LinelistValidationWarnings = {};
1113
+
1114
+ // Warn about missing category (recommended but not required)
1115
+ if (!draft.categoryUuid?.trim()) {
1116
+ warnings.category = 'No category selected. Reports are easier to find when organized into categories.';
1117
+ }
1118
+
1119
+ // Warn about population definition based on build method
1120
+ const buildMethod = draft.population?.buildMethod || 'SQL_BUILDER';
1121
+ const indicatorRules = draft.population?.indicatorRules || draft.indicatorRules;
1122
+
1123
+ // For SQL_BUILDER and VISUAL_FILTER modes, warn about missing SQL
1124
+ if (buildMethod !== 'INDICATOR_BASED' && !draft.population.sqlTemplate?.trim()) {
1125
+ warnings.population = 'No population SQL defined. Add SQL to define the patient cohort.';
1126
+ }
1127
+ // For INDICATOR_BASED mode, warn about missing indicator rules
1128
+ else if (buildMethod === 'INDICATOR_BASED' && (!indicatorRules || indicatorRules.length === 0)) {
1129
+ warnings.population = 'No indicator rules defined. Add at least one indicator to define the patient cohort.';
1130
+ }
1131
+
1132
+ // SQL warnings
1133
+ if (draft.population.sqlTemplate?.trim()) {
1134
+ const upperSql = draft.population.sqlTemplate.toUpperCase();
1135
+
1136
+ // Recommend DISTINCT for patient grain
1137
+ if (draft.rowGrain === 'PATIENT' && !upperSql.includes('DISTINCT')) {
1138
+ warnings.population = 'Consider using SELECT DISTINCT to avoid duplicate patients';
1139
+ }
1140
+
1141
+ // Check for potential performance issues
1142
+ if (upperSql.includes('SELECT *')) {
1143
+ warnings.performance = 'Avoid SELECT * for better performance';
1144
+ }
1145
+ }
1146
+
1147
+ // Column count warning
1148
+ if (draft.columns.length > 50) {
1149
+ warnings.columns = 'Large number of columns may impact performance';
1150
+ }
1151
+
1152
+ // SQL per column warning
1153
+ const sqlColumnCount = draft.columns.filter((col) => col.dataDefinitionType === 'SQL').length;
1154
+ if (sqlColumnCount > 10) {
1155
+ warnings.performance = `Many SQL columns (${sqlColumnCount}) may execute slowly. Consider using ETL joins.`;
1156
+ }
1157
+
1158
+ return warnings;
1159
+ }
1160
+
1161
+ /**
1162
+ * Report definition DTO with linelist config
1163
+ * Used for API responses
1164
+ */
1165
+ export type LinelistReportDto = {
1166
+ uuid: string;
1167
+ name: string;
1168
+ description?: string;
1169
+ code?: string;
1170
+ reportType: 'LINE_LIST';
1171
+ configJson?: string;
1172
+ metaJson?: string;
1173
+ retired?: boolean;
1174
+ compiledReportDefinitionUuid?: string; // Compiled report definition UUID from backend
1175
+ compiledReportDesignUuid?: string;
1176
+ lastCompiledAt?: string;
1177
+ compileStatus?: string;
1178
+ };
1179
+
1180
+ /**
1181
+ * Extract SQL template from an indicator definition
1182
+ * Handles multiple possible locations for the SQL template
1183
+ */
1184
+ function extractIndicatorSql(indicator: { sqlTemplate?: string | null; configJson?: string | null } | undefined): string {
1185
+ if (!indicator) return '';
1186
+
1187
+ // Try direct sqlTemplate first
1188
+ if (indicator.sqlTemplate?.trim()) {
1189
+ return indicator.sqlTemplate.trim();
1190
+ }
1191
+
1192
+ // Try parsing configJson
1193
+ if (indicator.configJson) {
1194
+ try {
1195
+ const parsed = JSON.parse(indicator.configJson);
1196
+
1197
+ // Try various possible paths
1198
+ const paths = [
1199
+ parsed.sqlPreview,
1200
+ parsed.sqlTemplate,
1201
+ parsed.base?.sqlPreview,
1202
+ parsed.base?.sqlTemplate,
1203
+ parsed.authoring?.base?.sqlPreview,
1204
+ parsed.authoring?.base?.sqlTemplate,
1205
+ parsed.baseIndicator?.sqlPreview,
1206
+ parsed.baseIndicator?.sqlTemplate,
1207
+ ];
1208
+
1209
+ for (const sql of paths) {
1210
+ if (typeof sql === 'string' && sql.trim()) {
1211
+ return sql.trim();
1212
+ }
1213
+ }
1214
+ } catch (e) {
1215
+ console.warn('Failed to parse indicator configJson:', e);
1216
+ }
1217
+ }
1218
+
1219
+ return '';
1220
+ }
1221
+
1222
+ /**
1223
+ * Convert COUNT SQL to population SQL
1224
+ * Handles both simple COUNT queries and composite WITH CTE queries
1225
+ */
1226
+ function convertCountSqlToPopulationSql(countSql: string): string {
1227
+ const sql = countSql.trim().replace(/;+\s*$/, '');
1228
+ if (!sql) return '';
1229
+
1230
+ // Case 1: COUNT(DISTINCT column) - convert to SELECT DISTINCT column
1231
+ const distinctPattern = /SELECT\s+COUNT\s*\(\s*DISTINCT\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)\s*\)\s+AS\s+(\w+)\s*/i;
1232
+ const distinctMatch = sql.match(distinctPattern);
1233
+ if (distinctMatch) {
1234
+ const columnRef = distinctMatch[1];
1235
+ const afterSelect = sql.substring(distinctMatch[0].length);
1236
+ if (columnRef.includes('.')) {
1237
+ // Extract column name from table.column reference
1238
+ const colName = columnRef.split('.')[1];
1239
+ return `SELECT DISTINCT ${columnRef} AS ${colName} ${afterSelect}`.trim();
1240
+ }
1241
+ // Extract table alias from FROM clause
1242
+ const fromPattern = /FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(?:AS\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\b/i;
1243
+ const fromMatch = afterSelect.match(fromPattern);
1244
+ const tableAlias = fromMatch ? fromMatch[2] : 'a';
1245
+ return `SELECT DISTINCT ${tableAlias}.${columnRef} AS ${columnRef} ${afterSelect}`.trim();
1246
+ }
1247
+
1248
+ // Case 2: Composite WITH ... SELECT COUNT(*) FROM (...) X
1249
+ const withPattern = /^WITH\s+([\s\S]*?)\s*SELECT\s+COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s+FROM\s*\(\s*(SELECT[\s\S]*?)\)\s*X\b/i;
1250
+ const withMatch = sql.match(withPattern);
1251
+ if (withMatch) {
1252
+ const ctes = withMatch[1];
1253
+ const inner = withMatch[2].trim();
1254
+ return `WITH ${ctes}\nSELECT DISTINCT pop.patient_id\nFROM (\n ${inner}\n) pop`;
1255
+ }
1256
+
1257
+ // Case 3: Simple SELECT COUNT(*) AS total FROM ...
1258
+ const simplePattern = /SELECT\s+COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s*/i;
1259
+ const simpleMatch = sql.match(simplePattern);
1260
+ if (simpleMatch) {
1261
+ const afterCount = sql.substring(simpleMatch[0].length);
1262
+ // Extract table alias and detect patient_id column name
1263
+ const fromPattern = /FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(?:AS\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\b/i;
1264
+ const fromMatch = afterCount.match(fromPattern);
1265
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1266
+ const tableAlias = fromMatch ? fromMatch[2] : 'a';
1267
+
1268
+ // Detect the patient_id column name from the WHERE clause or JOIN conditions
1269
+ // Common column names: patient_id, patient_id, person_id
1270
+ const columnPatterns = [
1271
+ /\b(?:${tableAlias}\.)?(patient_id|patient_id|person_id)\b/i,
1272
+ // Also check JOIN conditions for column references
1273
+ /ON\s+\w+\.(?:patient_id|patient_id|person_id)\s*=\s*${tableAlias}\.(patient_id|patient_id|person_id)/i,
1274
+ ];
1275
+
1276
+ let patientIdColumnName = 'patient_id'; // default
1277
+ for (const pattern of columnPatterns) {
1278
+ const match = afterCount.match(pattern);
1279
+ if (match) {
1280
+ patientIdColumnName = match[1] || match[2] || 'patient_id';
1281
+ break;
1282
+ }
1283
+ }
1284
+
1285
+ // Use the detected column name consistently
1286
+ return `SELECT DISTINCT ${tableAlias}.${patientIdColumnName} AS patient_id ${afterCount}`.trim();
1287
+ }
1288
+
1289
+ // Fallback: couldn't parse, return as-is (will likely fail in backend)
1290
+ return sql;
1291
+ }
1292
+
1293
+ /**
1294
+ * Combine multiple population SQLs with AND/OR logic
1295
+ */
1296
+ type PopulationSqlPart = {
1297
+ sql: string;
1298
+ operator: 'AND' | 'OR';
1299
+ negate?: boolean;
1300
+ };
1301
+
1302
+ function combinePopulationSqls(parts: PopulationSqlPart[]): string {
1303
+ if (parts.length === 0) return '';
1304
+ if (parts.length === 1) return parts[0].sql;
1305
+
1306
+ // Separate included and excluded populations
1307
+ const included: string[] = [];
1308
+ const excluded: string[] = [];
1309
+
1310
+ for (const part of parts) {
1311
+ if (part.negate) {
1312
+ excluded.push(part.sql);
1313
+ } else {
1314
+ included.push(part.sql);
1315
+ }
1316
+ }
1317
+
1318
+ // Build the base query from included populations
1319
+ let baseQuery = '';
1320
+ if (included.length === 0) {
1321
+ // If all are excluded, we need a different approach
1322
+ // Use all patients and subtract excluded ones
1323
+ baseQuery = 'SELECT DISTINCT patient_id AS patient_id FROM mamba_fact_patients_latest WHERE patient_id IS NOT NULL';
1324
+ } else if (included.length === 1) {
1325
+ baseQuery = included[0];
1326
+ } else {
1327
+ // Combine included populations with INTERSECT (AND) or UNION (OR)
1328
+ // For now, use INTERSECT as default - should be improved to track operators
1329
+ baseQuery = `(${included.join('\n INTERSECT\n ')})`;
1330
+ }
1331
+
1332
+ // Apply exclusions using EXCEPT
1333
+ if (excluded.length > 0) {
1334
+ for (const excludeSql of excluded) {
1335
+ baseQuery = `(${baseQuery}\n EXCEPT\n ${excludeSql})`;
1336
+ }
1337
+ }
1338
+
1339
+ return baseQuery;
1340
+ }
1341
+
1342
+ /**
1343
+ * Convert a draft to the final configuration format
1344
+ *
1345
+ * @param draft - The linelist report draft
1346
+ * @param indicators - Optional map of indicator UUIDs to indicator definitions for INDICATOR mode
1347
+ */
1348
+ export function draftToConfig(
1349
+ draft: LinelistReportDraft,
1350
+ indicators?: Map<string, { sqlTemplate?: string | null; configJson?: string | null }>
1351
+ ): LinelistReportDefinitionConfig {
1352
+
1353
+ /**
1354
+ * Build data sources array from draft columns.
1355
+ * Groups columns by their ACTUAL source table (extracted from the SQL
1356
+ * expression) so the data source list accurately reflects where each
1357
+ * column's data comes from.
1358
+ */
1359
+ const buildDataSources = (): DataSourceConfig[] => {
1360
+ const sourceMap = new Map<string, DataSourceConfig>();
1361
+
1362
+ // Determine the primary data source uuid (for role assignment)
1363
+ // If no PRIMARY is set in draft, we'll assign one after building the map
1364
+ const primaryDsUuid = draft.dataSources.find(ds => ds.role === 'PRIMARY')?.uuid || '';
1365
+ let hasAssignedPrimary = !!primaryDsUuid;
1366
+
1367
+ /**
1368
+ * Extract the actual source table from a column's SQL expression.
1369
+ * - Simple reference like `table`.`column` → returns the table name
1370
+ * - Custom query (SELECT ... / contains :patientId) → returns 'custom_sql'
1371
+ * - Cannot determine → returns 'unknown'
1372
+ */
1373
+ const extractSourceTable = (sql?: string): string => {
1374
+ if (!sql) return 'unknown';
1375
+ const trimmed = sql.trim();
1376
+ // Custom SQL query (per-row subquery)
1377
+ if (/^SELECT\s/i.test(trimmed) || /:(patientId|patient_id)\b/i.test(trimmed)) {
1378
+ return 'custom_sql';
1379
+ }
1380
+ // Simple table.column reference (with or without backticks)
1381
+ const match = trimmed.match(/`?(\w+)`?\.\s*`?\w+`?/);
1382
+ return match ? match[1] : 'unknown';
1383
+ };
1384
+
1385
+ // Always include core OpenMRS sources if used
1386
+ const hasPersonAttributes = draft.columns.some(col =>
1387
+ col.dataDefinitionType === 'PERSON_ATTRIBUTE'
1388
+ );
1389
+ const hasIdentifiers = draft.columns.some(col =>
1390
+ col.dataDefinitionType === 'IDENTIFIER'
1391
+ );
1392
+
1393
+ if (hasPersonAttributes) {
1394
+ sourceMap.set('person_attributes', {
1395
+ uuid: 'person_attributes',
1396
+ name: 'Person Attributes',
1397
+ type: 'CORE',
1398
+ role: 'REFERENCE',
1399
+ });
1400
+ }
1401
+
1402
+ if (hasIdentifiers) {
1403
+ sourceMap.set('patient_identifiers', {
1404
+ uuid: 'patient_identifiers',
1405
+ name: 'Patient Identifiers',
1406
+ type: 'CORE',
1407
+ role: 'REFERENCE',
1408
+ });
1409
+ }
1410
+
1411
+ // Track the first non-REFERENCE source for default PRIMARY assignment
1412
+ let firstNonReferenceSource: string | null = null;
1413
+
1414
+ // Group columns by their actual source table
1415
+ draft.columns.forEach(col => {
1416
+ if (col.dataDefinitionType === 'PERSON_ATTRIBUTE' || col.dataDefinitionType === 'IDENTIFIER') {
1417
+ return; // Already handled above
1418
+ }
1419
+
1420
+ // Resolve the real source table
1421
+ let table: string;
1422
+ if (col.dataDefinitionType === 'CALCULATION') {
1423
+ table = 'calculated';
1424
+ } else if (col.dataDefinitionType === 'OBSERVATION') {
1425
+ table = 'observations';
1426
+ } else if (col.dataDefinitionType === 'ENCOUNTER_DIAGNOSIS') {
1427
+ table = 'encounter_diagnoses';
1428
+ } else if (col.dataDefinitionType === 'PERSON_ADDRESS') {
1429
+ table = 'person_address';
1430
+ } else if (col.dataDefinitionType === 'PERSON_NAME') {
1431
+ table = 'person_name';
1432
+ } else {
1433
+ // For SQL columns, derive the table from the SQL expression
1434
+ table = extractSourceTable(col.dataDefinitionConfig?.sql);
1435
+ }
1436
+
1437
+ // The data source uuid IS the table name (accurate grouping)
1438
+ const dsUuid = table;
1439
+ const isPrimary = dsUuid === primaryDsUuid;
1440
+ const dsType = dsUuid === 'custom_sql'
1441
+ ? 'SQL'
1442
+ : dsUuid === 'calculated'
1443
+ ? 'SQL'
1444
+ : dsUuid.startsWith('mamba_') ? 'ETL' : 'SQL';
1445
+
1446
+ // Map table names to user-friendly data source names
1447
+ const dataSourceNames: Record<string, string> = {
1448
+ 'observations': 'Observations',
1449
+ 'encounter_diagnoses': 'Encounter Diagnoses',
1450
+ 'person_address': 'Person Address',
1451
+ 'person_name': 'Person Names',
1452
+ 'calculated': 'Calculated Fields',
1453
+ 'custom_sql': 'Custom SQL',
1454
+ };
1455
+
1456
+ if (!sourceMap.has(dsUuid)) {
1457
+ sourceMap.set(dsUuid, {
1458
+ uuid: dsUuid,
1459
+ name: dataSourceNames[dsUuid] || dsUuid,
1460
+ type: dsType as DataSourceConfig['type'],
1461
+ role: isPrimary ? 'PRIMARY' : 'SECONDARY',
1462
+ columns: {},
1463
+ });
1464
+ // Track the first non-REFERENCE source for default PRIMARY assignment
1465
+ // (CORE sources like person_attributes/patient_identifiers are already handled above)
1466
+ if (!hasAssignedPrimary && !firstNonReferenceSource) {
1467
+ firstNonReferenceSource = dsUuid;
1468
+ }
1469
+ }
1470
+
1471
+ const ds = sourceMap.get(dsUuid);
1472
+ if (ds && ds.columns) {
1473
+ ds.columns[col.name] = {
1474
+ name: col.name,
1475
+ type: col.source?.fieldType || 'UNKNOWN',
1476
+ sourceTable: table,
1477
+ };
1478
+ }
1479
+ });
1480
+
1481
+ // If no primary was assigned and we have a non-REFERENCE source, make it PRIMARY
1482
+ if (!hasAssignedPrimary && firstNonReferenceSource) {
1483
+ const ds = sourceMap.get(firstNonReferenceSource);
1484
+ if (ds) {
1485
+ ds.role = 'PRIMARY';
1486
+ hasAssignedPrimary = true;
1487
+ }
1488
+ }
1489
+
1490
+ return Array.from(sourceMap.values());
1491
+ };
1492
+ /**
1493
+ * Build population SQL from indicator rules
1494
+ */
1495
+ const buildIndicatorPopulationSql = (): string => {
1496
+ // Check both locations for indicator rules (population.indicatorRules or legacy top-level)
1497
+ const indicatorRules = draft.population.indicatorRules || draft.indicatorRules;
1498
+ if (!indicatorRules || indicatorRules.length === 0) {
1499
+ return draft.population.sqlTemplate || '';
1500
+ }
1501
+
1502
+ const populationSqlParts: PopulationSqlPart[] = [];
1503
+
1504
+ for (const rule of indicatorRules) {
1505
+ if (!rule.indicatorUuid || !indicators?.has(rule.indicatorUuid)) {
1506
+ continue;
1507
+ }
1508
+
1509
+ const indicator = indicators.get(rule.indicatorUuid);
1510
+ const countSql = extractIndicatorSql(indicator);
1511
+
1512
+ if (!countSql) {
1513
+ console.warn(`No SQL found for indicator: ${rule.name} (${rule.indicatorUuid})`);
1514
+ continue;
1515
+ }
1516
+
1517
+ // Convert COUNT SQL to population SQL
1518
+ const populationSql = convertCountSqlToPopulationSql(countSql);
1519
+
1520
+ if (!populationSql) {
1521
+ continue;
1522
+ }
1523
+
1524
+ populationSqlParts.push({ sql: populationSql, operator: rule.logicalOperator || 'AND', negate: rule.negate });
1525
+ }
1526
+
1527
+ if (populationSqlParts.length === 0) {
1528
+ return draft.population.sqlTemplate || '';
1529
+ }
1530
+
1531
+ // Combine population SQL parts with AND/OR logic
1532
+ return combinePopulationSqls(populationSqlParts);
1533
+ };
1534
+
1535
+ // Get the final cohort SQL based on population mode
1536
+ const getCohortSql = (): string => {
1537
+ switch (draft.populationMode) {
1538
+ case 'INDICATOR':
1539
+ return buildIndicatorPopulationSql();
1540
+ case 'HYBRID': {
1541
+ // Combine existing cohort SQL with indicator SQL
1542
+ const indicatorSql = buildIndicatorPopulationSql();
1543
+ if (indicatorSql && draft.population.sqlTemplate) {
1544
+ return `SELECT DISTINCT base.patient_id AS patient_id
1545
+ FROM (${draft.population.sqlTemplate}) base
1546
+ INNER JOIN (${indicatorSql}) indicators ON indicators.patient_id = base.patient_id`;
1547
+ }
1548
+ return indicatorSql || draft.population.sqlTemplate || '';
1549
+ }
1550
+ default:
1551
+ return draft.population.sqlTemplate || '';
1552
+ }
1553
+ };
1554
+
1555
+ const finalCohortSql = getCohortSql();
1556
+
1557
+ // Convert column drafts to full column definitions
1558
+ const columns: LinelistColumnDefinition[] = draft.columns
1559
+ .sort((a, b) => a.sortOrder - b.sortOrder)
1560
+ .map((col, index) => {
1561
+ const columnDef: LinelistColumnDefinition = {
1562
+ name: col.name,
1563
+ dataDefinition: buildDataDefinition(col.dataDefinitionType, col.dataDefinitionConfig),
1564
+ };
1565
+
1566
+ // Add _metadata with column position for backend ordering
1567
+ (columnDef as any)._metadata = {
1568
+ id: col.id,
1569
+ position: col.sortOrder ?? index,
1570
+ };
1571
+
1572
+ // Add repeat resolution if configured
1573
+ if (col.repeatResolution) {
1574
+ (columnDef as any).repeatResolution = col.repeatResolution;
1575
+ } else {
1576
+ // For SQL columns that may return multiple values, add default repeat resolution
1577
+ // if one isn't already configured. This prevents backend compilation errors.
1578
+ const sql = col.dataDefinitionConfig.sql || '';
1579
+
1580
+ // Check if SQL uses an aggregate function (always returns single value)
1581
+ const hasAggregate = /^(?:SELECT\s+(?:DISTINCT\s+)?(?:COUNT|MAX|MIN|SUM|AVG|GROUP_CONCAT|ARRAY_AGG|STRING_AGG|JSON_AGG)\s*\()/i.test(sql) ||
1582
+ /GROUP\s+BY\s+/i.test(sql);
1583
+
1584
+ // Check if SQL already has LIMIT 1
1585
+ const hasLimitOne = /\bLIMIT\s+1\b/i.test(sql);
1586
+
1587
+ // Only add repeat resolution if:
1588
+ // 1. It references patient-related tables (encounter, observation, etc.)
1589
+ // 2. It's NOT an aggregate query
1590
+ // 3. It doesn't have LIMIT 1
1591
+ const isPotentiallyRepeated = col.dataDefinitionType === 'SQL' &&
1592
+ /appointment|observation|encounter|program_enrollment|visit/i.test(sql) &&
1593
+ !hasAggregate && !hasLimitOne;
1594
+
1595
+ if (isPotentiallyRepeated) {
1596
+ // Use the SQL's ORDER BY field if present, otherwise use a default
1597
+ const orderByMatch = sql.match(/ORDER BY\s+([a-z_][a-z0-9_]*)/i);
1598
+ const orderByField = orderByMatch ? orderByMatch[1] : 'encounter_date';
1599
+
1600
+ (columnDef as any).repeatResolution = {
1601
+ strategy: 'LATEST',
1602
+ orderBy: orderByField,
1603
+ restrictToPeriod: false,
1604
+ ignoreVoided: true,
1605
+ };
1606
+ }
1607
+ }
1608
+
1609
+ return columnDef;
1610
+ });
1611
+
1612
+ // Convert parameter drafts to full parameter definitions
1613
+ const parameters: LinelistParameter[] = (draft.parameters || [])
1614
+ .sort((a, b) => a.displayOrder - b.displayOrder)
1615
+ .map((param) => ({
1616
+ name: param.name,
1617
+ label: param.label,
1618
+ type: param.type,
1619
+ required: param.required,
1620
+ ...(param.config && { config: param.config }),
1621
+ }));
1622
+
1623
+ // Default to startDate/endDate if no parameters defined
1624
+ const finalParameters = parameters.length > 0
1625
+ ? parameters
1626
+ : [
1627
+ { name: 'startDate', label: 'Start Date', type: 'DATE' as const, required: true },
1628
+ { name: 'endDate', label: 'End Date', type: 'DATE' as const, required: true },
1629
+ ];
1630
+
1631
+ const config: LinelistReportDefinitionConfig = {
1632
+ version: 3, // Updated to version 3 for population sources support
1633
+ type: 'LINE_LIST',
1634
+ categoryUuid: draft.categoryUuid,
1635
+ // Include both for compatibility - new code uses dataSources
1636
+ dataSourceUuid: draft.dataSources.find(ds => ds.role === 'PRIMARY')?.uuid,
1637
+ dataSources: buildDataSources(),
1638
+ rowGrain: draft.rowGrain,
1639
+ templateId: draft.templateId,
1640
+ parameters: finalParameters,
1641
+ // Preserve builder metadata in configJson so the editor can reconstruct
1642
+ // the draft (selected indicators, build method) when re-opening for edit.
1643
+ // Read indicatorRules from both the new (population) and legacy (top-level)
1644
+ // fields — the UI writes to the top-level field.
1645
+ // Derive buildMethod from whether indicators are actually present, since the
1646
+ // UI may not always keep population.buildMethod in sync.
1647
+ indicatorRules: draft.population.indicatorRules || draft.indicatorRules,
1648
+ buildMethod: ((draft.population.indicatorRules || draft.indicatorRules || []).length > 0)
1649
+ ? 'INDICATOR_BASED'
1650
+ : (draft.population.buildMethod || 'SQL_BUILDER'),
1651
+ baseCohortDefinition: {
1652
+ type: 'SQL',
1653
+ name: draft.population.buildMethod === 'INDICATOR_BASED'
1654
+ ? 'Indicator-based Patient Cohort'
1655
+ : 'Patient Cohort',
1656
+ config: {
1657
+ sql: finalCohortSql,
1658
+ ...(draft.population.filterMap && { filterMap: draft.population.filterMap }),
1659
+ },
1660
+ },
1661
+ dataSetDefinitions: [
1662
+ {
1663
+ name: draft.dataSetName || 'PATIENT_LIST',
1664
+ type: 'PATIENT_DATA_SET',
1665
+ rowFilter: {
1666
+ type: 'SQL',
1667
+ name: 'Row Filter',
1668
+ config: {
1669
+ sql: finalCohortSql,
1670
+ ...(draft.population.filterMap && { filterMap: draft.population.filterMap }),
1671
+ },
1672
+ },
1673
+ columns,
1674
+ },
1675
+ ],
1676
+ };
1677
+
1678
+ // Handle sort configuration - convert to legacy format for backend compatibility
1679
+ if (draft.sortConfig && draft.sortConfig.length > 0) {
1680
+ // For now, use the first sort rule as the primary sort
1681
+ // In the future, the backend may support multiple sort rules
1682
+ const primarySort = draft.sortConfig[0];
1683
+ config.orderBy = primarySort.columnId;
1684
+ config.orderDirection = primarySort.direction;
1685
+
1686
+ // Note: Multi-column sorting would require backend changes
1687
+ // For now, additional sort rules are stored but only the first is used
1688
+ if (draft.sortConfig.length > 1) {
1689
+ console.warn('Multiple sort rules defined, but only the first will be used until backend supports multi-column sorting');
1690
+ }
1691
+ }
1692
+
1693
+ if (draft.limit) {
1694
+ config.limit = draft.limit;
1695
+ }
1696
+
1697
+ return config;
1698
+ }
1699
+
1700
+ /**
1701
+ * Build a data definition from type and config
1702
+ */
1703
+ function buildDataDefinition(
1704
+ type: keyof LinelistDataDefinitionMap,
1705
+ config: Record<string, any>
1706
+ ): LinelistDataDefinition {
1707
+ switch (type) {
1708
+ case 'IDENTIFIER':
1709
+ return { type: 'IDENTIFIER', config: config as LinelistIdentifierDefinition['config'] };
1710
+ case 'PERSON_NAME':
1711
+ return { type: 'PERSON_NAME', config: config as LinelistPersonNameDefinition['config'] };
1712
+ case 'PERSON_ATTRIBUTE':
1713
+ return { type: 'PERSON_ATTRIBUTE', config: config as LinelistPersonAttributeDefinition['config'] };
1714
+ case 'CALCULATION':
1715
+ return { type: 'CALCULATION', config: config as LinelistCalculationDefinition['config'] };
1716
+ case 'SQL':
1717
+ return { type: 'SQL', config: config as LinelistSqlDefinition['config'] };
1718
+ case 'PERSON_ADDRESS':
1719
+ return { type: 'PERSON_ADDRESS', config: config as LinelistPersonAddressDefinition['config'] };
1720
+ case 'CONVERTER':
1721
+ return { type: 'CONVERTER', config: config as LinelistConverterDefinition['config'] };
1722
+ case 'OBSERVATION':
1723
+ return { type: 'OBSERVATION', config: config as LinelistObservationDefinition['config'] };
1724
+ case 'ENCOUNTER_DIAGNOSIS':
1725
+ return { type: 'ENCOUNTER_DIAGNOSIS', config: config as LinelistEncounterDiagnosisDefinition['config'] };
1726
+ default:
1727
+ // Default to SQL definition for unknown types
1728
+ return { type: 'SQL', config: { sql: config.sql || '' } };
1729
+ }
1730
+ }
1731
+
1732
+ /**
1733
+ * Validate a linelist draft
1734
+ */
1735
+ export function validateLinelistDraft(draft: LinelistReportDraft): LinelistValidationErrors {
1736
+ const errors: LinelistValidationErrors = {};
1737
+
1738
+ if (!draft.name?.trim()) {
1739
+ errors.name = 'Name is required';
1740
+ }
1741
+
1742
+ // Category is recommended but not required for compilation
1743
+ // if (!draft.categoryUuid?.trim()) {
1744
+ // errors.categoryUuid = 'Category is required';
1745
+ // }
1746
+
1747
+ // Validate population definition based on build method
1748
+ const buildMethod = draft.population?.buildMethod || 'SQL_BUILDER';
1749
+ const indicatorRules = draft.population?.indicatorRules || draft.indicatorRules;
1750
+
1751
+ // For INDICATOR_BASED mode, validate indicator rules instead of population sources
1752
+ if (buildMethod === 'INDICATOR_BASED') {
1753
+ if (!indicatorRules || indicatorRules.length === 0) {
1754
+ errors.population = 'At least one indicator rule is required for indicator-based population';
1755
+ }
1756
+ }
1757
+ // For SQL_BUILDER and VISUAL_FILTER modes, validate population SQL
1758
+ else if (!draft.population.sqlTemplate?.trim()) {
1759
+ errors.population = 'Population SQL is required';
1760
+ }
1761
+
1762
+ // Validate data sources for columns (v2 - at least one primary data source required)
1763
+ if (!draft.dataSources || draft.dataSources.length === 0) {
1764
+ errors.dataSources = 'At least one column data source is required';
1765
+ } else {
1766
+ const hasPrimary = draft.dataSources.some(ds => ds.role === 'PRIMARY');
1767
+ if (!hasPrimary) {
1768
+ errors.dataSources = 'A primary data source is required';
1769
+ }
1770
+ }
1771
+
1772
+ if (!draft.rowGrain?.trim()) {
1773
+ errors.rowGrain = 'Row grain is required';
1774
+ }
1775
+
1776
+ // Validate population SQL (v2 - in population.sqlTemplate)
1777
+ if (!draft.population.sqlTemplate?.trim()) {
1778
+ errors.population = 'Population SQL is required';
1779
+ } else {
1780
+ // Check that SQL includes the parameters defined in the report
1781
+ // Only validate parameters that are actually configured
1782
+ const requiredParams = draft.parameters
1783
+ .filter(p => p.required)
1784
+ .map(p => p.name);
1785
+
1786
+ const missingParams: string[] = [];
1787
+ for (const param of requiredParams) {
1788
+ const paramPlaceholder = `:${param}`;
1789
+ if (!draft.population.sqlTemplate.includes(paramPlaceholder)) {
1790
+ missingParams.push(param);
1791
+ }
1792
+ }
1793
+
1794
+ if (missingParams.length > 0) {
1795
+ errors.population = `Population SQL must include these parameters: ${missingParams.map(p => `:${p}`).join(', ')}`;
1796
+ }
1797
+ }
1798
+
1799
+ if (draft.columns.length === 0) {
1800
+ errors.columns = 'At least one column is required';
1801
+ } else {
1802
+ // Validate each column
1803
+ const invalidColumns = draft.columns.filter((col) => !col.name?.trim());
1804
+ if (invalidColumns.length > 0) {
1805
+ errors.columns = 'All columns must have a display name';
1806
+ }
1807
+
1808
+ // Check for unique column IDs
1809
+ const columnIds = draft.columns.map((col) => col.id);
1810
+ const uniqueIds = new Set(columnIds);
1811
+ if (columnIds.length !== uniqueIds.size) {
1812
+ errors.columns = 'Columns must have unique identifiers';
1813
+ }
1814
+
1815
+ // Check for repeated fields without resolution strategy (v2 - using dataDefinitionConfig.sql)
1816
+ const repeatedColumnsWithoutResolution: string[] = [];
1817
+ draft.columns.forEach((col) => {
1818
+ // Only check SQL columns
1819
+ if (col.dataDefinitionType !== 'SQL') return;
1820
+
1821
+ const sql = col.dataDefinitionConfig.sql || '';
1822
+
1823
+ // Skip empty SQL
1824
+ if (!sql.trim()) return;
1825
+
1826
+ // Check if repeatResolution is configured
1827
+ const hasResolution = col.repeatResolution?.strategy && col.repeatResolution.strategy !== 'NONE';
1828
+
1829
+ // Check if SQL already handles multiple values with LIMIT 1
1830
+ const hasLimitOne = /\bLIMIT\s+1\b/i.test(sql);
1831
+
1832
+ // Check if SQL uses an aggregate function (always returns single value)
1833
+ // Common aggregate functions: COUNT, MAX, MIN, SUM, AVG, GROUP_CONCAT, etc.
1834
+ const hasAggregate = /^(?:SELECT\s+(?:DISTINCT\s+)?(?:COUNT|MAX|MIN|SUM|AVG|GROUP_CONCAT|ARRAY_AGG|STRING_AGG|JSON_AGG)\s*\()/i.test(sql) ||
1835
+ /GROUP\s+BY\s+/i.test(sql);
1836
+
1837
+ // Check if SQL is a scalar subquery (single value return)
1838
+ // Pattern: SELECT single_column FROM ... WHERE patient_id = :patientId AND specific condition LIMIT 1
1839
+ const isScalarSubquery = /\bpatient_id\s*=\s*:patientId\b/i.test(sql) && /\bLIMIT\s+1\b/i.test(sql);
1840
+
1841
+ // A query is potentially repeated (returns multiple rows) if:
1842
+ // 1. It references patient-related tables (encounter, observation, etc.)
1843
+ // 2. It's NOT an aggregate query (COUNT, MAX, etc.)
1844
+ // 3. It doesn't have LIMIT 1
1845
+ // 4. It's not a scalar subquery with proper conditions
1846
+ const referencesPatientData = /appointment|observation|encounter|program_enrollment|visit/i.test(sql);
1847
+ const isPotentiallyRepeated = referencesPatientData && !hasAggregate && !hasLimitOne && !isScalarSubquery && !hasResolution;
1848
+
1849
+ if (isPotentiallyRepeated) {
1850
+ repeatedColumnsWithoutResolution.push(col.name);
1851
+ }
1852
+ });
1853
+
1854
+ if (repeatedColumnsWithoutResolution.length > 0) {
1855
+ errors.columns = `Columns that may return multiple values per row: ${repeatedColumnsWithoutResolution.join(', ')}. Please configure repeat resolution for each.`;
1856
+ }
1857
+ }
1858
+
1859
+ // Validate sort column references
1860
+ if (draft.sortConfig && draft.sortConfig.length > 0) {
1861
+ const invalidSorts = draft.sortConfig.filter((sort) => {
1862
+ return !draft.columns.some((col) => col.id === sort.columnId);
1863
+ });
1864
+
1865
+ if (invalidSorts.length > 0) {
1866
+ errors.general = 'Sort rules reference columns that are not in the report';
1867
+ }
1868
+ }
1869
+
1870
+ // Check for basic SQL syntax validation (v2 - using population.sqlTemplate)
1871
+ if (draft.population.sqlTemplate?.trim()) {
1872
+ const upperSql = draft.population.sqlTemplate.toUpperCase().trim();
1873
+
1874
+ // Check for SELECT statement
1875
+ if (!upperSql.includes('SELECT')) {
1876
+ errors.population = 'SQL must include a SELECT statement';
1877
+ }
1878
+
1879
+ // Check for patient_id column
1880
+ if (!upperSql.includes('PATIENT_ID')) {
1881
+ errors.population = 'SQL must select patient_id or patient_id column';
1882
+ }
1883
+
1884
+ // Recommend DISTINCT for patient grain
1885
+ if (draft.rowGrain === 'PATIENT' && !upperSql.includes('DISTINCT')) {
1886
+ // This is a warning, not an error - we could add a warnings object
1887
+ // For now, we'll add it to errors but it won't block save in draft mode
1888
+ // errors.population = 'Consider using SELECT DISTINCT to avoid duplicate patients';
1889
+ }
1890
+ }
1891
+
1892
+ return errors;
1893
+ }
1894
+
1895
+ /**
1896
+ * Check if a linelist draft is valid
1897
+ */
1898
+ export function isLinelistDraftValid(draft: LinelistReportDraft): boolean {
1899
+ const errors = validateLinelistDraft(draft);
1900
+ return Object.keys(errors).length === 0;
1901
+ }
1902
+
1903
+ /**
1904
+ * Check if a linelist draft is ready to compile
1905
+ * This includes stricter validation than draft validation
1906
+ *
1907
+ * Compiling requires:
1908
+ * - All draft validations pass
1909
+ * - No unresolved repeated fields (will be fully enforced in Task 3)
1910
+ * - SQL definitions would pass server validation (placeholder for server validation)
1911
+ */
1912
+ export function isLinelistDraftReadyToCompile(draft: LinelistReportDraft): boolean {
1913
+ const errors = validateLinelistDraft(draft);
1914
+ const warnings = generateLinelistWarnings(draft);
1915
+
1916
+ // Basic validation must pass
1917
+ if (Object.keys(errors).length > 0) {
1918
+ return false;
1919
+ }
1920
+
1921
+ // Check for unresolved repeated fields
1922
+ const hasUnresolvedRepeatedFields = draft.columns.some((col) => {
1923
+ if (col.dataDefinitionType !== 'SQL') return false;
1924
+
1925
+ const sql = col.dataDefinitionConfig.sql || '';
1926
+ if (!sql.trim()) return false;
1927
+
1928
+ // Check if SQL uses an aggregate function (always returns single value)
1929
+ const hasAggregate = /^(?:SELECT\s+(?:DISTINCT\s+)?(?:COUNT|MAX|MIN|SUM|AVG|GROUP_CONCAT|ARRAY_AGG|STRING_AGG|JSON_AGG)\s*\()/i.test(sql) ||
1930
+ /GROUP\s+BY\s+/i.test(sql);
1931
+
1932
+ // Check if SQL already has LIMIT 1
1933
+ const hasLimitOne = /\bLIMIT\s+1\b/i.test(sql);
1934
+
1935
+ // Check if a valid resolution strategy is configured
1936
+ const hasValidResolution = col.repeatResolution?.strategy &&
1937
+ col.repeatResolution.strategy !== 'NONE';
1938
+
1939
+ // Only consider unresolved if it references patient-related tables,
1940
+ // doesn't have aggregate, doesn't have LIMIT 1, and no valid resolution
1941
+ const referencesPatientData = /appointment|observation|encounter|program_enrollment|visit/i.test(sql);
1942
+ const isPotentiallyRepeated = referencesPatientData && !hasAggregate && !hasLimitOne;
1943
+
1944
+ return isPotentiallyRepeated && !hasValidResolution;
1945
+ });
1946
+
1947
+ if (hasUnresolvedRepeatedFields) {
1948
+ return false;
1949
+ }
1950
+
1951
+ // Check for critical performance warnings that should block compiling
1952
+ if (warnings.performance?.includes('Many SQL columns') && draft.columns.length > 20) {
1953
+ return false;
1954
+ }
1955
+
1956
+ return true;
1957
+ }
1958
+
1959
+ /**
1960
+ * @deprecated Use isLinelistDraftReadyToCompile instead
1961
+ */
1962
+ export function isLinelistDraftReadyToPublish(draft: LinelistReportDraft): boolean {
1963
+ return isLinelistDraftReadyToCompile(draft);
1964
+ }
1965
+
1966
+ /**
1967
+ * Create an empty linelist report draft with default values (v2)
1968
+ */
1969
+ export function createEmptyDraft(): LinelistReportDraft {
1970
+ const now = new Date().toISOString();
1971
+
1972
+ return {
1973
+ version: 3, // Updated to version 3 for population sources support
1974
+ name: '',
1975
+ description: '',
1976
+ code: '',
1977
+ currentPanel: 'basics',
1978
+ categoryUuid: '',
1979
+ rowGrain: 'PATIENT',
1980
+ templateId: '',
1981
+
1982
+ // Population sources (v3 - multiple sources with join types)
1983
+ populationSources: [],
1984
+
1985
+ // Data sources (v2 - array, not single)
1986
+ dataSources: [],
1987
+
1988
+ // Population definition
1989
+ population: {
1990
+ baseDataSourceUuid: '',
1991
+ buildMethod: 'SQL_BUILDER',
1992
+ sqlTemplate: '',
1993
+ parameterReferences: [],
1994
+ visualFilter: {
1995
+ rootGroup: {
1996
+ id: 'root',
1997
+ logicalOperator: 'AND',
1998
+ conditions: [],
1999
+ nestedGroups: [],
2000
+ },
2001
+ useVisualBuilder: false,
2002
+ },
2003
+ buildHistory: [],
2004
+ filterMap: undefined,
2005
+ },
2006
+
2007
+ columns: [],
2008
+ // Default parameters: every linelist report uses a date range
2009
+ parameters: [
2010
+ {
2011
+ id: 'param-startDate',
2012
+ name: 'startDate',
2013
+ label: 'Start Date',
2014
+ type: 'DATE',
2015
+ required: true,
2016
+ defaultValue: '',
2017
+ displayOrder: 0,
2018
+ },
2019
+ {
2020
+ id: 'param-endDate',
2021
+ name: 'endDate',
2022
+ label: 'End Date',
2023
+ type: 'DATE',
2024
+ required: true,
2025
+ defaultValue: '',
2026
+ displayOrder: 1,
2027
+ },
2028
+ ],
2029
+ sortConfig: [],
2030
+ limit: 1000,
2031
+
2032
+ displaySettings: {
2033
+ defaultPageSize: 25,
2034
+ freezeFirstColumn: true,
2035
+ freezeHeader: true,
2036
+ dateDisplayFormat: 'dd MMM yyyy',
2037
+ nullDisplayValue: '—',
2038
+ maxInteractiveRows: 500,
2039
+ maxExportRows: 100000,
2040
+ allowedExports: ['CSV', 'XLSX', 'PDF'],
2041
+ includeParametersInExportHeader: true,
2042
+ includeGeneratedTimestamp: true,
2043
+ },
2044
+
2045
+ validation: {
2046
+ errors: {},
2047
+ warnings: {},
2048
+ lastValidated: now,
2049
+ },
2050
+
2051
+ metadata: {
2052
+ createdAt: now,
2053
+ lastModified: now,
2054
+ buildMethod: 'NEW',
2055
+ version: 2,
2056
+ status: 'DRAFT',
2057
+ },
2058
+ };
2059
+ }