@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,125 @@
1
+ import React from 'react';
2
+ import { Button, TextInput, Select, SelectItem } from '@carbon/react';
3
+ import { Add, TrashCan } from '@carbon/icons-react';
4
+
5
+ import type { DataThemeConfig, ThemeField, FieldType } from '../../../types/theme/data-theme.types';
6
+
7
+ type Props = {
8
+ config: DataThemeConfig;
9
+ onChange: (next: DataThemeConfig) => void;
10
+ open: boolean;
11
+ };
12
+
13
+ const FIELD_TYPES: FieldType[] = ['string', 'number', 'date', 'datetime', 'boolean', 'coded', 'json'];
14
+
15
+ function makeKey(label: string) {
16
+ return (label ?? '')
17
+ .toLowerCase()
18
+ .trim()
19
+ .replace(/[^a-z0-9]+/g, '_')
20
+ .replace(/^_+|_+$/g, '')
21
+ .slice(0, 40);
22
+ }
23
+
24
+ export default function DataThemeFieldsEditorSection({ config, onChange }: Props) {
25
+ const fields = config.fields ?? [];
26
+
27
+ const addField = () => {
28
+ const next: ThemeField = { key: `field_${fields.length + 1}`, label: '', expr: '', type: 'string' };
29
+ onChange({ ...config, fields: [...fields, next] });
30
+ };
31
+
32
+ const updateField = (idx: number, patch: Partial<ThemeField>) => {
33
+ const next = fields.map((f, i) => (i === idx ? { ...f, ...patch } : f));
34
+ onChange({ ...config, fields: next });
35
+ };
36
+
37
+ const removeField = (idx: number) => {
38
+ const next = fields.filter((_, i) => i !== idx);
39
+ onChange({ ...config, fields: next });
40
+ };
41
+
42
+ return (
43
+ <div>
44
+ <div style={{ fontWeight: 600, marginBottom: '0.75rem' }}>Fields</div>
45
+
46
+ <div style={{ marginBottom: '0.75rem', opacity: 0.85, fontSize: '0.875rem' }}>
47
+ Define the fields exposed by this theme. <b>expr</b> can be a column name or a SQL expression.
48
+ </div>
49
+
50
+ {fields.length === 0 ? (
51
+ <div style={{ marginBottom: '0.75rem', opacity: 0.8 }}>No fields yet. Add at least one field.</div>
52
+ ) : null}
53
+
54
+ <div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1.2fr 2fr 1fr auto', gap: '0.75rem' }}>
55
+ <div style={{ fontWeight: 600 }}>Key</div>
56
+ <div style={{ fontWeight: 600 }}>Label</div>
57
+ <div style={{ fontWeight: 600 }}>Expression</div>
58
+ <div style={{ fontWeight: 600 }}>Type</div>
59
+ <div />
60
+
61
+ {fields.map((f, idx) => (
62
+ <React.Fragment key={idx}>
63
+ <TextInput
64
+ id={`theme-field-key-${idx}`}
65
+ labelText=""
66
+ hideLabel
67
+ value={f.key}
68
+ placeholder="field_key"
69
+ onChange={(e) => updateField(idx, { key: (e.target as HTMLInputElement).value })}
70
+ />
71
+
72
+ <TextInput
73
+ id={`theme-field-label-${idx}`}
74
+ labelText=""
75
+ hideLabel
76
+ value={f.label}
77
+ placeholder="Field label"
78
+ onBlur={() => {
79
+ // auto-fill key if empty
80
+ if (!f.key && f.label) updateField(idx, { key: makeKey(f.label) });
81
+ }}
82
+ onChange={(e) => updateField(idx, { label: (e.target as HTMLInputElement).value })}
83
+ />
84
+
85
+ <TextInput
86
+ id={`theme-field-expr-${idx}`}
87
+ labelText=""
88
+ hideLabel
89
+ value={f.expr}
90
+ placeholder="column_name or SQL expression"
91
+ onChange={(e) => updateField(idx, { expr: (e.target as HTMLInputElement).value })}
92
+ />
93
+
94
+ <Select
95
+ id={`theme-field-type-${idx}`}
96
+ labelText=""
97
+ hideLabel
98
+ value={f.type}
99
+ onChange={(e) => updateField(idx, { type: (e.target as HTMLSelectElement).value as FieldType })}
100
+ >
101
+ {FIELD_TYPES.map((t) => (
102
+ <SelectItem key={t} value={t} text={t} />
103
+ ))}
104
+ </Select>
105
+
106
+ <Button
107
+ kind="ghost"
108
+ size="sm"
109
+ hasIconOnly
110
+ iconDescription="Remove field"
111
+ renderIcon={TrashCan}
112
+ onClick={() => removeField(idx)}
113
+ />
114
+ </React.Fragment>
115
+ ))}
116
+ </div>
117
+
118
+ <div style={{ marginTop: '1rem' }}>
119
+ <Button kind="secondary" size="sm" renderIcon={Add} onClick={addField}>
120
+ Add field
121
+ </Button>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,245 @@
1
+ import React from 'react';
2
+ import {
3
+ ComboBox,
4
+ Checkbox,
5
+ MultiSelect,
6
+ NumberInput,
7
+ Stack,
8
+ TextArea,
9
+ TextInput,
10
+ } from '@carbon/react';
11
+
12
+ import { themeIconOptions, type ThemeIconKey, getThemeIcon } from '../icon-registry';
13
+
14
+ export type AllowedIndicatorKind = 'BASE' | 'FINAL';
15
+
16
+ export type DataThemeMeta = {
17
+ icon: string;
18
+ color: string; // hex
19
+ category: string;
20
+ order: number;
21
+ beta: boolean;
22
+ descriptionShort: string;
23
+ allowedIndicatorKinds: AllowedIndicatorKind[];
24
+ };
25
+
26
+ type Props = {
27
+ value: DataThemeMeta;
28
+ onChange: (next: DataThemeMeta) => void;
29
+ open: boolean;
30
+ };
31
+
32
+ const KIND_ITEMS: AllowedIndicatorKind[] = ['BASE', 'FINAL'];
33
+
34
+ function normalizeHex(input: string, fallback = '#0f62fe') {
35
+ const raw = (input ?? '').trim();
36
+ if (!raw) return fallback;
37
+
38
+ const v = raw.startsWith('#') ? raw : `#${raw}`;
39
+ const short = /^#[0-9a-fA-F]{3}$/;
40
+ const full = /^#[0-9a-fA-F]{6}$/;
41
+
42
+ if (full.test(v)) return v.toLowerCase();
43
+ if (short.test(v)) {
44
+ const r = v[1],
45
+ g = v[2],
46
+ b = v[3];
47
+ return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
48
+ }
49
+
50
+ return fallback;
51
+ }
52
+
53
+ export default function DataThemeMetadataSection({ value, onChange, open }: Props) {
54
+ const colorValue = normalizeHex(value.color);
55
+ const IconPreview = getThemeIcon(value.icon);
56
+
57
+ return (
58
+ <div>
59
+ <div style={{ fontWeight: 600, marginBottom: '0.75rem' }}>Metadata</div>
60
+
61
+ <div style={{ marginBottom: '0.75rem', opacity: 0.85, fontSize: '0.875rem' }}>
62
+ Controls how this theme appears in the UI (icon, color, category, ordering, etc.).
63
+ Stored as <b>metaJson</b>.
64
+ </div>
65
+
66
+ <Stack gap={5}>
67
+
68
+ {/* ICON */}
69
+ <div>
70
+ <ComboBox
71
+ id="theme-meta-icon"
72
+ titleText="Icon"
73
+ items={[...themeIconOptions]}
74
+ disabled={!open}
75
+ itemToString={(it) => (it ? it.label : '')}
76
+ selectedItem={
77
+ themeIconOptions.find((x) => x.key === value.icon) ?? null
78
+ }
79
+ placeholder="Type to search icons…"
80
+ onChange={(e: any) => {
81
+ const picked = e?.selectedItem as
82
+ | (typeof themeIconOptions)[number]
83
+ | null;
84
+ onChange({
85
+ ...value,
86
+ icon: (picked?.key as ThemeIconKey) ?? '',
87
+ });
88
+ }}
89
+ />
90
+
91
+ {/* Preview */}
92
+ {IconPreview ? (
93
+ <div
94
+ style={{
95
+ marginTop: '0.5rem',
96
+ display: 'flex',
97
+ alignItems: 'center',
98
+ gap: '0.5rem',
99
+ opacity: 0.9,
100
+ }}
101
+ >
102
+ <IconPreview size={20} />
103
+ <span style={{ fontSize: '0.875rem' }}>{value.icon}</span>
104
+ </div>
105
+ ) : null}
106
+ </div>
107
+
108
+ {/* COLOR */}
109
+ <div>
110
+ <div style={{ fontSize: '0.875rem', marginBottom: '0.25rem' }}>
111
+ Color
112
+ </div>
113
+
114
+ <div
115
+ style={{
116
+ display: 'flex',
117
+ alignItems: 'center',
118
+ gap: '0.75rem',
119
+ }}
120
+ >
121
+ <input
122
+ id="theme-meta-color-picker"
123
+ type="color"
124
+ value={colorValue}
125
+ disabled={!open}
126
+ onChange={(e) =>
127
+ onChange({
128
+ ...value,
129
+ color: normalizeHex(e.target.value),
130
+ })
131
+ }
132
+ style={{
133
+ width: '3rem',
134
+ height: '2.5rem',
135
+ padding: 0,
136
+ border: '1px solid var(--cds-border-subtle)',
137
+ borderRadius: '4px',
138
+ background: 'transparent',
139
+ cursor: open ? 'pointer' : 'not-allowed',
140
+ }}
141
+ />
142
+
143
+ <TextInput
144
+ id="theme-meta-color-hex"
145
+ labelText=""
146
+ hideLabel
147
+ value={colorValue}
148
+ disabled={!open}
149
+ onChange={(e) =>
150
+ onChange({
151
+ ...value,
152
+ color: normalizeHex(
153
+ (e.target as HTMLInputElement).value,
154
+ ),
155
+ })
156
+ }
157
+ />
158
+ </div>
159
+ </div>
160
+
161
+ {/* CATEGORY + ORDER */}
162
+ <div
163
+ style={{
164
+ display: 'grid',
165
+ gridTemplateColumns: '1fr 1fr',
166
+ gap: '0.75rem',
167
+ }}
168
+ >
169
+ <TextInput
170
+ id="theme-meta-category"
171
+ labelText="Category"
172
+ value={value.category ?? ''}
173
+ disabled={!open}
174
+ placeholder="e.g. Clinical"
175
+ onChange={(e) =>
176
+ onChange({
177
+ ...value,
178
+ category: (e.target as HTMLInputElement).value,
179
+ })
180
+ }
181
+ />
182
+
183
+ <NumberInput
184
+ id="theme-meta-order"
185
+ label="Order"
186
+ value={Number.isFinite(value.order) ? value.order : 0}
187
+ disabled={!open}
188
+ min={0}
189
+ step={1}
190
+ onChange={(_, data) => {
191
+ const n = Number(data?.value);
192
+ onChange({
193
+ ...value,
194
+ order: Number.isFinite(n) ? n : 0,
195
+ });
196
+ }}
197
+ />
198
+ </div>
199
+
200
+ {/* BETA */}
201
+ <Checkbox
202
+ id="theme-meta-beta"
203
+ labelText="Beta"
204
+ checked={Boolean(value.beta)}
205
+ disabled={!open}
206
+ onChange={(_, { checked }) =>
207
+ onChange({ ...value, beta: Boolean(checked) })
208
+ }
209
+ />
210
+
211
+ {/* SHORT DESCRIPTION */}
212
+ <TextArea
213
+ id="theme-meta-description-short"
214
+ labelText="Short description"
215
+ value={value.descriptionShort ?? ''}
216
+ disabled={!open}
217
+ placeholder="e.g. Diagnosis-based indicators"
218
+ onChange={(e) =>
219
+ onChange({
220
+ ...value,
221
+ descriptionShort: (
222
+ e.target as HTMLTextAreaElement
223
+ ).value,
224
+ })
225
+ }
226
+ />
227
+
228
+ {/* ALLOWED KINDS */}
229
+ <MultiSelect
230
+ id="theme-meta-kinds"
231
+ titleText="Allowed indicator kinds"
232
+ items={KIND_ITEMS}
233
+ disabled={!open}
234
+ itemToString={(x) => (x ? String(x) : '')}
235
+ initialSelectedItems={value.allowedIndicatorKinds ?? []}
236
+ onChange={(e: any) => {
237
+ const next = (e?.selectedItems ?? []) as AllowedIndicatorKind[];
238
+ onChange({ ...value, allowedIndicatorKinds: next });
239
+ }}
240
+ label="Select kinds"
241
+ />
242
+ </Stack>
243
+ </div>
244
+ );
245
+ }
@@ -0,0 +1,115 @@
1
+ import React from 'react';
2
+ import { Button, TextArea } from '@carbon/react';
3
+ import { Copy } from '@carbon/icons-react';
4
+
5
+ type Props = {
6
+ config: any; // or DataThemeConfig
7
+ metaJson: string; // pass the already-wrapped metaJson string you're saving
8
+ onConfigJson: (json: string) => void;
9
+ };
10
+
11
+ function copyToClipboard(text: string) {
12
+ if (!text) return;
13
+ navigator.clipboard?.writeText(text).catch(() => {});
14
+ }
15
+
16
+ export default function DataThemePreviewSection({ config, metaJson, onConfigJson }: Props) {
17
+ const configJson = React.useMemo(() => JSON.stringify(config, null, 2), [config]);
18
+
19
+ React.useEffect(() => {
20
+ onConfigJson(configJson);
21
+ }, [configJson, onConfigJson]);
22
+
23
+ const Panel = ({
24
+ title,
25
+ subtitle,
26
+ value,
27
+ copyLabel,
28
+ textAreaId,
29
+ }: {
30
+ title: string;
31
+ subtitle?: string;
32
+ value: string;
33
+ copyLabel: string;
34
+ textAreaId: string;
35
+ }) => (
36
+ <div
37
+ style={{
38
+ background: 'var(--cds-layer-01)',
39
+ border: '1px solid var(--cds-border-subtle)',
40
+ borderRadius: 8,
41
+ padding: '1rem',
42
+ }}
43
+ >
44
+ <div style={{ display: 'flex', alignItems: 'start', justifyContent: 'space-between', gap: '1rem' }}>
45
+ <div>
46
+ <div style={{ fontWeight: 600 }}>{title}</div>
47
+ {subtitle ? (
48
+ <div style={{ fontSize: '0.875rem', opacity: 0.8, marginTop: '0.25rem' }}>{subtitle}</div>
49
+ ) : null}
50
+ </div>
51
+
52
+ <Button
53
+ kind="ghost"
54
+ size="sm"
55
+ renderIcon={Copy}
56
+ iconDescription={copyLabel}
57
+ onClick={() => copyToClipboard(value)}
58
+ >
59
+ Copy
60
+ </Button>
61
+ </div>
62
+
63
+ <div style={{ marginTop: '0.75rem' }}>
64
+ <TextArea
65
+ id={textAreaId}
66
+ labelText=""
67
+ hideLabel
68
+ value={value}
69
+ readOnly
70
+ style={{
71
+ minHeight: 420,
72
+ fontFamily:
73
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
74
+ }}
75
+ />
76
+ </div>
77
+ </div>
78
+ );
79
+
80
+ return (
81
+ <div>
82
+ <div style={{ marginBottom: '0.75rem' }}>
83
+ <div style={{ fontWeight: 600 }}>Preview</div>
84
+ <div style={{ fontSize: '0.875rem', opacity: 0.8 }}>
85
+ These are the exact JSON payloads that will be saved.
86
+ </div>
87
+ </div>
88
+
89
+ <div
90
+ style={{
91
+ display: 'grid',
92
+ gridTemplateColumns: '1fr 1fr',
93
+ gap: '1rem',
94
+ alignItems: 'start',
95
+ }}
96
+ >
97
+ <Panel
98
+ title="configJson"
99
+ subtitle="Data source + fields + conditions."
100
+ value={configJson}
101
+ copyLabel="Copy configJson"
102
+ textAreaId="theme-config-preview"
103
+ />
104
+
105
+ <Panel
106
+ title="metaJson"
107
+ subtitle="UI metadata (icon, color, category, ordering)."
108
+ value={metaJson}
109
+ copyLabel="Copy metaJson"
110
+ textAreaId="theme-meta-preview"
111
+ />
112
+ </div>
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,184 @@
1
+ import React from 'react';
2
+ import { ComboBox, Select, SelectItem, InlineLoading } from '@carbon/react';
3
+
4
+ import type { DataThemeConfig } from '../../../types/theme/data-theme.types';
5
+ import { getSchemaTables, type SchemaTable } from '../../../resources/theme/mamba-schema.api';
6
+ import { getMambaTableMeta, type TableColumn } from '../../../resources/theme/mamba-table-meta.api';
7
+
8
+ type Props = {
9
+ open: boolean;
10
+ config: DataThemeConfig;
11
+ onChange: (next: DataThemeConfig) => void;
12
+ };
13
+
14
+ function getTableName(t: SchemaTable): string {
15
+ // supports { name }, { table }, { tableName }, or string-ish payloads (defensive)
16
+ if (typeof (t as any) === 'string') return String(t);
17
+ return (t as any)?.name ?? (t as any)?.table ?? (t as any)?.tableName ?? '';
18
+ }
19
+
20
+ export default function DataThemeSourceSection({ open, config, onChange }: Props) {
21
+ // tables state
22
+ const [tables, setTables] = React.useState<SchemaTable[]>([]);
23
+ const [loadingTables, setLoadingTables] = React.useState(false);
24
+ const [tablesError, setTablesError] = React.useState<string | null>(null);
25
+
26
+ // columns state
27
+ const [columns, setColumns] = React.useState<TableColumn[]>([]);
28
+ const [loadingCols, setLoadingCols] = React.useState(false);
29
+ const [colsError, setColsError] = React.useState<string | null>(null);
30
+
31
+ // Load schema tables only when modal is open
32
+ React.useEffect(() => {
33
+ if (!open) {
34
+ setTables([]);
35
+ setLoadingTables(false);
36
+ setTablesError(null);
37
+ return;
38
+ }
39
+
40
+ const ac = new AbortController();
41
+ setLoadingTables(true);
42
+ setTablesError(null);
43
+
44
+ getSchemaTables(ac.signal)
45
+ .then((data) => setTables(data ?? []))
46
+ .catch((e) => {
47
+ if (e?.name !== 'AbortError') setTablesError(e?.message ?? 'Failed to load tables');
48
+ })
49
+ .finally(() => setLoadingTables(false));
50
+
51
+ return () => ac.abort();
52
+ }, [open]);
53
+
54
+ // Load columns only when modal is open AND a table is selected
55
+ React.useEffect(() => {
56
+ if (!open) {
57
+ setColumns([]);
58
+ setLoadingCols(false);
59
+ setColsError(null);
60
+ return;
61
+ }
62
+
63
+ const table = config?.sourceTable;
64
+ if (!table) {
65
+ setColumns([]);
66
+ setLoadingCols(false);
67
+ setColsError(null);
68
+ return;
69
+ }
70
+
71
+ const ac = new AbortController();
72
+ setLoadingCols(true);
73
+ setColsError(null);
74
+
75
+ getMambaTableMeta(table, ac.signal)
76
+ .then((data) => setColumns(data ?? []))
77
+ .catch((e) => {
78
+ if (e?.name !== 'AbortError') setColsError(e?.message ?? 'Failed to load columns');
79
+ })
80
+ .finally(() => setLoadingCols(false));
81
+
82
+ return () => ac.abort();
83
+ }, [open, config?.sourceTable]);
84
+
85
+ const colNames = React.useMemo(
86
+ () => (columns ?? []).map((c) => c?.name).filter(Boolean) as string[],
87
+ [columns],
88
+ );
89
+
90
+ const tableNames = React.useMemo(() => (tables ?? []).map(getTableName).filter(Boolean), [tables]);
91
+
92
+ const canPickTable = open && !loadingTables && !tablesError;
93
+ const canPickCols = open && Boolean(config.sourceTable) && !loadingCols && !colsError;
94
+
95
+ return (
96
+ <div>
97
+ <div style={{ fontWeight: 600, marginBottom: '0.75rem' }}>Source</div>
98
+
99
+ {/* ✅ Searchable table selector */}
100
+ <ComboBox
101
+ id="theme-source-table"
102
+ titleText="Source table/view"
103
+ items={tableNames}
104
+ selectedItem={config.sourceTable || null}
105
+ disabled={!canPickTable}
106
+ placeholder={loadingTables ? 'Loading…' : tablesError ? 'Failed to load tables' : 'Type to search tables…'}
107
+ onChange={(e: any) => {
108
+ const nextTable = e?.selectedItem ?? '';
109
+
110
+ onChange({
111
+ ...config,
112
+ sourceTable: nextTable,
113
+ patientIdColumn: '',
114
+ dateColumn: '',
115
+ locationColumn: '',
116
+ fields: config.fields ?? [],
117
+ });
118
+ }}
119
+ />
120
+
121
+ <div style={{ marginTop: '0.5rem' }}>
122
+ {loadingTables ? <InlineLoading description="Loading tables…" /> : null}
123
+ {!loadingTables && tablesError ? (
124
+ <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{tablesError}</div>
125
+ ) : null}
126
+ </div>
127
+
128
+ <div
129
+ style={{
130
+ display: 'grid',
131
+ gridTemplateColumns: '1fr 1fr 1fr',
132
+ gap: '0.75rem',
133
+ marginTop: '0.75rem',
134
+ }}
135
+ >
136
+ <Select
137
+ id="theme-patient-id-col"
138
+ labelText="patient_id column"
139
+ value={config.patientIdColumn || ''}
140
+ disabled={!canPickCols}
141
+ onChange={(e) => onChange({ ...config, patientIdColumn: (e.target as HTMLSelectElement).value })}
142
+ >
143
+ <SelectItem value="" disabled text={loadingCols ? 'Loading…' : 'Select'} />
144
+ {colNames.map((c) => (
145
+ <SelectItem key={c} value={c} text={c} />
146
+ ))}
147
+ </Select>
148
+
149
+ <Select
150
+ id="theme-date-col"
151
+ labelText="date column"
152
+ value={config.dateColumn || ''}
153
+ disabled={!canPickCols}
154
+ onChange={(e) => onChange({ ...config, dateColumn: (e.target as HTMLSelectElement).value })}
155
+ >
156
+ <SelectItem value="" disabled text={loadingCols ? 'Loading…' : 'Select'} />
157
+ {colNames.map((c) => (
158
+ <SelectItem key={c} value={c} text={c} />
159
+ ))}
160
+ </Select>
161
+
162
+ <Select
163
+ id="theme-location-col"
164
+ labelText="location column (optional)"
165
+ value={config.locationColumn || ''}
166
+ disabled={!canPickCols}
167
+ onChange={(e) => onChange({ ...config, locationColumn: (e.target as HTMLSelectElement).value })}
168
+ >
169
+ <SelectItem value="" disabled text={loadingCols ? 'Loading…' : 'Select'} />
170
+ {colNames.map((c) => (
171
+ <SelectItem key={c} value={c} text={c} />
172
+ ))}
173
+ </Select>
174
+ </div>
175
+
176
+ <div style={{ marginTop: '0.5rem' }}>
177
+ {loadingCols ? <InlineLoading description="Loading columns…" /> : null}
178
+ {!loadingCols && colsError ? (
179
+ <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{colsError}</div>
180
+ ) : null}
181
+ </div>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,24 @@
1
+ Indicator Feature Replacement (Clean)
2
+
3
+ This zip contains a cleaned indicator feature implementation:
4
+
5
+ - `indicators/` -> place under: `src/report-builder/components/indicators/`
6
+ - `services/` -> place under: `src/report-builder/resources/`
7
+
8
+ Key changes
9
+ - Create Base Indicator modal is broken into small components:
10
+ - `base-indicator-basic-fields.component.tsx`
11
+ - `base-indicator-theme-unit-fields.component.tsx`
12
+ - `diagnosis-filters-form.component.tsx`
13
+ - `sql-preview.component.tsx`
14
+ - Diagnosis concept selection now supports:
15
+ - search OpenMRS concepts (REST)
16
+ - add one or more concepts
17
+ - renders selected concepts as removable pills (tags)
18
+ - SQL builder uses `=` for 1 concept and `IN (...)` for 2+ concepts
19
+ - OpenMRS concept search response is expected to be `{ "results": [...] }` and we parse `CIEL: <number>` mappings
20
+ to derive the numeric concept_id used in warehouse (`diagnosis_coded`).
21
+
22
+ Notes
23
+ - REST base URL resolver is in: `services/concepts/openmrs-rest-base.ts`
24
+ - Optionally set `window.OPENMRS_BASE_URL = 'http://host:port/openmrs'` (no trailing slash)