@openmrs/esm-report-builder 0.1.1-pre.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/128.css +1 -0
- package/dist/128.css.map +1 -0
- package/dist/128.js +338 -0
- package/dist/128.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/2131.js +1 -0
- package/dist/2131.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2408.js +1 -0
- package/dist/2408.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/2997.js +1 -0
- package/dist/2997.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/4171.css +1 -0
- package/dist/4171.css.map +1 -0
- package/dist/4171.js +43 -0
- package/dist/4171.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/4668.js +1 -0
- package/dist/4668.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5098.js +1 -0
- package/dist/5098.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/550.js +1 -0
- package/dist/550.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6237.js +1 -0
- package/dist/6237.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6652.js +1 -0
- package/dist/6652.js.map +1 -0
- package/dist/6686.js +1 -0
- package/dist/6686.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/6867.js +19 -0
- package/dist/6867.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7475.js +1 -0
- package/dist/7475.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8251.js +1 -0
- package/dist/8251.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/997.js +1 -0
- package/dist/997.js.map +1 -0
- package/dist/openmrs-esm-report-builder.js +3 -0
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +1433 -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 +35 -0
- package/e2e/pages/etl-sources.page.ts +36 -0
- package/e2e/pages/report-sections.page.ts +21 -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 +32 -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 +6 -0
- package/e2e/tsconfig.json +11 -0
- package/example.env +5 -0
- package/linelist_report_builder_design_spec.md +816 -0
- package/package.json +98 -0
- package/playwright.config.ts +28 -0
- package/rspack.config.js +2 -0
- package/src/__debug__/all-designs.test.tsx +33 -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 +448 -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 +985 -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 +324 -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 +341 -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 +64 -0
- package/src/components/security/access-denied-page.scss +39 -0
- package/src/components/security/privilege-route.component.test.tsx +76 -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 +151 -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 +54 -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 +286 -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 +9 -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 +92 -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 +31 -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 +147 -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 +151 -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 +240 -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/i18next-parser.config.js +0 -0
- package/tools/setup-tests.ts +1 -0
- package/tools/update-openmrs-deps.mjs +42 -0
- package/translations/am.json +65 -0
- package/translations/ar.json +65 -0
- package/translations/ar_SY.json +65 -0
- package/translations/bn.json +65 -0
- package/translations/cs.json +65 -0
- package/translations/de.json +65 -0
- package/translations/en.json +65 -0
- package/translations/en_US.json +65 -0
- package/translations/es.json +65 -0
- package/translations/es_MX.json +65 -0
- package/translations/fr.json +65 -0
- package/translations/he.json +65 -0
- package/translations/hi.json +65 -0
- package/translations/hi_IN.json +65 -0
- package/translations/id.json +65 -0
- package/translations/it.json +65 -0
- package/translations/ka.json +65 -0
- package/translations/km.json +65 -0
- package/translations/ku.json +65 -0
- package/translations/ky.json +65 -0
- package/translations/lg.json +65 -0
- package/translations/ne.json +65 -0
- package/translations/pl.json +65 -0
- package/translations/pt.json +65 -0
- package/translations/pt_BR.json +65 -0
- package/translations/qu.json +65 -0
- package/translations/ro_RO.json +65 -0
- package/translations/ru_RU.json +65 -0
- package/translations/si.json +65 -0
- package/translations/sq.json +65 -0
- package/translations/sw.json +65 -0
- package/translations/sw_KE.json +65 -0
- package/translations/tr.json +65 -0
- package/translations/tr_TR.json +65 -0
- package/translations/uk.json +65 -0
- package/translations/uz.json +65 -0
- package/translations/uz@Latn.json +65 -0
- package/translations/uz_UZ.json +65 -0
- package/translations/vi.json +65 -0
- package/translations/zh.json +65 -0
- package/translations/zh_CN.json +65 -0
- package/translations/zh_TW.json +65 -0
- package/tsconfig.json +24 -0
- package/vitest.config.mts +42 -0
|
@@ -0,0 +1,2059 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linelist Report Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* These types define the structure for linelist (patient list) report definitions.
|
|
5
|
+
* Linelists are standalone reports that return individual patient records,
|
|
6
|
+
* as opposed to aggregate reports which return counts with disaggregation.
|
|
7
|
+
*
|
|
8
|
+
* Based on backend linelist report definition structure from:
|
|
9
|
+
* /openmrs-module-report-builder/omod/src/main/resources/reports/etl/linelist/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Population source join types
|
|
14
|
+
* How multiple population sources combine
|
|
15
|
+
*/
|
|
16
|
+
export type PopulationJoinType =
|
|
17
|
+
| 'JOIN' // INNER JOIN on patient_id
|
|
18
|
+
| 'LEFT_JOIN' // LEFT JOIN on patient_id
|
|
19
|
+
| 'INTERSECT' // Patients in ALL sources (AND logic)
|
|
20
|
+
| 'UNION' // Patients from ANY source (OR logic)
|
|
21
|
+
| 'EXCEPT'; // Patients in source A but NOT in source B
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Population source definition
|
|
25
|
+
* Represents a datasource used for patient population selection
|
|
26
|
+
*/
|
|
27
|
+
export type PopulationSource = {
|
|
28
|
+
uuid: string; // Table name (e.g., "mamba_fact_patients_latest")
|
|
29
|
+
name: string; // Display name
|
|
30
|
+
type: 'ETL' | 'CORE' | 'REFERENCE' | 'SQL' | 'INDICATOR';
|
|
31
|
+
joinType: PopulationJoinType; // How this source combines with others
|
|
32
|
+
joinCondition?: string; // Custom ON clause for JOINs
|
|
33
|
+
enabled: boolean; // Whether this source is active
|
|
34
|
+
order: number; // Display order for UI
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Data source configuration in report definition
|
|
39
|
+
* Defines which datasources are used and their roles
|
|
40
|
+
*/
|
|
41
|
+
export type DataSourceConfig = {
|
|
42
|
+
uuid: string; // Data source/table UUID or name
|
|
43
|
+
name: string; // Display name
|
|
44
|
+
type: 'ETL' | 'SQL' | 'REFERENCE' | 'INDICATOR' | 'CORE'; // Source type
|
|
45
|
+
role: 'PRIMARY' | 'SECONDARY' | 'REFERENCE'; // Role in the report
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Columns selected from this datasource
|
|
49
|
+
* Maps column names to their definitions
|
|
50
|
+
*/
|
|
51
|
+
columns?: Record<string, {
|
|
52
|
+
name: string;
|
|
53
|
+
type: string;
|
|
54
|
+
sourceTable: string;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Linelist report definition configuration
|
|
60
|
+
* This is stored in the configJson field of a report definition
|
|
61
|
+
*
|
|
62
|
+
* Version 2: Supports multiple data sources
|
|
63
|
+
* Version 3: Supports multiple population sources with join types
|
|
64
|
+
*/
|
|
65
|
+
export type LinelistReportDefinitionConfig = {
|
|
66
|
+
version: 1 | 2 | 3; // Version for migration support
|
|
67
|
+
type: 'LINE_LIST';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Report metadata
|
|
71
|
+
*/
|
|
72
|
+
categoryUuid?: string;
|
|
73
|
+
themeUuid?: string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Version 1: Single data source (legacy)
|
|
77
|
+
* Version 2: Multiple data sources
|
|
78
|
+
*/
|
|
79
|
+
dataSourceUuid?: string; // V1 only - deprecated, use dataSources
|
|
80
|
+
dataSources?: DataSourceConfig[]; // V2 - array of data sources
|
|
81
|
+
|
|
82
|
+
rowGrain?: LinelistRowGrain;
|
|
83
|
+
templateId?: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parameters supported by this linelist report
|
|
87
|
+
* Typically startDate and endDate for time-based filtering
|
|
88
|
+
*/
|
|
89
|
+
parameters?: Array<LinelistParameter>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Base cohort definition - SQL query that selects patients
|
|
93
|
+
* This query should return distinct patient IDs
|
|
94
|
+
*/
|
|
95
|
+
baseCohortDefinition: LinelistCohortDefinition;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dataset definitions define the output columns and their sources
|
|
99
|
+
* Each dataset becomes a tab/key in the output JSON
|
|
100
|
+
*/
|
|
101
|
+
dataSetDefinitions: LinelistDataSetDefinition[];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Optional sorting configuration
|
|
105
|
+
*/
|
|
106
|
+
orderBy?: string;
|
|
107
|
+
orderDirection?: 'ASC' | 'DESC';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builder metadata - persisted so the editor can reconstruct the draft
|
|
111
|
+
* when re-opening for edit. Ignored by the backend at run time.
|
|
112
|
+
*/
|
|
113
|
+
buildMethod?: 'SQL_BUILDER' | 'VISUAL_FILTER' | 'INDICATOR_BASED';
|
|
114
|
+
indicatorRules?: Array<{
|
|
115
|
+
id: string;
|
|
116
|
+
indicatorUuid: string;
|
|
117
|
+
name: string;
|
|
118
|
+
conditions?: Array<{ key: string; operator?: string; value: any }>;
|
|
119
|
+
logicalOperator?: 'AND' | 'OR';
|
|
120
|
+
negate?: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Optional row limit for safety
|
|
125
|
+
*/
|
|
126
|
+
limit?: number;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parameter definition for linelist reports
|
|
131
|
+
*/
|
|
132
|
+
export type LinelistParameter = {
|
|
133
|
+
name: string;
|
|
134
|
+
label: string;
|
|
135
|
+
type: LinelistParameterType;
|
|
136
|
+
required?: boolean;
|
|
137
|
+
defaultValue?: string;
|
|
138
|
+
displayOrder?: number;
|
|
139
|
+
/**
|
|
140
|
+
* Type-specific configuration
|
|
141
|
+
*/
|
|
142
|
+
config?: LinelistParameterConfig;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Available parameter types
|
|
147
|
+
*/
|
|
148
|
+
export type LinelistParameterType =
|
|
149
|
+
| 'DATE'
|
|
150
|
+
| 'DATETIME'
|
|
151
|
+
| 'LOCATION'
|
|
152
|
+
| 'PROGRAM'
|
|
153
|
+
| 'PROVIDER'
|
|
154
|
+
| 'CONCEPT'
|
|
155
|
+
| 'CODED_VALUE'
|
|
156
|
+
| 'IDENTIFIER_TYPE' // Patient identifier types
|
|
157
|
+
| 'PERSON_ATTRIBUTE' // Person attribute types
|
|
158
|
+
| 'BOOLEAN'
|
|
159
|
+
| 'NUMBER'
|
|
160
|
+
| 'TEXT'
|
|
161
|
+
| 'LIST'; // Configurable list with predefined options
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Type-specific configuration for parameters
|
|
165
|
+
*/
|
|
166
|
+
export type LinelistParameterConfig =
|
|
167
|
+
// DATE/DATETIME configuration
|
|
168
|
+
| {
|
|
169
|
+
type: 'DATE' | 'DATETIME';
|
|
170
|
+
includeTime?: boolean;
|
|
171
|
+
minDate?: string; // ISO date string
|
|
172
|
+
maxDate?: string; // ISO date string
|
|
173
|
+
}
|
|
174
|
+
// LOCATION configuration
|
|
175
|
+
| {
|
|
176
|
+
type: 'LOCATION';
|
|
177
|
+
locationUuids?: string[]; // Restrict to specific locations
|
|
178
|
+
userDefaultToSessionLocation?: boolean;
|
|
179
|
+
tagUuid?: string; // Filter by location tag
|
|
180
|
+
}
|
|
181
|
+
// CONCEPT configuration
|
|
182
|
+
| {
|
|
183
|
+
type: 'CONCEPT';
|
|
184
|
+
conceptClassUuids?: string[]; // Restrict to concept classes
|
|
185
|
+
sourceUuid?: string; // Concept source UUID
|
|
186
|
+
allowMultiSelect?: boolean;
|
|
187
|
+
}
|
|
188
|
+
// CODED_VALUE configuration
|
|
189
|
+
| {
|
|
190
|
+
type: 'CODED_VALUE';
|
|
191
|
+
sourceUuid?: string; // Data source for coded values
|
|
192
|
+
options?: Array<{ uuid: string; label: string; }>;
|
|
193
|
+
allowMultiSelect?: boolean;
|
|
194
|
+
}
|
|
195
|
+
// IDENTIFIER_TYPE configuration
|
|
196
|
+
| {
|
|
197
|
+
type: 'IDENTIFIER_TYPE';
|
|
198
|
+
identifierTypeUuids?: string[]; // Restrict to specific types
|
|
199
|
+
}
|
|
200
|
+
// PERSON_ATTRIBUTE configuration
|
|
201
|
+
| {
|
|
202
|
+
type: 'PERSON_ATTRIBUTE';
|
|
203
|
+
attributeTypeUuids?: string[]; // Restrict to specific types
|
|
204
|
+
format?: string; // Filter by format (e.g., 'org.openmrs.Concept')
|
|
205
|
+
}
|
|
206
|
+
// LIST configuration (predefined options)
|
|
207
|
+
| {
|
|
208
|
+
type: 'LIST';
|
|
209
|
+
options: Array<{
|
|
210
|
+
value: string;
|
|
211
|
+
label: string;
|
|
212
|
+
}>;
|
|
213
|
+
allowMultiSelect?: boolean;
|
|
214
|
+
}
|
|
215
|
+
// TEXT configuration
|
|
216
|
+
| {
|
|
217
|
+
type: 'TEXT';
|
|
218
|
+
minLength?: number;
|
|
219
|
+
maxLength?: number;
|
|
220
|
+
pattern?: string; // Regex pattern for validation
|
|
221
|
+
}
|
|
222
|
+
// NUMBER configuration
|
|
223
|
+
| {
|
|
224
|
+
type: 'NUMBER';
|
|
225
|
+
min?: number;
|
|
226
|
+
max?: number;
|
|
227
|
+
step?: number;
|
|
228
|
+
integerOnly?: boolean;
|
|
229
|
+
}
|
|
230
|
+
// BOOLEAN configuration
|
|
231
|
+
| {
|
|
232
|
+
type: 'BOOLEAN';
|
|
233
|
+
trueLabel?: string; // Custom label for true value
|
|
234
|
+
falseLabel?: string; // Custom label for false value
|
|
235
|
+
}
|
|
236
|
+
// PROGRAM configuration
|
|
237
|
+
| {
|
|
238
|
+
type: 'PROGRAM';
|
|
239
|
+
programUuids?: string[]; // Restrict to specific programs
|
|
240
|
+
}
|
|
241
|
+
// PROVIDER configuration
|
|
242
|
+
| {
|
|
243
|
+
type: 'PROVIDER';
|
|
244
|
+
providerRole?: string; // Filter by provider role
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parameter draft for builder UI
|
|
249
|
+
*/
|
|
250
|
+
export type LinelistParameterDraft = {
|
|
251
|
+
id: string; // Unique ID for React keys
|
|
252
|
+
name: string;
|
|
253
|
+
label: string;
|
|
254
|
+
type: LinelistParameterType;
|
|
255
|
+
required: boolean;
|
|
256
|
+
defaultValue: string;
|
|
257
|
+
displayOrder: number;
|
|
258
|
+
config?: LinelistParameterConfig;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Sort configuration for linelist reports
|
|
263
|
+
* Defines ordered multi-column sorting
|
|
264
|
+
*/
|
|
265
|
+
export type LinelistSortConfig = {
|
|
266
|
+
id: string; // Unique ID for React keys
|
|
267
|
+
columnId: string; // ID of the column to sort by
|
|
268
|
+
columnName: string; // Display name of the column
|
|
269
|
+
direction: 'ASC' | 'DESC'; // Sort direction
|
|
270
|
+
nulls: 'FIRST' | 'LAST'; // Where to place null values
|
|
271
|
+
sortOrder: number; // Priority in sort order (0 = highest priority)
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Visual filter condition types
|
|
276
|
+
* Used for building SQL queries visually
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Field type for filter conditions
|
|
281
|
+
*/
|
|
282
|
+
export type FilterFieldType = 'TEXT' | 'NUMBER' | 'DATE' | 'CODED' | 'BOOLEAN' | 'LOCATION';
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Available operators by field type
|
|
286
|
+
*/
|
|
287
|
+
export type FilterOperator =
|
|
288
|
+
// Text operators
|
|
289
|
+
| 'EQUALS' | 'NOT_EQUALS' | 'CONTAINS' | 'STARTS_WITH' | 'ENDS_WITH' | 'IS_BLANK' | 'IS_NOT_BLANK'
|
|
290
|
+
// Number operators
|
|
291
|
+
| 'GREATER_THAN' | 'LESS_THAN' | 'GREATER_EQUAL' | 'LESS_EQUAL' | 'BETWEEN'
|
|
292
|
+
// Date operators
|
|
293
|
+
| 'ON' | 'BEFORE' | 'AFTER' | 'BETWEEN_DATES' | 'IN_PREVIOUS_PERIOD' | 'IN_NEXT_PERIOD'
|
|
294
|
+
// Coded operators
|
|
295
|
+
| 'IS_ONE_OF' | 'IS_NOT_ONE_OF'
|
|
296
|
+
// Boolean operators
|
|
297
|
+
| 'IS_TRUE' | 'IS_FALSE' | 'NOT_RECORDED'
|
|
298
|
+
// Location operators
|
|
299
|
+
| 'WITHIN_HIERARCHY' | 'IN_LOCATION';
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Logical operator for combining conditions
|
|
303
|
+
*/
|
|
304
|
+
export type LogicalOperator = 'AND' | 'OR';
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Filter condition definition
|
|
308
|
+
*/
|
|
309
|
+
export type FilterCondition = {
|
|
310
|
+
id: string;
|
|
311
|
+
field: string; // Field name (e.g., "appointment_date", "gender")
|
|
312
|
+
fieldLabel: string; // Display name for the field
|
|
313
|
+
fieldType: FilterFieldType;
|
|
314
|
+
operator: FilterOperator;
|
|
315
|
+
value?: string | string[]; // Single value or array of values
|
|
316
|
+
value2?: string; // Second value for "between" operators
|
|
317
|
+
negate?: boolean; // Whether to negate the condition
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Filter group - contains conditions and nested groups
|
|
322
|
+
*/
|
|
323
|
+
export type FilterGroup = {
|
|
324
|
+
id: string;
|
|
325
|
+
logicalOperator: LogicalOperator; // How to combine items in this group
|
|
326
|
+
conditions: FilterCondition[];
|
|
327
|
+
nestedGroups?: FilterGroup[]; // Nested groups for complex logic
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Visual filter builder state
|
|
332
|
+
*/
|
|
333
|
+
export type VisualFilterState = {
|
|
334
|
+
rootGroup: FilterGroup;
|
|
335
|
+
useVisualBuilder: boolean; // Toggle between visual and SQL mode
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Cohort definition for patient selection
|
|
340
|
+
*/
|
|
341
|
+
/**
|
|
342
|
+
* FilterMap configuration for base cohort
|
|
343
|
+
* Maps parameter names to column references for use in column SQL queries
|
|
344
|
+
* Enables column queries to filter to the exact row that matched the base cohort criteria
|
|
345
|
+
*/
|
|
346
|
+
export type FilterMap = {
|
|
347
|
+
/**
|
|
348
|
+
* Map of parameter names to column references
|
|
349
|
+
* Example: { "patientId": "vl.patient_id", "orderId": "vl.native_order_id", "episodeId": "vl.episode_id" }
|
|
350
|
+
* Column SQL can then use :patientId, :orderId, :episodeId as parameters
|
|
351
|
+
*/
|
|
352
|
+
[parameterName: string]: string;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Cohort definition for patient selection
|
|
357
|
+
*/
|
|
358
|
+
export type LinelistCohortDefinition = {
|
|
359
|
+
type: 'SQL';
|
|
360
|
+
name: string;
|
|
361
|
+
config: {
|
|
362
|
+
/**
|
|
363
|
+
* SQL query that returns patient IDs
|
|
364
|
+
* Should use :startDate and :endDate parameters for time-based filtering
|
|
365
|
+
* Example: SELECT DISTINCT patient_id FROM mamba_fact_patients_latest WHERE ...
|
|
366
|
+
*/
|
|
367
|
+
sql: string;
|
|
368
|
+
/**
|
|
369
|
+
* Optional filterMap for column parameter binding
|
|
370
|
+
* When column queries reference the same table as the base cohort,
|
|
371
|
+
* filterMap enables filtering to the exact row that matched cohort criteria
|
|
372
|
+
*/
|
|
373
|
+
filterMap?: FilterMap;
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Dataset definition for output columns
|
|
379
|
+
* Each dataset becomes a separate section in the report output
|
|
380
|
+
*/
|
|
381
|
+
export type LinelistDataSetDefinition = {
|
|
382
|
+
name: string;
|
|
383
|
+
type: 'PATIENT_DATA_SET';
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Row filter defines which patients are included
|
|
387
|
+
* Often the same as the base cohort SQL
|
|
388
|
+
*/
|
|
389
|
+
rowFilter?: {
|
|
390
|
+
type: 'SQL';
|
|
391
|
+
name: string;
|
|
392
|
+
config: {
|
|
393
|
+
sql: string;
|
|
394
|
+
filterMap?: FilterMap;
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Column definitions for the output
|
|
400
|
+
*/
|
|
401
|
+
columns: LinelistColumnDefinition[];
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Column definition for linelist output
|
|
406
|
+
* Each column defines one field in the patient list
|
|
407
|
+
*/
|
|
408
|
+
export type LinelistColumnDefinition = {
|
|
409
|
+
/**
|
|
410
|
+
* Display name for the column (used as header in output)
|
|
411
|
+
*/
|
|
412
|
+
name: string;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Data definition specifies how to get the column value
|
|
416
|
+
*/
|
|
417
|
+
dataDefinition: LinelistDataDefinition;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Optional repeat resolution for fields that may return multiple values
|
|
421
|
+
*/
|
|
422
|
+
repeatResolution?: LinelistRepeatResolution;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Data definition types for linelist columns
|
|
427
|
+
* Based on backend-supported column types
|
|
428
|
+
*/
|
|
429
|
+
export type LinelistDataDefinition =
|
|
430
|
+
| LinelistIdentifierDefinition
|
|
431
|
+
| LinelistPersonNameDefinition
|
|
432
|
+
| LinelistPersonAttributeDefinition
|
|
433
|
+
| LinelistCalculationDefinition
|
|
434
|
+
| LinelistSqlDefinition
|
|
435
|
+
| LinelistPersonAddressDefinition
|
|
436
|
+
| LinelistConverterDefinition
|
|
437
|
+
| LinelistObservationDefinition
|
|
438
|
+
| LinelistEncounterDiagnosisDefinition;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Base data definition structure
|
|
442
|
+
*/
|
|
443
|
+
type BaseDataDefinition<T extends string = string> = {
|
|
444
|
+
type: T;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Identifier column (e.g., Clinic Number, EID Number)
|
|
449
|
+
* Uses OpenMRS patient identifier types
|
|
450
|
+
*/
|
|
451
|
+
export type LinelistIdentifierDefinition = BaseDataDefinition<'IDENTIFIER'> & {
|
|
452
|
+
config: {
|
|
453
|
+
identifierTypeUuid: string;
|
|
454
|
+
preferred?: boolean;
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Person name column
|
|
460
|
+
* Formats person name according to specified type
|
|
461
|
+
*/
|
|
462
|
+
export type LinelistPersonNameDefinition = BaseDataDefinition<'PERSON_NAME'> & {
|
|
463
|
+
config: {
|
|
464
|
+
type?: 'FULL_NAME' | 'GIVEN_NAME' | 'MIDDLE_NAME' | 'FAMILY_NAME';
|
|
465
|
+
preferred?: boolean;
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Person attribute column
|
|
471
|
+
* Uses OpenMRS person attribute types
|
|
472
|
+
*/
|
|
473
|
+
export type LinelistPersonAttributeDefinition = BaseDataDefinition<'PERSON_ATTRIBUTE'> & {
|
|
474
|
+
config: {
|
|
475
|
+
attributeTypeUuid: string;
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Calculation column
|
|
481
|
+
* Performs calculations on patient data (e.g., age)
|
|
482
|
+
*/
|
|
483
|
+
export type LinelistCalculationDefinition = BaseDataDefinition<'CALCULATION'> & {
|
|
484
|
+
config: {
|
|
485
|
+
calculation: 'AGE' | 'AGE_IN_MONTHS' | 'AGE_IN_YEARS' | 'BMI' | string;
|
|
486
|
+
onDate?: boolean; // Whether to calculate as of the report end date
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* SQL column
|
|
492
|
+
* Custom SQL expression for complex column values
|
|
493
|
+
* The :patientId placeholder will be replaced with the patient ID
|
|
494
|
+
*/
|
|
495
|
+
export type LinelistSqlDefinition = BaseDataDefinition<'SQL'> & {
|
|
496
|
+
config: {
|
|
497
|
+
sql: string;
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Person address column
|
|
503
|
+
* Retrieves address information
|
|
504
|
+
*
|
|
505
|
+
* UgandaEMR Address Template Mappings:
|
|
506
|
+
* - ADDRESS1: Address Line 1
|
|
507
|
+
* - ADDRESS2: Address Line 2
|
|
508
|
+
* - ADDRESS3: Subcounty
|
|
509
|
+
* - ADDRESS4: Parish
|
|
510
|
+
* - ADDRESS5: Village
|
|
511
|
+
* - STATE_PROVINCE: County
|
|
512
|
+
* - COUNTY_DISTRICT: District
|
|
513
|
+
* - COUNTRY: Country
|
|
514
|
+
*/
|
|
515
|
+
export type LinelistPersonAddressDefinition = BaseDataDefinition<'PERSON_ADDRESS'> & {
|
|
516
|
+
config: {
|
|
517
|
+
type?: 'PERSON_ADDRESS' | string;
|
|
518
|
+
field?: 'ADDRESS1' | 'ADDRESS2' | 'ADDRESS3' | 'ADDRESS4' | 'ADDRESS5'
|
|
519
|
+
| 'CITY_VILLAGE' | 'STATE_PROVINCE' | 'COUNTY_DISTRICT' | 'COUNTRY'
|
|
520
|
+
| 'LATITUDE' | 'LONGITUDE' | 'POSTAL_CODE' | string;
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Converter column
|
|
526
|
+
* Converts/transforms values from another source
|
|
527
|
+
*/
|
|
528
|
+
export type LinelistConverterDefinition = BaseDataDefinition<'CONVERTER'> & {
|
|
529
|
+
config: {
|
|
530
|
+
type: string;
|
|
531
|
+
format?: string;
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Observation column
|
|
537
|
+
* Uses OpenMRS observations based on concepts
|
|
538
|
+
* Returns the most recent observation value for each patient
|
|
539
|
+
*/
|
|
540
|
+
export type LinelistObservationDefinition = BaseDataDefinition<'OBSERVATION'> & {
|
|
541
|
+
config: {
|
|
542
|
+
conceptUuid: string;
|
|
543
|
+
conceptName?: string;
|
|
544
|
+
/**
|
|
545
|
+
* Strategy for handling multiple observations
|
|
546
|
+
* LATEST: Most recent observation by date
|
|
547
|
+
* EARLIEST: Oldest observation by date
|
|
548
|
+
* CLOSEST_TO_START: Value closest to report start date
|
|
549
|
+
* CLOSEST_TO_END: Value closest to report end date
|
|
550
|
+
* FIRST_WITHIN_PERIOD: First observation within reporting period
|
|
551
|
+
* LAST_WITHIN_PERIOD: Last observation within reporting period
|
|
552
|
+
*/
|
|
553
|
+
strategy?: 'LATEST' | 'EARLIEST' | 'CLOSEST_TO_START' | 'CLOSEST_TO_END' | 'FIRST_WITHIN_PERIOD' | 'LAST_WITHIN_PERIOD';
|
|
554
|
+
/**
|
|
555
|
+
* Whether to return the concept's display name instead of UUID
|
|
556
|
+
*/
|
|
557
|
+
returnDisplay?: boolean;
|
|
558
|
+
/**
|
|
559
|
+
* For coded concepts, optionally return a specific answer
|
|
560
|
+
*/
|
|
561
|
+
answerConceptUuid?: string;
|
|
562
|
+
};
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Encounter Diagnosis column
|
|
567
|
+
* Uses OpenMRS encounter diagnoses based on concepts
|
|
568
|
+
* Returns diagnoses for each patient within the reporting period
|
|
569
|
+
*/
|
|
570
|
+
export type LinelistEncounterDiagnosisDefinition = BaseDataDefinition<'ENCOUNTER_DIAGNOSIS'> & {
|
|
571
|
+
config: {
|
|
572
|
+
/**
|
|
573
|
+
* Optional concept UUID to filter diagnoses by specific condition
|
|
574
|
+
* If not provided, returns all diagnoses
|
|
575
|
+
*/
|
|
576
|
+
conceptUuid?: string;
|
|
577
|
+
conceptName?: string;
|
|
578
|
+
/**
|
|
579
|
+
* Rank of diagnosis (PRIMARY/SECONDARY)
|
|
580
|
+
*/
|
|
581
|
+
rank?: 'PRIMARY' | 'SECONDARY' | 'ANY';
|
|
582
|
+
/**
|
|
583
|
+
* Whether to return only confirmed diagnoses
|
|
584
|
+
*/
|
|
585
|
+
confirmedOnly?: boolean;
|
|
586
|
+
/**
|
|
587
|
+
* Strategy for handling multiple diagnoses
|
|
588
|
+
* ALL_VALUES: Return all diagnoses as comma-separated list
|
|
589
|
+
* LATEST: Most recent diagnosis
|
|
590
|
+
* FIRST_WITHIN_PERIOD: First diagnosis within reporting period
|
|
591
|
+
* LAST_WITHIN_PERIOD: Last diagnosis within reporting period
|
|
592
|
+
*/
|
|
593
|
+
strategy?: 'ALL_VALUES' | 'LATEST' | 'FIRST_WITHIN_PERIOD' | 'LAST_WITHIN_PERIOD';
|
|
594
|
+
/**
|
|
595
|
+
* Whether to return the concept's display name instead of UUID
|
|
596
|
+
*/
|
|
597
|
+
returnDisplay?: boolean;
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Population configuration mode
|
|
603
|
+
* Indicates how the cohort/patient selection is defined
|
|
604
|
+
*/
|
|
605
|
+
export type PopulationMode = 'SQL' | 'INDICATOR' | 'HYBRID';
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Draft state for linelist report builder UI
|
|
609
|
+
* Version 2.0 - Supports multiple data sources and full builder tracking
|
|
610
|
+
* Version 3.0 - Supports multiple population sources with join types
|
|
611
|
+
*/
|
|
612
|
+
export type LinelistReportDraft = {
|
|
613
|
+
// === VERSION ===
|
|
614
|
+
version: 2 | 3; // Contract version for migration support
|
|
615
|
+
|
|
616
|
+
// === BASIC INFO ===
|
|
617
|
+
name: string;
|
|
618
|
+
description?: string;
|
|
619
|
+
code?: string;
|
|
620
|
+
|
|
621
|
+
// Current panel in the builder UI
|
|
622
|
+
currentPanel: LinelistBuilderPanel;
|
|
623
|
+
|
|
624
|
+
// Report categorization
|
|
625
|
+
categoryUuid?: string; // Optional - report category
|
|
626
|
+
rowGrain?: LinelistRowGrain; // Required - what one row represents
|
|
627
|
+
templateId?: string; // Optional - starting template
|
|
628
|
+
|
|
629
|
+
// === POPULATION SOURCES (v3 - Multiple sources with join types) ===
|
|
630
|
+
populationSources: PopulationSource[];
|
|
631
|
+
|
|
632
|
+
// === DATA SOURCES (v2 - Multiple sources supported for columns) ===
|
|
633
|
+
dataSources: DataSourceInfo[];
|
|
634
|
+
|
|
635
|
+
// === LEGACY PROPERTIES (for backward compatibility during migration) ===
|
|
636
|
+
/** @deprecated Use dataSources array instead */
|
|
637
|
+
dataSourceUuid?: string;
|
|
638
|
+
/** @deprecated Use population.sqlTemplate instead */
|
|
639
|
+
cohortSql?: string;
|
|
640
|
+
/** @deprecated Use population.indicatorRules instead */
|
|
641
|
+
indicatorRules?: any[];
|
|
642
|
+
/** @deprecated Use population.visualFilter instead */
|
|
643
|
+
visualFilter?: any;
|
|
644
|
+
/** @deprecated Not used in V2 */
|
|
645
|
+
populationMode?: string;
|
|
646
|
+
/** @deprecated Not used in V2 */
|
|
647
|
+
dataSetName?: string;
|
|
648
|
+
/** UI state flag for unsaved changes */
|
|
649
|
+
unsavedChanges?: boolean;
|
|
650
|
+
/** Convenience property - aliases to metadata.status */
|
|
651
|
+
status?: 'DRAFT' | 'PUBLISHED' | 'RETIRED';
|
|
652
|
+
/** Embedded validation errors (for UI display convenience) */
|
|
653
|
+
errors?: any;
|
|
654
|
+
|
|
655
|
+
// === POPULATION (COHORT) DEFINITION ===
|
|
656
|
+
population: PopulationDefinition;
|
|
657
|
+
|
|
658
|
+
// === COLUMNS ===
|
|
659
|
+
columns: LinelistColumnDraft[];
|
|
660
|
+
|
|
661
|
+
// === PARAMETERS ===
|
|
662
|
+
parameters: LinelistParameterDraft[];
|
|
663
|
+
|
|
664
|
+
// === SORTING ===
|
|
665
|
+
sortConfig: LinelistSortConfig[];
|
|
666
|
+
|
|
667
|
+
// === ROW LIMIT ===
|
|
668
|
+
limit?: number;
|
|
669
|
+
|
|
670
|
+
// === DISPLAY SETTINGS ===
|
|
671
|
+
displaySettings: DisplaySettings;
|
|
672
|
+
|
|
673
|
+
// === VALIDATION STATE ===
|
|
674
|
+
validation: ValidationState;
|
|
675
|
+
|
|
676
|
+
// === METADATA ===
|
|
677
|
+
metadata: BuilderMetadata;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Data source information with role and configuration
|
|
682
|
+
*/
|
|
683
|
+
export type DataSourceInfo = {
|
|
684
|
+
uuid: string; // Data source UUID
|
|
685
|
+
name: string; // Display name
|
|
686
|
+
type: 'ETL' | 'SQL' | 'REFERENCE' | 'INDICATOR' | 'CORE'; // Source type
|
|
687
|
+
role: 'PRIMARY' | 'SECONDARY' | 'REFERENCE'; // Role in the report
|
|
688
|
+
tables?: string[]; // Tables used from this source
|
|
689
|
+
joinConfig?: DataSourceJoinConfig; // How this source joins to primary
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Data source join configuration
|
|
694
|
+
*/
|
|
695
|
+
export type DataSourceJoinConfig = {
|
|
696
|
+
joinType: 'LEFT_JOIN' | 'INNER_JOIN' | 'FULL_JOIN';
|
|
697
|
+
joinCondition: string; // Field name to join on
|
|
698
|
+
joinSql?: string; // Custom join SQL if needed
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Population (cohort) definition
|
|
703
|
+
* Captures both the result and the build process
|
|
704
|
+
*/
|
|
705
|
+
export type PopulationDefinition = {
|
|
706
|
+
// Which data source is the base
|
|
707
|
+
baseDataSourceUuid: string;
|
|
708
|
+
|
|
709
|
+
// How the population was built
|
|
710
|
+
buildMethod: 'SQL_BUILDER' | 'VISUAL_FILTER' | 'INDICATOR_BASED';
|
|
711
|
+
|
|
712
|
+
// SQL Builder state
|
|
713
|
+
sqlTemplate: string;
|
|
714
|
+
parameterReferences: string[];
|
|
715
|
+
|
|
716
|
+
// Visual Filter (if used)
|
|
717
|
+
visualFilter?: VisualFilterState;
|
|
718
|
+
|
|
719
|
+
// Indicator-based (if used)
|
|
720
|
+
indicatorRules?: Array<{
|
|
721
|
+
id: string;
|
|
722
|
+
indicatorUuid: string;
|
|
723
|
+
name: string;
|
|
724
|
+
conditions?: Array<{
|
|
725
|
+
key: string;
|
|
726
|
+
operator?: string;
|
|
727
|
+
value: any;
|
|
728
|
+
}>;
|
|
729
|
+
logicalOperator?: 'AND' | 'OR';
|
|
730
|
+
negate?: boolean;
|
|
731
|
+
}>;
|
|
732
|
+
|
|
733
|
+
// Build history for reproducibility
|
|
734
|
+
buildHistory?: PopulationBuildStep[];
|
|
735
|
+
|
|
736
|
+
// FilterMap for column parameter binding
|
|
737
|
+
filterMap?: FilterMap;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Population build step for tracking construction history
|
|
742
|
+
*/
|
|
743
|
+
export type PopulationBuildStep = {
|
|
744
|
+
timestamp: string; // ISO timestamp
|
|
745
|
+
action: string; // What was done
|
|
746
|
+
description: string; // Human-readable description
|
|
747
|
+
configSnapshot?: any; // Optional state at this point
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Display settings configuration
|
|
752
|
+
*/
|
|
753
|
+
export type DisplaySettings = {
|
|
754
|
+
defaultPageSize: number;
|
|
755
|
+
freezeFirstColumn: boolean;
|
|
756
|
+
freezeHeader: boolean;
|
|
757
|
+
dateDisplayFormat: string;
|
|
758
|
+
nullDisplayValue: string;
|
|
759
|
+
maxInteractiveRows: number;
|
|
760
|
+
maxExportRows: number;
|
|
761
|
+
allowedExports: Array<'CSV' | 'XLSX' | 'PDF' | 'HTML'>;
|
|
762
|
+
includeParametersInExportHeader: boolean;
|
|
763
|
+
includeGeneratedTimestamp: boolean;
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Validation state
|
|
768
|
+
*/
|
|
769
|
+
export type ValidationState = {
|
|
770
|
+
errors: LinelistValidationErrors;
|
|
771
|
+
warnings: LinelistValidationWarnings;
|
|
772
|
+
lastValidated: string; // ISO timestamp
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Builder metadata
|
|
777
|
+
*/
|
|
778
|
+
export type BuilderMetadata = {
|
|
779
|
+
createdAt: string; // ISO timestamp when draft was created
|
|
780
|
+
lastModified: string; // ISO timestamp of last change
|
|
781
|
+
lastModifiedBy?: string; // User who last modified
|
|
782
|
+
buildMethod: 'NEW' | 'TEMPLATE' | 'DUPLICATE' | 'IMPORT' | 'EDIT';
|
|
783
|
+
sourceReportUuid?: string; // If duplicated/imported, the source
|
|
784
|
+
version: number; // For migration support
|
|
785
|
+
status: 'DRAFT' | 'PUBLISHED' | 'RETIRED';
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Column draft for builder UI
|
|
790
|
+
* Version 2.0 - With full source tracking
|
|
791
|
+
*/
|
|
792
|
+
export type LinelistColumnDraft = {
|
|
793
|
+
id: string; // Unique ID for React keys
|
|
794
|
+
name: string; // Display name
|
|
795
|
+
description?: string; // Optional description
|
|
796
|
+
|
|
797
|
+
// === SOURCE INFORMATION ===
|
|
798
|
+
source: ColumnSource;
|
|
799
|
+
|
|
800
|
+
// === DATA DEFINITION ===
|
|
801
|
+
dataDefinitionType: keyof LinelistDataDefinitionMap;
|
|
802
|
+
dataDefinitionConfig: Record<string, any>;
|
|
803
|
+
|
|
804
|
+
/** @deprecated Use dataDefinitionConfig instead */
|
|
805
|
+
config?: Record<string, any>;
|
|
806
|
+
|
|
807
|
+
// === ADDITION INFO ===
|
|
808
|
+
additionInfo: ColumnAdditionInfo;
|
|
809
|
+
|
|
810
|
+
// === DISPLAY ===
|
|
811
|
+
display: ColumnDisplaySettings;
|
|
812
|
+
|
|
813
|
+
// === SORT ORDER ===
|
|
814
|
+
sortOrder: number;
|
|
815
|
+
|
|
816
|
+
// === REPEAT RESOLUTION ===
|
|
817
|
+
repeatResolution?: LinelistRepeatResolution;
|
|
818
|
+
|
|
819
|
+
// === TRANSFORMATIONS ===
|
|
820
|
+
transformations?: LinelistColumnTransformation[];
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Column source information
|
|
825
|
+
*/
|
|
826
|
+
export type ColumnSource = {
|
|
827
|
+
dataSourceUuid: string; // Which data source
|
|
828
|
+
dataSourceName: string; // Display name
|
|
829
|
+
table: string; // Source table
|
|
830
|
+
field: string; // Source field
|
|
831
|
+
fieldType: string; // Original field type
|
|
832
|
+
attributeTypeUuid?: string; // For person attributes
|
|
833
|
+
identifierTypeUuid?: string; // For identifiers
|
|
834
|
+
conceptUuid?: string; // For observations and encounter diagnoses
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Column addition information
|
|
839
|
+
*/
|
|
840
|
+
export type ColumnAdditionInfo = {
|
|
841
|
+
addedVia: 'DRAG_DROP' | 'SQL_BUILDER' | 'VISUAL_SELECTOR' | 'IMPORT' | 'CALCULATED';
|
|
842
|
+
addedAt: string; // ISO timestamp
|
|
843
|
+
addedBy?: string; // User who added
|
|
844
|
+
orderAdded: number; // Sequence in which this was added
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Column display settings
|
|
849
|
+
*/
|
|
850
|
+
export type ColumnDisplaySettings = {
|
|
851
|
+
width: number;
|
|
852
|
+
align: 'left' | 'center' | 'right';
|
|
853
|
+
sortable: boolean;
|
|
854
|
+
filterable: boolean;
|
|
855
|
+
format?: 'text' | 'date' | 'number' | 'coded' | 'boolean';
|
|
856
|
+
formatConfig?: Record<string, any>;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Enhanced column draft with full builder details
|
|
861
|
+
* Extends the base column draft with construction metadata
|
|
862
|
+
*/
|
|
863
|
+
export type LinelistColumnDraftExtended = LinelistColumnDraft & {
|
|
864
|
+
/**
|
|
865
|
+
* Source tracking - where this column came from
|
|
866
|
+
*/
|
|
867
|
+
sourceInfo?: {
|
|
868
|
+
dataSourceUuid: string; // Which data source
|
|
869
|
+
dataSourceName: string; // Display name of data source
|
|
870
|
+
table: string; // Source table (e.g., "mamba_fact_patients_latest")
|
|
871
|
+
field: string; // Source field name in the table
|
|
872
|
+
fieldPath?: string; // Full path if nested (e.g., "address.city")
|
|
873
|
+
fieldType: string; // Original field type (TEXT, NUMBER, DATE, etc.)
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* How this column was added to the report
|
|
878
|
+
*/
|
|
879
|
+
additionInfo?: {
|
|
880
|
+
addedVia: 'DRAG_DROP' | 'SQL_BUILDER' | 'VISUAL_SELECTOR' | 'IMPORT';
|
|
881
|
+
addedAt: string; // ISO timestamp
|
|
882
|
+
addedBy?: string; // User who added
|
|
883
|
+
orderAdded: number; // Sequence in which this was added
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Transformation pipeline - all transformations applied
|
|
888
|
+
* Preserves the order and configuration of each transformation
|
|
889
|
+
*/
|
|
890
|
+
transformationPipeline?: Array<{
|
|
891
|
+
type: string; // Transformation type
|
|
892
|
+
config: Record<string, any>; // Transformation configuration
|
|
893
|
+
enabled: boolean; // Whether this transformation is active
|
|
894
|
+
order: number; // Order in pipeline
|
|
895
|
+
}>;
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Preview/sample data for this column (for UX)
|
|
899
|
+
*/
|
|
900
|
+
previewData?: {
|
|
901
|
+
sampleValues: string[]; // Example values from actual data
|
|
902
|
+
nullCount?: number; // How many nulls in sample
|
|
903
|
+
uniqueCount?: number; // How many unique values
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Row grain options - what one row represents in the linelist
|
|
909
|
+
*/
|
|
910
|
+
export type LinelistRowGrain =
|
|
911
|
+
| 'PATIENT'
|
|
912
|
+
| 'ENCOUNTER'
|
|
913
|
+
| 'OBSERVATION'
|
|
914
|
+
| 'PROGRAM_ENROLLMENT'
|
|
915
|
+
| 'APPOINTMENT'
|
|
916
|
+
| 'ORDER';
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Repeat resolution strategy for fields that can return multiple values
|
|
920
|
+
*/
|
|
921
|
+
export type LinelistRepeatResolution = {
|
|
922
|
+
strategy: RepeatResolutionStrategy;
|
|
923
|
+
orderBy?: string; // Field to use for ordering (e.g., return_visit_date)
|
|
924
|
+
restrictToPeriod?: boolean; // Only use values within reporting period
|
|
925
|
+
ignoreVoided?: boolean; // Ignore voided records
|
|
926
|
+
tieBreakField?: string; // Field to use as tie-breaker
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Available repeat resolution strategies
|
|
931
|
+
*/
|
|
932
|
+
export type RepeatResolutionStrategy =
|
|
933
|
+
| 'LATEST' // Most recent value based on order by field
|
|
934
|
+
| 'EARLIEST' // Oldest value based on order by field
|
|
935
|
+
| 'HIGHEST' // Maximum value
|
|
936
|
+
| 'LOWEST' // Minimum value
|
|
937
|
+
| 'CLOSEST_TO_START' // Value closest to report start date
|
|
938
|
+
| 'CLOSEST_TO_END' // Value closest to report end date
|
|
939
|
+
| 'FIRST_WITHIN_PERIOD' // First value within reporting period
|
|
940
|
+
| 'LAST_WITHIN_PERIOD' // Last value within reporting period
|
|
941
|
+
| 'CONCATENATE' // Concatenate all values with separator
|
|
942
|
+
| 'ALL_VALUES' // Return all values and change row grain (one row per value)
|
|
943
|
+
| 'NONE'; // Explicitly choose not to resolve (advanced users only)
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Column transformation types
|
|
947
|
+
* Transformations are applied in sequence to column values
|
|
948
|
+
*/
|
|
949
|
+
export type LinelistColumnTransformation =
|
|
950
|
+
| LinelistDateFormatTransformation
|
|
951
|
+
| LinelistAgeCalculationTransformation
|
|
952
|
+
| LinelistCodedToDisplayTransformation
|
|
953
|
+
| LinelistBooleanMappingTransformation
|
|
954
|
+
| LinelistReplaceNullTransformation
|
|
955
|
+
| LinelistNumericRoundingTransformation
|
|
956
|
+
| LinelistCategorizeNumericTransformation
|
|
957
|
+
| LinelistConcatenateTextTransformation
|
|
958
|
+
| LinelistMaskIdentifierTransformation;
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Base transformation type
|
|
962
|
+
*/
|
|
963
|
+
type BaseTransformation<T extends string = string> = {
|
|
964
|
+
type: T;
|
|
965
|
+
enabled?: boolean;
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Date format transformation
|
|
970
|
+
* Converts date values to a specified format
|
|
971
|
+
*/
|
|
972
|
+
export type LinelistDateFormatTransformation = BaseTransformation<'DATE_FORMAT'> & {
|
|
973
|
+
format: string; // e.g., "dd/MM/yyyy", "yyyy-MM-dd", "MMM dd, yyyy"
|
|
974
|
+
inputFormat?: string; // If input needs parsing
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Age calculation transformation
|
|
979
|
+
* Calculates age as of a specific date
|
|
980
|
+
*/
|
|
981
|
+
export type LinelistAgeCalculationTransformation = BaseTransformation<'AGE_ON_DATE'> & {
|
|
982
|
+
dateField?: string; // Field to calculate age as of (default: endDate)
|
|
983
|
+
years?: boolean; // Include years
|
|
984
|
+
months?: boolean; // Include months
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Coded to display name transformation
|
|
989
|
+
* Converts coded values (UUIDs) to their display names
|
|
990
|
+
*/
|
|
991
|
+
export type LinelistCodedToDisplayTransformation = BaseTransformation<'CODED_TO_DISPLAY'> & {
|
|
992
|
+
fallback?: string; // Value to use if lookup fails
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Boolean mapping transformation
|
|
997
|
+
* Maps boolean values to custom display strings
|
|
998
|
+
*/
|
|
999
|
+
export type LinelistBooleanMappingTransformation = BaseTransformation<'BOOLEAN_MAPPING'> & {
|
|
1000
|
+
trueValue: string; // Display value for true (e.g., "Yes")
|
|
1001
|
+
falseValue: string; // Display value for false (e.g., "No")
|
|
1002
|
+
nullValue?: string; // Display value for null (e.g., "Unknown")
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Replace null transformation
|
|
1007
|
+
* Replaces null/empty values with a specified value
|
|
1008
|
+
*/
|
|
1009
|
+
export type LinelistReplaceNullTransformation = BaseTransformation<'REPLACE_NULL'> & {
|
|
1010
|
+
replacementValue: string; // Value to use when null
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Numeric rounding transformation
|
|
1015
|
+
* Rounds numeric values to specified precision
|
|
1016
|
+
*/
|
|
1017
|
+
export type LinelistNumericRoundingTransformation = BaseTransformation<'NUMERIC_ROUNDING'> & {
|
|
1018
|
+
decimalPlaces: number; // Number of decimal places (0-10)
|
|
1019
|
+
roundingMode?: 'UP' | 'DOWN' | 'HALF_UP' | 'HALF_DOWN'; // Rounding strategy
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Categorize numeric transformation
|
|
1024
|
+
* Categorizes numeric values into ranges
|
|
1025
|
+
*/
|
|
1026
|
+
export type LinelistCategorizeNumericTransformation = BaseTransformation<'CATEGORIZE_NUMERIC'> & {
|
|
1027
|
+
ranges: Array<{
|
|
1028
|
+
min: number;
|
|
1029
|
+
max: number;
|
|
1030
|
+
label: string;
|
|
1031
|
+
}>;
|
|
1032
|
+
defaultValue?: string; // Label for values not in any range
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Concatenate text transformation
|
|
1037
|
+
* Concatenates multiple field values
|
|
1038
|
+
*/
|
|
1039
|
+
export type LinelistConcatenateTextTransformation = BaseTransformation<'CONCATENATE_TEXT'> & {
|
|
1040
|
+
fields: string[]; // Field names to concatenate
|
|
1041
|
+
separator?: string; // Separator between values (default: ", ")
|
|
1042
|
+
includeNulls?: boolean; // Whether to include null values
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Mask identifier transformation
|
|
1047
|
+
* Masks sensitive identifiers for privacy
|
|
1048
|
+
*/
|
|
1049
|
+
export type LinelistMaskIdentifierTransformation = BaseTransformation<'MASK_IDENTIFIER'> & {
|
|
1050
|
+
maskChar?: string; // Character to use for masking (default: "*")
|
|
1051
|
+
visibleStart?: number; // Number of characters to show at start
|
|
1052
|
+
visibleEnd?: number; // Number of characters to show at end
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Map of data definition type keys to their full types
|
|
1057
|
+
*/
|
|
1058
|
+
export type LinelistDataDefinitionMap = {
|
|
1059
|
+
IDENTIFIER: LinelistIdentifierDefinition;
|
|
1060
|
+
PERSON_NAME: LinelistPersonNameDefinition;
|
|
1061
|
+
PERSON_ATTRIBUTE: LinelistPersonAttributeDefinition;
|
|
1062
|
+
CALCULATION: LinelistCalculationDefinition;
|
|
1063
|
+
SQL: LinelistSqlDefinition;
|
|
1064
|
+
PERSON_ADDRESS: LinelistPersonAddressDefinition;
|
|
1065
|
+
CONVERTER: LinelistConverterDefinition;
|
|
1066
|
+
OBSERVATION: LinelistObservationDefinition;
|
|
1067
|
+
ENCOUNTER_DIAGNOSIS: LinelistEncounterDiagnosisDefinition;
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Builder panel options
|
|
1072
|
+
*/
|
|
1073
|
+
export type LinelistBuilderPanel =
|
|
1074
|
+
| 'basics'
|
|
1075
|
+
| 'cohort'
|
|
1076
|
+
| 'columns'
|
|
1077
|
+
| 'sort'
|
|
1078
|
+
| 'parameters'
|
|
1079
|
+
| 'display-export'
|
|
1080
|
+
| 'preview'
|
|
1081
|
+
| 'review';
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Validation errors for linelist draft
|
|
1085
|
+
*/
|
|
1086
|
+
export type LinelistValidationErrors = {
|
|
1087
|
+
name?: string;
|
|
1088
|
+
categoryUuid?: string;
|
|
1089
|
+
themeUuid?: string;
|
|
1090
|
+
dataSources?: string;
|
|
1091
|
+
rowGrain?: string;
|
|
1092
|
+
population?: string;
|
|
1093
|
+
columns?: string;
|
|
1094
|
+
general?: string;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Validation warnings for linelist draft (non-blocking)
|
|
1099
|
+
*/
|
|
1100
|
+
export type LinelistValidationWarnings = {
|
|
1101
|
+
population?: string;
|
|
1102
|
+
columns?: string;
|
|
1103
|
+
performance?: string;
|
|
1104
|
+
category?: string;
|
|
1105
|
+
theme?: string;
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Generate warnings for a linelist draft (non-blocking issues)
|
|
1110
|
+
*/
|
|
1111
|
+
export function generateLinelistWarnings(draft: LinelistReportDraft): LinelistValidationWarnings {
|
|
1112
|
+
const warnings: LinelistValidationWarnings = {};
|
|
1113
|
+
|
|
1114
|
+
// Warn about missing category (recommended but not required)
|
|
1115
|
+
if (!draft.categoryUuid?.trim()) {
|
|
1116
|
+
warnings.category = 'No category selected. Reports are easier to find when organized into categories.';
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Warn about population definition based on build method
|
|
1120
|
+
const buildMethod = draft.population?.buildMethod || 'SQL_BUILDER';
|
|
1121
|
+
const indicatorRules = draft.population?.indicatorRules || draft.indicatorRules;
|
|
1122
|
+
|
|
1123
|
+
// For SQL_BUILDER and VISUAL_FILTER modes, warn about missing SQL
|
|
1124
|
+
if (buildMethod !== 'INDICATOR_BASED' && !draft.population.sqlTemplate?.trim()) {
|
|
1125
|
+
warnings.population = 'No population SQL defined. Add SQL to define the patient cohort.';
|
|
1126
|
+
}
|
|
1127
|
+
// For INDICATOR_BASED mode, warn about missing indicator rules
|
|
1128
|
+
else if (buildMethod === 'INDICATOR_BASED' && (!indicatorRules || indicatorRules.length === 0)) {
|
|
1129
|
+
warnings.population = 'No indicator rules defined. Add at least one indicator to define the patient cohort.';
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// SQL warnings
|
|
1133
|
+
if (draft.population.sqlTemplate?.trim()) {
|
|
1134
|
+
const upperSql = draft.population.sqlTemplate.toUpperCase();
|
|
1135
|
+
|
|
1136
|
+
// Recommend DISTINCT for patient grain
|
|
1137
|
+
if (draft.rowGrain === 'PATIENT' && !upperSql.includes('DISTINCT')) {
|
|
1138
|
+
warnings.population = 'Consider using SELECT DISTINCT to avoid duplicate patients';
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Check for potential performance issues
|
|
1142
|
+
if (upperSql.includes('SELECT *')) {
|
|
1143
|
+
warnings.performance = 'Avoid SELECT * for better performance';
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Column count warning
|
|
1148
|
+
if (draft.columns.length > 50) {
|
|
1149
|
+
warnings.columns = 'Large number of columns may impact performance';
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// SQL per column warning
|
|
1153
|
+
const sqlColumnCount = draft.columns.filter((col) => col.dataDefinitionType === 'SQL').length;
|
|
1154
|
+
if (sqlColumnCount > 10) {
|
|
1155
|
+
warnings.performance = `Many SQL columns (${sqlColumnCount}) may execute slowly. Consider using ETL joins.`;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return warnings;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Report definition DTO with linelist config
|
|
1163
|
+
* Used for API responses
|
|
1164
|
+
*/
|
|
1165
|
+
export type LinelistReportDto = {
|
|
1166
|
+
uuid: string;
|
|
1167
|
+
name: string;
|
|
1168
|
+
description?: string;
|
|
1169
|
+
code?: string;
|
|
1170
|
+
reportType: 'LINE_LIST';
|
|
1171
|
+
configJson?: string;
|
|
1172
|
+
metaJson?: string;
|
|
1173
|
+
retired?: boolean;
|
|
1174
|
+
compiledReportDefinitionUuid?: string; // Compiled report definition UUID from backend
|
|
1175
|
+
compiledReportDesignUuid?: string;
|
|
1176
|
+
lastCompiledAt?: string;
|
|
1177
|
+
compileStatus?: string;
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Extract SQL template from an indicator definition
|
|
1182
|
+
* Handles multiple possible locations for the SQL template
|
|
1183
|
+
*/
|
|
1184
|
+
function extractIndicatorSql(indicator: { sqlTemplate?: string | null; configJson?: string | null } | undefined): string {
|
|
1185
|
+
if (!indicator) return '';
|
|
1186
|
+
|
|
1187
|
+
// Try direct sqlTemplate first
|
|
1188
|
+
if (indicator.sqlTemplate?.trim()) {
|
|
1189
|
+
return indicator.sqlTemplate.trim();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Try parsing configJson
|
|
1193
|
+
if (indicator.configJson) {
|
|
1194
|
+
try {
|
|
1195
|
+
const parsed = JSON.parse(indicator.configJson);
|
|
1196
|
+
|
|
1197
|
+
// Try various possible paths
|
|
1198
|
+
const paths = [
|
|
1199
|
+
parsed.sqlPreview,
|
|
1200
|
+
parsed.sqlTemplate,
|
|
1201
|
+
parsed.base?.sqlPreview,
|
|
1202
|
+
parsed.base?.sqlTemplate,
|
|
1203
|
+
parsed.authoring?.base?.sqlPreview,
|
|
1204
|
+
parsed.authoring?.base?.sqlTemplate,
|
|
1205
|
+
parsed.baseIndicator?.sqlPreview,
|
|
1206
|
+
parsed.baseIndicator?.sqlTemplate,
|
|
1207
|
+
];
|
|
1208
|
+
|
|
1209
|
+
for (const sql of paths) {
|
|
1210
|
+
if (typeof sql === 'string' && sql.trim()) {
|
|
1211
|
+
return sql.trim();
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
console.warn('Failed to parse indicator configJson:', e);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return '';
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Convert COUNT SQL to population SQL
|
|
1224
|
+
* Handles both simple COUNT queries and composite WITH CTE queries
|
|
1225
|
+
*/
|
|
1226
|
+
function convertCountSqlToPopulationSql(countSql: string): string {
|
|
1227
|
+
const sql = countSql.trim().replace(/;+\s*$/, '');
|
|
1228
|
+
if (!sql) return '';
|
|
1229
|
+
|
|
1230
|
+
// Case 1: COUNT(DISTINCT column) - convert to SELECT DISTINCT column
|
|
1231
|
+
const distinctPattern = /SELECT\s+COUNT\s*\(\s*DISTINCT\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)\s*\)\s+AS\s+(\w+)\s*/i;
|
|
1232
|
+
const distinctMatch = sql.match(distinctPattern);
|
|
1233
|
+
if (distinctMatch) {
|
|
1234
|
+
const columnRef = distinctMatch[1];
|
|
1235
|
+
const afterSelect = sql.substring(distinctMatch[0].length);
|
|
1236
|
+
if (columnRef.includes('.')) {
|
|
1237
|
+
// Extract column name from table.column reference
|
|
1238
|
+
const colName = columnRef.split('.')[1];
|
|
1239
|
+
return `SELECT DISTINCT ${columnRef} AS ${colName} ${afterSelect}`.trim();
|
|
1240
|
+
}
|
|
1241
|
+
// Extract table alias from FROM clause
|
|
1242
|
+
const fromPattern = /FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(?:AS\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\b/i;
|
|
1243
|
+
const fromMatch = afterSelect.match(fromPattern);
|
|
1244
|
+
const tableAlias = fromMatch ? fromMatch[2] : 'a';
|
|
1245
|
+
return `SELECT DISTINCT ${tableAlias}.${columnRef} AS ${columnRef} ${afterSelect}`.trim();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Case 2: Composite WITH ... SELECT COUNT(*) FROM (...) X
|
|
1249
|
+
const withPattern = /^WITH\s+([\s\S]*?)\s*SELECT\s+COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s+FROM\s*\(\s*(SELECT[\s\S]*?)\)\s*X\b/i;
|
|
1250
|
+
const withMatch = sql.match(withPattern);
|
|
1251
|
+
if (withMatch) {
|
|
1252
|
+
const ctes = withMatch[1];
|
|
1253
|
+
const inner = withMatch[2].trim();
|
|
1254
|
+
return `WITH ${ctes}\nSELECT DISTINCT pop.patient_id\nFROM (\n ${inner}\n) pop`;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Case 3: Simple SELECT COUNT(*) AS total FROM ...
|
|
1258
|
+
const simplePattern = /SELECT\s+COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s*/i;
|
|
1259
|
+
const simpleMatch = sql.match(simplePattern);
|
|
1260
|
+
if (simpleMatch) {
|
|
1261
|
+
const afterCount = sql.substring(simpleMatch[0].length);
|
|
1262
|
+
// Extract table alias and detect patient_id column name
|
|
1263
|
+
const fromPattern = /FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(?:AS\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\b/i;
|
|
1264
|
+
const fromMatch = afterCount.match(fromPattern);
|
|
1265
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1266
|
+
const tableAlias = fromMatch ? fromMatch[2] : 'a';
|
|
1267
|
+
|
|
1268
|
+
// Detect the patient_id column name from the WHERE clause or JOIN conditions
|
|
1269
|
+
// Common column names: patient_id, patient_id, person_id
|
|
1270
|
+
const columnPatterns = [
|
|
1271
|
+
/\b(?:${tableAlias}\.)?(patient_id|patient_id|person_id)\b/i,
|
|
1272
|
+
// Also check JOIN conditions for column references
|
|
1273
|
+
/ON\s+\w+\.(?:patient_id|patient_id|person_id)\s*=\s*${tableAlias}\.(patient_id|patient_id|person_id)/i,
|
|
1274
|
+
];
|
|
1275
|
+
|
|
1276
|
+
let patientIdColumnName = 'patient_id'; // default
|
|
1277
|
+
for (const pattern of columnPatterns) {
|
|
1278
|
+
const match = afterCount.match(pattern);
|
|
1279
|
+
if (match) {
|
|
1280
|
+
patientIdColumnName = match[1] || match[2] || 'patient_id';
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Use the detected column name consistently
|
|
1286
|
+
return `SELECT DISTINCT ${tableAlias}.${patientIdColumnName} AS patient_id ${afterCount}`.trim();
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Fallback: couldn't parse, return as-is (will likely fail in backend)
|
|
1290
|
+
return sql;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Combine multiple population SQLs with AND/OR logic
|
|
1295
|
+
*/
|
|
1296
|
+
type PopulationSqlPart = {
|
|
1297
|
+
sql: string;
|
|
1298
|
+
operator: 'AND' | 'OR';
|
|
1299
|
+
negate?: boolean;
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
function combinePopulationSqls(parts: PopulationSqlPart[]): string {
|
|
1303
|
+
if (parts.length === 0) return '';
|
|
1304
|
+
if (parts.length === 1) return parts[0].sql;
|
|
1305
|
+
|
|
1306
|
+
// Separate included and excluded populations
|
|
1307
|
+
const included: string[] = [];
|
|
1308
|
+
const excluded: string[] = [];
|
|
1309
|
+
|
|
1310
|
+
for (const part of parts) {
|
|
1311
|
+
if (part.negate) {
|
|
1312
|
+
excluded.push(part.sql);
|
|
1313
|
+
} else {
|
|
1314
|
+
included.push(part.sql);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Build the base query from included populations
|
|
1319
|
+
let baseQuery = '';
|
|
1320
|
+
if (included.length === 0) {
|
|
1321
|
+
// If all are excluded, we need a different approach
|
|
1322
|
+
// Use all patients and subtract excluded ones
|
|
1323
|
+
baseQuery = 'SELECT DISTINCT patient_id AS patient_id FROM mamba_fact_patients_latest WHERE patient_id IS NOT NULL';
|
|
1324
|
+
} else if (included.length === 1) {
|
|
1325
|
+
baseQuery = included[0];
|
|
1326
|
+
} else {
|
|
1327
|
+
// Combine included populations with INTERSECT (AND) or UNION (OR)
|
|
1328
|
+
// For now, use INTERSECT as default - should be improved to track operators
|
|
1329
|
+
baseQuery = `(${included.join('\n INTERSECT\n ')})`;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Apply exclusions using EXCEPT
|
|
1333
|
+
if (excluded.length > 0) {
|
|
1334
|
+
for (const excludeSql of excluded) {
|
|
1335
|
+
baseQuery = `(${baseQuery}\n EXCEPT\n ${excludeSql})`;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return baseQuery;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Convert a draft to the final configuration format
|
|
1344
|
+
*
|
|
1345
|
+
* @param draft - The linelist report draft
|
|
1346
|
+
* @param indicators - Optional map of indicator UUIDs to indicator definitions for INDICATOR mode
|
|
1347
|
+
*/
|
|
1348
|
+
export function draftToConfig(
|
|
1349
|
+
draft: LinelistReportDraft,
|
|
1350
|
+
indicators?: Map<string, { sqlTemplate?: string | null; configJson?: string | null }>
|
|
1351
|
+
): LinelistReportDefinitionConfig {
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Build data sources array from draft columns.
|
|
1355
|
+
* Groups columns by their ACTUAL source table (extracted from the SQL
|
|
1356
|
+
* expression) so the data source list accurately reflects where each
|
|
1357
|
+
* column's data comes from.
|
|
1358
|
+
*/
|
|
1359
|
+
const buildDataSources = (): DataSourceConfig[] => {
|
|
1360
|
+
const sourceMap = new Map<string, DataSourceConfig>();
|
|
1361
|
+
|
|
1362
|
+
// Determine the primary data source uuid (for role assignment)
|
|
1363
|
+
// If no PRIMARY is set in draft, we'll assign one after building the map
|
|
1364
|
+
const primaryDsUuid = draft.dataSources.find(ds => ds.role === 'PRIMARY')?.uuid || '';
|
|
1365
|
+
let hasAssignedPrimary = !!primaryDsUuid;
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Extract the actual source table from a column's SQL expression.
|
|
1369
|
+
* - Simple reference like `table`.`column` → returns the table name
|
|
1370
|
+
* - Custom query (SELECT ... / contains :patientId) → returns 'custom_sql'
|
|
1371
|
+
* - Cannot determine → returns 'unknown'
|
|
1372
|
+
*/
|
|
1373
|
+
const extractSourceTable = (sql?: string): string => {
|
|
1374
|
+
if (!sql) return 'unknown';
|
|
1375
|
+
const trimmed = sql.trim();
|
|
1376
|
+
// Custom SQL query (per-row subquery)
|
|
1377
|
+
if (/^SELECT\s/i.test(trimmed) || /:(patientId|patient_id)\b/i.test(trimmed)) {
|
|
1378
|
+
return 'custom_sql';
|
|
1379
|
+
}
|
|
1380
|
+
// Simple table.column reference (with or without backticks)
|
|
1381
|
+
const match = trimmed.match(/`?(\w+)`?\.\s*`?\w+`?/);
|
|
1382
|
+
return match ? match[1] : 'unknown';
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
// Always include core OpenMRS sources if used
|
|
1386
|
+
const hasPersonAttributes = draft.columns.some(col =>
|
|
1387
|
+
col.dataDefinitionType === 'PERSON_ATTRIBUTE'
|
|
1388
|
+
);
|
|
1389
|
+
const hasIdentifiers = draft.columns.some(col =>
|
|
1390
|
+
col.dataDefinitionType === 'IDENTIFIER'
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
if (hasPersonAttributes) {
|
|
1394
|
+
sourceMap.set('person_attributes', {
|
|
1395
|
+
uuid: 'person_attributes',
|
|
1396
|
+
name: 'Person Attributes',
|
|
1397
|
+
type: 'CORE',
|
|
1398
|
+
role: 'REFERENCE',
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (hasIdentifiers) {
|
|
1403
|
+
sourceMap.set('patient_identifiers', {
|
|
1404
|
+
uuid: 'patient_identifiers',
|
|
1405
|
+
name: 'Patient Identifiers',
|
|
1406
|
+
type: 'CORE',
|
|
1407
|
+
role: 'REFERENCE',
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Track the first non-REFERENCE source for default PRIMARY assignment
|
|
1412
|
+
let firstNonReferenceSource: string | null = null;
|
|
1413
|
+
|
|
1414
|
+
// Group columns by their actual source table
|
|
1415
|
+
draft.columns.forEach(col => {
|
|
1416
|
+
if (col.dataDefinitionType === 'PERSON_ATTRIBUTE' || col.dataDefinitionType === 'IDENTIFIER') {
|
|
1417
|
+
return; // Already handled above
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Resolve the real source table
|
|
1421
|
+
let table: string;
|
|
1422
|
+
if (col.dataDefinitionType === 'CALCULATION') {
|
|
1423
|
+
table = 'calculated';
|
|
1424
|
+
} else if (col.dataDefinitionType === 'OBSERVATION') {
|
|
1425
|
+
table = 'observations';
|
|
1426
|
+
} else if (col.dataDefinitionType === 'ENCOUNTER_DIAGNOSIS') {
|
|
1427
|
+
table = 'encounter_diagnoses';
|
|
1428
|
+
} else if (col.dataDefinitionType === 'PERSON_ADDRESS') {
|
|
1429
|
+
table = 'person_address';
|
|
1430
|
+
} else if (col.dataDefinitionType === 'PERSON_NAME') {
|
|
1431
|
+
table = 'person_name';
|
|
1432
|
+
} else {
|
|
1433
|
+
// For SQL columns, derive the table from the SQL expression
|
|
1434
|
+
table = extractSourceTable(col.dataDefinitionConfig?.sql);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// The data source uuid IS the table name (accurate grouping)
|
|
1438
|
+
const dsUuid = table;
|
|
1439
|
+
const isPrimary = dsUuid === primaryDsUuid;
|
|
1440
|
+
const dsType = dsUuid === 'custom_sql'
|
|
1441
|
+
? 'SQL'
|
|
1442
|
+
: dsUuid === 'calculated'
|
|
1443
|
+
? 'SQL'
|
|
1444
|
+
: dsUuid.startsWith('mamba_') ? 'ETL' : 'SQL';
|
|
1445
|
+
|
|
1446
|
+
// Map table names to user-friendly data source names
|
|
1447
|
+
const dataSourceNames: Record<string, string> = {
|
|
1448
|
+
'observations': 'Observations',
|
|
1449
|
+
'encounter_diagnoses': 'Encounter Diagnoses',
|
|
1450
|
+
'person_address': 'Person Address',
|
|
1451
|
+
'person_name': 'Person Names',
|
|
1452
|
+
'calculated': 'Calculated Fields',
|
|
1453
|
+
'custom_sql': 'Custom SQL',
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
if (!sourceMap.has(dsUuid)) {
|
|
1457
|
+
sourceMap.set(dsUuid, {
|
|
1458
|
+
uuid: dsUuid,
|
|
1459
|
+
name: dataSourceNames[dsUuid] || dsUuid,
|
|
1460
|
+
type: dsType as DataSourceConfig['type'],
|
|
1461
|
+
role: isPrimary ? 'PRIMARY' : 'SECONDARY',
|
|
1462
|
+
columns: {},
|
|
1463
|
+
});
|
|
1464
|
+
// Track the first non-REFERENCE source for default PRIMARY assignment
|
|
1465
|
+
// (CORE sources like person_attributes/patient_identifiers are already handled above)
|
|
1466
|
+
if (!hasAssignedPrimary && !firstNonReferenceSource) {
|
|
1467
|
+
firstNonReferenceSource = dsUuid;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const ds = sourceMap.get(dsUuid);
|
|
1472
|
+
if (ds && ds.columns) {
|
|
1473
|
+
ds.columns[col.name] = {
|
|
1474
|
+
name: col.name,
|
|
1475
|
+
type: col.source?.fieldType || 'UNKNOWN',
|
|
1476
|
+
sourceTable: table,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// If no primary was assigned and we have a non-REFERENCE source, make it PRIMARY
|
|
1482
|
+
if (!hasAssignedPrimary && firstNonReferenceSource) {
|
|
1483
|
+
const ds = sourceMap.get(firstNonReferenceSource);
|
|
1484
|
+
if (ds) {
|
|
1485
|
+
ds.role = 'PRIMARY';
|
|
1486
|
+
hasAssignedPrimary = true;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
return Array.from(sourceMap.values());
|
|
1491
|
+
};
|
|
1492
|
+
/**
|
|
1493
|
+
* Build population SQL from indicator rules
|
|
1494
|
+
*/
|
|
1495
|
+
const buildIndicatorPopulationSql = (): string => {
|
|
1496
|
+
// Check both locations for indicator rules (population.indicatorRules or legacy top-level)
|
|
1497
|
+
const indicatorRules = draft.population.indicatorRules || draft.indicatorRules;
|
|
1498
|
+
if (!indicatorRules || indicatorRules.length === 0) {
|
|
1499
|
+
return draft.population.sqlTemplate || '';
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const populationSqlParts: PopulationSqlPart[] = [];
|
|
1503
|
+
|
|
1504
|
+
for (const rule of indicatorRules) {
|
|
1505
|
+
if (!rule.indicatorUuid || !indicators?.has(rule.indicatorUuid)) {
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const indicator = indicators.get(rule.indicatorUuid);
|
|
1510
|
+
const countSql = extractIndicatorSql(indicator);
|
|
1511
|
+
|
|
1512
|
+
if (!countSql) {
|
|
1513
|
+
console.warn(`No SQL found for indicator: ${rule.name} (${rule.indicatorUuid})`);
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Convert COUNT SQL to population SQL
|
|
1518
|
+
const populationSql = convertCountSqlToPopulationSql(countSql);
|
|
1519
|
+
|
|
1520
|
+
if (!populationSql) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
populationSqlParts.push({ sql: populationSql, operator: rule.logicalOperator || 'AND', negate: rule.negate });
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (populationSqlParts.length === 0) {
|
|
1528
|
+
return draft.population.sqlTemplate || '';
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Combine population SQL parts with AND/OR logic
|
|
1532
|
+
return combinePopulationSqls(populationSqlParts);
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// Get the final cohort SQL based on population mode
|
|
1536
|
+
const getCohortSql = (): string => {
|
|
1537
|
+
switch (draft.populationMode) {
|
|
1538
|
+
case 'INDICATOR':
|
|
1539
|
+
return buildIndicatorPopulationSql();
|
|
1540
|
+
case 'HYBRID': {
|
|
1541
|
+
// Combine existing cohort SQL with indicator SQL
|
|
1542
|
+
const indicatorSql = buildIndicatorPopulationSql();
|
|
1543
|
+
if (indicatorSql && draft.population.sqlTemplate) {
|
|
1544
|
+
return `SELECT DISTINCT base.patient_id AS patient_id
|
|
1545
|
+
FROM (${draft.population.sqlTemplate}) base
|
|
1546
|
+
INNER JOIN (${indicatorSql}) indicators ON indicators.patient_id = base.patient_id`;
|
|
1547
|
+
}
|
|
1548
|
+
return indicatorSql || draft.population.sqlTemplate || '';
|
|
1549
|
+
}
|
|
1550
|
+
default:
|
|
1551
|
+
return draft.population.sqlTemplate || '';
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const finalCohortSql = getCohortSql();
|
|
1556
|
+
|
|
1557
|
+
// Convert column drafts to full column definitions
|
|
1558
|
+
const columns: LinelistColumnDefinition[] = draft.columns
|
|
1559
|
+
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
1560
|
+
.map((col, index) => {
|
|
1561
|
+
const columnDef: LinelistColumnDefinition = {
|
|
1562
|
+
name: col.name,
|
|
1563
|
+
dataDefinition: buildDataDefinition(col.dataDefinitionType, col.dataDefinitionConfig),
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
// Add _metadata with column position for backend ordering
|
|
1567
|
+
(columnDef as any)._metadata = {
|
|
1568
|
+
id: col.id,
|
|
1569
|
+
position: col.sortOrder ?? index,
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
// Add repeat resolution if configured
|
|
1573
|
+
if (col.repeatResolution) {
|
|
1574
|
+
(columnDef as any).repeatResolution = col.repeatResolution;
|
|
1575
|
+
} else {
|
|
1576
|
+
// For SQL columns that may return multiple values, add default repeat resolution
|
|
1577
|
+
// if one isn't already configured. This prevents backend compilation errors.
|
|
1578
|
+
const sql = col.dataDefinitionConfig.sql || '';
|
|
1579
|
+
|
|
1580
|
+
// Check if SQL uses an aggregate function (always returns single value)
|
|
1581
|
+
const hasAggregate = /^(?:SELECT\s+(?:DISTINCT\s+)?(?:COUNT|MAX|MIN|SUM|AVG|GROUP_CONCAT|ARRAY_AGG|STRING_AGG|JSON_AGG)\s*\()/i.test(sql) ||
|
|
1582
|
+
/GROUP\s+BY\s+/i.test(sql);
|
|
1583
|
+
|
|
1584
|
+
// Check if SQL already has LIMIT 1
|
|
1585
|
+
const hasLimitOne = /\bLIMIT\s+1\b/i.test(sql);
|
|
1586
|
+
|
|
1587
|
+
// Only add repeat resolution if:
|
|
1588
|
+
// 1. It references patient-related tables (encounter, observation, etc.)
|
|
1589
|
+
// 2. It's NOT an aggregate query
|
|
1590
|
+
// 3. It doesn't have LIMIT 1
|
|
1591
|
+
const isPotentiallyRepeated = col.dataDefinitionType === 'SQL' &&
|
|
1592
|
+
/appointment|observation|encounter|program_enrollment|visit/i.test(sql) &&
|
|
1593
|
+
!hasAggregate && !hasLimitOne;
|
|
1594
|
+
|
|
1595
|
+
if (isPotentiallyRepeated) {
|
|
1596
|
+
// Use the SQL's ORDER BY field if present, otherwise use a default
|
|
1597
|
+
const orderByMatch = sql.match(/ORDER BY\s+([a-z_][a-z0-9_]*)/i);
|
|
1598
|
+
const orderByField = orderByMatch ? orderByMatch[1] : 'encounter_date';
|
|
1599
|
+
|
|
1600
|
+
(columnDef as any).repeatResolution = {
|
|
1601
|
+
strategy: 'LATEST',
|
|
1602
|
+
orderBy: orderByField,
|
|
1603
|
+
restrictToPeriod: false,
|
|
1604
|
+
ignoreVoided: true,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
return columnDef;
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// Convert parameter drafts to full parameter definitions
|
|
1613
|
+
const parameters: LinelistParameter[] = (draft.parameters || [])
|
|
1614
|
+
.sort((a, b) => a.displayOrder - b.displayOrder)
|
|
1615
|
+
.map((param) => ({
|
|
1616
|
+
name: param.name,
|
|
1617
|
+
label: param.label,
|
|
1618
|
+
type: param.type,
|
|
1619
|
+
required: param.required,
|
|
1620
|
+
...(param.config && { config: param.config }),
|
|
1621
|
+
}));
|
|
1622
|
+
|
|
1623
|
+
// Default to startDate/endDate if no parameters defined
|
|
1624
|
+
const finalParameters = parameters.length > 0
|
|
1625
|
+
? parameters
|
|
1626
|
+
: [
|
|
1627
|
+
{ name: 'startDate', label: 'Start Date', type: 'DATE' as const, required: true },
|
|
1628
|
+
{ name: 'endDate', label: 'End Date', type: 'DATE' as const, required: true },
|
|
1629
|
+
];
|
|
1630
|
+
|
|
1631
|
+
const config: LinelistReportDefinitionConfig = {
|
|
1632
|
+
version: 3, // Updated to version 3 for population sources support
|
|
1633
|
+
type: 'LINE_LIST',
|
|
1634
|
+
categoryUuid: draft.categoryUuid,
|
|
1635
|
+
// Include both for compatibility - new code uses dataSources
|
|
1636
|
+
dataSourceUuid: draft.dataSources.find(ds => ds.role === 'PRIMARY')?.uuid,
|
|
1637
|
+
dataSources: buildDataSources(),
|
|
1638
|
+
rowGrain: draft.rowGrain,
|
|
1639
|
+
templateId: draft.templateId,
|
|
1640
|
+
parameters: finalParameters,
|
|
1641
|
+
// Preserve builder metadata in configJson so the editor can reconstruct
|
|
1642
|
+
// the draft (selected indicators, build method) when re-opening for edit.
|
|
1643
|
+
// Read indicatorRules from both the new (population) and legacy (top-level)
|
|
1644
|
+
// fields — the UI writes to the top-level field.
|
|
1645
|
+
// Derive buildMethod from whether indicators are actually present, since the
|
|
1646
|
+
// UI may not always keep population.buildMethod in sync.
|
|
1647
|
+
indicatorRules: draft.population.indicatorRules || draft.indicatorRules,
|
|
1648
|
+
buildMethod: ((draft.population.indicatorRules || draft.indicatorRules || []).length > 0)
|
|
1649
|
+
? 'INDICATOR_BASED'
|
|
1650
|
+
: (draft.population.buildMethod || 'SQL_BUILDER'),
|
|
1651
|
+
baseCohortDefinition: {
|
|
1652
|
+
type: 'SQL',
|
|
1653
|
+
name: draft.population.buildMethod === 'INDICATOR_BASED'
|
|
1654
|
+
? 'Indicator-based Patient Cohort'
|
|
1655
|
+
: 'Patient Cohort',
|
|
1656
|
+
config: {
|
|
1657
|
+
sql: finalCohortSql,
|
|
1658
|
+
...(draft.population.filterMap && { filterMap: draft.population.filterMap }),
|
|
1659
|
+
},
|
|
1660
|
+
},
|
|
1661
|
+
dataSetDefinitions: [
|
|
1662
|
+
{
|
|
1663
|
+
name: draft.dataSetName || 'PATIENT_LIST',
|
|
1664
|
+
type: 'PATIENT_DATA_SET',
|
|
1665
|
+
rowFilter: {
|
|
1666
|
+
type: 'SQL',
|
|
1667
|
+
name: 'Row Filter',
|
|
1668
|
+
config: {
|
|
1669
|
+
sql: finalCohortSql,
|
|
1670
|
+
...(draft.population.filterMap && { filterMap: draft.population.filterMap }),
|
|
1671
|
+
},
|
|
1672
|
+
},
|
|
1673
|
+
columns,
|
|
1674
|
+
},
|
|
1675
|
+
],
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
// Handle sort configuration - convert to legacy format for backend compatibility
|
|
1679
|
+
if (draft.sortConfig && draft.sortConfig.length > 0) {
|
|
1680
|
+
// For now, use the first sort rule as the primary sort
|
|
1681
|
+
// In the future, the backend may support multiple sort rules
|
|
1682
|
+
const primarySort = draft.sortConfig[0];
|
|
1683
|
+
config.orderBy = primarySort.columnId;
|
|
1684
|
+
config.orderDirection = primarySort.direction;
|
|
1685
|
+
|
|
1686
|
+
// Note: Multi-column sorting would require backend changes
|
|
1687
|
+
// For now, additional sort rules are stored but only the first is used
|
|
1688
|
+
if (draft.sortConfig.length > 1) {
|
|
1689
|
+
console.warn('Multiple sort rules defined, but only the first will be used until backend supports multi-column sorting');
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (draft.limit) {
|
|
1694
|
+
config.limit = draft.limit;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return config;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Build a data definition from type and config
|
|
1702
|
+
*/
|
|
1703
|
+
function buildDataDefinition(
|
|
1704
|
+
type: keyof LinelistDataDefinitionMap,
|
|
1705
|
+
config: Record<string, any>
|
|
1706
|
+
): LinelistDataDefinition {
|
|
1707
|
+
switch (type) {
|
|
1708
|
+
case 'IDENTIFIER':
|
|
1709
|
+
return { type: 'IDENTIFIER', config: config as LinelistIdentifierDefinition['config'] };
|
|
1710
|
+
case 'PERSON_NAME':
|
|
1711
|
+
return { type: 'PERSON_NAME', config: config as LinelistPersonNameDefinition['config'] };
|
|
1712
|
+
case 'PERSON_ATTRIBUTE':
|
|
1713
|
+
return { type: 'PERSON_ATTRIBUTE', config: config as LinelistPersonAttributeDefinition['config'] };
|
|
1714
|
+
case 'CALCULATION':
|
|
1715
|
+
return { type: 'CALCULATION', config: config as LinelistCalculationDefinition['config'] };
|
|
1716
|
+
case 'SQL':
|
|
1717
|
+
return { type: 'SQL', config: config as LinelistSqlDefinition['config'] };
|
|
1718
|
+
case 'PERSON_ADDRESS':
|
|
1719
|
+
return { type: 'PERSON_ADDRESS', config: config as LinelistPersonAddressDefinition['config'] };
|
|
1720
|
+
case 'CONVERTER':
|
|
1721
|
+
return { type: 'CONVERTER', config: config as LinelistConverterDefinition['config'] };
|
|
1722
|
+
case 'OBSERVATION':
|
|
1723
|
+
return { type: 'OBSERVATION', config: config as LinelistObservationDefinition['config'] };
|
|
1724
|
+
case 'ENCOUNTER_DIAGNOSIS':
|
|
1725
|
+
return { type: 'ENCOUNTER_DIAGNOSIS', config: config as LinelistEncounterDiagnosisDefinition['config'] };
|
|
1726
|
+
default:
|
|
1727
|
+
// Default to SQL definition for unknown types
|
|
1728
|
+
return { type: 'SQL', config: { sql: config.sql || '' } };
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Validate a linelist draft
|
|
1734
|
+
*/
|
|
1735
|
+
export function validateLinelistDraft(draft: LinelistReportDraft): LinelistValidationErrors {
|
|
1736
|
+
const errors: LinelistValidationErrors = {};
|
|
1737
|
+
|
|
1738
|
+
if (!draft.name?.trim()) {
|
|
1739
|
+
errors.name = 'Name is required';
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Category is recommended but not required for compilation
|
|
1743
|
+
// if (!draft.categoryUuid?.trim()) {
|
|
1744
|
+
// errors.categoryUuid = 'Category is required';
|
|
1745
|
+
// }
|
|
1746
|
+
|
|
1747
|
+
// Validate population definition based on build method
|
|
1748
|
+
const buildMethod = draft.population?.buildMethod || 'SQL_BUILDER';
|
|
1749
|
+
const indicatorRules = draft.population?.indicatorRules || draft.indicatorRules;
|
|
1750
|
+
|
|
1751
|
+
// For INDICATOR_BASED mode, validate indicator rules instead of population sources
|
|
1752
|
+
if (buildMethod === 'INDICATOR_BASED') {
|
|
1753
|
+
if (!indicatorRules || indicatorRules.length === 0) {
|
|
1754
|
+
errors.population = 'At least one indicator rule is required for indicator-based population';
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
// For SQL_BUILDER and VISUAL_FILTER modes, validate population SQL
|
|
1758
|
+
else if (!draft.population.sqlTemplate?.trim()) {
|
|
1759
|
+
errors.population = 'Population SQL is required';
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Validate data sources for columns (v2 - at least one primary data source required)
|
|
1763
|
+
if (!draft.dataSources || draft.dataSources.length === 0) {
|
|
1764
|
+
errors.dataSources = 'At least one column data source is required';
|
|
1765
|
+
} else {
|
|
1766
|
+
const hasPrimary = draft.dataSources.some(ds => ds.role === 'PRIMARY');
|
|
1767
|
+
if (!hasPrimary) {
|
|
1768
|
+
errors.dataSources = 'A primary data source is required';
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
if (!draft.rowGrain?.trim()) {
|
|
1773
|
+
errors.rowGrain = 'Row grain is required';
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Validate population SQL (v2 - in population.sqlTemplate)
|
|
1777
|
+
if (!draft.population.sqlTemplate?.trim()) {
|
|
1778
|
+
errors.population = 'Population SQL is required';
|
|
1779
|
+
} else {
|
|
1780
|
+
// Check that SQL includes the parameters defined in the report
|
|
1781
|
+
// Only validate parameters that are actually configured
|
|
1782
|
+
const requiredParams = draft.parameters
|
|
1783
|
+
.filter(p => p.required)
|
|
1784
|
+
.map(p => p.name);
|
|
1785
|
+
|
|
1786
|
+
const missingParams: string[] = [];
|
|
1787
|
+
for (const param of requiredParams) {
|
|
1788
|
+
const paramPlaceholder = `:${param}`;
|
|
1789
|
+
if (!draft.population.sqlTemplate.includes(paramPlaceholder)) {
|
|
1790
|
+
missingParams.push(param);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
if (missingParams.length > 0) {
|
|
1795
|
+
errors.population = `Population SQL must include these parameters: ${missingParams.map(p => `:${p}`).join(', ')}`;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (draft.columns.length === 0) {
|
|
1800
|
+
errors.columns = 'At least one column is required';
|
|
1801
|
+
} else {
|
|
1802
|
+
// Validate each column
|
|
1803
|
+
const invalidColumns = draft.columns.filter((col) => !col.name?.trim());
|
|
1804
|
+
if (invalidColumns.length > 0) {
|
|
1805
|
+
errors.columns = 'All columns must have a display name';
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Check for unique column IDs
|
|
1809
|
+
const columnIds = draft.columns.map((col) => col.id);
|
|
1810
|
+
const uniqueIds = new Set(columnIds);
|
|
1811
|
+
if (columnIds.length !== uniqueIds.size) {
|
|
1812
|
+
errors.columns = 'Columns must have unique identifiers';
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Check for repeated fields without resolution strategy (v2 - using dataDefinitionConfig.sql)
|
|
1816
|
+
const repeatedColumnsWithoutResolution: string[] = [];
|
|
1817
|
+
draft.columns.forEach((col) => {
|
|
1818
|
+
// Only check SQL columns
|
|
1819
|
+
if (col.dataDefinitionType !== 'SQL') return;
|
|
1820
|
+
|
|
1821
|
+
const sql = col.dataDefinitionConfig.sql || '';
|
|
1822
|
+
|
|
1823
|
+
// Skip empty SQL
|
|
1824
|
+
if (!sql.trim()) return;
|
|
1825
|
+
|
|
1826
|
+
// Check if repeatResolution is configured
|
|
1827
|
+
const hasResolution = col.repeatResolution?.strategy && col.repeatResolution.strategy !== 'NONE';
|
|
1828
|
+
|
|
1829
|
+
// Check if SQL already handles multiple values with LIMIT 1
|
|
1830
|
+
const hasLimitOne = /\bLIMIT\s+1\b/i.test(sql);
|
|
1831
|
+
|
|
1832
|
+
// Check if SQL uses an aggregate function (always returns single value)
|
|
1833
|
+
// Common aggregate functions: COUNT, MAX, MIN, SUM, AVG, GROUP_CONCAT, etc.
|
|
1834
|
+
const hasAggregate = /^(?:SELECT\s+(?:DISTINCT\s+)?(?:COUNT|MAX|MIN|SUM|AVG|GROUP_CONCAT|ARRAY_AGG|STRING_AGG|JSON_AGG)\s*\()/i.test(sql) ||
|
|
1835
|
+
/GROUP\s+BY\s+/i.test(sql);
|
|
1836
|
+
|
|
1837
|
+
// Check if SQL is a scalar subquery (single value return)
|
|
1838
|
+
// Pattern: SELECT single_column FROM ... WHERE patient_id = :patientId AND specific condition LIMIT 1
|
|
1839
|
+
const isScalarSubquery = /\bpatient_id\s*=\s*:patientId\b/i.test(sql) && /\bLIMIT\s+1\b/i.test(sql);
|
|
1840
|
+
|
|
1841
|
+
// A query is potentially repeated (returns multiple rows) if:
|
|
1842
|
+
// 1. It references patient-related tables (encounter, observation, etc.)
|
|
1843
|
+
// 2. It's NOT an aggregate query (COUNT, MAX, etc.)
|
|
1844
|
+
// 3. It doesn't have LIMIT 1
|
|
1845
|
+
// 4. It's not a scalar subquery with proper conditions
|
|
1846
|
+
const referencesPatientData = /appointment|observation|encounter|program_enrollment|visit/i.test(sql);
|
|
1847
|
+
const isPotentiallyRepeated = referencesPatientData && !hasAggregate && !hasLimitOne && !isScalarSubquery && !hasResolution;
|
|
1848
|
+
|
|
1849
|
+
if (isPotentiallyRepeated) {
|
|
1850
|
+
repeatedColumnsWithoutResolution.push(col.name);
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
if (repeatedColumnsWithoutResolution.length > 0) {
|
|
1855
|
+
errors.columns = `Columns that may return multiple values per row: ${repeatedColumnsWithoutResolution.join(', ')}. Please configure repeat resolution for each.`;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// Validate sort column references
|
|
1860
|
+
if (draft.sortConfig && draft.sortConfig.length > 0) {
|
|
1861
|
+
const invalidSorts = draft.sortConfig.filter((sort) => {
|
|
1862
|
+
return !draft.columns.some((col) => col.id === sort.columnId);
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
if (invalidSorts.length > 0) {
|
|
1866
|
+
errors.general = 'Sort rules reference columns that are not in the report';
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Check for basic SQL syntax validation (v2 - using population.sqlTemplate)
|
|
1871
|
+
if (draft.population.sqlTemplate?.trim()) {
|
|
1872
|
+
const upperSql = draft.population.sqlTemplate.toUpperCase().trim();
|
|
1873
|
+
|
|
1874
|
+
// Check for SELECT statement
|
|
1875
|
+
if (!upperSql.includes('SELECT')) {
|
|
1876
|
+
errors.population = 'SQL must include a SELECT statement';
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Check for patient_id column
|
|
1880
|
+
if (!upperSql.includes('PATIENT_ID')) {
|
|
1881
|
+
errors.population = 'SQL must select patient_id or patient_id column';
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// Recommend DISTINCT for patient grain
|
|
1885
|
+
if (draft.rowGrain === 'PATIENT' && !upperSql.includes('DISTINCT')) {
|
|
1886
|
+
// This is a warning, not an error - we could add a warnings object
|
|
1887
|
+
// For now, we'll add it to errors but it won't block save in draft mode
|
|
1888
|
+
// errors.population = 'Consider using SELECT DISTINCT to avoid duplicate patients';
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return errors;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/**
|
|
1896
|
+
* Check if a linelist draft is valid
|
|
1897
|
+
*/
|
|
1898
|
+
export function isLinelistDraftValid(draft: LinelistReportDraft): boolean {
|
|
1899
|
+
const errors = validateLinelistDraft(draft);
|
|
1900
|
+
return Object.keys(errors).length === 0;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Check if a linelist draft is ready to compile
|
|
1905
|
+
* This includes stricter validation than draft validation
|
|
1906
|
+
*
|
|
1907
|
+
* Compiling requires:
|
|
1908
|
+
* - All draft validations pass
|
|
1909
|
+
* - No unresolved repeated fields (will be fully enforced in Task 3)
|
|
1910
|
+
* - SQL definitions would pass server validation (placeholder for server validation)
|
|
1911
|
+
*/
|
|
1912
|
+
export function isLinelistDraftReadyToCompile(draft: LinelistReportDraft): boolean {
|
|
1913
|
+
const errors = validateLinelistDraft(draft);
|
|
1914
|
+
const warnings = generateLinelistWarnings(draft);
|
|
1915
|
+
|
|
1916
|
+
// Basic validation must pass
|
|
1917
|
+
if (Object.keys(errors).length > 0) {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Check for unresolved repeated fields
|
|
1922
|
+
const hasUnresolvedRepeatedFields = draft.columns.some((col) => {
|
|
1923
|
+
if (col.dataDefinitionType !== 'SQL') return false;
|
|
1924
|
+
|
|
1925
|
+
const sql = col.dataDefinitionConfig.sql || '';
|
|
1926
|
+
if (!sql.trim()) return false;
|
|
1927
|
+
|
|
1928
|
+
// Check if SQL uses an aggregate function (always returns single value)
|
|
1929
|
+
const hasAggregate = /^(?:SELECT\s+(?:DISTINCT\s+)?(?:COUNT|MAX|MIN|SUM|AVG|GROUP_CONCAT|ARRAY_AGG|STRING_AGG|JSON_AGG)\s*\()/i.test(sql) ||
|
|
1930
|
+
/GROUP\s+BY\s+/i.test(sql);
|
|
1931
|
+
|
|
1932
|
+
// Check if SQL already has LIMIT 1
|
|
1933
|
+
const hasLimitOne = /\bLIMIT\s+1\b/i.test(sql);
|
|
1934
|
+
|
|
1935
|
+
// Check if a valid resolution strategy is configured
|
|
1936
|
+
const hasValidResolution = col.repeatResolution?.strategy &&
|
|
1937
|
+
col.repeatResolution.strategy !== 'NONE';
|
|
1938
|
+
|
|
1939
|
+
// Only consider unresolved if it references patient-related tables,
|
|
1940
|
+
// doesn't have aggregate, doesn't have LIMIT 1, and no valid resolution
|
|
1941
|
+
const referencesPatientData = /appointment|observation|encounter|program_enrollment|visit/i.test(sql);
|
|
1942
|
+
const isPotentiallyRepeated = referencesPatientData && !hasAggregate && !hasLimitOne;
|
|
1943
|
+
|
|
1944
|
+
return isPotentiallyRepeated && !hasValidResolution;
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
if (hasUnresolvedRepeatedFields) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// Check for critical performance warnings that should block compiling
|
|
1952
|
+
if (warnings.performance?.includes('Many SQL columns') && draft.columns.length > 20) {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* @deprecated Use isLinelistDraftReadyToCompile instead
|
|
1961
|
+
*/
|
|
1962
|
+
export function isLinelistDraftReadyToPublish(draft: LinelistReportDraft): boolean {
|
|
1963
|
+
return isLinelistDraftReadyToCompile(draft);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Create an empty linelist report draft with default values (v2)
|
|
1968
|
+
*/
|
|
1969
|
+
export function createEmptyDraft(): LinelistReportDraft {
|
|
1970
|
+
const now = new Date().toISOString();
|
|
1971
|
+
|
|
1972
|
+
return {
|
|
1973
|
+
version: 3, // Updated to version 3 for population sources support
|
|
1974
|
+
name: '',
|
|
1975
|
+
description: '',
|
|
1976
|
+
code: '',
|
|
1977
|
+
currentPanel: 'basics',
|
|
1978
|
+
categoryUuid: '',
|
|
1979
|
+
rowGrain: 'PATIENT',
|
|
1980
|
+
templateId: '',
|
|
1981
|
+
|
|
1982
|
+
// Population sources (v3 - multiple sources with join types)
|
|
1983
|
+
populationSources: [],
|
|
1984
|
+
|
|
1985
|
+
// Data sources (v2 - array, not single)
|
|
1986
|
+
dataSources: [],
|
|
1987
|
+
|
|
1988
|
+
// Population definition
|
|
1989
|
+
population: {
|
|
1990
|
+
baseDataSourceUuid: '',
|
|
1991
|
+
buildMethod: 'SQL_BUILDER',
|
|
1992
|
+
sqlTemplate: '',
|
|
1993
|
+
parameterReferences: [],
|
|
1994
|
+
visualFilter: {
|
|
1995
|
+
rootGroup: {
|
|
1996
|
+
id: 'root',
|
|
1997
|
+
logicalOperator: 'AND',
|
|
1998
|
+
conditions: [],
|
|
1999
|
+
nestedGroups: [],
|
|
2000
|
+
},
|
|
2001
|
+
useVisualBuilder: false,
|
|
2002
|
+
},
|
|
2003
|
+
buildHistory: [],
|
|
2004
|
+
filterMap: undefined,
|
|
2005
|
+
},
|
|
2006
|
+
|
|
2007
|
+
columns: [],
|
|
2008
|
+
// Default parameters: every linelist report uses a date range
|
|
2009
|
+
parameters: [
|
|
2010
|
+
{
|
|
2011
|
+
id: 'param-startDate',
|
|
2012
|
+
name: 'startDate',
|
|
2013
|
+
label: 'Start Date',
|
|
2014
|
+
type: 'DATE',
|
|
2015
|
+
required: true,
|
|
2016
|
+
defaultValue: '',
|
|
2017
|
+
displayOrder: 0,
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
id: 'param-endDate',
|
|
2021
|
+
name: 'endDate',
|
|
2022
|
+
label: 'End Date',
|
|
2023
|
+
type: 'DATE',
|
|
2024
|
+
required: true,
|
|
2025
|
+
defaultValue: '',
|
|
2026
|
+
displayOrder: 1,
|
|
2027
|
+
},
|
|
2028
|
+
],
|
|
2029
|
+
sortConfig: [],
|
|
2030
|
+
limit: 1000,
|
|
2031
|
+
|
|
2032
|
+
displaySettings: {
|
|
2033
|
+
defaultPageSize: 25,
|
|
2034
|
+
freezeFirstColumn: true,
|
|
2035
|
+
freezeHeader: true,
|
|
2036
|
+
dateDisplayFormat: 'dd MMM yyyy',
|
|
2037
|
+
nullDisplayValue: '—',
|
|
2038
|
+
maxInteractiveRows: 500,
|
|
2039
|
+
maxExportRows: 100000,
|
|
2040
|
+
allowedExports: ['CSV', 'XLSX', 'PDF'],
|
|
2041
|
+
includeParametersInExportHeader: true,
|
|
2042
|
+
includeGeneratedTimestamp: true,
|
|
2043
|
+
},
|
|
2044
|
+
|
|
2045
|
+
validation: {
|
|
2046
|
+
errors: {},
|
|
2047
|
+
warnings: {},
|
|
2048
|
+
lastValidated: now,
|
|
2049
|
+
},
|
|
2050
|
+
|
|
2051
|
+
metadata: {
|
|
2052
|
+
createdAt: now,
|
|
2053
|
+
lastModified: now,
|
|
2054
|
+
buildMethod: 'NEW',
|
|
2055
|
+
version: 2,
|
|
2056
|
+
status: 'DRAFT',
|
|
2057
|
+
},
|
|
2058
|
+
};
|
|
2059
|
+
}
|