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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/.tx/config +12 -0
  5. package/LICENSE +401 -0
  6. package/README.md +336 -0
  7. package/__mocks__/react-i18next.js +12 -0
  8. package/dist/162.js +1 -0
  9. package/dist/162.js.map +1 -0
  10. package/dist/184.js +2 -0
  11. package/dist/184.js.LICENSE.txt +14 -0
  12. package/dist/184.js.map +1 -0
  13. package/dist/242.js +2 -0
  14. package/dist/242.js.LICENSE.txt +21 -0
  15. package/dist/242.js.map +1 -0
  16. package/dist/253.js +1 -0
  17. package/dist/253.js.map +1 -0
  18. package/dist/300.js +1 -0
  19. package/dist/311.js +1 -0
  20. package/dist/311.js.map +1 -0
  21. package/dist/469.js +2 -0
  22. package/dist/469.js.LICENSE.txt +45 -0
  23. package/dist/469.js.map +1 -0
  24. package/dist/478.js +2 -0
  25. package/dist/478.js.LICENSE.txt +9 -0
  26. package/dist/478.js.map +1 -0
  27. package/dist/539.js +1 -0
  28. package/dist/539.js.map +1 -0
  29. package/dist/540.js +2 -0
  30. package/dist/540.js.LICENSE.txt +9 -0
  31. package/dist/540.js.map +1 -0
  32. package/dist/669.js +1 -0
  33. package/dist/669.js.map +1 -0
  34. package/dist/710.js +1 -0
  35. package/dist/710.js.map +1 -0
  36. package/dist/729.js +1 -0
  37. package/dist/729.js.map +1 -0
  38. package/dist/838.js +2 -0
  39. package/dist/838.js.LICENSE.txt +9 -0
  40. package/dist/838.js.map +1 -0
  41. package/dist/917.js +1 -0
  42. package/dist/917.js.map +1 -0
  43. package/dist/961.js +2 -0
  44. package/dist/961.js.LICENSE.txt +19 -0
  45. package/dist/961.js.map +1 -0
  46. package/dist/main.js +1 -0
  47. package/dist/main.js.map +1 -0
  48. package/dist/openmrs-esm-report-builder.js +1 -0
  49. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +530 -0
  50. package/dist/openmrs-esm-report-builder.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/e2e/core/env.ts +5 -0
  53. package/e2e/core/global-setup.ts +34 -0
  54. package/e2e/core/test.ts +21 -0
  55. package/e2e/pages/app-shell.page.ts +34 -0
  56. package/e2e/pages/etl-sources.page.ts +35 -0
  57. package/e2e/pages/report-sections.page.ts +20 -0
  58. package/e2e/pages/section-preview.modal.ts +29 -0
  59. package/e2e/specs/etl-sources.spec.ts +51 -0
  60. package/e2e/specs/navigation.spec.ts +13 -0
  61. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  62. package/e2e/specs/section-preview.spec.ts +94 -0
  63. package/e2e/support/github/Dockerfile +22 -0
  64. package/e2e/support/github/Dockerfile.dockerignore +13 -0
  65. package/e2e/support/github/docker-compose.yml +31 -0
  66. package/e2e/support/github/run-e2e-docker-env.sh +131 -0
  67. package/e2e/support/github/spa-assemble-config.json +9 -0
  68. package/e2e/tsconfig.json +11 -0
  69. package/example.env +5 -0
  70. package/jest.config.js +28 -0
  71. package/linelist_report_builder_design_spec.md +816 -0
  72. package/package.json +111 -0
  73. package/playwright.config.ts +28 -0
  74. package/routes.json +22 -0
  75. package/src/__debug__/all-designs.test.tsx +30 -0
  76. package/src/admin-card-link.extension.module.scss +55 -0
  77. package/src/admin-card-link.extension.tsx +24 -0
  78. package/src/components/admin/admin-page.component.tsx +75 -0
  79. package/src/components/admin/age-categories-page.component.tsx +109 -0
  80. package/src/components/admin/age-groups-page.component.tsx +146 -0
  81. package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
  82. package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
  83. package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
  84. package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
  85. package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
  86. package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
  87. package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
  88. package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
  89. package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
  90. package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
  91. package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
  92. package/src/components/admin/etl-sources-page.component.tsx +355 -0
  93. package/src/components/admin/etl-tasks-page.component.scss +370 -0
  94. package/src/components/admin/etl-tasks-page.component.tsx +768 -0
  95. package/src/components/admin/report-categories-page.component.tsx +267 -0
  96. package/src/components/admin/report-library-page.component.tsx +551 -0
  97. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  98. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  99. package/src/components/ai-support/ai.service.ts +21 -0
  100. package/src/components/ai-support/ai.types.ts +12 -0
  101. package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
  102. package/src/components/app-shell/report-builder-shell.scss +18 -0
  103. package/src/components/dashboards/dashboard-config.schema.ts +97 -0
  104. package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
  105. package/src/components/dashboards/dashboards-admin.scss +62 -0
  106. package/src/components/dashboards/dashboards-page.component.tsx +153 -0
  107. package/src/components/dashboards/dashboards-table.component.tsx +130 -0
  108. package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
  109. package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
  110. package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
  111. package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
  112. package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
  113. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  114. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  115. package/src/components/data-themes/data-themes-page.component.tsx +139 -0
  116. package/src/components/data-themes/data-themes-table.component.tsx +98 -0
  117. package/src/components/data-themes/icon-registry.ts +35 -0
  118. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  119. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  120. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  121. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  122. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  123. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  124. package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
  125. package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
  126. package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
  127. package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
  128. package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
  129. package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
  130. package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
  131. package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
  132. package/src/components/data-visualizer/components/header/header.scss +71 -0
  133. package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
  134. package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
  135. package/src/components/data-visualizer/components/panel/panel.scss +25 -0
  136. package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
  137. package/src/components/data-visualizer/constants.ts +445 -0
  138. package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
  139. package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
  140. package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
  141. package/src/components/data-visualizer/data-visualizer.scss +449 -0
  142. package/src/components/data-visualizer/declarations.d.ts +131 -0
  143. package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
  144. package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
  145. package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
  146. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
  147. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
  148. package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
  149. package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
  150. package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
  151. package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
  152. package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
  153. package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
  154. package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
  155. package/src/components/data-visualizer/types.ts +206 -0
  156. package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
  157. package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
  158. package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
  159. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
  160. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
  161. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
  162. package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
  163. package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
  164. package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
  165. package/src/components/etl-monitor/builder/index.ts +5 -0
  166. package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
  167. package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
  168. package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
  169. package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
  170. package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
  171. package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
  172. package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
  173. package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
  174. package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
  175. package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
  176. package/src/components/etl-monitor/builder/steps/index.ts +5 -0
  177. package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
  178. package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
  179. package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
  180. package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
  181. package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
  182. package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
  183. package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
  184. package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
  185. package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
  186. package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
  187. package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
  188. package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
  189. package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
  190. package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
  191. package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
  192. package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
  193. package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
  194. package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
  195. package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
  196. package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
  197. package/src/components/etl-monitor/renderers/components/index.ts +6 -0
  198. package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
  199. package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
  200. package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
  201. package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
  202. package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
  203. package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
  204. package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
  205. package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
  206. package/src/components/etl-monitor/renderers/index.ts +11 -0
  207. package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
  208. package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
  209. package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
  210. package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
  211. package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
  212. package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
  213. package/src/components/import-export/artifact-groups.ts +153 -0
  214. package/src/components/import-export/export-report-modal.component.tsx +265 -0
  215. package/src/components/import-export/import-export-page.component.tsx +610 -0
  216. package/src/components/import-export/import-export.styles.scss +825 -0
  217. package/src/components/import-export/import-package-modal.component.tsx +401 -0
  218. package/src/components/import-export/index.ts +3 -0
  219. package/src/components/indicators/README.md +24 -0
  220. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  221. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  222. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  223. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  224. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  225. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  226. package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
  227. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
  228. package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
  229. package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
  230. package/src/components/indicators/custom-condition-input.component.tsx +310 -0
  231. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  232. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  233. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  234. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  235. package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
  236. package/src/components/indicators/indicator-search-select.component.tsx +297 -0
  237. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  238. package/src/components/indicators/indicators-page.component.tsx +673 -0
  239. package/src/components/indicators/indicators-page.scss +121 -0
  240. package/src/components/indicators/indicators-table.component.tsx +188 -0
  241. package/src/components/indicators/preview-table.component.tsx +31 -0
  242. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  243. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
  244. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  245. package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
  246. package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
  247. package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
  248. package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
  249. package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
  250. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  251. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  252. package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
  253. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  254. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  255. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  256. package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
  257. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
  258. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  259. package/src/components/indicators/sql-preview.component.tsx +19 -0
  260. package/src/components/indicators/tag-input.component.tsx +177 -0
  261. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  262. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  263. package/src/components/indicators/types/composite-indicator.types.ts +27 -0
  264. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  265. package/src/components/indicators/types/custom-indicator.types.ts +161 -0
  266. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  267. package/src/components/indicators/types/indicator-types.ts +57 -0
  268. package/src/components/indicators/types/sql-builders.ts +43 -0
  269. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
  270. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
  271. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  272. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
  273. package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
  274. package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
  275. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  276. package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
  277. package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
  278. package/src/components/indicators/utils/population-sql.compiler.test.ts +999 -0
  279. package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
  280. package/src/components/landing/landing-action-item.component.tsx +50 -0
  281. package/src/components/landing/landing-action-list.component.tsx +12 -0
  282. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  283. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  284. package/src/components/landing/landing-hero.component.tsx +18 -0
  285. package/src/components/landing/landing-section-card.component.tsx +33 -0
  286. package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
  287. package/src/components/landing/report-builder-landing-page.scss +131 -0
  288. package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
  289. package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
  290. package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
  291. package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
  292. package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
  293. package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
  294. package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
  295. package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
  296. package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
  297. package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
  298. package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
  299. package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
  300. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
  301. package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
  302. package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
  303. package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
  304. package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
  305. package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
  306. package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
  307. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
  308. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
  309. package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
  310. package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
  311. package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
  312. package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
  313. package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
  314. package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
  315. package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
  316. package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
  317. package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
  318. package/src/components/linelist/design/panels/column-category-selector.scss +329 -0
  319. package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
  320. package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
  321. package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
  322. package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
  323. package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
  324. package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
  325. package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
  326. package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
  327. package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
  328. package/src/components/linelist/design/panels/json-preview.scss +85 -0
  329. package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
  330. package/src/components/linelist/design/panels/query-config-panel.scss +342 -0
  331. package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
  332. package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
  333. package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
  334. package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
  335. package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
  336. package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
  337. package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
  338. package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
  339. package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
  340. package/src/components/linelist/linelist-dashboard.page.scss +240 -0
  341. package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
  342. package/src/components/linelist/linelist-editor.page.scss +28 -0
  343. package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
  344. package/src/components/linelist/linelist-run-report.page.scss +136 -0
  345. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
  346. package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
  347. package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
  348. package/src/components/report/dashboard/reports-table.component.tsx +186 -0
  349. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  350. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  351. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  352. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  353. package/src/components/report/definition/report-definition.types.ts +66 -0
  354. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  355. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  356. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  357. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  358. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  359. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  360. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  361. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  362. package/src/components/report/design/report-design-editor.component.tsx +1170 -0
  363. package/src/components/report/design/report-design.types.ts +48 -0
  364. package/src/components/report/design/report-design.utils.ts +86 -0
  365. package/src/components/report/panels/report-detail.component.tsx +71 -0
  366. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  367. package/src/components/report/report-dashboard.page.component.tsx +554 -0
  368. package/src/components/report/report-editor.page.component.tsx +590 -0
  369. package/src/components/report/report.routes.tsx +32 -0
  370. package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
  371. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  372. package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
  373. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  374. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  375. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
  376. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  377. package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
  378. package/src/components/report-sections/report-sections-page.component.tsx +325 -0
  379. package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
  380. package/src/components/report-sections/section-types.ts +55 -0
  381. package/src/components/report-sections/section-utils.ts +50 -0
  382. package/src/components/report-sections/sections-page.scss +79 -0
  383. package/src/components/run-reports/run-reports-page.component.tsx +8 -0
  384. package/src/components/security/access-denied-page.component.tsx +69 -0
  385. package/src/components/security/access-denied-page.scss +39 -0
  386. package/src/components/security/privilege-route.component.test.tsx +79 -0
  387. package/src/components/security/privilege-route.component.tsx +39 -0
  388. package/src/components/shared/compile-setup-modal.component.tsx +182 -0
  389. package/src/components/shared/concept-selector.component.tsx +245 -0
  390. package/src/components/shared/concept-selector.scss +121 -0
  391. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  392. package/src/components/shared/header/header.component.tsx +85 -0
  393. package/src/components/shared/header/header.scss +150 -0
  394. package/src/components/shared/header/illustration.component.tsx +34 -0
  395. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  396. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  397. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  398. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  399. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  400. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  401. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  402. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  403. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  404. package/src/config-schema.ts +43 -0
  405. package/src/constants/privileges.test.ts +53 -0
  406. package/src/constants/privileges.ts +118 -0
  407. package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
  408. package/src/features/dashboards/DashboardsPage.tsx +164 -0
  409. package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
  410. package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
  411. package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
  412. package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
  413. package/src/features/dashboards/components/DashboardSection.tsx +46 -0
  414. package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
  415. package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
  416. package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
  417. package/src/features/dashboards/dashboards.scss +196 -0
  418. package/src/features/dashboards/hooks/useDashboard.ts +61 -0
  419. package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
  420. package/src/features/dashboards/hooks/useDashboards.ts +56 -0
  421. package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
  422. package/src/features/dashboards/index.ts +11 -0
  423. package/src/features/dashboards/utils/dashboard-config.util.test.ts +255 -0
  424. package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
  425. package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
  426. package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
  427. package/src/features/report-visualizer/components/Export/index.ts +2 -0
  428. package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
  429. package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
  430. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
  431. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
  432. package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
  433. package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
  434. package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
  435. package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
  436. package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
  437. package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
  438. package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
  439. package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
  440. package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
  441. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
  442. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
  443. package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
  444. package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
  445. package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
  446. package/src/features/report-visualizer/components/TableView/index.ts +2 -0
  447. package/src/features/report-visualizer/hooks/index.ts +4 -0
  448. package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
  449. package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
  450. package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
  451. package/src/features/report-visualizer/hooks/useReports.ts +135 -0
  452. package/src/features/report-visualizer/index.ts +6 -0
  453. package/src/features/report-visualizer/report-visualizer.scss +593 -0
  454. package/src/features/report-visualizer/types/index.ts +1 -0
  455. package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
  456. package/src/features/report-visualizer/utils/status-colors.ts +117 -0
  457. package/src/globals.d.ts +14 -0
  458. package/src/hooks/address-template.ts +86 -0
  459. package/src/hooks/concept.ts +112 -0
  460. package/src/hooks/indicator/index.ts +1 -0
  461. package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
  462. package/src/hooks/openmrs-reference-data.ts +432 -0
  463. package/src/hooks/theme/index.ts +4 -0
  464. package/src/hooks/theme/useDataTheme.ts +61 -0
  465. package/src/hooks/theme/useDataThemes.ts +28 -0
  466. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  467. package/src/hooks/theme/useMambaTables.ts +95 -0
  468. package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
  469. package/src/hooks/use-report-builder-privileges.ts +58 -0
  470. package/src/index.ts +29 -0
  471. package/src/resources/address-template/address-template.api.ts +180 -0
  472. package/src/resources/agegroup/agegroups.api.ts +58 -0
  473. package/src/resources/concept/concept.api.ts +116 -0
  474. package/src/resources/concepts/concept-mapper.ts +21 -0
  475. package/src/resources/concepts/concept-types.ts +31 -0
  476. package/src/resources/concepts/concepts.resource.ts +44 -0
  477. package/src/resources/concepts/useConceptSearch.ts +41 -0
  478. package/src/resources/dashboard/dashboard.api.ts +107 -0
  479. package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
  480. package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
  481. package/src/resources/etl-source/etl-source.api.ts +65 -0
  482. package/src/resources/etl-task/etl-task.api.ts +96 -0
  483. package/src/resources/etl-task/etl-task.types.ts +31 -0
  484. package/src/resources/indicator/indicators.api.ts +125 -0
  485. package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
  486. package/src/resources/linelist/linelist-reports.api.ts +616 -0
  487. package/src/resources/location/location.api.ts +135 -0
  488. package/src/resources/openmrs-api.ts +40 -0
  489. package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
  490. package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
  491. package/src/resources/preview/sql-preview.api.ts +54 -0
  492. package/src/resources/report/reports.api.ts +140 -0
  493. package/src/resources/report-category/report-category.api.ts +41 -0
  494. package/src/resources/report-import-export/import-export-api.ts +164 -0
  495. package/src/resources/report-library/report-library.api.ts +63 -0
  496. package/src/resources/report-section/report-sections.api.ts +73 -0
  497. package/src/resources/report-section/section-preview.api.ts +25 -0
  498. package/src/resources/theme/data-theme.api.ts +88 -0
  499. package/src/resources/theme/etl-schema.api.ts +101 -0
  500. package/src/resources/theme/etl-table-meta.api.ts +45 -0
  501. package/src/root.component.tsx +20 -0
  502. package/src/routes/report-builder.routes.tsx +160 -0
  503. package/src/routes/report-builder.scss +56 -0
  504. package/src/routes.json +28 -0
  505. package/src/sample-template.ts +121 -0
  506. package/src/template-utils.ts +110 -0
  507. package/src/types/carbon-react-inert.d.ts +13 -0
  508. package/src/types/condition-operators.ts +63 -0
  509. package/src/types/dashboard/dashboard.types.ts +137 -0
  510. package/src/types/dashboard/index.ts +4 -0
  511. package/src/types/etl/etl-types.ts +238 -0
  512. package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
  513. package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
  514. package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
  515. package/src/types/etl-monitor/index.ts +6 -0
  516. package/src/types/linelist/compile-config.ts +455 -0
  517. package/src/types/linelist-types.ts +2059 -0
  518. package/src/types/theme/data-theme.types.ts +88 -0
  519. package/src/utils/api-error.utils.test.ts +151 -0
  520. package/src/utils/api-error.utils.ts +131 -0
  521. package/src/utils/config-enhancer.ts +62 -0
  522. package/src/utils/etl-browser/etl-browser.util.test.ts +145 -0
  523. package/src/utils/etl-browser/export-csv.util.ts +43 -0
  524. package/src/utils/etl-browser/group-tables.util.ts +84 -0
  525. package/src/utils/etl-browser/index.ts +4 -0
  526. package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
  527. package/src/utils/etl-browser/table-sql.util.ts +20 -0
  528. package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
  529. package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
  530. package/src/utils/etl-monitor/index.ts +10 -0
  531. package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
  532. package/src/utils/etl-monitor/normalization-examples.ts +0 -0
  533. package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
  534. package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
  535. package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
  536. package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
  537. package/src/utils/etl-task/etl-task-card.util.test.ts +239 -0
  538. package/src/utils/etl-task/etl-task-card.util.ts +276 -0
  539. package/src/utils/etl-task/index.ts +1 -0
  540. package/src/utils/html-entities.utils.ts +34 -0
  541. package/src/utils/parameter-resolution.ts +218 -0
  542. package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
  543. package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
  544. package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
  545. package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
  546. package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
  547. package/tools/scss-stub.js +12 -0
  548. package/tools/setup-tests.ts +1 -0
  549. package/translations/en.json +65 -0
  550. package/tsconfig.json +43 -0
  551. package/webpack.config.js +30 -0
