@visns-studio/visns-components 5.10.6 → 5.10.8

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.
@@ -22,8 +22,11 @@ import {
22
22
  Money,
23
23
  ShippingBoxV1,
24
24
  SettingsVertical,
25
+ TriangleAlert,
25
26
  Cart,
26
27
  Newspaper,
28
+ ChevronDown,
29
+ Rss,
27
30
  } from 'akar-icons';
28
31
  import CustomFetch from '../Fetch';
29
32
  import Download from '../Download';
@@ -121,6 +124,300 @@ const getTableDescription = (tableName, definition) => {
121
124
  return `Table: ${tableName}`;
122
125
  };
123
126
 
127
+ /* This function has been moved inside renderFilters() to access state variables
128
+ const getSmartSuggestions = (selectedTable, joins, tableColumns, definition) => {
129
+ const suggestions = {
130
+ sorting: [],
131
+ filters: []
132
+ };
133
+
134
+ if (!selectedTable || !tableColumns) return suggestions;
135
+
136
+ // Get table display name for better suggestions
137
+ const tableDisplayName = getTableDisplayName(selectedTable, definition);
138
+
139
+ // Define smart patterns based on table types and common business logic
140
+ const tablePatterns = {
141
+ // User/Client related tables
142
+ users: { priority: ['name', 'created_at', 'updated_at', 'email'], filters: ['active', 'status', 'created_at'] },
143
+ clients: { priority: ['name', 'created_at', 'updated_at', 'email'], filters: ['active', 'status', 'created_at'] },
144
+ contacts: { priority: ['name', 'email', 'created_at'], filters: ['active', 'client_id'] },
145
+
146
+ // Project/Lead related tables
147
+ leads: { priority: ['created_at', 'updated_at', 'name', 'status'], filters: ['status', 'source', 'likelihood', 'created_at'] },
148
+ projects: { priority: ['created_at', 'deadline', 'name', 'status'], filters: ['status', 'priority', 'created_at'] },
149
+ surveys: { priority: ['created_at', 'updated_at', 'status'], filters: ['status', 'type', 'created_at'] },
150
+
151
+ // Location related tables
152
+ sites: { priority: ['name', 'address', 'created_at'], filters: ['active', 'state', 'client_id'] },
153
+ locations: { priority: ['name', 'address', 'created_at'], filters: ['active', 'state'] },
154
+
155
+ // Reference/lookup tables
156
+ industries: { priority: ['name', 'id'], filters: [] },
157
+ sources: { priority: ['name', 'id'], filters: ['active'] },
158
+ actions: { priority: ['name', 'priority'], filters: ['active'] },
159
+ tags: { priority: ['name', 'created_at'], filters: ['active'] },
160
+
161
+ // Document/content tables
162
+ documents: { priority: ['created_at', 'name', 'type'], filters: ['type', 'status', 'created_at'] },
163
+ notes: { priority: ['created_at', 'updated_at'], filters: ['created_at', 'user_id'] },
164
+
165
+ // Transactional tables
166
+ bookings: { priority: ['date', 'created_at', 'status'], filters: ['status', 'date', 'facility_id'] },
167
+ facilities: { priority: ['name', 'type', 'rate'], filters: ['type', 'available'] },
168
+ };
169
+
170
+ // Find matching pattern for current table
171
+ let pattern = null;
172
+ const lowerTableName = selectedTable.toLowerCase();
173
+
174
+ // Direct match
175
+ if (tablePatterns[lowerTableName]) {
176
+ pattern = tablePatterns[lowerTableName];
177
+ } else {
178
+ // Partial match (e.g., client_notes matches clients pattern)
179
+ for (const [key, value] of Object.entries(tablePatterns)) {
180
+ if (lowerTableName.includes(key) || key.includes(lowerTableName)) {
181
+ pattern = value;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+
187
+ // Generate sorting suggestions
188
+ if (pattern) {
189
+ // Use pattern-based priorities
190
+ pattern.priority.forEach((columnName, index) => {
191
+ const column = tableColumns.find(col =>
192
+ col.name.toLowerCase() === columnName.toLowerCase() ||
193
+ col.name.toLowerCase().includes(columnName.toLowerCase())
194
+ );
195
+ if (column) {
196
+ suggestions.sorting.push({
197
+ table: selectedTable,
198
+ column: column.name,
199
+ direction: columnName.includes('created_at') || columnName.includes('updated_at') || columnName.includes('date') ? 'DESC' : 'ASC',
200
+ priority: index + 1,
201
+ reason: `Common sorting for ${tableDisplayName}`
202
+ });
203
+ }
204
+ });
205
+ } else {
206
+ // Fallback: Common column patterns
207
+ const commonSortColumns = [
208
+ { patterns: ['created_at', 'created'], direction: 'DESC', reason: 'Most recent first' },
209
+ { patterns: ['updated_at', 'updated', 'modified'], direction: 'DESC', reason: 'Recently updated first' },
210
+ { patterns: ['name', 'title', 'label'], direction: 'ASC', reason: 'Alphabetical order' },
211
+ { patterns: ['date', 'due_date', 'deadline'], direction: 'DESC', reason: 'Latest dates first' },
212
+ { patterns: ['priority', 'order', 'sequence'], direction: 'ASC', reason: 'By priority' },
213
+ { patterns: ['status'], direction: 'ASC', reason: 'By status' }
214
+ ];
215
+
216
+ commonSortColumns.forEach((sortOption, index) => {
217
+ const column = tableColumns.find(col =>
218
+ sortOption.patterns.some(pattern =>
219
+ col.name.toLowerCase().includes(pattern)
220
+ )
221
+ );
222
+ if (column) {
223
+ suggestions.sorting.push({
224
+ table: selectedTable,
225
+ column: column.name,
226
+ direction: sortOption.direction,
227
+ priority: index + 1,
228
+ reason: sortOption.reason
229
+ });
230
+ }
231
+ });
232
+ }
233
+
234
+ // Generate filter suggestions
235
+ if (pattern) {
236
+ pattern.filters.forEach(columnName => {
237
+ const column = tableColumns.find(col =>
238
+ col.name.toLowerCase() === columnName.toLowerCase() ||
239
+ col.name.toLowerCase().includes(columnName.toLowerCase())
240
+ );
241
+ if (column) {
242
+ // Determine appropriate filter based on column type and name
243
+ let filterType = 'equals';
244
+ let suggestedValue = '';
245
+ let reason = `Filter by ${formatName(column.name)}`;
246
+
247
+ if (column.name.toLowerCase().includes('status')) {
248
+ suggestedValue = 'active';
249
+ reason = 'Show only active records';
250
+ } else if (column.name.toLowerCase().includes('created_at')) {
251
+ filterType = 'date_range';
252
+ suggestedValue = 'last_30_days';
253
+ reason = 'Recent records (last 30 days)';
254
+ } else if (column.name.toLowerCase().includes('active')) {
255
+ suggestedValue = '1';
256
+ reason = 'Show only active records';
257
+ }
258
+
259
+ suggestions.filters.push({
260
+ table: selectedTable,
261
+ column: column.name,
262
+ operator: filterType,
263
+ value: suggestedValue,
264
+ reason: reason
265
+ });
266
+ }
267
+ });
268
+ }
269
+
270
+ // Add relationship-based suggestions
271
+ joins.forEach(join => {
272
+ if (join.availableColumns && join.availableColumns.length > 0) {
273
+ // Add sorting suggestions from joined tables
274
+ const joinTableName = join.targetTable.toLowerCase();
275
+ if (tablePatterns[joinTableName]) {
276
+ const joinPattern = tablePatterns[joinTableName];
277
+ joinPattern.priority.slice(0, 2).forEach((columnName, index) => {
278
+ const column = join.availableColumns.find(col =>
279
+ col.name.toLowerCase().includes(columnName.toLowerCase())
280
+ );
281
+ if (column) {
282
+ suggestions.sorting.push({
283
+ table: join.targetTable,
284
+ column: column.name,
285
+ direction: columnName.includes('created_at') || columnName.includes('date') ? 'DESC' : 'ASC',
286
+ priority: suggestions.sorting.length + index + 1,
287
+ reason: `Sort by ${getSmartRelationshipName(join.targetTable, definition)}`
288
+ });
289
+ }
290
+ });
291
+ }
292
+ }
293
+ });
294
+
295
+ // Limit suggestions to top 3-4 items each
296
+ suggestions.sorting = suggestions.sorting.slice(0, 4);
297
+ suggestions.filters = suggestions.filters.slice(0, 3);
298
+
299
+ // Combine all suggestions into a single array
300
+ const allSuggestions = [...suggestions.sorting, ...suggestions.filters];
301
+
302
+ // Check which suggestions are already applied
303
+ const enhancedSuggestions = allSuggestions.map(suggestion => {
304
+ let alreadyApplied = false;
305
+
306
+ if (suggestion.type === 'sorting') {
307
+ // Check if this sort criterion already exists
308
+ alreadyApplied = sortCriteria.some(
309
+ criteria => criteria.table === suggestion.table &&
310
+ criteria.column === suggestion.column &&
311
+ criteria.direction === suggestion.direction
312
+ );
313
+ } else if (suggestion.type === 'filter') {
314
+ // Check if this filter already exists in any group
315
+ alreadyApplied = filterCriteria.groups && filterCriteria.groups.some(group =>
316
+ group.filters.some(filter =>
317
+ filter.table === suggestion.table &&
318
+ filter.column === suggestion.column &&
319
+ filter.operator === suggestion.operator &&
320
+ filter.value === suggestion.value
321
+ )
322
+ );
323
+ }
324
+
325
+ return {
326
+ ...suggestion,
327
+ alreadyApplied
328
+ };
329
+ });
330
+
331
+ // Return prioritized suggestions (max 6-8 for clean interface)
332
+ return enhancedSuggestions.slice(0, 8);
333
+ };
334
+ */
335
+
336
+ // Smart naming for suggested relationships
337
+ const getSmartRelationshipName = (tableName, definition) => {
338
+ // First try to get the defined label
339
+ const definedName = getTableDisplayName(tableName, definition);
340
+ if (definedName !== formatName(tableName)) {
341
+ return definedName;
342
+ }
343
+
344
+ // Smart mapping for common relationship patterns
345
+ const smartMappings = {
346
+ // Handle client_* tables by using the User label from clients table
347
+ client_agreements: () => {
348
+ const clientLabel = getTableDisplayName('clients', definition);
349
+ return `${clientLabel} Agreements`;
350
+ },
351
+ client_insurances: () => {
352
+ const clientLabel = getTableDisplayName('clients', definition);
353
+ return `${clientLabel} Insurances`;
354
+ },
355
+ client_notes: () => {
356
+ const clientLabel = getTableDisplayName('clients', definition);
357
+ return `${clientLabel} Notes`;
358
+ },
359
+ client_documents: () => {
360
+ const clientLabel = getTableDisplayName('clients', definition);
361
+ return `${clientLabel} Documents`;
362
+ },
363
+ client_contracts: () => {
364
+ const clientLabel = getTableDisplayName('clients', definition);
365
+ return `${clientLabel} Contracts`;
366
+ },
367
+ // Handle other common patterns
368
+ user_permissions: () => {
369
+ const clientLabel = getTableDisplayName('clients', definition);
370
+ return `${clientLabel} Permissions`;
371
+ },
372
+ contact_notes: 'Contact Notes',
373
+ site_documents: () => {
374
+ const siteLabel = getTableDisplayName('sites', definition);
375
+ return `${siteLabel} Documents`;
376
+ },
377
+ lead_notes: () => {
378
+ const leadLabel = getTableDisplayName('leads', definition);
379
+ return `${leadLabel} Notes`;
380
+ },
381
+ project_documents: 'Project Documents',
382
+ survey_responses: 'Survey Responses',
383
+ // Add more patterns as needed
384
+ };
385
+
386
+ // Check if we have a smart mapping for this table
387
+ if (smartMappings[tableName]) {
388
+ const mapping = smartMappings[tableName];
389
+ return typeof mapping === 'function' ? mapping() : mapping;
390
+ }
391
+
392
+ // If no smart mapping, try to detect patterns and apply main table label
393
+ if (tableName.includes('client_') || tableName.includes('clients_')) {
394
+ const clientLabel = getTableDisplayName('clients', definition);
395
+ const suffix = tableName.replace(/^clients?_/, '');
396
+ return `${clientLabel} ${formatName(suffix)}`;
397
+ }
398
+
399
+ if (tableName.includes('contact_') || tableName.includes('contacts_')) {
400
+ const contactLabel = getTableDisplayName('contacts', definition);
401
+ const suffix = tableName.replace(/^contacts?_/, '');
402
+ return `${contactLabel} ${formatName(suffix)}`;
403
+ }
404
+
405
+ if (tableName.includes('site_') || tableName.includes('sites_')) {
406
+ const siteLabel = getTableDisplayName('sites', definition);
407
+ const suffix = tableName.replace(/^sites?_/, '');
408
+ return `${siteLabel} ${formatName(suffix)}`;
409
+ }
410
+
411
+ if (tableName.includes('lead_') || tableName.includes('leads_')) {
412
+ const leadLabel = getTableDisplayName('leads', definition);
413
+ const suffix = tableName.replace(/^leads?_/, '');
414
+ return `${leadLabel} ${formatName(suffix)}`;
415
+ }
416
+
417
+ // Fallback to formatted name
418
+ return formatName(tableName);
419
+ };
420
+
124
421
  // Utility function to format names from camelCase or snake_case to user-friendly format
