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