@visns-studio/visns-components 5.8.4 → 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.
@@ -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
- return `${formatName(sourceTable)} ↔ ${formatName(
67
- finalTargetTable
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
- return `${formatName(sourceTable)} ${formatName(targetTable)}`;
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
- return `${formatName(sourceTable)} ${formatName(targetTable)}`;
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
- return `${formatName(sourceTable)} ${formatName(targetTable)}`;
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 `${formatName(sourceTable)} - ${formatName(targetTable)}`;
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 (description.includes('pivot table')) {
97
- return `${formatName(sourceTable)} ↔ ${formatName(targetTable)}`;
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 `${formatName(sourceTable)} → ${formatName(targetTable)}`;
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) => !hiddenTables.includes(table.name)
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]: res.data.data.suggestedJoins,
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
- // Create a more compact representation for display
2428
- const formattedJson = JSON.stringify(
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
- return value;
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