@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.
- package/dist/538.js +1 -0
- package/dist/538.js.map +1 -0
- package/dist/729.js +1 -1
- package/dist/main.js +1 -1
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +26 -26
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/components/indicators/indicator-search-select.component.tsx +0 -12
- package/src/components/indicators/indicators-page.component.tsx +2 -3
- package/src/components/linelist/builder/column-category-selector.component.tsx +162 -9
- package/src/components/linelist/builder/column-category-selector.scss +148 -3
- package/src/components/linelist/builder/column-debug-panel.component.tsx +197 -0
- package/src/components/linelist/builder/column-debug-panel.scss +141 -0
- package/src/components/linelist/builder/encounter-diagnosis-column-modal.component.tsx +43 -22
- package/src/components/linelist/builder/linelist-builder-workspace.component.tsx +1 -23
- package/src/components/linelist/builder/observation-column-modal.component.tsx +125 -40
- package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +78 -8
- package/src/components/linelist/pages/linelist-report-editor-page.component.tsx +0 -2
- package/src/components/linelist/shared/json-preview.component.tsx +5 -2
- package/src/components/report-sections/hooks/useSectionEditorState.ts +4 -7
- package/src/components/shared/concept-selector.component.tsx +19 -2
- package/src/hooks/theme/useMambaTables.ts +0 -2
- package/src/types/linelist-types.ts +19 -1
- package/src/utils/config-enhancer.ts +62 -0
- package/dist/378.js +0 -1
- 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: '
|
|
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
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
18
|
-
return () =>
|
|
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={
|
|
185
|
+
items={items}
|
|
170
186
|
itemToString={itemToString}
|
|
171
|
-
placeholder={
|
|
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
|
+
}
|