@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,721 @@
1
+ import type { DataThemeConfig, ThemeCondition } from '../types/data-theme-config.types';
2
+ import type { IndicatorCondition } from '../types/indicator-types';
3
+
4
+ import { normalizeOperator, isInOperator, isNullCheckOperator } from '../../../types/condition-operators';
5
+ import { getConceptByUuid } from '../../../resources/concepts/concepts.resource';
6
+
7
+ const DEMO_JOIN_TABLE = 'mamba_fact_patients_latest_patient_demographics';
8
+
9
+ function sqlQuote(v: string) {
10
+ return `'${String(v).replace(/'/g, "''")}'`;
11
+ }
12
+
13
+ function qualifyColumn(col: string) {
14
+ const c = String(col ?? '').trim();
15
+ if (!c) return c;
16
+ if (c.includes('.') || c.includes('(') || c.includes(' ') || c.includes('`')) return c;
17
+ return `a.${c}`;
18
+ }
19
+
20
+ /**
21
+ * Extract numeric concept ID from OpenMRS UUID format.
22
+ * OpenMRS UUIDs for concepts are often: {numericId} + A... padding
23
+ * e.g., "116128AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" → 116128
24
+ * Returns original value if not in this format.
25
+ */
26
+ function extractConceptIdFromUuid(uuid: string): string {
27
+ const str = String(uuid).trim();
28
+ // Match OpenMRS concept UUID format: numeric prefix followed by 'A' characters
29
+ const match = str.match(/^(\d+)A+$/);
30
+ if (match) return match[1];
31
+
32
+ // Try extracting from standard UUID format that might have numeric prefix
33
+ // e.g., "116128..." in various formats
34
+ const numericPrefix = str.match(/^(\d+)/);
35
+ if (numericPrefix) return numericPrefix[1];
36
+
37
+ // Return original if no pattern matches
38
+ return str;
39
+ }
40
+
41
+ function parseQaColumnsFromExpr(expr?: string | null) {
42
+ const raw = String(expr ?? '').trim();
43
+ if (!raw) return { questionColumn: undefined as string | undefined, answerColumn: undefined as string | undefined };
44
+
45
+ const m = raw.match(/QA\s*$begin:math:text$\(\.\*\)$end:math:text$/i);
46
+ if (!m) return { questionColumn: undefined, answerColumn: undefined };
47
+
48
+ const inner = (m[1] ?? '').trim();
49
+ if (!inner) return { questionColumn: undefined, answerColumn: undefined };
50
+
51
+ const parts = inner
52
+ .split(',')
53
+ .map((x) => x.trim())
54
+ .filter(Boolean);
55
+
56
+ return { questionColumn: parts[0], answerColumn: parts[1] };
57
+ }
58
+
59
+ function looksLikeQaExpression(expr?: string | null) {
60
+ const raw = String(expr ?? '').trim();
61
+ return /^([a-z]\.)?QA\s*\(/i.test(raw);
62
+ }
63
+
64
+ type QAValue = {
65
+ question?: string | number | null;
66
+ questions?: Array<string | number>;
67
+ answers?: Array<string | number>;
68
+ };
69
+
70
+ function isQAValue(v: any): v is QAValue {
71
+ return v && typeof v === 'object' && ('question' in v || 'questions' in v || 'answers' in v);
72
+ }
73
+
74
+ function normalizeArrayValue(v: any) {
75
+ const arr = Array.isArray(v) ? v : [v];
76
+ return arr.map((x) => String(x)).filter((x) => x.trim().length > 0);
77
+ }
78
+
79
+ export function buildSqlPreview(themeCfg: DataThemeConfig, excludeDateFilter: boolean = false, countDistinctPatientId: boolean = false) {
80
+ const src = themeCfg.sourceTable;
81
+ const pid = themeCfg.patientIdColumn;
82
+ const dateCol = themeCfg.dateColumn;
83
+
84
+ const lines: string[] = [];
85
+ lines.push(`SELECT`);
86
+ lines.push(` COUNT${countDistinctPatientId ? `(DISTINCT a.${pid})` : '(*)'} AS total`);
87
+ lines.push(`FROM ${src} a`);
88
+ lines.push(`JOIN ${DEMO_JOIN_TABLE} mdp`);
89
+ lines.push(` ON mdp.${pid} = a.${pid}`);
90
+ lines.push(`WHERE`);
91
+
92
+ // Build WHERE clause conditions
93
+ // - The first condition never needs AND
94
+ // - All subsequent conditions need AND prefix
95
+ if (!excludeDateFilter) {
96
+ // Date filtering enabled - date filters come first
97
+ lines.push(` a.${dateCol} >= :startDate`);
98
+ lines.push(` AND a.${dateCol} < :endDate`);
99
+ }
100
+
101
+ // Demographic filters (always present, conditionally add AND prefix)
102
+ const needsAnd = !excludeDateFilter; // Need AND if date filters were added
103
+ if (needsAnd) {
104
+ lines.push(` AND mdp.birthdate IS NOT NULL`);
105
+ lines.push(` AND mdp.gender IS NOT NULL`);
106
+ } else {
107
+ lines.push(` mdp.birthdate IS NOT NULL`);
108
+ lines.push(` AND mdp.gender IS NOT NULL`);
109
+ }
110
+
111
+ lines.push(`;`);
112
+
113
+ return lines.join('\n');
114
+ }
115
+
116
+ export function applyConditionClauses(baseSql: string, themeConditions: ThemeCondition[], picked: IndicatorCondition[]) {
117
+ const sqlLines = baseSql.split('\n');
118
+
119
+ // Find WHERE clause and insert conditions after the last condition
120
+ const whereIdx = sqlLines.findIndex((l) => l.trim() === 'WHERE');
121
+ let insertAt = sqlLines.length; // Default to end if no WHERE found
122
+
123
+ if (whereIdx >= 0) {
124
+ // Find the last condition line before empty line, semicolon, or other SQL clauses
125
+ for (let i = whereIdx + 1; i < sqlLines.length; i++) {
126
+ const line = sqlLines[i]?.trim();
127
+ // Stop at empty line, semicolon, JOIN, ORDER BY, GROUP BY, LIMIT, etc.
128
+ if (!line || line === ';' || line.startsWith('JOIN') || line.startsWith('ORDER BY') ||
129
+ line.startsWith('GROUP BY') || line.startsWith('LIMIT') || line.startsWith('HAVING')) {
130
+ insertAt = i;
131
+ break;
132
+ }
133
+ }
134
+ }
135
+
136
+ const clauses: string[] = [];
137
+
138
+ // All clauses get AND prefix since we're appending after existing conditions
139
+ const addClause = (clause: string) => {
140
+ clauses.push(` AND ${clause}`);
141
+ };
142
+
143
+ for (const tc of themeConditions ?? []) {
144
+ const pc = picked.find((x) => x.key === tc.key);
145
+ if (!pc) continue;
146
+
147
+ const v: any = pc.value;
148
+ if (v === null || v === undefined) continue;
149
+
150
+ // ✅ Normalize operator tokens from theme/UI
151
+ const op = normalizeOperator((pc.operator as any) ?? (tc.operator as any) ?? 'IN');
152
+
153
+ // QUESTION_ANSWER_CONCEPT_SEARCH
154
+ if (tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH' && isQAValue(v)) {
155
+ const qVals: any[] = Array.isArray(v.questions)
156
+ ? v.questions
157
+ : v.question !== null && v.question !== undefined && String(v.question).trim() !== ''
158
+ ? [v.question]
159
+ : [];
160
+
161
+ const av = Array.isArray(v.answers) ? v.answers : [];
162
+
163
+ let questionColumn = (tc as any)?.columns?.question ?? (tc as any).questionColumn;
164
+ let answerColumn = (tc as any)?.columns?.answer ?? (tc as any).answerColumn;
165
+
166
+ if ((!questionColumn || !answerColumn) && (tc as any)?.column) {
167
+ const parsed = parseQaColumnsFromExpr((tc as any).column);
168
+ questionColumn = questionColumn ?? parsed.questionColumn;
169
+ answerColumn = answerColumn ?? parsed.answerColumn;
170
+ }
171
+
172
+ const tcCol = (tc as any)?.column as string | undefined;
173
+ if (!questionColumn && tcCol) questionColumn = tcCol;
174
+ if (!answerColumn && tcCol && !looksLikeQaExpression(tcCol)) answerColumn = tcCol;
175
+
176
+ if (questionColumn && qVals.length) {
177
+ const arr = normalizeArrayValue(qVals);
178
+ if (arr.length) {
179
+ const isNumericList = tc.valueType === 'conceptId' || arr.every((x) => /^[0-9]+$/.test(x));
180
+ const rendered = arr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
181
+ const col = qualifyColumn(questionColumn);
182
+
183
+ // For questions we keep original behavior: single => =, many => IN (...)
184
+ if (arr.length === 1) addClause(`${col} = ${rendered}`);
185
+ else addClause(`${col} IN (${rendered})`);
186
+ }
187
+ }
188
+
189
+ if (answerColumn && Array.isArray(av) && av.length) {
190
+ const arr = normalizeArrayValue(av);
191
+ if (arr.length) {
192
+ // For concept answer columns, extract numeric IDs from UUIDs
193
+ const isConceptColumn = /diagnosis_coded|diagnosis|concept|coded/i.test(answerColumn || '');
194
+ const shouldExtractIds = isConceptColumn && (
195
+ tc.valueType === 'conceptUuid' ||
196
+ arr.some((x) => /^[0-9]+A+$/.test(String(x))) || arr.some((x) => String(x).length > 20)
197
+ );
198
+
199
+ const processedArr = shouldExtractIds
200
+ ? arr.map((x) => extractConceptIdFromUuid(String(x)))
201
+ : arr;
202
+
203
+ const isNumericList = tc.valueType === 'conceptId' || processedArr.every((x) => /^[0-9]+$/.test(x));
204
+ const rendered = processedArr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
205
+ const col = qualifyColumn(answerColumn);
206
+
207
+ // ✅ Always bracket IN/NOT IN
208
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${rendered})`);
209
+ else addClause(`${col} IN (${rendered})`);
210
+ }
211
+ }
212
+
213
+ continue;
214
+ }
215
+
216
+ // array values
217
+ if (Array.isArray(v)) {
218
+ const arr = normalizeArrayValue(v);
219
+ if (!arr.length) continue;
220
+
221
+ // For concept columns (diagnosis_coded, etc.), extract numeric IDs from UUIDs
222
+ // This handles both explicit conceptUuid valueType and auto-detection of UUIDs
223
+ const isConceptColumn = /diagnosis_coded|diagnosis|concept|coded/i.test(tc.column || '');
224
+ const shouldExtractIds = isConceptColumn && (
225
+ tc.valueType === 'conceptUuid' ||
226
+ arr.some((x) => /^[0-9]+A+$/.test(String(x))) || arr.some((x) => String(x).length > 20)
227
+ );
228
+
229
+ const processedArr = shouldExtractIds
230
+ ? arr.map((x) => extractConceptIdFromUuid(String(x)))
231
+ : arr;
232
+
233
+ const isNumericList = tc.valueType === 'conceptId' || processedArr.every((x) => /^[0-9]+$/.test(x));
234
+ const rendered = processedArr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
235
+
236
+ const col = qualifyColumn(tc.column);
237
+
238
+ // ✅ BETWEEN requires exactly 2 values: BETWEEN val1 AND val2
239
+ if (op === 'BETWEEN') {
240
+ if (arr.length === 2) {
241
+ const [v1, v2] = processedArr.map((x) => (isNumericList ? x : sqlQuote(x)));
242
+ addClause(`${col} BETWEEN ${v1} AND ${v2}`);
243
+ }
244
+ // If BETWEEN doesn't have exactly 2 values, skip it (invalid)
245
+ continue;
246
+ }
247
+
248
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${rendered})`);
249
+ else if (op === 'IN') addClause(`${col} IN (${rendered})`);
250
+ else addClause(`${col} ${op} (${rendered})`);
251
+
252
+ continue;
253
+ }
254
+
255
+ // ✅ BETWEEN with { start, end } object format
256
+ if (op === 'BETWEEN' && typeof v === 'object' && v !== null && 'start' in v && 'end' in v) {
257
+ const startVal = (v as any).start;
258
+ const endVal = (v as any).end;
259
+
260
+ if (startVal !== null && startVal !== undefined && startVal !== '' &&
261
+ endVal !== null && endVal !== undefined && endVal !== '') {
262
+
263
+ const isNumericStart = tc.valueType === 'conceptId' || /^[0-9]+$/.test(String(startVal));
264
+ const isNumericEnd = tc.valueType === 'conceptId' || /^[0-9]+$/.test(String(endVal));
265
+
266
+ const renderedStart = isNumericStart ? startVal : sqlQuote(String(startVal));
267
+ const renderedEnd = isNumericEnd ? endVal : sqlQuote(String(endVal));
268
+ const col = qualifyColumn(tc.column);
269
+
270
+ addClause(`${col} BETWEEN ${renderedStart} AND ${renderedEnd}`);
271
+ }
272
+ continue;
273
+ }
274
+
275
+ // other object not supported
276
+ if (typeof v === 'object') continue;
277
+
278
+ const col = qualifyColumn(tc.column);
279
+
280
+ // ✅ IS NULL / IS NOT NULL - no value needed
281
+ if (isNullCheckOperator(op)) {
282
+ addClause(`${col} ${op}`);
283
+ continue;
284
+ }
285
+
286
+ // scalar
287
+ const sval = String(v);
288
+ // Skip empty strings to avoid IN('') or similar invalid SQL
289
+ if (sval === '') continue;
290
+
291
+ const isNumeric = tc.valueType === 'conceptId' || /^[0-9]+$/.test(sval);
292
+ const renderedScalar = isNumeric ? sval : sqlQuote(sval);
293
+
294
+ // ✅ IN must always have brackets, even for scalar
295
+ if (isInOperator(op)) {
296
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${renderedScalar})`);
297
+ else addClause(`${col} IN (${renderedScalar})`);
298
+ } else if (op === 'BETWEEN') {
299
+ // BETWEEN with scalar is invalid (requires 2 values), skip it
300
+ continue;
301
+ } else {
302
+ addClause(`${col} ${op} ${renderedScalar}`);
303
+ }
304
+ }
305
+
306
+ sqlLines.splice(insertAt, 0, ...clauses);
307
+ return sqlLines.join('\n');
308
+ }
309
+
310
+ /**
311
+ * Async version of applyConditionClauses that fetches concept IDs from UUIDs.
312
+ * Use this when conditions contain concept UUIDs that need to be resolved to numeric IDs.
313
+ */
314
+ export async function applyConditionClausesAsync(
315
+ baseSql: string,
316
+ themeConditions: ThemeCondition[],
317
+ picked: IndicatorCondition[],
318
+ signal?: AbortSignal
319
+ ): Promise<string> {
320
+ const sqlLines = baseSql.split('\n');
321
+
322
+ // Find WHERE clause and insert conditions after the last condition
323
+ const whereIdx = sqlLines.findIndex((l) => l.trim() === 'WHERE');
324
+ let insertAt = sqlLines.length; // Default to end if no WHERE found
325
+
326
+ if (whereIdx >= 0) {
327
+ // Find the last condition line before empty line, semicolon, or other SQL clauses
328
+ for (let i = whereIdx + 1; i < sqlLines.length; i++) {
329
+ const line = sqlLines[i]?.trim();
330
+ // Stop at empty line, semicolon, JOIN, ORDER BY, GROUP BY, LIMIT, etc.
331
+ if (!line || line === ';' || line.startsWith('JOIN') || line.startsWith('ORDER BY') ||
332
+ line.startsWith('GROUP BY') || line.startsWith('LIMIT') || line.startsWith('HAVING')) {
333
+ insertAt = i;
334
+ break;
335
+ }
336
+ }
337
+ }
338
+
339
+ const clauses: string[] = [];
340
+
341
+ // All clauses get AND prefix since we're appending after existing conditions
342
+ const addClause = (clause: string) => {
343
+ clauses.push(` AND ${clause}`);
344
+ };
345
+
346
+ // Helper to resolve concept UUID to ID
347
+ const resolveConceptId = async (uuid: string): Promise<string> => {
348
+ const str = String(uuid).trim();
349
+ // First try to extract from OpenMRS UUID format (numeric prefix + A's)
350
+ const match = str.match(/^(\d+)A+$/);
351
+ if (match) return match[1];
352
+
353
+ // If it's not in OpenMRS UUID format, fetch from API
354
+ try {
355
+ const concept = await getConceptByUuid(str, signal);
356
+ return String(concept.id);
357
+ } catch (e) {
358
+ // If API call fails, fall back to original value
359
+ console.warn(`Failed to resolve concept UUID ${str}:`, e);
360
+ return str;
361
+ }
362
+ };
363
+
364
+ // Helper to resolve multiple UUIDs in parallel
365
+ const resolveConceptIds = async (values: string[]): Promise<string[]> => {
366
+ // First filter out values that are already numeric (no need to fetch)
367
+ const numericValues: string[] = [];
368
+ const uuidValues: string[] = [];
369
+
370
+ for (const v of values) {
371
+ const trimmed = v.trim();
372
+ // Check if it's already a number or in OpenMRS UUID format (can extract without API)
373
+ const match = trimmed.match(/^(\d+)A+$/);
374
+ if (match) {
375
+ numericValues.push(match[1]);
376
+ } else if (/^\d+$/.test(trimmed)) {
377
+ numericValues.push(trimmed);
378
+ } else {
379
+ uuidValues.push(trimmed);
380
+ }
381
+ }
382
+
383
+ // Fetch UUIDs in parallel
384
+ const resolved = await Promise.all(
385
+ uuidValues.map(uuid => resolveConceptId(uuid))
386
+ );
387
+
388
+ return [...numericValues, ...resolved];
389
+ };
390
+
391
+ for (const tc of themeConditions ?? []) {
392
+ const pc = picked.find((x) => x.key === tc.key);
393
+ if (!pc) continue;
394
+
395
+ const v: any = pc.value;
396
+ if (v === null || v === undefined) continue;
397
+
398
+ // Normalize operator tokens from theme/UI
399
+ const op = normalizeOperator((pc.operator as any) ?? (tc.operator as any) ?? 'IN');
400
+
401
+ // QUESTION_ANSWER_CONCEPT_SEARCH
402
+ if (tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH' && isQAValue(v)) {
403
+ const qVals: any[] = Array.isArray(v.questions)
404
+ ? v.questions
405
+ : v.question !== null && v.question !== undefined && String(v.question).trim() !== ''
406
+ ? [v.question]
407
+ : [];
408
+
409
+ const av = Array.isArray(v.answers) ? v.answers : [];
410
+
411
+ let questionColumn = (tc as any)?.columns?.question ?? (tc as any).questionColumn;
412
+ let answerColumn = (tc as any)?.columns?.answer ?? (tc as any).answerColumn;
413
+
414
+ if ((!questionColumn || !answerColumn) && (tc as any)?.column) {
415
+ const parsed = parseQaColumnsFromExpr((tc as any).column);
416
+ questionColumn = questionColumn ?? parsed.questionColumn;
417
+ answerColumn = answerColumn ?? parsed.answerColumn;
418
+ }
419
+
420
+ const tcCol = (tc as any)?.column as string | undefined;
421
+ if (!questionColumn && tcCol) questionColumn = tcCol;
422
+ if (!answerColumn && tcCol && !looksLikeQaExpression(tcCol)) answerColumn = tcCol;
423
+
424
+ if (questionColumn && qVals.length) {
425
+ const arr = normalizeArrayValue(qVals);
426
+ if (arr.length) {
427
+ // Resolve concept IDs if this is a concept column
428
+ const isConceptColumn = /question|concept|coded/i.test(questionColumn || '');
429
+ const shouldFetch = isConceptColumn && tc.valueType === 'conceptUuid';
430
+
431
+ const processedArr = shouldFetch
432
+ ? await resolveConceptIds(arr)
433
+ : arr;
434
+
435
+ const isNumericList = tc.valueType === 'conceptId' || processedArr.every((x) => /^[0-9]+$/.test(x));
436
+ const rendered = processedArr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
437
+ const col = qualifyColumn(questionColumn);
438
+
439
+ if (arr.length === 1) addClause(`${col} = ${rendered}`);
440
+ else addClause(`${col} IN (${rendered})`);
441
+ }
442
+ }
443
+
444
+ if (answerColumn && Array.isArray(av) && av.length) {
445
+ const arr = normalizeArrayValue(av);
446
+ if (arr.length) {
447
+ const isConceptColumn = /diagnosis_coded|diagnosis|concept|coded/i.test(answerColumn || '');
448
+ const shouldFetch = isConceptColumn && tc.valueType === 'conceptUuid';
449
+
450
+ const processedArr = shouldFetch
451
+ ? await resolveConceptIds(arr)
452
+ : arr;
453
+
454
+ const isNumericList = tc.valueType === 'conceptId' || processedArr.every((x) => /^[0-9]+$/.test(x));
455
+ const rendered = processedArr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
456
+ const col = qualifyColumn(answerColumn);
457
+
458
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${rendered})`);
459
+ else addClause(`${col} IN (${rendered})`);
460
+ }
461
+ }
462
+
463
+ continue;
464
+ }
465
+
466
+ // array values
467
+ if (Array.isArray(v)) {
468
+ const arr = normalizeArrayValue(v);
469
+ if (!arr.length) continue;
470
+
471
+ const isConceptColumn = /diagnosis_coded|diagnosis|concept|coded/i.test(tc.column || '');
472
+ const shouldFetch = isConceptColumn && tc.valueType === 'conceptUuid';
473
+
474
+ const processedArr = shouldFetch
475
+ ? await resolveConceptIds(arr)
476
+ : arr;
477
+
478
+ const isNumericList = tc.valueType === 'conceptId' || processedArr.every((x) => /^[0-9]+$/.test(x));
479
+ const rendered = processedArr.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
480
+
481
+ const col = qualifyColumn(tc.column);
482
+
483
+ if (op === 'BETWEEN') {
484
+ if (arr.length === 2) {
485
+ const [v1, v2] = processedArr.map((x) => (isNumericList ? x : sqlQuote(x)));
486
+ addClause(`${col} BETWEEN ${v1} AND ${v2}`);
487
+ }
488
+ continue;
489
+ }
490
+
491
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${rendered})`);
492
+ else if (op === 'IN') addClause(`${col} IN (${rendered})`);
493
+ else addClause(`${col} ${op} (${rendered})`);
494
+
495
+ continue;
496
+ }
497
+
498
+ // BETWEEN with { start, end } object format
499
+ if (op === 'BETWEEN' && typeof v === 'object' && v !== null && 'start' in v && 'end' in v) {
500
+ const startVal = (v as any).start;
501
+ const endVal = (v as any).end;
502
+
503
+ if (startVal !== null && startVal !== undefined && startVal !== '' &&
504
+ endVal !== null && endVal !== undefined && endVal !== '') {
505
+
506
+ const isConceptColumn = /diagnosis_coded|diagnosis|concept|coded/i.test(tc.column || '');
507
+ const shouldFetch = isConceptColumn && tc.valueType === 'conceptUuid';
508
+
509
+ let processedStart = String(startVal);
510
+ let processedEnd = String(endVal);
511
+
512
+ if (shouldFetch) {
513
+ const resolved = await resolveConceptIds([processedStart, processedEnd]);
514
+ processedStart = resolved[0];
515
+ processedEnd = resolved[1];
516
+ }
517
+
518
+ const isNumericStart = tc.valueType === 'conceptId' || /^[0-9]+$/.test(processedStart);
519
+ const isNumericEnd = tc.valueType === 'conceptId' || /^[0-9]+$/.test(processedEnd);
520
+
521
+ const renderedStart = isNumericStart ? processedStart : sqlQuote(processedStart);
522
+ const renderedEnd = isNumericEnd ? processedEnd : sqlQuote(processedEnd);
523
+ const col = qualifyColumn(tc.column);
524
+
525
+ addClause(`${col} BETWEEN ${renderedStart} AND ${renderedEnd}`);
526
+ }
527
+ continue;
528
+ }
529
+
530
+ // other object not supported
531
+ if (typeof v === 'object') continue;
532
+
533
+ const col = qualifyColumn(tc.column);
534
+
535
+ // IS NULL / IS NOT NULL - no value needed
536
+ if (isNullCheckOperator(op)) {
537
+ addClause(`${col} ${op}`);
538
+ continue;
539
+ }
540
+
541
+ // scalar
542
+ const sval = String(v);
543
+ if (sval === '') continue;
544
+
545
+ const isConceptColumn = /diagnosis_coded|diagnosis|concept|coded/i.test(tc.column || '');
546
+ const shouldFetch = isConceptColumn && tc.valueType === 'conceptUuid';
547
+
548
+ let processedScalar = sval;
549
+ if (shouldFetch) {
550
+ const resolved = await resolveConceptIds([sval]);
551
+ processedScalar = resolved[0];
552
+ }
553
+
554
+ const isNumeric = tc.valueType === 'conceptId' || /^[0-9]+$/.test(processedScalar);
555
+ const renderedScalar = isNumeric ? processedScalar : sqlQuote(processedScalar);
556
+
557
+ if (isInOperator(op)) {
558
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${renderedScalar})`);
559
+ else addClause(`${col} IN (${renderedScalar})`);
560
+ } else if (op === 'BETWEEN') {
561
+ continue;
562
+ } else {
563
+ addClause(`${col} ${op} ${renderedScalar}`);
564
+ }
565
+ }
566
+
567
+ sqlLines.splice(insertAt, 0, ...clauses);
568
+ return sqlLines.join('\n');
569
+ }
570
+
571
+ export type CustomCondition = {
572
+ id: string;
573
+ column: string;
574
+ operator: string;
575
+ value: string | string[] | boolean | { start: string; end: string };
576
+ wildcardMode?: 'none' | 'contains' | 'startsWith' | 'endsWith';
577
+ };
578
+
579
+ /**
580
+ * Applies custom user-defined conditions to the SQL.
581
+ * These conditions are defined by the user during indicator creation,
582
+ * separate from the theme-defined conditions.
583
+ */
584
+ export function applyCustomConditions(baseSql: string, customConditions: CustomCondition[]): string {
585
+ if (!customConditions || customConditions.length === 0) return baseSql;
586
+
587
+ const sqlLines = baseSql.split('\n');
588
+
589
+ // Find WHERE clause and insert conditions after the last condition
590
+ const whereIdx = sqlLines.findIndex((l) => l.trim() === 'WHERE');
591
+ let insertAt = sqlLines.length; // Default to end if no WHERE found
592
+
593
+ if (whereIdx >= 0) {
594
+ // Find the last condition line before empty line, semicolon, or other SQL clauses
595
+ for (let i = whereIdx + 1; i < sqlLines.length; i++) {
596
+ const line = sqlLines[i]?.trim();
597
+ // Stop at empty line, semicolon, JOIN, ORDER BY, GROUP BY, LIMIT, etc.
598
+ if (!line || line === ';' || line.startsWith('JOIN') || line.startsWith('ORDER BY') ||
599
+ line.startsWith('GROUP BY') || line.startsWith('LIMIT') || line.startsWith('HAVING')) {
600
+ insertAt = i;
601
+ break;
602
+ }
603
+ }
604
+ }
605
+
606
+ const clauses: string[] = [];
607
+
608
+ // All clauses get AND prefix since we're appending after existing conditions
609
+ const addClause = (clause: string) => {
610
+ clauses.push(` AND ${clause}`);
611
+ };
612
+
613
+ for (const cc of customConditions) {
614
+ const col = qualifyColumn(cc.column);
615
+ const op = normalizeOperator(cc.operator);
616
+ const v: any = cc.value;
617
+
618
+ // IS NULL / IS NOT NULL - no value needed
619
+ if (isNullCheckOperator(op)) {
620
+ if (v === true) {
621
+ addClause(`${col} ${op}`);
622
+ }
623
+ continue;
624
+ }
625
+
626
+ // Array values (IN, NOT IN)
627
+ if (Array.isArray(v)) {
628
+ if (v.length === 0) continue;
629
+
630
+ const isNumericList = v.every((x) => /^[0-9]+$/.test(String(x)));
631
+ const rendered = v.map((x) => (isNumericList ? x : sqlQuote(String(x)))).join(',');
632
+
633
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${rendered})`);
634
+ else if (op === 'IN') addClause(`${col} IN (${rendered})`);
635
+ else addClause(`${col} ${op} (${rendered})`);
636
+ continue;
637
+ }
638
+
639
+ // BETWEEN with { start, end } object or JSON string
640
+ let betweenObj: { start: string; end: string } | null = null;
641
+ if (typeof v === 'object' && v !== null && 'start' in v && 'end' in v) {
642
+ betweenObj = v as { start: string; end: string };
643
+ } else if (typeof v === 'string') {
644
+ try {
645
+ const parsed = JSON.parse(v);
646
+ if (parsed && typeof parsed === 'object' && 'start' in parsed && 'end' in parsed) {
647
+ betweenObj = parsed;
648
+ }
649
+ } catch {
650
+ // Not valid JSON, continue to other handlers
651
+ }
652
+ }
653
+
654
+ if (betweenObj) {
655
+ const startVal = betweenObj.start;
656
+ const endVal = betweenObj.end;
657
+
658
+ if (startVal !== null && startVal !== undefined && startVal !== '' &&
659
+ endVal !== null && endVal !== undefined && endVal !== '') {
660
+
661
+ const isNumericStart = /^[0-9]+$/.test(String(startVal));
662
+ const isNumericEnd = /^[0-9]+$/.test(String(endVal));
663
+
664
+ const renderedStart = isNumericStart ? startVal : sqlQuote(String(startVal));
665
+ const renderedEnd = isNumericEnd ? endVal : sqlQuote(String(endVal));
666
+
667
+ addClause(`${col} BETWEEN ${renderedStart} AND ${renderedEnd}`);
668
+ }
669
+ continue;
670
+ }
671
+
672
+ // Scalar values
673
+ const sval = String(v);
674
+ if (sval === '') continue;
675
+
676
+ // Check for comma-separated values in IN/NOT IN
677
+ if (isInOperator(op) && sval.includes(',')) {
678
+ const parts = sval.split(',').map((x) => x.trim()).filter(Boolean);
679
+ if (parts.length === 0) continue;
680
+
681
+ const isNumericList = parts.every((x) => /^[0-9]+$/.test(x));
682
+ const rendered = parts.map((x) => (isNumericList ? x : sqlQuote(x))).join(',');
683
+
684
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${rendered})`);
685
+ else addClause(`${col} IN (${rendered})`);
686
+ continue;
687
+ }
688
+
689
+ // Single scalar value
690
+ const isNumeric = /^[0-9]+$/.test(sval);
691
+ const renderedScalar = isNumeric ? sval : sqlQuote(sval);
692
+
693
+ if (isInOperator(op)) {
694
+ if (op === 'NOT IN') addClause(`${col} NOT IN (${renderedScalar})`);
695
+ else addClause(`${col} IN (${renderedScalar})`);
696
+ } else if (op === 'BETWEEN') {
697
+ // BETWEEN with scalar is invalid
698
+ continue;
699
+ } else if (op === 'LIKE') {
700
+ // Apply wildcard mode
701
+ let likeValue = sval;
702
+ if (cc.wildcardMode === 'contains') {
703
+ likeValue = `%${sval}%`;
704
+ } else if (cc.wildcardMode === 'startsWith') {
705
+ likeValue = `${sval}%`;
706
+ } else if (cc.wildcardMode === 'endsWith') {
707
+ likeValue = `%${sval}`;
708
+ }
709
+ // 'none' uses the value as-is
710
+ const renderedLike = sqlQuote(likeValue);
711
+ addClause(`${col} LIKE ${renderedLike}`);
712
+ } else {
713
+ addClause(`${col} ${op} ${renderedScalar}`);
714
+ }
715
+ }
716
+
717
+ if (clauses.length === 0) return baseSql;
718
+
719
+ sqlLines.splice(insertAt, 0, ...clauses);
720
+ return sqlLines.join('\n');
721
+ }