@epcare/esm-report-builder 0.1.1-pre.39 → 0.1.1-pre.40

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 (26) hide show
  1. package/dist/538.js +1 -0
  2. package/dist/538.js.map +1 -0
  3. package/dist/729.js +1 -1
  4. package/dist/main.js +1 -1
  5. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +26 -26
  6. package/dist/routes.json +1 -1
  7. package/package.json +1 -1
  8. package/src/components/indicators/indicator-search-select.component.tsx +0 -12
  9. package/src/components/indicators/indicators-page.component.tsx +2 -3
  10. package/src/components/linelist/builder/column-category-selector.component.tsx +162 -9
  11. package/src/components/linelist/builder/column-category-selector.scss +148 -3
  12. package/src/components/linelist/builder/column-debug-panel.component.tsx +197 -0
  13. package/src/components/linelist/builder/column-debug-panel.scss +141 -0
  14. package/src/components/linelist/builder/encounter-diagnosis-column-modal.component.tsx +43 -22
  15. package/src/components/linelist/builder/linelist-builder-workspace.component.tsx +1 -23
  16. package/src/components/linelist/builder/observation-column-modal.component.tsx +125 -40
  17. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +78 -8
  18. package/src/components/linelist/pages/linelist-report-editor-page.component.tsx +0 -2
  19. package/src/components/linelist/shared/json-preview.component.tsx +5 -2
  20. package/src/components/report-sections/hooks/useSectionEditorState.ts +4 -7
  21. package/src/components/shared/concept-selector.component.tsx +19 -2
  22. package/src/hooks/theme/useMambaTables.ts +0 -2
  23. package/src/types/linelist-types.ts +19 -1
  24. package/src/utils/config-enhancer.ts +62 -0
  25. package/dist/378.js +0 -1
  26. package/dist/378.js.map +0 -1
@@ -15,6 +15,7 @@ import {
15
15
  Stack,
16
16
  FormGroup,
17
17
  Checkbox,
18
+ Button,
18
19
  } from '@carbon/react';
19
20
  import ConceptSelector from '../../shared/concept-selector.component';
20
21
  import type { ConceptSummary } from '../../../resources/concepts/concept-types';
@@ -30,6 +31,8 @@ type Props = {
30
31
  onAddColumn: (column: LinelistColumnDraft) => void;
31
32
  /** Existing columns to check for duplicates */
32
33
  existingColumns?: LinelistColumnDraft[];
34
+ /** Column to edit (if provided, modal is in edit mode) */
35
+ editingColumn?: LinelistColumnDraft | null;
33
36
  };
34
37
 
35
38
  type ColumnModifier = 'ANY' | 'FIRST' | 'MOST_RECENT' | 'FIRST_N' | 'MOST_RECENT_N';
