@epcare/esm-report-builder 0.1.1-pre.13
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/LICENSE +401 -0
- package/README.md +317 -0
- package/dist/184.js +2 -0
- package/dist/184.js.LICENSE.txt +14 -0
- package/dist/184.js.map +1 -0
- package/dist/211.js +1 -0
- package/dist/211.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/44.js +2 -0
- package/dist/44.js.LICENSE.txt +30 -0
- package/dist/44.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/535.js +1 -0
- package/dist/535.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/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/872.js +1 -0
- package/dist/872.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/972.js +2 -0
- package/dist/972.js.LICENSE.txt +21 -0
- package/dist/972.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 +482 -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/tsconfig.json +11 -0
- package/example.env +5 -0
- package/jest.config.js +28 -0
- package/package.json +102 -0
- package/playwright.config.ts +28 -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 +31 -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-sources-page.component.tsx +344 -0
- package/src/components/admin/report-categories-page.component.tsx +256 -0
- package/src/components/admin/report-library-page.component.tsx +540 -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 +120 -0
- package/src/components/app-shell/report-builder-shell.scss +15 -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 +132 -0
- package/src/components/data-themes/data-themes-table.component.tsx +91 -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/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 +501 -0
- package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +297 -0
- package/src/components/indicators/create-final-indicator-modal.component.tsx +260 -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-status-tag.component.tsx +9 -0
- package/src/components/indicators/indicators-page.component.tsx +481 -0
- package/src/components/indicators/indicators-page.scss +121 -0
- package/src/components/indicators/indicators-table.component.tsx +166 -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 +161 -0
- package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -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 +92 -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 +194 -0
- package/src/components/indicators/sections/indicator-sql-preview.section.tsx +26 -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/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 +21 -0
- package/src/components/indicators/types/condition-ui.types.ts +11 -0
- package/src/components/indicators/types/data-theme-config.types.ts +88 -0
- package/src/components/indicators/types/indicator-types.ts +56 -0
- package/src/components/indicators/types/sql-builders.ts +43 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.ts +206 -0
- package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
- package/src/components/indicators/utils/final-indicator-sql.utils.ts +124 -0
- package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
- package/src/components/indicators/utils/indicator-sql.utils.ts +185 -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 +103 -0
- package/src/components/landing/report-builder-landing-page.scss +131 -0
- package/src/components/report/dashboard/reports-table.component.tsx +145 -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 +894 -0
- package/src/components/report/design/report-design.types.ts +43 -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 +75 -0
- package/src/components/report/report-editor.page.component.tsx +466 -0
- package/src/components/report/report.routes.tsx +30 -0
- package/src/components/report-sections/create-report-section-modal.component.tsx +278 -0
- package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
- package/src/components/report-sections/hooks/useSectionEditorState.ts +306 -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 +143 -0
- package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
- package/src/components/report-sections/report-section-preview-modal.component.tsx +418 -0
- package/src/components/report-sections/report-sections-page.component.tsx +313 -0
- package/src/components/report-sections/section-types.ts +55 -0
- package/src/components/report-sections/section-utils.ts +46 -0
- package/src/components/report-sections/sections-page.scss +79 -0
- package/src/components/run-reports/run-reports-page.component.tsx +23 -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 +23 -0
- package/src/globals.d.ts +14 -0
- package/src/hooks/theme/useDataThemes.ts +28 -0
- package/src/hooks/theme/useMambaTableMeta.ts +34 -0
- package/src/hooks/theme/useMambaTables.ts +63 -0
- package/src/index.ts +23 -0
- package/src/resources/agegroup/agegroups.api.ts +58 -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/etl-source/etl-source.api.ts +65 -0
- package/src/resources/indicator/indicators.api.ts +125 -0
- package/src/resources/openmrs-api.ts +24 -0
- package/src/resources/preview/sql-preview.api.ts +54 -0
- package/src/resources/report/reports.api.ts +117 -0
- package/src/resources/report-category/report-category.api.ts +41 -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 +21 -0
- package/src/resources/theme/data-theme.api.ts +88 -0
- package/src/resources/theme/mamba-schema.api.ts +31 -0
- package/src/resources/theme/mamba-table-meta.api.ts +45 -0
- package/src/root.component.tsx +20 -0
- package/src/routes/report-builder.routes.tsx +66 -0
- package/src/routes/report-builder.scss +56 -0
- package/src/routes.json +21 -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 +49 -0
- package/src/types/theme/data-theme.types.ts +88 -0
- package/src/utils/html-entities.utils.ts +23 -0
- package/tools/setup-tests.ts +1 -0
- package/translations/en.json +4 -0
- package/tsconfig.json +43 -0
- package/webpack.config.js +14 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Stack, InlineNotification } from '@carbon/react';
|
|
3
|
+
|
|
4
|
+
import { getIndicator, type IndicatorDto } from '../../resources/indicator/indicators.api';
|
|
5
|
+
|
|
6
|
+
import type { BaseIndicatorOption, CompositeOperator } from './types/composite-indicator.types';
|
|
7
|
+
|
|
8
|
+
import CompositeIndicatorBasicsSection from './sections/composite-indicator-basics.section';
|
|
9
|
+
import CompositeIndicatorPickerSection from './sections/composite-indicator-picker.section';
|
|
10
|
+
import CompositeIndicatorSqlPreviewSection from './sections/composite-indicator-sql-preview.section';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
countSqlToPopulationSql,
|
|
14
|
+
buildCompositeCountSql,
|
|
15
|
+
tryGetPatientIdColumnFromConfig,
|
|
16
|
+
tryGetCountSqlFromIndicator,
|
|
17
|
+
} from './utils/composite-indicator-sql.utils';
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
open: boolean;
|
|
21
|
+
mode?: 'create' | 'edit';
|
|
22
|
+
initial?: IndicatorDto | null;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onCreate: (payload: Partial<IndicatorDto>) => Promise<void>;
|
|
25
|
+
onUpdate?: (uuid: string, payload: Partial<IndicatorDto>) => Promise<void>;
|
|
26
|
+
onSaved: () => void;
|
|
27
|
+
baseIndicators: BaseIndicatorOption[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CompositeIndicatorAuthoring = {
|
|
31
|
+
version: 1;
|
|
32
|
+
unit: 'Patients' | 'Encounters';
|
|
33
|
+
operator: CompositeOperator;
|
|
34
|
+
indicatorAId: string;
|
|
35
|
+
indicatorBId: string;
|
|
36
|
+
indicatorACode?: string;
|
|
37
|
+
indicatorBCode?: string;
|
|
38
|
+
sqlPreview: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function safeParseJson<T = any>(input?: string | null): T | null {
|
|
42
|
+
if (!input) return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(input) as T;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const toCode = (name: string) =>
|
|
51
|
+
name
|
|
52
|
+
.toUpperCase()
|
|
53
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
54
|
+
.replace(/^_+|_+$/g, '')
|
|
55
|
+
.slice(0, 50);
|
|
56
|
+
|
|
57
|
+
const CreateCompositeBaseIndicatorModal: React.FC<Props> = ({
|
|
58
|
+
open,
|
|
59
|
+
mode = 'create',
|
|
60
|
+
initial,
|
|
61
|
+
onClose,
|
|
62
|
+
onCreate,
|
|
63
|
+
onUpdate,
|
|
64
|
+
onSaved,
|
|
65
|
+
baseIndicators,
|
|
66
|
+
}) => {
|
|
67
|
+
// ✅ IMPORTANT: no defaults for create mode
|
|
68
|
+
const [name, setName] = React.useState('');
|
|
69
|
+
const [code, setCode] = React.useState('');
|
|
70
|
+
const [description, setDescription] = React.useState('');
|
|
71
|
+
|
|
72
|
+
const [indicatorAId, setIndicatorAId] = React.useState('');
|
|
73
|
+
const [indicatorBId, setIndicatorBId] = React.useState('');
|
|
74
|
+
const [operator, setOperator] = React.useState<CompositeOperator>('AND');
|
|
75
|
+
|
|
76
|
+
const [loadingA, setLoadingA] = React.useState(false);
|
|
77
|
+
const [loadingB, setLoadingB] = React.useState(false);
|
|
78
|
+
const [errA, setErrA] = React.useState<string | null>(null);
|
|
79
|
+
const [errB, setErrB] = React.useState<string | null>(null);
|
|
80
|
+
|
|
81
|
+
const [indA, setIndA] = React.useState<IndicatorDto | null>(null);
|
|
82
|
+
const [indB, setIndB] = React.useState<IndicatorDto | null>(null);
|
|
83
|
+
|
|
84
|
+
// reset / initialize on open
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (!open) return;
|
|
87
|
+
|
|
88
|
+
// EDIT: hydrate from selected composite indicator
|
|
89
|
+
if (mode === 'edit' && initial?.uuid) {
|
|
90
|
+
const cfg = safeParseJson<any>(initial.configJson) ?? {};
|
|
91
|
+
|
|
92
|
+
setName(initial.name ?? '');
|
|
93
|
+
setCode(initial.code ?? '');
|
|
94
|
+
setDescription(initial.description ?? '');
|
|
95
|
+
|
|
96
|
+
// support both indicatorAId/indicatorBId and indicatorAUuid/indicatorBUuid
|
|
97
|
+
setIndicatorAId(String(cfg.indicatorAId ?? cfg.indicatorAUuid ?? ''));
|
|
98
|
+
setIndicatorBId(String(cfg.indicatorBId ?? cfg.indicatorBUuid ?? ''));
|
|
99
|
+
setOperator((String(cfg.operator ?? 'AND').toUpperCase() as CompositeOperator) ?? 'AND');
|
|
100
|
+
|
|
101
|
+
setErrA(null);
|
|
102
|
+
setErrB(null);
|
|
103
|
+
setIndA(null);
|
|
104
|
+
setIndB(null);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// CREATE: start blank so users don't confuse it with edit
|
|
109
|
+
setName('');
|
|
110
|
+
setCode('');
|
|
111
|
+
setDescription('');
|
|
112
|
+
setIndicatorAId('');
|
|
113
|
+
setIndicatorBId('');
|
|
114
|
+
setOperator('AND');
|
|
115
|
+
setErrA(null);
|
|
116
|
+
setErrB(null);
|
|
117
|
+
setIndA(null);
|
|
118
|
+
setIndB(null);
|
|
119
|
+
}, [open, mode, initial?.uuid]);
|
|
120
|
+
|
|
121
|
+
const AOpt = React.useMemo(() => baseIndicators.find((x) => x.id === indicatorAId) ?? null, [baseIndicators, indicatorAId]);
|
|
122
|
+
const BOpt = React.useMemo(() => baseIndicators.find((x) => x.id === indicatorBId) ?? null, [baseIndicators, indicatorBId]);
|
|
123
|
+
|
|
124
|
+
const samePick = Boolean(indicatorAId && indicatorBId && indicatorAId === indicatorBId);
|
|
125
|
+
|
|
126
|
+
const inferredUnit: 'Patients' | 'Encounters' = React.useMemo(() => 'Patients', []);
|
|
127
|
+
|
|
128
|
+
// load full indicators for A/B
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
if (!open) return;
|
|
131
|
+
if (!indicatorAId) {
|
|
132
|
+
setIndA(null);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ac = new AbortController();
|
|
137
|
+
setLoadingA(true);
|
|
138
|
+
setErrA(null);
|
|
139
|
+
|
|
140
|
+
getIndicator(indicatorAId, ac.signal, 'full')
|
|
141
|
+
.then((full) => setIndA(full))
|
|
142
|
+
.catch((e: any) => setErrA(e?.message ?? 'Failed to load indicator A'))
|
|
143
|
+
.finally(() => setLoadingA(false));
|
|
144
|
+
|
|
145
|
+
return () => ac.abort();
|
|
146
|
+
}, [open, indicatorAId]);
|
|
147
|
+
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (!open) return;
|
|
150
|
+
if (!indicatorBId) {
|
|
151
|
+
setIndB(null);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ac = new AbortController();
|
|
156
|
+
setLoadingB(true);
|
|
157
|
+
setErrB(null);
|
|
158
|
+
|
|
159
|
+
getIndicator(indicatorBId, ac.signal, 'full')
|
|
160
|
+
.then((full) => setIndB(full))
|
|
161
|
+
.catch((e: any) => setErrB(e?.message ?? 'Failed to load indicator B'))
|
|
162
|
+
.finally(() => setLoadingB(false));
|
|
163
|
+
|
|
164
|
+
return () => ac.abort();
|
|
165
|
+
}, [open, indicatorBId]);
|
|
166
|
+
|
|
167
|
+
const populationSqlA = React.useMemo(() => {
|
|
168
|
+
if (!indA) return '';
|
|
169
|
+
const { sql: countSql } = tryGetCountSqlFromIndicator(indA);
|
|
170
|
+
const pidCol = tryGetPatientIdColumnFromConfig(indA);
|
|
171
|
+
if (!countSql) return '';
|
|
172
|
+
return countSqlToPopulationSql(countSql, pidCol, inferredUnit);
|
|
173
|
+
}, [indA, inferredUnit]);
|
|
174
|
+
|
|
175
|
+
const populationSqlB = React.useMemo(() => {
|
|
176
|
+
if (!indB) return '';
|
|
177
|
+
const { sql: countSql } = tryGetCountSqlFromIndicator(indB);
|
|
178
|
+
const pidCol = tryGetPatientIdColumnFromConfig(indB);
|
|
179
|
+
if (!countSql) return '';
|
|
180
|
+
return countSqlToPopulationSql(countSql, pidCol, inferredUnit);
|
|
181
|
+
}, [indB, inferredUnit]);
|
|
182
|
+
|
|
183
|
+
const compositeSql = React.useMemo(() => {
|
|
184
|
+
if (!populationSqlA || !populationSqlB) return '';
|
|
185
|
+
return buildCompositeCountSql({
|
|
186
|
+
unit: inferredUnit,
|
|
187
|
+
operator,
|
|
188
|
+
populationSqlA,
|
|
189
|
+
populationSqlB,
|
|
190
|
+
});
|
|
191
|
+
}, [populationSqlA, populationSqlB, inferredUnit, operator]);
|
|
192
|
+
|
|
193
|
+
const canSubmit =
|
|
194
|
+
Boolean(name.trim()) &&
|
|
195
|
+
Boolean(indicatorAId) &&
|
|
196
|
+
Boolean(indicatorBId) &&
|
|
197
|
+
!samePick &&
|
|
198
|
+
Boolean(compositeSql.trim());
|
|
199
|
+
|
|
200
|
+
const submit = async () => {
|
|
201
|
+
if (!canSubmit) return;
|
|
202
|
+
|
|
203
|
+
const finalCode = code.trim() ? code.trim().toUpperCase() : toCode(name);
|
|
204
|
+
|
|
205
|
+
const authoring: CompositeIndicatorAuthoring = {
|
|
206
|
+
version: 1,
|
|
207
|
+
unit: inferredUnit,
|
|
208
|
+
operator,
|
|
209
|
+
indicatorAId,
|
|
210
|
+
indicatorBId,
|
|
211
|
+
indicatorACode: AOpt?.code,
|
|
212
|
+
indicatorBCode: BOpt?.code,
|
|
213
|
+
sqlPreview: compositeSql,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const payload: Partial<IndicatorDto> = {
|
|
217
|
+
name: name.trim(),
|
|
218
|
+
code: finalCode,
|
|
219
|
+
description: description.trim() || undefined,
|
|
220
|
+
kind: 'COMPOSITE',
|
|
221
|
+
defaultValueType: 'NUMBER',
|
|
222
|
+
themeUuid: null,
|
|
223
|
+
configJson: JSON.stringify(authoring, null, 2),
|
|
224
|
+
sqlTemplate: compositeSql,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (mode === 'edit' && initial?.uuid) {
|
|
228
|
+
if (!onUpdate) throw new Error('onUpdate handler is required for edit mode');
|
|
229
|
+
await onUpdate(initial.uuid, payload);
|
|
230
|
+
} else {
|
|
231
|
+
await onCreate(payload);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onSaved();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Modal
|
|
239
|
+
open={open}
|
|
240
|
+
onRequestClose={onClose}
|
|
241
|
+
modalHeading={mode === 'edit' ? 'Edit Composite Indicator' : 'Create Composite Indicator'}
|
|
242
|
+
primaryButtonText={mode === 'edit' ? 'Update Indicator' : 'Save Indicator'}
|
|
243
|
+
secondaryButtonText="Cancel"
|
|
244
|
+
onRequestSubmit={submit}
|
|
245
|
+
primaryButtonDisabled={!canSubmit}
|
|
246
|
+
size="lg"
|
|
247
|
+
>
|
|
248
|
+
<Stack gap={5}>
|
|
249
|
+
<div style={{ opacity: 0.8 }}>
|
|
250
|
+
Select two base indicators, pick an operator, and we generate a composite SQL preview.
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{samePick ? (
|
|
254
|
+
<InlineNotification
|
|
255
|
+
kind="error"
|
|
256
|
+
lowContrast
|
|
257
|
+
title="Pick two different indicators"
|
|
258
|
+
subtitle="Indicator A and Indicator B cannot be the same."
|
|
259
|
+
/>
|
|
260
|
+
) : null}
|
|
261
|
+
|
|
262
|
+
<CompositeIndicatorBasicsSection
|
|
263
|
+
value={{ name, code, description }}
|
|
264
|
+
onChange={(next) => {
|
|
265
|
+
setName(next.name);
|
|
266
|
+
setCode(next.code);
|
|
267
|
+
setDescription(next.description);
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
|
|
271
|
+
<hr style={{ border: 0, borderTop: '1px solid var(--cds-border-subtle, #e0e0e0)' }} />
|
|
272
|
+
|
|
273
|
+
<CompositeIndicatorPickerSection
|
|
274
|
+
baseIndicators={baseIndicators}
|
|
275
|
+
indicatorAId={indicatorAId}
|
|
276
|
+
indicatorBId={indicatorBId}
|
|
277
|
+
operator={operator}
|
|
278
|
+
inferredUnit={inferredUnit}
|
|
279
|
+
samePick={samePick}
|
|
280
|
+
onChangeA={setIndicatorAId}
|
|
281
|
+
onChangeB={setIndicatorBId}
|
|
282
|
+
onChangeOperator={setOperator}
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<CompositeIndicatorSqlPreviewSection
|
|
286
|
+
loading={loadingA || loadingB}
|
|
287
|
+
errA={errA}
|
|
288
|
+
errB={errB}
|
|
289
|
+
compositeSql={compositeSql}
|
|
290
|
+
show={Boolean(AOpt && BOpt)}
|
|
291
|
+
/>
|
|
292
|
+
</Stack>
|
|
293
|
+
</Modal>
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export default CreateCompositeBaseIndicatorModal;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Stack, InlineLoading, InlineNotification } from '@carbon/react';
|
|
3
|
+
|
|
4
|
+
import type { IndicatorDto } from '../../resources/indicator/indicators.api';
|
|
5
|
+
import { getIndicator } from '../../resources/indicator/indicators.api';
|
|
6
|
+
|
|
7
|
+
import type { BaseIndicatorOption } from './types/composite-indicator.types';
|
|
8
|
+
import { listAgeCategoriesWithGroups, type AgeCategoryOption } from '../../resources/agegroup/agegroups.api';
|
|
9
|
+
|
|
10
|
+
import FinalIndicatorBasicsSection from './sections/final-indicator-basics.section';
|
|
11
|
+
import FinalIndicatorPickerSection from './sections/final-indicator-picker.section';
|
|
12
|
+
import FinalIndicatorDisaggregationSection from './sections/final-indicator-disaggregation.section';
|
|
13
|
+
import FinalIndicatorResultsPreviewSection from './sections/final-indicator-results-preview.section';
|
|
14
|
+
|
|
15
|
+
import { buildFinalIndicatorSql, type FinalIndicatorAuthoringV1 } from './utils/final-indicator-sql.utils';
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
open: boolean;
|
|
19
|
+
mode?: 'create' | 'edit';
|
|
20
|
+
initial?: IndicatorDto | null;
|
|
21
|
+
|
|
22
|
+
baseIndicators: BaseIndicatorOption[];
|
|
23
|
+
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
|
|
26
|
+
onCreate: (payload: Partial<IndicatorDto>) => Promise<void>;
|
|
27
|
+
onUpdate?: (uuid: string, payload: Partial<IndicatorDto>) => Promise<void>;
|
|
28
|
+
onSaved: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function safeParseJson<T = any>(input?: string | null): T | null {
|
|
32
|
+
if (!input) return null;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(input) as T;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const toCode = (name: string) =>
|
|
41
|
+
name
|
|
42
|
+
.toUpperCase()
|
|
43
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
44
|
+
.replace(/^_+|_+$/g, '')
|
|
45
|
+
.slice(0, 50);
|
|
46
|
+
|
|
47
|
+
function todayIso(): string {
|
|
48
|
+
const d = new Date();
|
|
49
|
+
const yyyy = d.getFullYear();
|
|
50
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
51
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
52
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function CreateFinalIndicatorModal({
|
|
56
|
+
open,
|
|
57
|
+
mode = 'create',
|
|
58
|
+
initial,
|
|
59
|
+
baseIndicators,
|
|
60
|
+
onClose,
|
|
61
|
+
onCreate,
|
|
62
|
+
onUpdate,
|
|
63
|
+
onSaved,
|
|
64
|
+
}: Props) {
|
|
65
|
+
const [ageCategories, setAgeCategories] = React.useState<AgeCategoryOption[]>([]);
|
|
66
|
+
const [loadingCats, setLoadingCats] = React.useState(false);
|
|
67
|
+
const [catsError, setCatsError] = React.useState<string | null>(null);
|
|
68
|
+
|
|
69
|
+
const [basics, setBasics] = React.useState({ name: '', code: '', description: '' });
|
|
70
|
+
|
|
71
|
+
const [baseIndicatorId, setBaseIndicatorId] = React.useState('');
|
|
72
|
+
const [ageCategoryCode, setAgeCategoryCode] = React.useState('');
|
|
73
|
+
const [genders, setGenders] = React.useState<Array<'F' | 'M'>>(['F', 'M']);
|
|
74
|
+
|
|
75
|
+
const [baseFull, setBaseFull] = React.useState<IndicatorDto | null>(null);
|
|
76
|
+
const [loadingBase, setLoadingBase] = React.useState(false);
|
|
77
|
+
const [baseError, setBaseError] = React.useState<string | null>(null);
|
|
78
|
+
|
|
79
|
+
const [sqlPreview, setSqlPreview] = React.useState('');
|
|
80
|
+
|
|
81
|
+
// ✅ Preview parameters (must be inside component)
|
|
82
|
+
const [previewStartDate, setPreviewStartDate] = React.useState('2025-01-01');
|
|
83
|
+
const [previewEndDate, setPreviewEndDate] = React.useState(todayIso);
|
|
84
|
+
|
|
85
|
+
// load categories when modal opens
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
if (!open) return;
|
|
88
|
+
const ac = new AbortController();
|
|
89
|
+
|
|
90
|
+
setLoadingCats(true);
|
|
91
|
+
setCatsError(null);
|
|
92
|
+
|
|
93
|
+
listAgeCategoriesWithGroups(ac.signal)
|
|
94
|
+
.then((items) => setAgeCategories(items))
|
|
95
|
+
.catch((e: any) => setCatsError(e?.message ?? 'Failed to load age categories'))
|
|
96
|
+
.finally(() => setLoadingCats(false));
|
|
97
|
+
|
|
98
|
+
return () => ac.abort();
|
|
99
|
+
}, [open]);
|
|
100
|
+
|
|
101
|
+
// init create vs edit
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (!open) return;
|
|
104
|
+
|
|
105
|
+
if (mode === 'edit' && initial?.uuid) {
|
|
106
|
+
const cfg = safeParseJson<Partial<FinalIndicatorAuthoringV1>>(initial.configJson) ?? {};
|
|
107
|
+
|
|
108
|
+
setBasics({
|
|
109
|
+
name: initial.name ?? '',
|
|
110
|
+
code: initial.code ?? '',
|
|
111
|
+
description: initial.description ?? '',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
setBaseIndicatorId(String(cfg.baseIndicatorId ?? ''));
|
|
115
|
+
setAgeCategoryCode(String(cfg.ageGroupSetCode ?? cfg.ageCategoryCode ?? '')); // tolerate both keys
|
|
116
|
+
setGenders((cfg.genders as any) ?? ['F', 'M']);
|
|
117
|
+
|
|
118
|
+
setSqlPreview(String(cfg.sqlPreview ?? initial.sqlTemplate ?? ''));
|
|
119
|
+
|
|
120
|
+
// keep preview dates as-is (user controlled)
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// create defaults
|
|
125
|
+
setBasics({ name: '', code: '', description: '' });
|
|
126
|
+
setBaseIndicatorId('');
|
|
127
|
+
setAgeCategoryCode('');
|
|
128
|
+
setGenders(['F', 'M']);
|
|
129
|
+
setSqlPreview('');
|
|
130
|
+
setBaseFull(null);
|
|
131
|
+
setBaseError(null);
|
|
132
|
+
// preview dates remain (or you can reset here if desired)
|
|
133
|
+
}, [open, mode, initial?.uuid]);
|
|
134
|
+
|
|
135
|
+
// load full base indicator
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
if (!open) return;
|
|
138
|
+
if (!baseIndicatorId) {
|
|
139
|
+
setBaseFull(null);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ac = new AbortController();
|
|
144
|
+
setLoadingBase(true);
|
|
145
|
+
setBaseError(null);
|
|
146
|
+
|
|
147
|
+
getIndicator(baseIndicatorId, ac.signal, 'full')
|
|
148
|
+
.then((full) => setBaseFull(full))
|
|
149
|
+
.catch((e: any) => setBaseError(e?.message ?? 'Failed to load selected base indicator'))
|
|
150
|
+
.finally(() => setLoadingBase(false));
|
|
151
|
+
|
|
152
|
+
return () => ac.abort();
|
|
153
|
+
}, [open, baseIndicatorId]);
|
|
154
|
+
|
|
155
|
+
// compute sql preview
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
if (!open) return;
|
|
158
|
+
|
|
159
|
+
if (!baseFull || !ageCategoryCode) {
|
|
160
|
+
setSqlPreview('');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sql = buildFinalIndicatorSql({
|
|
165
|
+
baseIndicator: baseFull,
|
|
166
|
+
ageCategoryCode,
|
|
167
|
+
genders,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
setSqlPreview(sql);
|
|
171
|
+
}, [open, baseFull, ageCategoryCode, genders]);
|
|
172
|
+
|
|
173
|
+
const canSubmit =
|
|
174
|
+
Boolean(basics.name.trim()) &&
|
|
175
|
+
Boolean(baseIndicatorId) &&
|
|
176
|
+
Boolean(ageCategoryCode) &&
|
|
177
|
+
Boolean(sqlPreview.trim());
|
|
178
|
+
|
|
179
|
+
const submit = async () => {
|
|
180
|
+
if (!canSubmit || !baseFull) return;
|
|
181
|
+
|
|
182
|
+
const finalCode = basics.code.trim() ? basics.code.trim().toUpperCase() : toCode(basics.name);
|
|
183
|
+
|
|
184
|
+
// store both keys for forward compatibility (you can later remove ageGroupSetCode)
|
|
185
|
+
const authoring: any = {
|
|
186
|
+
version: 1,
|
|
187
|
+
baseIndicatorId,
|
|
188
|
+
ageCategoryCode,
|
|
189
|
+
ageGroupSetCode: ageCategoryCode,
|
|
190
|
+
genders,
|
|
191
|
+
sqlPreview,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const payload: Partial<IndicatorDto> = {
|
|
195
|
+
name: basics.name.trim(),
|
|
196
|
+
code: finalCode,
|
|
197
|
+
description: basics.description.trim() || undefined,
|
|
198
|
+
kind: 'FINAL',
|
|
199
|
+
defaultValueType: 'NUMBER',
|
|
200
|
+
themeUuid: null,
|
|
201
|
+
configJson: JSON.stringify(authoring, null, 2),
|
|
202
|
+
sqlTemplate: sqlPreview,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (mode === 'edit' && initial?.uuid) {
|
|
206
|
+
if (!onUpdate) throw new Error('onUpdate handler is required for edit mode');
|
|
207
|
+
await onUpdate(initial.uuid, payload);
|
|
208
|
+
} else {
|
|
209
|
+
await onCreate(payload);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
onSaved();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<Modal
|
|
218
|
+
open={open}
|
|
219
|
+
onRequestClose={onClose}
|
|
220
|
+
modalHeading={mode === 'edit' ? 'Edit Final Indicator' : 'Create Final Indicator'}
|
|
221
|
+
primaryButtonText={mode === 'edit' ? 'Update Indicator' : 'Save Indicator'}
|
|
222
|
+
secondaryButtonText="Cancel"
|
|
223
|
+
onRequestSubmit={submit}
|
|
224
|
+
primaryButtonDisabled={!canSubmit}
|
|
225
|
+
size="lg"
|
|
226
|
+
>
|
|
227
|
+
<Stack gap={6}>
|
|
228
|
+
{loadingCats ? <InlineLoading description="Loading age categories…" /> : null}
|
|
229
|
+
{catsError ? <InlineNotification kind="error" lowContrast title="Age categories" subtitle={catsError} /> : null}
|
|
230
|
+
|
|
231
|
+
<FinalIndicatorBasicsSection value={basics} onChange={setBasics} />
|
|
232
|
+
|
|
233
|
+
<hr style={{ border: 0, borderTop: '1px solid var(--cds-border-subtle, #e0e0e0)' }} />
|
|
234
|
+
|
|
235
|
+
<FinalIndicatorPickerSection
|
|
236
|
+
baseIndicators={baseIndicators}
|
|
237
|
+
ageCategories={ageCategories}
|
|
238
|
+
selectedBaseId={baseIndicatorId}
|
|
239
|
+
selectedAgeCategoryCode={ageCategoryCode}
|
|
240
|
+
onChangeBaseId={setBaseIndicatorId}
|
|
241
|
+
onChangeAgeCategoryCode={setAgeCategoryCode}
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
{loadingBase ? <InlineLoading description="Loading base indicator details…" /> : null}
|
|
245
|
+
{baseError ? <InlineNotification kind="error" lowContrast title="Base indicator" subtitle={baseError} /> : null}
|
|
246
|
+
|
|
247
|
+
<FinalIndicatorDisaggregationSection genders={genders} onChange={setGenders} />
|
|
248
|
+
|
|
249
|
+
<FinalIndicatorResultsPreviewSection
|
|
250
|
+
sql={sqlPreview}
|
|
251
|
+
maxRows={200}
|
|
252
|
+
startDate={previewStartDate}
|
|
253
|
+
endDate={previewEndDate}
|
|
254
|
+
onChangeStartDate={setPreviewStartDate}
|
|
255
|
+
onChangeEndDate={setPreviewEndDate}
|
|
256
|
+
/>
|
|
257
|
+
</Stack>
|
|
258
|
+
</Modal>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Select, SelectItem, TextInput } from '@carbon/react';
|
|
4
|
+
|
|
5
|
+
import ConceptSearchMultiSelect, { SelectedConcept } from './concept-search-multiselect.component';
|
|
6
|
+
import type { DiagnosisBaseConfig } from './types/indicator-types';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
value: DiagnosisBaseConfig;
|
|
10
|
+
onChange: (next: DiagnosisBaseConfig) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function DiagnosisFiltersForm({ value, onChange }: Props) {
|
|
14
|
+
const onChangeConcepts = (selected: SelectedConcept[]) => {
|
|
15
|
+
// Use a for-loop to avoid any surprises and to keep behavior explicit.
|
|
16
|
+
const conceptIds: number[] = [];
|
|
17
|
+
const conceptUuids: string[] = [];
|
|
18
|
+
const conceptLabels: string[] = [];
|
|
19
|
+
const icd10Codes: string[] = [];
|
|
20
|
+
const icd11Codes: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const x of selected ?? []) {
|
|
23
|
+
// IDs (warehouse join)
|
|
24
|
+
const idNum = Number((x as any)?.id);
|
|
25
|
+
if (Number.isFinite(idNum) && idNum > 0) {
|
|
26
|
+
conceptIds.push(idNum);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Labels / uuids
|
|
30
|
+
if (x?.uuid) conceptUuids.push(x.uuid);
|
|
31
|
+
if (x?.display) conceptLabels.push(x.display);
|
|
32
|
+
|
|
33
|
+
// Optional metadata
|
|
34
|
+
if (x?.icd10Code) icd10Codes.push(x.icd10Code);
|
|
35
|
+
if (x?.icd11Code) icd11Codes.push(x.icd11Code);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onChange({
|
|
39
|
+
...value,
|
|
40
|
+
// ✅ self-contained selection (survives search reset)
|
|
41
|
+
selectedConcepts: selected ?? [],
|
|
42
|
+
|
|
43
|
+
// ✅ derived fields used by SQL builder + UI
|
|
44
|
+
conceptIds,
|
|
45
|
+
conceptUuids,
|
|
46
|
+
conceptLabels,
|
|
47
|
+
icd10Codes,
|
|
48
|
+
icd11Codes,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<div style={{ fontWeight: 600 }}>Diagnosis filters</div>
|
|
55
|
+
|
|
56
|
+
<ConceptSearchMultiSelect
|
|
57
|
+
id="diag-concepts"
|
|
58
|
+
labelText="Diagnosis concept(s)"
|
|
59
|
+
helperText="Search OpenMRS concepts and add one or more. ICD codes are shown to help you choose."
|
|
60
|
+
// ✅ Pass the full self-contained objects
|
|
61
|
+
value={value.selectedConcepts ?? []}
|
|
62
|
+
onChange={onChangeConcepts}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem', marginTop: '0.75rem' }}>
|
|
66
|
+
<Select
|
|
67
|
+
id="diag-certainty"
|
|
68
|
+
labelText="Certainty"
|
|
69
|
+
value={value.certainty}
|
|
70
|
+
onChange={(e) =>
|
|
71
|
+
onChange({
|
|
72
|
+
...value,
|
|
73
|
+
certainty: (e.target as HTMLSelectElement).value as DiagnosisBaseConfig['certainty'],
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<SelectItem value="PROVISIONAL" text="PROVISIONAL" />
|
|
78
|
+
<SelectItem value="CONFIRMED" text="CONFIRMED" />
|
|
79
|
+
</Select>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem', marginTop: '0.75rem' }}>
|
|
83
|
+
<TextInput
|
|
84
|
+
id="diag-ranks"
|
|
85
|
+
labelText="Diagnosis ranks (CSV)"
|
|
86
|
+
helperText="Example: 1,2"
|
|
87
|
+
value={value.dxRanksCsv}
|
|
88
|
+
onChange={(e) => onChange({ ...value, dxRanksCsv: (e.target as HTMLInputElement).value })}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<TextInput
|
|
92
|
+
id="diag-derived-ids"
|
|
93
|
+
labelText="Derived diagnosis_coded IDs"
|
|
94
|
+
helperText="Uses concept.id (perfect warehouse join)."
|
|
95
|
+
value={value.conceptIds?.length ? value.conceptIds.join(',') : ''}
|
|
96
|
+
readOnly
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}*/
|