@visns-studio/visns-components 5.8.3 → 5.8.5
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/package.json +1 -1
- package/src/components/crm/TableFilter.jsx +21 -11
- package/src/components/crm/generic/GenericReport.jsx +1192 -124
- package/src/components/crm/generic/styles/GenericReport.module.scss +350 -14
- package/src/components/crm/styles/DataGrid.module.scss +0 -5
- package/src/components/styles/global.css +0 -5
|
@@ -63,42 +63,129 @@ const formatRelationshipDescription = (
|
|
|
63
63
|
// Handle many-to-many relationships with pivot tables
|
|
64
64
|
if (isFirstPartOfManyToMany && joinData.secondJoin) {
|
|
65
65
|
const finalTargetTable = joinData.secondJoin.targetTable;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const sourceFormatted = formatName(sourceTable);
|
|
67
|
+
const targetFormatted = formatName(finalTargetTable);
|
|
68
|
+
|
|
69
|
+
// Add user-friendly explanation
|
|
70
|
+
return (
|
|
71
|
+
<div className={styles.relationshipWithDescription}>
|
|
72
|
+
<div>{`${sourceFormatted} ↔ ${targetFormatted}`}</div>
|
|
73
|
+
<div className={styles.relationshipDescription}>
|
|
74
|
+
Each {sourceFormatted.toLowerCase()} can have many{' '}
|
|
75
|
+
{targetFormatted.toLowerCase()}, and each{' '}
|
|
76
|
+
{targetFormatted.toLowerCase()} can belong to many{' '}
|
|
77
|
+
{sourceFormatted.toLowerCase()}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
// Handle regular joins based on join type
|
|
84
|
+
const sourceFormatted = formatName(sourceTable);
|
|
85
|
+
const targetFormatted = formatName(targetTable);
|
|
86
|
+
|
|
72
87
|
if (joinType.includes('LEFT')) {
|
|
73
|
-
|
|
88
|
+
// LEFT JOIN typically means "has many" relationship
|
|
89
|
+
return (
|
|
90
|
+
<div className={styles.relationshipWithDescription}>
|
|
91
|
+
<div>{`${sourceFormatted} → ${targetFormatted}`}</div>
|
|
92
|
+
<div className={styles.relationshipDescription}>
|
|
93
|
+
Each {sourceFormatted.toLowerCase()} can have multiple{' '}
|
|
94
|
+
{targetFormatted.toLowerCase()}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
74
98
|
} else if (joinType.includes('RIGHT')) {
|
|
75
|
-
|
|
99
|
+
// RIGHT JOIN typically means "belongs to" relationship (reversed)
|
|
100
|
+
return (
|
|
101
|
+
<div className={styles.relationshipWithDescription}>
|
|
102
|
+
<div>{`${sourceFormatted} ← ${targetFormatted}`}</div>
|
|
103
|
+
<div className={styles.relationshipDescription}>
|
|
104
|
+
Each {sourceFormatted.toLowerCase()} belongs to a{' '}
|
|
105
|
+
{targetFormatted.toLowerCase()}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
76
109
|
} else if (joinType.includes('INNER')) {
|
|
77
|
-
|
|
110
|
+
// INNER JOIN typically means a required relationship
|
|
111
|
+
return (
|
|
112
|
+
<div className={styles.relationshipWithDescription}>
|
|
113
|
+
<div>{`${sourceFormatted} ⟷ ${targetFormatted}`}</div>
|
|
114
|
+
<div className={styles.relationshipDescription}>
|
|
115
|
+
Each {sourceFormatted.toLowerCase()} must have a
|
|
116
|
+
matching {targetFormatted.toLowerCase()}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
78
120
|
}
|
|
79
121
|
|
|
80
|
-
// Default case - just show the relationship
|
|
81
|
-
return
|
|
122
|
+
// Default case - just show the relationship with a generic description
|
|
123
|
+
return (
|
|
124
|
+
<div className={styles.relationshipWithDescription}>
|
|
125
|
+
<div>{`${sourceFormatted} - ${targetFormatted}`}</div>
|
|
126
|
+
<div className={styles.relationshipDescription}>
|
|
127
|
+
{sourceFormatted} is related to {targetFormatted}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
82
131
|
}
|
|
83
132
|
|
|
84
133
|
// Extract table names from the description if no join data
|
|
85
134
|
const sourceTablePattern = /([a-z_]+)\./i;
|
|
86
135
|
const targetTablePattern = /references\s+([a-z_]+)\./i;
|
|
136
|
+
const hasManyPattern = /has many/i;
|
|
87
137
|
|
|
88
138
|
const sourceMatch = description.match(sourceTablePattern);
|
|
89
139
|
const targetMatch = description.match(targetTablePattern);
|
|
140
|
+
const isHasMany = description.match(hasManyPattern);
|
|
90
141
|
|
|
91
142
|
if (sourceMatch && targetMatch) {
|
|
92
143
|
const sourceTable = sourceMatch[1];
|
|
93
144
|
const targetTable = targetMatch[1];
|
|
145
|
+
const sourceFormatted = formatName(sourceTable);
|
|
146
|
+
const targetFormatted = formatName(targetTable);
|
|
94
147
|
|
|
95
148
|
// Check if this is a pivot table description
|
|
96
|
-
if (
|
|
97
|
-
|
|
149
|
+
if (
|
|
150
|
+
description.includes('pivot table') ||
|
|
151
|
+
description.includes('through')
|
|
152
|
+
) {
|
|
153
|
+
return (
|
|
154
|
+
<div className={styles.relationshipWithDescription}>
|
|
155
|
+
<div>{`${sourceFormatted} ↔ ${targetFormatted}`}</div>
|
|
156
|
+
<div className={styles.relationshipDescription}>
|
|
157
|
+
Each {sourceFormatted.toLowerCase()} can have many{' '}
|
|
158
|
+
{targetFormatted.toLowerCase()}, and each{' '}
|
|
159
|
+
{targetFormatted.toLowerCase()} can belong to many{' '}
|
|
160
|
+
{sourceFormatted.toLowerCase()}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if this is a "has many" relationship
|
|
167
|
+
if (isHasMany) {
|
|
168
|
+
return (
|
|
169
|
+
<div className={styles.relationshipWithDescription}>
|
|
170
|
+
<div>{`${sourceFormatted} → ${targetFormatted}`}</div>
|
|
171
|
+
<div className={styles.relationshipDescription}>
|
|
172
|
+
Each {sourceFormatted.toLowerCase()} can have multiple{' '}
|
|
173
|
+
{targetFormatted.toLowerCase()}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
98
177
|
}
|
|
99
178
|
|
|
100
|
-
// Regular relationship
|
|
101
|
-
return
|
|
179
|
+
// Regular relationship (likely "belongs to")
|
|
180
|
+
return (
|
|
181
|
+
<div className={styles.relationshipWithDescription}>
|
|
182
|
+
<div>{`${sourceFormatted} → ${targetFormatted}`}</div>
|
|
183
|
+
<div className={styles.relationshipDescription}>
|
|
184
|
+
Each {sourceFormatted.toLowerCase()} is linked to a{' '}
|
|
185
|
+
{targetFormatted.toLowerCase()}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
102
189
|
}
|
|
103
190
|
|
|
104
191
|
// Fallback to just the table name if we can't parse the relationship
|
|
@@ -132,6 +219,116 @@ const formatJsonKey = (key) => {
|
|
|
132
219
|
.join(' ');
|
|
133
220
|
};
|
|
134
221
|
|
|
222
|
+
// Smart format for boolean and status values
|
|
223
|
+
const formatSmartValue = (value, columnName, columnType) => {
|
|
224
|
+
// Skip null/undefined values
|
|
225
|
+
if (value === null || value === undefined) {
|
|
226
|
+
return '';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Convert to string for comparison
|
|
230
|
+
const strValue = String(value).toLowerCase();
|
|
231
|
+
const lowerColumnName = columnName.toLowerCase();
|
|
232
|
+
|
|
233
|
+
// Handle boolean values (0/1) in status columns
|
|
234
|
+
if (
|
|
235
|
+
(columnType === 'tinyint' ||
|
|
236
|
+
columnType === 'boolean' ||
|
|
237
|
+
columnType === 'bit') &&
|
|
238
|
+
(strValue === '0' ||
|
|
239
|
+
strValue === '1' ||
|
|
240
|
+
strValue === 'true' ||
|
|
241
|
+
strValue === 'false')
|
|
242
|
+
) {
|
|
243
|
+
const boolValue = strValue === '1' || strValue === 'true';
|
|
244
|
+
|
|
245
|
+
// Status-specific formatting
|
|
246
|
+
if (lowerColumnName.includes('status')) {
|
|
247
|
+
return (
|
|
248
|
+
<span
|
|
249
|
+
className={boolValue ? 'status-active' : 'status-inactive'}
|
|
250
|
+
>
|
|
251
|
+
{boolValue ? 'Active' : 'Inactive'}
|
|
252
|
+
</span>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Completion-specific formatting
|
|
257
|
+
if (
|
|
258
|
+
lowerColumnName.includes('complete') ||
|
|
259
|
+
lowerColumnName.includes('completed') ||
|
|
260
|
+
lowerColumnName.includes('done') ||
|
|
261
|
+
lowerColumnName.includes('finished')
|
|
262
|
+
) {
|
|
263
|
+
return (
|
|
264
|
+
<span className={boolValue ? 'boolean-yes' : 'boolean-no'}>
|
|
265
|
+
{boolValue ? 'Yes' : 'No'}
|
|
266
|
+
</span>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Enabled/disabled formatting
|
|
271
|
+
if (
|
|
272
|
+
lowerColumnName.includes('enable') ||
|
|
273
|
+
lowerColumnName.includes('enabled') ||
|
|
274
|
+
lowerColumnName.includes('disable') ||
|
|
275
|
+
lowerColumnName.includes('disabled')
|
|
276
|
+
) {
|
|
277
|
+
return (
|
|
278
|
+
<span
|
|
279
|
+
className={boolValue ? 'status-enabled' : 'status-disabled'}
|
|
280
|
+
>
|
|
281
|
+
{boolValue ? 'Enabled' : 'Disabled'}
|
|
282
|
+
</span>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Approved/rejected formatting
|
|
287
|
+
if (
|
|
288
|
+
lowerColumnName.includes('approve') ||
|
|
289
|
+
lowerColumnName.includes('approved') ||
|
|
290
|
+
lowerColumnName.includes('reject') ||
|
|
291
|
+
lowerColumnName.includes('rejected')
|
|
292
|
+
) {
|
|
293
|
+
return (
|
|
294
|
+
<span
|
|
295
|
+
className={
|
|
296
|
+
boolValue ? 'status-approved' : 'status-rejected'
|
|
297
|
+
}
|
|
298
|
+
>
|
|
299
|
+
{boolValue ? 'Approved' : 'Rejected'}
|
|
300
|
+
</span>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Verified formatting
|
|
305
|
+
if (
|
|
306
|
+
lowerColumnName.includes('verify') ||
|
|
307
|
+
lowerColumnName.includes('verified')
|
|
308
|
+
) {
|
|
309
|
+
return (
|
|
310
|
+
<span
|
|
311
|
+
className={
|
|
312
|
+
boolValue ? 'status-verified' : 'status-not-verified'
|
|
313
|
+
}
|
|
314
|
+
>
|
|
315
|
+
{boolValue ? 'Verified' : 'Not Verified'}
|
|
316
|
+
</span>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Default boolean formatting
|
|
321
|
+
return (
|
|
322
|
+
<span className={boolValue ? 'boolean-yes' : 'boolean-no'}>
|
|
323
|
+
{boolValue ? 'Yes' : 'No'}
|
|
324
|
+
</span>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Return the original value if no smart formatting applies
|
|
329
|
+
return value;
|
|
330
|
+
};
|
|
331
|
+
|
|
135
332
|
// Transform JSON object to have human-readable keys
|
|
136
333
|
const transformJsonForDisplay = (json) => {
|
|
137
334
|
if (!json || typeof json !== 'object') return json;
|
|
@@ -160,6 +357,83 @@ const transformJsonForDisplay = (json) => {
|
|
|
160
357
|
return result;
|
|
161
358
|
};
|
|
162
359
|
|
|
360
|
+
// Render JSON data in a human-friendly format
|
|
361
|
+
const renderJsonData = (jsonData) => {
|
|
362
|
+
if (!jsonData || typeof jsonData !== 'object') return String(jsonData);
|
|
363
|
+
|
|
364
|
+
// Human-friendly format
|
|
365
|
+
// For simple objects with just a few key-value pairs, render as a list
|
|
366
|
+
const entries = Object.entries(jsonData);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className={styles.humanJsonContainer}>
|
|
370
|
+
{entries.map(([key, value], index) => {
|
|
371
|
+
// Format the key to be more readable
|
|
372
|
+
const formattedKey = formatJsonKey(key);
|
|
373
|
+
|
|
374
|
+
// Handle different value types
|
|
375
|
+
let displayValue;
|
|
376
|
+
|
|
377
|
+
if (value === null) {
|
|
378
|
+
displayValue = 'None';
|
|
379
|
+
} else if (typeof value === 'boolean') {
|
|
380
|
+
displayValue = value ? (
|
|
381
|
+
<span style={{ color: '#16a34a' }}>Yes</span>
|
|
382
|
+
) : (
|
|
383
|
+
<span style={{ color: '#dc2626' }}>No</span>
|
|
384
|
+
);
|
|
385
|
+
} else if (typeof value === 'number') {
|
|
386
|
+
displayValue = value;
|
|
387
|
+
} else if (typeof value === 'string') {
|
|
388
|
+
// Check if it's a URL
|
|
389
|
+
if (value.match(/^https?:\/\//i)) {
|
|
390
|
+
displayValue = (
|
|
391
|
+
<a
|
|
392
|
+
href={value}
|
|
393
|
+
target="_blank"
|
|
394
|
+
rel="noopener noreferrer"
|
|
395
|
+
style={{
|
|
396
|
+
color: '#2563eb',
|
|
397
|
+
textDecoration: 'underline',
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
{value}
|
|
401
|
+
</a>
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
displayValue = value;
|
|
405
|
+
}
|
|
406
|
+
} else if (Array.isArray(value)) {
|
|
407
|
+
// For arrays, join the values with commas
|
|
408
|
+
displayValue = value
|
|
409
|
+
.map((item) =>
|
|
410
|
+
typeof item === 'object' && item !== null
|
|
411
|
+
? '[Object]'
|
|
412
|
+
: String(item)
|
|
413
|
+
)
|
|
414
|
+
.join(', ');
|
|
415
|
+
} else if (typeof value === 'object') {
|
|
416
|
+
// For nested objects, show a placeholder
|
|
417
|
+
displayValue = '[Object]';
|
|
418
|
+
} else {
|
|
419
|
+
displayValue = String(value);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<div key={index} className={styles.humanJsonRow}>
|
|
424
|
+
<span className={styles.humanJsonKey}>
|
|
425
|
+
{formattedKey + ': '}
|
|
426
|
+
</span>
|
|
427
|
+
<span className={styles.humanJsonValue}>
|
|
428
|
+
{displayValue}
|
|
429
|
+
</span>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
})}
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
};
|
|
436
|
+
|
|
163
437
|
// Check if a value is JSON
|
|
164
438
|
const isJsonValue = (value) => {
|
|
165
439
|
if (!value) return false;
|
|
@@ -569,6 +843,12 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
569
843
|
const [loadingJsonKeys, setLoadingJsonKeys] = useState({});
|
|
570
844
|
const [showJsonKeySelector, setShowJsonKeySelector] = useState({});
|
|
571
845
|
|
|
846
|
+
// State for suggested filters
|
|
847
|
+
const [suggestedFilters, setSuggestedFilters] = useState([]);
|
|
848
|
+
const [isGeneratingSuggestions, setIsGeneratingSuggestions] =
|
|
849
|
+
useState(false);
|
|
850
|
+
const [showSuggestedFilters, setShowSuggestedFilters] = useState(false);
|
|
851
|
+
|
|
572
852
|
// Extract additional settings with defaults
|
|
573
853
|
const {
|
|
574
854
|
reportsUrl = '/ajax/reportBuilder/reports',
|
|
@@ -591,6 +871,13 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
591
871
|
}
|
|
592
872
|
}, [selectedTable]);
|
|
593
873
|
|
|
874
|
+
// Generate suggested filters when table columns are loaded or joins change
|
|
875
|
+
useEffect(() => {
|
|
876
|
+
if (selectedTable && tableColumns.length > 0) {
|
|
877
|
+
generateSuggestedFilters();
|
|
878
|
+
}
|
|
879
|
+
}, [selectedTable, tableColumns, joins, jsonFieldKeys]);
|
|
880
|
+
|
|
594
881
|
// Update available tables for joins
|
|
595
882
|
useEffect(() => {
|
|
596
883
|
if (tables.length > 0 && selectedTable) {
|
|
@@ -656,9 +943,11 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
656
943
|
const tablesData = res.data?.data || res.data;
|
|
657
944
|
|
|
658
945
|
if (Array.isArray(tablesData)) {
|
|
659
|
-
// Filter out the hidden tables
|
|
946
|
+
// Filter out the hidden tables and pivot tables (tables with underscore in name)
|
|
660
947
|
const filteredTables = tablesData.filter(
|
|
661
|
-
(table) =>
|
|
948
|
+
(table) =>
|
|
949
|
+
!hiddenTables.includes(table.name) &&
|
|
950
|
+
!table.name.includes('_')
|
|
662
951
|
);
|
|
663
952
|
setTables(filteredTables);
|
|
664
953
|
} else {
|
|
@@ -706,10 +995,27 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
706
995
|
});
|
|
707
996
|
|
|
708
997
|
if (res.data?.success && res.data?.data?.suggestedJoins) {
|
|
998
|
+
// Filter out any relationships involving safebook/safebooks tables
|
|
999
|
+
const filteredJoins = res.data.data.suggestedJoins.filter(
|
|
1000
|
+
(join) => {
|
|
1001
|
+
// Check if either source or target table contains "safebook"
|
|
1002
|
+
return !(
|
|
1003
|
+
(join.sourceTable &&
|
|
1004
|
+
join.sourceTable
|
|
1005
|
+
.toLowerCase()
|
|
1006
|
+
.includes('safebook')) ||
|
|
1007
|
+
(join.targetTable &&
|
|
1008
|
+
join.targetTable
|
|
1009
|
+
.toLowerCase()
|
|
1010
|
+
.includes('safebook'))
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
|
|
709
1015
|
// Update suggested joins for this specific table
|
|
710
1016
|
setSuggestedJoins((prev) => ({
|
|
711
1017
|
...prev,
|
|
712
|
-
[tableKey]:
|
|
1018
|
+
[tableKey]: filteredJoins,
|
|
713
1019
|
}));
|
|
714
1020
|
} else {
|
|
715
1021
|
// Set empty array for this table
|
|
@@ -1187,6 +1493,55 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1187
1493
|
setShowFilteringSection(true);
|
|
1188
1494
|
};
|
|
1189
1495
|
|
|
1496
|
+
// Apply a suggested filter
|
|
1497
|
+
const applySuggestedFilter = (suggestedFilter) => {
|
|
1498
|
+
// Create a deep copy of the current filter criteria
|
|
1499
|
+
const updatedFilterCriteria = JSON.parse(
|
|
1500
|
+
JSON.stringify(filterCriteria)
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
// Use the first group or create one if none exists
|
|
1504
|
+
if (updatedFilterCriteria.groups.length === 0) {
|
|
1505
|
+
updatedFilterCriteria.groups.push({
|
|
1506
|
+
operator: 'AND',
|
|
1507
|
+
filters: [],
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Add the suggested filter to the first group
|
|
1512
|
+
updatedFilterCriteria.groups[0].filters.push(suggestedFilter.filter);
|
|
1513
|
+
|
|
1514
|
+
// Update the filter criteria
|
|
1515
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
1516
|
+
|
|
1517
|
+
// Show the filtering section
|
|
1518
|
+
setShowFilteringSection(true);
|
|
1519
|
+
|
|
1520
|
+
// If it's a JSON field, automatically show the JSON key selector
|
|
1521
|
+
if (suggestedFilter.filter.jsonKey) {
|
|
1522
|
+
const groupIndex = 0;
|
|
1523
|
+
const filterIndex =
|
|
1524
|
+
updatedFilterCriteria.groups[0].filters.length - 1;
|
|
1525
|
+
|
|
1526
|
+
// Fetch JSON keys if needed
|
|
1527
|
+
fetchJsonFieldKeys(
|
|
1528
|
+
suggestedFilter.filter.table,
|
|
1529
|
+
suggestedFilter.filter.column
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1532
|
+
// Show the JSON key selector after a short delay to ensure the filter is rendered
|
|
1533
|
+
setTimeout(() => {
|
|
1534
|
+
setShowJsonKeySelector((prev) => ({
|
|
1535
|
+
...prev,
|
|
1536
|
+
[`${groupIndex}-${filterIndex}`]: true,
|
|
1537
|
+
}));
|
|
1538
|
+
}, 100);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Show a success toast
|
|
1542
|
+
toast.success(`Added filter: ${suggestedFilter.name}`);
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1190
1545
|
// Add a new filter group
|
|
1191
1546
|
const handleAddFilterGroup = () => {
|
|
1192
1547
|
// Create a deep copy of the current filter criteria
|
|
@@ -1298,6 +1653,494 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1298
1653
|
return false;
|
|
1299
1654
|
};
|
|
1300
1655
|
|
|
1656
|
+
// Generate suggested filters based on table columns and relationships
|
|
1657
|
+
const generateSuggestedFilters = () => {
|
|
1658
|
+
if (!selectedTable || tableColumns.length === 0) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
setIsGeneratingSuggestions(true);
|
|
1663
|
+
|
|
1664
|
+
try {
|
|
1665
|
+
const suggestions = [];
|
|
1666
|
+
|
|
1667
|
+
// 1. Table-specific suggestions based on common columns
|
|
1668
|
+
|
|
1669
|
+
// 1.1 Find date columns for date-based filters
|
|
1670
|
+
const dateColumns = tableColumns.filter(
|
|
1671
|
+
(col) =>
|
|
1672
|
+
col.type &&
|
|
1673
|
+
['date', 'datetime', 'timestamp'].includes(
|
|
1674
|
+
col.type.toLowerCase()
|
|
1675
|
+
)
|
|
1676
|
+
);
|
|
1677
|
+
|
|
1678
|
+
// Add date-based filters
|
|
1679
|
+
dateColumns.forEach((col) => {
|
|
1680
|
+
// Last 30 days filter for date columns
|
|
1681
|
+
if (
|
|
1682
|
+
[
|
|
1683
|
+
'created_at',
|
|
1684
|
+
'updated_at',
|
|
1685
|
+
'date',
|
|
1686
|
+
'start_date',
|
|
1687
|
+
'end_date',
|
|
1688
|
+
].includes(col.name)
|
|
1689
|
+
) {
|
|
1690
|
+
const today = new Date();
|
|
1691
|
+
const thirtyDaysAgo = new Date();
|
|
1692
|
+
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
1693
|
+
|
|
1694
|
+
suggestions.push({
|
|
1695
|
+
category: 'Date Filters',
|
|
1696
|
+
name: `${formatName(col.name)} in last 30 days`,
|
|
1697
|
+
description: `Filter records where ${formatName(
|
|
1698
|
+
col.name
|
|
1699
|
+
)} is within the last 30 days`,
|
|
1700
|
+
filter: {
|
|
1701
|
+
table: selectedTable,
|
|
1702
|
+
column: col.name,
|
|
1703
|
+
operator: '>=',
|
|
1704
|
+
value: thirtyDaysAgo.toISOString().split('T')[0], // YYYY-MM-DD format
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// Current month filter
|
|
1709
|
+
const firstDayOfMonth = new Date(
|
|
1710
|
+
today.getFullYear(),
|
|
1711
|
+
today.getMonth(),
|
|
1712
|
+
1
|
|
1713
|
+
);
|
|
1714
|
+
suggestions.push({
|
|
1715
|
+
category: 'Date Filters',
|
|
1716
|
+
name: `${formatName(col.name)} this month`,
|
|
1717
|
+
description: `Filter records where ${formatName(
|
|
1718
|
+
col.name
|
|
1719
|
+
)} is in the current month`,
|
|
1720
|
+
filter: {
|
|
1721
|
+
table: selectedTable,
|
|
1722
|
+
column: col.name,
|
|
1723
|
+
operator: '>=',
|
|
1724
|
+
value: firstDayOfMonth.toISOString().split('T')[0], // YYYY-MM-DD format
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
// 1.2 Find status columns for active/inactive filters
|
|
1731
|
+
const statusColumns = tableColumns.filter((col) =>
|
|
1732
|
+
[
|
|
1733
|
+
'status',
|
|
1734
|
+
'active',
|
|
1735
|
+
'enabled',
|
|
1736
|
+
'is_active',
|
|
1737
|
+
'is_enabled',
|
|
1738
|
+
].includes(col.name.toLowerCase())
|
|
1739
|
+
);
|
|
1740
|
+
|
|
1741
|
+
// Add active/inactive filters
|
|
1742
|
+
statusColumns.forEach((col) => {
|
|
1743
|
+
suggestions.push({
|
|
1744
|
+
category: 'Status Filters',
|
|
1745
|
+
name: `Only Active ${formatName(selectedTable)}`,
|
|
1746
|
+
description: `Filter to show only active ${formatName(
|
|
1747
|
+
selectedTable
|
|
1748
|
+
)}`,
|
|
1749
|
+
filter: {
|
|
1750
|
+
table: selectedTable,
|
|
1751
|
+
column: col.name,
|
|
1752
|
+
operator: '=',
|
|
1753
|
+
value: '1',
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
suggestions.push({
|
|
1758
|
+
category: 'Status Filters',
|
|
1759
|
+
name: `Only Inactive ${formatName(selectedTable)}`,
|
|
1760
|
+
description: `Filter to show only inactive ${formatName(
|
|
1761
|
+
selectedTable
|
|
1762
|
+
)}`,
|
|
1763
|
+
filter: {
|
|
1764
|
+
table: selectedTable,
|
|
1765
|
+
column: col.name,
|
|
1766
|
+
operator: '=',
|
|
1767
|
+
value: '0',
|
|
1768
|
+
},
|
|
1769
|
+
});
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
// 1.3 Find JSON columns and suggest filters for common keys
|
|
1773
|
+
const jsonColumns = tableColumns.filter(
|
|
1774
|
+
(col) => col.type && col.type.toLowerCase() === 'json'
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
// For each JSON column, check if we have keys and suggest filters for the most common ones
|
|
1778
|
+
jsonColumns.forEach((col) => {
|
|
1779
|
+
const cacheKey = `${selectedTable}.${col.name}`;
|
|
1780
|
+
const keys = jsonFieldKeys[cacheKey];
|
|
1781
|
+
|
|
1782
|
+
if (keys && keys.length > 0) {
|
|
1783
|
+
// Sort by percentage (most common first)
|
|
1784
|
+
const sortedKeys = [...keys].sort(
|
|
1785
|
+
(a, b) => b.percentage - a.percentage
|
|
1786
|
+
);
|
|
1787
|
+
|
|
1788
|
+
// Take top 3 keys with high usage
|
|
1789
|
+
const topKeys = sortedKeys
|
|
1790
|
+
.slice(0, 3)
|
|
1791
|
+
.filter((k) => k.percentage > 30);
|
|
1792
|
+
|
|
1793
|
+
topKeys.forEach((keyInfo) => {
|
|
1794
|
+
// Create a suggested filter for this JSON key
|
|
1795
|
+
suggestions.push({
|
|
1796
|
+
category: 'JSON Field Filters',
|
|
1797
|
+
name: `Filter by ${formatJsonKey(keyInfo.key)}`,
|
|
1798
|
+
description: `Filter records where JSON field "${formatName(
|
|
1799
|
+
col.name
|
|
1800
|
+
)}" has "${keyInfo.key}" with a specific value`,
|
|
1801
|
+
filter: {
|
|
1802
|
+
table: selectedTable,
|
|
1803
|
+
column: col.name,
|
|
1804
|
+
operator: '=',
|
|
1805
|
+
jsonKey: keyInfo.key,
|
|
1806
|
+
value: '',
|
|
1807
|
+
},
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
// 1.4 Add "ID equals" filter for primary key
|
|
1814
|
+
const idColumn = tableColumns.find((col) => col.name === 'id');
|
|
1815
|
+
if (idColumn) {
|
|
1816
|
+
suggestions.push({
|
|
1817
|
+
category: 'ID Filters',
|
|
1818
|
+
name: `Find by ID`,
|
|
1819
|
+
description: `Find a specific ${formatName(
|
|
1820
|
+
selectedTable
|
|
1821
|
+
)} by ID`,
|
|
1822
|
+
filter: {
|
|
1823
|
+
table: selectedTable,
|
|
1824
|
+
column: 'id',
|
|
1825
|
+
operator: '=',
|
|
1826
|
+
value: '',
|
|
1827
|
+
},
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// 1.5 Add common name/title/description filters
|
|
1832
|
+
const nameColumns = tableColumns.filter((col) =>
|
|
1833
|
+
['name', 'title', 'label', 'description'].includes(
|
|
1834
|
+
col.name.toLowerCase()
|
|
1835
|
+
)
|
|
1836
|
+
);
|
|
1837
|
+
|
|
1838
|
+
nameColumns.forEach((col) => {
|
|
1839
|
+
suggestions.push({
|
|
1840
|
+
category: 'Text Filters',
|
|
1841
|
+
name: `Search by ${formatName(col.name)}`,
|
|
1842
|
+
description: `Filter records where ${formatName(
|
|
1843
|
+
col.name
|
|
1844
|
+
)} contains specific text`,
|
|
1845
|
+
filter: {
|
|
1846
|
+
table: selectedTable,
|
|
1847
|
+
column: col.name,
|
|
1848
|
+
operator: 'LIKE',
|
|
1849
|
+
value: '',
|
|
1850
|
+
},
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
// 2. Relationship-specific suggestions
|
|
1855
|
+
if (joins.length > 0) {
|
|
1856
|
+
// For each join, add relevant filter suggestions
|
|
1857
|
+
joins.forEach((join) => {
|
|
1858
|
+
if (!join.targetTable || !join.availableColumns) return;
|
|
1859
|
+
|
|
1860
|
+
// 2.1 Find ID columns in the joined table for relationship filtering
|
|
1861
|
+
const joinedIdColumn = join.availableColumns.find(
|
|
1862
|
+
(col) => col.name === 'id'
|
|
1863
|
+
);
|
|
1864
|
+
if (joinedIdColumn) {
|
|
1865
|
+
suggestions.push({
|
|
1866
|
+
category: `${formatName(join.targetTable)} Filters`,
|
|
1867
|
+
name: `Filter by ${formatName(
|
|
1868
|
+
join.targetTable
|
|
1869
|
+
)} ID`,
|
|
1870
|
+
description: `Filter records related to a specific ${formatName(
|
|
1871
|
+
join.targetTable
|
|
1872
|
+
)}`,
|
|
1873
|
+
filter: {
|
|
1874
|
+
table: join.targetTable,
|
|
1875
|
+
column: 'id',
|
|
1876
|
+
operator: '=',
|
|
1877
|
+
value: '',
|
|
1878
|
+
},
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// 2.2 Find status columns in the joined table
|
|
1883
|
+
const joinedStatusColumns = join.availableColumns.filter(
|
|
1884
|
+
(col) =>
|
|
1885
|
+
[
|
|
1886
|
+
'status',
|
|
1887
|
+
'active',
|
|
1888
|
+
'enabled',
|
|
1889
|
+
'is_active',
|
|
1890
|
+
'is_enabled',
|
|
1891
|
+
].includes(col.name.toLowerCase())
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
joinedStatusColumns.forEach((col) => {
|
|
1895
|
+
suggestions.push({
|
|
1896
|
+
category: `${formatName(join.targetTable)} Filters`,
|
|
1897
|
+
name: `Only Active ${formatName(join.targetTable)}`,
|
|
1898
|
+
description: `Filter to show only records with active ${formatName(
|
|
1899
|
+
join.targetTable
|
|
1900
|
+
)}`,
|
|
1901
|
+
filter: {
|
|
1902
|
+
table: join.targetTable,
|
|
1903
|
+
column: col.name,
|
|
1904
|
+
operator: '=',
|
|
1905
|
+
value: '1',
|
|
1906
|
+
},
|
|
1907
|
+
});
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
// 2.3 Find name columns in the joined table
|
|
1911
|
+
const joinedNameColumns = join.availableColumns.filter(
|
|
1912
|
+
(col) =>
|
|
1913
|
+
['name', 'title', 'label'].includes(
|
|
1914
|
+
col.name.toLowerCase()
|
|
1915
|
+
)
|
|
1916
|
+
);
|
|
1917
|
+
|
|
1918
|
+
joinedNameColumns.forEach((col) => {
|
|
1919
|
+
suggestions.push({
|
|
1920
|
+
category: `${formatName(join.targetTable)} Filters`,
|
|
1921
|
+
name: `Search by ${formatName(
|
|
1922
|
+
join.targetTable
|
|
1923
|
+
)} ${formatName(col.name)}`,
|
|
1924
|
+
description: `Filter records where related ${formatName(
|
|
1925
|
+
join.targetTable
|
|
1926
|
+
)} ${formatName(col.name)} contains specific text`,
|
|
1927
|
+
filter: {
|
|
1928
|
+
table: join.targetTable,
|
|
1929
|
+
column: col.name,
|
|
1930
|
+
operator: 'LIKE',
|
|
1931
|
+
value: '',
|
|
1932
|
+
},
|
|
1933
|
+
});
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
// 2.4 Find date columns in the joined table
|
|
1937
|
+
const joinedDateColumns = join.availableColumns.filter(
|
|
1938
|
+
(col) =>
|
|
1939
|
+
col.type &&
|
|
1940
|
+
['date', 'datetime', 'timestamp'].includes(
|
|
1941
|
+
col.type.toLowerCase()
|
|
1942
|
+
) &&
|
|
1943
|
+
['created_at', 'updated_at', 'date'].includes(
|
|
1944
|
+
col.name
|
|
1945
|
+
)
|
|
1946
|
+
);
|
|
1947
|
+
|
|
1948
|
+
joinedDateColumns.forEach((col) => {
|
|
1949
|
+
const today = new Date();
|
|
1950
|
+
const thirtyDaysAgo = new Date();
|
|
1951
|
+
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
1952
|
+
|
|
1953
|
+
suggestions.push({
|
|
1954
|
+
category: `${formatName(join.targetTable)} Filters`,
|
|
1955
|
+
name: `${formatName(join.targetTable)} ${formatName(
|
|
1956
|
+
col.name
|
|
1957
|
+
)} in last 30 days`,
|
|
1958
|
+
description: `Filter records where related ${formatName(
|
|
1959
|
+
join.targetTable
|
|
1960
|
+
)} ${formatName(
|
|
1961
|
+
col.name
|
|
1962
|
+
)} is within the last 30 days`,
|
|
1963
|
+
filter: {
|
|
1964
|
+
table: join.targetTable,
|
|
1965
|
+
column: col.name,
|
|
1966
|
+
operator: '>=',
|
|
1967
|
+
value: thirtyDaysAgo
|
|
1968
|
+
.toISOString()
|
|
1969
|
+
.split('T')[0],
|
|
1970
|
+
},
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// 3. Table-specific contextual suggestions based on common table names
|
|
1977
|
+
|
|
1978
|
+
// 3.1 For "users" table
|
|
1979
|
+
if (
|
|
1980
|
+
selectedTable.toLowerCase() === 'users' ||
|
|
1981
|
+
selectedTable.toLowerCase() === 'user'
|
|
1982
|
+
) {
|
|
1983
|
+
// Check for email column
|
|
1984
|
+
const emailColumn = tableColumns.find(
|
|
1985
|
+
(col) => col.name.toLowerCase() === 'email'
|
|
1986
|
+
);
|
|
1987
|
+
if (emailColumn) {
|
|
1988
|
+
suggestions.push({
|
|
1989
|
+
category: 'User Filters',
|
|
1990
|
+
name: 'Filter by Email Domain',
|
|
1991
|
+
description:
|
|
1992
|
+
'Filter users by their email domain (e.g., @example.com)',
|
|
1993
|
+
filter: {
|
|
1994
|
+
table: selectedTable,
|
|
1995
|
+
column: 'email',
|
|
1996
|
+
operator: 'LIKE',
|
|
1997
|
+
value: '@',
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Check for role column
|
|
2003
|
+
const roleColumn = tableColumns.find((col) =>
|
|
2004
|
+
['role', 'role_id', 'user_role', 'user_type'].includes(
|
|
2005
|
+
col.name.toLowerCase()
|
|
2006
|
+
)
|
|
2007
|
+
);
|
|
2008
|
+
if (roleColumn) {
|
|
2009
|
+
suggestions.push({
|
|
2010
|
+
category: 'User Filters',
|
|
2011
|
+
name: 'Filter by User Role',
|
|
2012
|
+
description: 'Filter users by their role or type',
|
|
2013
|
+
filter: {
|
|
2014
|
+
table: selectedTable,
|
|
2015
|
+
column: roleColumn.name,
|
|
2016
|
+
operator: '=',
|
|
2017
|
+
value: '',
|
|
2018
|
+
},
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// 3.2 For "orders" or "invoices" table
|
|
2024
|
+
if (
|
|
2025
|
+
['orders', 'order', 'invoices', 'invoice'].includes(
|
|
2026
|
+
selectedTable.toLowerCase()
|
|
2027
|
+
)
|
|
2028
|
+
) {
|
|
2029
|
+
// Check for total/amount column
|
|
2030
|
+
const amountColumn = tableColumns.find((col) =>
|
|
2031
|
+
[
|
|
2032
|
+
'total',
|
|
2033
|
+
'amount',
|
|
2034
|
+
'price',
|
|
2035
|
+
'total_amount',
|
|
2036
|
+
'invoice_total',
|
|
2037
|
+
].includes(col.name.toLowerCase())
|
|
2038
|
+
);
|
|
2039
|
+
if (amountColumn) {
|
|
2040
|
+
suggestions.push({
|
|
2041
|
+
category: 'Order Filters',
|
|
2042
|
+
name: 'High Value Orders',
|
|
2043
|
+
description:
|
|
2044
|
+
'Filter orders with high value (amount greater than 1000)',
|
|
2045
|
+
filter: {
|
|
2046
|
+
table: selectedTable,
|
|
2047
|
+
column: amountColumn.name,
|
|
2048
|
+
operator: '>',
|
|
2049
|
+
value: '1000',
|
|
2050
|
+
},
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// Check for status column
|
|
2055
|
+
const statusColumn = tableColumns.find((col) =>
|
|
2056
|
+
['status', 'order_status', 'invoice_status'].includes(
|
|
2057
|
+
col.name.toLowerCase()
|
|
2058
|
+
)
|
|
2059
|
+
);
|
|
2060
|
+
if (statusColumn) {
|
|
2061
|
+
suggestions.push({
|
|
2062
|
+
category: 'Order Filters',
|
|
2063
|
+
name: 'Completed Orders',
|
|
2064
|
+
description: 'Filter orders that have been completed',
|
|
2065
|
+
filter: {
|
|
2066
|
+
table: selectedTable,
|
|
2067
|
+
column: statusColumn.name,
|
|
2068
|
+
operator: '=',
|
|
2069
|
+
value: 'completed',
|
|
2070
|
+
},
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
suggestions.push({
|
|
2074
|
+
category: 'Order Filters',
|
|
2075
|
+
name: 'Pending Orders',
|
|
2076
|
+
description: 'Filter orders that are pending',
|
|
2077
|
+
filter: {
|
|
2078
|
+
table: selectedTable,
|
|
2079
|
+
column: statusColumn.name,
|
|
2080
|
+
operator: '=',
|
|
2081
|
+
value: 'pending',
|
|
2082
|
+
},
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// 3.3 For "products" table
|
|
2088
|
+
if (['products', 'product'].includes(selectedTable.toLowerCase())) {
|
|
2089
|
+
// Check for price column
|
|
2090
|
+
const priceColumn = tableColumns.find((col) =>
|
|
2091
|
+
['price', 'product_price', 'unit_price'].includes(
|
|
2092
|
+
col.name.toLowerCase()
|
|
2093
|
+
)
|
|
2094
|
+
);
|
|
2095
|
+
if (priceColumn) {
|
|
2096
|
+
suggestions.push({
|
|
2097
|
+
category: 'Product Filters',
|
|
2098
|
+
name: 'Price Range Filter',
|
|
2099
|
+
description:
|
|
2100
|
+
'Filter products within a specific price range',
|
|
2101
|
+
filter: {
|
|
2102
|
+
table: selectedTable,
|
|
2103
|
+
column: priceColumn.name,
|
|
2104
|
+
operator: 'BETWEEN',
|
|
2105
|
+
value: '10,100',
|
|
2106
|
+
},
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// Check for stock/inventory column
|
|
2111
|
+
const stockColumn = tableColumns.find((col) =>
|
|
2112
|
+
[
|
|
2113
|
+
'stock',
|
|
2114
|
+
'inventory',
|
|
2115
|
+
'quantity',
|
|
2116
|
+
'stock_quantity',
|
|
2117
|
+
].includes(col.name.toLowerCase())
|
|
2118
|
+
);
|
|
2119
|
+
if (stockColumn) {
|
|
2120
|
+
suggestions.push({
|
|
2121
|
+
category: 'Product Filters',
|
|
2122
|
+
name: 'Low Stock Products',
|
|
2123
|
+
description:
|
|
2124
|
+
'Filter products with low stock (less than 10)',
|
|
2125
|
+
filter: {
|
|
2126
|
+
table: selectedTable,
|
|
2127
|
+
column: stockColumn.name,
|
|
2128
|
+
operator: '<',
|
|
2129
|
+
value: '10',
|
|
2130
|
+
},
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// Update state with suggestions
|
|
2136
|
+
setSuggestedFilters(suggestions);
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
console.error('Error generating suggested filters:', error);
|
|
2139
|
+
} finally {
|
|
2140
|
+
setIsGeneratingSuggestions(false);
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
|
|
1301
2144
|
// Toggle JSON key selector visibility
|
|
1302
2145
|
const toggleJsonKeySelector = (groupIndex, filterIndex) => {
|
|
1303
2146
|
const key = `${groupIndex}-${filterIndex}`;
|
|
@@ -1351,11 +2194,25 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1351
2194
|
if (!filter.jsonKey) {
|
|
1352
2195
|
filter.jsonKey = '';
|
|
1353
2196
|
}
|
|
2197
|
+
|
|
2198
|
+
// Automatically show the JSON key selector for this filter
|
|
2199
|
+
const selectorKey = `${groupIndex}-${filterIndex}`;
|
|
2200
|
+
setShowJsonKeySelector((prev) => ({
|
|
2201
|
+
...prev,
|
|
2202
|
+
[selectorKey]: true,
|
|
2203
|
+
}));
|
|
1354
2204
|
} else {
|
|
1355
2205
|
// If it's not a JSON field, remove the jsonKey field if it exists
|
|
1356
2206
|
if (filter.jsonKey) {
|
|
1357
2207
|
delete filter.jsonKey;
|
|
1358
2208
|
}
|
|
2209
|
+
|
|
2210
|
+
// Hide the JSON key selector if it was shown
|
|
2211
|
+
const selectorKey = `${groupIndex}-${filterIndex}`;
|
|
2212
|
+
setShowJsonKeySelector((prev) => ({
|
|
2213
|
+
...prev,
|
|
2214
|
+
[selectorKey]: false,
|
|
2215
|
+
}));
|
|
1359
2216
|
}
|
|
1360
2217
|
}
|
|
1361
2218
|
}
|
|
@@ -2397,12 +3254,23 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
2397
3254
|
return isJsonValue(row[key]);
|
|
2398
3255
|
});
|
|
2399
3256
|
|
|
3257
|
+
// Get column type from the data
|
|
3258
|
+
const columnType =
|
|
3259
|
+
firstRow[key] !== null &&
|
|
3260
|
+
firstRow[key] !== undefined
|
|
3261
|
+
? typeof firstRow[key] === 'number' &&
|
|
3262
|
+
Number.isInteger(firstRow[key]) &&
|
|
3263
|
+
(firstRow[key] === 0 || firstRow[key] === 1)
|
|
3264
|
+
? 'tinyint' // Infer tinyint for 0/1 values
|
|
3265
|
+
: typeof firstRow[key]
|
|
3266
|
+
: '';
|
|
3267
|
+
|
|
2400
3268
|
return {
|
|
2401
3269
|
name: key,
|
|
2402
3270
|
header: key,
|
|
2403
3271
|
defaultFlex: 1,
|
|
2404
3272
|
minWidth: hasJsonValues ? 250 : 120,
|
|
2405
|
-
// Add a custom renderer for JSON values
|
|
3273
|
+
// Add a custom renderer for JSON values and smart formatting
|
|
2406
3274
|
render: ({ value }) => {
|
|
2407
3275
|
if (isJsonValue(value)) {
|
|
2408
3276
|
try {
|
|
@@ -2424,81 +3292,15 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
2424
3292
|
const transformedJson =
|
|
2425
3293
|
transformJsonForDisplay(jsonValue);
|
|
2426
3294
|
|
|
2427
|
-
//
|
|
2428
|
-
|
|
2429
|
-
transformedJson,
|
|
2430
|
-
null,
|
|
2431
|
-
2
|
|
2432
|
-
);
|
|
2433
|
-
|
|
2434
|
-
// Truncate if too long
|
|
2435
|
-
const isTruncated =
|
|
2436
|
-
formattedJson.length > 300;
|
|
2437
|
-
const displayJson = isTruncated
|
|
2438
|
-
? formattedJson.substring(0, 300) +
|
|
2439
|
-
'...\n}'
|
|
2440
|
-
: formattedJson;
|
|
2441
|
-
|
|
2442
|
-
// Apply syntax highlighting to JSON
|
|
2443
|
-
// This function adds CSS classes to different parts of the JSON
|
|
2444
|
-
// which are styled in GenericReport.module.scss
|
|
2445
|
-
const highlightJson = (json) => {
|
|
2446
|
-
// Simple regex-based JSON syntax highlighting
|
|
2447
|
-
return (
|
|
2448
|
-
json
|
|
2449
|
-
// Highlight keys (before the colon) - blue color
|
|
2450
|
-
.replace(
|
|
2451
|
-
/"([^"]+)"\s*:/g,
|
|
2452
|
-
'"<span class="json-key">$1</span>":'
|
|
2453
|
-
)
|
|
2454
|
-
// Highlight string values - green color
|
|
2455
|
-
.replace(
|
|
2456
|
-
/:\s*"([^"]*)"/g,
|
|
2457
|
-
': "<span class="json-string">$1</span>"'
|
|
2458
|
-
)
|
|
2459
|
-
// Highlight numeric values - purple color
|
|
2460
|
-
.replace(
|
|
2461
|
-
/:\s*([0-9]+(\.[0-9]+)?)/g,
|
|
2462
|
-
': <span class="json-number">$1</span>'
|
|
2463
|
-
)
|
|
2464
|
-
// Highlight boolean values - amber color
|
|
2465
|
-
.replace(
|
|
2466
|
-
/:\s*(true|false)/g,
|
|
2467
|
-
': <span class="json-boolean">$1</span>'
|
|
2468
|
-
)
|
|
2469
|
-
// Highlight null values - red color
|
|
2470
|
-
.replace(
|
|
2471
|
-
/:\s*(null)/g,
|
|
2472
|
-
': <span class="json-null">$1</span>'
|
|
2473
|
-
)
|
|
2474
|
-
);
|
|
2475
|
-
};
|
|
2476
|
-
|
|
2477
|
-
return (
|
|
2478
|
-
<div className={styles.jsonPreview}>
|
|
2479
|
-
<pre
|
|
2480
|
-
dangerouslySetInnerHTML={{
|
|
2481
|
-
__html: highlightJson(
|
|
2482
|
-
displayJson
|
|
2483
|
-
),
|
|
2484
|
-
}}
|
|
2485
|
-
/>
|
|
2486
|
-
{isTruncated && (
|
|
2487
|
-
<div
|
|
2488
|
-
className={
|
|
2489
|
-
styles.jsonTruncated
|
|
2490
|
-
}
|
|
2491
|
-
>
|
|
2492
|
-
(truncated)
|
|
2493
|
-
</div>
|
|
2494
|
-
)}
|
|
2495
|
-
</div>
|
|
2496
|
-
);
|
|
3295
|
+
// Just return the human-friendly format
|
|
3296
|
+
return renderJsonData(transformedJson);
|
|
2497
3297
|
} catch (e) {
|
|
2498
3298
|
return String(value);
|
|
2499
3299
|
}
|
|
2500
3300
|
}
|
|
2501
|
-
|
|
3301
|
+
|
|
3302
|
+
// Apply smart formatting for boolean/status values
|
|
3303
|
+
return formatSmartValue(value, key, columnType);
|
|
2502
3304
|
},
|
|
2503
3305
|
};
|
|
2504
3306
|
});
|
|
@@ -3505,17 +4307,6 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
3505
4307
|
Auto-detected based
|
|
3506
4308
|
on database
|
|
3507
4309
|
structure.
|
|
3508
|
-
<span
|
|
3509
|
-
className={
|
|
3510
|
-
styles.ratingInfo
|
|
3511
|
-
}
|
|
3512
|
-
>
|
|
3513
|
-
⭐⭐⭐ = High
|
|
3514
|
-
confidence |
|
|
3515
|
-
⭐⭐ = Medium
|
|
3516
|
-
confidence | ⭐
|
|
3517
|
-
= Low confidence
|
|
3518
|
-
</span>
|
|
3519
4310
|
</p>
|
|
3520
4311
|
<div
|
|
3521
4312
|
className={
|
|
@@ -3558,19 +4349,6 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
3558
4349
|
join
|
|
3559
4350
|
)}
|
|
3560
4351
|
</span>
|
|
3561
|
-
<span
|
|
3562
|
-
className={
|
|
3563
|
-
styles.confidenceBadge
|
|
3564
|
-
}
|
|
3565
|
-
>
|
|
3566
|
-
{join.confidence ===
|
|
3567
|
-
'high'
|
|
3568
|
-
? '⭐⭐⭐'
|
|
3569
|
-
: join.confidence ===
|
|
3570
|
-
'medium'
|
|
3571
|
-
? '⭐⭐'
|
|
3572
|
-
: '⭐'}
|
|
3573
|
-
</span>
|
|
3574
4352
|
</div>
|
|
3575
4353
|
<button
|
|
3576
4354
|
className={
|
|
@@ -3720,19 +4498,6 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
3720
4498
|
suggestedJoin
|
|
3721
4499
|
)}
|
|
3722
4500
|
</span>
|
|
3723
|
-
<span
|
|
3724
|
-
className={
|
|
3725
|
-
styles.confidenceBadge
|
|
3726
|
-
}
|
|
3727
|
-
>
|
|
3728
|
-
{suggestedJoin.confidence ===
|
|
3729
|
-
'high'
|
|
3730
|
-
? '⭐⭐⭐'
|
|
3731
|
-
: suggestedJoin.confidence ===
|
|
3732
|
-
'medium'
|
|
3733
|
-
? '⭐⭐'
|
|
3734
|
-
: '⭐'}
|
|
3735
|
-
</span>
|
|
3736
4501
|
</div>
|
|
3737
4502
|
<button
|
|
3738
4503
|
className={
|
|
@@ -4220,6 +4985,173 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
4220
4985
|
</div>
|
|
4221
4986
|
</div>
|
|
4222
4987
|
|
|
4988
|
+
{/* Suggested Filters Section */}
|
|
4989
|
+
{suggestedFilters.length > 0 && (
|
|
4990
|
+
<div
|
|
4991
|
+
className={
|
|
4992
|
+
styles.suggestedFiltersContainer
|
|
4993
|
+
}
|
|
4994
|
+
>
|
|
4995
|
+
<div
|
|
4996
|
+
className={
|
|
4997
|
+
styles.suggestedFiltersHeader
|
|
4998
|
+
}
|
|
4999
|
+
onClick={() =>
|
|
5000
|
+
setShowSuggestedFilters(
|
|
5001
|
+
!showSuggestedFilters
|
|
5002
|
+
)
|
|
5003
|
+
}
|
|
5004
|
+
style={{ cursor: 'pointer' }}
|
|
5005
|
+
>
|
|
5006
|
+
<div
|
|
5007
|
+
className={
|
|
5008
|
+
styles.suggestedFiltersHeaderTitle
|
|
5009
|
+
}
|
|
5010
|
+
>
|
|
5011
|
+
<h4>
|
|
5012
|
+
Suggested Filters
|
|
5013
|
+
{!showSuggestedFilters &&
|
|
5014
|
+
suggestedFilters.length >
|
|
5015
|
+
0 && (
|
|
5016
|
+
<span
|
|
5017
|
+
className={
|
|
5018
|
+
styles.suggestedFiltersCount
|
|
5019
|
+
}
|
|
5020
|
+
>
|
|
5021
|
+
(
|
|
5022
|
+
{
|
|
5023
|
+
suggestedFilters.length
|
|
5024
|
+
}
|
|
5025
|
+
)
|
|
5026
|
+
</span>
|
|
5027
|
+
)}
|
|
5028
|
+
</h4>
|
|
5029
|
+
<small>
|
|
5030
|
+
{showSuggestedFilters
|
|
5031
|
+
? 'Click to add a filter'
|
|
5032
|
+
: 'Click to expand'}
|
|
5033
|
+
</small>
|
|
5034
|
+
</div>
|
|
5035
|
+
<div
|
|
5036
|
+
className={`${
|
|
5037
|
+
styles.toggleBtn
|
|
5038
|
+
} ${
|
|
5039
|
+
showSuggestedFilters
|
|
5040
|
+
? styles.toggleBtnActive
|
|
5041
|
+
: ''
|
|
5042
|
+
}`}
|
|
5043
|
+
>
|
|
5044
|
+
{showSuggestedFilters
|
|
5045
|
+
? '▲'
|
|
5046
|
+
: '▼'}
|
|
5047
|
+
</div>
|
|
5048
|
+
</div>
|
|
5049
|
+
|
|
5050
|
+
{/* Group suggestions by category - only show when expanded */}
|
|
5051
|
+
{showSuggestedFilters &&
|
|
5052
|
+
(() => {
|
|
5053
|
+
// Get unique categories
|
|
5054
|
+
const categories = [
|
|
5055
|
+
...new Set(
|
|
5056
|
+
suggestedFilters.map(
|
|
5057
|
+
(s) =>
|
|
5058
|
+
s.category ||
|
|
5059
|
+
'General'
|
|
5060
|
+
)
|
|
5061
|
+
),
|
|
5062
|
+
];
|
|
5063
|
+
|
|
5064
|
+
return categories.map(
|
|
5065
|
+
(
|
|
5066
|
+
category,
|
|
5067
|
+
catIndex
|
|
5068
|
+
) => {
|
|
5069
|
+
// Get suggestions for this category
|
|
5070
|
+
const categorySuggestions =
|
|
5071
|
+
suggestedFilters.filter(
|
|
5072
|
+
(s) =>
|
|
5073
|
+
(s.category ||
|
|
5074
|
+
'General') ===
|
|
5075
|
+
category
|
|
5076
|
+
);
|
|
5077
|
+
|
|
5078
|
+
return (
|
|
5079
|
+
<div
|
|
5080
|
+
key={`category-${catIndex}`}
|
|
5081
|
+
className={
|
|
5082
|
+
styles.suggestedFiltersCategory
|
|
5083
|
+
}
|
|
5084
|
+
>
|
|
5085
|
+
<div
|
|
5086
|
+
className={
|
|
5087
|
+
styles.suggestedFiltersCategoryHeader
|
|
5088
|
+
}
|
|
5089
|
+
>
|
|
5090
|
+
{
|
|
5091
|
+
category
|
|
5092
|
+
}
|
|
5093
|
+
</div>
|
|
5094
|
+
<div
|
|
5095
|
+
className={
|
|
5096
|
+
styles.suggestedFiltersList
|
|
5097
|
+
}
|
|
5098
|
+
>
|
|
5099
|
+
{categorySuggestions.map(
|
|
5100
|
+
(
|
|
5101
|
+
suggestion,
|
|
5102
|
+
index
|
|
5103
|
+
) => (
|
|
5104
|
+
<div
|
|
5105
|
+
key={`suggestion-${category}-${index}`}
|
|
5106
|
+
className={
|
|
5107
|
+
styles.suggestedFilterItem
|
|
5108
|
+
}
|
|
5109
|
+
onClick={() =>
|
|
5110
|
+
applySuggestedFilter(
|
|
5111
|
+
suggestion
|
|
5112
|
+
)
|
|
5113
|
+
}
|
|
5114
|
+
title={
|
|
5115
|
+
suggestion.description
|
|
5116
|
+
}
|
|
5117
|
+
>
|
|
5118
|
+
<div
|
|
5119
|
+
className={
|
|
5120
|
+
styles.suggestedFilterName
|
|
5121
|
+
}
|
|
5122
|
+
>
|
|
5123
|
+
{
|
|
5124
|
+
suggestion.name
|
|
5125
|
+
}
|
|
5126
|
+
</div>
|
|
5127
|
+
<button
|
|
5128
|
+
className={
|
|
5129
|
+
styles.addSuggestedFilterBtn
|
|
5130
|
+
}
|
|
5131
|
+
onClick={(
|
|
5132
|
+
e
|
|
5133
|
+
) => {
|
|
5134
|
+
e.stopPropagation();
|
|
5135
|
+
applySuggestedFilter(
|
|
5136
|
+
suggestion
|
|
5137
|
+
);
|
|
5138
|
+
}}
|
|
5139
|
+
title="Add this filter"
|
|
5140
|
+
>
|
|
5141
|
+
+
|
|
5142
|
+
</button>
|
|
5143
|
+
</div>
|
|
5144
|
+
)
|
|
5145
|
+
)}
|
|
5146
|
+
</div>
|
|
5147
|
+
</div>
|
|
5148
|
+
);
|
|
5149
|
+
}
|
|
5150
|
+
);
|
|
5151
|
+
})()}
|
|
5152
|
+
</div>
|
|
5153
|
+
)}
|
|
5154
|
+
|
|
4223
5155
|
{filterCriteria.groups.map(
|
|
4224
5156
|
(group, groupIndex) => (
|
|
4225
5157
|
<div
|
|
@@ -4487,6 +5419,78 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
4487
5419
|
].column =
|
|
4488
5420
|
column;
|
|
4489
5421
|
|
|
5422
|
+
// Check if it's a JSON field
|
|
5423
|
+
const isJson =
|
|
5424
|
+
isJsonColumn(
|
|
5425
|
+
table,
|
|
5426
|
+
column
|
|
5427
|
+
);
|
|
5428
|
+
if (
|
|
5429
|
+
isJson
|
|
5430
|
+
) {
|
|
5431
|
+
// Fetch JSON keys
|
|
5432
|
+
fetchJsonFieldKeys(
|
|
5433
|
+
table,
|
|
5434
|
+
column
|
|
5435
|
+
);
|
|
5436
|
+
|
|
5437
|
+
// Add a jsonKey field if it doesn't exist
|
|
5438
|
+
if (
|
|
5439
|
+
!updatedFilterCriteria
|
|
5440
|
+
.groups[
|
|
5441
|
+
groupIndex
|
|
5442
|
+
]
|
|
5443
|
+
.filters[
|
|
5444
|
+
filterIndex
|
|
5445
|
+
]
|
|
5446
|
+
.jsonKey
|
|
5447
|
+
) {
|
|
5448
|
+
updatedFilterCriteria.groups[
|
|
5449
|
+
groupIndex
|
|
5450
|
+
].filters[
|
|
5451
|
+
filterIndex
|
|
5452
|
+
].jsonKey =
|
|
5453
|
+
'';
|
|
5454
|
+
}
|
|
5455
|
+
|
|
5456
|
+
// Automatically show the JSON key selector for this filter
|
|
5457
|
+
const selectorKey = `${groupIndex}-${filterIndex}`;
|
|
5458
|
+
setTimeout(
|
|
5459
|
+
() => {
|
|
5460
|
+
setShowJsonKeySelector(
|
|
5461
|
+
(
|
|
5462
|
+
prev
|
|
5463
|
+
) => ({
|
|
5464
|
+
...prev,
|
|
5465
|
+
[selectorKey]: true,
|
|
5466
|
+
})
|
|
5467
|
+
);
|
|
5468
|
+
},
|
|
5469
|
+
100
|
|
5470
|
+
); // Small delay to ensure the filter is updated first
|
|
5471
|
+
} else {
|
|
5472
|
+
// If it's not a JSON field, remove the jsonKey field if it exists
|
|
5473
|
+
if (
|
|
5474
|
+
updatedFilterCriteria
|
|
5475
|
+
.groups[
|
|
5476
|
+
groupIndex
|
|
5477
|
+
]
|
|
5478
|
+
.filters[
|
|
5479
|
+
filterIndex
|
|
5480
|
+
]
|
|
5481
|
+
.jsonKey
|
|
5482
|
+
) {
|
|
5483
|
+
delete updatedFilterCriteria
|
|
5484
|
+
.groups[
|
|
5485
|
+
groupIndex
|
|
5486
|
+
]
|
|
5487
|
+
.filters[
|
|
5488
|
+
filterIndex
|
|
5489
|
+
]
|
|
5490
|
+
.jsonKey;
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
|
|
4490
5494
|
// Set the state once with both updates
|
|
4491
5495
|
setFilterCriteria(
|
|
4492
5496
|
updatedFilterCriteria
|
|
@@ -4726,6 +5730,32 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
4726
5730
|
styles.jsonKeyDropdown
|
|
4727
5731
|
}
|
|
4728
5732
|
>
|
|
5733
|
+
<div
|
|
5734
|
+
className={
|
|
5735
|
+
styles.jsonKeyDropdownHeader
|
|
5736
|
+
}
|
|
5737
|
+
>
|
|
5738
|
+
<span>
|
|
5739
|
+
Available
|
|
5740
|
+
JSON
|
|
5741
|
+
Keys
|
|
5742
|
+
</span>
|
|
5743
|
+
<button
|
|
5744
|
+
type="button"
|
|
5745
|
+
className={
|
|
5746
|
+
styles.jsonKeyCloseButton
|
|
5747
|
+
}
|
|
5748
|
+
onClick={() =>
|
|
5749
|
+
toggleJsonKeySelector(
|
|
5750
|
+
groupIndex,
|
|
5751
|
+
filterIndex
|
|
5752
|
+
)
|
|
5753
|
+
}
|
|
5754
|
+
title="Close selector"
|
|
5755
|
+
>
|
|
5756
|
+
×
|
|
5757
|
+
</button>
|
|
5758
|
+
</div>
|
|
4729
5759
|
{loadingJsonKeys[
|
|
4730
5760
|
`${criterion.table}.${criterion.column}`
|
|
4731
5761
|
] ? (
|
|
@@ -4749,6 +5779,27 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
4749
5779
|
styles.jsonKeyList
|
|
4750
5780
|
}
|
|
4751
5781
|
>
|
|
5782
|
+
<div
|
|
5783
|
+
className={
|
|
5784
|
+
styles.jsonKeyListHeader
|
|
5785
|
+
}
|
|
5786
|
+
>
|
|
5787
|
+
<span
|
|
5788
|
+
className={
|
|
5789
|
+
styles.jsonKeyNameHeader
|
|
5790
|
+
}
|
|
5791
|
+
>
|
|
5792
|
+
JSON
|
|
5793
|
+
Path
|
|
5794
|
+
</span>
|
|
5795
|
+
<span
|
|
5796
|
+
className={
|
|
5797
|
+
styles.jsonKeyFrequencyHeader
|
|
5798
|
+
}
|
|
5799
|
+
>
|
|
5800
|
+
Usage
|
|
5801
|
+
</span>
|
|
5802
|
+
</div>
|
|
4752
5803
|
{jsonFieldKeys[
|
|
4753
5804
|
`${criterion.table}.${criterion.column}`
|
|
4754
5805
|
].map(
|
|
@@ -4812,6 +5863,23 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
4812
5863
|
manually.
|
|
4813
5864
|
</div>
|
|
4814
5865
|
)}
|
|
5866
|
+
<div
|
|
5867
|
+
className={
|
|
5868
|
+
styles.jsonKeyDropdownFooter
|
|
5869
|
+
}
|
|
5870
|
+
>
|
|
5871
|
+
<small>
|
|
5872
|
+
Click
|
|
5873
|
+
on
|
|
5874
|
+
a
|
|
5875
|
+
key
|
|
5876
|
+
to
|
|
5877
|
+
select
|
|
5878
|
+
it
|
|
5879
|
+
for
|
|
5880
|
+
filtering
|
|
5881
|
+
</small>
|
|
5882
|
+
</div>
|
|
4815
5883
|
</div>
|
|
4816
5884
|
)}
|
|
4817
5885
|
|