@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,858 @@
1
+ /**
2
+ * Recursive Population SQL Compiler for Indicators
3
+ *
4
+ * This module implements a recursive compiler that builds population SQL
5
+ * directly from indicator configuration, avoiding the fragile approach of
6
+ * parsing/modifying generated SQL queries.
7
+ *
8
+ * Key design principles:
9
+ * 1. Every indicator (atomic or composite) compiles to population SQL whose output column
10
+ * is always aliased AS patient_id (see convertCountToPopulation), regardless of the
11
+ * source indicator's configured id column
12
+ * 2. Scalar counts and disaggregations are wrappers around population SQL
13
+ * 3. Nested composite indicators are handled recursively
14
+ * 4. Circular dependencies are detected
15
+ * 5. Missing references are validated
16
+ */
17
+
18
+ import type { IndicatorDto } from '../../../resources/indicator/indicators.api';
19
+ import type { CompositeOperator } from '../types/composite-indicator.types';
20
+
21
+ /**
22
+ * Result of compiling an indicator to population SQL.
23
+ */
24
+ export type PopulationCompileResult = {
25
+ /** The population SQL that returns the patient ID column */
26
+ sql: string;
27
+ /** The column name used for patient identification (e.g., 'client_id', 'patient_id', 'encounter_id') */
28
+ patientIdColumn: string;
29
+ /** Any warnings or information about the compilation */
30
+ warnings?: string[];
31
+ /** Whether this indicator was retired (for informational purposes) */
32
+ retired?: boolean;
33
+ /** Compilation error, if any */
34
+ error?: PopulationSqlError;
35
+ };
36
+
37
+ /**
38
+ * Error codes for population SQL compilation failures.
39
+ */
40
+ export const POPULATION_ERRORS = {
41
+ AGGREGATE_SQL: 'AGGREGATE_SQL',
42
+ ID_NOT_EXPOSED: 'ID_NOT_EXPOSED',
43
+ EMPTY_SQL: 'EMPTY_SQL',
44
+ COMPILATION_FAILED: 'COMPILATION_FAILED'
45
+ } as const;
46
+
47
+ /**
48
+ * Compiler options for population SQL generation.
49
+ */
50
+ export type CompilerOptions = {
51
+ /** Whether to allow using retired indicators as operands */
52
+ allowRetired?: boolean;
53
+ /** Maximum nesting depth to prevent infinite recursion (default: 10) */
54
+ maxDepth?: number;
55
+ };
56
+
57
+ /**
58
+ * Circular dependency error.
59
+ */
60
+ export class CircularDependencyError extends Error {
61
+ constructor(public chain: string[]) {
62
+ super(`Circular indicator dependency detected: ${chain.join(' → ')}`);
63
+ this.name = 'CircularDependencyError';
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Missing reference error.
69
+ */
70
+ export class MissingReferenceError extends Error {
71
+ constructor(
72
+ public parentIndicator: string,
73
+ public missingRef: string,
74
+ public refType: 'indicatorAId' | 'indicatorBId'
75
+ ) {
76
+ super(`Missing ${refType} in indicator ${parentIndicator}: ${missingRef}`);
77
+ this.name = 'MissingReferenceError';
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Unsupported operator error.
83
+ */
84
+ export class UnsupportedOperatorError extends Error {
85
+ constructor(
86
+ public indicator: string,
87
+ public operator: string
88
+ ) {
89
+ super(`Unsupported operator '${operator}' in indicator ${indicator}`);
90
+ this.name = 'UnsupportedOperatorError';
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Population SQL error.
96
+ * Used when the compiler produces invalid SQL (aggregate instead of population).
97
+ */
98
+ export class PopulationSqlError extends Error {
99
+ constructor(
100
+ public code: string,
101
+ message: string,
102
+ public indicator?: { uuid: string; name: string; code: string }
103
+ ) {
104
+ super(message);
105
+ this.name = 'PopulationSqlError';
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Atomic indicator compilation error.
111
+ */
112
+ export class AtomicIndicatorError extends Error {
113
+ constructor(
114
+ public indicator: string,
115
+ public reason: string
116
+ ) {
117
+ super(`Cannot compile atomic indicator ${indicator}: ${reason}`);
118
+ this.name = 'AtomicIndicatorError';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Configuration for a composite indicator from config_json.
124
+ */
125
+ type CompositeIndicatorConfig = {
126
+ version?: number;
127
+ unit?: 'Patients' | 'Encounters';
128
+ operator?: CompositeOperator;
129
+ indicatorAId?: string;
130
+ indicatorBId?: string;
131
+ indicatorACode?: string;
132
+ indicatorBCode?: string;
133
+ // Legacy fields that might be present
134
+ sqlPreview?: string;
135
+ sqlTemplate?: string;
136
+ base?: {
137
+ sqlPreview?: string;
138
+ sqlTemplate?: string;
139
+ themeConfig?: {
140
+ patientIdColumn?: string;
141
+ };
142
+ };
143
+ themeConfig?: {
144
+ patientIdColumn?: string;
145
+ };
146
+ authoring?: {
147
+ base?: {
148
+ sqlPreview?: string;
149
+ sqlTemplate?: string;
150
+ themeConfig?: {
151
+ patientIdColumn?: string;
152
+ };
153
+ };
154
+ };
155
+ baseIndicator?: {
156
+ sqlPreview?: string;
157
+ sqlTemplate?: string;
158
+ themeConfig?: {
159
+ patientIdColumn?: string;
160
+ };
161
+ };
162
+ };
163
+
164
+ type ValidationResult = {
165
+ valid: boolean;
166
+ error?: { code: string; message: string };
167
+ };
168
+
169
+ /**
170
+ * Validates that SQL is proper population SQL (not aggregate).
171
+ *
172
+ * Population SQL must:
173
+ * 1. Expose the patient ID column in the SELECT clause
174
+ * 2. Not be an aggregate query (COUNT, SUM, AVG in final projection)
175
+ * 3. Be a SELECT statement (may start with WITH CTEs)
176
+ *
177
+ * @param sql - The SQL to validate
178
+ * @param patientIdColumn - The patient ID column name that should be exposed
179
+ * @returns Validation result
180
+ */
181
+ function validatePopulationSql(sql: string, patientIdColumn: string): ValidationResult {
182
+ const trimmed = sql.trim();
183
+
184
+ // Check for empty SQL
185
+ if (!trimmed) {
186
+ return {
187
+ valid: false,
188
+ error: { code: POPULATION_ERRORS.EMPTY_SQL, message: 'Population SQL is empty' }
189
+ };
190
+ }
191
+
192
+ // Check it's a SELECT statement (may have WITH CTEs first)
193
+ const upperSql = trimmed.toUpperCase();
194
+ if (!upperSql.startsWith('WITH') && !upperSql.startsWith('SELECT')) {
195
+ return {
196
+ valid: false,
197
+ error: { code: POPULATION_ERRORS.COMPILATION_FAILED, message: 'Population SQL must be a SELECT statement' }
198
+ };
199
+ }
200
+
201
+ // Check for aggregate functions in final projection
202
+ // This pattern matches SELECT COUNT(...), SELECT SUM(...), etc. at the start (after WITH)
203
+ // Need to skip past WITH clauses when checking
204
+ let sqlToCheck = trimmed;
205
+ if (upperSql.startsWith('WITH')) {
206
+ // Find the main SELECT after the CTEs
207
+ const selectMatch = trimmed.match(/\)\s*SELECT\s+/i);
208
+ if (selectMatch) {
209
+ const selectIndex = trimmed.indexOf(selectMatch[0]) + selectMatch[0].length;
210
+ sqlToCheck = trimmed.substring(selectIndex).trim();
211
+ }
212
+ }
213
+
214
+ // Check for aggregate in final projection
215
+ const aggregatePattern = /^SELECT\s+(?:COUNT|SUM|AVG|MAX|MIN)\s*\(/i;
216
+ if (aggregatePattern.test(sqlToCheck)) {
217
+ return {
218
+ valid: false,
219
+ error: {
220
+ code: POPULATION_ERRORS.AGGREGATE_SQL,
221
+ message: 'Population SQL cannot use aggregate functions (COUNT, SUM, AVG) in final projection. Use SELECT DISTINCT instead.'
222
+ }
223
+ };
224
+ }
225
+
226
+ // Check that patient ID column is exposed
227
+ // Pattern should match SELECT DISTINCT ... patient_idColumn ... or SELECT ... AS patient_idColumn
228
+ const columnPattern = new RegExp(
229
+ `SELECT\\s+DISTINCT\\s+[\\w\\.]+\\s+AS\\s+${patientIdColumn}\\b|` +
230
+ `SELECT\\s+DISTINCT\\s+(?:[\\w]+\\.)?${patientIdColumn}\\b`,
231
+ 'i'
232
+ );
233
+
234
+ // Also check if it's in the main SELECT (after WITH)
235
+ if (upperSql.startsWith('WITH')) {
236
+ const mainSelectPattern = new RegExp(`\\bSELECT\\s+DISTINCT\\s+(?:[\\w]+\\.)?${patientIdColumn}\\b`, 'i');
237
+ if (!mainSelectPattern.test(trimmed) && !columnPattern.test(trimmed)) {
238
+ return {
239
+ valid: false,
240
+ error: {
241
+ code: POPULATION_ERRORS.ID_NOT_EXPOSED,
242
+ message: `Population SQL must expose '${patientIdColumn}' column in the main SELECT (e.g., SELECT DISTINCT a.${patientIdColumn} AS ${patientIdColumn})`
243
+ }
244
+ };
245
+ }
246
+ } else if (!columnPattern.test(trimmed)) {
247
+ return {
248
+ valid: false,
249
+ error: {
250
+ code: POPULATION_ERRORS.ID_NOT_EXPOSED,
251
+ message: `Population SQL must expose '${patientIdColumn}' column (e.g., SELECT DISTINCT a.${patientIdColumn} AS ${patientIdColumn})`
252
+ }
253
+ };
254
+ }
255
+
256
+ return { valid: true };
257
+ }
258
+
259
+ /**
260
+ * Simple hash function for strings.
261
+ * Used to create cache keys from SQL content.
262
+ */
263
+ function hashString(str: string): string {
264
+ let hash = 0;
265
+ for (let i = 0; i < str.length; i++) {
266
+ const char = str.charCodeAt(i);
267
+ hash = ((hash << 5) - hash) + char;
268
+ hash = hash & hash; // Convert to 32-bit integer
269
+ }
270
+ return Math.abs(hash).toString(36);
271
+ }
272
+
273
+ /**
274
+ * Generates a cache key for an indicator.
275
+ * Includes UUID, SQL content hash, and config hash to prevent stale results.
276
+ */
277
+ function getCacheKey(indicator: IndicatorDto, options: CompilerOptions): string {
278
+ const sqlHash = hashString(indicator.sqlTemplate || '');
279
+ const configHash = hashString(indicator.configJson || '');
280
+ const allowRetired = options.allowRetired ? 'allowRetired' : '';
281
+ return `${indicator.uuid}|${sqlHash}|${configHash}|${allowRetired}`;
282
+ }
283
+
284
+ /**
285
+ * Cache to avoid re-compiling the same indicator multiple times.
286
+ * Map<cacheKey, PopulationCompileResult>
287
+ */
288
+ const compilationCache = new Map<string, PopulationCompileResult>();
289
+
290
+ /**
291
+ * Clear the compilation cache. Useful for testing or when indicators change.
292
+ */
293
+ export function clearCompilationCache(): void {
294
+ compilationCache.clear();
295
+ }
296
+
297
+ /**
298
+ * Recursively compile an indicator to population SQL.
299
+ *
300
+ * This function follows these rules:
301
+ * 1. Check cache first
302
+ * 2. Detect circular dependencies
303
+ * 3. Parse config_json to get configuration
304
+ * 4. For COMPOSITE: recursively compile A and B, then combine
305
+ * 5. For BASE: extract population SQL from sql_template
306
+ * 6. Fix common typos like `:stratDate` -> `:startDate`
307
+ *
308
+ * @param indicator - The indicator to compile
309
+ * @param getIndicator - Function to fetch referenced indicators by UUID
310
+ * @param visited - Set of already-visited indicator UUIDs (for cycle detection)
311
+ * @param options - Compiler options
312
+ * @returns Population SQL that returns client_id
313
+ */
314
+ export async function compilePopulationSql(
315
+ indicator: IndicatorDto,
316
+ getIndicator: (uuid: string) => Promise<IndicatorDto | null>,
317
+ visited: Set<string> = new Set(),
318
+ options: CompilerOptions = {}
319
+ ): Promise<PopulationCompileResult> {
320
+ const { allowRetired = false, maxDepth = 10 } = options;
321
+
322
+ // Check cache using improved key (includes SQL content hash)
323
+ const cacheKey = getCacheKey(indicator, options);
324
+ if (compilationCache.has(cacheKey)) {
325
+ return compilationCache.get(cacheKey)!;
326
+ }
327
+
328
+ // Check depth limit
329
+ if (visited.size >= maxDepth) {
330
+ throw new Error(`Maximum compilation depth ${maxDepth} exceeded. Possible circular dependency.`);
331
+ }
332
+
333
+ // Check for circular dependency
334
+ if (visited.has(indicator.uuid)) {
335
+ throw new CircularDependencyError([...visited, indicator.uuid]);
336
+ }
337
+
338
+ // Add to visited set
339
+ const newVisited = new Set(visited);
340
+ newVisited.add(indicator.uuid);
341
+
342
+ // Check if retired
343
+ if (indicator.retired && !allowRetired) {
344
+ throw new Error(`Cannot use retired indicator ${indicator.code || indicator.uuid} as operand`);
345
+ }
346
+
347
+ const warnings: string[] = [];
348
+ if (indicator.retired) {
349
+ warnings.push(`Using retired indicator: ${indicator.code || indicator.name}`);
350
+ }
351
+
352
+ // Parse config_json
353
+ let config: CompositeIndicatorConfig | null = null;
354
+ try {
355
+ config = indicator.configJson ? JSON.parse(indicator.configJson) : null;
356
+ } catch (e) {
357
+ warnings.push(`Failed to parse config_json: ${e}`);
358
+ }
359
+
360
+ // Handle different indicator types
361
+ if (indicator.kind === 'COMPOSITE' && config) {
362
+ // Composite indicator - compile recursively
363
+ const result = await compileCompositePopulation(indicator, config, getIndicator, newVisited, options);
364
+ result.warnings = [...warnings, ...(result.warnings || [])];
365
+ compilationCache.set(cacheKey, result);
366
+ return result;
367
+ } else if (indicator.kind === 'BASE') {
368
+ // Base indicator - extract population SQL
369
+ const result = compileBasePopulation(indicator, config);
370
+ result.warnings = warnings;
371
+ compilationCache.set(cacheKey, result);
372
+ return result;
373
+ } else {
374
+ // FINAL indicator or unsupported type
375
+ // For FINAL indicators, we might need different handling
376
+ // For now, try to extract population SQL like a base indicator
377
+ try {
378
+ const result = compileBasePopulation(indicator, config);
379
+ result.warnings = [...warnings, 'FINAL indicator treated as BASE'];
380
+ compilationCache.set(cacheKey, result);
381
+ return result;
382
+ } catch (e) {
383
+ throw new AtomicIndicatorError(
384
+ indicator.code || indicator.uuid,
385
+ `Unsupported indicator kind: ${indicator.kind}`
386
+ );
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Compile a composite indicator's population SQL.
393
+ */
394
+ async function compileCompositePopulation(
395
+ indicator: IndicatorDto,
396
+ config: CompositeIndicatorConfig,
397
+ getIndicator: (uuid: string) => Promise<IndicatorDto | null>,
398
+ visited: Set<string>,
399
+ options: CompilerOptions
400
+ ): Promise<PopulationCompileResult> {
401
+ const warnings: string[] = [];
402
+
403
+ // Validate required fields
404
+ if (!config.indicatorAId) {
405
+ throw new MissingReferenceError(
406
+ indicator.code || indicator.uuid,
407
+ 'missing',
408
+ 'indicatorAId'
409
+ );
410
+ }
411
+
412
+ if (!config.indicatorBId) {
413
+ throw new MissingReferenceError(
414
+ indicator.code || indicator.uuid,
415
+ 'missing',
416
+ 'indicatorBId'
417
+ );
418
+ }
419
+
420
+ if (!config.operator) {
421
+ throw new UnsupportedOperatorError(indicator.code || indicator.uuid, 'missing');
422
+ }
423
+
424
+ // Get referenced indicators
425
+ const indicatorA = await getIndicator(config.indicatorAId);
426
+ const indicatorB = await getIndicator(config.indicatorBId);
427
+
428
+ if (!indicatorA) {
429
+ throw new MissingReferenceError(
430
+ indicator.code || indicator.uuid,
431
+ config.indicatorAId,
432
+ 'indicatorAId'
433
+ );
434
+ }
435
+
436
+ if (!indicatorB) {
437
+ throw new MissingReferenceError(
438
+ indicator.code || indicator.uuid,
439
+ config.indicatorBId,
440
+ 'indicatorBId'
441
+ );
442
+ }
443
+
444
+ // Recursively compile operand A
445
+ const resultA = await compilePopulationSql(indicatorA, getIndicator, visited, options);
446
+
447
+ // Recursively compile operand B
448
+ const resultB = await compilePopulationSql(indicatorB, getIndicator, visited, options);
449
+
450
+ // Combine with the appropriate operator
451
+ const sql = combineWithOperator(
452
+ config.operator!,
453
+ resultA.sql,
454
+ resultB.sql
455
+ );
456
+
457
+ // Validate the combined SQL
458
+ const validation = validatePopulationSql(sql, resultA.patientIdColumn);
459
+ if (!validation.valid) {
460
+ return {
461
+ sql,
462
+ patientIdColumn: resultA.patientIdColumn,
463
+ warnings: [...warnings, ...(resultA.warnings || []), ...(resultB.warnings || [])],
464
+ retired: indicator.retired,
465
+ error: new PopulationSqlError(
466
+ validation.error.code,
467
+ validation.error.message,
468
+ { uuid: indicator.uuid, name: indicator.name, code: indicator.code || indicator.uuid }
469
+ )
470
+ };
471
+ }
472
+
473
+ return {
474
+ sql,
475
+ patientIdColumn: resultA.patientIdColumn, // Use the patientIdColumn from operand A
476
+ warnings: [...warnings, ...(resultA.warnings || []), ...(resultB.warnings || [])],
477
+ retired: indicator.retired
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Combine two population SQL queries with the given operator.
483
+ *
484
+ * Handles the case where population SQL may already contain WITH clauses
485
+ * by using unique CTE names that won't conflict with inner CTEs.
486
+ */
487
+ function combineWithOperator(
488
+ operator: CompositeOperator,
489
+ populationSqlA: string,
490
+ populationSqlB: string
491
+ ): string {
492
+ // Contract: convertCountToPopulation normalizes ALL population SQL to expose a single
493
+ // output column aliased AS patient_id, so the combine step must reference patient_id
494
+ // regardless of the source indicators' configured id column or unit.
495
+ const idField = 'patient_id';
496
+
497
+ // Clean the SQL - remove trailing semicolons
498
+ const cleanA = populationSqlA.trim().replace(/;+\s*$/, '');
499
+ const cleanB = populationSqlB.trim().replace(/;+\s*$/, '');
500
+
501
+ // Check if the population SQL contains WITH clauses (nested CTEs)
502
+ const aHasWith = /^\s*WITH\s+/i.test(cleanA);
503
+ const bHasWith = /^\s*WITH\s+/i.test(cleanB);
504
+
505
+ // Use unique CTE names to avoid conflicts with inner CTEs
506
+ // If the population SQL has WITH clauses, use more specific names
507
+ const aName = aHasWith ? 'operand_a_base' : 'A';
508
+ const bName = bHasWith ? 'operand_b_base' : 'B';
509
+
510
+ if (operator === 'AND') {
511
+ // Intersection: A INNER JOIN B on client_id
512
+ return `
513
+ WITH ${aName} AS (
514
+ ${indent(cleanA, 0)}
515
+ ),
516
+ ${bName} AS (
517
+ ${indent(cleanB, 0)}
518
+ )
519
+ SELECT DISTINCT
520
+ ${aName}.${idField}
521
+ FROM ${aName}
522
+ INNER JOIN ${bName}
523
+ ON ${bName}.${idField} = ${aName}.${idField}
524
+ `.trim();
525
+ } else if (operator === 'OR') {
526
+ // Union: A UNION B
527
+ return `
528
+ WITH ${aName} AS (
529
+ ${indent(cleanA, 0)}
530
+ ),
531
+ ${bName} AS (
532
+ ${indent(cleanB, 0)}
533
+ )
534
+ SELECT ${idField} FROM ${aName}
535
+ UNION
536
+ SELECT ${idField} FROM ${bName}
537
+ `.trim();
538
+ } else if (operator === 'A_AND_NOT_B') {
539
+ // Set difference: A LEFT JOIN B WHERE B IS NULL
540
+ return `
541
+ WITH ${aName} AS (
542
+ ${indent(cleanA, 0)}
543
+ ),
544
+ ${bName} AS (
545
+ ${indent(cleanB, 0)}
546
+ )
547
+ SELECT DISTINCT
548
+ ${aName}.${idField}
549
+ FROM ${aName}
550
+ LEFT JOIN ${bName}
551
+ ON ${bName}.${idField} = ${aName}.${idField}
552
+ WHERE ${bName}.${idField} IS NULL
553
+ `.trim();
554
+ }
555
+
556
+ throw new Error(`Unsupported operator: ${operator}`);
557
+ }
558
+
559
+ /**
560
+ * Compile a base indicator's population SQL.
561
+ *
562
+ * For base indicators, we extract the population SQL from the sql_template
563
+ * or from config_json fields. The population SQL should return the patient ID column.
564
+ */
565
+ function compileBasePopulation(
566
+ indicator: IndicatorDto,
567
+ config: CompositeIndicatorConfig | null
568
+ ): PopulationCompileResult {
569
+ // Try to get SQL from various sources
570
+ let sql = indicator.sqlTemplate || '';
571
+
572
+ if (!sql && config) {
573
+ // Try config_json fields
574
+ sql = config.sqlPreview || config.sqlTemplate || config.base?.sqlPreview || config.base?.sqlTemplate || '';
575
+ }
576
+
577
+ if (!sql) {
578
+ throw new AtomicIndicatorError(
579
+ indicator.code || indicator.uuid,
580
+ 'No SQL template found'
581
+ );
582
+ }
583
+
584
+ // Fix common typos
585
+ sql = fixCommonTypos(sql);
586
+
587
+ // Get the patient ID column from config (e.g., 'client_id' for HIV indicators)
588
+ const patientIdColumn = getPatientIdColumnFromConfig(config);
589
+
590
+ // Convert COUNT SQL to population SQL if needed
591
+ const populationSql = convertCountToPopulation(sql, patientIdColumn);
592
+
593
+ // Validate the output is proper population SQL
594
+ const validation = validatePopulationSql(populationSql, patientIdColumn);
595
+ if (!validation.valid) {
596
+ return {
597
+ sql: populationSql,
598
+ patientIdColumn,
599
+ retired: indicator.retired,
600
+ error: new PopulationSqlError(
601
+ validation.error.code,
602
+ validation.error.message,
603
+ { uuid: indicator.uuid, name: indicator.name, code: indicator.code || indicator.uuid }
604
+ )
605
+ };
606
+ }
607
+
608
+ return {
609
+ sql: populationSql,
610
+ patientIdColumn,
611
+ retired: indicator.retired
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Get the patient ID column from indicator config.
617
+ * Defaults to 'client_id' if not specified.
618
+ */
619
+ function getPatientIdColumnFromConfig(config: CompositeIndicatorConfig | null): string {
620
+ try {
621
+ const cfg =
622
+ config?.themeConfig ||
623
+ config?.base?.themeConfig ||
624
+ config?.authoring?.base?.themeConfig ||
625
+ config?.baseIndicator?.themeConfig ||
626
+ null;
627
+ const pid = cfg?.patientIdColumn;
628
+ return pid ? String(pid) : 'client_id';
629
+ } catch {
630
+ return 'client_id';
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Convert COUNT SQL to population SQL.
636
+ *
637
+ * This handles the case where a base indicator has COUNT(*) SQL
638
+ * or COUNT(DISTINCT column) SQL and needs to be converted to return
639
+ * patient_id or client_id instead.
640
+ *
641
+ * IMPORTANT: The output column is ALWAYS aliased as the patientIdColumn value
642
+ * (e.g., 'client_id' or 'patient_id') for consistency with downstream queries.
643
+ */
644
+ function convertCountToPopulation(sql: string, patientIdColumn: string = 'client_id'): string {
645
+ let trimmed = sql.trim();
646
+
647
+ // Handle escaped newlines BEFORE pattern matching
648
+ // This ensures multi-line SQL with GROUP BY, HAVING, etc. on separate lines is preserved
649
+ trimmed = trimmed.replace(/\\n/g, '\n');
650
+
651
+ // Remove any trailing semicolons first (they cause issues when used as CTE)
652
+ const withoutSemicolon = trimmed.replace(/;+\s*$/, '');
653
+
654
+ // Check if it's already a population query (SELECT DISTINCT with patient_id, client_id, or encounter_id)
655
+ const populationCheck = new RegExp(
656
+ `SELECT\\s+DISTINCT\\s+(?:\\w+\\.)?(?:${patientIdColumn}|client_id|patient_id|encounter_id)`,
657
+ 'i'
658
+ );
659
+ if (populationCheck.test(withoutSemicolon)) {
660
+ // Ensure the output is normalized to patient_id for disaggregation compatibility
661
+ const fixed = fixCommonTypos(withoutSemicolon);
662
+ // If it's already aliasing as patient_id, return as-is
663
+ const aliasCheck = new RegExp(`AS\\s+patient_id\\b`, 'im');
664
+ if (aliasCheck.test(fixed)) {
665
+ return fixed;
666
+ }
667
+ // Fix the alias to normalize to patient_id (handles client_id, encounter_id, etc.)
668
+ // Handles both qualified (a.client_id) and unqualified (client_id) column references,
669
+ // with or without an existing alias. The contract requires EVERY population SQL to
670
+ // expose patient_id, otherwise combineWithOperator/disaggregation reference a column
671
+ // the CTE does not expose.
672
+ const replacePattern = /SELECT\s+DISTINCT\s+(?:(\w+)\.)?(client_id|patient_id|encounter_id)(?:\s+AS\s+\w+)?/i;
673
+ return fixed.replace(
674
+ replacePattern,
675
+ (_match, tableAlias: string | undefined, column: string) =>
676
+ `SELECT DISTINCT ${tableAlias ? `${tableAlias}.` : ''}${column} AS patient_id`
677
+ );
678
+ }
679
+
680
+ // Check for COUNT(DISTINCT column) pattern - e.g., SELECT COUNT(DISTINCT a.client_id) AS total FROM ...
681
+ // Also handles patterns without AS or with subqueries: SELECT COUNT(DISTINCT a.client_id) FROM (...)
682
+ const countDistinctPattern = /SELECT\s+COUNT\s*\(\s*DISTINCT\s+(\w+\.(?:client_id|patient_id|encounter_id))\s*\)(?:\s+AS\s+\w+)?\s+FROM/i;
683
+ const countDistinctMatch = withoutSemicolon.match(countDistinctPattern);
684
+ if (countDistinctMatch) {
685
+ const columnReference = countDistinctMatch[1]; // e.g., "a.client_id"
686
+ // Extract just the column name without the alias
687
+ const columnName = columnReference.split('.').pop()!;
688
+ const alias = columnReference.split('.')[0];
689
+
690
+ // Replace with SELECT DISTINCT alias.columnName AS patient_id FROM
691
+ // Keep the rest of the SQL after FROM (including subqueries)
692
+ const result = withoutSemicolon.replace(
693
+ /SELECT\s+COUNT\s*\(\s*DISTINCT\s+\w+\.(?:client_id|patient_id|encounter_id)\s*\)(?:\s+AS\s+\w+)?\s+FROM/i,
694
+ `SELECT DISTINCT ${alias}.${columnName} AS patient_id FROM`
695
+ );
696
+
697
+ return fixCommonTypos(result);
698
+ }
699
+
700
+ // Check for COUNT(*) pattern - e.g., SELECT COUNT(*) AS total FROM ...
701
+ const countPattern = /SELECT\s+.*?COUNT\s*\(\s*\*\s*\)\s+AS\s+total/i;
702
+ if (countPattern.test(withoutSemicolon)) {
703
+ // Try to extract the table and alias from the FROM clause
704
+ const fromMatch = withoutSemicolon.match(/FROM\s+(\S+)\s+(\S+)/i);
705
+ if (fromMatch) {
706
+ const alias = fromMatch[2];
707
+
708
+ // Build population SQL - normalize to patient_id
709
+ const result = withoutSemicolon
710
+ .replace(/SELECT\s+.*?COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s*/i, '')
711
+ .replace(/FROM\s+/i, `SELECT DISTINCT ${alias}.${patientIdColumn} AS patient_id FROM `);
712
+
713
+ return fixCommonTypos(result);
714
+ }
715
+
716
+ // If we can't parse it, try a simple replacement - normalize to patient_id
717
+ const result = withoutSemicolon.replace(
718
+ /SELECT\s+.*?COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s+FROM\s+/i,
719
+ `SELECT DISTINCT patient_id FROM `
720
+ );
721
+
722
+ return fixCommonTypos(result);
723
+ }
724
+
725
+ // Not a COUNT query we recognize - assume it's already correct but fix typos
726
+ return fixCommonTypos(withoutSemicolon);
727
+ }
728
+
729
+ /**
730
+ * Fix common typos in generated SQL.
731
+ */
732
+ function fixCommonTypos(sql: string): string {
733
+ let fixed = sql;
734
+ // Handle escaped newlines - convert literal \n to actual newlines
735
+ // This can happen when SQL is serialized through JSON in the backend
736
+ fixed = fixed.replace(/\\n/g, '\n');
737
+ // Fix :stratDate -> :startDate
738
+ fixed = fixed.replace(/:stratDate\b/g, ':startDate');
739
+ // Remove trailing semicolons - population SQL used as CTE shouldn't have semicolons
740
+ fixed = fixed.replace(/;+\s*$/, '');
741
+ return fixed;
742
+ }
743
+
744
+ /**
745
+ * Indent SQL by a number of spaces.
746
+ */
747
+ function indent(sql: string, spaces: number): string {
748
+ const pad = ' '.repeat(spaces);
749
+ return sql
750
+ .split('\n')
751
+ .map((line) => (line.trim() ? pad + line : line))
752
+ .join('\n');
753
+ }
754
+
755
+ /**
756
+ * Generate scalar COUNT SQL from population SQL.
757
+ *
758
+ * This wraps the population SQL in a COUNT query.
759
+ * Population SQL must expose the patient_id column per the compiler contract.
760
+ */
761
+ export function generateScalarCountSql(populationSql: string): string {
762
+ const clean = populationSql.trim().replace(/;+\s*$/, '');
763
+
764
+ return `
765
+ WITH base_population AS (
766
+ ${indent(clean, 0)}
767
+ )
768
+ SELECT
769
+ COUNT(DISTINCT patient_id) AS total
770
+ FROM base_population;
771
+ `.trim();
772
+ }
773
+
774
+ /**
775
+ * Generate age/sex disaggregation SQL from population SQL.
776
+ *
777
+ * This wraps the population SQL in the disaggregation CTE structure.
778
+ *
779
+ * @deprecated The gender defaulting behavior (defaulting to both F and M when none selected)
780
+ * will be removed in a future version. Always specify genders explicitly.
781
+ */
782
+ export function generateAgeSexDisaggregationSql(args: {
783
+ populationSql: string;
784
+ ageCategoryCode: string;
785
+ genders: Array<'F' | 'M'>;
786
+ }): string {
787
+ const { populationSql, ageCategoryCode, genders } = args;
788
+
789
+ // Default to both genders if none selected (for backward compatibility)
790
+ // TODO: Remove this default in a future version and require explicit gender selection
791
+ const selectedGenders = (genders || []).length ? genders : (['F', 'M'] as Array<'F' | 'M'>);
792
+
793
+ // Log deprecation warning when defaulting
794
+ if (!genders || genders.length === 0) {
795
+ console.warn('[generateAgeSexDisaggregationSql] No genders specified, defaulting to both F and M. This default will be removed in a future version.');
796
+ }
797
+
798
+ const clean = populationSql.trim().replace(/;+\s*$/, '');
799
+
800
+ // All population SQL is normalized to patient_id, so hardcode patient_id here
801
+ const patientIdColumn = 'patient_id';
802
+
803
+ return `
804
+ WITH base_pop AS (
805
+ ${indent(clean, 2)}
806
+ ),
807
+ ag AS (
808
+ SELECT
809
+ ag.age_group_id,
810
+ ag.label,
811
+ ag.min_age_days,
812
+ ag.max_age_days,
813
+ ag.sort_order
814
+ FROM report_builder_dim_age_group ag
815
+ JOIN report_builder_dim_age_category ac
816
+ ON ac.age_category_id = ag.age_category_id
817
+ WHERE ac.code = '${escapeSql(ageCategoryCode)}'
818
+ AND ag.is_active = 1
819
+ ),
820
+ genders AS (
821
+ SELECT 'F' AS gender
822
+ UNION ALL SELECT 'M' AS gender
823
+ ),
824
+ cnt AS (
825
+ SELECT
826
+ ag.age_group_id AS age_group_id,
827
+ mdp.gender AS gender,
828
+ COUNT(DISTINCT base_pop.${patientIdColumn}) AS value
829
+ FROM base_pop
830
+ JOIN mamba_fact_patients_latest_patient_demographics mdp
831
+ ON mdp.${patientIdColumn} = base_pop.${patientIdColumn}
832
+ JOIN ag
833
+ ON TIMESTAMPDIFF(DAY, mdp.birthdate, :endDate)
834
+ BETWEEN ag.min_age_days AND ag.max_age_days
835
+ WHERE mdp.birthdate IS NOT NULL
836
+ AND mdp.gender IS NOT NULL
837
+ AND mdp.gender IN (${selectedGenders.map((g) => `'${g}'`).join(',')})
838
+ GROUP BY ag.age_group_id, mdp.gender
839
+ )
840
+ SELECT
841
+ ag.label AS age_group,
842
+ g.gender AS gender,
843
+ COALESCE(cnt.value, 0) AS value
844
+ FROM ag
845
+ CROSS JOIN genders g
846
+ LEFT JOIN cnt
847
+ ON cnt.age_group_id = ag.age_group_id
848
+ AND cnt.gender = g.gender
849
+ ORDER BY ag.sort_order, g.gender;
850
+ `.trim();
851
+ }
852
+
853
+ /**
854
+ * Escape SQL literal values.
855
+ */
856
+ function escapeSql(s: string): string {
857
+ return String(s).replace(/'/g, "''");
858
+ }