@openmrs/esm-report-builder 0.1.1-pre.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +5 -0
- package/.eslintrc.js +19 -0
- package/.prettierrc +8 -0
- package/.tx/config +12 -0
- package/LICENSE +401 -0
- package/README.md +336 -0
- package/__mocks__/react-i18next.js +12 -0
- package/dist/162.js +1 -0
- package/dist/162.js.map +1 -0
- package/dist/184.js +2 -0
- package/dist/184.js.LICENSE.txt +14 -0
- package/dist/184.js.map +1 -0
- package/dist/242.js +2 -0
- package/dist/242.js.LICENSE.txt +21 -0
- package/dist/242.js.map +1 -0
- package/dist/253.js +1 -0
- package/dist/253.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/311.js +1 -0
- package/dist/311.js.map +1 -0
- package/dist/469.js +2 -0
- package/dist/469.js.LICENSE.txt +45 -0
- package/dist/469.js.map +1 -0
- package/dist/478.js +2 -0
- package/dist/478.js.LICENSE.txt +9 -0
- package/dist/478.js.map +1 -0
- package/dist/539.js +1 -0
- package/dist/539.js.map +1 -0
- package/dist/540.js +2 -0
- package/dist/540.js.LICENSE.txt +9 -0
- package/dist/540.js.map +1 -0
- package/dist/669.js +1 -0
- package/dist/669.js.map +1 -0
- package/dist/710.js +1 -0
- package/dist/710.js.map +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/838.js +2 -0
- package/dist/838.js.LICENSE.txt +9 -0
- package/dist/838.js.map +1 -0
- package/dist/917.js +1 -0
- package/dist/917.js.map +1 -0
- package/dist/961.js +2 -0
- package/dist/961.js.LICENSE.txt +19 -0
- package/dist/961.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-report-builder.js +1 -0
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +530 -0
- package/dist/openmrs-esm-report-builder.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/core/env.ts +5 -0
- package/e2e/core/global-setup.ts +34 -0
- package/e2e/core/test.ts +21 -0
- package/e2e/pages/app-shell.page.ts +34 -0
- package/e2e/pages/etl-sources.page.ts +35 -0
- package/e2e/pages/report-sections.page.ts +20 -0
- package/e2e/pages/section-preview.modal.ts +29 -0
- package/e2e/specs/etl-sources.spec.ts +51 -0
- package/e2e/specs/navigation.spec.ts +13 -0
- package/e2e/specs/section-preview-validation.spec.ts +38 -0
- package/e2e/specs/section-preview.spec.ts +94 -0
- package/e2e/support/github/Dockerfile +22 -0
- package/e2e/support/github/Dockerfile.dockerignore +13 -0
- package/e2e/support/github/docker-compose.yml +31 -0
- package/e2e/support/github/run-e2e-docker-env.sh +131 -0
- package/e2e/support/github/spa-assemble-config.json +9 -0
- package/e2e/tsconfig.json +11 -0
- package/example.env +5 -0
- package/jest.config.js +28 -0
- package/linelist_report_builder_design_spec.md +816 -0
- package/package.json +111 -0
- package/playwright.config.ts +28 -0
- package/routes.json +22 -0
- package/src/__debug__/all-designs.test.tsx +30 -0
- package/src/admin-card-link.extension.module.scss +55 -0
- package/src/admin-card-link.extension.tsx +24 -0
- package/src/components/admin/admin-page.component.tsx +75 -0
- package/src/components/admin/age-categories-page.component.tsx +109 -0
- package/src/components/admin/age-groups-page.component.tsx +146 -0
- package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
- package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
- package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
- package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
- package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
- package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
- package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
- package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
- package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
- package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
- package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
- package/src/components/admin/etl-sources-page.component.tsx +355 -0
- package/src/components/admin/etl-tasks-page.component.scss +370 -0
- package/src/components/admin/etl-tasks-page.component.tsx +768 -0
- package/src/components/admin/report-categories-page.component.tsx +267 -0
- package/src/components/admin/report-library-page.component.tsx +551 -0
- package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
- package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
- package/src/components/ai-support/ai.service.ts +21 -0
- package/src/components/ai-support/ai.types.ts +12 -0
- package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
- package/src/components/app-shell/report-builder-shell.scss +18 -0
- package/src/components/dashboards/dashboard-config.schema.ts +97 -0
- package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
- package/src/components/dashboards/dashboards-admin.scss +62 -0
- package/src/components/dashboards/dashboards-page.component.tsx +153 -0
- package/src/components/dashboards/dashboards-table.component.tsx +130 -0
- package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
- package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
- package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
- package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
- package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
- package/src/components/data-themes/data-theme-form.component.tsx +59 -0
- package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
- package/src/components/data-themes/data-themes-page.component.tsx +139 -0
- package/src/components/data-themes/data-themes-table.component.tsx +98 -0
- package/src/components/data-themes/icon-registry.ts +35 -0
- package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
- package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
- package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
- package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
- package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
- package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
- package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
- package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
- package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
- package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
- package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
- package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
- package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
- package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
- package/src/components/data-visualizer/components/header/header.scss +71 -0
- package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
- package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
- package/src/components/data-visualizer/components/panel/panel.scss +25 -0
- package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
- package/src/components/data-visualizer/constants.ts +445 -0
- package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
- package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
- package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
- package/src/components/data-visualizer/data-visualizer.scss +449 -0
- package/src/components/data-visualizer/declarations.d.ts +131 -0
- package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
- package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
- package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
- package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
- package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
- package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
- package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
- package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
- package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
- package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
- package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
- package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
- package/src/components/data-visualizer/types.ts +206 -0
- package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
- package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
- package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
- package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
- package/src/components/etl-monitor/builder/index.ts +5 -0
- package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
- package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
- package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
- package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
- package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
- package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
- package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
- package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
- package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
- package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
- package/src/components/etl-monitor/builder/steps/index.ts +5 -0
- package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
- package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
- package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
- package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
- package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
- package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
- package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
- package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
- package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
- package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
- package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
- package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
- package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
- package/src/components/etl-monitor/renderers/components/index.ts +6 -0
- package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
- package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
- package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
- package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
- package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
- package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
- package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
- package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
- package/src/components/etl-monitor/renderers/index.ts +11 -0
- package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
- package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
- package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
- package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
- package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
- package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
- package/src/components/import-export/artifact-groups.ts +153 -0
- package/src/components/import-export/export-report-modal.component.tsx +265 -0
- package/src/components/import-export/import-export-page.component.tsx +610 -0
- package/src/components/import-export/import-export.styles.scss +825 -0
- package/src/components/import-export/import-package-modal.component.tsx +401 -0
- package/src/components/import-export/index.ts +3 -0
- package/src/components/indicators/README.md +24 -0
- package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
- package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
- package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
- package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
- package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
- package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
- package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
- package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
- package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
- package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
- package/src/components/indicators/custom-condition-input.component.tsx +310 -0
- package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
- package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
- package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
- package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
- package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
- package/src/components/indicators/indicator-search-select.component.tsx +297 -0
- package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
- package/src/components/indicators/indicators-page.component.tsx +673 -0
- package/src/components/indicators/indicators-page.scss +121 -0
- package/src/components/indicators/indicators-table.component.tsx +188 -0
- package/src/components/indicators/preview-table.component.tsx +31 -0
- package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
- package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
- package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
- package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
- package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
- package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
- package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
- package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
- package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
- package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
- package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
- package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
- package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
- package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
- package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
- package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
- package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
- package/src/components/indicators/sql-preview.component.tsx +19 -0
- package/src/components/indicators/tag-input.component.tsx +177 -0
- package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
- package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
- package/src/components/indicators/types/composite-indicator.types.ts +27 -0
- package/src/components/indicators/types/condition-ui.types.ts +11 -0
- package/src/components/indicators/types/custom-indicator.types.ts +161 -0
- package/src/components/indicators/types/data-theme-config.types.ts +88 -0
- package/src/components/indicators/types/indicator-types.ts +57 -0
- package/src/components/indicators/types/sql-builders.ts +43 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
- package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
- package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
- package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
- package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
- package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
- package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
- package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
- package/src/components/indicators/utils/population-sql.compiler.test.ts +999 -0
- package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
- package/src/components/landing/landing-action-item.component.tsx +50 -0
- package/src/components/landing/landing-action-list.component.tsx +12 -0
- package/src/components/landing/landing-activity-list.component.tsx +47 -0
- package/src/components/landing/landing-footer-grid.component.tsx +45 -0
- package/src/components/landing/landing-hero.component.tsx +18 -0
- package/src/components/landing/landing-section-card.component.tsx +33 -0
- package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
- package/src/components/landing/report-builder-landing-page.scss +131 -0
- package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
- package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
- package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
- package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
- package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
- package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
- package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
- package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
- package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
- package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
- package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
- package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
- package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
- package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
- package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
- package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
- package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
- package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
- package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
- package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
- package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
- package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
- package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
- package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
- package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
- package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
- package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
- package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
- package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
- package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
- package/src/components/linelist/design/panels/column-category-selector.scss +329 -0
- package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
- package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
- package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
- package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
- package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
- package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
- package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
- package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
- package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
- package/src/components/linelist/design/panels/json-preview.scss +85 -0
- package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
- package/src/components/linelist/design/panels/query-config-panel.scss +342 -0
- package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
- package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
- package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
- package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
- package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
- package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
- package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
- package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
- package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
- package/src/components/linelist/linelist-dashboard.page.scss +240 -0
- package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
- package/src/components/linelist/linelist-editor.page.scss +28 -0
- package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
- package/src/components/linelist/linelist-run-report.page.scss +136 -0
- package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
- package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
- package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
- package/src/components/report/dashboard/reports-table.component.tsx +186 -0
- package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
- package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
- package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
- package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
- package/src/components/report/definition/report-definition.types.ts +66 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
- package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
- package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
- package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
- package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
- package/src/components/report/design/panels/template-structure-panel.scss +81 -0
- package/src/components/report/design/report-design-editor.component.tsx +1170 -0
- package/src/components/report/design/report-design.types.ts +48 -0
- package/src/components/report/design/report-design.utils.ts +86 -0
- package/src/components/report/panels/report-detail.component.tsx +71 -0
- package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
- package/src/components/report/report-dashboard.page.component.tsx +554 -0
- package/src/components/report/report-editor.page.component.tsx +590 -0
- package/src/components/report/report.routes.tsx +32 -0
- package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
- package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
- package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
- package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
- package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
- package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
- package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
- package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
- package/src/components/report-sections/report-sections-page.component.tsx +325 -0
- package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
- package/src/components/report-sections/section-types.ts +55 -0
- package/src/components/report-sections/section-utils.ts +50 -0
- package/src/components/report-sections/sections-page.scss +79 -0
- package/src/components/run-reports/run-reports-page.component.tsx +8 -0
- package/src/components/security/access-denied-page.component.tsx +69 -0
- package/src/components/security/access-denied-page.scss +39 -0
- package/src/components/security/privilege-route.component.test.tsx +79 -0
- package/src/components/security/privilege-route.component.tsx +39 -0
- package/src/components/shared/compile-setup-modal.component.tsx +182 -0
- package/src/components/shared/concept-selector.component.tsx +245 -0
- package/src/components/shared/concept-selector.scss +121 -0
- package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
- package/src/components/shared/header/header.component.tsx +85 -0
- package/src/components/shared/header/header.scss +150 -0
- package/src/components/shared/header/illustration.component.tsx +34 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
- package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
- package/src/config-schema.ts +43 -0
- package/src/constants/privileges.test.ts +53 -0
- package/src/constants/privileges.ts +118 -0
- package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
- package/src/features/dashboards/DashboardsPage.tsx +164 -0
- package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
- package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
- package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
- package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
- package/src/features/dashboards/components/DashboardSection.tsx +46 -0
- package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
- package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
- package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
- package/src/features/dashboards/dashboards.scss +196 -0
- package/src/features/dashboards/hooks/useDashboard.ts +61 -0
- package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
- package/src/features/dashboards/hooks/useDashboards.ts +56 -0
- package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
- package/src/features/dashboards/index.ts +11 -0
- package/src/features/dashboards/utils/dashboard-config.util.test.ts +255 -0
- package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
- package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
- package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
- package/src/features/report-visualizer/components/Export/index.ts +2 -0
- package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
- package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
- package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
- package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
- package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
- package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
- package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
- package/src/features/report-visualizer/components/TableView/index.ts +2 -0
- package/src/features/report-visualizer/hooks/index.ts +4 -0
- package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
- package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
- package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
- package/src/features/report-visualizer/hooks/useReports.ts +135 -0
- package/src/features/report-visualizer/index.ts +6 -0
- package/src/features/report-visualizer/report-visualizer.scss +593 -0
- package/src/features/report-visualizer/types/index.ts +1 -0
- package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
- package/src/features/report-visualizer/utils/status-colors.ts +117 -0
- package/src/globals.d.ts +14 -0
- package/src/hooks/address-template.ts +86 -0
- package/src/hooks/concept.ts +112 -0
- package/src/hooks/indicator/index.ts +1 -0
- package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
- package/src/hooks/openmrs-reference-data.ts +432 -0
- package/src/hooks/theme/index.ts +4 -0
- package/src/hooks/theme/useDataTheme.ts +61 -0
- package/src/hooks/theme/useDataThemes.ts +28 -0
- package/src/hooks/theme/useMambaTableMeta.ts +34 -0
- package/src/hooks/theme/useMambaTables.ts +95 -0
- package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
- package/src/hooks/use-report-builder-privileges.ts +58 -0
- package/src/index.ts +29 -0
- package/src/resources/address-template/address-template.api.ts +180 -0
- package/src/resources/agegroup/agegroups.api.ts +58 -0
- package/src/resources/concept/concept.api.ts +116 -0
- package/src/resources/concepts/concept-mapper.ts +21 -0
- package/src/resources/concepts/concept-types.ts +31 -0
- package/src/resources/concepts/concepts.resource.ts +44 -0
- package/src/resources/concepts/useConceptSearch.ts +41 -0
- package/src/resources/dashboard/dashboard.api.ts +107 -0
- package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
- package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
- package/src/resources/etl-source/etl-source.api.ts +65 -0
- package/src/resources/etl-task/etl-task.api.ts +96 -0
- package/src/resources/etl-task/etl-task.types.ts +31 -0
- package/src/resources/indicator/indicators.api.ts +125 -0
- package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
- package/src/resources/linelist/linelist-reports.api.ts +616 -0
- package/src/resources/location/location.api.ts +135 -0
- package/src/resources/openmrs-api.ts +40 -0
- package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
- package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
- package/src/resources/preview/sql-preview.api.ts +54 -0
- package/src/resources/report/reports.api.ts +140 -0
- package/src/resources/report-category/report-category.api.ts +41 -0
- package/src/resources/report-import-export/import-export-api.ts +164 -0
- package/src/resources/report-library/report-library.api.ts +63 -0
- package/src/resources/report-section/report-sections.api.ts +73 -0
- package/src/resources/report-section/section-preview.api.ts +25 -0
- package/src/resources/theme/data-theme.api.ts +88 -0
- package/src/resources/theme/etl-schema.api.ts +101 -0
- package/src/resources/theme/etl-table-meta.api.ts +45 -0
- package/src/root.component.tsx +20 -0
- package/src/routes/report-builder.routes.tsx +160 -0
- package/src/routes/report-builder.scss +56 -0
- package/src/routes.json +28 -0
- package/src/sample-template.ts +121 -0
- package/src/template-utils.ts +110 -0
- package/src/types/carbon-react-inert.d.ts +13 -0
- package/src/types/condition-operators.ts +63 -0
- package/src/types/dashboard/dashboard.types.ts +137 -0
- package/src/types/dashboard/index.ts +4 -0
- package/src/types/etl/etl-types.ts +238 -0
- package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
- package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
- package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
- package/src/types/etl-monitor/index.ts +6 -0
- package/src/types/linelist/compile-config.ts +455 -0
- package/src/types/linelist-types.ts +2059 -0
- package/src/types/theme/data-theme.types.ts +88 -0
- package/src/utils/api-error.utils.test.ts +151 -0
- package/src/utils/api-error.utils.ts +131 -0
- package/src/utils/config-enhancer.ts +62 -0
- package/src/utils/etl-browser/etl-browser.util.test.ts +145 -0
- package/src/utils/etl-browser/export-csv.util.ts +43 -0
- package/src/utils/etl-browser/group-tables.util.ts +84 -0
- package/src/utils/etl-browser/index.ts +4 -0
- package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
- package/src/utils/etl-browser/table-sql.util.ts +20 -0
- package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
- package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
- package/src/utils/etl-monitor/index.ts +10 -0
- package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
- package/src/utils/etl-monitor/normalization-examples.ts +0 -0
- package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
- package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
- package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
- package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
- package/src/utils/etl-task/etl-task-card.util.test.ts +239 -0
- package/src/utils/etl-task/etl-task-card.util.ts +276 -0
- package/src/utils/etl-task/index.ts +1 -0
- package/src/utils/html-entities.utils.ts +34 -0
- package/src/utils/parameter-resolution.ts +218 -0
- package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
- package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
- package/tools/scss-stub.js +12 -0
- package/tools/setup-tests.ts +1 -0
- package/translations/en.json +65 -0
- package/tsconfig.json +43 -0
- package/webpack.config.js +30 -0
|
@@ -0,0 +1,1676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linelist Builder Workspace - Three-Panel Layout
|
|
3
|
+
*
|
|
4
|
+
* Full-width, three-panel workspace for creating and editing linelist reports.
|
|
5
|
+
* Follows design spec section 6: Screen 3 — Builder workspace.
|
|
6
|
+
*
|
|
7
|
+
* Layout (Desktop 1440px+):
|
|
8
|
+
* - Left: Data Catalogue (280px, resizable)
|
|
9
|
+
* - Middle: Query Configuration (400px, resizable)
|
|
10
|
+
* - Right: Preview (remaining width, minimum 560px)
|
|
11
|
+
*
|
|
12
|
+
* Panels can collapse at smaller desktop widths.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
Button,
|
|
18
|
+
ButtonSet,
|
|
19
|
+
InlineNotification,
|
|
20
|
+
Tag,
|
|
21
|
+
TextInput,
|
|
22
|
+
OverflowMenu,
|
|
23
|
+
OverflowMenuItem,
|
|
24
|
+
Table,
|
|
25
|
+
TableBody,
|
|
26
|
+
TableCell,
|
|
27
|
+
TableContainer,
|
|
28
|
+
TableHead,
|
|
29
|
+
TableHeader,
|
|
30
|
+
TableRow,
|
|
31
|
+
} from '@carbon/react';
|
|
32
|
+
import {
|
|
33
|
+
Save,
|
|
34
|
+
Send,
|
|
35
|
+
Document,
|
|
36
|
+
OverflowMenuHorizontal,
|
|
37
|
+
ChevronLeft,
|
|
38
|
+
ChevronRight,
|
|
39
|
+
} from '@carbon/react/icons';
|
|
40
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
41
|
+
|
|
42
|
+
import type {
|
|
43
|
+
LinelistReportDraft,
|
|
44
|
+
LinelistColumnDraft,
|
|
45
|
+
FilterFieldType,
|
|
46
|
+
FilterOperator,
|
|
47
|
+
DataSourceInfo,
|
|
48
|
+
ColumnSource,
|
|
49
|
+
LinelistSortConfig,
|
|
50
|
+
LinelistDataDefinitionMap,
|
|
51
|
+
PopulationDefinition,
|
|
52
|
+
PopulationSource,
|
|
53
|
+
LinelistParameterConfig,
|
|
54
|
+
} from '../../../types/linelist-types';
|
|
55
|
+
import type { EtlStructure } from '../../../types/etl/etl-types';
|
|
56
|
+
import {
|
|
57
|
+
draftToConfig,
|
|
58
|
+
// isLinelistDraftReadyToCompile,
|
|
59
|
+
// isLinelistDraftReadyToPublish,
|
|
60
|
+
createEmptyDraft,
|
|
61
|
+
// isLinelistDraftValid,
|
|
62
|
+
validateLinelistDraft,
|
|
63
|
+
} from '../../../types/linelist-types';
|
|
64
|
+
|
|
65
|
+
import {
|
|
66
|
+
createLinelistReport,
|
|
67
|
+
updateLinelistReport,
|
|
68
|
+
configToSavePayload,
|
|
69
|
+
compileLinelistReport,
|
|
70
|
+
previewLinelistReport,
|
|
71
|
+
} from '../../../resources/linelist/linelist-reports.api';
|
|
72
|
+
import type { LinelistBuilderMeta } from '../../../resources/linelist/linelist-reports.api';
|
|
73
|
+
import type { LinelistReportDto } from '../../../types/linelist-types';
|
|
74
|
+
import { listReportCategories, type ReportCategoryDto } from '../../../resources/report-category/report-category.api';
|
|
75
|
+
import { useIndicatorsByTheme } from '../../../hooks/indicator';
|
|
76
|
+
|
|
77
|
+
import DataCatalogue from '../design/panels/data-catalogue.component';
|
|
78
|
+
import QueryConfigPanel from '../design/panels/query-config-panel.component';
|
|
79
|
+
import MultiDatasourceSelector from './panels/multi-datasource-selector.component';
|
|
80
|
+
import PopulationSourceSelector from './panels/population-source-selector.component';
|
|
81
|
+
import type { CatalogueField } from '../design/panels/data-catalogue.component';
|
|
82
|
+
import type { CustomSqlColumnConfig } from './panels/custom-sql-column-modal.component';
|
|
83
|
+
import PreviewParameterModal from '../modals/preview-parameter-modal.component';
|
|
84
|
+
import styles from './linelist-definition-editor.scss';
|
|
85
|
+
import type { LinelistReportDefinitionConfig } from '../../../types/linelist-types';
|
|
86
|
+
import CompileSetupModal, { type CompileSetupResult } from '../../shared/compile-setup-modal.component';
|
|
87
|
+
import { RB } from '../../../constants/privileges';
|
|
88
|
+
import { useReportBuilderPrivileges } from '../../../hooks/use-report-builder-privileges';
|
|
89
|
+
|
|
90
|
+
type Props = {};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert LinelistReportDto to LinelistReportDraft for editing (v2)
|
|
94
|
+
* This function populates the draft with all builder details from the saved report
|
|
95
|
+
*/
|
|
96
|
+
function reportToDraft(report: LinelistReportDto): LinelistReportDraft {
|
|
97
|
+
let config: LinelistReportDefinitionConfig | null = null;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
config = report.configJson ? JSON.parse(report.configJson) : null;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error('Failed to parse report config:', e);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for valid LINE_LIST config (handle both type and reportType formats)
|
|
106
|
+
const isValidLinelist = config && (
|
|
107
|
+
config.type === 'LINE_LIST' ||
|
|
108
|
+
(config as any).reportType === 'LINE_LIST' ||
|
|
109
|
+
(config as any).reportType === 'LINELIST'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (!config || !isValidLinelist) {
|
|
113
|
+
console.warn('Invalid config or type, returning empty draft');
|
|
114
|
+
return createEmptyDraft();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const dataSet = config.dataSetDefinitions?.[0];
|
|
119
|
+
|
|
120
|
+
// The compiled backend format stores builder state under a `_builder` key.
|
|
121
|
+
// Fall back to top-level config fields for older/intermediate formats.
|
|
122
|
+
const builderState = (config as any)._builder;
|
|
123
|
+
|
|
124
|
+
// === BUILD DATA SOURCES ARRAY ===
|
|
125
|
+
const dataSources: DataSourceInfo[] = [];
|
|
126
|
+
const configDataSources = builderState?.dataSources || config.dataSources;
|
|
127
|
+
|
|
128
|
+
if (configDataSources && configDataSources.length > 0) {
|
|
129
|
+
// V2 format (or compiled _builder) - use dataSources array directly
|
|
130
|
+
configDataSources.forEach((ds: any) => {
|
|
131
|
+
dataSources.push({
|
|
132
|
+
uuid: ds.uuid,
|
|
133
|
+
name: ds.name,
|
|
134
|
+
type: ds.type as any,
|
|
135
|
+
role: ds.role as any,
|
|
136
|
+
tables: ds.columns ? Object.keys(ds.columns) : [],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
} else if (config.version === 1 || !config.dataSources) {
|
|
140
|
+
// V1 format - migrate from dataSourceUuid
|
|
141
|
+
const primaryDataSourceUuid = config.dataSourceUuid || '';
|
|
142
|
+
if (primaryDataSourceUuid) {
|
|
143
|
+
dataSources.push({
|
|
144
|
+
uuid: primaryDataSourceUuid,
|
|
145
|
+
name: primaryDataSourceUuid, // Will be resolved by data catalogue
|
|
146
|
+
type: 'ETL',
|
|
147
|
+
role: 'PRIMARY',
|
|
148
|
+
tables: [],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add CORE datasources if columns use them
|
|
153
|
+
if (dataSet?.columns) {
|
|
154
|
+
const hasPersonAttributes = dataSet.columns.some(col =>
|
|
155
|
+
(col.dataDefinition as any)?.type === 'PERSON_ATTRIBUTE'
|
|
156
|
+
);
|
|
157
|
+
const hasIdentifiers = dataSet.columns.some(col =>
|
|
158
|
+
(col.dataDefinition as any)?.type === 'IDENTIFIER'
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (hasPersonAttributes) {
|
|
162
|
+
dataSources.push({
|
|
163
|
+
uuid: 'person_attributes',
|
|
164
|
+
name: 'Person Attributes',
|
|
165
|
+
type: 'CORE',
|
|
166
|
+
role: 'REFERENCE',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (hasIdentifiers) {
|
|
171
|
+
dataSources.push({
|
|
172
|
+
uuid: 'patient_identifiers',
|
|
173
|
+
name: 'Patient Identifiers',
|
|
174
|
+
type: 'CORE',
|
|
175
|
+
role: 'REFERENCE',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// V2 format - use dataSources array directly
|
|
181
|
+
if (config.dataSources && config.dataSources.length > 0) {
|
|
182
|
+
config.dataSources.forEach(ds => {
|
|
183
|
+
dataSources.push({
|
|
184
|
+
uuid: ds.uuid,
|
|
185
|
+
name: ds.name,
|
|
186
|
+
type: ds.type as any,
|
|
187
|
+
role: ds.role as any,
|
|
188
|
+
tables: ds.columns ? Object.keys(ds.columns) : [],
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Get primary datasource UUID from the migrated dataSources
|
|
195
|
+
const primaryDataSourceUuid = dataSources.find(ds => ds.role === 'PRIMARY')?.uuid || '';
|
|
196
|
+
|
|
197
|
+
// === BUILD COLUMN → SOURCE LOOKUP FROM dataSources METADATA ===
|
|
198
|
+
// The V2 contract stores per-column source info inside dataSources[].columns.
|
|
199
|
+
// Build a map: columnName (lowercase) → { dataSource, table, fieldType }
|
|
200
|
+
type SourceLookup = {
|
|
201
|
+
dataSourceUuid: string;
|
|
202
|
+
dataSourceName: string;
|
|
203
|
+
table: string;
|
|
204
|
+
fieldType: string;
|
|
205
|
+
};
|
|
206
|
+
const columnSourceLookup = new Map<string, SourceLookup>();
|
|
207
|
+
(configDataSources as any[]).forEach((ds) => {
|
|
208
|
+
const dsColumns = ds.columns || {};
|
|
209
|
+
Object.values(dsColumns).forEach((colInfo: any) => {
|
|
210
|
+
const lookupKey = (colInfo?.name || '').toLowerCase();
|
|
211
|
+
if (lookupKey) {
|
|
212
|
+
columnSourceLookup.set(lookupKey, {
|
|
213
|
+
dataSourceUuid: ds.uuid,
|
|
214
|
+
dataSourceName: ds.name,
|
|
215
|
+
table: colInfo?.sourceTable || ds.uuid,
|
|
216
|
+
fieldType: colInfo?.type || 'UNKNOWN',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Track unique tables used
|
|
223
|
+
const tablesUsed = new Set<string>();
|
|
224
|
+
|
|
225
|
+
// === EXTRACT COLUMNS WITH SOURCE INFO ===
|
|
226
|
+
const columns: LinelistColumnDraft[] = [];
|
|
227
|
+
|
|
228
|
+
if (dataSet?.columns) {
|
|
229
|
+
dataSet.columns.forEach((col, idx) => {
|
|
230
|
+
const defType = (col.dataDefinition as any)?.type;
|
|
231
|
+
const defConfig = (col.dataDefinition as any)?.config;
|
|
232
|
+
const colNameLower = col.name.toLowerCase();
|
|
233
|
+
const sqlStr = defConfig?.sql || '';
|
|
234
|
+
// Detect custom SQL (a full query, not a simple table.column reference)
|
|
235
|
+
const isCustomSql = /^\s*SELECT\s/i.test(sqlStr) || /:(patientId|client_id)\b/i.test(sqlStr);
|
|
236
|
+
|
|
237
|
+
// Determine source based on data definition type
|
|
238
|
+
let source: ColumnSource;
|
|
239
|
+
|
|
240
|
+
if (defType === 'PERSON_ATTRIBUTE') {
|
|
241
|
+
source = {
|
|
242
|
+
dataSourceUuid: 'person_attributes',
|
|
243
|
+
dataSourceName: 'Person Attributes',
|
|
244
|
+
table: 'person_attribute',
|
|
245
|
+
field: col.name,
|
|
246
|
+
fieldType: 'PERSON_ATTRIBUTE',
|
|
247
|
+
attributeTypeUuid: defConfig?.attributeTypeUuid,
|
|
248
|
+
};
|
|
249
|
+
} else if (defType === 'IDENTIFIER') {
|
|
250
|
+
source = {
|
|
251
|
+
dataSourceUuid: 'patient_identifiers',
|
|
252
|
+
dataSourceName: 'Patient Identifiers',
|
|
253
|
+
table: 'patient_identifier',
|
|
254
|
+
field: col.name,
|
|
255
|
+
fieldType: 'IDENTIFIER',
|
|
256
|
+
identifierTypeUuid: defConfig?.identifierTypeUuid,
|
|
257
|
+
};
|
|
258
|
+
} else if (defType === 'CALCULATION') {
|
|
259
|
+
// Calculated field (e.g., Age)
|
|
260
|
+
source = {
|
|
261
|
+
dataSourceUuid: primaryDataSourceUuid,
|
|
262
|
+
dataSourceName: primaryDataSourceUuid,
|
|
263
|
+
table: 'calculated',
|
|
264
|
+
field: col.name,
|
|
265
|
+
fieldType: 'CALCULATED',
|
|
266
|
+
};
|
|
267
|
+
} else {
|
|
268
|
+
// SQL column - use the dataSources metadata lookup first, then fall back
|
|
269
|
+
const lookup = columnSourceLookup.get(colNameLower);
|
|
270
|
+
|
|
271
|
+
if (lookup && !isCustomSql) {
|
|
272
|
+
// Found in the dataSources metadata
|
|
273
|
+
source = {
|
|
274
|
+
dataSourceUuid: lookup.dataSourceUuid,
|
|
275
|
+
dataSourceName: lookup.dataSourceName,
|
|
276
|
+
table: lookup.table,
|
|
277
|
+
field: col.name,
|
|
278
|
+
fieldType: lookup.fieldType,
|
|
279
|
+
};
|
|
280
|
+
tablesUsed.add(lookup.table);
|
|
281
|
+
} else if (isCustomSql) {
|
|
282
|
+
// Custom SQL column - per-row query with :patientId / :client_id
|
|
283
|
+
source = {
|
|
284
|
+
dataSourceUuid: lookup?.dataSourceUuid || primaryDataSourceUuid || 'custom_sql',
|
|
285
|
+
dataSourceName: lookup?.dataSourceName || 'Custom SQL',
|
|
286
|
+
table: 'custom_sql',
|
|
287
|
+
field: col.name,
|
|
288
|
+
fieldType: 'SQL',
|
|
289
|
+
};
|
|
290
|
+
} else {
|
|
291
|
+
// Simple SQL reference like `table`.`column` - parse it
|
|
292
|
+
const sqlMatch = sqlStr.match(/`?(\w+)`?\.\s*`?(\w+)`?/);
|
|
293
|
+
if (sqlMatch) {
|
|
294
|
+
source = {
|
|
295
|
+
dataSourceUuid: primaryDataSourceUuid,
|
|
296
|
+
dataSourceName: primaryDataSourceUuid,
|
|
297
|
+
table: sqlMatch[1],
|
|
298
|
+
field: sqlMatch[2],
|
|
299
|
+
fieldType: 'DERIVED',
|
|
300
|
+
};
|
|
301
|
+
tablesUsed.add(sqlMatch[1]);
|
|
302
|
+
} else {
|
|
303
|
+
// Fallback
|
|
304
|
+
source = {
|
|
305
|
+
dataSourceUuid: primaryDataSourceUuid,
|
|
306
|
+
dataSourceName: primaryDataSourceUuid,
|
|
307
|
+
table: 'unknown',
|
|
308
|
+
field: col.name,
|
|
309
|
+
fieldType: 'UNKNOWN',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const columnDraft: LinelistColumnDraft = {
|
|
316
|
+
id: `col-${idx}`,
|
|
317
|
+
name: col.name,
|
|
318
|
+
description: '',
|
|
319
|
+
source,
|
|
320
|
+
dataDefinitionType: (defType || 'SQL') as keyof LinelistDataDefinitionMap,
|
|
321
|
+
dataDefinitionConfig: defConfig || {},
|
|
322
|
+
additionInfo: {
|
|
323
|
+
addedVia: isCustomSql ? 'SQL_BUILDER' : 'IMPORT',
|
|
324
|
+
addedAt: now,
|
|
325
|
+
orderAdded: idx,
|
|
326
|
+
},
|
|
327
|
+
display: {
|
|
328
|
+
width: 150,
|
|
329
|
+
align: defType === 'CALCULATION' ? 'center' : 'left',
|
|
330
|
+
sortable: true,
|
|
331
|
+
filterable: defType !== 'CALCULATION',
|
|
332
|
+
format: defType === 'CALCULATION' ? 'number' : 'text',
|
|
333
|
+
},
|
|
334
|
+
sortOrder: idx,
|
|
335
|
+
repeatResolution: (col as any).repeatResolution,
|
|
336
|
+
transformations: (col as any).transformations || [],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
columns.push(columnDraft);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Update data sources with tables used
|
|
344
|
+
dataSources.forEach(ds => {
|
|
345
|
+
if (ds.uuid === primaryDataSourceUuid) {
|
|
346
|
+
ds.tables = Array.from(tablesUsed);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// === EXTRACT PARAMETERS ===
|
|
351
|
+
let parameters = config.parameters?.map((param, idx) => ({
|
|
352
|
+
id: `param-${idx}`,
|
|
353
|
+
name: param.name,
|
|
354
|
+
label: param.label,
|
|
355
|
+
type: param.type,
|
|
356
|
+
required: param.required || false,
|
|
357
|
+
defaultValue: param.defaultValue || '',
|
|
358
|
+
displayOrder: param.displayOrder || idx,
|
|
359
|
+
config: (param.config || { type: param.type }) as LinelistParameterConfig,
|
|
360
|
+
})) || [];
|
|
361
|
+
|
|
362
|
+
// === EXTRACT BUILDER METADATA (build method, indicators, visualFilter, filterMap) ===
|
|
363
|
+
// Builder state lives under `_builder` (compiled format) or at the top level
|
|
364
|
+
// (intermediate format). Read from _builder first, then fall back.
|
|
365
|
+
let buildMethod: PopulationDefinition['buildMethod'] = builderState?.buildMethod || (config as any).buildMethod || 'SQL_BUILDER';
|
|
366
|
+
let indicatorRules: PopulationDefinition['indicatorRules'] = undefined;
|
|
367
|
+
let loadedVisualFilter: any = undefined;
|
|
368
|
+
let loadedFilterMap: any = undefined;
|
|
369
|
+
|
|
370
|
+
// Try to load visualFilter from config._builder or top-level config
|
|
371
|
+
const configVisualFilter = builderState?.visualFilter || (config as any).visualFilter;
|
|
372
|
+
if (configVisualFilter) {
|
|
373
|
+
loadedVisualFilter = configVisualFilter;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Try to load filterMap from config._builder or config.baseCohortDefinition.config
|
|
377
|
+
const configFilterMap = builderState?.filterMap || config.baseCohortDefinition?.config?.filterMap;
|
|
378
|
+
if (configFilterMap) {
|
|
379
|
+
loadedFilterMap = configFilterMap;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const configIndicatorRules = builderState?.indicatorRules || (config as any).indicatorRules;
|
|
383
|
+
if (Array.isArray(configIndicatorRules) && configIndicatorRules.length > 0) {
|
|
384
|
+
indicatorRules = configIndicatorRules.map((rule: any, idx: number) => ({
|
|
385
|
+
id: rule.id || `rule-${idx}`,
|
|
386
|
+
indicatorUuid: rule.indicatorUuid,
|
|
387
|
+
name: rule.name || '',
|
|
388
|
+
conditions: rule.conditions,
|
|
389
|
+
logicalOperator: rule.logicalOperator || 'AND',
|
|
390
|
+
negate: rule.negate || false,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Fall back to metaJson for buildMethod, indicator rules, parameters if not in config
|
|
395
|
+
// Load parameters from metaJson if available (for dynamic parameter options like LIST options)
|
|
396
|
+
if (report.metaJson) {
|
|
397
|
+
try {
|
|
398
|
+
const meta = JSON.parse(report.metaJson);
|
|
399
|
+
// Try to get buildMethod from metaJson as fallback
|
|
400
|
+
if (meta.buildMethod && (meta.buildMethod === 'INDICATOR_BASED' || meta.buildMethod === 'VISUAL_FILTER' || meta.buildMethod === 'SQL_BUILDER')) {
|
|
401
|
+
buildMethod = meta.buildMethod;
|
|
402
|
+
}
|
|
403
|
+
// Also try to get indicator rules from metaJson as fallback
|
|
404
|
+
if (!indicatorRules && Array.isArray(meta.indicatorRules) && meta.indicatorRules.length > 0) {
|
|
405
|
+
indicatorRules = meta.indicatorRules.map((rule: any, idx: number) => ({
|
|
406
|
+
id: rule.id || `rule-${idx}`,
|
|
407
|
+
indicatorUuid: rule.indicatorUuid,
|
|
408
|
+
name: rule.name || '',
|
|
409
|
+
conditions: rule.conditions,
|
|
410
|
+
logicalOperator: rule.logicalOperator || 'AND',
|
|
411
|
+
negate: rule.negate || false,
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
// Load parameters from metaJson if available (for dynamic parameter options)
|
|
415
|
+
// This takes precedence over config parameters since metaJson may have updated options
|
|
416
|
+
if (meta.parameters && Array.isArray(meta.parameters) && meta.parameters.length > 0) {
|
|
417
|
+
parameters = meta.parameters.map((param: any, idx: number) => ({
|
|
418
|
+
id: param.id || `param-${idx}`,
|
|
419
|
+
name: param.name,
|
|
420
|
+
label: param.label,
|
|
421
|
+
type: param.type,
|
|
422
|
+
required: param.required || false,
|
|
423
|
+
defaultValue: param.defaultValue || '',
|
|
424
|
+
displayOrder: param.displayOrder !== undefined ? param.displayOrder : idx,
|
|
425
|
+
config: (param.config || { type: param.type }) as LinelistParameterConfig,
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.warn('Failed to parse metaJson:', e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// === BUILD SORT CONFIG ===
|
|
434
|
+
const sortConfig: LinelistSortConfig[] = [];
|
|
435
|
+
if (config.orderBy) {
|
|
436
|
+
sortConfig.push({
|
|
437
|
+
id: `sort-0`,
|
|
438
|
+
columnId: config.orderBy,
|
|
439
|
+
columnName: config.orderBy,
|
|
440
|
+
direction: config.orderDirection || 'ASC',
|
|
441
|
+
nulls: 'LAST',
|
|
442
|
+
sortOrder: 0,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// === BUILD POPULATION DEFINITION ===
|
|
447
|
+
const cohortSql = config.baseCohortDefinition?.config?.sql || '';
|
|
448
|
+
const baseDsUuid = dataSources.find(ds => ds.role === 'PRIMARY')?.uuid || primaryDataSourceUuid;
|
|
449
|
+
|
|
450
|
+
const population: PopulationDefinition = {
|
|
451
|
+
baseDataSourceUuid: baseDsUuid,
|
|
452
|
+
buildMethod,
|
|
453
|
+
sqlTemplate: cohortSql,
|
|
454
|
+
parameterReferences: extractParameterReferences(cohortSql),
|
|
455
|
+
// Restore visual filter from metaJson if available, otherwise use empty default
|
|
456
|
+
visualFilter: loadedVisualFilter || {
|
|
457
|
+
rootGroup: {
|
|
458
|
+
id: 'root',
|
|
459
|
+
logicalOperator: 'AND',
|
|
460
|
+
conditions: [],
|
|
461
|
+
nestedGroups: [],
|
|
462
|
+
},
|
|
463
|
+
useVisualBuilder: false,
|
|
464
|
+
},
|
|
465
|
+
// Restore filter map from metaJson or config, otherwise undefined
|
|
466
|
+
filterMap: loadedFilterMap || undefined,
|
|
467
|
+
// Reconstruct indicator rules so they show as selected on edit
|
|
468
|
+
indicatorRules,
|
|
469
|
+
buildHistory: [
|
|
470
|
+
{
|
|
471
|
+
timestamp: now,
|
|
472
|
+
action: 'IMPORTED_FROM_EXISTING',
|
|
473
|
+
description: config.version === 1 ? 'Migrated from V1 format (single datasource)' : 'Imported from existing report definition',
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// === CREATE THE DRAFT ===
|
|
479
|
+
// Build population sources from primary datasource
|
|
480
|
+
const populationSources: PopulationSource[] = [];
|
|
481
|
+
const primaryDs = dataSources.find(ds => ds.role === 'PRIMARY');
|
|
482
|
+
if (primaryDs) {
|
|
483
|
+
populationSources.push({
|
|
484
|
+
uuid: primaryDs.uuid,
|
|
485
|
+
name: primaryDs.name,
|
|
486
|
+
type: primaryDs.type,
|
|
487
|
+
joinType: 'JOIN',
|
|
488
|
+
enabled: true,
|
|
489
|
+
order: 0,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const draft: LinelistReportDraft = {
|
|
494
|
+
version: 3,
|
|
495
|
+
name: report.name || '',
|
|
496
|
+
description: report.description || '',
|
|
497
|
+
code: report.code || '',
|
|
498
|
+
currentPanel: 'basics',
|
|
499
|
+
categoryUuid: config.categoryUuid || '',
|
|
500
|
+
rowGrain: config.rowGrain || 'PATIENT',
|
|
501
|
+
templateId: config.templateId || '',
|
|
502
|
+
populationSources,
|
|
503
|
+
dataSources,
|
|
504
|
+
population,
|
|
505
|
+
columns,
|
|
506
|
+
parameters,
|
|
507
|
+
sortConfig,
|
|
508
|
+
limit: config.limit,
|
|
509
|
+
// Legacy top-level fields used by the indicator UI and getCohortSql().
|
|
510
|
+
// Keep them in sync with population.* so the existing code paths work.
|
|
511
|
+
populationMode: buildMethod === 'INDICATOR_BASED' ? 'INDICATOR' : 'SQL',
|
|
512
|
+
indicatorRules,
|
|
513
|
+
displaySettings: {
|
|
514
|
+
defaultPageSize: 25,
|
|
515
|
+
freezeFirstColumn: true,
|
|
516
|
+
freezeHeader: true,
|
|
517
|
+
dateDisplayFormat: 'dd MMM yyyy',
|
|
518
|
+
nullDisplayValue: '—',
|
|
519
|
+
maxInteractiveRows: 500,
|
|
520
|
+
maxExportRows: 100000,
|
|
521
|
+
allowedExports: ['CSV', 'XLSX', 'PDF'],
|
|
522
|
+
includeParametersInExportHeader: true,
|
|
523
|
+
includeGeneratedTimestamp: true,
|
|
524
|
+
},
|
|
525
|
+
validation: {
|
|
526
|
+
errors: {},
|
|
527
|
+
warnings: {},
|
|
528
|
+
lastValidated: now,
|
|
529
|
+
},
|
|
530
|
+
metadata: {
|
|
531
|
+
createdAt: now,
|
|
532
|
+
lastModified: now,
|
|
533
|
+
buildMethod: 'EDIT',
|
|
534
|
+
sourceReportUuid: report.uuid,
|
|
535
|
+
version: 3,
|
|
536
|
+
status: 'DRAFT',
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
return draft;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Extract parameter references from SQL
|
|
545
|
+
* Finds patterns like :paramName
|
|
546
|
+
*/
|
|
547
|
+
function extractParameterReferences(sql: string): string[] {
|
|
548
|
+
const paramRegex = /:(\w+)/g;
|
|
549
|
+
const params = new Set<string>();
|
|
550
|
+
let match;
|
|
551
|
+
while ((match = paramRegex.exec(sql)) !== null) {
|
|
552
|
+
params.add(match[1]);
|
|
553
|
+
}
|
|
554
|
+
return Array.from(params);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const LinelistBuilderWorkspace: React.FC<Props> = () => {
|
|
558
|
+
const navigate = useNavigate();
|
|
559
|
+
const { reportId } = useParams();
|
|
560
|
+
const { has: hasPrivilege } = useReportBuilderPrivileges();
|
|
561
|
+
|
|
562
|
+
// Report state
|
|
563
|
+
const [draft, setDraft] = useState<LinelistReportDraft>(createEmptyDraft());
|
|
564
|
+
const [initialReport, setInitialReport] = useState<LinelistReportDto | null>(null);
|
|
565
|
+
const [loading, setLoading] = useState(false);
|
|
566
|
+
const [saving, setSaving] = useState(false);
|
|
567
|
+
const [compiling, setCompiling] = useState(false);
|
|
568
|
+
const [error, setError] = useState<string | null>(null);
|
|
569
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
570
|
+
const [compileError, setCompileError] = useState<string | null>(null);
|
|
571
|
+
const [compileSuccess, setCompileSuccess] = useState<string | null>(null);
|
|
572
|
+
const [mode, setMode] = useState<'create' | 'edit'>('create');
|
|
573
|
+
|
|
574
|
+
// Reference data
|
|
575
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
576
|
+
const [categories, setCategories] = useState<ReportCategoryDto[]>([]);
|
|
577
|
+
|
|
578
|
+
// Panel state
|
|
579
|
+
const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(false);
|
|
580
|
+
const [middlePanelCollapsed, setMiddlePanelCollapsed] = useState(false);
|
|
581
|
+
const [leftPanelWidth, setLeftPanelWidth] = useState(500);
|
|
582
|
+
const [middlePanelWidth, setMiddlePanelWidth] = useState(500);
|
|
583
|
+
|
|
584
|
+
// Resizable state
|
|
585
|
+
const [isResizingLeft, setIsResizingLeft] = useState(false);
|
|
586
|
+
const [isResizingMiddle, setIsResizingMiddle] = useState(false);
|
|
587
|
+
|
|
588
|
+
// Preview state
|
|
589
|
+
const [previewRunning, setPreviewRunning] = useState(false);
|
|
590
|
+
const [cachedReportDefinitionUuid, setCachedReportDefinitionUuid] = useState<string | null>(null);
|
|
591
|
+
const [previewData, setPreviewData] = useState<{
|
|
592
|
+
columns: string[];
|
|
593
|
+
rows: Record<string, any>[];
|
|
594
|
+
rowCount: number;
|
|
595
|
+
html?: string;
|
|
596
|
+
} | null>(null);
|
|
597
|
+
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
598
|
+
const [previewParamModalOpen, setPreviewParamModalOpen] = useState(false);
|
|
599
|
+
|
|
600
|
+
// Available fields from data catalogue (for visual filter builder)
|
|
601
|
+
const [availableFields, setAvailableFields] = useState<Array<{ name: string; label: string; type: FilterFieldType }>>([]);
|
|
602
|
+
|
|
603
|
+
// ETL structure for selected data source (for future use)
|
|
604
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
605
|
+
const [etlStructure, setEtlStructure] = useState<EtlStructure | null>(null);
|
|
606
|
+
|
|
607
|
+
// Indicators for population (load all available indicators)
|
|
608
|
+
const { indicators } = useIndicatorsByTheme(''); // Empty string to load all indicators
|
|
609
|
+
|
|
610
|
+
// Compile setup modal state
|
|
611
|
+
const [showCompileSetupModal, setShowCompileSetupModal] = useState(false);
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Load report for editing
|
|
616
|
+
*/
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
if (reportId && reportId !== 'new') {
|
|
619
|
+
setLoading(true);
|
|
620
|
+
setError(null);
|
|
621
|
+
|
|
622
|
+
import('../../../resources/linelist/linelist-reports.api')
|
|
623
|
+
.then(({ getLinelistReport }) => getLinelistReport(reportId))
|
|
624
|
+
.then((report: LinelistReportDto) => {
|
|
625
|
+
setInitialReport(report);
|
|
626
|
+
setMode('edit');
|
|
627
|
+
// Convert report to draft
|
|
628
|
+
const draft = reportToDraft(report);
|
|
629
|
+
setDraft(draft);
|
|
630
|
+
// Cache the compiledReportDefinitionUuid from the loaded report for preview
|
|
631
|
+
if (report.compiledReportDefinitionUuid) {
|
|
632
|
+
setCachedReportDefinitionUuid(report.compiledReportDefinitionUuid);
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
.catch((err) => {
|
|
636
|
+
console.error('Failed to load report:', err);
|
|
637
|
+
setError(err instanceof Error ? err.message : 'Failed to load report');
|
|
638
|
+
})
|
|
639
|
+
.finally(() => setLoading(false));
|
|
640
|
+
} else {
|
|
641
|
+
setMode('create');
|
|
642
|
+
setDraft(createEmptyDraft());
|
|
643
|
+
setLoading(false);
|
|
644
|
+
}
|
|
645
|
+
}, [reportId]);
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Load reference data
|
|
649
|
+
*/
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
const loadData = async () => {
|
|
652
|
+
try {
|
|
653
|
+
const cats = await listReportCategories();
|
|
654
|
+
setCategories(cats);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
console.error('Failed to load reference data:', err);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
loadData();
|
|
660
|
+
}, []);
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Invalidate compiled cache when draft structure changes
|
|
664
|
+
* This ensures we recompile when columns, data sources, or SQL changes
|
|
665
|
+
*/
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
// Clear cache when draft structure changes
|
|
668
|
+
setCachedReportDefinitionUuid(null);
|
|
669
|
+
}, [draft.columns.length, draft.dataSources.length, draft.population.sqlTemplate, draft.rowGrain]);
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Handle save draft
|
|
673
|
+
*/
|
|
674
|
+
const handleSave = useCallback(async () => {
|
|
675
|
+
setSaving(true);
|
|
676
|
+
setSaveError(null);
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
// Convert indicators array to Map for draftToConfig
|
|
680
|
+
const indicatorsMap = new Map(
|
|
681
|
+
indicators.map(ind => [ind.uuid, ind])
|
|
682
|
+
);
|
|
683
|
+
// Pass indicators map so draftToConfig can generate SQL from indicator rules
|
|
684
|
+
const config = draftToConfig(draft, indicatorsMap);
|
|
685
|
+
// Preserve builder state (indicators, build method, population sources, visual filter, filter map) in metaJson so the
|
|
686
|
+
// editor can reconstruct the draft when re-opening for edit.
|
|
687
|
+
const builderMeta: LinelistBuilderMeta = {
|
|
688
|
+
buildMethod: draft.population.buildMethod,
|
|
689
|
+
indicatorRules: draft.population.indicatorRules,
|
|
690
|
+
populationSources: draft.populationSources,
|
|
691
|
+
// Preserve visual filter state for reconstructing the filter UI
|
|
692
|
+
visualFilter: draft.population.visualFilter,
|
|
693
|
+
// Preserve filter map for parameter-to-column mapping
|
|
694
|
+
filterMap: draft.population.filterMap,
|
|
695
|
+
};
|
|
696
|
+
const payload = configToSavePayload(config, draft, builderMeta);
|
|
697
|
+
|
|
698
|
+
let result;
|
|
699
|
+
if (mode === 'create') {
|
|
700
|
+
result = await createLinelistReport(payload);
|
|
701
|
+
// Navigate to edit mode after creating a new report
|
|
702
|
+
navigate(`/linelist/edit/${result.uuid}`, { replace: true });
|
|
703
|
+
} else {
|
|
704
|
+
result = await updateLinelistReport(initialReport?.uuid || '', payload);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
setInitialReport(result);
|
|
708
|
+
setDraft(prev => ({ ...prev, unsavedChanges: false }));
|
|
709
|
+
} catch (err) {
|
|
710
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save report');
|
|
711
|
+
} finally {
|
|
712
|
+
setSaving(false);
|
|
713
|
+
}
|
|
714
|
+
}, [draft, mode, initialReport, indicators, navigate]);
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Handle compile
|
|
718
|
+
* Always shows the compile setup modal for category confirmation/selection
|
|
719
|
+
*/
|
|
720
|
+
const handleCompile = useCallback(async () => {
|
|
721
|
+
setCompileError(null);
|
|
722
|
+
setSaveError(null);
|
|
723
|
+
|
|
724
|
+
// Always show the modal to allow category confirmation/selection
|
|
725
|
+
setShowCompileSetupModal(true);
|
|
726
|
+
}, []);
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Handle navigate back
|
|
730
|
+
*/
|
|
731
|
+
const handleBack = useCallback(() => {
|
|
732
|
+
navigate('/linelist');
|
|
733
|
+
}, [navigate]);
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Update draft field
|
|
737
|
+
*/
|
|
738
|
+
const updateDraft = useCallback((updates: Partial<LinelistReportDraft>) => {
|
|
739
|
+
setDraft(prev => ({ ...prev, ...updates, unsavedChanges: true }));
|
|
740
|
+
}, []);
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Handle compile setup modal cancel
|
|
744
|
+
*/
|
|
745
|
+
const handleCompileSetupCancel = useCallback(() => {
|
|
746
|
+
setShowCompileSetupModal(false);
|
|
747
|
+
}, []);
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Handle compile setup modal confirm
|
|
751
|
+
* This is called after the user selects category in the modal
|
|
752
|
+
*/
|
|
753
|
+
const handleCompileSetupConfirm = useCallback(async (result: CompileSetupResult) => {
|
|
754
|
+
setShowCompileSetupModal(false);
|
|
755
|
+
|
|
756
|
+
// Create updated draft with new category for validation
|
|
757
|
+
const updatedDraft = { ...draft, categoryUuid: result.categoryUuid };
|
|
758
|
+
|
|
759
|
+
// Check validation before saving/compiling
|
|
760
|
+
const errors = validateLinelistDraft(updatedDraft);
|
|
761
|
+
if (Object.keys(errors).length > 0) {
|
|
762
|
+
// Show specific validation errors
|
|
763
|
+
const errorMessages = Object.values(errors).join(', ');
|
|
764
|
+
setSaveError(`Cannot compile: ${errorMessages}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Update the draft with selected category
|
|
769
|
+
updateDraft({
|
|
770
|
+
categoryUuid: result.categoryUuid,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Save if there are unsaved changes or no initial report
|
|
774
|
+
if (draft.unsavedChanges || !initialReport?.uuid) {
|
|
775
|
+
await handleSave();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!initialReport?.uuid) {
|
|
779
|
+
setCompileError('Cannot compile: Report must be saved first');
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
setCompiling(true);
|
|
784
|
+
setCompileError(null);
|
|
785
|
+
setCompileSuccess(null);
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
const compiledResult = await compileLinelistReport(initialReport.uuid, draft.categoryUuid);
|
|
789
|
+
|
|
790
|
+
// Cache the reportDefinitionUuid for subsequent previews
|
|
791
|
+
if (compiledResult?.reportDefinitionUuid) {
|
|
792
|
+
setCachedReportDefinitionUuid(compiledResult.reportDefinitionUuid);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
setCompileSuccess(
|
|
796
|
+
compiledResult?.reportDefinitionUuid
|
|
797
|
+
? `Compiled successfully. Runtime report UUID: ${compiledResult.reportDefinitionUuid}`
|
|
798
|
+
: 'Compiled successfully.'
|
|
799
|
+
);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
setCompileError(err instanceof Error ? err.message : 'Failed to compile report');
|
|
802
|
+
} finally {
|
|
803
|
+
setCompiling(false);
|
|
804
|
+
}
|
|
805
|
+
}, [draft, initialReport?.uuid, handleSave, updateDraft]);
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Handle datasources change
|
|
809
|
+
* Updates the dataSources array in the draft
|
|
810
|
+
*/
|
|
811
|
+
const handleDataSourcesChange = useCallback((dataSources: DataSourceInfo[]) => {
|
|
812
|
+
updateDraft({ dataSources });
|
|
813
|
+
}, [updateDraft]);
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Handle data source table change
|
|
817
|
+
* Updates the primary data source in the dataSources array
|
|
818
|
+
*/
|
|
819
|
+
const handleTableChange = useCallback((table: string) => {
|
|
820
|
+
// For now, update the first data source or create a new primary one
|
|
821
|
+
const newDataSources = [...draft.dataSources];
|
|
822
|
+
if (newDataSources.length > 0) {
|
|
823
|
+
newDataSources[0].uuid = table;
|
|
824
|
+
newDataSources[0].name = table;
|
|
825
|
+
} else {
|
|
826
|
+
newDataSources.push({
|
|
827
|
+
uuid: table,
|
|
828
|
+
name: table,
|
|
829
|
+
type: 'ETL',
|
|
830
|
+
role: 'PRIMARY',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
updateDraft({ dataSources: newDataSources });
|
|
834
|
+
}, [updateDraft, draft.dataSources]);
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Get list of selected field IDs for highlighting in catalogue
|
|
838
|
+
* Constructs field IDs based on column type to match catalogue formats:
|
|
839
|
+
* - Person attributes: person-attribute-${uuid}
|
|
840
|
+
* - Patient identifiers: patient-identifier-${uuid}
|
|
841
|
+
* - Calculated fields: openmrs.calc.${name}
|
|
842
|
+
* - CORE/Demographic: openmrs.${table}.${name}
|
|
843
|
+
* - ETL columns: ${table}.${name}
|
|
844
|
+
*/
|
|
845
|
+
const selectedFieldIds = useMemo(() => {
|
|
846
|
+
return draft.columns.map((col) => {
|
|
847
|
+
// Person attributes
|
|
848
|
+
if (col.dataDefinitionType === 'PERSON_ATTRIBUTE') {
|
|
849
|
+
return `person-attribute-${col.source.attributeTypeUuid || col.name}`;
|
|
850
|
+
}
|
|
851
|
+
// Patient identifiers
|
|
852
|
+
if (col.dataDefinitionType === 'IDENTIFIER') {
|
|
853
|
+
return `patient-identifier-${col.source.identifierTypeUuid || col.name}`;
|
|
854
|
+
}
|
|
855
|
+
// Calculated fields
|
|
856
|
+
if (col.dataDefinitionType === 'CALCULATION') {
|
|
857
|
+
const calcName = (col.dataDefinitionConfig.calculation || col.name).toLowerCase();
|
|
858
|
+
return `openmrs.calc.${calcName}`;
|
|
859
|
+
}
|
|
860
|
+
// CORE/Demographic fields (person, person_address tables)
|
|
861
|
+
if (col.source.table === 'person' || col.source.table === 'person_address') {
|
|
862
|
+
return `openmrs.${col.source.table}.${col.source.field || col.name}`;
|
|
863
|
+
}
|
|
864
|
+
// Person name fields
|
|
865
|
+
if (col.dataDefinitionType === 'PERSON_NAME') {
|
|
866
|
+
return `openmrs.person.${col.source.field || col.name}`;
|
|
867
|
+
}
|
|
868
|
+
// ETL columns - use data source UUID or table name
|
|
869
|
+
const table = col.source.table || col.source.dataSourceUuid || 'unknown';
|
|
870
|
+
return `${table}.${col.source.field || col.name}`;
|
|
871
|
+
});
|
|
872
|
+
}, [draft.columns]);
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Handle removing a column
|
|
876
|
+
*/
|
|
877
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
878
|
+
const handleRemoveColumn = useCallback((columnId: string) => {
|
|
879
|
+
// Clear any error message
|
|
880
|
+
setSaveError(null);
|
|
881
|
+
}, []);
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Open preview parameter modal
|
|
885
|
+
*/
|
|
886
|
+
const handleRunPreview = useCallback(() => {
|
|
887
|
+
// Check if draft has required fields
|
|
888
|
+
if (!draft.population?.sqlTemplate || draft.columns.length === 0) {
|
|
889
|
+
setPreviewError('Please add a base cohort definition and at least one column before running preview.');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Check if report needs to be saved first (for new reports)
|
|
894
|
+
if (!initialReport?.uuid) {
|
|
895
|
+
setPreviewError('Please save the report first before running preview.');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Open the parameter modal
|
|
900
|
+
setPreviewParamModalOpen(true);
|
|
901
|
+
setPreviewError(null);
|
|
902
|
+
}, [draft.population?.sqlTemplate, draft.columns.length, initialReport?.uuid]);
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Run preview with parameters
|
|
906
|
+
*/
|
|
907
|
+
const handleRunPreviewWithParams = useCallback(async (parameterValues: Record<string, any>) => {
|
|
908
|
+
// Close the modal
|
|
909
|
+
setPreviewParamModalOpen(false);
|
|
910
|
+
setPreviewRunning(true);
|
|
911
|
+
setPreviewError(null);
|
|
912
|
+
|
|
913
|
+
try {
|
|
914
|
+
// Convert indicators array to Map for draftToConfig
|
|
915
|
+
const indicatorRules = draft.indicatorRules || draft.population?.indicatorRules || [];
|
|
916
|
+
const indicatorsMap = new Map(
|
|
917
|
+
indicatorRules.map(rule => [
|
|
918
|
+
rule.indicatorUuid,
|
|
919
|
+
{ sqlTemplate: rule.sqlTemplate, configJson: rule.configJson }
|
|
920
|
+
])
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
// Convert draft to config
|
|
924
|
+
const config = draftToConfig(draft, indicatorsMap);
|
|
925
|
+
|
|
926
|
+
// Call preview API with report UUID and parameters
|
|
927
|
+
// Only works with saved reports (not new drafts)
|
|
928
|
+
const result = await previewLinelistReport({
|
|
929
|
+
reportUuid: initialReport?.uuid || '',
|
|
930
|
+
config,
|
|
931
|
+
parameters: parameterValues,
|
|
932
|
+
maxRows: 100, // Limit preview to 100 rows
|
|
933
|
+
cachedReportDefinitionUuid: cachedReportDefinitionUuid || undefined,
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (!result.success) {
|
|
937
|
+
setPreviewError(result.error || 'Preview failed');
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (result.data) {
|
|
942
|
+
// Store the full result with metadata
|
|
943
|
+
setPreviewData({
|
|
944
|
+
columns: result.data.columns,
|
|
945
|
+
rows: result.data.rows,
|
|
946
|
+
rowCount: result.data.rowCount,
|
|
947
|
+
html: result.data.html,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
} catch (err) {
|
|
951
|
+
setPreviewError(err instanceof Error ? err.message : 'Preview failed');
|
|
952
|
+
} finally {
|
|
953
|
+
setPreviewRunning(false);
|
|
954
|
+
}
|
|
955
|
+
}, [draft]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Handle adding field from catalogue as column
|
|
959
|
+
*/
|
|
960
|
+
const handleAddFieldAsColumn = useCallback((field: CatalogueField) => {
|
|
961
|
+
// Check if column already exists
|
|
962
|
+
if (draft.columns.some((col) => col.name === field.label || col.name === field.name)) {
|
|
963
|
+
setSaveError('This field is already added as a column');
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Handle special cases for person attributes and patient identifiers
|
|
968
|
+
const isPersonAttribute = field.id.startsWith('person-attribute-');
|
|
969
|
+
const isPatientIdentifier = field.id.startsWith('patient-identifier-');
|
|
970
|
+
const uuid = field.name; // For API fields, we store the UUID in the name field
|
|
971
|
+
const now = new Date().toISOString();
|
|
972
|
+
|
|
973
|
+
let newColumn: LinelistColumnDraft;
|
|
974
|
+
let source: ColumnSource;
|
|
975
|
+
|
|
976
|
+
if (isPersonAttribute) {
|
|
977
|
+
// Person Attribute column
|
|
978
|
+
source = {
|
|
979
|
+
dataSourceUuid: 'person_attributes',
|
|
980
|
+
dataSourceName: 'Person Attributes',
|
|
981
|
+
table: 'person_attribute',
|
|
982
|
+
field: field.label,
|
|
983
|
+
fieldType: 'PERSON_ATTRIBUTE',
|
|
984
|
+
attributeTypeUuid: uuid,
|
|
985
|
+
};
|
|
986
|
+
newColumn = {
|
|
987
|
+
id: `col-${Date.now()}`,
|
|
988
|
+
name: field.label,
|
|
989
|
+
description: '',
|
|
990
|
+
source,
|
|
991
|
+
dataDefinitionType: 'PERSON_ATTRIBUTE',
|
|
992
|
+
dataDefinitionConfig: { attributeTypeUuid: uuid },
|
|
993
|
+
additionInfo: {
|
|
994
|
+
addedVia: 'DRAG_DROP',
|
|
995
|
+
addedAt: now,
|
|
996
|
+
orderAdded: draft.columns.length,
|
|
997
|
+
},
|
|
998
|
+
display: {
|
|
999
|
+
width: 150,
|
|
1000
|
+
align: 'left',
|
|
1001
|
+
sortable: true,
|
|
1002
|
+
filterable: true,
|
|
1003
|
+
format: 'text',
|
|
1004
|
+
},
|
|
1005
|
+
sortOrder: draft.columns.length,
|
|
1006
|
+
transformations: [],
|
|
1007
|
+
};
|
|
1008
|
+
} else if (isPatientIdentifier) {
|
|
1009
|
+
// Patient Identifier column
|
|
1010
|
+
source = {
|
|
1011
|
+
dataSourceUuid: 'patient_identifiers',
|
|
1012
|
+
dataSourceName: 'Patient Identifiers',
|
|
1013
|
+
table: 'patient_identifier',
|
|
1014
|
+
field: field.label,
|
|
1015
|
+
fieldType: 'IDENTIFIER',
|
|
1016
|
+
identifierTypeUuid: uuid,
|
|
1017
|
+
};
|
|
1018
|
+
newColumn = {
|
|
1019
|
+
id: `col-${Date.now()}`,
|
|
1020
|
+
name: field.label,
|
|
1021
|
+
description: '',
|
|
1022
|
+
source,
|
|
1023
|
+
dataDefinitionType: 'IDENTIFIER',
|
|
1024
|
+
dataDefinitionConfig: { identifierTypeUuid: uuid, preferred: false },
|
|
1025
|
+
additionInfo: {
|
|
1026
|
+
addedVia: 'DRAG_DROP',
|
|
1027
|
+
addedAt: now,
|
|
1028
|
+
orderAdded: draft.columns.length,
|
|
1029
|
+
},
|
|
1030
|
+
display: {
|
|
1031
|
+
width: 150,
|
|
1032
|
+
align: 'left',
|
|
1033
|
+
sortable: true,
|
|
1034
|
+
filterable: true,
|
|
1035
|
+
format: 'text',
|
|
1036
|
+
},
|
|
1037
|
+
sortOrder: draft.columns.length,
|
|
1038
|
+
repeatResolution: {
|
|
1039
|
+
strategy: 'LATEST',
|
|
1040
|
+
orderBy: undefined,
|
|
1041
|
+
restrictToPeriod: false,
|
|
1042
|
+
ignoreVoided: true,
|
|
1043
|
+
},
|
|
1044
|
+
transformations: [],
|
|
1045
|
+
};
|
|
1046
|
+
} else if (field.source === 'CALCULATION') {
|
|
1047
|
+
// Calculated field
|
|
1048
|
+
const primaryDataSource = draft.dataSources.find(ds => ds.role === 'PRIMARY');
|
|
1049
|
+
source = {
|
|
1050
|
+
dataSourceUuid: primaryDataSource?.uuid || '',
|
|
1051
|
+
dataSourceName: primaryDataSource?.name || '',
|
|
1052
|
+
table: 'calculated',
|
|
1053
|
+
field: field.name,
|
|
1054
|
+
fieldType: 'CALCULATED',
|
|
1055
|
+
};
|
|
1056
|
+
newColumn = {
|
|
1057
|
+
id: `col-${Date.now()}`,
|
|
1058
|
+
name: field.label || field.name,
|
|
1059
|
+
description: '',
|
|
1060
|
+
source,
|
|
1061
|
+
dataDefinitionType: 'CALCULATION',
|
|
1062
|
+
dataDefinitionConfig: { calculation: field.name.toUpperCase(), onDate: ':startDate' },
|
|
1063
|
+
additionInfo: {
|
|
1064
|
+
addedVia: 'DRAG_DROP',
|
|
1065
|
+
addedAt: now,
|
|
1066
|
+
orderAdded: draft.columns.length,
|
|
1067
|
+
},
|
|
1068
|
+
display: {
|
|
1069
|
+
width: 120,
|
|
1070
|
+
align: 'center',
|
|
1071
|
+
sortable: true,
|
|
1072
|
+
filterable: false,
|
|
1073
|
+
format: 'number',
|
|
1074
|
+
},
|
|
1075
|
+
sortOrder: draft.columns.length,
|
|
1076
|
+
transformations: [],
|
|
1077
|
+
};
|
|
1078
|
+
} else if (field.id.startsWith('openmrs.person_address.') || field.table === 'person_address') {
|
|
1079
|
+
// Person Address field
|
|
1080
|
+
source = {
|
|
1081
|
+
dataSourceUuid: 'person_address',
|
|
1082
|
+
dataSourceName: 'Person Address',
|
|
1083
|
+
table: 'person_address',
|
|
1084
|
+
field: field.name,
|
|
1085
|
+
fieldType: 'TEXT',
|
|
1086
|
+
};
|
|
1087
|
+
newColumn = {
|
|
1088
|
+
id: `col-${Date.now()}`,
|
|
1089
|
+
name: field.label || field.name,
|
|
1090
|
+
description: '',
|
|
1091
|
+
source,
|
|
1092
|
+
dataDefinitionType: 'PERSON_ADDRESS',
|
|
1093
|
+
dataDefinitionConfig: {
|
|
1094
|
+
type: 'ADDRESS_FIELD',
|
|
1095
|
+
field: field.name, // e.g., 'address5', 'address4', etc.
|
|
1096
|
+
},
|
|
1097
|
+
additionInfo: {
|
|
1098
|
+
addedVia: 'DRAG_DROP',
|
|
1099
|
+
addedAt: now,
|
|
1100
|
+
orderAdded: draft.columns.length,
|
|
1101
|
+
},
|
|
1102
|
+
display: {
|
|
1103
|
+
width: 150,
|
|
1104
|
+
align: 'left',
|
|
1105
|
+
sortable: true,
|
|
1106
|
+
filterable: true,
|
|
1107
|
+
format: 'text',
|
|
1108
|
+
},
|
|
1109
|
+
sortOrder: draft.columns.length,
|
|
1110
|
+
transformations: [],
|
|
1111
|
+
};
|
|
1112
|
+
} else {
|
|
1113
|
+
// SQL/ETL column
|
|
1114
|
+
source = {
|
|
1115
|
+
dataSourceUuid: field.dataSourceUuid || field.table,
|
|
1116
|
+
dataSourceName: field.dataSourceName || field.table,
|
|
1117
|
+
table: field.table,
|
|
1118
|
+
field: field.name,
|
|
1119
|
+
fieldType: field.type || 'UNKNOWN',
|
|
1120
|
+
};
|
|
1121
|
+
newColumn = {
|
|
1122
|
+
id: `col-${Date.now()}`,
|
|
1123
|
+
name: field.label || field.name,
|
|
1124
|
+
description: '',
|
|
1125
|
+
source,
|
|
1126
|
+
dataDefinitionType: 'SQL',
|
|
1127
|
+
dataDefinitionConfig: {
|
|
1128
|
+
sql: `\`${field.table}\`.\`${field.name}\``, // Default SQL - table.column
|
|
1129
|
+
},
|
|
1130
|
+
additionInfo: {
|
|
1131
|
+
addedVia: 'DRAG_DROP',
|
|
1132
|
+
addedAt: now,
|
|
1133
|
+
orderAdded: draft.columns.length,
|
|
1134
|
+
},
|
|
1135
|
+
display: {
|
|
1136
|
+
width: 150,
|
|
1137
|
+
align: 'left',
|
|
1138
|
+
sortable: true,
|
|
1139
|
+
filterable: true,
|
|
1140
|
+
format: 'text',
|
|
1141
|
+
},
|
|
1142
|
+
sortOrder: draft.columns.length,
|
|
1143
|
+
repeatResolution: field.isRepeated ? {
|
|
1144
|
+
strategy: 'LATEST',
|
|
1145
|
+
orderBy: undefined,
|
|
1146
|
+
restrictToPeriod: false,
|
|
1147
|
+
ignoreVoided: true,
|
|
1148
|
+
} : undefined,
|
|
1149
|
+
transformations: [],
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const updatedColumns = [...draft.columns, newColumn];
|
|
1154
|
+
updateDraft({ columns: updatedColumns });
|
|
1155
|
+
}, [draft.columns, draft.dataSources, updateDraft]);
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Handle adding a custom SQL column from the modal
|
|
1159
|
+
* The SQL runs per-row and can reference :client_id / :patient_id
|
|
1160
|
+
*/
|
|
1161
|
+
const handleAddCustomSqlColumn = useCallback((config: CustomSqlColumnConfig) => {
|
|
1162
|
+
// Prevent duplicate column names
|
|
1163
|
+
if (draft.columns.some((col) => col.name.toLowerCase() === config.name.toLowerCase())) {
|
|
1164
|
+
setSaveError('A column with this name already exists');
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const now = new Date().toISOString();
|
|
1169
|
+
const primaryDataSource = draft.dataSources.find(ds => ds.role === 'PRIMARY');
|
|
1170
|
+
|
|
1171
|
+
const newColumn: LinelistColumnDraft = {
|
|
1172
|
+
id: `col-${Date.now()}`,
|
|
1173
|
+
name: config.name,
|
|
1174
|
+
description: config.description || '',
|
|
1175
|
+
source: {
|
|
1176
|
+
dataSourceUuid: primaryDataSource?.uuid || 'custom_sql',
|
|
1177
|
+
dataSourceName: primaryDataSource?.name || 'Custom SQL',
|
|
1178
|
+
table: 'custom_sql',
|
|
1179
|
+
field: config.name,
|
|
1180
|
+
fieldType: 'SQL',
|
|
1181
|
+
},
|
|
1182
|
+
dataDefinitionType: 'SQL',
|
|
1183
|
+
dataDefinitionConfig: {
|
|
1184
|
+
sql: config.sql,
|
|
1185
|
+
},
|
|
1186
|
+
additionInfo: {
|
|
1187
|
+
addedVia: 'SQL_BUILDER',
|
|
1188
|
+
addedAt: now,
|
|
1189
|
+
orderAdded: draft.columns.length,
|
|
1190
|
+
},
|
|
1191
|
+
display: {
|
|
1192
|
+
width: 150,
|
|
1193
|
+
align: 'left',
|
|
1194
|
+
sortable: true,
|
|
1195
|
+
filterable: true,
|
|
1196
|
+
format: 'text',
|
|
1197
|
+
},
|
|
1198
|
+
sortOrder: draft.columns.length,
|
|
1199
|
+
repeatResolution: config.repeatResolution ? {
|
|
1200
|
+
strategy: 'LATEST',
|
|
1201
|
+
orderBy: undefined,
|
|
1202
|
+
restrictToPeriod: false,
|
|
1203
|
+
ignoreVoided: true,
|
|
1204
|
+
} : undefined,
|
|
1205
|
+
transformations: [],
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const updatedColumns = [...draft.columns, newColumn];
|
|
1209
|
+
updateDraft({ columns: updatedColumns });
|
|
1210
|
+
}, [draft.columns, draft.dataSources, updateDraft]);
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Handle adding draft column (from observation/diagnosis modals)
|
|
1214
|
+
*/
|
|
1215
|
+
const handleAddDraftColumn = useCallback((column: LinelistColumnDraft) => {
|
|
1216
|
+
// Prevent duplicate column names
|
|
1217
|
+
if (draft.columns.some((col) => col.name.toLowerCase() === column.name.toLowerCase())) {
|
|
1218
|
+
setSaveError('A column with this name already exists');
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const updatedColumns = [...draft.columns, column];
|
|
1223
|
+
updateDraft({ columns: updatedColumns });
|
|
1224
|
+
}, [draft.columns, updateDraft]);
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Handle adding field from catalogue as filter
|
|
1228
|
+
*/
|
|
1229
|
+
const handleAddFieldAsFilter = useCallback((field: CatalogueField) => {
|
|
1230
|
+
const newCondition = {
|
|
1231
|
+
id: `cond-${Date.now()}`,
|
|
1232
|
+
field: field.name,
|
|
1233
|
+
fieldLabel: field.label,
|
|
1234
|
+
fieldType: field.type,
|
|
1235
|
+
operator: getDefaultOperator(field.type),
|
|
1236
|
+
value: '',
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
const updatedFilter = {
|
|
1240
|
+
...draft.population.visualFilter,
|
|
1241
|
+
rootGroup: {
|
|
1242
|
+
...draft.population.visualFilter?.rootGroup,
|
|
1243
|
+
conditions: [...(draft.population.visualFilter?.rootGroup?.conditions || []), newCondition],
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
updateDraft({ population: { ...draft.population, visualFilter: updatedFilter } });
|
|
1248
|
+
}, [draft.population, updateDraft]);
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Handle fields available from data catalogue
|
|
1252
|
+
* Convert CatalogueField[] to format expected by visual filter builder
|
|
1253
|
+
*/
|
|
1254
|
+
const handleFieldsAvailable = useCallback((fields: CatalogueField[]) => {
|
|
1255
|
+
const filterFields = fields.map((f) => ({
|
|
1256
|
+
name: f.name,
|
|
1257
|
+
label: f.label,
|
|
1258
|
+
type: f.type,
|
|
1259
|
+
}));
|
|
1260
|
+
setAvailableFields(filterFields);
|
|
1261
|
+
}, []);
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Handle ETL structure detection from data catalogue
|
|
1265
|
+
*/
|
|
1266
|
+
const handleEtlStructureDetected = useCallback((structure: EtlStructure) => {
|
|
1267
|
+
setEtlStructure(structure);
|
|
1268
|
+
}, []);
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Handle panel resize with mouse
|
|
1272
|
+
*/
|
|
1273
|
+
useEffect(() => {
|
|
1274
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
1275
|
+
if (isResizingLeft) {
|
|
1276
|
+
const newWidth = Math.max(200, Math.min(500, e.clientX));
|
|
1277
|
+
setLeftPanelWidth(newWidth);
|
|
1278
|
+
}
|
|
1279
|
+
if (isResizingMiddle) {
|
|
1280
|
+
const workspaceRect = document.querySelector(`.${styles.workspace}`)?.getBoundingClientRect();
|
|
1281
|
+
if (workspaceRect) {
|
|
1282
|
+
const leftPanelEnd = leftPanelCollapsed ? 48 : leftPanelWidth;
|
|
1283
|
+
const newWidth = Math.max(400, Math.min(700, e.clientX - workspaceRect.left - leftPanelEnd));
|
|
1284
|
+
setMiddlePanelWidth(newWidth);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
const handleMouseUp = () => {
|
|
1290
|
+
setIsResizingLeft(false);
|
|
1291
|
+
setIsResizingMiddle(false);
|
|
1292
|
+
document.body.style.cursor = 'default';
|
|
1293
|
+
document.body.style.userSelect = '';
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
if (isResizingLeft || isResizingMiddle) {
|
|
1297
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
1298
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
1299
|
+
document.body.style.cursor = 'col-resize';
|
|
1300
|
+
document.body.style.userSelect = 'none';
|
|
1301
|
+
|
|
1302
|
+
return () => {
|
|
1303
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
1304
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
1305
|
+
document.body.style.cursor = 'default';
|
|
1306
|
+
document.body.style.userSelect = '';
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
}, [isResizingLeft, isResizingMiddle, leftPanelWidth, leftPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Start resizing left panel
|
|
1313
|
+
*/
|
|
1314
|
+
const startResizeLeft = useCallback((e: React.MouseEvent) => {
|
|
1315
|
+
e.preventDefault();
|
|
1316
|
+
setIsResizingLeft(true);
|
|
1317
|
+
}, []);
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Start resizing middle panel
|
|
1321
|
+
*/
|
|
1322
|
+
const startResizeMiddle = useCallback((e: React.MouseEvent) => {
|
|
1323
|
+
e.preventDefault();
|
|
1324
|
+
setIsResizingMiddle(true);
|
|
1325
|
+
}, []);
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Get default operator for field type
|
|
1329
|
+
*/
|
|
1330
|
+
function getDefaultOperator(fieldType: FilterFieldType): FilterOperator {
|
|
1331
|
+
switch (fieldType) {
|
|
1332
|
+
case 'TEXT':
|
|
1333
|
+
return 'CONTAINS';
|
|
1334
|
+
case 'NUMBER':
|
|
1335
|
+
return 'EQUALS';
|
|
1336
|
+
case 'DATE':
|
|
1337
|
+
return 'BETWEEN_DATES';
|
|
1338
|
+
case 'CODED':
|
|
1339
|
+
return 'IS_ONE_OF';
|
|
1340
|
+
case 'BOOLEAN':
|
|
1341
|
+
return 'IS_TRUE';
|
|
1342
|
+
case 'LOCATION':
|
|
1343
|
+
return 'IN_LOCATION';
|
|
1344
|
+
default:
|
|
1345
|
+
return 'EQUALS';
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (loading) {
|
|
1350
|
+
return (
|
|
1351
|
+
<div className={styles.workspace}>
|
|
1352
|
+
<div className={styles.loading}>Loading report...</div>
|
|
1353
|
+
</div>
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return (
|
|
1358
|
+
<div className={styles.workspace}>
|
|
1359
|
+
{/* Header */}
|
|
1360
|
+
<header className={styles.header}>
|
|
1361
|
+
<div className={styles.headerLeft}>
|
|
1362
|
+
<Button
|
|
1363
|
+
kind="ghost"
|
|
1364
|
+
size="sm"
|
|
1365
|
+
hasIconOnly
|
|
1366
|
+
renderIcon={ChevronLeft}
|
|
1367
|
+
iconDescription="Back"
|
|
1368
|
+
onClick={handleBack}
|
|
1369
|
+
/>
|
|
1370
|
+
</div>
|
|
1371
|
+
<div className={styles.headerCenter}>
|
|
1372
|
+
<TextInput
|
|
1373
|
+
id="report-name"
|
|
1374
|
+
labelText=""
|
|
1375
|
+
value={draft.name}
|
|
1376
|
+
onChange={(e) => updateDraft({ name: e.target.value })}
|
|
1377
|
+
placeholder="Report name"
|
|
1378
|
+
className={styles.nameInput}
|
|
1379
|
+
size="sm"
|
|
1380
|
+
/>
|
|
1381
|
+
<Tag size="sm" type={compileSuccess ? 'green' : 'gray'}>
|
|
1382
|
+
{compileSuccess ? 'Compiled' : 'Draft'}
|
|
1383
|
+
</Tag>
|
|
1384
|
+
{draft.unsavedChanges && (
|
|
1385
|
+
<Tag size="sm" type="blue">Unsaved</Tag>
|
|
1386
|
+
)}
|
|
1387
|
+
</div>
|
|
1388
|
+
<div className={styles.headerRight} >
|
|
1389
|
+
<ButtonSet>
|
|
1390
|
+
<Button
|
|
1391
|
+
kind="ghost"
|
|
1392
|
+
size="sm"
|
|
1393
|
+
renderIcon={Save}
|
|
1394
|
+
onClick={handleSave}
|
|
1395
|
+
disabled={saving || !draft.unsavedChanges}
|
|
1396
|
+
>
|
|
1397
|
+
{saving ? 'Saving...' : 'Save'}
|
|
1398
|
+
</Button>
|
|
1399
|
+
<Button
|
|
1400
|
+
kind="primary"
|
|
1401
|
+
size="sm"
|
|
1402
|
+
renderIcon={Send}
|
|
1403
|
+
onClick={handleCompile}
|
|
1404
|
+
disabled={compiling || !initialReport?.uuid || !hasPrivilege(RB.REPORT_COMPILE)}
|
|
1405
|
+
title={
|
|
1406
|
+
hasPrivilege(RB.REPORT_COMPILE)
|
|
1407
|
+
? undefined
|
|
1408
|
+
: 'Requires the report compile privilege'
|
|
1409
|
+
}
|
|
1410
|
+
>
|
|
1411
|
+
{compiling ? 'Compiling...' : 'Compile'}
|
|
1412
|
+
</Button>
|
|
1413
|
+
</ButtonSet>
|
|
1414
|
+
<OverflowMenu size="sm" renderIcon={OverflowMenuHorizontal} align="right">
|
|
1415
|
+
<OverflowMenuItem itemText="Save as" />
|
|
1416
|
+
<OverflowMenuItem itemText="Export JSON" />
|
|
1417
|
+
<OverflowMenuItem itemText="Import JSON" />
|
|
1418
|
+
<OverflowMenuItem itemText="Validate" />
|
|
1419
|
+
<OverflowMenuItem itemText="View history" />
|
|
1420
|
+
<OverflowMenuItem itemText="Retire" />
|
|
1421
|
+
</OverflowMenu>
|
|
1422
|
+
</div>
|
|
1423
|
+
</header>
|
|
1424
|
+
|
|
1425
|
+
{/* Error notifications */}
|
|
1426
|
+
{error && (
|
|
1427
|
+
<div className={styles.notification}>
|
|
1428
|
+
<InlineNotification kind="error" title="Error" subtitle={error} />
|
|
1429
|
+
</div>
|
|
1430
|
+
)}
|
|
1431
|
+
{saveError && (
|
|
1432
|
+
<div className={styles.notification}>
|
|
1433
|
+
<InlineNotification kind="error" title="Save Error" subtitle={saveError} onClose={() => setSaveError(null)} />
|
|
1434
|
+
</div>
|
|
1435
|
+
)}
|
|
1436
|
+
{compileError && (
|
|
1437
|
+
<div className={styles.notification}>
|
|
1438
|
+
<InlineNotification kind="error" title="Compile Error" subtitle={compileError} onClose={() => setCompileError(null)} />
|
|
1439
|
+
</div>
|
|
1440
|
+
)}
|
|
1441
|
+
{compileSuccess && (
|
|
1442
|
+
<div className={styles.notification}>
|
|
1443
|
+
<InlineNotification kind="success" title="Compiled" subtitle={compileSuccess} onClose={() => setCompileSuccess(null)} />
|
|
1444
|
+
</div>
|
|
1445
|
+
)}
|
|
1446
|
+
|
|
1447
|
+
{/* Compile Setup Modal */}
|
|
1448
|
+
<CompileSetupModal
|
|
1449
|
+
open={showCompileSetupModal}
|
|
1450
|
+
currentCategoryUuid={draft.categoryUuid}
|
|
1451
|
+
onConfirm={handleCompileSetupConfirm}
|
|
1452
|
+
onClose={handleCompileSetupCancel}
|
|
1453
|
+
reportType="linelist"
|
|
1454
|
+
/>
|
|
1455
|
+
|
|
1456
|
+
{/* Preview Parameter Modal */}
|
|
1457
|
+
<PreviewParameterModal
|
|
1458
|
+
open={previewParamModalOpen}
|
|
1459
|
+
onClose={() => setPreviewParamModalOpen(false)}
|
|
1460
|
+
onRun={handleRunPreviewWithParams}
|
|
1461
|
+
parameters={draft.parameters}
|
|
1462
|
+
loading={previewRunning}
|
|
1463
|
+
/>
|
|
1464
|
+
|
|
1465
|
+
{/* Three-Panel Layout */}
|
|
1466
|
+
<div className={styles.panels}>
|
|
1467
|
+
{/* Left Panel - Data Catalogue */}
|
|
1468
|
+
<aside
|
|
1469
|
+
className={styles.leftPanel}
|
|
1470
|
+
style={{
|
|
1471
|
+
width: leftPanelCollapsed ? 48 : leftPanelWidth,
|
|
1472
|
+
}}
|
|
1473
|
+
>
|
|
1474
|
+
<div className={styles.panelHeader}>
|
|
1475
|
+
{!leftPanelCollapsed && (
|
|
1476
|
+
<>
|
|
1477
|
+
<h3 className={styles.panelTitle}>Data Catalogue</h3>
|
|
1478
|
+
<Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronLeft} iconDescription="Collapse left panel" onClick={() => setLeftPanelCollapsed(true)} />
|
|
1479
|
+
</>
|
|
1480
|
+
)}
|
|
1481
|
+
{leftPanelCollapsed && (
|
|
1482
|
+
<Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronRight} iconDescription="Expand left panel" onClick={() => setLeftPanelCollapsed(false)} />
|
|
1483
|
+
)}
|
|
1484
|
+
</div>
|
|
1485
|
+
|
|
1486
|
+
{!leftPanelCollapsed && (
|
|
1487
|
+
<div className={styles.panelContent}>
|
|
1488
|
+
{/* Column Sources Section */}
|
|
1489
|
+
<div className={styles.columnSourcesSection}>
|
|
1490
|
+
{/* Population Sources Section */}
|
|
1491
|
+
<div className={styles.populationCard}>
|
|
1492
|
+
<PopulationSourceSelector
|
|
1493
|
+
populationSources={draft.populationSources}
|
|
1494
|
+
onChange={(sources) => updateDraft({ populationSources: sources })}
|
|
1495
|
+
disabled={loading}
|
|
1496
|
+
/>
|
|
1497
|
+
</div>
|
|
1498
|
+
|
|
1499
|
+
{/* Column Sources Section */}
|
|
1500
|
+
<div className={styles.columnSourcesCard}>
|
|
1501
|
+
<div className={styles.columnSourcesHeader}>
|
|
1502
|
+
<h4 className={styles.columnSourcesTitle}>
|
|
1503
|
+
<span className={styles.columnSourcesIcon}>📊</span>
|
|
1504
|
+
Column Sources
|
|
1505
|
+
</h4>
|
|
1506
|
+
<span className={styles.columnSourcesSubtitle}>
|
|
1507
|
+
Select columns for output
|
|
1508
|
+
</span>
|
|
1509
|
+
</div>
|
|
1510
|
+
<MultiDatasourceSelector
|
|
1511
|
+
dataSources={draft.dataSources}
|
|
1512
|
+
onChange={handleDataSourcesChange}
|
|
1513
|
+
disabled={loading}
|
|
1514
|
+
/>
|
|
1515
|
+
<DataCatalogue
|
|
1516
|
+
dataSources={draft.dataSources}
|
|
1517
|
+
populationSources={draft.populationSources}
|
|
1518
|
+
onPopulationSourcesChange={undefined}
|
|
1519
|
+
onAddToColumns={handleAddFieldAsColumn}
|
|
1520
|
+
onAddToFilters={handleAddFieldAsFilter}
|
|
1521
|
+
onTableChange={handleTableChange}
|
|
1522
|
+
onFieldsAvailable={handleFieldsAvailable}
|
|
1523
|
+
onEtlStructureDetected={handleEtlStructureDetected}
|
|
1524
|
+
selectedFields={selectedFieldIds}
|
|
1525
|
+
onAddCustomSqlColumn={handleAddCustomSqlColumn}
|
|
1526
|
+
onAddDraftColumn={handleAddDraftColumn}
|
|
1527
|
+
idColumnAlias="client_id"
|
|
1528
|
+
showPopulationSelector={false}
|
|
1529
|
+
/>
|
|
1530
|
+
</div>
|
|
1531
|
+
</div>
|
|
1532
|
+
</div>
|
|
1533
|
+
)}
|
|
1534
|
+
</aside>
|
|
1535
|
+
|
|
1536
|
+
{/* Resize Handle - Left/Middle */}
|
|
1537
|
+
<div
|
|
1538
|
+
className={`${styles.resizeHandle} ${styles.resizeHandleLeftMiddle}`}
|
|
1539
|
+
onMouseDown={startResizeLeft}
|
|
1540
|
+
style={{ cursor: isResizingLeft ? 'col-resize' : 'col-resize' }}
|
|
1541
|
+
/>
|
|
1542
|
+
|
|
1543
|
+
{/* Middle Panel - Query Configuration */}
|
|
1544
|
+
<aside
|
|
1545
|
+
className={styles.middlePanel}
|
|
1546
|
+
style={{
|
|
1547
|
+
width: middlePanelCollapsed ? 48 : middlePanelWidth,
|
|
1548
|
+
}}
|
|
1549
|
+
>
|
|
1550
|
+
<div className={styles.panelHeader}>
|
|
1551
|
+
{!middlePanelCollapsed && (
|
|
1552
|
+
<>
|
|
1553
|
+
<h3 className={styles.panelTitle}>Query Configuration</h3>
|
|
1554
|
+
<Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronLeft} iconDescription="Collapse middle panel" onClick={() => setMiddlePanelCollapsed(true)} />
|
|
1555
|
+
</>
|
|
1556
|
+
)}
|
|
1557
|
+
{middlePanelCollapsed && (
|
|
1558
|
+
<Button kind="ghost" size="sm" hasIconOnly renderIcon={ChevronRight} iconDescription="Expand middle panel" onClick={() => setMiddlePanelCollapsed(false)} />
|
|
1559
|
+
)}
|
|
1560
|
+
</div>
|
|
1561
|
+
|
|
1562
|
+
{!middlePanelCollapsed && (
|
|
1563
|
+
<div className={styles.panelContent}>
|
|
1564
|
+
<QueryConfigPanel
|
|
1565
|
+
draft={draft}
|
|
1566
|
+
onDraftChange={updateDraft}
|
|
1567
|
+
availableFields={availableFields}
|
|
1568
|
+
indicators={indicators}
|
|
1569
|
+
onRemoveColumn={handleRemoveColumn}
|
|
1570
|
+
/>
|
|
1571
|
+
</div>
|
|
1572
|
+
)}
|
|
1573
|
+
</aside>
|
|
1574
|
+
|
|
1575
|
+
{/* Resize Handle - Middle/Right */}
|
|
1576
|
+
<div
|
|
1577
|
+
className={`${styles.resizeHandle} ${styles.resizeHandleMiddleRight}`}
|
|
1578
|
+
onMouseDown={startResizeMiddle}
|
|
1579
|
+
style={{ cursor: isResizingMiddle ? 'col-resize' : 'col-resize' }}
|
|
1580
|
+
/>
|
|
1581
|
+
|
|
1582
|
+
{/* Right Panel - Preview */}
|
|
1583
|
+
<main className={styles.rightPanel}>
|
|
1584
|
+
<div className={styles.panelHeader}>
|
|
1585
|
+
<h3 className={styles.panelTitle}>Preview</h3>
|
|
1586
|
+
<ButtonSet>
|
|
1587
|
+
<Button
|
|
1588
|
+
kind="primary"
|
|
1589
|
+
size="sm"
|
|
1590
|
+
onClick={handleRunPreview}
|
|
1591
|
+
disabled={
|
|
1592
|
+
previewRunning ||
|
|
1593
|
+
!initialReport?.compiledReportDefinitionUuid ||
|
|
1594
|
+
!hasPrivilege(RB.SQL_EXECUTE)
|
|
1595
|
+
}
|
|
1596
|
+
tooltipAlignment="end"
|
|
1597
|
+
tooltipPosition="bottom"
|
|
1598
|
+
>
|
|
1599
|
+
{previewRunning ? 'Loading...' : 'Run Preview'}
|
|
1600
|
+
</Button>
|
|
1601
|
+
</ButtonSet>
|
|
1602
|
+
</div>
|
|
1603
|
+
|
|
1604
|
+
<div className={styles.panelContent}>
|
|
1605
|
+
{!initialReport?.compiledReportDefinitionUuid && mode === 'edit' && (
|
|
1606
|
+
<InlineNotification
|
|
1607
|
+
kind="info"
|
|
1608
|
+
title="Report Not Compiled"
|
|
1609
|
+
subtitle="Please compile the report first before running preview. Click the 'Compile' button above to generate the report definition."
|
|
1610
|
+
lowContrast
|
|
1611
|
+
/>
|
|
1612
|
+
)}
|
|
1613
|
+
|
|
1614
|
+
{previewError && (
|
|
1615
|
+
<InlineNotification kind="error" title="Preview Error" subtitle={previewError} />
|
|
1616
|
+
)}
|
|
1617
|
+
|
|
1618
|
+
{!previewData && !previewError && initialReport?.compiledReportDefinitionUuid && (
|
|
1619
|
+
<div className={styles.emptyPreview}>
|
|
1620
|
+
<Document size={32} />
|
|
1621
|
+
<p>Run a preview to see sample data</p>
|
|
1622
|
+
</div>
|
|
1623
|
+
)}
|
|
1624
|
+
|
|
1625
|
+
{previewData && previewData.rowCount > 0 && (
|
|
1626
|
+
<div className={styles.previewResults}>
|
|
1627
|
+
<div className={styles.previewStatus}>
|
|
1628
|
+
<span>Preview rows: {previewData.rowCount}</span>
|
|
1629
|
+
<span>Query duration: —</span>
|
|
1630
|
+
</div>
|
|
1631
|
+
{previewData.html ? (
|
|
1632
|
+
<div
|
|
1633
|
+
className={styles.htmlPreview}
|
|
1634
|
+
dangerouslySetInnerHTML={{ __html: previewData.html }}
|
|
1635
|
+
/>
|
|
1636
|
+
) : (
|
|
1637
|
+
<TableContainer>
|
|
1638
|
+
<Table>
|
|
1639
|
+
<TableHead>
|
|
1640
|
+
<TableRow>
|
|
1641
|
+
{previewData.columns.map((column) => (
|
|
1642
|
+
<TableHeader key={column}>{column}</TableHeader>
|
|
1643
|
+
))}
|
|
1644
|
+
</TableRow>
|
|
1645
|
+
</TableHead>
|
|
1646
|
+
<TableBody>
|
|
1647
|
+
{previewData.rows.map((row, index) => (
|
|
1648
|
+
<TableRow key={index}>
|
|
1649
|
+
{previewData.columns.map((column) => (
|
|
1650
|
+
<TableCell key={`${index}-${column}`}>
|
|
1651
|
+
{row[column]?.toString() ?? ''}
|
|
1652
|
+
</TableCell>
|
|
1653
|
+
))}
|
|
1654
|
+
</TableRow>
|
|
1655
|
+
))}
|
|
1656
|
+
</TableBody>
|
|
1657
|
+
</Table>
|
|
1658
|
+
</TableContainer>
|
|
1659
|
+
)}
|
|
1660
|
+
</div>
|
|
1661
|
+
)}
|
|
1662
|
+
|
|
1663
|
+
{previewData && previewData.rowCount === 0 && (
|
|
1664
|
+
<div className={styles.emptyPreview}>
|
|
1665
|
+
<Document size={32} />
|
|
1666
|
+
<p>No data found for the current configuration</p>
|
|
1667
|
+
</div>
|
|
1668
|
+
)}
|
|
1669
|
+
</div>
|
|
1670
|
+
</main>
|
|
1671
|
+
</div>
|
|
1672
|
+
</div>
|
|
1673
|
+
);
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
export default LinelistBuilderWorkspace;
|