@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.
- package/.eslintignore +5 -0
- package/.eslintrc.js +19 -0
- package/.prettierrc +8 -0
- package/.tx/config +12 -0
- package/LICENSE +401 -0
- package/README.md +336 -0
- package/__mocks__/react-i18next.js +12 -0
- package/dist/162.js +1 -0
- package/dist/162.js.map +1 -0
- package/dist/184.js +2 -0
- package/dist/184.js.LICENSE.txt +14 -0
- package/dist/184.js.map +1 -0
- package/dist/242.js +2 -0
- package/dist/242.js.LICENSE.txt +21 -0
- package/dist/242.js.map +1 -0
- package/dist/253.js +1 -0
- package/dist/253.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/311.js +1 -0
- package/dist/311.js.map +1 -0
- package/dist/469.js +2 -0
- package/dist/469.js.LICENSE.txt +45 -0
- package/dist/469.js.map +1 -0
- package/dist/478.js +2 -0
- package/dist/478.js.LICENSE.txt +9 -0
- package/dist/478.js.map +1 -0
- package/dist/539.js +1 -0
- package/dist/539.js.map +1 -0
- package/dist/540.js +2 -0
- package/dist/540.js.LICENSE.txt +9 -0
- package/dist/540.js.map +1 -0
- package/dist/669.js +1 -0
- package/dist/669.js.map +1 -0
- package/dist/710.js +1 -0
- package/dist/710.js.map +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/838.js +2 -0
- package/dist/838.js.LICENSE.txt +9 -0
- package/dist/838.js.map +1 -0
- package/dist/917.js +1 -0
- package/dist/917.js.map +1 -0
- package/dist/961.js +2 -0
- package/dist/961.js.LICENSE.txt +19 -0
- package/dist/961.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-report-builder.js +1 -0
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +530 -0
- package/dist/openmrs-esm-report-builder.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/core/env.ts +5 -0
- package/e2e/core/global-setup.ts +34 -0
- package/e2e/core/test.ts +21 -0
- package/e2e/pages/app-shell.page.ts +34 -0
- package/e2e/pages/etl-sources.page.ts +35 -0
- package/e2e/pages/report-sections.page.ts +20 -0
- package/e2e/pages/section-preview.modal.ts +29 -0
- package/e2e/specs/etl-sources.spec.ts +51 -0
- package/e2e/specs/navigation.spec.ts +13 -0
- package/e2e/specs/section-preview-validation.spec.ts +38 -0
- package/e2e/specs/section-preview.spec.ts +94 -0
- package/e2e/support/github/Dockerfile +22 -0
- package/e2e/support/github/Dockerfile.dockerignore +13 -0
- package/e2e/support/github/docker-compose.yml +31 -0
- package/e2e/support/github/run-e2e-docker-env.sh +131 -0
- package/e2e/support/github/spa-assemble-config.json +9 -0
- package/e2e/tsconfig.json +11 -0
- package/example.env +5 -0
- package/jest.config.js +28 -0
- package/linelist_report_builder_design_spec.md +816 -0
- package/package.json +111 -0
- package/playwright.config.ts +28 -0
- package/routes.json +22 -0
- package/src/__debug__/all-designs.test.tsx +30 -0
- package/src/admin-card-link.extension.module.scss +55 -0
- package/src/admin-card-link.extension.tsx +24 -0
- package/src/components/admin/admin-page.component.tsx +75 -0
- package/src/components/admin/age-categories-page.component.tsx +109 -0
- package/src/components/admin/age-groups-page.component.tsx +146 -0
- package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
- package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
- package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
- package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
- package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
- package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
- package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
- package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
- package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
- package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
- package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
- package/src/components/admin/etl-sources-page.component.tsx +355 -0
- package/src/components/admin/etl-tasks-page.component.scss +370 -0
- package/src/components/admin/etl-tasks-page.component.tsx +768 -0
- package/src/components/admin/report-categories-page.component.tsx +267 -0
- package/src/components/admin/report-library-page.component.tsx +551 -0
- package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
- package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
- package/src/components/ai-support/ai.service.ts +21 -0
- package/src/components/ai-support/ai.types.ts +12 -0
- package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
- package/src/components/app-shell/report-builder-shell.scss +18 -0
- package/src/components/dashboards/dashboard-config.schema.ts +97 -0
- package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
- package/src/components/dashboards/dashboards-admin.scss +62 -0
- package/src/components/dashboards/dashboards-page.component.tsx +153 -0
- package/src/components/dashboards/dashboards-table.component.tsx +130 -0
- package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
- package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
- package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
- package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
- package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
- package/src/components/data-themes/data-theme-form.component.tsx +59 -0
- package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
- package/src/components/data-themes/data-themes-page.component.tsx +139 -0
- package/src/components/data-themes/data-themes-table.component.tsx +98 -0
- package/src/components/data-themes/icon-registry.ts +35 -0
- package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
- package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
- package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
- package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
- package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
- package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
- package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
- package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
- package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
- package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
- package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
- package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
- package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
- package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
- package/src/components/data-visualizer/components/header/header.scss +71 -0
- package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
- package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
- package/src/components/data-visualizer/components/panel/panel.scss +25 -0
- package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
- package/src/components/data-visualizer/constants.ts +445 -0
- package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
- package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
- package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
- package/src/components/data-visualizer/data-visualizer.scss +449 -0
- package/src/components/data-visualizer/declarations.d.ts +131 -0
- package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
- package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
- package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
- package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
- package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
- package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
- package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
- package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
- package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
- package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
- package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
- package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
- package/src/components/data-visualizer/types.ts +206 -0
- package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
- package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
- package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
- package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
- package/src/components/etl-monitor/builder/index.ts +5 -0
- package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
- package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
- package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
- package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
- package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
- package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
- package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
- package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
- package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
- package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
- package/src/components/etl-monitor/builder/steps/index.ts +5 -0
- package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
- package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
- package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
- package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
- package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
- package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
- package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
- package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
- package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
- package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
- package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
- package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
- package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
- package/src/components/etl-monitor/renderers/components/index.ts +6 -0
- package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
- package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
- package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
- package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
- package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
- package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
- package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
- package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
- package/src/components/etl-monitor/renderers/index.ts +11 -0
- package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
- package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
- package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
- package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
- package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
- package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
- package/src/components/import-export/artifact-groups.ts +153 -0
- package/src/components/import-export/export-report-modal.component.tsx +265 -0
- package/src/components/import-export/import-export-page.component.tsx +610 -0
- package/src/components/import-export/import-export.styles.scss +825 -0
- package/src/components/import-export/import-package-modal.component.tsx +401 -0
- package/src/components/import-export/index.ts +3 -0
- package/src/components/indicators/README.md +24 -0
- package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
- package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
- package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
- package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
- package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
- package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
- package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
- package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
- package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
- package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
- package/src/components/indicators/custom-condition-input.component.tsx +310 -0
- package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
- package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
- package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
- package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
- package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
- package/src/components/indicators/indicator-search-select.component.tsx +297 -0
- package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
- package/src/components/indicators/indicators-page.component.tsx +673 -0
- package/src/components/indicators/indicators-page.scss +121 -0
- package/src/components/indicators/indicators-table.component.tsx +188 -0
- package/src/components/indicators/preview-table.component.tsx +31 -0
- package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
- package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
- package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
- package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
- package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
- package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
- package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
- package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
- package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
- package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
- package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
- package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
- package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
- package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
- package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
- package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
- package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
- package/src/components/indicators/sql-preview.component.tsx +19 -0
- package/src/components/indicators/tag-input.component.tsx +177 -0
- package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
- package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
- package/src/components/indicators/types/composite-indicator.types.ts +27 -0
- package/src/components/indicators/types/condition-ui.types.ts +11 -0
- package/src/components/indicators/types/custom-indicator.types.ts +161 -0
- package/src/components/indicators/types/data-theme-config.types.ts +88 -0
- package/src/components/indicators/types/indicator-types.ts +57 -0
- package/src/components/indicators/types/sql-builders.ts +43 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
- package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
- package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
- package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
- package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
- package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
- package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
- package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
- package/src/components/indicators/utils/population-sql.compiler.test.ts +999 -0
- package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
- package/src/components/landing/landing-action-item.component.tsx +50 -0
- package/src/components/landing/landing-action-list.component.tsx +12 -0
- package/src/components/landing/landing-activity-list.component.tsx +47 -0
- package/src/components/landing/landing-footer-grid.component.tsx +45 -0
- package/src/components/landing/landing-hero.component.tsx +18 -0
- package/src/components/landing/landing-section-card.component.tsx +33 -0
- package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
- package/src/components/landing/report-builder-landing-page.scss +131 -0
- package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
- package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
- package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
- package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
- package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
- package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
- package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
- package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
- package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
- package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
- package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
- package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
- package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
- package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
- package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
- package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
- package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
- package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
- package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
- package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
- package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
- package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
- package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
- package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
- package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
- package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
- package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
- package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
- package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
- package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
- package/src/components/linelist/design/panels/column-category-selector.scss +329 -0
- package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
- package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
- package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
- package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
- package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
- package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
- package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
- package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
- package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
- package/src/components/linelist/design/panels/json-preview.scss +85 -0
- package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
- package/src/components/linelist/design/panels/query-config-panel.scss +342 -0
- package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
- package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
- package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
- package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
- package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
- package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
- package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
- package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
- package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
- package/src/components/linelist/linelist-dashboard.page.scss +240 -0
- package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
- package/src/components/linelist/linelist-editor.page.scss +28 -0
- package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
- package/src/components/linelist/linelist-run-report.page.scss +136 -0
- package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
- package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
- package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
- package/src/components/report/dashboard/reports-table.component.tsx +186 -0
- package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
- package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
- package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
- package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
- package/src/components/report/definition/report-definition.types.ts +66 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
- package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
- package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
- package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
- package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
- package/src/components/report/design/panels/template-structure-panel.scss +81 -0
- package/src/components/report/design/report-design-editor.component.tsx +1170 -0
- package/src/components/report/design/report-design.types.ts +48 -0
- package/src/components/report/design/report-design.utils.ts +86 -0
- package/src/components/report/panels/report-detail.component.tsx +71 -0
- package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
- package/src/components/report/report-dashboard.page.component.tsx +554 -0
- package/src/components/report/report-editor.page.component.tsx +590 -0
- package/src/components/report/report.routes.tsx +32 -0
- package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
- package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
- package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
- package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
- package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
- package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
- package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
- package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
- package/src/components/report-sections/report-sections-page.component.tsx +325 -0
- package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
- package/src/components/report-sections/section-types.ts +55 -0
- package/src/components/report-sections/section-utils.ts +50 -0
- package/src/components/report-sections/sections-page.scss +79 -0
- package/src/components/run-reports/run-reports-page.component.tsx +8 -0
- package/src/components/security/access-denied-page.component.tsx +69 -0
- package/src/components/security/access-denied-page.scss +39 -0
- package/src/components/security/privilege-route.component.test.tsx +79 -0
- package/src/components/security/privilege-route.component.tsx +39 -0
- package/src/components/shared/compile-setup-modal.component.tsx +182 -0
- package/src/components/shared/concept-selector.component.tsx +245 -0
- package/src/components/shared/concept-selector.scss +121 -0
- package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
- package/src/components/shared/header/header.component.tsx +85 -0
- package/src/components/shared/header/header.scss +150 -0
- package/src/components/shared/header/illustration.component.tsx +34 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
- package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
- package/src/config-schema.ts +43 -0
- package/src/constants/privileges.test.ts +53 -0
- package/src/constants/privileges.ts +118 -0
- package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
- package/src/features/dashboards/DashboardsPage.tsx +164 -0
- package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
- package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
- package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
- package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
- package/src/features/dashboards/components/DashboardSection.tsx +46 -0
- package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
- package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
- package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
- package/src/features/dashboards/dashboards.scss +196 -0
- package/src/features/dashboards/hooks/useDashboard.ts +61 -0
- package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
- package/src/features/dashboards/hooks/useDashboards.ts +56 -0
- package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
- package/src/features/dashboards/index.ts +11 -0
- package/src/features/dashboards/utils/dashboard-config.util.test.ts +255 -0
- package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
- package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
- package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
- package/src/features/report-visualizer/components/Export/index.ts +2 -0
- package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
- package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
- package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
- package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
- package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
- package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
- package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
- package/src/features/report-visualizer/components/TableView/index.ts +2 -0
- package/src/features/report-visualizer/hooks/index.ts +4 -0
- package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
- package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
- package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
- package/src/features/report-visualizer/hooks/useReports.ts +135 -0
- package/src/features/report-visualizer/index.ts +6 -0
- package/src/features/report-visualizer/report-visualizer.scss +593 -0
- package/src/features/report-visualizer/types/index.ts +1 -0
- package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
- package/src/features/report-visualizer/utils/status-colors.ts +117 -0
- package/src/globals.d.ts +14 -0
- package/src/hooks/address-template.ts +86 -0
- package/src/hooks/concept.ts +112 -0
- package/src/hooks/indicator/index.ts +1 -0
- package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
- package/src/hooks/openmrs-reference-data.ts +432 -0
- package/src/hooks/theme/index.ts +4 -0
- package/src/hooks/theme/useDataTheme.ts +61 -0
- package/src/hooks/theme/useDataThemes.ts +28 -0
- package/src/hooks/theme/useMambaTableMeta.ts +34 -0
- package/src/hooks/theme/useMambaTables.ts +95 -0
- package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
- package/src/hooks/use-report-builder-privileges.ts +58 -0
- package/src/index.ts +29 -0
- package/src/resources/address-template/address-template.api.ts +180 -0
- package/src/resources/agegroup/agegroups.api.ts +58 -0
- package/src/resources/concept/concept.api.ts +116 -0
- package/src/resources/concepts/concept-mapper.ts +21 -0
- package/src/resources/concepts/concept-types.ts +31 -0
- package/src/resources/concepts/concepts.resource.ts +44 -0
- package/src/resources/concepts/useConceptSearch.ts +41 -0
- package/src/resources/dashboard/dashboard.api.ts +107 -0
- package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
- package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
- package/src/resources/etl-source/etl-source.api.ts +65 -0
- package/src/resources/etl-task/etl-task.api.ts +96 -0
- package/src/resources/etl-task/etl-task.types.ts +31 -0
- package/src/resources/indicator/indicators.api.ts +125 -0
- package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
- package/src/resources/linelist/linelist-reports.api.ts +616 -0
- package/src/resources/location/location.api.ts +135 -0
- package/src/resources/openmrs-api.ts +40 -0
- package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
- package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
- package/src/resources/preview/sql-preview.api.ts +54 -0
- package/src/resources/report/reports.api.ts +140 -0
- package/src/resources/report-category/report-category.api.ts +41 -0
- package/src/resources/report-import-export/import-export-api.ts +164 -0
- package/src/resources/report-library/report-library.api.ts +63 -0
- package/src/resources/report-section/report-sections.api.ts +73 -0
- package/src/resources/report-section/section-preview.api.ts +25 -0
- package/src/resources/theme/data-theme.api.ts +88 -0
- package/src/resources/theme/etl-schema.api.ts +101 -0
- package/src/resources/theme/etl-table-meta.api.ts +45 -0
- package/src/root.component.tsx +20 -0
- package/src/routes/report-builder.routes.tsx +160 -0
- package/src/routes/report-builder.scss +56 -0
- package/src/routes.json +28 -0
- package/src/sample-template.ts +121 -0
- package/src/template-utils.ts +110 -0
- package/src/types/carbon-react-inert.d.ts +13 -0
- package/src/types/condition-operators.ts +63 -0
- package/src/types/dashboard/dashboard.types.ts +137 -0
- package/src/types/dashboard/index.ts +4 -0
- package/src/types/etl/etl-types.ts +238 -0
- package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
- package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
- package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
- package/src/types/etl-monitor/index.ts +6 -0
- package/src/types/linelist/compile-config.ts +455 -0
- package/src/types/linelist-types.ts +2059 -0
- package/src/types/theme/data-theme.types.ts +88 -0
- package/src/utils/api-error.utils.test.ts +151 -0
- package/src/utils/api-error.utils.ts +131 -0
- package/src/utils/config-enhancer.ts +62 -0
- package/src/utils/etl-browser/etl-browser.util.test.ts +145 -0
- package/src/utils/etl-browser/export-csv.util.ts +43 -0
- package/src/utils/etl-browser/group-tables.util.ts +84 -0
- package/src/utils/etl-browser/index.ts +4 -0
- package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
- package/src/utils/etl-browser/table-sql.util.ts +20 -0
- package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
- package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
- package/src/utils/etl-monitor/index.ts +10 -0
- package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
- package/src/utils/etl-monitor/normalization-examples.ts +0 -0
- package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
- package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
- package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
- package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
- package/src/utils/etl-task/etl-task-card.util.test.ts +239 -0
- package/src/utils/etl-task/etl-task-card.util.ts +276 -0
- package/src/utils/etl-task/index.ts +1 -0
- package/src/utils/html-entities.utils.ts +34 -0
- package/src/utils/parameter-resolution.ts +218 -0
- package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
- package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
- package/tools/scss-stub.js +12 -0
- package/tools/setup-tests.ts +1 -0
- package/translations/en.json +65 -0
- package/tsconfig.json +43 -0
- 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(/&lt;/g, '<')
|
|
170
|
+
.replace(/&gt;/g, '>')
|
|
171
|
+
.replace(/&amp;/g, '&')
|
|
172
|
+
.replace(/</g, '<')
|
|
173
|
+
.replace(/>/g, '>')
|
|
174
|
+
.replace(/&/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();
|