@@ -0,0 +1,702 @@
1
+ /**
2
+ * Custom Indicator Query Interpreter
3
+ *
4
+ * Analyzes and processes custom indicator SQL to enable:
5
+ * 1. Population SQL extraction for section disaggregation
6
+ * 2. Filter logic preservation
7
+ * 3. Query structure analysis
8
+ * 4. Configuration validation
9
+ */
10
+
11
+ import type {
12
+ CustomIndicatorConfig,
13
+ PatientIdColumn,
14
+ QueryStructureAnalysis,
15
+ FilterPreservationRule
16
+ } from '../types/custom-indicator.types';
17
+
18
+ // Re-export types for convenience
19
+ export type { CustomIndicatorConfig, PatientIdColumn, QueryStructureAnalysis, FilterPreservationRule };
20
+
21
+ /**
22
+ * Result of population SQL extraction
23
+ */
24
+ export type PopulationExtractionResult = {
25
+ /** The extracted population SQL */
26
+ sql: string;
27
+ /** Whether extraction was successful */
28
+ success: boolean;
29
+ /** Any warnings or issues */
30
+ warnings?: string[];
31
+ /** The detected patient ID column */
32
+ patientIdColumn?: PatientIdColumn;
33
+ };
34
+
35
+ /**
36
+ * Query validation result
37
+ */
38
+ export type ValidationResult = {
39
+ /** Whether the configuration is valid */
40
+ valid: boolean;
41
+ /** Validation errors */
42
+ errors: string[];
43
+ /** Validation warnings */
44
+ warnings: string[];
45
+ /** Information messages */
46
+ info: string[];
47
+ };
48
+
49
+ /**
50
+ * Custom Indicator Query Interpreter
51
+ *
52
+ * Analyzes complex custom indicator SQL and enables section-level re-disaggregation
53
+ * while preserving business logic and filters.
54
+ */
55
+ export class CustomIndicatorInterpreter {
56
+ /**
57
+ * Analyze custom indicator SQL and extract metadata
58
+ *
59
+ * @param sql - The SQL template to analyze
60
+ * @param config - Optional custom indicator configuration
61
+ * @returns Query structure analysis
62
+ */
63
+ analyze(sql: string, config?: CustomIndicatorConfig): QueryStructureAnalysis {
64
+ const result: QueryStructureAnalysis = {
65
+ patientIdColumn: config?.patientIdColumn || this.detectPatientIdColumn(sql),
66
+ hasPopulationQuery: false,
67
+ filters: [],
68
+ joinStructure: [],
69
+ redisaggregatable: false,
70
+ confidence: 0
71
+ };
72
+
73
+ // Detect population query structure
74
+ const populationPatterns = [
75
+ // Pattern 1: COUNT DISTINCT with FROM subquery
76
+ /SELECT\s+COUNT\s*\(\s*DISTINCT\s+(\w+)\.?(\w+)\s*\)\s*FROM\s*\(\s*SELECT/i,
77
+ // Pattern 2: Simple subquery with GROUP BY
78
+ /FROM\s*\(\s*SELECT\s+.*?GROUP\s+BY\s+(client_id|patient_id|person_id)/i,
79
+ // Pattern 3: Direct population query
80
+ /SELECT\s+DISTINCT\s+(client_id|patient_id|person_id)/i
81
+ ];
82
+
83
+ for (const pattern of populationPatterns) {
84
+ if (pattern.test(sql)) {
85
+ result.hasPopulationQuery = true;
86
+ result.confidence += 0.3;
87
+ break;
88
+ }
89
+ }
90
+
91
+ // Extract JOINs
92
+ const joinPatterns = [
93
+ { type: 'INNER JOIN', pattern: /INNER\s+JOIN\s+(\w+(?:\s+\w+)?)\s+(?:AS\s+)?(\w+)\s+ON\s+([^\n]+)/gi },
94
+ { type: 'LEFT JOIN', pattern: /LEFT\s+(?:OUTER\s+)?JOIN\s+(\w+(?:\s+\w+)?)\s+(?:AS\s+)?(\w+)\s+ON\s+([^\n]+)/gi },
95
+ { type: 'CROSS JOIN', pattern: /CROSS\s+JOIN\s+(\w+(?:\s+\w+)?)\s+(?:AS\s+)?(\w+)/gi }
96
+ ];
97
+
98
+ for (const { type, pattern } of joinPatterns) {
99
+ let match;
100
+ while ((match = pattern.exec(sql)) !== null) {
101
+ result.joinStructure.push({
102
+ type: type as any,
103
+ table: match[1]?.trim() || '',
104
+ alias: match[2]?.trim() || '',
105
+ condition: match[3]?.trim() || ''
106
+ });
107
+ result.confidence += 0.1;
108
+ }
109
+ }
110
+
111
+ // Detect filters
112
+ const filterPatterns = [
113
+ // Death filter
114
+ { name: 'death_filter', pattern: /p\.dead\s*=\s*1/i, type: 'WHERE' },
115
+ // Transfer out filter
116
+ { name: 'transfer_out_filter', pattern: /mfto\.client_id\s+IS\s+NULL/i, type: 'WHERE' },
117
+ // Lost to follow-up filter
118
+ { name: 'lost_filter', pattern: /ltfp_days\s*>\s*(?:\d+)/i, type: 'WHERE' },
119
+ // Prison filter
120
+ { name: 'prison_filter', pattern: /special_category\s*=\s*'In prison'/i, type: 'INNER JOIN' }
121
+ ];
122
+
123
+ for (const { name, pattern, type } of filterPatterns) {
124
+ if (pattern.test(sql)) {
125
+ result.filters.push({ name, type: type as any, pattern: pattern.source });
126
+ result.confidence += 0.05;
127
+ }
128
+ }
129
+
130
+ // Determine re-disaggregatability
131
+ result.redisaggregatable =
132
+ result.hasPopulationQuery &&
133
+ result.confidence >= 0.5 &&
134
+ result.patientIdColumn !== undefined;
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Extract population SQL for section disaggregation
141
+ *
142
+ * @param sql - The SQL template to process
143
+ * @param config - Custom indicator configuration
144
+ * @returns Extracted population SQL or error message
145
+ */
146
+ extractPopulationSql(sql: string, config?: CustomIndicatorConfig): PopulationExtractionResult {
147
+ const result: PopulationExtractionResult = {
148
+ sql: '',
149
+ success: false,
150
+ warnings: []
151
+ };
152
+
153
+ console.log('🔍 [Interpreter] Extracting population SQL:', {
154
+ sqlLength: sql?.length || 0,
155
+ sqlPreview: sql?.substring(0, 200) || 'EMPTY',
156
+ config: config,
157
+ fullSql: sql?.substring(0, 500) || 'EMPTY'
158
+ });
159
+
160
+ if (!sql || !sql.trim()) {
161
+ result.warnings?.push('CUSTOM_POPULATION_MISSING: Indicator has supportsRedisaggregation=true but no population SQL source');
162
+ result.warnings?.push('Expected one of: populationSql, sqlTemplate, or sqlPreview in configJson');
163
+ console.error('❌ [Interpreter] CUSTOM_POPULATION_MISSING: Empty SQL provided');
164
+ return result;
165
+ }
166
+
167
+ // Decode HTML entities (common in database-stored SQL)
168
+ let processedSql = sql
169
+ .replace(/&amp;lt;/g, '<')
170
+ .replace(/&amp;gt;/g, '>')
171
+ .replace(/&amp;amp;/g, '&')
172
+ .replace(/&lt;/g, '<')
173
+ .replace(/&gt;/g, '>')
174
+ .replace(/&amp;/g, '&')
175
+ .replace(/\\n/g, '\n')
176
+ .trim();
177
+
178
+ // Fix quoted parameters - convert ':parameter' to :parameter
179
+ // Some legacy SQL uses quotes around named parameters
180
+ processedSql = processedSql.replace(/':(\w+)'/g, ':$1');
181
+
182
+ // Pattern 0: TX-RTT style with age_group/sex in SELECT, followed by COUNT(DISTINCT)
183
+ // SELECT ... AS age_group, ... AS sex, COUNT(DISTINCT a.client_id) AS value FROM (...) alias ...
184
+ // This handles indicators like PWIDS that have additional SELECT columns before the COUNT
185
+ // Require GROUP BY client_id to avoid stopping at nested function calls like TIMESTAMPDIFF(...)
186
+ //
187
+ // We use balanced parenthesis matching to handle nested subqueries correctly
188
+ const txRttDisaggregatedPattern =
189
+ /SELECT\s+[\s\S]*?,\s*COUNT\s*\(\s*DISTINCT\s+(\w+)\.(\w+)\s*\)\s*(?:AS\s+\w+)?\s+FROM\s*\(\s*SELECT\s+client_id/i;
190
+ const txRttDisaggregatedMatch = processedSql.match(txRttDisaggregatedPattern);
191
+
192
+ if (txRttDisaggregatedMatch) {
193
+ console.log('✅ [Pattern 0] Detected TX-RTT disaggregated query with age_group/sex');
194
+ const columnName = txRttDisaggregatedMatch[2];
195
+
196
+ // Find the position after "FROM ("
197
+ const afterFromIndex = txRttDisaggregatedMatch.index + txRttDisaggregatedMatch[0].length;
198
+
199
+ // Use balanced parenthesis matching to get the complete inner query
200
+ const balancedMatch = this.matchBalancedParens(processedSql, afterFromIndex);
201
+
202
+ if (balancedMatch) {
203
+ const innerQuery = `SELECT client_id${balancedMatch.text}`.trim();
204
+ const afterInnerQuery = balancedMatch.endIndex;
205
+
206
+ // Extract the outer alias (should be right after the closing paren)
207
+ const aliasPattern = /\s*(\w+)([\s\S]*)/i;
208
+ const aliasMatch = processedSql.substring(afterInnerQuery).match(aliasPattern);
209
+
210
+ if (aliasMatch) {
211
+ const outerAlias = aliasMatch[1];
212
+ const restOfQuery = aliasMatch[2] || '';
213
+
214
+ result.patientIdColumn = (columnName === 'client_id' || columnName === 'patient_id' || columnName === 'person_id')
215
+ ? columnName as PatientIdColumn
216
+ : 'client_id';
217
+
218
+ // Build population SQL by converting COUNT DISTINCT to SELECT DISTINCT
219
+ const cleanedRest = restOfQuery.replace(/GROUP\s+BY\s+[\s\S]*?;?\s*$/i, '').trim();
220
+
221
+ const populationQuery = `SELECT DISTINCT ${outerAlias}.${columnName} AS patient_id
222
+ FROM (
223
+ ${this.indent(innerQuery, 2)}
224
+ ) ${outerAlias}
225
+ ${cleanedRest}`.trim();
226
+
227
+ result.sql = populationQuery;
228
+ result.success = true;
229
+ result.warnings?.push('Extracted population SQL from TX-RTT disaggregated query');
230
+ console.log('📊 [Pattern 0] Generated population SQL, length:', populationQuery.length);
231
+ return result;
232
+ }
233
+ }
234
+ }
235
+
236
+
237
+ // Pattern 1: COUNT DISTINCT with FROM subquery that has JOINs
238
+ // Handles both:
239
+ // - SELECT COUNT(DISTINCT a.client_id) FROM (SELECT ...) a
240
+ // - SELECT 'PWIDS', COUNT(DISTINCT a.client_id) FROM (SELECT ...) a
241
+ // This is the most common pattern for CUSTOM indicators like TX_RTT, TX_ML
242
+ //
243
+ // We use balanced parenthesis matching to handle nested subqueries correctly
244
+ const countDistinctPattern =
245
+ /SELECT\s+(?:'[^']+'(?:\s*,\s*)?)?COUNT\s*\(\s*DISTINCT\s+(\w+)\.(\w+)\s*\)\s*(?:AS\s+\w+)?\s*FROM\s*\(\s*/i;
246
+ const countDistinctMatch = processedSql.match(countDistinctPattern);
247
+
248
+ if (countDistinctMatch) {
249
+ const tableAlias = countDistinctMatch[1];
250
+ const columnName = countDistinctMatch[2];
251
+
252
+ // Find the position after "FROM ("
253
+ const afterFromIndex = countDistinctMatch.index + countDistinctMatch[0].length;
254
+
255
+ // Use balanced parenthesis matching to get the complete inner query
256
+ const balancedMatch = this.matchBalancedParens(processedSql, afterFromIndex);
257
+
258
+ if (balancedMatch) {
259
+ const innerQuery = balancedMatch.text.trim();
260
+ const afterInnerQuery = balancedMatch.endIndex;
261
+
262
+ // Extract the outer alias (should be right after the closing paren)
263
+ const aliasPattern = /\s*(\w+)([\s\S]*)/i;
264
+ const aliasMatch = processedSql.substring(afterInnerQuery).match(aliasPattern);
265
+
266
+ if (aliasMatch) {
267
+ const outerAlias = aliasMatch[1];
268
+ const restOfQuery = aliasMatch[2] || '';
269
+
270
+ console.log('🔍 [Pattern 1] COUNT DISTINCT matched:', {
271
+ tableAlias,
272
+ columnName,
273
+ outerAlias,
274
+ hasInnerQuery: !!innerQuery,
275
+ innerQueryLength: innerQuery.length,
276
+ restLength: restOfQuery.length
277
+ });
278
+
279
+ // Determine the patient ID column from the COUNT expression
280
+ // This normalizes client_id/patient_id/person_id to a standard patient_id
281
+ result.patientIdColumn = (columnName === 'client_id' || columnName === 'patient_id' || columnName === 'person_id')
282
+ ? columnName as PatientIdColumn
283
+ : 'client_id';
284
+
285
+ // Build population SQL by converting COUNT DISTINCT to SELECT DISTINCT
286
+ // We preserve ALL the business logic:
287
+ // 1. The inner query (derived table) with all its logic
288
+ // 2. All JOINs from the outer query
289
+ // 3. All WHERE conditions from the outer query
290
+ //
291
+ // The only thing we change is the outer projection:
292
+ // FROM: SELECT COUNT(DISTINCT a.client_id)
293
+ // TO: SELECT DISTINCT a.client_id AS patient_id
294
+ //
295
+ // This ensures we get patient-level rows, not a scalar count
296
+
297
+ // Remove trailing GROUP BY if present (legacy artifacts)
298
+ const cleanedRest = restOfQuery.replace(/GROUP\s+BY\s+[\s\S]*?;?\s*$/i, '').trim();
299
+
300
+ const populationQuery = `SELECT DISTINCT ${outerAlias}.${columnName} AS patient_id
301
+ FROM (
302
+ ${this.indent(innerQuery, 2)}
303
+ ) ${outerAlias}
304
+ ${cleanedRest}`.trim();
305
+
306
+ result.sql = populationQuery;
307
+ result.success = true;
308
+ result.warnings?.push('Extracted population SQL from COUNT DISTINCT query');
309
+ console.log('✅ [Pattern 1] Generated population SQL, length:', populationQuery.length);
310
+ return result;
311
+ }
312
+ }
313
+ }
314
+
315
+ // Pattern 2: Simple population query (already in correct format)
316
+ // Handles both:
317
+ // - SELECT DISTINCT a.client_id AS patient_id FROM ...
318
+ // - SELECT DISTINCT a.client_id FROM (...) a ...
319
+ // This is for indicators like PIPS and sqlPreview that already have population SQL structure
320
+ const populationQueryPatterns = [
321
+ // Pattern 2a: With AS alias (SELECT DISTINCT a.client_id AS patient_id FROM ...)
322
+ /SELECT\s+DISTINCT\s+(\w+\.\w+)\s+AS\s+(?:patient_id|client_id)(?:\s+FROM\s+[\s\S]+)?/i,
323
+ // Pattern 2b: Without AS alias, followed by FROM (SELECT DISTINCT a.client_id FROM ...)
324
+ /SELECT\s+DISTINCT\s+(\w+\.\w+)\s+FROM\s+/i,
325
+ ];
326
+
327
+ for (const pattern of populationQueryPatterns) {
328
+ const match = processedSql.match(pattern);
329
+ if (match && match[1]) {
330
+ console.log('✅ [Interpreter] Detected simple population query, using as-is');
331
+ // Extract patient ID column from the match
332
+ const columnRef = match[1]; // e.g., 'a.client_id'
333
+ const idColumn = columnRef.includes('.') ? columnRef.split('.')[1] : columnRef;
334
+ result.patientIdColumn = (idColumn === 'client_id' || idColumn === 'patient_id' || idColumn === 'person_id')
335
+ ? idColumn as PatientIdColumn
336
+ : 'client_id';
337
+
338
+ // If the SQL doesn't have an AS alias for the patient_id, add one for consistency
339
+ // This ensures the column name matches what applyDisaggregation expects
340
+ if (!/\s+AS\s+(?:patient_id|client_id)\s*FROM/i.test(processedSql)) {
341
+ // Add AS alias
342
+ result.sql = processedSql.replace(
343
+ new RegExp(`SELECT\\s+DISTINCT\\s+${columnRef.replace('.', '\\.')}\\s+FROM`, 'i'),
344
+ `SELECT DISTINCT ${columnRef} AS ${result.patientIdColumn} FROM`
345
+ );
346
+ } else {
347
+ result.sql = processedSql;
348
+ }
349
+
350
+ result.success = true;
351
+ result.warnings?.push('Using simple population query as-is');
352
+ return result;
353
+ }
354
+ }
355
+
356
+ // Pattern 3: Already-disaggregated query with age_group/sex and COUNT
357
+ // SELECT ... AS age_group, ... AS sex, COUNT(...) AS value FROM (subquery) a ... GROUP BY age_group, sex
358
+ // Use balanced parenthesis matching to handle nested subqueries
359
+ const alreadyDisaggPattern =
360
+ /SELECT\s+[\s\S]*?AS\s+age_group[\s\S]*?AS\s+(?:sex|gender)[\s\S]*?COUNT\s*\([\s\S]*?\)\s*(?:AS\s+\w+)?\s+FROM\s*\(\s*/i;
361
+ const alreadyDisaggMatch = processedSql.match(alreadyDisaggPattern);
362
+
363
+ if (alreadyDisaggMatch) {
364
+ console.log('✅ [Interpreter] Detected already-disaggregated query, extracting population SQL');
365
+
366
+ // Find the position after "FROM ("
367
+ const afterFromIndex = alreadyDisaggMatch.index + alreadyDisaggMatch[0].length;
368
+
369
+ // Use balanced parenthesis matching to get the complete inner query
370
+ const balancedMatch = this.matchBalancedParens(processedSql, afterFromIndex);
371
+
372
+ if (balancedMatch) {
373
+ const innerQuery = balancedMatch.text.trim();
374
+ const afterInnerQuery = balancedMatch.endIndex;
375
+
376
+ // Extract the outer alias (should be right after the closing paren)
377
+ const aliasPattern = /\s*(\w+)([\s\S]*?)GROUP\s+BY\s+(?:age_group|months|lost)[\s\S]*?,\s*(?:sex|gender)/i;
378
+ const aliasMatch = processedSql.substring(afterInnerQuery).match(aliasPattern);
379
+
380
+ if (aliasMatch) {
381
+ const outerAlias = aliasMatch[1];
382
+ const restOfQuery = aliasMatch[2] || '';
383
+
384
+ // Try to find patient_id column in the inner query
385
+ const patientIdMatch = innerQuery.match(/(?:FROM|JOIN)\s+(\w+)\.(?:\w+)?\s+(?:\w+)\s+ON|(?:\w+\.)?(client_id|patient_id|person_id)/i);
386
+ const patientIdColumn = patientIdMatch ? (patientIdMatch[1] || patientIdMatch[2]) as PatientIdColumn : 'client_id';
387
+
388
+ result.patientIdColumn = patientIdColumn;
389
+ // Reconstruct the population query including the JOINs and WHERE conditions
390
+ result.sql = `SELECT DISTINCT ${outerAlias}.${patientIdColumn} AS patient_id\nFROM (\n ${this.indent(innerQuery, 2)}\n) ${outerAlias}${restOfQuery.trim()}`.replace(/GROUP\s+BY\s+[\s\S]*?;/i, ';');
391
+ result.success = true;
392
+ result.warnings?.push('Extracted population SQL from already-disaggregated query');
393
+ return result;
394
+ }
395
+ }
396
+ }
397
+
398
+ // Pattern 4: Complex WITH clause with built-in disaggregation
399
+ // WITH base_pop AS (SELECT age_group, sex, COUNT(*) FROM (population_query) ...)
400
+ // Use balanced parenthesis matching to handle nested subqueries
401
+ const complexWithPattern =
402
+ /WITH\s+\w+\s+AS\s*\(\s*SELECT\s+[\s\S]*?FROM\s*\(\s*/i;
403
+ const withMatch = processedSql.match(complexWithPattern);
404
+
405
+ if (withMatch) {
406
+ // Find the position after "FROM ("
407
+ const afterFromIndex = withMatch.index + withMatch[0].length;
408
+
409
+ // Use balanced parenthesis matching to get the complete inner query
410
+ const balancedMatch = this.matchBalancedParens(processedSql, afterFromIndex);
411
+
412
+ if (balancedMatch) {
413
+ const populationSql = balancedMatch.text.trim();
414
+
415
+ if (/GROUP\s+BY\s+(client_id|patient_id|person_id)/i.test(populationSql)) {
416
+ const patientIdMatch = populationSql.match(/GROUP\s+BY\s+(client_id|patient_id|person_id)/i);
417
+ if (patientIdMatch) {
418
+ result.patientIdColumn = patientIdMatch[1] as PatientIdColumn;
419
+ }
420
+
421
+ result.sql = populationSql;
422
+ result.success = true;
423
+ result.warnings?.push('Extracted population SQL from complex WITH clause');
424
+ return result;
425
+ }
426
+ }
427
+ }
428
+
429
+ // Pattern 5: Simple COUNT query without recognizable structure
430
+ if (/SELECT\s+COUNT\s*\(/i.test(processedSql)) {
431
+ result.warnings?.push('CUSTOM_POPULATION_AGGREGATED: SQL returns COUNT(...) instead of patient-level rows');
432
+ result.warnings?.push('The indicator SQL is a metric query, not a population query.');
433
+ result.warnings?.push('Expected: SELECT DISTINCT patient_id FROM ...');
434
+ result.warnings?.push('Got: SELECT COUNT(...) FROM ...');
435
+ result.warnings?.push('To fix: Add explicit populationSql to the indicator configuration, or modify the SQL to return patient-level rows.');
436
+ return result;
437
+ }
438
+
439
+ // No pattern matched - return structured error
440
+ result.warnings?.push('CUSTOM_SQL_EXTRACTION_FAILED: Unable to safely derive population SQL from CUSTOM indicator SQL');
441
+ result.warnings?.push('The SQL structure is not recognized as a valid population query.');
442
+ result.warnings?.push('Expected patterns:');
443
+ result.warnings?.push(' - SELECT COUNT(DISTINCT table.column) FROM (...) alias ...');
444
+ result.warnings?.push(' - SELECT DISTINCT table.column AS patient_id FROM ...');
445
+ result.warnings?.push(' - Already-disaggregated query with age_group/sex');
446
+ result.warnings?.push('');
447
+ result.warnings?.push('To fix this issue:');
448
+ result.warnings?.push(' 1. Add explicit populationSql to the indicator configJson:');
449
+ result.warnings?.push(' {"populationSql": "SELECT DISTINCT a.client_id AS patient_id FROM ..."}');
450
+ result.warnings?.push(' 2. Ensure the SQL follows one of the recognized patterns above');
451
+ result.warnings?.push(' 3. Contact administrator if the SQL structure is correct but not recognized');
452
+
453
+ console.error('❌ [Interpreter] No matching pattern for SQL extraction');
454
+ return result;
455
+ }
456
+
457
+ /**
458
+ * Apply section disaggregation while preserving filters
459
+ *
460
+ * @param sql - The population SQL
461
+ * @param sectionConfig - Section configuration for disaggregation
462
+ * @param config - Custom indicator configuration
463
+ * @returns SQL with section disaggregation applied
464
+ */
465
+ applyDisaggregation(
466
+ sql: string,
467
+ sectionConfig: { ageCategoryCode: string; genders: Array<'F' | 'M'> },
468
+ config?: CustomIndicatorConfig
469
+ ): string {
470
+ if (!sql || !sql.trim()) {
471
+ return `-- Error: CUSTOM_POPULATION_EMPTY
472
+ -- Empty population SQL provided for disaggregation.
473
+ -- Ensure the indicator has a valid population query.`;
474
+ }
475
+
476
+ // Validation: Check if SQL is already a scalar metric (not a population query)
477
+ if (/SELECT\s+COUNT\s*\(/i.test(sql) && !/SELECT\s+DISTINCT/i.test(sql)) {
478
+ return `-- Error: CUSTOM_POPULATION_AGGREGATED
479
+ -- The provided SQL is a metric query (COUNT), not a population query.
480
+ -- Section disaggregation requires patient-level rows.
481
+ -- Expected: SELECT DISTINCT patient_id FROM ...
482
+ -- Got: ${sql.substring(0, 100)}...`;
483
+ }
484
+
485
+ // Validation: Check if SQL is already disaggregated (contains age_group in projection)
486
+ // This must come before the patient_id check to properly reject queries like:
487
+ // SELECT age_group, gender, COUNT(*) FROM ... GROUP BY age_group, gender
488
+ if (/SELECT\s+.*?AS\s+age_group.*?FROM/i.test(sql)) {
489
+ return `-- Error: CUSTOM_ALREADY_DISAGGREGATED
490
+ -- Population SQL contains age_group disaggregation.
491
+ -- Section disaggregation requires raw patient-level population without age/gender.
492
+ -- Remove age_group, sex, or gender columns from the population query projection.`;
493
+ }
494
+
495
+ // Validation: Check if SQL exposes patient_id column
496
+ const hasPatientId = /patient_id\s*(?:AS|FROM|$)/i.test(sql) ||
497
+ /(?:client_id|person_id)\s+AS\s+patient_id/i.test(sql);
498
+
499
+ if (!hasPatientId) {
500
+ return `-- Error: CUSTOM_PATIENT_ID_MISSING
501
+ -- Population SQL does not expose the required patient_id column.
502
+ -- Expected: SELECT DISTINCT ... AS patient_id FROM ...
503
+ -- Got: ${sql.substring(0, 100)}...`;
504
+ }
505
+
506
+ // Remove trailing semicolons - they cause "Multiple statements" errors when used in CTEs
507
+ const cleanedSql = sql.trim().replace(/;+\s*$/, '');
508
+
509
+ const patientIdCol = config?.patientIdColumn || 'patient_id';
510
+ const selectedGenders = sectionConfig.genders.length ? sectionConfig.genders : ['F', 'M'];
511
+ const genderList = selectedGenders.map((g) => `'${g}'`).join(',');
512
+
513
+ return `
514
+ WITH base_pop AS (
515
+ ${this.indent(cleanedSql, 2)}
516
+ ),
517
+ ag AS (
518
+ SELECT
519
+ ag.age_group_id,
520
+ ag.label,
521
+ ag.min_age_days,
522
+ ag.max_age_days,
523
+ ag.sort_order
524
+ FROM report_builder_dim_age_group ag
525
+ JOIN report_builder_dim_age_category ac
526
+ ON ac.age_category_id = ag.age_category_id
527
+ WHERE ac.code = '${sectionConfig.ageCategoryCode}'
528
+ AND ag.is_active = 1
529
+ ),
530
+ genders AS (
531
+ SELECT 'F' AS gender
532
+ UNION ALL SELECT 'M' AS gender
533
+ ),
534
+ cnt AS (
535
+ SELECT
536
+ ag.age_group_id AS age_group_id,
537
+ mdp.gender AS gender,
538
+ COUNT(DISTINCT base_pop.${patientIdCol}) AS value
539
+ FROM base_pop
540
+ JOIN mamba_fact_patients_latest_patient_demographics mdp
541
+ ON mdp.${patientIdCol} = base_pop.${patientIdCol}
542
+ JOIN ag
543
+ ON TIMESTAMPDIFF(DAY, mdp.birthdate, :endDate)
544
+ BETWEEN ag.min_age_days AND ag.max_age_days
545
+ WHERE mdp.birthdate IS NOT NULL
546
+ AND mdp.gender IS NOT NULL
547
+ AND mdp.gender IN (${genderList})
548
+ GROUP BY ag.age_group_id, mdp.gender
549
+ )
550
+ SELECT
551
+ ag.label AS age_group,
552
+ g.gender AS gender,
553
+ COALESCE(cnt.value, 0) AS value
554
+ FROM ag
555
+ CROSS JOIN genders g
556
+ LEFT JOIN cnt
557
+ ON cnt.age_group_id = ag.age_group_id
558
+ AND cnt.gender = g.gender
559
+ ORDER BY ag.sort_order, g.gender;`.trim();
560
+ }
561
+
562
+ /**
563
+ * Validate custom indicator configuration
564
+ *
565
+ * @param config - Custom indicator configuration to validate
566
+ * @param sql - Optional SQL template for validation
567
+ * @returns Validation result
568
+ */
569
+ validate(config: CustomIndicatorConfig, sql?: string): ValidationResult {
570
+ const result: ValidationResult = {
571
+ valid: true,
572
+ errors: [],
573
+ warnings: [],
574
+ info: []
575
+ };
576
+
577
+ // Validate required fields
578
+ if (!config.patientIdColumn) {
579
+ result.errors.push('patientIdColumn is required');
580
+ result.valid = false;
581
+ }
582
+
583
+ if (!config.populationQuery) {
584
+ result.errors.push('populationQuery configuration is required');
585
+ result.valid = false;
586
+ }
587
+
588
+ if (!config.supportsRedisaggregation && config.redisaggregationStrategy !== 'none') {
589
+ result.warnings.push('Redisaggregation strategy set but supportsRedisaggregation is false');
590
+ }
591
+
592
+ // Validate SQL if provided
593
+ if (sql && sql.trim()) {
594
+ const analysis = this.analyze(sql, config);
595
+
596
+ if (!analysis.hasPopulationQuery && config.supportsRedisaggregation) {
597
+ result.warnings.push('SQL does not appear to have a clear population query structure');
598
+ }
599
+
600
+ if (analysis.patientIdColumn !== config.patientIdColumn) {
601
+ result.warnings.push(`Configured patientIdColumn (${config.patientIdColumn}) differs from detected (${analysis.patientIdColumn})`);
602
+ }
603
+
604
+ if (config.supportsRedisaggregation && !analysis.redisaggregatable) {
605
+ result.errors.push('SQL structure does not support re-disaggregation');
606
+ result.valid = false;
607
+ }
608
+
609
+ result.info.push(`Query analysis confidence: ${Math.round(analysis.confidence * 100)}%`);
610
+ }
611
+
612
+ // Validate filter preservation rules
613
+ if (config.preserveFilters && config.preserveFilters.length > 0) {
614
+ for (const filter of config.preserveFilters) {
615
+ if (!filter.name || !filter.joinPattern || !filter.wherePattern) {
616
+ result.errors.push(`Filter "${filter.name}" is missing required patterns`);
617
+ result.valid = false;
618
+ }
619
+ }
620
+ }
621
+
622
+ return result;
623
+ }
624
+
625
+ /**
626
+ * Detect patient ID column from SQL
627
+ *
628
+ * @param sql - SQL to analyze
629
+ * @returns Detected patient ID column
630
+ */
631
+ private detectPatientIdColumn(sql: string): PatientIdColumn {
632
+ const patterns = [
633
+ { column: 'client_id' as PatientIdColumn, pattern: /\bclient_id\b/i },
634
+ { column: 'patient_id' as PatientIdColumn, pattern: /\bpatient_id\b/i },
635
+ { column: 'person_id' as PatientIdColumn, pattern: /\bperson_id\b/i }
636
+ ];
637
+
638
+ // Count occurrences of each patient ID column
639
+ const counts = patterns.map(({ column, pattern }) => ({
640
+ column,
641
+ count: (sql.match(pattern) || []).length
642
+ }));
643
+
644
+ // Return the most common patient ID column
645
+ const mostCommon = counts.sort((a, b) => b.count - a.count)[0];
646
+ return mostCommon.count > 0 ? mostCommon.column : 'client_id';
647
+ }
648
+
649
+ /**
650
+ * Match a balanced parenthesis group in SQL
651
+ * Handles nested subqueries by counting opening/closing parens
652
+ *
653
+ * @param sql - SQL string to match
654
+ * @param startIndex - Index to start matching from (after opening paren)
655
+ * @returns Object with matched text and end index, or null if no match
656
+ */
657
+ private matchBalancedParens(sql: string, startIndex: number): { text: string; endIndex: number } | null {
658
+ let depth = 1;
659
+ let i = startIndex;
660
+ const len = sql.length;
661
+
662
+ while (i < len && depth > 0) {
663
+ const char = sql[i];
664
+ if (char === '(') {
665
+ depth++;
666
+ } else if (char === ')') {
667
+ depth--;
668
+ }
669
+ i++;
670
+ }
671
+
672
+ if (depth !== 0) {
673
+ // Unbalanced parentheses
674
+ return null;
675
+ }
676
+
677
+ return {
678
+ text: sql.substring(startIndex, i - 1),
679
+ endIndex: i
680
+ };
681
+ }
682
+
683
+ /**
684
+ * Indent SQL by a number of spaces
685
+ *
686
+ * @param sql - SQL to indent
687
+ * @param spaces - Number of spaces to indent
688
+ * @returns Indented SQL
689
+ */
690
+ private indent(sql: string, spaces: number): string {
691
+ const pad = ' '.repeat(spaces);
692
+ return sql
693
+ .split('\n')
694
+ .map((line) => (line.trim() ? pad + line : line))
695
+ .join('\n');
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Singleton instance of the query interpreter
701
+ */
702
+ export const customIndicatorInterpreter = new CustomIndicatorInterpreter();