@visns-studio/visns-components 5.10.5 → 5.10.7
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
|
|
780
|
-
const [
|
|
781
|
-
const [
|
|
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
|
-
|
|
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
|
-
|
|
1183
|
+
fetchDetectedJoins(selectedTable);
|
|
887
1184
|
}
|
|
888
|
-
}, [selectedTable, columnUrl,
|
|
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
|
|
1240
|
-
const
|
|
1241
|
-
if (!tableName || !
|
|
1536
|
+
// Fetch detected joins for a table
|
|
1537
|
+
const fetchDetectedJoins = async (tableName) => {
|
|
1538
|
+
if (!tableName || !detectedJoinsUrl) {
|
|
1242
1539
|
console.warn(
|
|
1243
|
-
'Missing tableName or
|
|
1540
|
+
'Missing tableName or detectedJoinsUrl for fetchDetectedJoins'
|
|
1244
1541
|
);
|
|
1245
1542
|
return;
|
|
1246
1543
|
}
|
|
1247
1544
|
|
|
1248
|
-
|
|
1249
|
-
...
|
|
1545
|
+
setIsLoadingDetectedJoins({
|
|
1546
|
+
...isLoadingDetectedJoins,
|
|
1250
1547
|
[tableName]: true,
|
|
1251
1548
|
});
|
|
1252
1549
|
|
|
1253
1550
|
try {
|
|
1254
|
-
const result = await CustomFetch(
|
|
1551
|
+
const result = await CustomFetch(detectedJoinsUrl, 'POST', {
|
|
1255
1552
|
table: tableName,
|
|
1256
1553
|
});
|
|
1257
1554
|
|
|
1258
|
-
console.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}
|
|
1596
|
+
`Found ${filteredJoins.length} detected joins for ${tableName}:`,
|
|
1300
1597
|
filteredJoins
|
|
1301
1598
|
);
|
|
1302
1599
|
|
|
1303
|
-
|
|
1304
|
-
...
|
|
1600
|
+
setDetectedJoins({
|
|
1601
|
+
...detectedJoins,
|
|
1305
1602
|
[tableName]: filteredJoins,
|
|
1306
1603
|
});
|
|
1307
1604
|
} else {
|
|
1308
1605
|
console.warn(
|
|
1309
|
-
'No valid
|
|
1606
|
+
'No valid detected joins found or invalid response format:',
|
|
1310
1607
|
result
|
|
1311
1608
|
);
|
|
1312
|
-
|
|
1313
|
-
...
|
|
1609
|
+
setDetectedJoins({
|
|
1610
|
+
...detectedJoins,
|
|
1314
1611
|
[tableName]: [],
|
|
1315
1612
|
});
|
|
1316
1613
|
}
|
|
1317
1614
|
}
|
|
1318
1615
|
} catch (error) {
|
|
1319
|
-
console.error('Error fetching
|
|
1320
|
-
|
|
1321
|
-
...
|
|
1616
|
+
console.error('Error fetching detected joins:', error);
|
|
1617
|
+
setDetectedJoins({
|
|
1618
|
+
...detectedJoins,
|
|
1322
1619
|
[tableName]: [],
|
|
1323
1620
|
});
|
|
1324
1621
|
} finally {
|
|
1325
|
-
|
|
1326
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
2676
|
-
const isLoading =
|
|
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
|
-
{/*
|
|
3115
|
+
{/* Detected Relationships */}
|
|
2728
3116
|
{tableSuggestions.length > 0 && (
|
|
2729
|
-
<div className={styles.
|
|
2730
|
-
<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.
|
|
2736
|
-
<div className={styles.
|
|
2737
|
-
<div className={styles.
|
|
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
|
-
{
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2782
|
-
|
|
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
|
-
|
|
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={
|
|
4174
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
4175
|
+
onClick={(e) => {
|
|
4176
|
+
e.stopPropagation();
|
|
4177
|
+
handleAddSortCriterion();
|
|
4178
|
+
}}
|
|
3373
4179
|
disabled={selectedColumns.length === 0}
|
|
3374
4180
|
>
|
|
3375
|
-
|
|
4181
|
+
<CirclePlus size={16} />
|
|
4182
|
+
Add Sort
|
|
3376
4183
|
</button>
|
|
3377
4184
|
<div
|
|
3378
|
-
className={`${styles.
|
|
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
|
-
{
|
|
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.
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
}
|
|
3405
|
-
|
|
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
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
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
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
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
|
-
|
|
3581
|
-
</div>
|
|
4337
|
+
</div>
|
|
3582
4338
|
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
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
|
-
|
|
3622
|
-
</div>
|
|
3623
|
-
</div>
|
|
3624
|
-
</div>
|
|
4364
|
+
</select>
|
|
3625
4365
|
</div>
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
5228
|
+
className={`${styles.btn} ${styles.btnExportPdf}`}
|
|
4364
5229
|
onClick={() => exportReport('pdf')}
|
|
4365
5230
|
disabled={
|
|
4366
5231
|
isLoading ||
|