@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.
Files changed (222) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/LICENSE +401 -0
  5. package/README.md +317 -0
  6. package/dist/184.js +2 -0
  7. package/dist/184.js.LICENSE.txt +14 -0
  8. package/dist/184.js.map +1 -0
  9. package/dist/211.js +1 -0
  10. package/dist/211.js.map +1 -0
  11. package/dist/300.js +1 -0
  12. package/dist/311.js +1 -0
  13. package/dist/311.js.map +1 -0
  14. package/dist/44.js +2 -0
  15. package/dist/44.js.LICENSE.txt +30 -0
  16. package/dist/44.js.map +1 -0
  17. package/dist/478.js +2 -0
  18. package/dist/478.js.LICENSE.txt +9 -0
  19. package/dist/478.js.map +1 -0
  20. package/dist/535.js +1 -0
  21. package/dist/535.js.map +1 -0
  22. package/dist/540.js +2 -0
  23. package/dist/540.js.LICENSE.txt +9 -0
  24. package/dist/540.js.map +1 -0
  25. package/dist/710.js +1 -0
  26. package/dist/710.js.map +1 -0
  27. package/dist/729.js +1 -0
  28. package/dist/729.js.map +1 -0
  29. package/dist/872.js +1 -0
  30. package/dist/872.js.map +1 -0
  31. package/dist/917.js +1 -0
  32. package/dist/917.js.map +1 -0
  33. package/dist/961.js +2 -0
  34. package/dist/961.js.LICENSE.txt +19 -0
  35. package/dist/961.js.map +1 -0
  36. package/dist/972.js +2 -0
  37. package/dist/972.js.LICENSE.txt +21 -0
  38. package/dist/972.js.map +1 -0
  39. package/dist/main.js +1 -0
  40. package/dist/main.js.map +1 -0
  41. package/dist/openmrs-esm-report-builder.js +1 -0
  42. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +482 -0
  43. package/dist/openmrs-esm-report-builder.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/e2e/core/env.ts +5 -0
  46. package/e2e/core/global-setup.ts +34 -0
  47. package/e2e/core/test.ts +21 -0
  48. package/e2e/pages/app-shell.page.ts +34 -0
  49. package/e2e/pages/etl-sources.page.ts +35 -0
  50. package/e2e/pages/report-sections.page.ts +20 -0
  51. package/e2e/pages/section-preview.modal.ts +29 -0
  52. package/e2e/specs/etl-sources.spec.ts +51 -0
  53. package/e2e/specs/navigation.spec.ts +13 -0
  54. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  55. package/e2e/specs/section-preview.spec.ts +94 -0
  56. package/e2e/tsconfig.json +11 -0
  57. package/example.env +5 -0
  58. package/jest.config.js +28 -0
  59. package/package.json +102 -0
  60. package/playwright.config.ts +28 -0
  61. package/src/admin-card-link.extension.module.scss +55 -0
  62. package/src/admin-card-link.extension.tsx +24 -0
  63. package/src/components/admin/admin-page.component.tsx +31 -0
  64. package/src/components/admin/age-categories-page.component.tsx +109 -0
  65. package/src/components/admin/age-groups-page.component.tsx +146 -0
  66. package/src/components/admin/etl-sources-page.component.tsx +344 -0
  67. package/src/components/admin/report-categories-page.component.tsx +256 -0
  68. package/src/components/admin/report-library-page.component.tsx +540 -0
  69. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  70. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  71. package/src/components/ai-support/ai.service.ts +21 -0
  72. package/src/components/ai-support/ai.types.ts +12 -0
  73. package/src/components/app-shell/report-builder-shell.component.tsx +120 -0
  74. package/src/components/app-shell/report-builder-shell.scss +15 -0
  75. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  76. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  77. package/src/components/data-themes/data-themes-page.component.tsx +132 -0
  78. package/src/components/data-themes/data-themes-table.component.tsx +91 -0
  79. package/src/components/data-themes/icon-registry.ts +35 -0
  80. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  81. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  82. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  83. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  84. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  85. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  86. package/src/components/indicators/README.md +24 -0
  87. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  88. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  89. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  90. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  91. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  92. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  93. package/src/components/indicators/create-base-indicator-modal.component.tsx +501 -0
  94. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +297 -0
  95. package/src/components/indicators/create-final-indicator-modal.component.tsx +260 -0
  96. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  97. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  98. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  99. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  100. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  101. package/src/components/indicators/indicators-page.component.tsx +481 -0
  102. package/src/components/indicators/indicators-page.scss +121 -0
  103. package/src/components/indicators/indicators-table.component.tsx +166 -0
  104. package/src/components/indicators/preview-table.component.tsx +31 -0
  105. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  106. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +161 -0
  107. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  108. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  109. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  110. package/src/components/indicators/sections/final-indicator-picker.section.tsx +92 -0
  111. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  112. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  113. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  114. package/src/components/indicators/sections/indicator-conditions.section.tsx +194 -0
  115. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +26 -0
  116. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  117. package/src/components/indicators/sql-preview.component.tsx +19 -0
  118. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  119. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  120. package/src/components/indicators/types/composite-indicator.types.ts +21 -0
  121. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  122. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  123. package/src/components/indicators/types/indicator-types.ts +56 -0
  124. package/src/components/indicators/types/sql-builders.ts +43 -0
  125. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +206 -0
  126. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  127. package/src/components/indicators/utils/final-indicator-sql.utils.ts +124 -0
  128. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  129. package/src/components/indicators/utils/indicator-sql.utils.ts +185 -0
  130. package/src/components/landing/landing-action-item.component.tsx +50 -0
  131. package/src/components/landing/landing-action-list.component.tsx +12 -0
  132. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  133. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  134. package/src/components/landing/landing-hero.component.tsx +18 -0
  135. package/src/components/landing/landing-section-card.component.tsx +33 -0
  136. package/src/components/landing/report-builder-landing-page.component.tsx +103 -0
  137. package/src/components/landing/report-builder-landing-page.scss +131 -0
  138. package/src/components/report/dashboard/reports-table.component.tsx +145 -0
  139. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  140. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  141. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  142. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  143. package/src/components/report/definition/report-definition.types.ts +66 -0
  144. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  145. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  146. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  147. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  148. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  149. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  150. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  151. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  152. package/src/components/report/design/report-design-editor.component.tsx +894 -0
  153. package/src/components/report/design/report-design.types.ts +43 -0
  154. package/src/components/report/design/report-design.utils.ts +86 -0
  155. package/src/components/report/panels/report-detail.component.tsx +71 -0
  156. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  157. package/src/components/report/report-dashboard.page.component.tsx +75 -0
  158. package/src/components/report/report-editor.page.component.tsx +466 -0
  159. package/src/components/report/report.routes.tsx +30 -0
  160. package/src/components/report-sections/create-report-section-modal.component.tsx +278 -0
  161. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  162. package/src/components/report-sections/hooks/useSectionEditorState.ts +306 -0
  163. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  164. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  165. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +143 -0
  166. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  167. package/src/components/report-sections/report-section-preview-modal.component.tsx +418 -0
  168. package/src/components/report-sections/report-sections-page.component.tsx +313 -0
  169. package/src/components/report-sections/section-types.ts +55 -0
  170. package/src/components/report-sections/section-utils.ts +46 -0
  171. package/src/components/report-sections/sections-page.scss +79 -0
  172. package/src/components/run-reports/run-reports-page.component.tsx +23 -0
  173. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  174. package/src/components/shared/header/header.component.tsx +85 -0
  175. package/src/components/shared/header/header.scss +150 -0
  176. package/src/components/shared/header/illustration.component.tsx +34 -0
  177. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  178. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  179. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  180. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  181. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  182. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  183. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  184. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  185. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  186. package/src/config-schema.ts +23 -0
  187. package/src/globals.d.ts +14 -0
  188. package/src/hooks/theme/useDataThemes.ts +28 -0
  189. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  190. package/src/hooks/theme/useMambaTables.ts +63 -0
  191. package/src/index.ts +23 -0
  192. package/src/resources/agegroup/agegroups.api.ts +58 -0
  193. package/src/resources/concepts/concept-mapper.ts +21 -0
  194. package/src/resources/concepts/concept-types.ts +31 -0
  195. package/src/resources/concepts/concepts.resource.ts +44 -0
  196. package/src/resources/concepts/useConceptSearch.ts +41 -0
  197. package/src/resources/etl-source/etl-source.api.ts +65 -0
  198. package/src/resources/indicator/indicators.api.ts +125 -0
  199. package/src/resources/openmrs-api.ts +24 -0
  200. package/src/resources/preview/sql-preview.api.ts +54 -0
  201. package/src/resources/report/reports.api.ts +117 -0
  202. package/src/resources/report-category/report-category.api.ts +41 -0
  203. package/src/resources/report-library/report-library.api.ts +63 -0
  204. package/src/resources/report-section/report-sections.api.ts +73 -0
  205. package/src/resources/report-section/section-preview.api.ts +21 -0
  206. package/src/resources/theme/data-theme.api.ts +88 -0
  207. package/src/resources/theme/mamba-schema.api.ts +31 -0
  208. package/src/resources/theme/mamba-table-meta.api.ts +45 -0
  209. package/src/root.component.tsx +20 -0
  210. package/src/routes/report-builder.routes.tsx +66 -0
  211. package/src/routes/report-builder.scss +56 -0
  212. package/src/routes.json +21 -0
  213. package/src/sample-template.ts +121 -0
  214. package/src/template-utils.ts +110 -0
  215. package/src/types/carbon-react-inert.d.ts +13 -0
  216. package/src/types/condition-operators.ts +49 -0
  217. package/src/types/theme/data-theme.types.ts +88 -0
  218. package/src/utils/html-entities.utils.ts +23 -0
  219. package/tools/setup-tests.ts +1 -0
  220. package/translations/en.json +4 -0
  221. package/tsconfig.json +43 -0
  222. package/webpack.config.js +14 -0
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { TextInput } from '@carbon/react';
3
+
4
+ import QueryResultsPreview from '../../shared/preview/query-results-preview.component';
5
+
6
+ type Props = {
7
+ sql: string;
8
+ maxRows?: number;
9
+
10
+ startDate: string;
11
+ endDate: string;
12
+ onChangeStartDate: (v: string) => void;
13
+ onChangeEndDate: (v: string) => void;
14
+ };
15
+
16
+ export default function FinalIndicatorResultsPreviewSection({
17
+ sql,
18
+ maxRows = 200,
19
+ startDate,
20
+ endDate,
21
+ onChangeStartDate,
22
+ onChangeEndDate,
23
+ }: Props) {
24
+ const canRun = Boolean(sql?.trim()) && Boolean(startDate) && Boolean(endDate);
25
+
26
+ return (
27
+ <div style={{ display: 'grid', gap: '0.75rem' }}>
28
+ <div
29
+ style={{
30
+ display: 'grid',
31
+ gridTemplateColumns: '320px 1fr',
32
+ gap: '1rem',
33
+ alignItems: 'start',
34
+ }}
35
+ >
36
+ {/* Left column: dates */}
37
+ <div style={{ display: 'grid', gap: '1rem' }}>
38
+ <TextInput
39
+ id="final-preview-start"
40
+ labelText="Start date"
41
+ type="date"
42
+ value={startDate}
43
+ onChange={(e) => onChangeStartDate((e.target as HTMLInputElement).value)}
44
+ />
45
+ <TextInput
46
+ id="final-preview-end"
47
+ labelText="End date"
48
+ type="date"
49
+ value={endDate}
50
+ onChange={(e) => onChangeEndDate((e.target as HTMLInputElement).value)}
51
+ />
52
+ </div>
53
+
54
+ {/* Right column: query preview */}
55
+ <QueryResultsPreview
56
+ title="Query results preview"
57
+ sql={sql}
58
+ params={{ startDate, endDate }}
59
+ maxRows={maxRows}
60
+ canRun={canRun}
61
+ />
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { Stack, TextInput, TextArea } from '@carbon/react';
3
+
4
+ type Value = {
5
+ name: string;
6
+ code: string;
7
+ description: string;
8
+ };
9
+
10
+ type Props = {
11
+ value: Value;
12
+ onChange: (next: Value) => void;
13
+ };
14
+
15
+ export default function IndicatorBasicsSection({ value, onChange }: Props) {
16
+ return (
17
+ <Stack gap={6}>
18
+ <TextInput
19
+ id="indicator-code"
20
+ labelText="Code (optional)"
21
+ value={value.code}
22
+ onChange={(e) => onChange({ ...value, code: (e.target as HTMLInputElement).value })}
23
+ />
24
+
25
+ <TextInput
26
+ id="indicator-name"
27
+ labelText="Name"
28
+ value={value.name}
29
+ onChange={(e) => onChange({ ...value, name: (e.target as HTMLInputElement).value })}
30
+ />
31
+
32
+ <TextArea
33
+ id="indicator-desc"
34
+ labelText="Description (optional)"
35
+ value={value.description}
36
+ onChange={(e) => onChange({ ...value, description: (e.target as HTMLTextAreaElement).value })}
37
+ />
38
+ </Stack>
39
+ );
40
+ }
@@ -0,0 +1,194 @@
1
+ import React from 'react';
2
+ import { Stack, TextInput } from '@carbon/react';
3
+
4
+ import type { ThemeCondition } from '../types/data-theme-config.types';
5
+ import type { IndicatorCondition } from '../types/indicator-types';
6
+
7
+ import ConceptSearchMultiSelect, { type SelectedConcept } from '../handler/concept-search-multiselect.component';
8
+ import QuestionAnswerConceptSearch from '../handler/question-answer-concept-search.component';
9
+
10
+ import type { QAUiState } from '../types/condition-ui.types';
11
+
12
+ type QAValue = { questions: Array<string | number>; answers: Array<string | number> };
13
+
14
+ type Props = {
15
+ conditions: ThemeCondition[];
16
+
17
+ picked: IndicatorCondition[];
18
+ /**
19
+ * We accept a React state setter signature so callers can pass setState directly
20
+ * and we can safely use functional updates.
21
+ */
22
+ onPickedChange: React.Dispatch<React.SetStateAction<IndicatorCondition[]>>;
23
+
24
+ conceptUi: Record<string, SelectedConcept[]>;
25
+ onConceptUiChange: (next: Record<string, SelectedConcept[]>) => void;
26
+
27
+ qaUi: Record<string, QAUiState>;
28
+ onQaUiChange: (next: Record<string, QAUiState>) => void;
29
+ };
30
+
31
+ function normalizeNumericIds(xs: Array<any>) {
32
+ return xs.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0);
33
+ }
34
+
35
+ function normalizeStrings(xs: Array<any>) {
36
+ return xs.map((x) => String(x)).filter(Boolean);
37
+ }
38
+
39
+ function upsert(list: IndicatorCondition[], next: IndicatorCondition) {
40
+ const idx = (list ?? []).findIndex((p) => p.key === next.key);
41
+ if (idx === -1) return [...(list ?? []), next];
42
+ const copy = [...(list ?? [])];
43
+ copy[idx] = { ...copy[idx], ...next };
44
+ return copy;
45
+ }
46
+
47
+ export default function IndicatorConditionsSection({
48
+ conditions,
49
+ picked,
50
+ onPickedChange,
51
+ conceptUi,
52
+ onConceptUiChange,
53
+ qaUi,
54
+ onQaUiChange,
55
+ }: Props) {
56
+ const upsertPicked = React.useCallback(
57
+ (tc: ThemeCondition, patch: Partial<IndicatorCondition>) => {
58
+ const existing =
59
+ (picked ?? []).find((p) => p.key === tc.key) ??
60
+ ({
61
+ key: tc.key,
62
+ operator: tc.operator,
63
+ valueType: tc.valueType,
64
+ value:
65
+ tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH'
66
+ ? ({ questions: [], answers: [] } as any)
67
+ : tc.operator === 'IN' || tc.operator === 'NOT_IN'
68
+ ? []
69
+ : '',
70
+ } as IndicatorCondition);
71
+
72
+ onPickedChange(upsert(picked ?? [], { ...existing, ...patch, key: tc.key }));
73
+ },
74
+ [onPickedChange, picked],
75
+ );
76
+
77
+ const mergeQaUi = React.useCallback(
78
+ (key: string, next: Partial<QAUiState>) => {
79
+ onQaUiChange({
80
+ ...(qaUi ?? {}),
81
+ [key]: {
82
+ ...(qaUi?.[key] ?? { questions: [], answers: [] }),
83
+ ...next,
84
+ },
85
+ });
86
+ },
87
+ [onQaUiChange, qaUi],
88
+ );
89
+
90
+ const mergeConceptUi = React.useCallback(
91
+ (key: string, nextSelected: SelectedConcept[]) => {
92
+ onConceptUiChange({
93
+ ...(conceptUi ?? {}),
94
+ [key]: nextSelected,
95
+ });
96
+ },
97
+ [onConceptUiChange, conceptUi],
98
+ );
99
+
100
+ const renderCondition = (tc: ThemeCondition) => {
101
+ const current = (picked ?? []).find((x) => x.key === tc.key);
102
+
103
+ // QUESTION + ANSWERS
104
+ if (tc.handler === 'QUESTION_ANSWER_CONCEPT_SEARCH') {
105
+ const ui = qaUi?.[tc.key] ?? { questions: [], answers: [] };
106
+
107
+ return (
108
+ <div key={tc.key}>
109
+ <div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{tc.label || tc.key}</div>
110
+
111
+ <QuestionAnswerConceptSearch
112
+ id={`cond-${tc.key}`}
113
+ labelText={tc.label || 'Select question(s) & answers'}
114
+ value={{ questions: ui.questions, answers: ui.answers }}
115
+ onChange={(next) => {
116
+ // UI
117
+ mergeQaUi(tc.key, { questions: next.questions, answers: next.answers, error: undefined });
118
+
119
+ // Payload
120
+ const qVals =
121
+ tc.valueType === 'conceptUuid'
122
+ ? normalizeStrings((next.questions ?? []).map((q) => q.uuid))
123
+ : normalizeNumericIds((next.questions ?? []).map((q) => q.id));
124
+
125
+ const aVals =
126
+ tc.valueType === 'conceptUuid'
127
+ ? normalizeStrings((next.answers ?? []).map((a) => a.uuid))
128
+ : normalizeNumericIds((next.answers ?? []).map((a) => a.id));
129
+
130
+ upsertPicked(tc, { value: { questions: qVals, answers: aVals } as QAValue as any });
131
+ }}
132
+ />
133
+
134
+ {qaUi?.[tc.key]?.error ? (
135
+ <div style={{ marginTop: '0.5rem', color: 'var(--cds-text-error, #da1e28)', fontSize: '0.875rem' }}>
136
+ {qaUi[tc.key].error}
137
+ </div>
138
+ ) : null}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ // CONCEPT_SEARCH
144
+ if (tc.handler === 'CONCEPT_SEARCH') {
145
+ const uiSelected = conceptUi?.[tc.key] ?? [];
146
+
147
+ return (
148
+ <div key={tc.key}>
149
+ <div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{tc.label || tc.key}</div>
150
+
151
+ <ConceptSearchMultiSelect
152
+ id={`cond-${tc.key}`}
153
+ labelText={tc.label || 'Select concepts'}
154
+ value={uiSelected}
155
+ onChange={(nextSelected) => {
156
+ mergeConceptUi(tc.key, nextSelected);
157
+
158
+ if (tc.valueType === 'conceptUuid') {
159
+ upsertPicked(tc, { value: normalizeStrings(nextSelected.map((c) => c.uuid)) as any });
160
+ } else {
161
+ upsertPicked(tc, { value: normalizeNumericIds(nextSelected.map((c) => c.id)) as any });
162
+ }
163
+ }}
164
+ />
165
+ </div>
166
+ );
167
+ }
168
+
169
+ // default: text input
170
+ return (
171
+ <div key={tc.key}>
172
+ <TextInput
173
+ id={`cond-${tc.key}`}
174
+ labelText={tc.label || tc.key}
175
+ value={typeof current?.value === 'string' ? current.value : ''}
176
+ onChange={(e) =>
177
+ upsertPicked(tc, {
178
+ value: (e.target as HTMLInputElement).value as any,
179
+ })
180
+ }
181
+ />
182
+ </div>
183
+ );
184
+ };
185
+
186
+ if (!conditions?.length) return <div style={{ opacity: 0.8 }}>No conditions configured for this theme.</div>;
187
+
188
+ return (
189
+ <div>
190
+ <div style={{ fontWeight: 600, marginBottom: '0.75rem' }}>Conditions</div>
191
+ <Stack gap={5}>{conditions.map(renderCondition)}</Stack>
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ type Props = {
4
+ sql: string;
5
+ };
6
+
7
+ export default function IndicatorSqlPreviewSection({ sql }: Props) {
8
+ if (!sql) return <div style={{ opacity: 0.8 }}>SQL preview will appear once a theme is selected.</div>;
9
+
10
+ return (
11
+ <div>
12
+ <div style={{ fontWeight: 600, marginBottom: '0.5rem' }}>SQL Preview</div>
13
+ <pre
14
+ style={{
15
+ fontSize: '0.875rem',
16
+ background: 'var(--cds-layer-01)',
17
+ padding: '0.75rem',
18
+ borderRadius: 6,
19
+ overflowX: 'auto',
20
+ }}
21
+ >
22
+ {sql}
23
+ </pre>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { InlineLoading, Select, SelectItem, Stack } from '@carbon/react';
3
+ import type { DataThemeDto } from '../../../resources/theme/data-theme.api';
4
+
5
+ type Props = {
6
+ themes: DataThemeDto[];
7
+ loading: boolean;
8
+ error?: string | null;
9
+
10
+ themeUuid: string;
11
+ onThemeUuidChange: (uuid: string) => void;
12
+
13
+ themeConfigError?: string | null;
14
+ };
15
+
16
+ export default function IndicatorThemeSection({
17
+ themes,
18
+ loading,
19
+ error,
20
+ themeUuid,
21
+ onThemeUuidChange,
22
+ themeConfigError,
23
+ }: Props) {
24
+ return (
25
+ <Stack gap={4}>
26
+ <div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>Data Theme</div>
27
+
28
+ {loading ? <InlineLoading description="Loading themes…" /> : null}
29
+ {!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
30
+
31
+ <Select
32
+ id="indicator-theme"
33
+ labelText="Select a theme"
34
+ value={themeUuid}
35
+ onChange={(e) => onThemeUuidChange((e.target as HTMLSelectElement).value)}
36
+ >
37
+ <SelectItem value="" text="Select…" />
38
+ {(themes ?? []).map((t) => (
39
+ <SelectItem key={t.uuid} value={t.uuid} text={`${t.name}${t.code ? ` (${t.code})` : ''}`} />
40
+ ))}
41
+ </Select>
42
+
43
+ {themeConfigError ? (
44
+ <div style={{ color: 'var(--cds-text-error, #da1e28)', marginTop: '0.25rem' }}>{themeConfigError}</div>
45
+ ) : null}
46
+ </Stack>
47
+ );
48
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { TextArea } from '@carbon/react';
3
+
4
+ type Props = {
5
+ value: string;
6
+ };
7
+
8
+ const SqlPreview: React.FC<Props> = ({ value }) => {
9
+ return (
10
+ <TextArea
11
+ id="sql-preview"
12
+ labelText="Generated SQL (base indicator)"
13
+ value={value}
14
+ readOnly
15
+ />
16
+ );
17
+ };
18
+
19
+ export default SqlPreview;
@@ -0,0 +1,17 @@
1
+ export type IndicatorUnit = 'Patients' | 'Encounters';
2
+
3
+ export type CreateBaseIndicatorPayload = {
4
+ code: string;
5
+ theme: string; // e.g. DIAGNOSIS, OBSERVATIONS, MEDICATIONS, LAB
6
+ indicatorName: string;
7
+ description: string;
8
+
9
+ // Concept selection (multi)
10
+ conditionConceptUuids: string[];
11
+ conditionConceptLabels: string[];
12
+
13
+ pregnancyStatus: string;
14
+
15
+ unit: IndicatorUnit;
16
+ countBy: IndicatorUnit;
17
+ };
@@ -0,0 +1,8 @@
1
+ export const THEME_OPTIONS = ['DIAGNOSIS', 'OBSERVATIONS', 'MEDICATIONS', 'LAB'] as const;
2
+
3
+ export const THEME_LABELS: Record<(typeof THEME_OPTIONS)[number], string> = {
4
+ DIAGNOSIS: 'Diagnosis Data',
5
+ OBSERVATIONS: 'Observations',
6
+ MEDICATIONS: 'Medications',
7
+ LAB: 'Laboratory',
8
+ };
@@ -0,0 +1,21 @@
1
+ export type CompositeOperator = 'AND' | 'OR' | 'A_AND_NOT_B';
2
+
3
+ export type BaseIndicatorOption = {
4
+ id: string; // uuid
5
+ code: string;
6
+ name: string;
7
+ unit?: 'Patients' | 'Encounters';
8
+ };
9
+
10
+ export type CreateCompositeBaseIndicatorPayload = {
11
+ code: string;
12
+ name: string;
13
+ description: string;
14
+
15
+ indicatorAId: string; // uuid
16
+ indicatorBId: string; // uuid
17
+ operator: CompositeOperator;
18
+
19
+ unit?: 'Patients' | 'Encounters';
20
+ sqlTemplate?: string; // composite COUNT SQL
21
+ };
@@ -0,0 +1,11 @@
1
+ import type { SelectedConcept } from '../handler/concept-search-multiselect.component';
2
+
3
+ export type QAUiState = {
4
+ /** Selected question concepts (multi-select) */
5
+ questions: SelectedConcept[];
6
+ answers: SelectedConcept[];
7
+ error?: string;
8
+ };
9
+
10
+ export type ConceptUiMap = Record<string, SelectedConcept[]>;
11
+ export type QaUiMap = Record<string, QAUiState>;
@@ -0,0 +1,88 @@
1
+ // src/report-builder/types/theme/data-theme.types.ts
2
+
3
+ export type MainDataDomain =
4
+ | 'OBSERVATIONS'
5
+ | 'TEST_ORDERS'
6
+ | 'MEDICATION_ORDERS'
7
+ | 'APPOINTMENTS'
8
+ | 'MEDICATION_DISPENSE'
9
+ | 'DIAGNOSIS';
10
+
11
+ export type FieldType = 'string' | 'number' | 'date' | 'datetime' | 'boolean' | 'coded' | 'json';
12
+
13
+ export type ThemeField = {
14
+ key: string;
15
+ label: string;
16
+ expr: string;
17
+ type: FieldType;
18
+ };
19
+
20
+ export type ConditionHandler =
21
+ | 'CONCEPT_SEARCH'
22
+ | 'QUESTION_ANSWER_CONCEPT_SEARCH'
23
+ | 'TEXT'
24
+ | 'NUMBER'
25
+ | 'DATE_RANGE'
26
+ | 'BOOLEAN'
27
+ | 'LOCATION_PICKER'
28
+ | 'CODED_LIST';
29
+
30
+ /**
31
+ * Theme-side operator tokens (legacy + UI-friendly).
32
+ * NOTE: SQL generation will normalize these to real SQL operators.
33
+ */
34
+ export type ConditionOperator = 'EQUALS' | 'IN' | 'NOT_IN' | 'LIKE' | 'BETWEEN' | 'GTE' | 'LTE';
35
+
36
+ export type ConditionValueType =
37
+ | 'conceptUuid'
38
+ | 'conceptId'
39
+ | 'string'
40
+ | 'number'
41
+ | 'date'
42
+ | 'datetime'
43
+ | 'boolean';
44
+
45
+ export type ThemeCondition = {
46
+ key: string;
47
+ label: string;
48
+ handler: ConditionHandler;
49
+ column: string;
50
+ operator: ConditionOperator;
51
+ valueType: ConditionValueType;
52
+ };
53
+
54
+ export type DataThemeConfig = {
55
+ sourceTable: string;
56
+ patientIdColumn: string;
57
+ dateColumn: string;
58
+ locationColumn?: string;
59
+
60
+ joins?: Array<{ alias: string; joinSql: string }>;
61
+ defaultFilters?: string[];
62
+
63
+ fields: ThemeField[];
64
+
65
+ conditions?: ThemeCondition[];
66
+
67
+ conditionColumns?: Record<string, string>;
68
+ };
69
+
70
+ export type DataTheme = {
71
+ uuid?: string;
72
+ name: string;
73
+ description?: string;
74
+ code: string;
75
+ domain: MainDataDomain;
76
+ configJson: string;
77
+ metaJson?: string;
78
+ retired?: boolean;
79
+ };
80
+
81
+ export type DataThemeRow = {
82
+ uuid: string;
83
+ name: string;
84
+ code: string;
85
+ domain: MainDataDomain;
86
+ description?: string;
87
+ retired?: boolean;
88
+ };
@@ -0,0 +1,56 @@
1
+ import type { DataThemeConfig, ThemeCondition } from './data-theme-config.types';
2
+
3
+ /**
4
+ * What the indicator builder saves/uses.
5
+ * Keep it flexible because you already have a backend model.
6
+ */
7
+
8
+ export type IndicatorKind = 'BASE' | 'FINAL';
9
+
10
+ export type IndicatorConditionValue =
11
+ | string
12
+ | number
13
+ | Array<string | number>
14
+ | { start?: string; end?: string }
15
+ | null;
16
+
17
+ export type IndicatorCondition = {
18
+ key: string; // matches ThemeCondition.key
19
+ operator?: ThemeCondition['operator'];
20
+ valueType?: ThemeCondition['valueType'];
21
+
22
+ /**
23
+ * Actual chosen value(s).
24
+ * - concept search returns ids/uuids depending on your handler config
25
+ */
26
+ value: IndicatorConditionValue;
27
+ };
28
+
29
+ export type BaseIndicatorDraft = {
30
+ uuid?: string;
31
+ name: string;
32
+ description?: string;
33
+ code?: string;
34
+
35
+ kind: IndicatorKind;
36
+
37
+ /**
38
+ * The selected data theme
39
+ */
40
+ themeUuid: string;
41
+
42
+ /**
43
+ * Optional snapshot of theme config at save time (good for debugging / auditing)
44
+ */
45
+ themeConfig?: DataThemeConfig;
46
+
47
+ /**
48
+ * User-picked conditions (values), keys should match theme config conditions
49
+ */
50
+ conditions?: IndicatorCondition[];
51
+
52
+ /**
53
+ * Optional SQL preview used by UI
54
+ */
55
+ sqlPreview?: string;
56
+ };
@@ -0,0 +1,43 @@
1
+ /*
2
+ import type { DiagnosisBaseConfig } from './indicator-types';
3
+
4
+ function normalizeCsvToSqlInList(csv: string) {
5
+ const cleaned = (csv ?? '')
6
+ .split(',')
7
+ .map((x) => x.trim())
8
+ .filter(Boolean)
9
+ .map((x) => String(Number(x)))
10
+ .filter((x) => x !== 'NaN');
11
+
12
+ return cleaned.length ? cleaned.join(',') : '1';
13
+ }
14
+
15
+ function diagnosisClause(ids: number[]) {
16
+ if (!ids || ids.length === 0) {
17
+ return `-- AND a.diagnosis_coded = <select a concept>`;
18
+ }
19
+ if (ids.length === 1) {
20
+ return `AND a.diagnosis_coded = ${ids[0]}`;
21
+ }
22
+ return `AND a.diagnosis_coded IN (${ids.join(',')})`;
23
+ }
24
+
25
+ export function buildDiagnosisBaseSql(cfg: DiagnosisBaseConfig) {
26
+ const ranks = normalizeCsvToSqlInList(cfg.dxRanksCsv);
27
+
28
+ return `
29
+ SELECT
30
+ COUNT(*) AS total
31
+ FROM mamba_fact_encounter_diagnosis a
32
+ JOIN mamba_fact_patients_latest_patient_demographics mdp
33
+ ON mdp.patient_id = a.patient_id
34
+ WHERE a.date_created >= ':startDate'
35
+ AND a.date_created < ':endDate'
36
+ ${diagnosisClause(cfg.conceptIds)}
37
+ AND a.certainty = '${cfg.certainty}'
38
+ AND a.dx_rank IN (${ranks})
39
+ ${cfg.onlyNotVoided ? 'AND a.voided = 0' : ''}
40
+ ${cfg.requireNonNullBirthdate ? 'AND mdp.birthdate IS NOT NULL' : ''}
41
+ ${cfg.requireNonNullGender ? 'AND mdp.gender IS NOT NULL' : ''}
42
+ ;`.trim();
43
+ }*/