@visns-studio/visns-components 5.14.9 → 5.14.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -7
- package/package.json +2 -1
- package/src/components/columns/ColumnRenderers.jsx +16 -6
- package/src/components/generic/GenericReport.jsx +1311 -158
- package/src/components/generic/GroupedReportRenderer.jsx +91 -0
- package/src/components/generic/SectionGroupedReport.jsx +388 -0
- package/src/components/generic/groupedReport.css +307 -0
- package/src/components/generic/shared/formatters.js +152 -23
- package/src/components/generic/shared/groupingUtils.js +118 -0
- package/src/components/styles/GenericReport.module.scss +532 -0
|
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import { toast } from 'react-toastify';
|
|
4
4
|
import moment from 'moment';
|
|
5
5
|
import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
|
|
6
|
+
import GroupedReportRenderer from './GroupedReportRenderer';
|
|
7
|
+
import { formatCellContent } from './shared/formatters';
|
|
6
8
|
import {
|
|
7
9
|
HelpCircle as Question,
|
|
8
10
|
Link as LinkChain,
|
|
@@ -29,13 +31,16 @@ import {
|
|
|
29
31
|
Rss,
|
|
30
32
|
Globe,
|
|
31
33
|
Lock,
|
|
34
|
+
Database,
|
|
32
35
|
} from 'lucide-react';
|
|
33
36
|
import CustomFetch from '../Fetch';
|
|
34
37
|
import Download from '../Download';
|
|
35
38
|
import { saveAs } from 'file-saver';
|
|
39
|
+
import * as XLSX from 'xlsx';
|
|
36
40
|
import Swal from 'sweetalert2';
|
|
37
41
|
import styles from '../styles/GenericReport.module.scss';
|
|
38
42
|
import '../styles/SweetAlert.module.css';
|
|
43
|
+
import './groupedReport.css';
|
|
39
44
|
|
|
40
45
|
// Icon mapping for common table types
|
|
41
46
|
const tableIcons = {
|
|
@@ -907,6 +912,22 @@ const wizardSteps = [
|
|
|
907
912
|
tip: 'Filters help you focus on specific data, like only active customers or recent transactions.',
|
|
908
913
|
},
|
|
909
914
|
},
|
|
915
|
+
{
|
|
916
|
+
id: 'grouping',
|
|
917
|
+
title: 'Group Configuration',
|
|
918
|
+
icon: Data,
|
|
919
|
+
description: 'Organize data into groups (optional)',
|
|
920
|
+
guidance: {
|
|
921
|
+
summary: 'Group your data by a specific field for better organization',
|
|
922
|
+
howTo: [
|
|
923
|
+
'Enable grouping to organize data into sections',
|
|
924
|
+
'Choose which field to group by (e.g., Department, Category, Status)',
|
|
925
|
+
'Configure group display options and styling',
|
|
926
|
+
'Skip this step for a standard table report',
|
|
927
|
+
],
|
|
928
|
+
tip: 'Grouping is perfect for reports like "Projects by Facility" or "Sales by Region".',
|
|
929
|
+
},
|
|
930
|
+
},
|
|
910
931
|
{
|
|
911
932
|
id: 'preview',
|
|
912
933
|
title: 'Preview & Save',
|
|
@@ -1016,13 +1037,14 @@ const GenericReportImproved = ({
|
|
|
1016
1037
|
|
|
1017
1038
|
// State for report preview
|
|
1018
1039
|
const [previewData, setPreviewData] = useState([]);
|
|
1040
|
+
const [groupedData, setGroupedData] = useState(null);
|
|
1019
1041
|
const [gridColumns, setGridColumns] = useState([]);
|
|
1020
1042
|
const [isLoading, setIsLoading] = useState(false);
|
|
1021
1043
|
const [hasAutoExecuted, setHasAutoExecuted] = useState(false);
|
|
1022
1044
|
|
|
1023
1045
|
// State for pagination - now using server-side pagination
|
|
1024
1046
|
const [currentPage, setCurrentPage] = useState(1);
|
|
1025
|
-
const [pageSize, setPageSize] = useState(
|
|
1047
|
+
const [pageSize, setPageSize] = useState(10); // Default to 10 to reduce scrolling
|
|
1026
1048
|
const [totalPages, setTotalPages] = useState(0);
|
|
1027
1049
|
const [totalCount, setTotalCount] = useState(0); // Total records from server
|
|
1028
1050
|
|
|
@@ -1039,6 +1061,27 @@ const GenericReportImproved = ({
|
|
|
1039
1061
|
});
|
|
1040
1062
|
const [customCalculatedFields, setCustomCalculatedFields] = useState([]);
|
|
1041
1063
|
|
|
1064
|
+
// State for group configuration
|
|
1065
|
+
const [groupingConfig, setGroupingConfig] = useState({
|
|
1066
|
+
enabled: false,
|
|
1067
|
+
groupByField: '',
|
|
1068
|
+
groupDisplayName: '',
|
|
1069
|
+
sortBy: '',
|
|
1070
|
+
sortDirection: 'asc',
|
|
1071
|
+
rowsPerGroup: 10,
|
|
1072
|
+
showEmptyRows: false,
|
|
1073
|
+
showGroupTotals: true,
|
|
1074
|
+
expandable: true,
|
|
1075
|
+
groupHeaderStyle: 'primary',
|
|
1076
|
+
emptyRowStyle: 'light'
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// State for custom URLs
|
|
1080
|
+
const [customUrls, setCustomUrls] = useState({
|
|
1081
|
+
executeUrl: '',
|
|
1082
|
+
exportUrl: ''
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1042
1085
|
// Note: Removed table search functionality to simplify user experience
|
|
1043
1086
|
|
|
1044
1087
|
// Extract additional settings with defaults
|
|
@@ -1375,13 +1418,30 @@ const GenericReportImproved = ({
|
|
|
1375
1418
|
currentWizardStep,
|
|
1376
1419
|
]);
|
|
1377
1420
|
|
|
1421
|
+
// Reset auto-execute flag when user navigates away from preview step
|
|
1422
|
+
useEffect(() => {
|
|
1423
|
+
if (currentWizardStep !== 5) {
|
|
1424
|
+
setHasAutoExecuted(false);
|
|
1425
|
+
}
|
|
1426
|
+
}, [currentWizardStep]);
|
|
1427
|
+
|
|
1428
|
+
// Reset auto-execute flag when report configuration changes
|
|
1429
|
+
useEffect(() => {
|
|
1430
|
+
setHasAutoExecuted(false);
|
|
1431
|
+
}, [
|
|
1432
|
+
selectedTable,
|
|
1433
|
+
selectedColumns.length,
|
|
1434
|
+
filterCriteria,
|
|
1435
|
+
joins.length,
|
|
1436
|
+
groupingConfig,
|
|
1437
|
+
]);
|
|
1438
|
+
|
|
1378
1439
|
// Auto-execute report when user reaches step 5 (preview step)
|
|
1379
1440
|
useEffect(() => {
|
|
1380
1441
|
if (
|
|
1381
|
-
currentWizardStep ===
|
|
1442
|
+
currentWizardStep === 5 && // Step 6 is index 5 (preview step)
|
|
1382
1443
|
selectedTable &&
|
|
1383
1444
|
selectedColumns.length > 0 &&
|
|
1384
|
-
previewData.length === 0 && // Only if no data loaded yet
|
|
1385
1445
|
!isLoading &&
|
|
1386
1446
|
!hasAutoExecuted // Prevent infinite loops
|
|
1387
1447
|
) {
|
|
@@ -1392,7 +1452,6 @@ const GenericReportImproved = ({
|
|
|
1392
1452
|
currentWizardStep,
|
|
1393
1453
|
selectedTable,
|
|
1394
1454
|
selectedColumns.length,
|
|
1395
|
-
previewData.length,
|
|
1396
1455
|
isLoading,
|
|
1397
1456
|
hasAutoExecuted,
|
|
1398
1457
|
]);
|
|
@@ -1496,8 +1555,10 @@ const GenericReportImproved = ({
|
|
|
1496
1555
|
return selectedColumns.length > 0;
|
|
1497
1556
|
case 'filters':
|
|
1498
1557
|
return true; // Optional step
|
|
1558
|
+
case 'grouping':
|
|
1559
|
+
return true; // Optional step - always complete (user can skip or configure)
|
|
1499
1560
|
case 'preview':
|
|
1500
|
-
return previewData.length > 0;
|
|
1561
|
+
return previewData.length > 0 || groupedData !== null;
|
|
1501
1562
|
default:
|
|
1502
1563
|
return false;
|
|
1503
1564
|
}
|
|
@@ -2191,12 +2252,80 @@ const GenericReportImproved = ({
|
|
|
2191
2252
|
|
|
2192
2253
|
// Create dataSource function for server-side pagination (similar to DataGrid)
|
|
2193
2254
|
const dataSource = useCallback(
|
|
2194
|
-
async ({ skip, limit, sortInfo, filterValue }) => {
|
|
2255
|
+
async ({ skip, limit, sortInfo, filterValue, grouping, template }) => {
|
|
2195
2256
|
if (!selectedTable || selectedColumns.length === 0) {
|
|
2196
2257
|
return { data: [], count: 0 };
|
|
2197
2258
|
}
|
|
2198
2259
|
|
|
2199
2260
|
try {
|
|
2261
|
+
// Check if this is a grouped report
|
|
2262
|
+
const isGroupedReport = grouping?.enabled || selectedTemplate?.grouping?.enabled;
|
|
2263
|
+
|
|
2264
|
+
if (isGroupedReport) {
|
|
2265
|
+
// Check if we're using a dynamic template (custom grouping) or predefined template
|
|
2266
|
+
const isDynamicTemplate = !!template;
|
|
2267
|
+
|
|
2268
|
+
// Priority: custom URL > template URL > default URL
|
|
2269
|
+
const templateExecuteUrl = customUrls.executeUrl || selectedTemplate?.executeUrl || executeUrl;
|
|
2270
|
+
|
|
2271
|
+
// Debug logging
|
|
2272
|
+
console.log('🔍 [DEBUG] Grouped Report Detected:', {
|
|
2273
|
+
selectedTemplate: selectedTemplate?.id,
|
|
2274
|
+
isDynamicTemplate,
|
|
2275
|
+
template,
|
|
2276
|
+
customExecuteUrl: customUrls.executeUrl,
|
|
2277
|
+
templateExecuteUrl: selectedTemplate?.executeUrl,
|
|
2278
|
+
defaultExecuteUrl: executeUrl,
|
|
2279
|
+
finalExecuteUrl: templateExecuteUrl,
|
|
2280
|
+
isGroupedReport,
|
|
2281
|
+
templateGroupingConfig: selectedTemplate?.grouping,
|
|
2282
|
+
customGroupingConfig: groupingConfig,
|
|
2283
|
+
usingCustomUrl: !!customUrls.executeUrl,
|
|
2284
|
+
usingTemplateExecuteUrl: !!selectedTemplate?.executeUrl
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
let payload;
|
|
2288
|
+
|
|
2289
|
+
if (isDynamicTemplate) {
|
|
2290
|
+
// For dynamic templates, send the complete template configuration
|
|
2291
|
+
payload = {
|
|
2292
|
+
dynamic_template: template,
|
|
2293
|
+
table: selectedTable,
|
|
2294
|
+
columns: selectedColumns,
|
|
2295
|
+
joins: joins,
|
|
2296
|
+
filters: flattenFilterCriteria(filterCriteria),
|
|
2297
|
+
sorting: sortCriteria
|
|
2298
|
+
};
|
|
2299
|
+
} else {
|
|
2300
|
+
// For predefined templates, use the existing approach
|
|
2301
|
+
payload = {
|
|
2302
|
+
template_id: selectedTemplate?.id || 'utilization_planning',
|
|
2303
|
+
filters: []
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
console.log('🚀 [DEBUG] Sending grouped report request:', {
|
|
2308
|
+
url: templateExecuteUrl,
|
|
2309
|
+
method: 'POST',
|
|
2310
|
+
payload
|
|
2311
|
+
});
|
|
2312
|
+
|
|
2313
|
+
const result = await CustomFetch(
|
|
2314
|
+
templateExecuteUrl,
|
|
2315
|
+
'POST',
|
|
2316
|
+
payload
|
|
2317
|
+
);
|
|
2318
|
+
|
|
2319
|
+
console.log('📊 [DEBUG] Grouped report response:', result);
|
|
2320
|
+
|
|
2321
|
+
if (result.error) {
|
|
2322
|
+
console.error('❌ Grouped report execution error:', result.error);
|
|
2323
|
+
return { data: [], count: 0 };
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
return result;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2200
2329
|
const page = Math.floor(skip / limit) + 1;
|
|
2201
2330
|
|
|
2202
2331
|
// Use the same payload format as GenericReport (with query wrapper)
|
|
@@ -2250,8 +2379,21 @@ const GenericReportImproved = ({
|
|
|
2250
2379
|
page: page,
|
|
2251
2380
|
};
|
|
2252
2381
|
|
|
2382
|
+
// Priority: custom URL > template URL > default URL
|
|
2383
|
+
const finalExecuteUrl = customUrls.executeUrl || selectedTemplate?.executeUrl || executeUrl;
|
|
2384
|
+
|
|
2385
|
+
console.log('🔍 [DEBUG] Regular Report API Call:', {
|
|
2386
|
+
selectedTemplate: selectedTemplate?.id,
|
|
2387
|
+
customExecuteUrl: customUrls.executeUrl,
|
|
2388
|
+
templateExecuteUrl: selectedTemplate?.executeUrl,
|
|
2389
|
+
defaultExecuteUrl: executeUrl,
|
|
2390
|
+
finalExecuteUrl,
|
|
2391
|
+
usingCustomUrl: !!customUrls.executeUrl,
|
|
2392
|
+
usingTemplateExecuteUrl: !!selectedTemplate?.executeUrl
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2253
2395
|
const result = await CustomFetch(
|
|
2254
|
-
|
|
2396
|
+
finalExecuteUrl,
|
|
2255
2397
|
'POST',
|
|
2256
2398
|
reportConfig
|
|
2257
2399
|
);
|
|
@@ -2292,11 +2434,23 @@ const GenericReportImproved = ({
|
|
|
2292
2434
|
filterCriteria,
|
|
2293
2435
|
sortCriteria,
|
|
2294
2436
|
executeUrl,
|
|
2437
|
+
selectedTemplate,
|
|
2438
|
+
groupingConfig,
|
|
2439
|
+
customUrls,
|
|
2295
2440
|
]
|
|
2296
2441
|
);
|
|
2297
2442
|
|
|
2298
2443
|
// Execute report function for manual execution (creates grid columns)
|
|
2299
2444
|
const executeReport = async () => {
|
|
2445
|
+
console.log('🎯 [DEBUG] executeReport called:', {
|
|
2446
|
+
selectedTable,
|
|
2447
|
+
selectedColumns: selectedColumns.length,
|
|
2448
|
+
selectedTemplate: selectedTemplate?.id,
|
|
2449
|
+
hasGrouping: selectedTemplate?.grouping?.enabled,
|
|
2450
|
+
executeUrl,
|
|
2451
|
+
setting
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2300
2454
|
if (!selectedTable || selectedColumns.length === 0) {
|
|
2301
2455
|
toast.warning(
|
|
2302
2456
|
'Please select at least one field to include in your report.'
|
|
@@ -2305,22 +2459,127 @@ const GenericReportImproved = ({
|
|
|
2305
2459
|
}
|
|
2306
2460
|
|
|
2307
2461
|
setIsLoading(true);
|
|
2462
|
+
setGroupedData(null); // Reset grouped data
|
|
2308
2463
|
|
|
2309
2464
|
try {
|
|
2310
|
-
//
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2465
|
+
// Check if selected template has grouping enabled OR custom grouping is enabled
|
|
2466
|
+
const hasTemplateGrouping = selectedTemplate?.grouping?.enabled;
|
|
2467
|
+
const hasCustomGrouping = groupingConfig.enabled;
|
|
2468
|
+
const hasGrouping = hasTemplateGrouping || hasCustomGrouping;
|
|
2469
|
+
let fetchedDataCount = 0;
|
|
2470
|
+
|
|
2471
|
+
console.log('📋 [DEBUG] Template analysis:', {
|
|
2472
|
+
selectedTemplate,
|
|
2473
|
+
hasTemplateGrouping,
|
|
2474
|
+
hasCustomGrouping,
|
|
2475
|
+
hasGrouping,
|
|
2476
|
+
customGroupingConfig: groupingConfig,
|
|
2477
|
+
templateGroupingConfig: selectedTemplate?.grouping
|
|
2316
2478
|
});
|
|
2479
|
+
console.log('🎯 [DEBUG] Initial fetchedDataCount:', fetchedDataCount);
|
|
2480
|
+
|
|
2481
|
+
if (hasGrouping) {
|
|
2482
|
+
// Determine which grouping configuration to use
|
|
2483
|
+
let effectiveGrouping;
|
|
2484
|
+
let dynamicTemplate;
|
|
2485
|
+
|
|
2486
|
+
if (hasCustomGrouping && groupingConfig.groupByField) {
|
|
2487
|
+
// Create dynamic template with custom grouping configuration
|
|
2488
|
+
dynamicTemplate = {
|
|
2489
|
+
id: `custom_grouped_${Date.now()}`,
|
|
2490
|
+
name: reportName || 'Custom Grouped Report',
|
|
2491
|
+
mainTable: selectedTable,
|
|
2492
|
+
defaultColumns: selectedColumns,
|
|
2493
|
+
calculatedFields: customCalculatedFields,
|
|
2494
|
+
grouping: {
|
|
2495
|
+
enabled: true,
|
|
2496
|
+
type: 'sections',
|
|
2497
|
+
groupByField: groupingConfig.groupByField,
|
|
2498
|
+
groupDisplayName: groupingConfig.groupDisplayName,
|
|
2499
|
+
sortBy: groupingConfig.groupByField, // Use same field for sorting
|
|
2500
|
+
sortDirection: groupingConfig.sortDirection,
|
|
2501
|
+
rowsPerGroup: groupingConfig.rowsPerGroup,
|
|
2502
|
+
showEmptyRows: groupingConfig.showEmptyRows,
|
|
2503
|
+
showGroupTotals: groupingConfig.showGroupTotals,
|
|
2504
|
+
expandable: groupingConfig.expandable,
|
|
2505
|
+
groupHeaderStyle: groupingConfig.groupHeaderStyle,
|
|
2506
|
+
emptyRowStyle: groupingConfig.emptyRowStyle
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
effectiveGrouping = dynamicTemplate.grouping;
|
|
2510
|
+
|
|
2511
|
+
console.log('🏗️ [DEBUG] Created dynamic template for custom grouping:', dynamicTemplate);
|
|
2512
|
+
} else if (hasCustomGrouping && !groupingConfig.groupByField) {
|
|
2513
|
+
// Custom grouping enabled but no field selected - show error and fallback to regular report
|
|
2514
|
+
toast.error('Please select a field to group by before executing the report.');
|
|
2515
|
+
console.log('⚠️ [DEBUG] Custom grouping enabled but no groupByField selected');
|
|
2516
|
+
setIsLoading(false);
|
|
2517
|
+
return;
|
|
2518
|
+
} else {
|
|
2519
|
+
// Use existing template grouping
|
|
2520
|
+
effectiveGrouping = selectedTemplate.grouping;
|
|
2521
|
+
console.log('📋 [DEBUG] Using template grouping:', effectiveGrouping);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// For grouped reports, fetch data with reasonable limit
|
|
2525
|
+
const groupedResult = await dataSource({
|
|
2526
|
+
skip: 0,
|
|
2527
|
+
limit: 1000, // Maximum allowed limit
|
|
2528
|
+
sortInfo: null,
|
|
2529
|
+
filterValue: null,
|
|
2530
|
+
grouping: effectiveGrouping,
|
|
2531
|
+
template: dynamicTemplate // Pass dynamic template if created
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
console.log('🎯 [DEBUG] Full API Response:', groupedResult);
|
|
2535
|
+
console.log('🎯 [DEBUG] groupedResult.data:', groupedResult.data);
|
|
2536
|
+
console.log('🎯 [DEBUG] groupedResult.data.grouped:', groupedResult.data?.grouped);
|
|
2537
|
+
console.log('🎯 [DEBUG] groupedResult.data.groupedData:', groupedResult.data?.groupedData);
|
|
2538
|
+
|
|
2539
|
+
if (groupedResult.data?.grouped && groupedResult.data?.groupedData?.groups) {
|
|
2540
|
+
console.log('✅ Processing grouped report');
|
|
2541
|
+
setGroupedData(groupedResult.data); // Store entire response with grouped flag
|
|
2542
|
+
setPreviewData([]); // Clear regular preview data
|
|
2543
|
+
// Get the count from the fetched grouped data
|
|
2544
|
+
console.log('🎯 [DEBUG] Before assignment - groupedResult.data.groupedData.totalRecords:', groupedResult.data.groupedData?.totalRecords);
|
|
2545
|
+
fetchedDataCount = groupedResult.data.groupedData?.totalRecords || 0;
|
|
2546
|
+
console.log('🎯 [DEBUG] After assignment - fetchedDataCount:', fetchedDataCount);
|
|
2547
|
+
console.log('🔢 [DEBUG] Grouped data count:', {
|
|
2548
|
+
totalRecords: groupedResult.data.groupedData?.totalRecords,
|
|
2549
|
+
fetchedDataCount,
|
|
2550
|
+
hasGroups: !!groupedResult.data.groupedData?.groups
|
|
2551
|
+
});
|
|
2552
|
+
} else {
|
|
2553
|
+
// Fallback to regular display if server doesn't support grouping
|
|
2554
|
+
console.log('⚠️ Fallback to regular display');
|
|
2555
|
+
const fallbackData = groupedResult.data?.data || [];
|
|
2556
|
+
console.log('🎯 [DEBUG] Fallback data length:', fallbackData.length);
|
|
2557
|
+
setPreviewData(fallbackData);
|
|
2558
|
+
fetchedDataCount = fallbackData.length;
|
|
2559
|
+
}
|
|
2560
|
+
} else {
|
|
2561
|
+
// Regular non-grouped report
|
|
2562
|
+
const currentPageResult = await dataSource({
|
|
2563
|
+
skip: (currentPage - 1) * pageSize,
|
|
2564
|
+
limit: pageSize,
|
|
2565
|
+
sortInfo: null,
|
|
2566
|
+
filterValue: null,
|
|
2567
|
+
});
|
|
2317
2568
|
|
|
2318
|
-
|
|
2319
|
-
|
|
2569
|
+
if (currentPageResult.data && currentPageResult.data.length > 0) {
|
|
2570
|
+
setPreviewData(currentPageResult.data);
|
|
2571
|
+
fetchedDataCount = currentPageResult.data.length;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2320
2574
|
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const
|
|
2575
|
+
// Create grid columns for regular reports only
|
|
2576
|
+
if (!hasGrouping) {
|
|
2577
|
+
const dataToProcess = previewData.length > 0 ? previewData : [];
|
|
2578
|
+
|
|
2579
|
+
if (dataToProcess.length > 0) {
|
|
2580
|
+
// Create grid columns dynamically from actual data with smart formatting
|
|
2581
|
+
const firstRow = dataToProcess[0];
|
|
2582
|
+
const dynamicColumns = Object.keys(firstRow).map((key) => {
|
|
2324
2583
|
// Find the column metadata from selected columns to get type information
|
|
2325
2584
|
const columnMetadata = selectedColumns.find((col) => {
|
|
2326
2585
|
// Handle both direct column names and aliased names
|
|
@@ -2333,7 +2592,7 @@ const GenericReportImproved = ({
|
|
|
2333
2592
|
});
|
|
2334
2593
|
|
|
2335
2594
|
// Check if any value in this column is JSON
|
|
2336
|
-
const hasJsonValues =
|
|
2595
|
+
const hasJsonValues = dataToProcess.some((row) => {
|
|
2337
2596
|
return isJsonValue(row[key]);
|
|
2338
2597
|
});
|
|
2339
2598
|
|
|
@@ -2428,11 +2687,11 @@ const GenericReportImproved = ({
|
|
|
2428
2687
|
return formattedValue;
|
|
2429
2688
|
},
|
|
2430
2689
|
};
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2690
|
+
});
|
|
2691
|
+
setGridColumns(dynamicColumns);
|
|
2692
|
+
} else {
|
|
2693
|
+
// Fallback to selected columns if no data
|
|
2694
|
+
const columns = selectedColumns.map((col) => {
|
|
2436
2695
|
// Find column type from table columns with enhanced detection
|
|
2437
2696
|
let columnType = 'varchar';
|
|
2438
2697
|
const tableColumn = tableColumns.find(
|
|
@@ -2468,7 +2727,7 @@ const GenericReportImproved = ({
|
|
|
2468
2727
|
}
|
|
2469
2728
|
|
|
2470
2729
|
return {
|
|
2471
|
-
name: col.displayName ||
|
|
2730
|
+
name: col.displayName || `${col.table}.${col.column}`,
|
|
2472
2731
|
header: col.displayName || formatName(col.column),
|
|
2473
2732
|
defaultFlex: 1,
|
|
2474
2733
|
minWidth: 100,
|
|
@@ -2499,16 +2758,18 @@ const GenericReportImproved = ({
|
|
|
2499
2758
|
return formattedValue;
|
|
2500
2759
|
},
|
|
2501
2760
|
};
|
|
2502
|
-
|
|
2503
|
-
|
|
2761
|
+
});
|
|
2762
|
+
setGridColumns(columns);
|
|
2763
|
+
}
|
|
2504
2764
|
}
|
|
2505
2765
|
|
|
2506
2766
|
if (currentWizardStep < wizardSteps.length - 1) {
|
|
2507
2767
|
setCurrentWizardStep(wizardSteps.length - 1);
|
|
2508
2768
|
}
|
|
2509
2769
|
|
|
2510
|
-
//
|
|
2511
|
-
|
|
2770
|
+
// Report executed successfully
|
|
2771
|
+
console.log('🎯 [DEBUG] Final fetchedDataCount:', fetchedDataCount);
|
|
2772
|
+
console.log('🎯 [DEBUG] Type of fetchedDataCount:', typeof fetchedDataCount);
|
|
2512
2773
|
} catch (error) {
|
|
2513
2774
|
toast.error('Failed to execute report');
|
|
2514
2775
|
console.error('Error executing report:', error);
|
|
@@ -2597,12 +2858,54 @@ const GenericReportImproved = ({
|
|
|
2597
2858
|
}
|
|
2598
2859
|
}
|
|
2599
2860
|
|
|
2861
|
+
// Restore grouping configuration
|
|
2862
|
+
if (config.grouping) {
|
|
2863
|
+
setGroupingConfig({
|
|
2864
|
+
enabled: config.isGrouped || false,
|
|
2865
|
+
groupByField: config.grouping.groupByField || '',
|
|
2866
|
+
groupDisplayName: config.grouping.groupDisplayName || '',
|
|
2867
|
+
sortBy: config.grouping.sortBy || '',
|
|
2868
|
+
sortDirection: config.grouping.sortDirection || 'asc',
|
|
2869
|
+
rowsPerGroup: config.grouping.rowsPerGroup || 10,
|
|
2870
|
+
showEmptyRows: config.grouping.showEmptyRows || false,
|
|
2871
|
+
showGroupTotals: config.grouping.showGroupTotals || true,
|
|
2872
|
+
expandable: config.grouping.expandable || true,
|
|
2873
|
+
groupHeaderStyle: config.grouping.groupHeaderStyle || 'primary',
|
|
2874
|
+
emptyRowStyle: config.grouping.emptyRowStyle || 'light'
|
|
2875
|
+
});
|
|
2876
|
+
} else {
|
|
2877
|
+
// Reset grouping config if not present in saved report
|
|
2878
|
+
setGroupingConfig({
|
|
2879
|
+
enabled: false,
|
|
2880
|
+
groupByField: '',
|
|
2881
|
+
groupDisplayName: '',
|
|
2882
|
+
sortBy: '',
|
|
2883
|
+
sortDirection: 'asc',
|
|
2884
|
+
rowsPerGroup: 10,
|
|
2885
|
+
showEmptyRows: false,
|
|
2886
|
+
showGroupTotals: true,
|
|
2887
|
+
expandable: true,
|
|
2888
|
+
groupHeaderStyle: 'primary',
|
|
2889
|
+
emptyRowStyle: 'light'
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// Restore custom calculated fields if present
|
|
2894
|
+
if (config.customCalculatedFields) {
|
|
2895
|
+
setCustomCalculatedFields(config.customCalculatedFields);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// Restore custom URLs if present
|
|
2899
|
+
if (config.customUrls) {
|
|
2900
|
+
setCustomUrls(config.customUrls);
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2600
2903
|
setReportName(report.label);
|
|
2601
2904
|
setIsPublic(report.is_public || false);
|
|
2602
2905
|
setLoadedReportId(report.id); // Track the loaded report ID for overwrite functionality
|
|
2603
2906
|
setShowTemplates(false);
|
|
2604
|
-
// Set to the preview step (index
|
|
2605
|
-
setCurrentWizardStep(
|
|
2907
|
+
// Set to the preview step (index 5) since the report is already configured
|
|
2908
|
+
setCurrentWizardStep(5);
|
|
2606
2909
|
|
|
2607
2910
|
toast.success(`Loaded report: ${report.label}`);
|
|
2608
2911
|
} catch (error) {
|
|
@@ -2614,6 +2917,13 @@ const GenericReportImproved = ({
|
|
|
2614
2917
|
// Load business template configuration
|
|
2615
2918
|
const loadBusinessTemplate = async (template) => {
|
|
2616
2919
|
try {
|
|
2920
|
+
console.log('🏗️ [DEBUG] Loading business template:', {
|
|
2921
|
+
templateId: template.id,
|
|
2922
|
+
templateName: template.name,
|
|
2923
|
+
hasGrouping: template.grouping?.enabled,
|
|
2924
|
+
groupingConfig: template.grouping
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2617
2927
|
setSelectedTemplate(template);
|
|
2618
2928
|
|
|
2619
2929
|
// Set main table
|
|
@@ -2673,32 +2983,87 @@ const GenericReportImproved = ({
|
|
|
2673
2983
|
setSelectedColumns(formattedColumns);
|
|
2674
2984
|
}
|
|
2675
2985
|
|
|
2676
|
-
// Apply quick filters as default
|
|
2677
|
-
if (template.quickFilters) {
|
|
2678
|
-
const
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
value: filter.value,
|
|
2688
|
-
label: filter.label,
|
|
2689
|
-
}));
|
|
2986
|
+
// Apply template quick filters as default filters
|
|
2987
|
+
if (template.quickFilters && template.quickFilters.length > 0) {
|
|
2988
|
+
const updatedFilterCriteria = {
|
|
2989
|
+
operator: 'AND',
|
|
2990
|
+
groups: [
|
|
2991
|
+
{
|
|
2992
|
+
operator: 'AND',
|
|
2993
|
+
filters: [],
|
|
2994
|
+
},
|
|
2995
|
+
],
|
|
2996
|
+
};
|
|
2690
2997
|
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2998
|
+
template.quickFilters.forEach((quickFilter) => {
|
|
2999
|
+
let filterValue = convertDynamicDateValue(quickFilter.value);
|
|
3000
|
+
|
|
3001
|
+
// Parse the field to extract table and column
|
|
3002
|
+
const fieldParts = quickFilter.field.split('.');
|
|
3003
|
+
const table = fieldParts[0];
|
|
3004
|
+
const column = fieldParts.slice(1).join('.'); // Handle cases like "leads.user.name"
|
|
3005
|
+
|
|
3006
|
+
// Convert date range objects to comma-separated string for BETWEEN operator
|
|
3007
|
+
if (quickFilter.type === 'date_range' && typeof filterValue === 'object' && filterValue.start && filterValue.end) {
|
|
3008
|
+
filterValue = `${filterValue.start},${filterValue.end}`;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
const newFilter = {
|
|
3012
|
+
id: `template_${quickFilter.id}_${Date.now()}`,
|
|
3013
|
+
field: quickFilter.field,
|
|
3014
|
+
table: table,
|
|
3015
|
+
column: column,
|
|
3016
|
+
operator:
|
|
3017
|
+
quickFilter.operator ||
|
|
3018
|
+
(quickFilter.type === 'date_range' ? 'BETWEEN' : '='),
|
|
3019
|
+
value: filterValue,
|
|
3020
|
+
label: quickFilter.label,
|
|
3021
|
+
type: quickFilter.type,
|
|
3022
|
+
};
|
|
3023
|
+
|
|
3024
|
+
updatedFilterCriteria.groups[0].filters.push(newFilter);
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
3028
|
+
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// Apply template grouping configuration
|
|
3032
|
+
if (template.grouping && template.grouping.enabled) {
|
|
3033
|
+
setGroupingConfig({
|
|
3034
|
+
enabled: template.grouping.enabled || false,
|
|
3035
|
+
groupByField: template.grouping.groupByField || '',
|
|
3036
|
+
groupDisplayName: template.grouping.groupDisplayName || '',
|
|
3037
|
+
sortBy: template.grouping.sortBy || '',
|
|
3038
|
+
sortDirection: template.grouping.sortDirection || 'asc',
|
|
3039
|
+
rowsPerGroup: template.grouping.rowsPerGroup || 10,
|
|
3040
|
+
showEmptyRows: template.grouping.showEmptyRows || false,
|
|
3041
|
+
showGroupTotals: template.grouping.showGroupTotals || true,
|
|
3042
|
+
expandable: template.grouping.expandable !== false, // Default true
|
|
3043
|
+
groupHeaderStyle: template.grouping.groupHeaderStyle || 'primary',
|
|
3044
|
+
emptyRowStyle: template.grouping.emptyRowStyle || 'light'
|
|
3045
|
+
});
|
|
3046
|
+
|
|
3047
|
+
console.log('🏗️ [DEBUG] Applied template grouping config:', {
|
|
3048
|
+
enabled: template.grouping.enabled,
|
|
3049
|
+
groupByField: template.grouping.groupByField,
|
|
3050
|
+
groupDisplayName: template.grouping.groupDisplayName
|
|
3051
|
+
});
|
|
3052
|
+
} else {
|
|
3053
|
+
// Reset grouping config if template doesn't have grouping
|
|
3054
|
+
setGroupingConfig({
|
|
3055
|
+
enabled: false,
|
|
3056
|
+
groupByField: '',
|
|
3057
|
+
groupDisplayName: '',
|
|
3058
|
+
sortBy: '',
|
|
3059
|
+
sortDirection: 'asc',
|
|
3060
|
+
rowsPerGroup: 10,
|
|
3061
|
+
showEmptyRows: false,
|
|
3062
|
+
showGroupTotals: true,
|
|
3063
|
+
expandable: true,
|
|
3064
|
+
groupHeaderStyle: 'primary',
|
|
3065
|
+
emptyRowStyle: 'light'
|
|
3066
|
+
});
|
|
2702
3067
|
}
|
|
2703
3068
|
|
|
2704
3069
|
// Set report name based on template
|
|
@@ -3080,6 +3445,8 @@ const GenericReportImproved = ({
|
|
|
3080
3445
|
return renderColumnSelection();
|
|
3081
3446
|
case 'filters':
|
|
3082
3447
|
return renderFilters();
|
|
3448
|
+
case 'grouping':
|
|
3449
|
+
return renderGrouping();
|
|
3083
3450
|
case 'preview':
|
|
3084
3451
|
return renderPreview();
|
|
3085
3452
|
default:
|
|
@@ -4308,32 +4675,26 @@ const GenericReportImproved = ({
|
|
|
4308
4675
|
handleRemoveFilterCriterion(groupIndex, filterIndex);
|
|
4309
4676
|
};
|
|
4310
4677
|
|
|
4311
|
-
//
|
|
4312
|
-
const
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
// Handle dynamic date ranges
|
|
4316
|
-
switch (quickFilter.value) {
|
|
4678
|
+
// Helper function to convert dynamic date values to actual date ranges
|
|
4679
|
+
const convertDynamicDateValue = (value) => {
|
|
4680
|
+
switch (value) {
|
|
4317
4681
|
case 'current_week':
|
|
4318
|
-
|
|
4682
|
+
return {
|
|
4319
4683
|
start: moment().startOf('week').format('YYYY-MM-DD'),
|
|
4320
4684
|
end: moment().endOf('week').format('YYYY-MM-DD'),
|
|
4321
4685
|
};
|
|
4322
|
-
break;
|
|
4323
4686
|
case 'next_30_days':
|
|
4324
|
-
|
|
4687
|
+
return {
|
|
4325
4688
|
start: moment().format('YYYY-MM-DD'),
|
|
4326
4689
|
end: moment().add(30, 'days').format('YYYY-MM-DD'),
|
|
4327
4690
|
};
|
|
4328
|
-
break;
|
|
4329
4691
|
case 'last_30_days':
|
|
4330
|
-
|
|
4692
|
+
return {
|
|
4331
4693
|
start: moment().subtract(30, 'days').format('YYYY-MM-DD'),
|
|
4332
4694
|
end: moment().format('YYYY-MM-DD'),
|
|
4333
4695
|
};
|
|
4334
|
-
break;
|
|
4335
4696
|
case 'last_quarter':
|
|
4336
|
-
|
|
4697
|
+
return {
|
|
4337
4698
|
start: moment()
|
|
4338
4699
|
.subtract(1, 'quarter')
|
|
4339
4700
|
.startOf('quarter')
|
|
@@ -4343,9 +4704,8 @@ const GenericReportImproved = ({
|
|
|
4343
4704
|
.endOf('quarter')
|
|
4344
4705
|
.format('YYYY-MM-DD'),
|
|
4345
4706
|
};
|
|
4346
|
-
break;
|
|
4347
4707
|
case 'last_year':
|
|
4348
|
-
|
|
4708
|
+
return {
|
|
4349
4709
|
start: moment()
|
|
4350
4710
|
.subtract(1, 'year')
|
|
4351
4711
|
.startOf('year')
|
|
@@ -4355,12 +4715,44 @@ const GenericReportImproved = ({
|
|
|
4355
4715
|
.endOf('year')
|
|
4356
4716
|
.format('YYYY-MM-DD'),
|
|
4357
4717
|
};
|
|
4358
|
-
|
|
4718
|
+
case 'today':
|
|
4719
|
+
return moment().format('YYYY-MM-DD');
|
|
4720
|
+
case 'yesterday':
|
|
4721
|
+
return moment().subtract(1, 'day').format('YYYY-MM-DD');
|
|
4722
|
+
case 'current_month':
|
|
4723
|
+
return {
|
|
4724
|
+
start: moment().startOf('month').format('YYYY-MM-DD'),
|
|
4725
|
+
end: moment().endOf('month').format('YYYY-MM-DD'),
|
|
4726
|
+
};
|
|
4727
|
+
case 'current_year':
|
|
4728
|
+
return {
|
|
4729
|
+
start: moment().startOf('year').format('YYYY-MM-DD'),
|
|
4730
|
+
end: moment().endOf('year').format('YYYY-MM-DD'),
|
|
4731
|
+
};
|
|
4732
|
+
default:
|
|
4733
|
+
return value; // Return original value if no conversion needed
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
|
|
4737
|
+
// Apply template-based quick filter
|
|
4738
|
+
const applyTemplateQuickFilter = (quickFilter) => {
|
|
4739
|
+
let filterValue = convertDynamicDateValue(quickFilter.value);
|
|
4740
|
+
|
|
4741
|
+
// Parse the field to extract table and column
|
|
4742
|
+
const fieldParts = quickFilter.field.split('.');
|
|
4743
|
+
const table = fieldParts[0];
|
|
4744
|
+
const column = fieldParts.slice(1).join('.'); // Handle cases like "leads.user.name"
|
|
4745
|
+
|
|
4746
|
+
// Convert date range objects to comma-separated string for BETWEEN operator
|
|
4747
|
+
if (quickFilter.type === 'date_range' && typeof filterValue === 'object' && filterValue.start && filterValue.end) {
|
|
4748
|
+
filterValue = `${filterValue.start},${filterValue.end}`;
|
|
4359
4749
|
}
|
|
4360
4750
|
|
|
4361
4751
|
const newFilter = {
|
|
4362
4752
|
id: `template_${quickFilter.id}_${Date.now()}`,
|
|
4363
4753
|
field: quickFilter.field,
|
|
4754
|
+
table: table,
|
|
4755
|
+
column: column,
|
|
4364
4756
|
operator:
|
|
4365
4757
|
quickFilter.operator ||
|
|
4366
4758
|
(quickFilter.type === 'date_range' ? 'BETWEEN' : '='),
|
|
@@ -5177,6 +5569,7 @@ const GenericReportImproved = ({
|
|
|
5177
5569
|
suggestions: smartSuggestions,
|
|
5178
5570
|
});
|
|
5179
5571
|
|
|
5572
|
+
|
|
5180
5573
|
return (
|
|
5181
5574
|
<div className={styles.filtersSection}>
|
|
5182
5575
|
{/* Template Quick Filters */}
|
|
@@ -6078,9 +6471,22 @@ const GenericReportImproved = ({
|
|
|
6078
6471
|
|
|
6079
6472
|
console.log('📁 Generated filename:', fileName);
|
|
6080
6473
|
|
|
6474
|
+
// Priority: custom export URL > template export URL > default export URL
|
|
6475
|
+
const finalExportUrl = customUrls.exportUrl || selectedTemplate?.exportUrl || exportUrl;
|
|
6476
|
+
|
|
6477
|
+
console.log('🔍 [DEBUG] Export API Call:', {
|
|
6478
|
+
selectedTemplate: selectedTemplate?.id,
|
|
6479
|
+
customExportUrl: customUrls.exportUrl,
|
|
6480
|
+
templateExportUrl: selectedTemplate?.exportUrl,
|
|
6481
|
+
defaultExportUrl: exportUrl,
|
|
6482
|
+
finalExportUrl,
|
|
6483
|
+
usingCustomExportUrl: !!customUrls.exportUrl,
|
|
6484
|
+
usingTemplateExportUrl: !!selectedTemplate?.exportUrl
|
|
6485
|
+
});
|
|
6486
|
+
|
|
6081
6487
|
// Use Download component which handles error checking and blob download properly
|
|
6082
|
-
console.log('📡 Making request to:',
|
|
6083
|
-
const result = await Download(
|
|
6488
|
+
console.log('📡 Making request to:', finalExportUrl);
|
|
6489
|
+
const result = await Download(finalExportUrl, 'POST', exportData);
|
|
6084
6490
|
|
|
6085
6491
|
console.log('Download component returned result:', result);
|
|
6086
6492
|
|
|
@@ -6185,6 +6591,183 @@ const GenericReportImproved = ({
|
|
|
6185
6591
|
}
|
|
6186
6592
|
};
|
|
6187
6593
|
|
|
6594
|
+
// Handle grouped report export to Excel
|
|
6595
|
+
const handleRegularExport = async () => {
|
|
6596
|
+
if (previewData.length === 0) {
|
|
6597
|
+
toast.error('No data available to export');
|
|
6598
|
+
return;
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
try {
|
|
6602
|
+
// Use the existing client-side Excel export functionality
|
|
6603
|
+
await exportReport('excel');
|
|
6604
|
+
} catch (error) {
|
|
6605
|
+
console.error('Regular export failed:', error);
|
|
6606
|
+
toast.error('Export failed. Please try again.');
|
|
6607
|
+
}
|
|
6608
|
+
};
|
|
6609
|
+
|
|
6610
|
+
const handleGroupedExport = async () => {
|
|
6611
|
+
if (!selectedTemplate) {
|
|
6612
|
+
toast.error('No template selected for export');
|
|
6613
|
+
return;
|
|
6614
|
+
}
|
|
6615
|
+
|
|
6616
|
+
// Debug logging to understand the groupedData state
|
|
6617
|
+
console.log('🔍 [DEBUG] Export function called with:', {
|
|
6618
|
+
groupedData: groupedData,
|
|
6619
|
+
hasGroupedData: !!groupedData,
|
|
6620
|
+
hasGroups: !!(groupedData && groupedData.groups),
|
|
6621
|
+
groupsLength: groupedData && groupedData.groups ? groupedData.groups.length : 'undefined',
|
|
6622
|
+
selectedColumns: selectedColumns,
|
|
6623
|
+
selectedTemplate: selectedTemplate?.name
|
|
6624
|
+
});
|
|
6625
|
+
|
|
6626
|
+
// Check the correct path: groupedData.groupedData.groups
|
|
6627
|
+
const actualGroups = groupedData?.groupedData?.groups;
|
|
6628
|
+
if (!groupedData || !actualGroups || actualGroups.length === 0) {
|
|
6629
|
+
console.error('❌ [DEBUG] Export failed - no grouped data:', {
|
|
6630
|
+
groupedData: groupedData,
|
|
6631
|
+
hasGroupedData: !!groupedData,
|
|
6632
|
+
hasGroupedDataProperty: !!(groupedData && groupedData.groupedData),
|
|
6633
|
+
hasGroups: !!(groupedData && groupedData.groupedData && groupedData.groupedData.groups),
|
|
6634
|
+
groupsLength: actualGroups ? actualGroups.length : 'undefined'
|
|
6635
|
+
});
|
|
6636
|
+
toast.error('No grouped data available for export');
|
|
6637
|
+
return;
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6640
|
+
try {
|
|
6641
|
+
toast.info('Generating Excel export...');
|
|
6642
|
+
|
|
6643
|
+
// Create workbook and worksheet
|
|
6644
|
+
const workbook = XLSX.utils.book_new();
|
|
6645
|
+
const worksheetData = [];
|
|
6646
|
+
|
|
6647
|
+
// Add headers
|
|
6648
|
+
const headers = selectedColumns.map(col => col.displayName || col.column);
|
|
6649
|
+
worksheetData.push(headers);
|
|
6650
|
+
|
|
6651
|
+
// Helper function to get cell value (same logic as SectionGroupedReport)
|
|
6652
|
+
const getCellValue = (row, column) => {
|
|
6653
|
+
if (row.__empty_row__) return '';
|
|
6654
|
+
|
|
6655
|
+
let value;
|
|
6656
|
+
|
|
6657
|
+
// Try multiple field access patterns
|
|
6658
|
+
if (column.displayName && row[`\`${column.displayName}\``] !== undefined) {
|
|
6659
|
+
value = row[`\`${column.displayName}\``];
|
|
6660
|
+
} else if (column.displayName && row[column.displayName] !== undefined) {
|
|
6661
|
+
value = row[column.displayName];
|
|
6662
|
+
} else if (column.table && row[`${column.table}_${column.column}`] !== undefined) {
|
|
6663
|
+
value = row[`${column.table}_${column.column}`];
|
|
6664
|
+
} else if (column.table && row[`\`${column.table}_${column.column}\``] !== undefined) {
|
|
6665
|
+
value = row[`\`${column.table}_${column.column}\``];
|
|
6666
|
+
} else if (row[column.column] !== undefined) {
|
|
6667
|
+
value = row[column.column];
|
|
6668
|
+
} else if (row[`\`${column.column}\``] !== undefined) {
|
|
6669
|
+
value = row[`\`${column.column}\``];
|
|
6670
|
+
} else if (column.id && row[column.id] !== undefined) {
|
|
6671
|
+
value = row[column.id];
|
|
6672
|
+
}
|
|
6673
|
+
|
|
6674
|
+
// Format the value
|
|
6675
|
+
if (value === null || value === undefined) return '';
|
|
6676
|
+
|
|
6677
|
+
// Handle dates
|
|
6678
|
+
if (column.type === 'date' || column.type === 'datetime') {
|
|
6679
|
+
if (value && moment(value).isValid()) {
|
|
6680
|
+
return column.type === 'date'
|
|
6681
|
+
? moment(value).format('DD/MM/YYYY')
|
|
6682
|
+
: moment(value).format('DD/MM/YYYY HH:mm:ss');
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
|
|
6686
|
+
// Handle Comments field - strip HTML and formatting
|
|
6687
|
+
if (column.displayName === 'Comments' && value) {
|
|
6688
|
+
return value.replace(/• /g, '').replace(/<[^>]*>/g, '').trim();
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
// Handle ABN (Australian Business Number) columns for export
|
|
6692
|
+
if ((column.displayName && column.displayName.toLowerCase().includes('abn')) ||
|
|
6693
|
+
(column.column && column.column.toLowerCase().includes('abn'))) {
|
|
6694
|
+
const cleanABN = String(value).replace(/\s/g, '');
|
|
6695
|
+
if (cleanABN.length === 11 && /^\d{11}$/.test(cleanABN)) {
|
|
6696
|
+
return `${cleanABN.slice(0,2)} ${cleanABN.slice(2,5)} ${cleanABN.slice(5,8)} ${cleanABN.slice(8,11)}`;
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
|
|
6700
|
+
// Handle ACN (Australian Company Number) columns for export
|
|
6701
|
+
if ((column.displayName && column.displayName.toLowerCase().includes('acn')) ||
|
|
6702
|
+
(column.column && column.column.toLowerCase().includes('acn'))) {
|
|
6703
|
+
const cleanACN = String(value).replace(/\s/g, '');
|
|
6704
|
+
if (cleanACN.length === 9 && /^\d{9}$/.test(cleanACN)) {
|
|
6705
|
+
return `${cleanACN.slice(0,3)} ${cleanACN.slice(3,6)} ${cleanACN.slice(6,9)}`;
|
|
6706
|
+
}
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6709
|
+
return String(value);
|
|
6710
|
+
};
|
|
6711
|
+
|
|
6712
|
+
// Add data for each group
|
|
6713
|
+
actualGroups.forEach((group, groupIndex) => {
|
|
6714
|
+
// Add group header row
|
|
6715
|
+
const groupDisplayName = selectedTemplate?.grouping?.groupDisplayName || 'Group';
|
|
6716
|
+
const groupHeaderRow = Array(headers.length).fill('');
|
|
6717
|
+
groupHeaderRow[0] = `${groupDisplayName}: ${group.groupDisplayName}`;
|
|
6718
|
+
worksheetData.push(groupHeaderRow);
|
|
6719
|
+
|
|
6720
|
+
// Add data rows for this group
|
|
6721
|
+
const allRows = [...group.rows, ...(group.emptyRows || [])];
|
|
6722
|
+
allRows.forEach(row => {
|
|
6723
|
+
const dataRow = selectedColumns.map(column => getCellValue(row, column));
|
|
6724
|
+
worksheetData.push(dataRow);
|
|
6725
|
+
});
|
|
6726
|
+
|
|
6727
|
+
// Add separator row between groups (except for last group)
|
|
6728
|
+
if (groupIndex < actualGroups.length - 1) {
|
|
6729
|
+
worksheetData.push(Array(headers.length).fill(''));
|
|
6730
|
+
}
|
|
6731
|
+
});
|
|
6732
|
+
|
|
6733
|
+
// Create worksheet from data
|
|
6734
|
+
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
|
|
6735
|
+
|
|
6736
|
+
// Set column widths based on content
|
|
6737
|
+
const colWidths = selectedColumns.map(column => {
|
|
6738
|
+
switch (column.displayName) {
|
|
6739
|
+
case 'Comments': return { wch: 40 };
|
|
6740
|
+
case 'Project':
|
|
6741
|
+
case 'Project Name': return { wch: 25 };
|
|
6742
|
+
case 'User':
|
|
6743
|
+
case 'Assigned User': return { wch: 20 };
|
|
6744
|
+
case 'Start Date':
|
|
6745
|
+
case 'End Date':
|
|
6746
|
+
case 'Database Updated': return { wch: 15 };
|
|
6747
|
+
default: return { wch: 18 };
|
|
6748
|
+
}
|
|
6749
|
+
});
|
|
6750
|
+
worksheet['!cols'] = colWidths;
|
|
6751
|
+
|
|
6752
|
+
|
|
6753
|
+
// Add worksheet to workbook
|
|
6754
|
+
const sheetName = selectedTemplate.name || 'Grouped Report';
|
|
6755
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
6756
|
+
|
|
6757
|
+
// Generate file and download
|
|
6758
|
+
const filename = `${selectedTemplate.name.replace(/[^a-zA-Z0-9]/g, '_')}_grouped_report_${new Date().toISOString().split('T')[0]}.xlsx`;
|
|
6759
|
+
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
|
6760
|
+
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
6761
|
+
|
|
6762
|
+
saveAs(blob, filename);
|
|
6763
|
+
toast.success('Excel export completed successfully!');
|
|
6764
|
+
|
|
6765
|
+
} catch (error) {
|
|
6766
|
+
console.error('Export error:', error);
|
|
6767
|
+
toast.error(`Export failed: ${error.message}`);
|
|
6768
|
+
}
|
|
6769
|
+
};
|
|
6770
|
+
|
|
6188
6771
|
// Save report configuration (with save/save-as functionality)
|
|
6189
6772
|
const saveReport = async (saveAs = false) => {
|
|
6190
6773
|
if (!reportName.trim()) {
|
|
@@ -6262,6 +6845,10 @@ const GenericReportImproved = ({
|
|
|
6262
6845
|
joins: joins,
|
|
6263
6846
|
filters: filterCriteria,
|
|
6264
6847
|
sorting: sortCriteria,
|
|
6848
|
+
grouping: groupingConfig.enabled ? groupingConfig : null,
|
|
6849
|
+
isGrouped: groupingConfig.enabled,
|
|
6850
|
+
customCalculatedFields: customCalculatedFields,
|
|
6851
|
+
customUrls: customUrls,
|
|
6265
6852
|
},
|
|
6266
6853
|
};
|
|
6267
6854
|
|
|
@@ -6314,6 +6901,254 @@ const GenericReportImproved = ({
|
|
|
6314
6901
|
}
|
|
6315
6902
|
};
|
|
6316
6903
|
|
|
6904
|
+
// Render grouping configuration
|
|
6905
|
+
const renderGrouping = () => {
|
|
6906
|
+
return (
|
|
6907
|
+
<div className={styles.groupingSection}>
|
|
6908
|
+
<div className={styles.sectionContent}>
|
|
6909
|
+
<div className={styles.groupingToggle}>
|
|
6910
|
+
<label className={styles.toggleLabel}>
|
|
6911
|
+
<input
|
|
6912
|
+
type="checkbox"
|
|
6913
|
+
checked={groupingConfig.enabled}
|
|
6914
|
+
onChange={(e) => setGroupingConfig({
|
|
6915
|
+
...groupingConfig,
|
|
6916
|
+
enabled: e.target.checked,
|
|
6917
|
+
// Reset fields when disabled
|
|
6918
|
+
...(e.target.checked ? {} : {
|
|
6919
|
+
groupByField: '',
|
|
6920
|
+
groupDisplayName: ''
|
|
6921
|
+
})
|
|
6922
|
+
})}
|
|
6923
|
+
className={styles.toggleInput}
|
|
6924
|
+
/>
|
|
6925
|
+
<span className={styles.toggleCheckbox}></span>
|
|
6926
|
+
<span className={styles.toggleText}>Enable Grouping</span>
|
|
6927
|
+
</label>
|
|
6928
|
+
<p className={styles.helpText}>
|
|
6929
|
+
Group data by a field to organize results into sections (e.g., "Projects by Facility")
|
|
6930
|
+
</p>
|
|
6931
|
+
</div>
|
|
6932
|
+
|
|
6933
|
+
{groupingConfig.enabled && (
|
|
6934
|
+
<div className={styles.groupingOptions}>
|
|
6935
|
+
{/* Group By Field Selection */}
|
|
6936
|
+
<div className={styles.formGroup}>
|
|
6937
|
+
<label className={styles.formLabel}>Group By Field *</label>
|
|
6938
|
+
<select
|
|
6939
|
+
value={groupingConfig.groupByField}
|
|
6940
|
+
onChange={(e) => {
|
|
6941
|
+
const selectedOption = e.target.selectedOptions[0];
|
|
6942
|
+
const fieldDisplayName = selectedOption?.text || '';
|
|
6943
|
+
// Extract table name for display
|
|
6944
|
+
const tableMatch = fieldDisplayName.match(/^([^.]+)\./);
|
|
6945
|
+
const tableName = tableMatch ? tableMatch[1] : 'Group';
|
|
6946
|
+
|
|
6947
|
+
setGroupingConfig({
|
|
6948
|
+
...groupingConfig,
|
|
6949
|
+
groupByField: e.target.value,
|
|
6950
|
+
groupDisplayName: groupingConfig.groupDisplayName || tableName,
|
|
6951
|
+
sortBy: e.target.value // Auto-set sort field
|
|
6952
|
+
});
|
|
6953
|
+
}}
|
|
6954
|
+
className={styles.formSelect}
|
|
6955
|
+
>
|
|
6956
|
+
<option value="">Select field to group by...</option>
|
|
6957
|
+
{getAvailableFilterColumns().map((col, index) => {
|
|
6958
|
+
const fieldValue = `${col.table}.${col.column}`;
|
|
6959
|
+
const displayName = `${col.tableName}.${col.columnName}`;
|
|
6960
|
+
return (
|
|
6961
|
+
<option key={index} value={fieldValue}>
|
|
6962
|
+
{displayName}
|
|
6963
|
+
</option>
|
|
6964
|
+
);
|
|
6965
|
+
})}
|
|
6966
|
+
</select>
|
|
6967
|
+
<p className={styles.fieldHelp}>
|
|
6968
|
+
Choose the field that contains the categories you want to group by
|
|
6969
|
+
</p>
|
|
6970
|
+
</div>
|
|
6971
|
+
|
|
6972
|
+
{/* Group Display Name */}
|
|
6973
|
+
<div className={styles.formGroup}>
|
|
6974
|
+
<label className={styles.formLabel}>Group Display Name</label>
|
|
6975
|
+
<input
|
|
6976
|
+
type="text"
|
|
6977
|
+
value={groupingConfig.groupDisplayName}
|
|
6978
|
+
onChange={(e) => setGroupingConfig({
|
|
6979
|
+
...groupingConfig,
|
|
6980
|
+
groupDisplayName: e.target.value
|
|
6981
|
+
})}
|
|
6982
|
+
placeholder="e.g., Facility, Department, Category"
|
|
6983
|
+
className={styles.formInput}
|
|
6984
|
+
/>
|
|
6985
|
+
<p className={styles.fieldHelp}>
|
|
6986
|
+
What to call each group in the report (e.g., "Facility: Main Building")
|
|
6987
|
+
</p>
|
|
6988
|
+
</div>
|
|
6989
|
+
|
|
6990
|
+
{/* Advanced Group Options */}
|
|
6991
|
+
<div className={styles.advancedGroupOptions}>
|
|
6992
|
+
<h4 className={styles.sectionTitle}>Advanced Options</h4>
|
|
6993
|
+
|
|
6994
|
+
<div className={styles.optionsGrid}>
|
|
6995
|
+
<div className={styles.formGroup}>
|
|
6996
|
+
<label className={styles.formLabel}>Rows Per Group</label>
|
|
6997
|
+
<input
|
|
6998
|
+
type="number"
|
|
6999
|
+
min="1"
|
|
7000
|
+
max="100"
|
|
7001
|
+
value={groupingConfig.rowsPerGroup}
|
|
7002
|
+
onChange={(e) => setGroupingConfig({
|
|
7003
|
+
...groupingConfig,
|
|
7004
|
+
rowsPerGroup: parseInt(e.target.value) || 10
|
|
7005
|
+
})}
|
|
7006
|
+
className={styles.formInput}
|
|
7007
|
+
/>
|
|
7008
|
+
</div>
|
|
7009
|
+
|
|
7010
|
+
<div className={styles.formGroup}>
|
|
7011
|
+
<label className={styles.formLabel}>Group Header Style</label>
|
|
7012
|
+
<select
|
|
7013
|
+
value={groupingConfig.groupHeaderStyle}
|
|
7014
|
+
onChange={(e) => setGroupingConfig({
|
|
7015
|
+
...groupingConfig,
|
|
7016
|
+
groupHeaderStyle: e.target.value
|
|
7017
|
+
})}
|
|
7018
|
+
className={styles.formSelect}
|
|
7019
|
+
>
|
|
7020
|
+
<option value="primary">Primary (Blue)</option>
|
|
7021
|
+
<option value="secondary">Secondary (Gray)</option>
|
|
7022
|
+
<option value="success">Success (Green)</option>
|
|
7023
|
+
<option value="warning">Warning (Orange)</option>
|
|
7024
|
+
</select>
|
|
7025
|
+
</div>
|
|
7026
|
+
|
|
7027
|
+
<div className={styles.formGroup}>
|
|
7028
|
+
<label className={styles.formLabel}>Sort Direction</label>
|
|
7029
|
+
<select
|
|
7030
|
+
value={groupingConfig.sortDirection}
|
|
7031
|
+
onChange={(e) => setGroupingConfig({
|
|
7032
|
+
...groupingConfig,
|
|
7033
|
+
sortDirection: e.target.value
|
|
7034
|
+
})}
|
|
7035
|
+
className={styles.formSelect}
|
|
7036
|
+
>
|
|
7037
|
+
<option value="asc">Ascending (A-Z)</option>
|
|
7038
|
+
<option value="desc">Descending (Z-A)</option>
|
|
7039
|
+
</select>
|
|
7040
|
+
</div>
|
|
7041
|
+
</div>
|
|
7042
|
+
|
|
7043
|
+
<div className={styles.checkboxGrid}>
|
|
7044
|
+
<label className={styles.checkboxLabel}>
|
|
7045
|
+
<input
|
|
7046
|
+
type="checkbox"
|
|
7047
|
+
checked={groupingConfig.showEmptyRows}
|
|
7048
|
+
onChange={(e) => setGroupingConfig({
|
|
7049
|
+
...groupingConfig,
|
|
7050
|
+
showEmptyRows: e.target.checked
|
|
7051
|
+
})}
|
|
7052
|
+
/>
|
|
7053
|
+
<span className={styles.checkboxText}>Show Empty Rows</span>
|
|
7054
|
+
</label>
|
|
7055
|
+
|
|
7056
|
+
<label className={styles.checkboxLabel}>
|
|
7057
|
+
<input
|
|
7058
|
+
type="checkbox"
|
|
7059
|
+
checked={groupingConfig.showGroupTotals}
|
|
7060
|
+
onChange={(e) => setGroupingConfig({
|
|
7061
|
+
...groupingConfig,
|
|
7062
|
+
showGroupTotals: e.target.checked
|
|
7063
|
+
})}
|
|
7064
|
+
/>
|
|
7065
|
+
<span className={styles.checkboxText}>Show Group Totals</span>
|
|
7066
|
+
</label>
|
|
7067
|
+
|
|
7068
|
+
<label className={styles.checkboxLabel}>
|
|
7069
|
+
<input
|
|
7070
|
+
type="checkbox"
|
|
7071
|
+
checked={groupingConfig.expandable}
|
|
7072
|
+
onChange={(e) => setGroupingConfig({
|
|
7073
|
+
...groupingConfig,
|
|
7074
|
+
expandable: e.target.checked
|
|
7075
|
+
})}
|
|
7076
|
+
/>
|
|
7077
|
+
<span className={styles.checkboxText}>Collapsible Groups</span>
|
|
7078
|
+
</label>
|
|
7079
|
+
</div>
|
|
7080
|
+
</div>
|
|
7081
|
+
|
|
7082
|
+
{/* Grouping Preview */}
|
|
7083
|
+
{groupingConfig.groupByField && (
|
|
7084
|
+
<div className={styles.groupingPreview}>
|
|
7085
|
+
<h4 className={styles.sectionTitle}>Configuration Preview</h4>
|
|
7086
|
+
<div className={styles.previewBox}>
|
|
7087
|
+
<p><strong>Group By:</strong> {groupingConfig.groupByField}</p>
|
|
7088
|
+
<p><strong>Display Name:</strong> {groupingConfig.groupDisplayName || 'Group'}</p>
|
|
7089
|
+
<p><strong>Sort:</strong> {groupingConfig.sortDirection === 'asc' ? 'Ascending' : 'Descending'}</p>
|
|
7090
|
+
<p>
|
|
7091
|
+
<strong>Options:</strong>
|
|
7092
|
+
{groupingConfig.showEmptyRows && ' Empty Rows'}
|
|
7093
|
+
{groupingConfig.showGroupTotals && ' Group Totals'}
|
|
7094
|
+
{groupingConfig.expandable && ' Collapsible'}
|
|
7095
|
+
</p>
|
|
7096
|
+
</div>
|
|
7097
|
+
</div>
|
|
7098
|
+
)}
|
|
7099
|
+
</div>
|
|
7100
|
+
)}
|
|
7101
|
+
|
|
7102
|
+
{/* Custom URLs Configuration - Only show when grouping is enabled */}
|
|
7103
|
+
{groupingConfig.enabled && (
|
|
7104
|
+
<div className={styles.customUrlsSection}>
|
|
7105
|
+
<h4 className={styles.sectionTitle}>Custom API Endpoints (Optional)</h4>
|
|
7106
|
+
<p className={styles.helpText}>
|
|
7107
|
+
Override default API endpoints for specialized report handling
|
|
7108
|
+
</p>
|
|
7109
|
+
|
|
7110
|
+
<div className={styles.optionsGrid}>
|
|
7111
|
+
<div className={styles.formGroup}>
|
|
7112
|
+
<label className={styles.formLabel}>Custom Execute URL</label>
|
|
7113
|
+
<input
|
|
7114
|
+
type="text"
|
|
7115
|
+
value={customUrls.executeUrl}
|
|
7116
|
+
onChange={(e) => setCustomUrls({
|
|
7117
|
+
...customUrls,
|
|
7118
|
+
executeUrl: e.target.value
|
|
7119
|
+
})}
|
|
7120
|
+
placeholder="/api/custom-reports/execute"
|
|
7121
|
+
className={styles.formInput}
|
|
7122
|
+
/>
|
|
7123
|
+
<p className={styles.fieldHelp}>
|
|
7124
|
+
Custom endpoint for report execution (overrides template and default URLs)
|
|
7125
|
+
</p>
|
|
7126
|
+
</div>
|
|
7127
|
+
|
|
7128
|
+
<div className={styles.formGroup}>
|
|
7129
|
+
<label className={styles.formLabel}>Custom Export URL</label>
|
|
7130
|
+
<input
|
|
7131
|
+
type="text"
|
|
7132
|
+
value={customUrls.exportUrl}
|
|
7133
|
+
onChange={(e) => setCustomUrls({
|
|
7134
|
+
...customUrls,
|
|
7135
|
+
exportUrl: e.target.value
|
|
7136
|
+
})}
|
|
7137
|
+
placeholder="/api/custom-reports/export"
|
|
7138
|
+
className={styles.formInput}
|
|
7139
|
+
/>
|
|
7140
|
+
<p className={styles.fieldHelp}>
|
|
7141
|
+
Custom endpoint for report exports (for server-side exports)
|
|
7142
|
+
</p>
|
|
7143
|
+
</div>
|
|
7144
|
+
</div>
|
|
7145
|
+
</div>
|
|
7146
|
+
)}
|
|
7147
|
+
</div>
|
|
7148
|
+
</div>
|
|
7149
|
+
);
|
|
7150
|
+
};
|
|
7151
|
+
|
|
6317
7152
|
// Render preview
|
|
6318
7153
|
const renderPreview = () => {
|
|
6319
7154
|
return (
|
|
@@ -6330,34 +7165,389 @@ const GenericReportImproved = ({
|
|
|
6330
7165
|
>
|
|
6331
7166
|
{isLoading ? 'Loading...' : 'Generate Report'}
|
|
6332
7167
|
</button>
|
|
7168
|
+
|
|
7169
|
+
{/* View Toggle Button - only show if both grouped and regular data are available */}
|
|
7170
|
+
{(groupingConfig.enabled || selectedTemplate?.grouping?.enabled) && (
|
|
7171
|
+
<div className={styles.viewToggle}>
|
|
7172
|
+
<span className={styles.viewLabel}>View:</span>
|
|
7173
|
+
<button
|
|
7174
|
+
className={`${styles.btn} ${styles.btnSecondary} ${
|
|
7175
|
+
groupedData ? styles.active : ''
|
|
7176
|
+
}`}
|
|
7177
|
+
onClick={() => {
|
|
7178
|
+
if (!groupedData && previewData.length > 0) {
|
|
7179
|
+
// Switch to grouped view
|
|
7180
|
+
executeReport();
|
|
7181
|
+
}
|
|
7182
|
+
}}
|
|
7183
|
+
disabled={isLoading}
|
|
7184
|
+
title="Show grouped report"
|
|
7185
|
+
>
|
|
7186
|
+
📊 Grouped
|
|
7187
|
+
</button>
|
|
7188
|
+
<button
|
|
7189
|
+
className={`${styles.btn} ${styles.btnSecondary} ${
|
|
7190
|
+
!groupedData ? styles.active : ''
|
|
7191
|
+
}`}
|
|
7192
|
+
onClick={() => {
|
|
7193
|
+
if (groupedData) {
|
|
7194
|
+
// Switch to regular table view
|
|
7195
|
+
setGroupedData(null);
|
|
7196
|
+
if (previewData.length === 0) {
|
|
7197
|
+
executeReport();
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
}}
|
|
7201
|
+
disabled={isLoading}
|
|
7202
|
+
title="Show regular table"
|
|
7203
|
+
>
|
|
7204
|
+
📋 Table
|
|
7205
|
+
</button>
|
|
7206
|
+
</div>
|
|
7207
|
+
)}
|
|
6333
7208
|
</div>
|
|
6334
7209
|
|
|
6335
|
-
{
|
|
7210
|
+
{/* Render grouped report if available */}
|
|
7211
|
+
{(() => {
|
|
7212
|
+
console.log('🎯 [DEBUG] Render check - groupedData:', groupedData);
|
|
7213
|
+
console.log('🎯 [DEBUG] Render check - groupedData?.grouped:', groupedData?.grouped);
|
|
7214
|
+
console.log('🎯 [DEBUG] Render check - selectedColumns:', selectedColumns);
|
|
7215
|
+
return groupedData && groupedData.grouped;
|
|
7216
|
+
})() && (
|
|
7217
|
+
<div className={styles.groupedReportContainer}>
|
|
7218
|
+
<GroupedReportRenderer
|
|
7219
|
+
data={{
|
|
7220
|
+
...groupedData.groupedData,
|
|
7221
|
+
grouped: true
|
|
7222
|
+
}}
|
|
7223
|
+
config={
|
|
7224
|
+
groupingConfig.enabled
|
|
7225
|
+
? groupingConfig
|
|
7226
|
+
: selectedTemplate?.grouping || {}
|
|
7227
|
+
}
|
|
7228
|
+
columns={selectedColumns}
|
|
7229
|
+
onExport={(data) => {
|
|
7230
|
+
handleGroupedExport();
|
|
7231
|
+
}}
|
|
7232
|
+
/>
|
|
7233
|
+
</div>
|
|
7234
|
+
)}
|
|
7235
|
+
|
|
7236
|
+
{/* Render regular grid if no grouping */}
|
|
7237
|
+
{!groupedData && previewData.length > 0 && (
|
|
6336
7238
|
<>
|
|
6337
7239
|
<div className={styles.gridContainer}>
|
|
6338
|
-
<
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
7240
|
+
<div className="grouped-report">
|
|
7241
|
+
<div className="grouped-report-section">
|
|
7242
|
+
<div className="grouped-report-header">
|
|
7243
|
+
<div className="grouped-report-summary">
|
|
7244
|
+
<h3 className="group-title">Report Results</h3>
|
|
7245
|
+
<span className="group-stats">({previewData.length} records)</span>
|
|
7246
|
+
</div>
|
|
7247
|
+
<div className="grouped-report-actions">
|
|
7248
|
+
<button
|
|
7249
|
+
className="btn btn-export"
|
|
7250
|
+
onClick={() => handleRegularExport()}
|
|
7251
|
+
disabled={isLoading || previewData.length === 0}
|
|
7252
|
+
title="Export report data as Excel file"
|
|
7253
|
+
>
|
|
7254
|
+
Export
|
|
7255
|
+
</button>
|
|
7256
|
+
</div>
|
|
7257
|
+
</div>
|
|
7258
|
+
<div className="grouped-table-container">
|
|
7259
|
+
<table className="grouped-table">
|
|
7260
|
+
<thead>
|
|
7261
|
+
<tr>
|
|
7262
|
+
{(() => {
|
|
7263
|
+
const headerColumns = previewData.length > 0 ? Object.keys(previewData[0]).map(key => ({name: key, header: key.replace(/\./g, ' ')})) : gridColumns;
|
|
7264
|
+
return headerColumns.map((col, index) => (
|
|
7265
|
+
<th key={index} className="table-header">
|
|
7266
|
+
{col.header || col.name}
|
|
7267
|
+
</th>
|
|
7268
|
+
));
|
|
7269
|
+
})()}
|
|
7270
|
+
</tr>
|
|
7271
|
+
</thead>
|
|
7272
|
+
<tbody>
|
|
7273
|
+
{(() => {
|
|
7274
|
+
console.log(`🎯 [DEBUG] Rendering ${previewData.length} table rows`);
|
|
7275
|
+
// Always use response keys as fallback instead of gridColumns that might have wrong names
|
|
7276
|
+
const actualColumns = previewData.length > 0 ? Object.keys(previewData[0]).map(key => ({name: key, header: key.replace(/\./g, ' ')})) : gridColumns;
|
|
7277
|
+
console.log(`🎯 [DEBUG] Using columns:`, actualColumns);
|
|
7278
|
+
return previewData.map((row, rowIndex) => (
|
|
7279
|
+
<tr key={rowIndex} className="data-row">
|
|
7280
|
+
{actualColumns.map((col, colIndex) => (
|
|
7281
|
+
<td key={colIndex} className="table-cell">
|
|
7282
|
+
{(() => {
|
|
7283
|
+
const value = row[col.name];
|
|
7284
|
+
|
|
7285
|
+
// Handle null/undefined
|
|
7286
|
+
if (value === null || value === undefined) {
|
|
7287
|
+
return <span style={{color: '#6c757d', fontStyle: 'italic'}}>—</span>;
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7290
|
+
// Handle JSON strings
|
|
7291
|
+
if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
|
|
7292
|
+
try {
|
|
7293
|
+
const parsed = JSON.parse(value);
|
|
7294
|
+
return (
|
|
7295
|
+
<div style={{fontSize: '0.85em', lineHeight: '1.3'}}>
|
|
7296
|
+
{Object.entries(parsed).map(([key, val]) => (
|
|
7297
|
+
<div key={key} style={{marginBottom: '2px'}}>
|
|
7298
|
+
<strong>{key.replace(/_/g, ' ')}:</strong> {val || '—'}
|
|
7299
|
+
</div>
|
|
7300
|
+
))}
|
|
7301
|
+
</div>
|
|
7302
|
+
);
|
|
7303
|
+
} catch (e) {
|
|
7304
|
+
return value;
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7307
|
+
|
|
7308
|
+
// Handle ABN (Australian Business Number) columns
|
|
7309
|
+
if (col.name.toLowerCase().includes('abn') && value) {
|
|
7310
|
+
const cleanABN = String(value).replace(/\s/g, ''); // Remove spaces, convert to string
|
|
7311
|
+
if (cleanABN.length === 11 && /^\d{11}$/.test(cleanABN)) {
|
|
7312
|
+
// Format as XX XXX XXX XXX
|
|
7313
|
+
const formatted = `${cleanABN.slice(0,2)} ${cleanABN.slice(2,5)} ${cleanABN.slice(5,8)} ${cleanABN.slice(8,11)}`;
|
|
7314
|
+
return <span style={{fontFamily: 'monospace', fontWeight: '500', color: '#495057'}}>{formatted}</span>;
|
|
7315
|
+
}
|
|
7316
|
+
}
|
|
7317
|
+
|
|
7318
|
+
// Handle ACN (Australian Company Number) columns
|
|
7319
|
+
if (col.name.toLowerCase().includes('acn') && value) {
|
|
7320
|
+
const cleanACN = String(value).replace(/\s/g, ''); // Remove spaces, convert to string
|
|
7321
|
+
if (cleanACN.length === 9 && /^\d{9}$/.test(cleanACN)) {
|
|
7322
|
+
// Format as XXX XXX XXX
|
|
7323
|
+
const formatted = `${cleanACN.slice(0,3)} ${cleanACN.slice(3,6)} ${cleanACN.slice(6,9)}`;
|
|
7324
|
+
return <span style={{fontFamily: 'monospace', fontWeight: '500', color: '#495057'}}>{formatted}</span>;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
|
|
7328
|
+
// Handle status/boolean-like columns
|
|
7329
|
+
if (col.name.toLowerCase().includes('status')) {
|
|
7330
|
+
if (value === 1 || value === '1' || value === true) {
|
|
7331
|
+
return <span style={{color: '#28a745', fontWeight: '500'}}>Active</span>;
|
|
7332
|
+
} else if (value === 0 || value === '0' || value === false) {
|
|
7333
|
+
return <span style={{color: '#6c757d', fontWeight: '500'}}>Inactive</span>;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
|
|
7337
|
+
// Handle other boolean values
|
|
7338
|
+
if (typeof value === 'boolean') {
|
|
7339
|
+
return value ?
|
|
7340
|
+
<span style={{color: '#28a745', fontWeight: '500'}}>Yes</span> :
|
|
7341
|
+
<span style={{color: '#6c757d', fontWeight: '500'}}>No</span>;
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
// Handle numeric values (but exclude potential boolean columns)
|
|
7345
|
+
if (typeof value === 'number' && !col.name.toLowerCase().includes('status')) {
|
|
7346
|
+
const columnName = col.name.toLowerCase();
|
|
7347
|
+
// Don't format as numbers if they might be boolean values
|
|
7348
|
+
const mightBeBoolean = (value === 0 || value === 1) && (
|
|
7349
|
+
columnName.includes('is_') || columnName.includes('has_') || columnName.includes('confirmed') ||
|
|
7350
|
+
columnName.includes('active') || columnName.includes('enabled') || columnName.includes('completed') ||
|
|
7351
|
+
columnName.includes('stage') || columnName.includes('final') || columnName.includes('flag') ||
|
|
7352
|
+
columnName.includes('approved') || columnName.includes('verified') || columnName.includes('sent') ||
|
|
7353
|
+
columnName.endsWith('?')
|
|
7354
|
+
);
|
|
7355
|
+
|
|
7356
|
+
if (!mightBeBoolean) {
|
|
7357
|
+
return value.toLocaleString();
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
|
|
7361
|
+
// Use formatCellContent for all other values including dates and smart boolean formatting
|
|
7362
|
+
const formattedValue = formatCellContent(value, {
|
|
7363
|
+
id: col.name,
|
|
7364
|
+
name: col.name,
|
|
7365
|
+
// Get type from template column definition if available, otherwise infer
|
|
7366
|
+
type: (() => {
|
|
7367
|
+
// First try to find type from template's defaultColumns
|
|
7368
|
+
if (selectedTemplate?.defaultColumns) {
|
|
7369
|
+
const templateColumn = selectedTemplate.defaultColumns.find(
|
|
7370
|
+
tc => `${tc.table}.${tc.column}` === col.name || tc.column === col.name
|
|
7371
|
+
);
|
|
7372
|
+
if (templateColumn?.type) {
|
|
7373
|
+
return templateColumn.type;
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
|
|
7377
|
+
// Fallback to column name-based type inference
|
|
7378
|
+
const columnName = col.name.toLowerCase();
|
|
7379
|
+
// Date column detection
|
|
7380
|
+
if (columnName.includes('date') || columnName.includes('time') || columnName.includes('created_at') || columnName.includes('updated_at')) {
|
|
7381
|
+
return 'date';
|
|
7382
|
+
}
|
|
7383
|
+
// Boolean column detection - enhanced logic
|
|
7384
|
+
if (columnName.includes('is_') || columnName.includes('has_') || columnName.includes('confirmed') ||
|
|
7385
|
+
columnName.includes('active') || columnName.includes('enabled') || columnName.includes('completed') ||
|
|
7386
|
+
columnName.includes('stage') || columnName.includes('final') || columnName.includes('flag') ||
|
|
7387
|
+
columnName.includes('approved') || columnName.includes('verified') || columnName.includes('sent') ||
|
|
7388
|
+
columnName.endsWith('?') || // Handle columns like "Confirmed?"
|
|
7389
|
+
(typeof value === 'number' && (value === 0 || value === 1)) ||
|
|
7390
|
+
(typeof value === 'string' && (value === '0' || value === '1'))) {
|
|
7391
|
+
return 'boolean';
|
|
7392
|
+
}
|
|
7393
|
+
return undefined; // Let formatCellContent auto-detect
|
|
7394
|
+
})()
|
|
7395
|
+
});
|
|
7396
|
+
|
|
7397
|
+
return formattedValue;
|
|
7398
|
+
})()}
|
|
7399
|
+
</td>
|
|
7400
|
+
))}
|
|
7401
|
+
</tr>
|
|
7402
|
+
));
|
|
7403
|
+
})()}
|
|
7404
|
+
</tbody>
|
|
7405
|
+
</table>
|
|
7406
|
+
</div>
|
|
7407
|
+
|
|
7408
|
+
{/* Pagination Controls */}
|
|
7409
|
+
{previewData.length > 0 && (
|
|
7410
|
+
<div className={styles.paginationContainer}>
|
|
7411
|
+
<div className={styles.paginationInfo}>
|
|
7412
|
+
{(() => {
|
|
7413
|
+
const startRecord = Math.min((currentPage - 1) * pageSize + 1, totalCount);
|
|
7414
|
+
const endRecord = Math.min(currentPage * pageSize, totalCount);
|
|
7415
|
+
return `Showing ${startRecord}-${endRecord} of ${totalCount} records`;
|
|
7416
|
+
})()}
|
|
7417
|
+
</div>
|
|
7418
|
+
|
|
7419
|
+
<div className={styles.paginationControls}>
|
|
7420
|
+
<div className={styles.pageSizeSelector}>
|
|
7421
|
+
<label>Show:</label>
|
|
7422
|
+
<select
|
|
7423
|
+
value={pageSize}
|
|
7424
|
+
onChange={(e) => {
|
|
7425
|
+
const newPageSize = parseInt(e.target.value);
|
|
7426
|
+
setPageSize(newPageSize);
|
|
7427
|
+
setCurrentPage(1); // Reset to first page
|
|
7428
|
+
// Re-execute the report with new page size
|
|
7429
|
+
if (previewData.length > 0) {
|
|
7430
|
+
executeReport();
|
|
7431
|
+
}
|
|
7432
|
+
}}
|
|
7433
|
+
className={styles.pageSizeSelect}
|
|
7434
|
+
>
|
|
7435
|
+
<option value={10}>10</option>
|
|
7436
|
+
<option value={25}>25</option>
|
|
7437
|
+
<option value={50}>50</option>
|
|
7438
|
+
<option value={100}>100</option>
|
|
7439
|
+
<option value={250}>250</option>
|
|
7440
|
+
</select>
|
|
7441
|
+
<span>records</span>
|
|
7442
|
+
</div>
|
|
7443
|
+
|
|
7444
|
+
<div className={styles.pageNavigation}>
|
|
7445
|
+
<button
|
|
7446
|
+
onClick={() => {
|
|
7447
|
+
if (currentPage > 1) {
|
|
7448
|
+
const newPage = currentPage - 1;
|
|
7449
|
+
setCurrentPage(newPage);
|
|
7450
|
+
executeReport();
|
|
7451
|
+
}
|
|
7452
|
+
}}
|
|
7453
|
+
disabled={currentPage <= 1}
|
|
7454
|
+
className={`${styles.pageBtn} ${currentPage <= 1 ? styles.disabled : ''}`}
|
|
7455
|
+
>
|
|
7456
|
+
‹ Previous
|
|
7457
|
+
</button>
|
|
7458
|
+
|
|
7459
|
+
<div className={styles.pageNumbers}>
|
|
7460
|
+
{(() => {
|
|
7461
|
+
const maxPagesToShow = 5;
|
|
7462
|
+
const totalPages = Math.ceil(totalCount / pageSize);
|
|
7463
|
+
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
|
|
7464
|
+
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
|
|
7465
|
+
|
|
7466
|
+
// Adjust start if we're near the end
|
|
7467
|
+
if (endPage - startPage < maxPagesToShow - 1) {
|
|
7468
|
+
startPage = Math.max(1, endPage - maxPagesToShow + 1);
|
|
7469
|
+
}
|
|
7470
|
+
|
|
7471
|
+
const pages = [];
|
|
7472
|
+
|
|
7473
|
+
// Add first page and ellipsis if needed
|
|
7474
|
+
if (startPage > 1) {
|
|
7475
|
+
pages.push(
|
|
7476
|
+
<button
|
|
7477
|
+
key={1}
|
|
7478
|
+
onClick={() => {
|
|
7479
|
+
setCurrentPage(1);
|
|
7480
|
+
executeReport();
|
|
7481
|
+
}}
|
|
7482
|
+
className={`${styles.pageBtn} ${1 === currentPage ? styles.active : ''}`}
|
|
7483
|
+
>
|
|
7484
|
+
1
|
|
7485
|
+
</button>
|
|
7486
|
+
);
|
|
7487
|
+
if (startPage > 2) {
|
|
7488
|
+
pages.push(<span key="start-ellipsis" className={styles.ellipsis}>...</span>);
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
|
|
7492
|
+
// Add visible page numbers
|
|
7493
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
7494
|
+
pages.push(
|
|
7495
|
+
<button
|
|
7496
|
+
key={i}
|
|
7497
|
+
onClick={() => {
|
|
7498
|
+
setCurrentPage(i);
|
|
7499
|
+
executeReport();
|
|
7500
|
+
}}
|
|
7501
|
+
className={`${styles.pageBtn} ${i === currentPage ? styles.active : ''}`}
|
|
7502
|
+
>
|
|
7503
|
+
{i}
|
|
7504
|
+
</button>
|
|
7505
|
+
);
|
|
7506
|
+
}
|
|
7507
|
+
|
|
7508
|
+
// Add ellipsis and last page if needed
|
|
7509
|
+
if (endPage < totalPages) {
|
|
7510
|
+
if (endPage < totalPages - 1) {
|
|
7511
|
+
pages.push(<span key="end-ellipsis" className={styles.ellipsis}>...</span>);
|
|
7512
|
+
}
|
|
7513
|
+
pages.push(
|
|
7514
|
+
<button
|
|
7515
|
+
key={totalPages}
|
|
7516
|
+
onClick={() => {
|
|
7517
|
+
setCurrentPage(totalPages);
|
|
7518
|
+
executeReport();
|
|
7519
|
+
}}
|
|
7520
|
+
className={`${styles.pageBtn} ${totalPages === currentPage ? styles.active : ''}`}
|
|
7521
|
+
>
|
|
7522
|
+
{totalPages}
|
|
7523
|
+
</button>
|
|
7524
|
+
);
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
return pages;
|
|
7528
|
+
})()}
|
|
7529
|
+
</div>
|
|
7530
|
+
|
|
7531
|
+
<button
|
|
7532
|
+
onClick={() => {
|
|
7533
|
+
const totalPages = Math.ceil(totalCount / pageSize);
|
|
7534
|
+
if (currentPage < totalPages) {
|
|
7535
|
+
const newPage = currentPage + 1;
|
|
7536
|
+
setCurrentPage(newPage);
|
|
7537
|
+
executeReport();
|
|
7538
|
+
}
|
|
7539
|
+
}}
|
|
7540
|
+
disabled={currentPage >= Math.ceil(totalCount / pageSize)}
|
|
7541
|
+
className={`${styles.pageBtn} ${currentPage >= Math.ceil(totalCount / pageSize) ? styles.disabled : ''}`}
|
|
7542
|
+
>
|
|
7543
|
+
Next ›
|
|
7544
|
+
</button>
|
|
7545
|
+
</div>
|
|
7546
|
+
</div>
|
|
7547
|
+
</div>
|
|
7548
|
+
)}
|
|
7549
|
+
</div>
|
|
7550
|
+
</div>
|
|
6361
7551
|
</div>
|
|
6362
7552
|
|
|
6363
7553
|
<div className={styles.reportActions}>
|
|
@@ -6390,69 +7580,32 @@ const GenericReportImproved = ({
|
|
|
6390
7580
|
</div>
|
|
6391
7581
|
</div>
|
|
6392
7582
|
|
|
6393
|
-
<div className={styles.exportOptions}>
|
|
6394
|
-
<h3>Export Your Report</h3>
|
|
6395
|
-
<p>
|
|
6396
|
-
Download your report data in various
|
|
6397
|
-
formats.
|
|
6398
|
-
</p>
|
|
6399
|
-
<div className={styles.exportButtons}>
|
|
6400
|
-
<button
|
|
6401
|
-
className={`${styles.btn} ${styles.btnExportExcel}`}
|
|
6402
|
-
onClick={() => exportReport('excel')}
|
|
6403
|
-
disabled={
|
|
6404
|
-
isLoading ||
|
|
6405
|
-
!reportName ||
|
|
6406
|
-
!reportName.trim()
|
|
6407
|
-
}
|
|
6408
|
-
title={
|
|
6409
|
-
!reportName
|
|
6410
|
-
? 'Please save your report first'
|
|
6411
|
-
: ''
|
|
6412
|
-
}
|
|
6413
|
-
>
|
|
6414
|
-
<DownloadIcon size={16} />
|
|
6415
|
-
Export as Excel
|
|
6416
|
-
</button>
|
|
6417
|
-
<button
|
|
6418
|
-
className={`${styles.btn} ${styles.btnExportCsv}`}
|
|
6419
|
-
onClick={() => exportReport('csv')}
|
|
6420
|
-
disabled={
|
|
6421
|
-
isLoading ||
|
|
6422
|
-
!reportName ||
|
|
6423
|
-
!reportName.trim()
|
|
6424
|
-
}
|
|
6425
|
-
title={
|
|
6426
|
-
!reportName
|
|
6427
|
-
? 'Please save your report first'
|
|
6428
|
-
: ''
|
|
6429
|
-
}
|
|
6430
|
-
>
|
|
6431
|
-
<DownloadIcon size={16} />
|
|
6432
|
-
Export as CSV
|
|
6433
|
-
</button>
|
|
6434
|
-
<button
|
|
6435
|
-
className={`${styles.btn} ${styles.btnExportPdf}`}
|
|
6436
|
-
onClick={() => exportReport('pdf')}
|
|
6437
|
-
disabled={
|
|
6438
|
-
isLoading ||
|
|
6439
|
-
!reportName ||
|
|
6440
|
-
!reportName.trim()
|
|
6441
|
-
}
|
|
6442
|
-
title={
|
|
6443
|
-
!reportName
|
|
6444
|
-
? 'Please save your report first'
|
|
6445
|
-
: ''
|
|
6446
|
-
}
|
|
6447
|
-
>
|
|
6448
|
-
<DownloadIcon size={16} />
|
|
6449
|
-
Export as PDF
|
|
6450
|
-
</button>
|
|
6451
|
-
</div>
|
|
6452
|
-
</div>
|
|
6453
7583
|
</div>
|
|
6454
7584
|
</>
|
|
6455
7585
|
)}
|
|
7586
|
+
|
|
7587
|
+
{/* Show no data message when no results found */}
|
|
7588
|
+
{!groupedData && previewData.length === 0 && !isLoading && selectedTable && selectedColumns.length > 0 && (
|
|
7589
|
+
<div className={styles.noDataContainer}>
|
|
7590
|
+
<div className={styles.noDataMessage}>
|
|
7591
|
+
<div className={styles.noDataIcon}>
|
|
7592
|
+
<Database size={48} />
|
|
7593
|
+
</div>
|
|
7594
|
+
<h3 className={styles.noDataTitle}>No Data Found</h3>
|
|
7595
|
+
<p className={styles.noDataText}>
|
|
7596
|
+
No records match the selected filters and criteria.
|
|
7597
|
+
</p>
|
|
7598
|
+
<div className={styles.noDataSuggestions}>
|
|
7599
|
+
<p><strong>Try:</strong></p>
|
|
7600
|
+
<ul>
|
|
7601
|
+
<li>Adjusting or removing filters</li>
|
|
7602
|
+
<li>Selecting a broader date range</li>
|
|
7603
|
+
<li>Checking if data exists for the selected table</li>
|
|
7604
|
+
</ul>
|
|
7605
|
+
</div>
|
|
7606
|
+
</div>
|
|
7607
|
+
</div>
|
|
7608
|
+
)}
|
|
6456
7609
|
</div>
|
|
6457
7610
|
);
|
|
6458
7611
|
};
|