@@ -57,6 +60,7 @@ const ObservationColumnModal: React.FC<Props> = ({
57
60
  onClose,
58
61
  onAddColumn,
59
62
  existingColumns = [],
63
+ editingColumn = null,
60
64
  }) => {
61
65
  const [conceptSelectorKey, setConceptSelectorKey] = useState(0);
62
66
  const [selectedConcept, setSelectedConcept] = useState<ConceptSummary | undefined>();
@@ -68,15 +72,6 @@ const ObservationColumnModal: React.FC<Props> = ({
68
72
  const [extraValues, setExtraValues] = useState<ExtraValue[]>([]);
69
73
  const [answerConcept, setAnswerConcept] = useState<ConceptSummary | undefined>();
70
74
 
71
- /**
72
- * Reset form state when modal opens/closes
73
- */
74
- useEffect(() => {
75
- if (open) {
76
- setConceptSelectorKey((k) => k + 1);
77
- }
78
- }, [open]);
79
-
80
75
  /**
81
76
  * Reset form state
82
77
  */
@@ -91,6 +86,79 @@ const ObservationColumnModal: React.FC<Props> = ({
91
86
  setAnswerConcept(undefined);
92
87
  }, []);
93
88
 
89
+ /**
90
+ * Reset form state when modal opens/closes
91
+ * Also populate form when editing existing column
92
+ */
93
+ useEffect(() => {
94
+ if (open) {
95
+ setConceptSelectorKey((k) => k + 1);
96
+ if (editingColumn) {
97
+ // Populate form with existing column data
98
+ const config = editingColumn.dataDefinitionConfig || {};
99
+
100
+ // Debug: Check for required fields
101
+ if (!config.conceptUuid) {
102
+ console.warn('⚠️ Missing conceptUuid in dataDefinitionConfig');
103
+ }
104
+ if (!config.conceptName && !editingColumn.name) {
105
+ console.warn('⚠️ Missing conceptName and column name');
106
+ }
107
+ if (!config.strategy) {
108
+ console.warn('⚠️ Missing strategy in dataDefinitionConfig');
109
+ }
110
+
111
+ // Map strategy back to columnModifier
112
+ let mappedColumnModifier: ColumnModifier = 'MOST_RECENT';
113
+ const strategy = config.strategy;
114
+
115
+ if (strategy === 'ALL_VALUES') {
116
+ mappedColumnModifier = 'ANY';
117
+ } else if (strategy === 'EARLIEST') {
118
+ mappedColumnModifier = 'FIRST';
119
+ } else if (strategy === 'LATEST') {
120
+ mappedColumnModifier = 'MOST_RECENT';
121
+ } else if (typeof strategy === 'string' && strategy.startsWith('FIRST_')) {
122
+ mappedColumnModifier = 'FIRST_N';
123
+ } else if (typeof strategy === 'string' && strategy.startsWith('LAST_')) {
124
+ mappedColumnModifier = 'MOST_RECENT_N';
125
+ }
126
+
127
+ // Extract modifier count from strategy if it's FIRST_N or LAST_N
128
+ let extractedModifierCount = 1;
129
+ if (typeof strategy === 'string' && strategy.startsWith('FIRST_')) {
130
+ const match = strategy.match(/^FIRST_(\d+)$/);
131
+ if (match) extractedModifierCount = parseInt(match[1]) || 1;
132
+ } else if (typeof strategy === 'string' && strategy.startsWith('LAST_')) {
133
+ const match = strategy.match(/^LAST_(\d+)$/);
134
+ if (match) extractedModifierCount = parseInt(match[1]) || 1;
135
+ }
136
+
137
+ const conceptForSelection = editingColumn.source?.conceptUuid ? {
138
+ uuid: editingColumn.source.conceptUuid,
139
+ display: config.conceptName || editingColumn.name,
140
+ datatype: { name: editingColumn.source?.fieldType || 'TEXT' },
141
+ conceptClass: undefined, // Not loaded from saved data
142
+ } as ConceptSummary : undefined;
143
+
144
+ setSelectedConcept(conceptForSelection);
145
+ setColumnName(editingColumn.name || '');
146
+ setColumnValue(editingColumn.description || '');
147
+ setColumnModifier(config.columnModifier || mappedColumnModifier);
148
+ setModifierCount(config.modifierCount || extractedModifierCount);
149
+ setReturnDisplay(config.returnDisplay !== false);
150
+ setExtraValues(config.extraValues || []);
151
+ setAnswerConcept(config.answerConceptUuid ? {
152
+ uuid: config.answerConceptUuid,
153
+ display: config.answerConceptName || '',
154
+ } as ConceptSummary : undefined);
155
+ } else {
156
+ // Reset for new column creation
157
+ resetForm();
158
+ }
159
+ }
160
+ }, [open, editingColumn, resetForm]);
161
+
94
162
  /**
95
163
  * Handle modal close
96
164
  */
@@ -133,9 +201,9 @@ const ObservationColumnModal: React.FC<Props> = ({
133
201
  return;
134
202
  }
135
203
 
136
- // Check for duplicate column names
204
+ // Check for duplicate column names (exclude current column when editing)
137
205
  const duplicateName = existingColumns.some(col =>
138
- col.name.toLowerCase() === columnName.toLowerCase()
206
+ col.id !== editingColumn?.id && col.name.toLowerCase() === columnName.toLowerCase()
139
207
  );
140
208
  if (duplicateName) {
141
209
  // TODO: Show error notification
@@ -174,13 +242,13 @@ const ObservationColumnModal: React.FC<Props> = ({
174
242
  }
175
243
 
176
244
  const newColumn: LinelistColumnDraft = {
177
- id: `col-${now}-${selectedConcept.uuid}`,
245
+ id: editingColumn?.id || `col-${now}-${selectedConcept.uuid}`,
178
246
  name: columnName.trim(),
179
247
  description: columnValue.trim() || selectedConcept.display,
180
248
  source: {
181
249
  dataSourceUuid: 'observations',
182
250
  dataSourceName: 'Observations',
183
- table: 'obs',
251
+ table: 'observations',
184
252
  field: selectedConcept.uuid,
185
253
  fieldType: selectedConcept.datatype?.name || 'TEXT',
186
254
  conceptUuid: selectedConcept.uuid,
@@ -197,18 +265,18 @@ const ObservationColumnModal: React.FC<Props> = ({
197
265
  ...(answerConcept && { answerConceptUuid: answerConcept.uuid }),
198
266
  },
199
267
  additionInfo: {
200
- addedVia: 'VISUAL_SELECTOR',
201
- addedAt: now,
202
- orderAdded: existingColumns.length,
268
+ addedVia: editingColumn?.additionInfo?.addedVia || 'VISUAL_SELECTOR',
269
+ addedAt: editingColumn?.additionInfo?.addedAt || now,
270
+ orderAdded: editingColumn?.additionInfo?.orderAdded || existingColumns.length,
203
271
  },
204
- display: {
272
+ display: editingColumn?.display || {
205
273
  width: 150,
206
274
  align: 'left',
207
275
  sortable: true,
208
276
  filterable: true,
209
277
  format: 'coded',
210
278
  },
211
- sortOrder: existingColumns.length,
279
+ sortOrder: editingColumn?.sortOrder ?? existingColumns.length,
212
280
  repeatResolution: columnModifier === 'ANY' ? undefined : {
213
281
  strategy: repeatStrategy,
214
282
  orderBy: 'obs_datetime',
@@ -219,7 +287,7 @@ const ObservationColumnModal: React.FC<Props> = ({
219
287
 
220
288
  onAddColumn(newColumn);
221
289
  handleClose();
222
- }, [selectedConcept, columnName, columnValue, columnModifier, modifierCount, returnDisplay, extraValues, answerConcept, existingColumns, onAddColumn, handleClose]);
290
+ }, [selectedConcept, columnName, columnValue, columnModifier, modifierCount, returnDisplay, extraValues, answerConcept, existingColumns, onAddColumn, handleClose, editingColumn]);
223
291
 
224
292
  /**
225
293
  * Check if form is valid
@@ -230,11 +298,11 @@ const ObservationColumnModal: React.FC<Props> = ({
230
298
  <Modal
231
299
  open={open}
232
300
  onRequestClose={handleClose}
233
- modalHeading="Add Observation Column"
301
+ modalHeading={editingColumn ? "Edit Observation Column" : "Add Observation Column"}
234
302
  modalLabel="Data Catalogue"
235
303
  size="md"
236
304
  preventCloseOnClickOutside
237
- primaryButtonText="Add Column"
305
+ primaryButtonText={editingColumn ? "Save Changes" : "Add Column"}
238
306
  primaryButtonDisabled={!isFormValid}
239
307
  onRequestSubmit={handleSubmit}
240
308
  secondaryButtonText="Cancel"
@@ -333,25 +401,42 @@ const ObservationColumnModal: React.FC<Props> = ({
333
401
  </div>
334
402
 
335
403
  {/* Answer Concept (for coded concepts with answers) */}
336
- {selectedConcept.answers && selectedConcept.answers.length > 0 && (
337
- <Select
338
- id="observation-answer-concept"
339
- labelText="Filter by Answer (Optional)"
340
- value={answerConcept?.uuid || ''}
341
- onChange={(e) => {
342
- const answer = selectedConcept.answers?.find(a => a.uuid === e.target.value);
343
- setAnswerConcept(answer);
344
- }}
345
- >
346
- <SelectItem value="" text="All answers" />
347
- {selectedConcept.answers.map(answer => (
348
- <SelectItem
349
- key={answer.uuid}
350
- value={answer.uuid}
351
- text={answer.display}
352
- />
353
- ))}
354
- </Select>
404
+ {(selectedConcept.answers && selectedConcept.answers.length > 0 || answerConcept) && (
405
+ <div>
406
+ {answerConcept && (!selectedConcept.answers || selectedConcept.answers.length === 0) && (
407
+ <div style={{ marginBottom: '0.5rem', fontSize: '0.875rem', opacity: 0.9 }}>
408
+ <strong>Filter by Answer:</strong> {answerConcept.display || answerConcept.uuid}
409
+ <Button
410
+ kind="ghost"
411
+ size="sm"
412
+ onClick={() => setAnswerConcept(undefined)}
413
+ style={{ marginLeft: '0.5rem' }}
414
+ >
415
+ Clear
416
+ </Button>
417
+ </div>
418
+ )}
419
+ {selectedConcept.answers && selectedConcept.answers.length > 0 && (
420
+ <Select
421
+ id="observation-answer-concept"
422
+ labelText="Filter by Answer (Optional)"
423
+ value={answerConcept?.uuid || ''}
424
+ onChange={(e) => {
425
+ const answer = selectedConcept.answers?.find(a => a.uuid === e.target.value);
426
+ setAnswerConcept(answer);
427
+ }}
428
+ >
429
+ <SelectItem value="" text="All answers" />
430
+ {selectedConcept.answers.map(answer => (
431
+ <SelectItem
432
+ key={answer.uuid}
433
+ value={answer.uuid}
434
+ text={answer.display}
435
+ />
436
+ ))}
437
+ </Select>
438
+ )}
439
+ </div>
355
440
  )}
356
441
  </Stack>
357
442
  </FormGroup>
@@ -68,6 +68,7 @@ import {
68
68
  import type { LinelistReportDto } from '../../../types/linelist-types';
69
69
  import { listReportCategories, type ReportCategoryDto } from '../../../resources/report-category/report-category.api';
70
70
  import { useETLTables } from '../../../hooks/theme';
71
+ import { enhanceConfigForPreview } from '../../../utils/config-enhancer';
71
72
 
72
73
  import CohortSQLEditor from '../config/cohort-sql-editor.component';
73
74
  import ColumnSelector from '../builder/column-selector.component';
@@ -178,6 +179,38 @@ function extractTableAndFieldFromSql(sql: string): { table: string; field: strin
178
179
  return null;
179
180
  }
180
181
 
182
+ /**
183
+ * Normalize a column to ensure it has required fields (id, sortOrder)
184
+ * This ensures backward compatibility with legacy configs
185
+ */
186
+ function normalizeColumn(column: any, index: number, existingIds: Set<string>): string {
187
+ // Generate a unique ID if missing or duplicate
188
+ let columnId = column.id;
189
+
190
+ if (!columnId) {
191
+ // Use index + timestamp for uniqueness
192
+ const timestamp = Date.now().toString(36);
193
+ columnId = `col-${timestamp}-${index}`;
194
+ // If still duplicate, add random suffix
195
+ if (existingIds.has(columnId)) {
196
+ columnId = `col-${timestamp}-${index}-${Math.random().toString(36).substr(2, 5)}`;
197
+ }
198
+ } else if (existingIds.has(columnId)) {
199
+ // Duplicate ID - generate new one
200
+ const timestamp = Date.now().toString(36);
201
+ columnId = `col-${timestamp}-${index}-dup`;
202
+ }
203
+
204
+ existingIds.add(columnId);
205
+
206
+ // Ensure sortOrder is set on source if it exists
207
+ if (column && typeof column.sortOrder !== 'number') {
208
+ column.sortOrder = index;
209
+ }
210
+
211
+ return columnId;
212
+ }
213
+
181
214
  /**
182
215
  * Convert LinelistReportDto to LinelistReportDraft for editing
183
216
  */
@@ -203,6 +236,7 @@ function reportDtoToDraft(report: LinelistReportDto): LinelistReportDraft {
203
236
  // Extract columns from dataset definitions
204
237
  const columns: LinelistColumnDraft[] = [];
205
238
  const dataSet = config.dataSetDefinitions?.[0];
239
+ const existingIds = new Set<string>(); // Track IDs to ensure uniqueness
206
240
 
207
241
  if (dataSet?.columns) {
208
242
  dataSet.columns.forEach((col, idx) => {
@@ -228,21 +262,58 @@ function reportDtoToDraft(report: LinelistReportDto): LinelistReportDraft {
228
262
  'CALCULATION': 'NUMBER',
229
263
  'PERSON_ADDRESS': 'TEXT',
230
264
  'SQL': 'TEXT',
265
+ 'OBSERVATION': 'CODED',
266
+ 'ENCOUNTER_DIAGNOSIS': 'CODED',
231
267
  };
232
268
  const fieldType = fieldTypeMap[dataDefType] || 'TEXT';
233
269
 
270
+ // Set data source name based on dataDefinitionType
271
+ let dataSourceNameToUse = dataSourceName;
272
+ let tableToUse = table;
273
+ let fieldToUse = field;
274
+
275
+ // Override source info for known column types
276
+ if (dataDefType === 'OBSERVATION') {
277
+ dataSourceNameToUse = 'Observations';
278
+ tableToUse = 'observations';
279
+ fieldToUse = dataDefConfig.conceptUuid || field;
280
+ } else if (dataDefType === 'ENCOUNTER_DIAGNOSIS') {
281
+ dataSourceNameToUse = 'Encounter Diagnoses';
282
+ tableToUse = 'encounter_diagnoses';
283
+ fieldToUse = dataDefConfig.conceptUuid || '*';
284
+ } else if (dataDefType === 'PERSON_NAME') {
285
+ dataSourceNameToUse = 'Person Names';
286
+ tableToUse = 'person_name';
287
+ } else if (dataDefType === 'PERSON_ADDRESS') {
288
+ dataSourceNameToUse = 'Person Address';
289
+ tableToUse = 'person_address';
290
+ } else if (dataDefType === 'IDENTIFIER') {
291
+ dataSourceNameToUse = 'Patient Identifiers';
292
+ tableToUse = 'patient_identifier';
293
+ } else if (dataDefType === 'PERSON_ATTRIBUTE') {
294
+ dataSourceNameToUse = 'Person Attributes';
295
+ tableToUse = 'person_attribute';
296
+ }
297
+
298
+ // Normalize ID and sortOrder
299
+ const columnId = normalizeColumn(col, idx, existingIds);
300
+
234
301
  columns.push({
235
- id: `col-${idx}`,
302
+ id: columnId,
236
303
  name: col.name,
304
+ description: (col as any).description || '',
237
305
  dataDefinitionType: dataDefType,
238
306
  dataDefinitionConfig: dataDefConfig,
239
307
  config: dataDefConfig, // @deprecated
240
308
  source: {
241
- dataSourceUuid,
242
- dataSourceName,
243
- table,
244
- field,
309
+ dataSourceUuid: dataSourceUuid || (dataDefType === 'OBSERVATION' ? 'observations' : dataDefType === 'ENCOUNTER_DIAGNOSIS' ? 'encounter_diagnoses' : ''),
310
+ dataSourceName: dataSourceNameToUse,
311
+ table: tableToUse,
312
+ field: fieldToUse,
245
313
  fieldType,
314
+ conceptUuid: dataDefConfig.conceptUuid,
315
+ attributeTypeUuid: dataDefConfig.attributeTypeUuid,
316
+ identifierTypeUuid: dataDefConfig.identifierTypeUuid,
246
317
  },
247
318
  additionInfo: {
248
319
  addedVia: 'IMPORT',
@@ -254,6 +325,7 @@ function reportDtoToDraft(report: LinelistReportDto): LinelistReportDraft {
254
325
  align: 'left',
255
326
  sortable: true,
256
327
  filterable: true,
328
+ format: dataDefType === 'OBSERVATION' || dataDefType === 'ENCOUNTER_DIAGNOSIS' ? 'coded' : 'text',
257
329
  },
258
330
  sortOrder: idx,
259
331
  repeatResolution: (col as any).repeatResolution,
@@ -801,8 +873,6 @@ function BasicsPanel({
801
873
  categories,
802
874
  tables,
803
875
  }: BasicsPanelProps) {
804
- console.log('BasicsPanel render, draft.name:', draft.name, 'draft.categoryUuid:', draft.categoryUuid);
805
-
806
876
  const rowGrainOptions: Array<{ value: LinelistRowGrain; label: string; description: string }> = [
807
877
  { value: 'PATIENT', label: 'Patient', description: 'One row per patient' },
808
878
  { value: 'ENCOUNTER', label: 'Encounter', description: 'One row per encounter/visit' },
@@ -1246,7 +1316,7 @@ function PreviewPanel({ draft }: PreviewPanelProps) {
1246
1316
  {showJson && (
1247
1317
  <div className={styles.sqlPreview}>
1248
1318
  <h4>Generated Configuration</h4>
1249
- <pre className={styles.jsonPreview}>{JSON.stringify(config, null, 2)}</pre>
1319
+ <pre className={styles.jsonPreview}>{JSON.stringify(enhanceConfigForPreview(config, draft), null, 2)}</pre>
1250
1320
  </div>
1251
1321
  )}
1252
1322
  </Stack>
@@ -49,8 +49,6 @@ const LinelistReportEditorPage: React.FC<Props> = () => {
49
49
 
50
50
  getLinelistReport(reportId)
51
51
  .then((report) => {
52
- console.error('🔴 LOADED REPORT DATA:', report?.name, report);
53
- console.log('Loaded report data:', report);
54
52
  setInitialReport(report);
55
53
  setMode('edit');
56
54
  })
@@ -11,6 +11,7 @@ import { Code } from '@carbon/react/icons';
11
11
  import type { LinelistReportDraft } from '../../../types/linelist-types';
12
12
  import { draftToConfig } from '../../../types/linelist-types';
13
13
  import type { IndicatorDto } from '../../../resources/indicator/indicators.api';
14
+ import { enhanceConfigForPreview } from '../../../utils/config-enhancer';
14
15
  import styles from './json-preview.scss';
15
16
 
16
17
  type Props = {
@@ -43,7 +44,8 @@ export default function JsonPreview({ draft, className, indicators = [] }: Props
43
44
  */
44
45
  const handleCopy = useCallback(() => {
45
46
  const config = draftToConfig(draft, indicatorMap);
46
- navigator.clipboard.writeText(JSON.stringify(config, null, 2));
47
+ const enhancedConfig = enhanceConfigForPreview(config, draft);
48
+ navigator.clipboard.writeText(JSON.stringify(enhancedConfig, null, 2));
47
49
  }, [draft, indicatorMap]);
48
50
 
49
51
  /**
@@ -78,6 +80,7 @@ export default function JsonPreview({ draft, className, indicators = [] }: Props
78
80
  };
79
81
 
80
82
  const config = draftToConfig(draft, indicatorMap);
83
+ const enhancedConfig = enhanceConfigForPreview(config, draft);
81
84
 
82
85
  return (
83
86
  <div className={`${styles.preview} ${className || ''}`}>
@@ -100,7 +103,7 @@ export default function JsonPreview({ draft, className, indicators = [] }: Props
100
103
  feedback="Copied!"
101
104
  className={styles.code}
102
105
  >
103
- {JSON.stringify(config, null, 2)}
106
+ {JSON.stringify(enhancedConfig, null, 2)}
104
107
  </CodeSnippet>
105
108
 
106
109
  <div className={styles.validation}>
@@ -14,8 +14,10 @@ export function useSectionEditorState(args: {
14
14
  const { open, mode, initialSection, indicators, ageCategories } = args;
15
15
  const isEdit = mode === 'edit';
16
16
  React.useEffect(() => {
17
- console.log('[hook] MOUNT');
18
- return () => console.log('[hook] UNMOUNT');
17
+ // Component mount/unmount
18
+ return () => {
19
+ // Component unmount
20
+ };
19
21
  }, []);
20
22
 
21
23
 
@@ -196,11 +198,7 @@ export function useSectionEditorState(args: {
196
198
 
197
199
  const toggleIndicator = React.useCallback(
198
200
  (i: SectionIndicatorRef, checked: boolean) => {
199
- console.log('[hook] toggleIndicator called', { id: i.id, checked });
200
-
201
201
  setSelected((prev) => {
202
- console.log('[hook] setSelected(prev) BEFORE', prev.map((p) => p.id));
203
-
204
202
  let next;
205
203
  if (checked) {
206
204
  if (prev.some((x) => x.id === i.id)) {
@@ -214,7 +212,6 @@ export function useSectionEditorState(args: {
214
212
  next = kept.map((x, idx) => ({ ...x, sortOrder: idx + 1 }));
215
213
  }
216
214
 
217
- console.log('[hook] setSelected(prev) AFTER', next.map((p) => p.id));
218
215
  return next;
219
216
  });
220
217
 
@@ -66,6 +66,22 @@ const ConceptSelector: React.FC<Props> = ({
66
66
 
67
67
  const { loading, results, error } = useConceptSearch(conceptQuery);
68
68
 
69
+ /**
70
+ * Combine search results with the currently selected concept
71
+ * This ensures the selected concept appears in the list even if not in search results
72
+ */
73
+ const items = React.useMemo(() => {
74
+ const combined = [...results];
75
+ // If there's a selected concept and it's not already in the results, add it
76
+ if (selectedConcept) {
77
+ const exists = results.some(r => r.uuid === selectedConcept.uuid);
78
+ if (!exists) {
79
+ combined.unshift(selectedConcept); // Add at the beginning
80
+ }
81
+ }
82
+ return combined;
83
+ }, [results, selectedConcept]);
84
+
69
85
  /**
70
86
  * Handle search input change
71
87
  */
@@ -166,13 +182,14 @@ const ConceptSelector: React.FC<Props> = ({
166
182
  key={multiKey}
167
183
  id="concept-select"
168
184
  titleText="Select concept"
169
- items={results}
185
+ items={items}
170
186
  itemToString={itemToString}
171
- placeholder={results.length ? 'Select a concept…' : 'No results'}
187
+ placeholder={items.length ? 'Select a concept…' : 'No results'}
172
188
  helperText={selectedConcept ? 'Click to change selection' : 'Type at least 2 characters to search'}
173
189
  onChange={(e: any) => handleSelect((e.selectedItems ?? []) as ConceptSummary[])}
174
190
  disabled={disabled}
175
191
  selectionFeedback="top"
192
+ initialSelectedItems={selectedConcept ? [selectedConcept] : []}
176
193
  />
177
194
 
178
195
  {/* Helper Text */}
@@ -32,8 +32,6 @@ export function useETLTables(enabled: boolean) {
32
32
 
33
33
  getSchemaTables(ac.signal).then((data) => {
34
34
  // data is expected to be SchemaTable[] or something close
35
-
36
- console.log("I am here trying to see if data is loaded")
37
35
  const names = (data ?? [])
38
36
  .map((t) => toTableName(t))
39
37
  .map((x) => x.trim())
@@ -1303,6 +1303,14 @@ export function draftToConfig(
1303
1303
  let table: string;
1304
1304
  if (col.dataDefinitionType === 'CALCULATION') {
1305
1305
  table = 'calculated';
1306
+ } else if (col.dataDefinitionType === 'OBSERVATION') {
1307
+ table = 'observations';
1308
+ } else if (col.dataDefinitionType === 'ENCOUNTER_DIAGNOSIS') {
1309
+ table = 'encounter_diagnoses';
1310
+ } else if (col.dataDefinitionType === 'PERSON_ADDRESS') {
1311
+ table = 'person_address';
1312
+ } else if (col.dataDefinitionType === 'PERSON_NAME') {
1313
+ table = 'person_name';
1306
1314
  } else {
1307
1315
  // For SQL columns, derive the table from the SQL expression
1308
1316
  table = extractSourceTable(col.dataDefinitionConfig?.sql);
@@ -1317,10 +1325,20 @@ export function draftToConfig(
1317
1325
  ? 'SQL'
1318
1326
  : dsUuid.startsWith('mamba_') ? 'ETL' : 'SQL';
1319
1327
 
1328
+ // Map table names to user-friendly data source names
1329
+ const dataSourceNames: Record<string, string> = {
1330
+ 'observations': 'Observations',
1331
+ 'encounter_diagnoses': 'Encounter Diagnoses',
1332
+ 'person_address': 'Person Address',
1333
+ 'person_name': 'Person Names',
1334
+ 'calculated': 'Calculated Fields',
1335
+ 'custom_sql': 'Custom SQL',
1336
+ };
1337
+
1320
1338
  if (!sourceMap.has(dsUuid)) {
1321
1339
  sourceMap.set(dsUuid, {
1322
1340
  uuid: dsUuid,
1323
- name: dsUuid,
1341
+ name: dataSourceNames[dsUuid] || dsUuid,
1324
1342
  type: dsType as DataSourceConfig['type'],
1325
1343
  role: isPrimary ? 'PRIMARY' : 'SECONDARY',
1326
1344
  columns: {},
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Config Enhancement Utilities
3
+ *
4
+ * Utilities for enhancing configuration objects with metadata
5
+ * for debugging and visibility in JSON previews.
6
+ */
7
+
8
+ import type { LinelistReportDefinitionConfig } from '../types/linelist-types';
9
+ import type { LinelistReportDraft } from '../types/linelist-types';
10
+
11
+ /**
12
+ * Enhance config for preview by adding column metadata (id, position)
13
+ * This helps with debugging and visibility in the JSON preview
14
+ */
15
+ export function enhanceConfigForPreview(
16
+ config: LinelistReportDefinitionConfig,
17
+ draft: LinelistReportDraft
18
+ ): LinelistReportDefinitionConfig {
19
+ const enhanced = { ...config };
20
+
21
+ // Add column metadata to dataSetDefinitions columns
22
+ if (enhanced.dataSetDefinitions && enhanced.dataSetDefinitions[0]) {
23
+ const dataSetDef = enhanced.dataSetDefinitions[0];
24
+ const columnsWithMetadata = dataSetDef.columns.map((col: any, idx: number) => {
25
+ const draftCol = draft.columns.find(c => c.name === col.name);
26
+ return {
27
+ ...col,
28
+ _metadata: {
29
+ id: draftCol?.id || `col-${idx}`,
30
+ position: draftCol?.sortOrder ?? idx,
31
+ },
32
+ };
33
+ });
34
+ (enhanced as any).dataSetDefinitions[0] = {
35
+ ...dataSetDef,
36
+ columns: columnsWithMetadata,
37
+ };
38
+ }
39
+
40
+ // Also add metadata to dataSources columns
41
+ if (enhanced.dataSources) {
42
+ (enhanced as any).dataSources = enhanced.dataSources.map((ds: any) => {
43
+ if (ds.columns) {
44
+ const columnsWithMetadata: any = {};
45
+ Object.entries(ds.columns).forEach(([name, col]: [string, any]) => {
46
+ const draftCol = draft.columns.find(c => c.name === name);
47
+ columnsWithMetadata[name] = {
48
+ ...col,
49
+ _metadata: {
50
+ id: draftCol?.id,
51
+ position: draftCol?.sortOrder,
52
+ },
53
+ };
54
+ });
55
+ return { ...ds, columns: columnsWithMetadata };
56
+ }
57
+ return ds;
58
+ });
59
+ }
60
+
61
+ return enhanced;
62
+ }