125
422
  const formatName = (name) => {
126
423
  if (!name) return '';
@@ -776,9 +1073,9 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
776
1073
  const [loadedReportId, setLoadedReportId] = useState(null); // Track loaded report for overwrite functionality
777
1074
  const [isLoadingReports, setIsLoadingReports] = useState(false);
778
1075
 
779
- // State for suggested joins
780
- const [suggestedJoins, setSuggestedJoins] = useState({});
781
- const [isLoadingSuggestedJoins, setIsLoadingSuggestedJoins] = useState({});
1076
+ // State for detected joins
1077
+ const [detectedJoins, setDetectedJoins] = useState({});
1078
+ const [isLoadingDetectedJoins, setIsLoadingDetectedJoins] = useState({});
782
1079
 
783
1080
  // State for sorting and filtering
784
1081
  const [sortCriteria, setSortCriteria] = useState([]);
@@ -820,7 +1117,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
820
1117
  const {
821
1118
  reportsUrl = '/ajax/reportBuilder/reports',
822
1119
  executeUrl = '/ajax/reportBuilder/execute',
823
- suggestedJoinsUrl = '/ajax/reportBuilder/getSuggestedJoins',
1120
+ detectedJoinsUrl = '/ajax/reportBuilder/getSuggestedJoins',
824
1121
  exportUrl = '/ajax/reportBuilder/export',
825
1122
  } = setting;
826
1123
 
@@ -883,9 +1180,9 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
883
1180
  useEffect(() => {
884
1181
  if (selectedTable) {
885
1182
  fetchTableColumns(selectedTable);
886
- fetchSuggestedJoins(selectedTable);
1183
+ fetchDetectedJoins(selectedTable);
887
1184
  }
888
- }, [selectedTable, columnUrl, suggestedJoinsUrl]);
1185
+ }, [selectedTable, columnUrl, detectedJoinsUrl]);
889
1186
 
890
1187
  // Auto-select common columns when user reaches the columns step
891
1188
  useEffect(() => {
@@ -1236,26 +1533,26 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1236
1533
  }
1237
1534
  };
1238
1535
 
1239
- // Fetch suggested joins for a table
1240
- const fetchSuggestedJoins = async (tableName) => {
1241
- if (!tableName || !suggestedJoinsUrl) {
1536
+ // Fetch detected joins for a table
1537
+ const fetchDetectedJoins = async (tableName) => {
1538
+ if (!tableName || !detectedJoinsUrl) {
1242
1539
  console.warn(
1243
- 'Missing tableName or suggestedJoinsUrl for fetchSuggestedJoins'
1540
+ 'Missing tableName or detectedJoinsUrl for fetchDetectedJoins'
1244
1541
  );
1245
1542
  return;
1246
1543
  }
1247
1544
 
1248
- setIsLoadingSuggestedJoins({
1249
- ...isLoadingSuggestedJoins,
1545
+ setIsLoadingDetectedJoins({
1546
+ ...isLoadingDetectedJoins,
1250
1547
  [tableName]: true,
1251
1548
  });
1252
1549
 
1253
1550
  try {
1254
- const result = await CustomFetch(suggestedJoinsUrl, 'POST', {
1551
+ const result = await CustomFetch(detectedJoinsUrl, 'POST', {
1255
1552
  table: tableName,
1256
1553
  });
1257
1554
 
1258
- console.log('Suggested joins API response:', result); // Debug log
1555
+ console.log('Detected joins API response:', result); // Debug log
1259
1556
 
1260
1557
  if (result.error) {
1261
1558
  toast.error(result.error);
@@ -1296,34 +1593,34 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1296
1593
  });
1297
1594
 
1298
1595
  console.log(
1299
- `Found ${filteredJoins.length} suggested joins for ${tableName}:`,
1596
+ `Found ${filteredJoins.length} detected joins for ${tableName}:`,
1300
1597
  filteredJoins
1301
1598
  );
1302
1599
 
1303
- setSuggestedJoins({
1304
- ...suggestedJoins,
1600
+ setDetectedJoins({
1601
+ ...detectedJoins,
1305
1602
  [tableName]: filteredJoins,
1306
1603
  });
1307
1604
  } else {
1308
1605
  console.warn(
1309
- 'No valid suggested joins found or invalid response format:',
1606
+ 'No valid detected joins found or invalid response format:',
1310
1607
  result
1311
1608
  );
1312
- setSuggestedJoins({
1313
- ...suggestedJoins,
1609
+ setDetectedJoins({
1610
+ ...detectedJoins,
1314
1611
  [tableName]: [],
1315
1612
  });
1316
1613
  }
1317
1614
  }
1318
1615
  } catch (error) {
1319
- console.error('Error fetching suggested joins:', error);
1320
- setSuggestedJoins({
1321
- ...suggestedJoins,
1616
+ console.error('Error fetching detected joins:', error);
1617
+ setDetectedJoins({
1618
+ ...detectedJoins,
1322
1619
  [tableName]: [],
1323
1620
  });
1324
1621
  } finally {
1325
- setIsLoadingSuggestedJoins({
1326
- ...isLoadingSuggestedJoins,
1622
+ setIsLoadingDetectedJoins({
1623
+ ...isLoadingDetectedJoins,
1327
1624
  [tableName]: false,
1328
1625
  });
1329
1626
  }
@@ -1446,12 +1743,35 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1446
1743
  } else {
1447
1744
  // Handle the response format similar to fetchTableColumns
1448
1745
  let columnsData = [];
1449
- if (result.data?.success && result.data.data) {
1746
+
1747
+ if (result.data?.success && result.data?.data?.columns) {
1748
+ // Primary format: {"success":true,"data":{"table":"contacts_tags","columns":[...]}}
1749
+ columnsData = result.data.data.columns;
1750
+ } else if (result.columns) {
1751
+ // Direct format: {"columns": [...]}
1752
+ columnsData = result.columns;
1753
+ } else if (result.data?.columns) {
1754
+ // Alternative format: {"data": {"columns": [...]}}
1755
+ columnsData = result.data.columns;
1756
+ } else if (result.data?.success && result.data.data) {
1757
+ // Legacy format
1450
1758
  columnsData = result.data.data;
1451
1759
  } else if (Array.isArray(result.data)) {
1452
1760
  columnsData = result.data;
1453
1761
  } else if (Array.isArray(result)) {
1454
1762
  columnsData = result;
1763
+ } else {
1764
+ // Fallback formats
1765
+ const fallback = result.data?.data || result.data;
1766
+ if (Array.isArray(fallback)) {
1767
+ columnsData = fallback;
1768
+ }
1769
+ }
1770
+
1771
+ // Ensure columnsData is always an array
1772
+ if (!Array.isArray(columnsData)) {
1773
+ console.error(`Invalid columns data format for ${type} table:`, result);
1774
+ columnsData = [];
1455
1775
  }
1456
1776
 
1457
1777
  console.log(
@@ -1465,6 +1785,11 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1465
1785
  }
1466
1786
  } catch (error) {
1467
1787
  console.error(`Error fetching ${type} columns:`, error);
1788
+ // Ensure state remains consistent even on error
1789
+ setManualJoinColumns((prev) => ({
1790
+ ...prev,
1791
+ [type]: [],
1792
+ }));
1468
1793
  }
1469
1794
  };
1470
1795
 
@@ -1488,10 +1813,17 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1488
1813
  targetColumn: manualJoin.targetColumn,
1489
1814
  joinType: manualJoin.joinType,
1490
1815
  isManual: true,
1816
+ availableColumns: [], // Add columns array like suggested relationships
1491
1817
  };
1492
1818
 
1493
1819
  setJoins([...joins, newJoin]);
1494
1820
 
1821
+ // Fetch columns for the joined table (same as suggested relationships)
1822
+ if (!availableTables.includes(manualJoin.targetTable)) {
1823
+ setAvailableTables([...availableTables, manualJoin.targetTable]);
1824
+ fetchJoinColumns(manualJoin.targetTable, joins.length); // Use current joins.length as index
1825
+ }
1826
+
1495
1827
  // Reset form and hide it
1496
1828
  setManualJoin({
1497
1829
  sourceTable: '',
@@ -1513,7 +1845,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1513
1845
  setJoins([]);
1514
1846
  setAvailableTables([tableName]);
1515
1847
  fetchTableColumns(tableName);
1516
- fetchSuggestedJoins(tableName);
1848
+ fetchDetectedJoins(tableName);
1517
1849
 
1518
1850
  goToNextStep(); // Go to relationships step
1519
1851
  };
@@ -1543,6 +1875,62 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1543
1875
  }
1544
1876
  };
1545
1877
 
1878
+ // Apply smart suggestion to the report configuration
1879
+ const applySuggestion = (suggestion) => {
1880
+ if (suggestion.type === 'sorting') {
1881
+ // Add sorting criterion
1882
+ const newSortCriterion = {
1883
+ table: suggestion.table,
1884
+ column: suggestion.column,
1885
+ direction: suggestion.direction,
1886
+ };
1887
+
1888
+ // Check if this sort already exists
1889
+ const existingIndex = sortCriteria.findIndex(
1890
+ (criteria) => criteria.table === suggestion.table && criteria.column === suggestion.column
1891
+ );
1892
+
1893
+ if (existingIndex >= 0) {
1894
+ // Update existing sort
1895
+ const updatedSort = [...sortCriteria];
1896
+ updatedSort[existingIndex] = newSortCriterion;
1897
+ setSortCriteria(updatedSort);
1898
+ toast.info(`Updated existing sort: ${suggestion.title}`);
1899
+ } else {
1900
+ // Add new sort
1901
+ setSortCriteria(prev => [...prev, newSortCriterion]);
1902
+ toast.success(`Applied sorting: ${suggestion.title}`);
1903
+ }
1904
+ } else if (suggestion.type === 'filter') {
1905
+ // Add filter criterion to the first filter group (or create one if none exists)
1906
+ const newFilterCriterion = {
1907
+ table: suggestion.table,
1908
+ column: suggestion.column,
1909
+ operator: suggestion.operator,
1910
+ value: suggestion.value,
1911
+ };
1912
+
1913
+ setFilterCriteria(prev => {
1914
+ const updatedCriteria = { ...prev };
1915
+
1916
+ // Ensure we have at least one group
1917
+ if (!updatedCriteria.groups || updatedCriteria.groups.length === 0) {
1918
+ updatedCriteria.groups = [{
1919
+ operator: 'AND',
1920
+ filters: []
1921
+ }];
1922
+ }
1923
+
1924
+ // Add to first group
1925
+ updatedCriteria.groups[0].filters.push(newFilterCriterion);
1926
+
1927
+ return updatedCriteria;
1928
+ });
1929
+
1930
+ toast.success(`Applied filter: ${suggestion.title}`);
1931
+ }
1932
+ };
1933
+
1546
1934
  // Create dataSource function for server-side pagination (similar to DataGrid)
1547
1935
  const dataSource = useCallback(
1548
1936
  async ({ skip, limit, sortInfo, filterValue }) => {
@@ -1885,7 +2273,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
1885
2273
  setSelectedTable(config.mainTable);
1886
2274
  setAvailableTables([config.mainTable]);
1887
2275
  fetchTableColumns(config.mainTable);
1888
- fetchSuggestedJoins(config.mainTable);
2276
+ fetchDetectedJoins(config.mainTable);
1889
2277
  }
1890
2278
 
1891
2279
  if (config.columns) {
@@ -2177,14 +2565,14 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2177
2565
  <div className={styles.wizardNavigation}>
2178
2566
  {currentWizardStep === 0 ? (
2179
2567
  <button
2180
- className={`${styles.btn} ${styles.btnSecondary}`}
2568
+ className={`${styles.btn} ${styles.btnCancel}`}
2181
2569
  onClick={startAgain}
2182
2570
  >
2183
2571
  Cancel
2184
2572
  </button>
2185
2573
  ) : (
2186
2574
  <button
2187
- className={`${styles.btn} ${styles.btnSecondary}`}
2575
+ className={`${styles.btn} ${styles.btnPrevious}`}
2188
2576
  onClick={goToPreviousStep}
2189
2577
  >
2190
2578
  Previous
@@ -2193,7 +2581,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2193
2581
 
2194
2582
  {currentWizardStep === wizardSteps.length - 1 ? (
2195
2583
  <button
2196
- className={`${styles.btn} ${styles.btnSecondary}`}
2584
+ className={`${styles.btn} ${styles.btnStartAgain}`}
2197
2585
  onClick={startAgain}
2198
2586
  >
2199
2587
  Start Again
@@ -2672,8 +3060,8 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2672
3060
 
2673
3061
  // Render relationships (simplified)
2674
3062
  const renderRelationships = () => {
2675
- const tableSuggestions = suggestedJoins[selectedTable] || [];
2676
- const isLoading = isLoadingSuggestedJoins[selectedTable] || false;
3063
+ const tableSuggestions = detectedJoins[selectedTable] || [];
3064
+ const isLoading = isLoadingDetectedJoins[selectedTable] || false;
2677
3065
 
2678
3066
  return (
2679
3067
  <div className={styles.relationshipsSection}>
@@ -2724,21 +3112,22 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2724
3112
  </div>
2725
3113
  )}
2726
3114
 
2727
- {/* Suggested Relationships */}
3115
+ {/* Detected Relationships */}
2728
3116
  {tableSuggestions.length > 0 && (
2729
- <div className={styles.suggestedJoins}>
2730
- <h3>Suggested Connections</h3>
3117
+ <div className={styles.detectedJoins}>
3118
+ <h3>Detected Connections</h3>
2731
3119
  <p>
2732
- We found these possible connections to other data:
3120
+ We found these possible connections to other data source:
2733
3121
  </p>
3122
+ <div className={styles.detectionsGrid}>
2734
3123
  {tableSuggestions.map((suggestion, index) => (
2735
- <div key={index} className={styles.suggestionCard}>
2736
- <div className={styles.suggestionInfo}>
2737
- <div className={styles.suggestionTitle}>
3124
+ <div key={index} className={styles.detectionCard}>
3125
+ <div className={styles.detectionInfo}>
3126
+ <div className={styles.detectionTitle}>
2738
3127
  <LinkChain size={16} />
2739
3128
  <span>
2740
3129
  Connect to{' '}
2741
- {getTableDisplayName(
3130
+ {getSmartRelationshipName(
2742
3131
  suggestion.targetTable,
2743
3132
  definition
2744
3133
  )}
@@ -2753,7 +3142,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2753
3142
  )}
2754
3143
  </div>
2755
3144
  </div>
2756
- <p className={styles.suggestionDescription}>
3145
+ <p className={styles.detectionDescription}>
2757
3146
  {suggestion.description ||
2758
3147
  `Link ${getTableDisplayName(
2759
3148
  selectedTable,
@@ -2765,24 +3154,47 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2765
3154
  </p>
2766
3155
  </div>
2767
3156
  <button
2768
- className={`${styles.btn} ${styles.btnPrimary}`}
3157
+ className={`${styles.connectIconBtn} ${
3158
+ joins.some(
3159
+ (j) =>
3160
+ j.targetTable ===
3161
+ suggestion.targetTable
3162
+ )
3163
+ ? styles.connected
3164
+ : ''
3165
+ }`}
2769
3166
  onClick={() => addJoin(suggestion)}
2770
3167
  disabled={joins.some(
2771
3168
  (j) =>
2772
3169
  j.targetTable ===
2773
3170
  suggestion.targetTable
2774
3171
  )}
3172
+ title={
3173
+ joins.some(
3174
+ (j) =>
3175
+ j.targetTable ===
3176
+ suggestion.targetTable
3177
+ )
3178
+ ? 'Already connected'
3179
+ : `Connect to ${getSmartRelationshipName(
3180
+ suggestion.targetTable,
3181
+ definition
3182
+ )}`
3183
+ }
2775
3184
  >
2776
3185
  {joins.some(
2777
3186
  (j) =>
2778
3187
  j.targetTable ===
2779
3188
  suggestion.targetTable
2780
- )
2781
- ? 'Connected'
2782
- : 'Connect'}
3189
+ ) ? (
3190
+ <CircleCheck size={20} />
3191
+ ) : (
3192
+ <LinkChain size={20} />
3193
+ )}
2783
3194
  </button>
2784
3195
  </div>
2785
3196
  ))}
3197
+ </div>
2786
3198
  </div>
2787
3199
  )}
2788
3200
 
@@ -2799,6 +3211,12 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2799
3211
  Can't find the connection you need? Create a custom
2800
3212
  relationship between tables.
2801
3213
  </p>
3214
+ <div className={styles.advancedWarning}>
3215
+ <TriangleAlert size={16} />
3216
+ <span>
3217
+ <strong>Advanced Feature:</strong> Manual relationships require understanding of database table structures and foreign key relationships. Only create manual relationships if you understand how database tables connect.
3218
+ </span>
3219
+ </div>
2802
3220
 
2803
3221
  {!showManualJoinForm ? (
2804
3222
  <button
@@ -2896,7 +3314,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2896
3314
  disabled={!manualJoin.sourceTable}
2897
3315
  >
2898
3316
  <option value="">Select Column</option>
2899
- {manualJoinColumns.source.map((col) => (
3317
+ {Array.isArray(manualJoinColumns.source) && manualJoinColumns.source.map((col) => (
2900
3318
  <option
2901
3319
  key={col.name}
2902
3320
  value={col.name}
@@ -2955,7 +3373,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
2955
3373
  disabled={!manualJoin.targetTable}
2956
3374
  >
2957
3375
  <option value="">Select Column</option>
2958
- {manualJoinColumns.target.map((col) => (
3376
+ {Array.isArray(manualJoinColumns.target) && manualJoinColumns.target.map((col) => (
2959
3377
  <option
2960
3378
  key={col.name}
2961
3379
  value={col.name}
@@ -3345,47 +3763,430 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
3345
3763
  // Advanced filter rendering (from original GenericReport)
3346
3764
  const renderFilters = () => {
3347
3765
  const availableColumns = getAvailableFilterColumns();
3766
+
3767
+ // Smart suggestions for sorting and filtering based on table type and relationships
3768
+ const getSmartSuggestions = () => {
3769
+ const suggestions = {
3770
+ sorting: [],
3771
+ filters: []
3772
+ };
3773
+
3774
+ if (!selectedTable || !tableColumns) return [];
3775
+
3776
+ // Get table display name for better suggestions
3777
+ const tableDisplayName = getTableDisplayName(selectedTable, definition);
3778
+
3779
+ // Define smart patterns based on table types and common business logic
3780
+ const tablePatterns = {
3781
+ // User/Client related tables
3782
+ users: { priority: ['name', 'created_at', 'updated_at', 'email'], filters: ['active', 'status', 'created_at', 'email', 'role'] },
3783
+ clients: { priority: ['name', 'created_at', 'updated_at', 'email'], filters: ['active', 'status', 'created_at', 'email', 'phone', 'type'] },
3784
+ contacts: { priority: ['name', 'email', 'created_at'], filters: ['active', 'client_id', 'email', 'phone', 'created_at'] },
3785
+
3786
+ // Project/Lead related tables
3787
+ leads: { priority: ['created_at', 'updated_at', 'name', 'status'], filters: ['status', 'source', 'priority', 'created_at', 'assigned_to', 'email'] },
3788
+ projects: { priority: ['created_at', 'deadline', 'name', 'status'], filters: ['status', 'priority', 'created_at', 'due_date', 'client_id', 'assigned_to'] },
3789
+ tasks: { priority: ['created_at', 'deadline', 'priority'], filters: ['status', 'priority', 'assigned_to', 'created_at', 'due_date'] },
3790
+ surveys: { priority: ['created_at', 'updated_at', 'status'], filters: ['status', 'type', 'created_at', 'active'] },
3791
+
3792
+ // Location related tables
3793
+ sites: { priority: ['name', 'address', 'created_at'], filters: ['active', 'state', 'client_id', 'type', 'status'] },
3794
+ locations: { priority: ['name', 'address', 'created_at'], filters: ['active', 'state', 'type', 'status'] },
3795
+
3796
+ // Reference/lookup tables
3797
+ industries: { priority: ['name', 'id'], filters: ['active'] },
3798
+ sources: { priority: ['name', 'id'], filters: ['active', 'type'] },
3799
+ actions: { priority: ['name', 'priority'], filters: ['active', 'priority', 'type'] },
3800
+ tags: { priority: ['name', 'created_at'], filters: ['active', 'type', 'category'] },
3801
+
3802
+ // Document/content tables
3803
+ documents: { priority: ['created_at', 'name', 'type'], filters: ['type', 'status', 'created_at', 'category', 'owner_id'] },
3804
+ notes: { priority: ['created_at', 'updated_at'], filters: ['created_at', 'user_id', 'type', 'priority'] },
3805
+
3806
+ // Communication tables
3807
+ emails: { priority: ['created_at', 'subject'], filters: ['status', 'created_at', 'from_email', 'to_email', 'type'] },
3808
+ calls: { priority: ['created_at', 'duration'], filters: ['status', 'created_at', 'type', 'outcome'] },
3809
+ messages: { priority: ['created_at', 'subject'], filters: ['status', 'created_at', 'type', 'priority'] },
3810
+
3811
+ // Transactional tables
3812
+ orders: { priority: ['created_at', 'status', 'total'], filters: ['status', 'created_at', 'client_id', 'type'] },
3813
+ invoices: { priority: ['created_at', 'due_date', 'status'], filters: ['status', 'created_at', 'due_date', 'client_id'] },
3814
+ bookings: { priority: ['date', 'created_at', 'status'], filters: ['status', 'date', 'created_at', 'client_id', 'type'] },
3815
+ facilities: { priority: ['name', 'type', 'rate'], filters: ['type', 'active', 'status', 'category'] },
3816
+
3817
+ // Financial tables
3818
+ payments: { priority: ['created_at', 'amount', 'status'], filters: ['status', 'created_at', 'method', 'type'] },
3819
+ transactions: { priority: ['created_at', 'amount', 'type'], filters: ['type', 'status', 'created_at', 'category'] },
3820
+ };
3821
+
3822
+ // Get the pattern for this table or fall back to generic patterns
3823
+ const pattern = tablePatterns[selectedTable] || {
3824
+ priority: ['created_at', 'updated_at', 'name', 'title', 'id'],
3825
+ filters: ['status', 'active', 'created_at']
3826
+ };
3827
+
3828
+ // Helper function to check if column exists in the table
3829
+ const columnExists = (columnName, table = selectedTable) => {
3830
+ if (table === selectedTable) {
3831
+ return tableColumns.some(col => col.name.toLowerCase() === columnName.toLowerCase());
3832
+ }
3833
+ // Check joined tables
3834
+ const join = joins.find(j => j.targetTable === table);
3835
+ return join && join.availableColumns && join.availableColumns.some(col => col.name.toLowerCase() === columnName.toLowerCase());
3836
+ };
3837
+
3838
+ // Generate sorting suggestions
3839
+ pattern.priority.forEach((columnName) => {
3840
+ if (columnExists(columnName)) {
3841
+ const isDateColumn = ['created_at', 'updated_at', 'date', 'deadline', 'due_date'].includes(columnName.toLowerCase());
3842
+ const direction = isDateColumn ? 'desc' : 'asc';
3843
+
3844
+ suggestions.sorting.push({
3845
+ type: 'sorting',
3846
+ table: selectedTable,
3847
+ column: columnName,
3848
+ direction: direction,
3849
+ title: `Sort by ${formatName(columnName)} (${direction === 'desc' ? 'Newest First' : 'A-Z'})`,
3850
+ description: `Order results by ${formatName(columnName)} in ${direction === 'desc' ? 'descending' : 'ascending'} order`,
3851
+ reasoning: isDateColumn ? 'Show most recent records first' : 'Alphabetical ordering for easy browsing'
3852
+ });
3853
+ }
3854
+ });
3855
+
3856
+ // Generate filter suggestions
3857
+ pattern.filters.forEach(columnName => {
3858
+ if (columnExists(columnName)) {
3859
+ const columnLower = columnName.toLowerCase();
3860
+
3861
+ // Special handling for common filter types
3862
+ if (['active', 'status'].includes(columnLower)) {
3863
+ // Active/Status filters
3864
+ if (columnLower === 'active') {
3865
+ suggestions.filters.push({
3866
+ type: 'filter',
3867
+ table: selectedTable,
3868
+ column: columnName,
3869
+ operator: '=',
3870
+ value: '1',
3871
+ title: 'Show only active records',
3872
+ description: `Filter to show only active ${tableDisplayName.toLowerCase()}`,
3873
+ reasoning: 'Focus on currently relevant records'
3874
+ });
3875
+ } else if (columnLower === 'status') {
3876
+ // Multiple status suggestions based on table type
3877
+ const statusSuggestions = [];
3878
+
3879
+ if (['leads', 'projects', 'tasks'].includes(selectedTable.toLowerCase())) {
3880
+ statusSuggestions.push({
3881
+ value: 'active',
3882
+ title: 'Show active items',
3883
+ description: `Show active ${tableDisplayName.toLowerCase()}`
3884
+ });
3885
+ statusSuggestions.push({
3886
+ value: 'pending',
3887
+ title: 'Show pending items',
3888
+ description: `Show pending ${tableDisplayName.toLowerCase()}`
3889
+ });
3890
+ } else if (['orders', 'bookings', 'invoices'].includes(selectedTable.toLowerCase())) {
3891
+ statusSuggestions.push({
3892
+ value: 'confirmed',
3893
+ title: 'Show confirmed items',
3894
+ description: `Show confirmed ${tableDisplayName.toLowerCase()}`
3895
+ });
3896
+ statusSuggestions.push({
3897
+ value: 'pending',
3898
+ title: 'Show pending items',
3899
+ description: `Show pending ${tableDisplayName.toLowerCase()}`
3900
+ });
3901
+ } else {
3902
+ statusSuggestions.push({
3903
+ value: 'active',
3904
+ title: 'Show active records',
3905
+ description: `Show active ${tableDisplayName.toLowerCase()}`
3906
+ });
3907
+ }
3908
+
3909
+ statusSuggestions.forEach(statusSug => {
3910
+ suggestions.filters.push({
3911
+ type: 'filter',
3912
+ table: selectedTable,
3913
+ column: columnName,
3914
+ operator: '=',
3915
+ value: statusSug.value,
3916
+ title: statusSug.title,
3917
+ description: statusSug.description,
3918
+ reasoning: 'Filter by status to focus on specific record states'
3919
+ });
3920
+ });
3921
+ }
3922
+ } else if (['created_at', 'updated_at', 'date', 'due_date', 'deadline'].includes(columnLower)) {
3923
+ // Date-based filters
3924
+ const dateFilters = [
3925
+ {
3926
+ days: 7,
3927
+ title: 'Last 7 days',
3928
+ description: `Show ${tableDisplayName.toLowerCase()} from the last week`
3929
+ },
3930
+ {
3931
+ days: 30,
3932
+ title: 'Last 30 days',
3933
+ description: `Show ${tableDisplayName.toLowerCase()} from the last month`
3934
+ },
3935
+ {
3936
+ days: 90,
3937
+ title: 'Last 3 months',
3938
+ description: `Show ${tableDisplayName.toLowerCase()} from the last quarter`
3939
+ }
3940
+ ];
3941
+
3942
+ dateFilters.forEach(dateFilter => {
3943
+ suggestions.filters.push({
3944
+ type: 'filter',
3945
+ table: selectedTable,
3946
+ column: columnName,
3947
+ operator: '>=',
3948
+ value: moment().subtract(dateFilter.days, 'days').format('YYYY-MM-DD'),
3949
+ title: `${dateFilter.title} (${formatName(columnName)})`,
3950
+ description: dateFilter.description,
3951
+ reasoning: 'Focus on recent activity and data'
3952
+ });
3953
+ });
3954
+ } else if (['priority', 'importance', 'urgency'].includes(columnLower)) {
3955
+ // Priority-based filters
3956
+ const priorityFilters = [
3957
+ { value: 'high', title: 'High priority only' },
3958
+ { value: 'medium', title: 'Medium priority and above' },
3959
+ { value: 'urgent', title: 'Urgent items only' }
3960
+ ];
3961
+
3962
+ priorityFilters.forEach(priorityFilter => {
3963
+ suggestions.filters.push({
3964
+ type: 'filter',
3965
+ table: selectedTable,
3966
+ column: columnName,
3967
+ operator: '=',
3968
+ value: priorityFilter.value,
3969
+ title: priorityFilter.title,
3970
+ description: `Filter to show ${priorityFilter.value} priority ${tableDisplayName.toLowerCase()}`,
3971
+ reasoning: 'Focus on high-priority items that need attention'
3972
+ });
3973
+ });
3974
+ } else if (['type', 'category', 'kind'].includes(columnLower)) {
3975
+ // Type/Category filters - generic suggestions
3976
+ suggestions.filters.push({
3977
+ type: 'filter',
3978
+ table: selectedTable,
3979
+ column: columnName,
3980
+ operator: '!=',
3981
+ value: '',
3982
+ title: `Show records with ${formatName(columnName)}`,
3983
+ description: `Filter out records without a ${formatName(columnName)} specified`,
3984
+ reasoning: 'Focus on categorized records'
3985
+ });
3986
+ } else if (['email', 'phone', 'mobile'].includes(columnLower)) {
3987
+ // Contact information filters
3988
+ suggestions.filters.push({
3989
+ type: 'filter',
3990
+ table: selectedTable,
3991
+ column: columnName,
3992
+ operator: '!=',
3993
+ value: '',
3994
+ title: `Has ${formatName(columnName)}`,
3995
+ description: `Show only records with ${formatName(columnName)} information`,
3996
+ reasoning: 'Focus on records with contact information'
3997
+ });
3998
+ } else if (['client_id', 'user_id', 'owner_id', 'assigned_to'].includes(columnLower)) {
3999
+ // Relationship filters
4000
+ suggestions.filters.push({
4001
+ type: 'filter',
4002
+ table: selectedTable,
4003
+ column: columnName,
4004
+ operator: '!=',
4005
+ value: '',
4006
+ title: `Has ${formatName(columnName.replace('_id', ''))}`,
4007
+ description: `Show only records with an assigned ${formatName(columnName.replace('_id', ''))}`,
4008
+ reasoning: 'Focus on records with assigned relationships'
4009
+ });
4010
+ }
4011
+ }
4012
+ });
4013
+
4014
+ // Add filter suggestions from joined tables
4015
+ joins.forEach(join => {
4016
+ if (join.targetTable && join.availableColumns) {
4017
+ const joinTableDisplay = getTableDisplayName(join.targetTable, definition);
4018
+
4019
+ // Add filter suggestions from joined tables
4020
+ ['status', 'active', 'type', 'priority'].forEach(columnName => {
4021
+ if (columnExists(columnName, join.targetTable)) {
4022
+ suggestions.filters.push({
4023
+ type: 'filter',
4024
+ table: join.targetTable,
4025
+ column: columnName,
4026
+ operator: '=',
4027
+ value: columnName === 'active' ? '1' : 'active',
4028
+ title: `Filter by ${joinTableDisplay} ${formatName(columnName)}`,
4029
+ description: `Show records based on ${formatName(columnName)} from connected ${joinTableDisplay.toLowerCase()}`,
4030
+ reasoning: `Use related ${joinTableDisplay.toLowerCase()} data for filtering`
4031
+ });
4032
+ }
4033
+ });
4034
+
4035
+ // Add date filters for joined tables
4036
+ ['created_at', 'updated_at'].forEach(columnName => {
4037
+ if (columnExists(columnName, join.targetTable)) {
4038
+ suggestions.filters.push({
4039
+ type: 'filter',
4040
+ table: join.targetTable,
4041
+ column: columnName,
4042
+ operator: '>=',
4043
+ value: moment().subtract(30, 'days').format('YYYY-MM-DD'),
4044
+ title: `Recent ${joinTableDisplay} (Last 30 days)`,
4045
+ description: `Filter by ${joinTableDisplay.toLowerCase()} ${formatName(columnName)} in the last 30 days`,
4046
+ reasoning: `Focus on recent ${joinTableDisplay.toLowerCase()} activity`
4047
+ });
4048
+ }
4049
+ });
4050
+ }
4051
+ });
4052
+
4053
+ // Combine all suggestions into a single array (prioritize filters)
4054
+ const allSuggestions = [...suggestions.sorting.slice(0, 3), ...suggestions.filters.slice(0, 5)];
4055
+
4056
+ // Check which suggestions are already applied
4057
+ const enhancedSuggestions = allSuggestions.map(suggestion => {
4058
+ let alreadyApplied = false;
4059
+
4060
+ if (suggestion.type === 'sorting') {
4061
+ // Check if this sort criterion already exists
4062
+ alreadyApplied = sortCriteria.some(
4063
+ criteria => criteria.table === suggestion.table &&
4064
+ criteria.column === suggestion.column &&
4065
+ criteria.direction === suggestion.direction
4066
+ );
4067
+ } else if (suggestion.type === 'filter') {
4068
+ // Check if this filter already exists in any group
4069
+ alreadyApplied = filterCriteria.groups && filterCriteria.groups.some(group =>
4070
+ group.filters.some(filter =>
4071
+ filter.table === suggestion.table &&
4072
+ filter.column === suggestion.column &&
4073
+ filter.operator === suggestion.operator &&
4074
+ filter.value === suggestion.value
4075
+ )
4076
+ );
4077
+ }
4078
+
4079
+ return {
4080
+ ...suggestion,
4081
+ alreadyApplied
4082
+ };
4083
+ });
4084
+
4085
+ // Return prioritized suggestions (max 6-8 for clean interface)
4086
+ return enhancedSuggestions.slice(0, 8);
4087
+ };
4088
+
4089
+ const smartSuggestions = getSmartSuggestions();
4090
+
4091
+ // Debug logging to check if suggestions are generated
4092
+ console.log('Smart suggestions debug:', {
4093
+ selectedTable,
4094
+ tableColumns: tableColumns?.length,
4095
+ smartSuggestions: smartSuggestions?.length,
4096
+ suggestions: smartSuggestions
4097
+ });
3348
4098
 
3349
4099
  return (
3350
4100
  <div className={styles.filtersSection}>
4101
+ {/* Smart Suggestions Section */}
4102
+ {smartSuggestions.length > 0 && (
4103
+ <div className={styles.reportSection}>
4104
+ <div className={styles.sectionHeader}>
4105
+ <h3>
4106
+ <Rss size={20} />
4107
+ Smart Suggestions
4108
+ </h3>
4109
+ <span className={styles.suggestionsCount}>
4110
+ {smartSuggestions.length} suggestion{smartSuggestions.length !== 1 ? 's' : ''}
4111
+ </span>
4112
+ </div>
4113
+
4114
+ <div className={styles.smartSuggestionsContainer}>
4115
+ <p className={styles.suggestionsIntro}>
4116
+ Based on your selected data source "{formatName(selectedTable)}" and relationships,
4117
+ here are some recommended sorting and filtering options:
4118
+ </p>
4119
+
4120
+ <div className={styles.suggestionsGrid}>
4121
+ {smartSuggestions.map((suggestion, index) => (
4122
+ <div key={index} className={styles.suggestionCard}>
4123
+ <div className={styles.suggestionHeader}>
4124
+ <span className={styles.suggestionTitle}>
4125
+ {suggestion.title}
4126
+ </span>
4127
+ <span className={styles.suggestionType}>
4128
+ {suggestion.type}
4129
+ </span>
4130
+ </div>
4131
+
4132
+ <p className={styles.suggestionDescription}>
4133
+ {suggestion.description}
4134
+ </p>
4135
+
4136
+ <div className={styles.suggestionActions}>
4137
+ <button
4138
+ className={`${styles.btn} ${styles.btnPrimary}`}
4139
+ onClick={() => applySuggestion(suggestion)}
4140
+ disabled={suggestion.alreadyApplied}
4141
+ >
4142
+ {suggestion.alreadyApplied ? 'Applied' : 'Apply'}
4143
+ </button>
4144
+ {suggestion.reasoning && (
4145
+ <span className={styles.suggestionReasoning}>
4146
+ {suggestion.reasoning}
4147
+ </span>
4148
+ )}
4149
+ </div>
4150
+ </div>
4151
+ ))}
4152
+ </div>
4153
+ </div>
4154
+ </div>
4155
+ )}
4156
+
3351
4157
  {/* Sorting Section */}
3352
4158
  <div className={styles.reportSection}>
3353
4159
  <div
3354
4160
  className={styles.sectionHeader}
4161
+ onClick={() => setShowSortingSection(!showSortingSection)}
3355
4162
  title={
3356
4163
  showSortingSection
3357
4164
  ? 'Hide sorting options'
3358
4165
  : 'Show sorting options'
3359
4166
  }
3360
4167
  >
3361
- <h3
3362
- onClick={() =>
3363
- setShowSortingSection(!showSortingSection)
3364
- }
3365
- style={{ cursor: 'pointer' }}
3366
- >
4168
+ <h3>
4169
+ <Data size={20} />
3367
4170
  Sorting
3368
4171
  </h3>
3369
4172
  <div className={styles.sectionHeaderActions}>
3370
4173
  <button
3371
- className={styles.btn}
3372
- onClick={handleAddSortCriterion}
4174
+ className={`${styles.btn} ${styles.btnSecondary}`}
4175
+ onClick={(e) => {
4176
+ e.stopPropagation();
4177
+ handleAddSortCriterion();
4178
+ }}
3373
4179
  disabled={selectedColumns.length === 0}
3374
4180
  >
3375
- Add Sort Criterion
4181
+ <CirclePlus size={16} />
4182
+ Add Sort
3376
4183
  </button>
3377
4184
  <div
3378
- className={`${styles.toggleBtn} ${
3379
- showSortingSection
3380
- ? styles.toggleBtnActive
3381
- : ''
4185
+ className={`${styles.streamlinedToggle} ${
4186
+ showSortingSection ? styles.expanded : ''
3382
4187
  }`}
3383
- onClick={() =>
3384
- setShowSortingSection(!showSortingSection)
3385
- }
3386
- style={{ cursor: 'pointer' }}
3387
4188
  >
3388
- {showSortingSection ? '▲' : '▼'}
4189
+ <ChevronDown size={16} />
3389
4190
  </div>
3390
4191
  </div>
3391
4192
  </div>
@@ -3393,76 +4194,32 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
3393
4194
  {showSortingSection && (
3394
4195
  <>
3395
4196
  {sortCriteria.length > 0 && (
3396
- <div className={styles.filterGroup}>
3397
- <div className={styles.joinTablesContainer}>
3398
- {sortCriteria.map(
3399
- (criterion, index) => (
3400
- <div
3401
- key={index}
3402
- className={
3403
- styles.joinSection
3404
- }
3405
- data-sort-index={index + 1}
4197
+ <div className={styles.sortingContainer}>
4198
+ {sortCriteria.map((criterion, index) => (
4199
+ <div
4200
+ key={index}
4201
+ className={styles.sortingItem}
4202
+ >
4203
+ <div className={styles.sortingHeader}>
4204
+ <h4>
4205
+ Sort #{index + 1} - {criterion.table && criterion.column
4206
+ ? `${formatName(criterion.table)}.${formatName(criterion.column)}`
4207
+ : 'Configure column'}
4208
+ </h4>
4209
+ <button
4210
+ className={styles.removeSortBtn}
4211
+ onClick={() => handleRemoveSortCriterion(index)}
4212
+ title="Remove Sort Criterion"
3406
4213
  >
3407
- <div
3408
- className={
3409
- styles.joinHeader
3410
- }
3411
- >
3412
- <h4
3413
- data-sort-index={
3414
- index + 1
3415
- }
3416
- >
3417
- by{' '}
3418
- {criterion.table &&
3419
- criterion.column
3420
- ? `${formatName(
3421
- criterion.table
3422
- )}.${formatName(
3423
- criterion.column
3424
- )}`
3425
- : `Criterion #${
3426
- index + 1
3427
- }`}
3428
- </h4>
3429
- <button
3430
- className={
3431
- styles.removeJoinBtn
3432
- }
3433
- onClick={() =>
3434
- handleRemoveSortCriterion(
3435
- index
3436
- )
3437
- }
3438
- title="Remove Sort Criterion"
3439
- >
3440
- ×
3441
- </button>
3442
- </div>
4214
+ ×
4215
+ </button>
4216
+ </div>
3443
4217
 
3444
- <div
3445
- className={
3446
- styles.joinGrid
3447
- }
3448
- >
3449
- <div
3450
- className={
3451
- styles.joinGridColumn
3452
- }
3453
- >
3454
- <div
3455
- className={
3456
- styles.joinField
3457
- }
3458
- >
3459
- <label>
3460
- Column:
3461
- </label>
3462
- <select
3463
- className={
3464
- styles.selectControl
3465
- }
4218
+ <div className={styles.sortingControls}>
4219
+ <div className={styles.sortingField}>
4220
+ <label>Column:</label>
4221
+ <select
4222
+ className={styles.sortingSelect}
3466
4223
  value={
3467
4224
  criterion.table &&
3468
4225
  criterion.column
@@ -3577,26 +4334,12 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
3577
4334
  ) : null
3578
4335
  )}
3579
4336
  </select>
3580
- </div>
3581
- </div>
4337
+ </div>
3582
4338
 
3583
- <div
3584
- className={
3585
- styles.joinGridColumn
3586
- }
3587
- >
3588
- <div
3589
- className={
3590
- styles.joinField
3591
- }
3592
- >
3593
- <label>
3594
- Direction:
3595
- </label>
3596
- <select
3597
- className={
3598
- styles.selectControl
3599
- }
4339
+ <div className={styles.sortingField}>
4340
+ <label>Direction:</label>
4341
+ <select
4342
+ className={styles.sortingSelect}
3600
4343
  value={
3601
4344
  criterion.direction
3602
4345
  }
@@ -3618,14 +4361,11 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
3618
4361
  <option value="DESC">
3619
4362
  Descending
3620
4363
  </option>
3621
- </select>
3622
- </div>
3623
- </div>
3624
- </div>
4364
+ </select>
3625
4365
  </div>
3626
- )
3627
- )}
3628
- </div>
4366
+ </div>
4367
+ </div>
4368
+ ))}
3629
4369
  </div>
3630
4370
  )}
3631
4371
  </>
@@ -4054,7 +4794,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4054
4794
  }
4055
4795
  };
4056
4796
 
4057
- // Export report in different formats (replicating GenericReport pattern)
4797
+ // Export report in different formats with proper binary handling
4058
4798
  const exportReport = async (format) => {
4059
4799
  if (!selectedTable || selectedColumns.length === 0) {
4060
4800
  toast.error('Please generate a report first before exporting.');
@@ -4066,6 +4806,41 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4066
4806
  return;
4067
4807
  }
4068
4808
 
4809
+ // Check for large datasets when exporting to PDF
4810
+ if (format === 'pdf' && previewData.length > 1000) {
4811
+ const result = await Swal.fire({
4812
+ title: '⚠️ Large Dataset Warning',
4813
+ html: `
4814
+ <div style="text-align: left;">
4815
+ <p>You are trying to export <strong>${previewData.length} rows</strong> to PDF.</p>
4816
+ <p>Large PDF exports may:</p>
4817
+ <ul>
4818
+ <li>Take a long time to generate</li>
4819
+ <li>Use significant server memory</li>
4820
+ <li>Result in very large files</li>
4821
+ <li>Potentially fail due to memory limits</li>
4822
+ </ul>
4823
+ <p><strong>Recommendations:</strong></p>
4824
+ <ul>
4825
+ <li>Apply filters to reduce the dataset</li>
4826
+ <li>Use Excel/CSV format for large exports</li>
4827
+ <li>Limit to first 1000 rows for PDF</li>
4828
+ </ul>
4829
+ </div>
4830
+ `,
4831
+ icon: 'warning',
4832
+ showCancelButton: true,
4833
+ confirmButtonText: 'Export Anyway',
4834
+ cancelButtonText: 'Cancel',
4835
+ confirmButtonColor: '#d33',
4836
+ cancelButtonColor: '#3085d6',
4837
+ });
4838
+
4839
+ if (!result.isConfirmed) {
4840
+ return;
4841
+ }
4842
+ }
4843
+
4069
4844
  try {
4070
4845
  // Use exact same payload format as GenericReport
4071
4846
  const queryConfig = {
@@ -4077,6 +4852,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4077
4852
  })),
4078
4853
  joins: joins.map((join) => ({
4079
4854
  joinType: join.joinType,
4855
+ sourceTable: join.sourceTable || selectedTable,
4080
4856
  targetTable: join.targetTable,
4081
4857
  sourceColumn: join.sourceColumn,
4082
4858
  targetColumn: join.targetColumn,
@@ -4090,6 +4866,21 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4090
4866
  format: format === 'excel' ? 'xlsx' : format, // Convert excel to xlsx
4091
4867
  };
4092
4868
 
4869
+ // DEBUG: Log the complete request payload
4870
+ console.group('🐛 PDF Export Debug');
4871
+ console.log('📋 Export Request Details:');
4872
+ console.log('URL:', exportUrl);
4873
+ console.log('Format:', format);
4874
+ console.log('Report Name:', reportName);
4875
+ console.log('Selected Table:', selectedTable);
4876
+ console.log('Selected Columns:', selectedColumns);
4877
+ console.log('Joins:', joins);
4878
+ console.log('Filter Criteria:', filterCriteria);
4879
+ console.log('Flattened Filters:', flattenFilterCriteria(filterCriteria));
4880
+ console.log('Sort Criteria:', sortCriteria);
4881
+ console.log('🚀 Final Payload:', JSON.stringify(exportData, null, 2));
4882
+ console.groupEnd();
4883
+
4093
4884
  // Generate filename with timestamp
4094
4885
  const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
4095
4886
  const fileName = `${reportName.replace(
@@ -4097,20 +4888,90 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4097
4888
  '_'
4098
4889
  )}_${timestamp}.${format === 'excel' ? 'xlsx' : format}`;
4099
4890
 
4100
- // Use Download component like GenericReport
4891
+ console.log('📁 Generated filename:', fileName);
4892
+
4893
+ // Use Download component which handles error checking and blob download properly
4894
+ console.log('📡 Making request to:', exportUrl);
4101
4895
  const result = await Download(exportUrl, 'POST', exportData);
4102
4896
 
4897
+ console.log('✅ Download component returned result:', result);
4898
+
4103
4899
  if (result && result.data) {
4900
+ console.log('✅ Result has data, triggering download');
4901
+ console.log('📄 Result data type:', typeof result.data);
4902
+ console.log('📄 Result data instanceof Blob:', result.data instanceof Blob);
4903
+
4104
4904
  // Use saveAs to trigger download
4105
4905
  saveAs(result.data, fileName);
4106
4906
  toast.success(`Report exported as ${format.toUpperCase()}`);
4107
4907
  } else {
4908
+ console.error('❌ No result data received');
4909
+ console.log('Full result object:', result);
4108
4910
  toast.error(
4109
4911
  `Failed to export report as ${format.toUpperCase()}`
4110
4912
  );
4111
4913
  }
4112
4914
  } catch (error) {
4113
- toast.error(`Failed to export report as ${format.toUpperCase()}`);
4915
+ // Handle binary error responses properly
4916
+ console.group('❌ Export Error Debug');
4917
+ console.error('Error object:', error);
4918
+ console.log('Error name:', error.name);
4919
+ console.log('Error message:', error.message);
4920
+ console.log('Error stack:', error.stack);
4921
+
4922
+ let errorMessage = `Failed to export report as ${format.toUpperCase()}`;
4923
+
4924
+ if (error.response) {
4925
+ console.log('📡 Error response received');
4926
+ console.log('Response status:', error.response.status);
4927
+ console.log('Response statusText:', error.response.statusText);
4928
+ console.log('Response headers:', error.response.headers);
4929
+ console.log('Response data type:', typeof error.response.data);
4930
+ console.log('Response data instanceof Blob:', error.response.data instanceof Blob);
4931
+ console.log('Raw response data:', error.response.data);
4932
+
4933
+ if (error.response.data instanceof Blob) {
4934
+ // If error response is a blob, try to read it as text
4935
+ try {
4936
+ console.log('🔍 Attempting to parse blob error response');
4937
+ const errorText = await error.response.data.text();
4938
+ console.log('📄 Blob content as text:', errorText);
4939
+
4940
+ try {
4941
+ const errorData = JSON.parse(errorText);
4942
+ console.log('📄 Parsed JSON error data:', errorData);
4943
+ errorMessage = errorData.message || errorData.error || errorMessage;
4944
+ } catch (jsonError) {
4945
+ console.error('❌ Failed to parse JSON from blob:', jsonError);
4946
+ errorMessage = errorText || errorMessage;
4947
+ }
4948
+ } catch (parseError) {
4949
+ // If parsing fails, use default message
4950
+ console.error('❌ Could not parse error response blob:', parseError);
4951
+ }
4952
+ } else if (error.response.data && error.response.data.message) {
4953
+ console.log('📄 Using error.response.data.message:', error.response.data.message);
4954
+ errorMessage = error.response.data.message;
4955
+ } else if (error.response.data && error.response.data.error) {
4956
+ console.log('📄 Using error.response.data.error:', error.response.data.error);
4957
+ errorMessage = error.response.data.error;
4958
+ }
4959
+
4960
+ // Handle specific 500 errors with empty response (likely memory issues)
4961
+ if (error.response.status === 500 && !error.response.data) {
4962
+ errorMessage = `Server error during ${format.toUpperCase()} generation. This may be due to:
4963
+ • Large dataset size (${previewData.length} rows)
4964
+ • Memory limitations on the server
4965
+ • Try reducing the dataset or using Excel/CSV format`;
4966
+ }
4967
+ } else {
4968
+ console.log('❌ No error.response available');
4969
+ }
4970
+
4971
+ console.log('🔔 Final error message:', errorMessage);
4972
+ console.groupEnd();
4973
+
4974
+ toast.error(errorMessage);
4114
4975
  console.error('Export error:', error);
4115
4976
  }
4116
4977
  };
@@ -4128,11 +4989,15 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4128
4989
  const isExistingReport = loadedReportId && !saveAs;
4129
4990
  const dialogTitle = isExistingReport ? 'Update Report' : 'Save Report';
4130
4991
  const confirmButtonText = isExistingReport
4131
- ? 'Update Report'
4992
+ ? 'Update'
4132
4993
  : 'Save Report';
4133
4994
 
4134
4995
  const result = await Swal.fire({
4135
4996
  title: dialogTitle,
4997
+ customClass: {
4998
+ confirmButton: 'swal-button-fixed-width',
4999
+ cancelButton: 'swal-button-fixed-width'
5000
+ },
4136
5001
  html: `
4137
5002
  <div style="text-align: left;">
4138
5003
  ${
@@ -4326,7 +5191,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4326
5191
  </p>
4327
5192
  <div className={styles.exportButtons}>
4328
5193
  <button
4329
- className={`${styles.btn} ${styles.btnSecondary}`}
5194
+ className={`${styles.btn} ${styles.btnExportExcel}`}
4330
5195
  onClick={() => exportReport('excel')}
4331
5196
  disabled={
4332
5197
  isLoading ||
@@ -4343,7 +5208,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4343
5208
  Export as Excel
4344
5209
  </button>
4345
5210
  <button
4346
- className={`${styles.btn} ${styles.btnSecondary}`}
5211
+ className={`${styles.btn} ${styles.btnExportCsv}`}
4347
5212
  onClick={() => exportReport('csv')}
4348
5213
  disabled={
4349
5214
  isLoading ||
@@ -4360,7 +5225,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
4360
5225
  Export as CSV
4361
5226
  </button>
4362
5227
  <button
4363
- className={`${styles.btn} ${styles.btnSecondary}`}
5228
+ className={`${styles.btn} ${styles.btnExportPdf}`}
4364
5229
  onClick={() => exportReport('pdf')}
4365
5230
  disabled={
4366
5231
  isLoading ||