@visns-studio/visns-components 5.14.8 → 5.14.10
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 +59 -0
- package/package.json +2 -1
- package/src/components/DataGrid.jsx +24 -47
- package/src/components/generic/GenericDashboard.jsx +42 -56
- package/src/components/generic/GenericDetail.jsx +21 -43
- package/src/components/generic/GenericIndex.jsx +14 -36
- package/src/components/generic/GenericReport.jsx +993 -114
- package/src/components/generic/GroupedReportRenderer.jsx +91 -0
- package/src/components/generic/SectionGroupedReport.jsx +388 -0
- package/src/components/generic/StandardModal.jsx +119 -0
- package/src/components/generic/groupedReport.css +307 -0
- package/src/components/generic/shared/groupingUtils.js +118 -0
- package/src/components/styles/GenericReport.module.scss +204 -0
- package/src/index.js +2 -0
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
import {
|
|
7
8
|
HelpCircle as Question,
|
|
8
9
|
Link as LinkChain,
|
|
@@ -33,9 +34,11 @@ import {
|
|
|
33
34
|
import CustomFetch from '../Fetch';
|
|
34
35
|
import Download from '../Download';
|
|
35
36
|
import { saveAs } from 'file-saver';
|
|
37
|
+
import * as XLSX from 'xlsx';
|
|
36
38
|
import Swal from 'sweetalert2';
|
|
37
39
|
import styles from '../styles/GenericReport.module.scss';
|
|
38
40
|
import '../styles/SweetAlert.module.css';
|
|
41
|
+
import './groupedReport.css';
|
|
39
42
|
|
|
40
43
|
// Icon mapping for common table types
|
|
41
44
|
const tableIcons = {
|
|
@@ -907,6 +910,22 @@ const wizardSteps = [
|
|
|
907
910
|
tip: 'Filters help you focus on specific data, like only active customers or recent transactions.',
|
|
908
911
|
},
|
|
909
912
|
},
|
|
913
|
+
{
|
|
914
|
+
id: 'grouping',
|
|
915
|
+
title: 'Group Configuration',
|
|
916
|
+
icon: Data,
|
|
917
|
+
description: 'Organize data into groups (optional)',
|
|
918
|
+
guidance: {
|
|
919
|
+
summary: 'Group your data by a specific field for better organization',
|
|
920
|
+
howTo: [
|
|
921
|
+
'Enable grouping to organize data into sections',
|
|
922
|
+
'Choose which field to group by (e.g., Department, Category, Status)',
|
|
923
|
+
'Configure group display options and styling',
|
|
924
|
+
'Skip this step for a standard table report',
|
|
925
|
+
],
|
|
926
|
+
tip: 'Grouping is perfect for reports like "Projects by Facility" or "Sales by Region".',
|
|
927
|
+
},
|
|
928
|
+
},
|
|
910
929
|
{
|
|
911
930
|
id: 'preview',
|
|
912
931
|
title: 'Preview & Save',
|
|
@@ -1016,6 +1035,7 @@ const GenericReportImproved = ({
|
|
|
1016
1035
|
|
|
1017
1036
|
// State for report preview
|
|
1018
1037
|
const [previewData, setPreviewData] = useState([]);
|
|
1038
|
+
const [groupedData, setGroupedData] = useState(null);
|
|
1019
1039
|
const [gridColumns, setGridColumns] = useState([]);
|
|
1020
1040
|
const [isLoading, setIsLoading] = useState(false);
|
|
1021
1041
|
const [hasAutoExecuted, setHasAutoExecuted] = useState(false);
|
|
@@ -1039,6 +1059,27 @@ const GenericReportImproved = ({
|
|
|
1039
1059
|
});
|
|
1040
1060
|
const [customCalculatedFields, setCustomCalculatedFields] = useState([]);
|
|
1041
1061
|
|
|
1062
|
+
// State for group configuration
|
|
1063
|
+
const [groupingConfig, setGroupingConfig] = useState({
|
|
1064
|
+
enabled: false,
|
|
1065
|
+
groupByField: '',
|
|
1066
|
+
groupDisplayName: '',
|
|
1067
|
+
sortBy: '',
|
|
1068
|
+
sortDirection: 'asc',
|
|
1069
|
+
rowsPerGroup: 10,
|
|
1070
|
+
showEmptyRows: false,
|
|
1071
|
+
showGroupTotals: true,
|
|
1072
|
+
expandable: true,
|
|
1073
|
+
groupHeaderStyle: 'primary',
|
|
1074
|
+
emptyRowStyle: 'light'
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// State for custom URLs
|
|
1078
|
+
const [customUrls, setCustomUrls] = useState({
|
|
1079
|
+
executeUrl: '',
|
|
1080
|
+
exportUrl: ''
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1042
1083
|
// Note: Removed table search functionality to simplify user experience
|
|
1043
1084
|
|
|
1044
1085
|
// Extract additional settings with defaults
|
|
@@ -1378,7 +1419,7 @@ const GenericReportImproved = ({
|
|
|
1378
1419
|
// Auto-execute report when user reaches step 5 (preview step)
|
|
1379
1420
|
useEffect(() => {
|
|
1380
1421
|
if (
|
|
1381
|
-
currentWizardStep ===
|
|
1422
|
+
currentWizardStep === 5 && // Step 6 is index 5 (preview step)
|
|
1382
1423
|
selectedTable &&
|
|
1383
1424
|
selectedColumns.length > 0 &&
|
|
1384
1425
|
previewData.length === 0 && // Only if no data loaded yet
|
|
@@ -1496,8 +1537,10 @@ const GenericReportImproved = ({
|
|
|
1496
1537
|
return selectedColumns.length > 0;
|
|
1497
1538
|
case 'filters':
|
|
1498
1539
|
return true; // Optional step
|
|
1540
|
+
case 'grouping':
|
|
1541
|
+
return true; // Optional step - always complete (user can skip or configure)
|
|
1499
1542
|
case 'preview':
|
|
1500
|
-
return previewData.length > 0;
|
|
1543
|
+
return previewData.length > 0 || groupedData !== null;
|
|
1501
1544
|
default:
|
|
1502
1545
|
return false;
|
|
1503
1546
|
}
|
|
@@ -2191,12 +2234,80 @@ const GenericReportImproved = ({
|
|
|
2191
2234
|
|
|
2192
2235
|
// Create dataSource function for server-side pagination (similar to DataGrid)
|
|
2193
2236
|
const dataSource = useCallback(
|
|
2194
|
-
async ({ skip, limit, sortInfo, filterValue }) => {
|
|
2237
|
+
async ({ skip, limit, sortInfo, filterValue, grouping, template }) => {
|
|
2195
2238
|
if (!selectedTable || selectedColumns.length === 0) {
|
|
2196
2239
|
return { data: [], count: 0 };
|
|
2197
2240
|
}
|
|
2198
2241
|
|
|
2199
2242
|
try {
|
|
2243
|
+
// Check if this is a grouped report
|
|
2244
|
+
const isGroupedReport = grouping?.enabled || selectedTemplate?.grouping?.enabled;
|
|
2245
|
+
|
|
2246
|
+
if (isGroupedReport) {
|
|
2247
|
+
// Check if we're using a dynamic template (custom grouping) or predefined template
|
|
2248
|
+
const isDynamicTemplate = !!template;
|
|
2249
|
+
|
|
2250
|
+
// Priority: custom URL > template URL > default URL
|
|
2251
|
+
const templateExecuteUrl = customUrls.executeUrl || selectedTemplate?.executeUrl || executeUrl;
|
|
2252
|
+
|
|
2253
|
+
// Debug logging
|
|
2254
|
+
console.log('🔍 [DEBUG] Grouped Report Detected:', {
|
|
2255
|
+
selectedTemplate: selectedTemplate?.id,
|
|
2256
|
+
isDynamicTemplate,
|
|
2257
|
+
template,
|
|
2258
|
+
customExecuteUrl: customUrls.executeUrl,
|
|
2259
|
+
templateExecuteUrl: selectedTemplate?.executeUrl,
|
|
2260
|
+
defaultExecuteUrl: executeUrl,
|
|
2261
|
+
finalExecuteUrl: templateExecuteUrl,
|
|
2262
|
+
isGroupedReport,
|
|
2263
|
+
templateGroupingConfig: selectedTemplate?.grouping,
|
|
2264
|
+
customGroupingConfig: groupingConfig,
|
|
2265
|
+
usingCustomUrl: !!customUrls.executeUrl,
|
|
2266
|
+
usingTemplateExecuteUrl: !!selectedTemplate?.executeUrl
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
let payload;
|
|
2270
|
+
|
|
2271
|
+
if (isDynamicTemplate) {
|
|
2272
|
+
// For dynamic templates, send the complete template configuration
|
|
2273
|
+
payload = {
|
|
2274
|
+
dynamic_template: template,
|
|
2275
|
+
table: selectedTable,
|
|
2276
|
+
columns: selectedColumns,
|
|
2277
|
+
joins: joins,
|
|
2278
|
+
filters: flattenFilterCriteria(filterCriteria),
|
|
2279
|
+
sorting: sortCriteria
|
|
2280
|
+
};
|
|
2281
|
+
} else {
|
|
2282
|
+
// For predefined templates, use the existing approach
|
|
2283
|
+
payload = {
|
|
2284
|
+
template_id: selectedTemplate?.id || 'utilization_planning',
|
|
2285
|
+
filters: []
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
console.log('🚀 [DEBUG] Sending grouped report request:', {
|
|
2290
|
+
url: templateExecuteUrl,
|
|
2291
|
+
method: 'POST',
|
|
2292
|
+
payload
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
const result = await CustomFetch(
|
|
2296
|
+
templateExecuteUrl,
|
|
2297
|
+
'POST',
|
|
2298
|
+
payload
|
|
2299
|
+
);
|
|
2300
|
+
|
|
2301
|
+
console.log('📊 [DEBUG] Grouped report response:', result);
|
|
2302
|
+
|
|
2303
|
+
if (result.error) {
|
|
2304
|
+
console.error('❌ Grouped report execution error:', result.error);
|
|
2305
|
+
return { data: [], count: 0 };
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
return result;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2200
2311
|
const page = Math.floor(skip / limit) + 1;
|
|
2201
2312
|
|
|
2202
2313
|
// Use the same payload format as GenericReport (with query wrapper)
|
|
@@ -2250,8 +2361,21 @@ const GenericReportImproved = ({
|
|
|
2250
2361
|
page: page,
|
|
2251
2362
|
};
|
|
2252
2363
|
|
|
2364
|
+
// Priority: custom URL > template URL > default URL
|
|
2365
|
+
const finalExecuteUrl = customUrls.executeUrl || selectedTemplate?.executeUrl || executeUrl;
|
|
2366
|
+
|
|
2367
|
+
console.log('🔍 [DEBUG] Regular Report API Call:', {
|
|
2368
|
+
selectedTemplate: selectedTemplate?.id,
|
|
2369
|
+
customExecuteUrl: customUrls.executeUrl,
|
|
2370
|
+
templateExecuteUrl: selectedTemplate?.executeUrl,
|
|
2371
|
+
defaultExecuteUrl: executeUrl,
|
|
2372
|
+
finalExecuteUrl,
|
|
2373
|
+
usingCustomUrl: !!customUrls.executeUrl,
|
|
2374
|
+
usingTemplateExecuteUrl: !!selectedTemplate?.executeUrl
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2253
2377
|
const result = await CustomFetch(
|
|
2254
|
-
|
|
2378
|
+
finalExecuteUrl,
|
|
2255
2379
|
'POST',
|
|
2256
2380
|
reportConfig
|
|
2257
2381
|
);
|
|
@@ -2292,11 +2416,23 @@ const GenericReportImproved = ({
|
|
|
2292
2416
|
filterCriteria,
|
|
2293
2417
|
sortCriteria,
|
|
2294
2418
|
executeUrl,
|
|
2419
|
+
selectedTemplate,
|
|
2420
|
+
groupingConfig,
|
|
2421
|
+
customUrls,
|
|
2295
2422
|
]
|
|
2296
2423
|
);
|
|
2297
2424
|
|
|
2298
2425
|
// Execute report function for manual execution (creates grid columns)
|
|
2299
2426
|
const executeReport = async () => {
|
|
2427
|
+
console.log('🎯 [DEBUG] executeReport called:', {
|
|
2428
|
+
selectedTable,
|
|
2429
|
+
selectedColumns: selectedColumns.length,
|
|
2430
|
+
selectedTemplate: selectedTemplate?.id,
|
|
2431
|
+
hasGrouping: selectedTemplate?.grouping?.enabled,
|
|
2432
|
+
executeUrl,
|
|
2433
|
+
setting
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2300
2436
|
if (!selectedTable || selectedColumns.length === 0) {
|
|
2301
2437
|
toast.warning(
|
|
2302
2438
|
'Please select at least one field to include in your report.'
|
|
@@ -2305,22 +2441,127 @@ const GenericReportImproved = ({
|
|
|
2305
2441
|
}
|
|
2306
2442
|
|
|
2307
2443
|
setIsLoading(true);
|
|
2444
|
+
setGroupedData(null); // Reset grouped data
|
|
2308
2445
|
|
|
2309
2446
|
try {
|
|
2310
|
-
//
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2447
|
+
// Check if selected template has grouping enabled OR custom grouping is enabled
|
|
2448
|
+
const hasTemplateGrouping = selectedTemplate?.grouping?.enabled;
|
|
2449
|
+
const hasCustomGrouping = groupingConfig.enabled;
|
|
2450
|
+
const hasGrouping = hasTemplateGrouping || hasCustomGrouping;
|
|
2451
|
+
let fetchedDataCount = 0;
|
|
2452
|
+
|
|
2453
|
+
console.log('📋 [DEBUG] Template analysis:', {
|
|
2454
|
+
selectedTemplate,
|
|
2455
|
+
hasTemplateGrouping,
|
|
2456
|
+
hasCustomGrouping,
|
|
2457
|
+
hasGrouping,
|
|
2458
|
+
customGroupingConfig: groupingConfig,
|
|
2459
|
+
templateGroupingConfig: selectedTemplate?.grouping
|
|
2316
2460
|
});
|
|
2461
|
+
console.log('🎯 [DEBUG] Initial fetchedDataCount:', fetchedDataCount);
|
|
2462
|
+
|
|
2463
|
+
if (hasGrouping) {
|
|
2464
|
+
// Determine which grouping configuration to use
|
|
2465
|
+
let effectiveGrouping;
|
|
2466
|
+
let dynamicTemplate;
|
|
2467
|
+
|
|
2468
|
+
if (hasCustomGrouping && groupingConfig.groupByField) {
|
|
2469
|
+
// Create dynamic template with custom grouping configuration
|
|
2470
|
+
dynamicTemplate = {
|
|
2471
|
+
id: `custom_grouped_${Date.now()}`,
|
|
2472
|
+
name: reportName || 'Custom Grouped Report',
|
|
2473
|
+
mainTable: selectedTable,
|
|
2474
|
+
defaultColumns: selectedColumns,
|
|
2475
|
+
calculatedFields: customCalculatedFields,
|
|
2476
|
+
grouping: {
|
|
2477
|
+
enabled: true,
|
|
2478
|
+
type: 'sections',
|
|
2479
|
+
groupByField: groupingConfig.groupByField,
|
|
2480
|
+
groupDisplayName: groupingConfig.groupDisplayName,
|
|
2481
|
+
sortBy: groupingConfig.groupByField, // Use same field for sorting
|
|
2482
|
+
sortDirection: groupingConfig.sortDirection,
|
|
2483
|
+
rowsPerGroup: groupingConfig.rowsPerGroup,
|
|
2484
|
+
showEmptyRows: groupingConfig.showEmptyRows,
|
|
2485
|
+
showGroupTotals: groupingConfig.showGroupTotals,
|
|
2486
|
+
expandable: groupingConfig.expandable,
|
|
2487
|
+
groupHeaderStyle: groupingConfig.groupHeaderStyle,
|
|
2488
|
+
emptyRowStyle: groupingConfig.emptyRowStyle
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
effectiveGrouping = dynamicTemplate.grouping;
|
|
2492
|
+
|
|
2493
|
+
console.log('🏗️ [DEBUG] Created dynamic template for custom grouping:', dynamicTemplate);
|
|
2494
|
+
} else if (hasCustomGrouping && !groupingConfig.groupByField) {
|
|
2495
|
+
// Custom grouping enabled but no field selected - show error and fallback to regular report
|
|
2496
|
+
toast.error('Please select a field to group by before executing the report.');
|
|
2497
|
+
console.log('⚠️ [DEBUG] Custom grouping enabled but no groupByField selected');
|
|
2498
|
+
setIsLoading(false);
|
|
2499
|
+
return;
|
|
2500
|
+
} else {
|
|
2501
|
+
// Use existing template grouping
|
|
2502
|
+
effectiveGrouping = selectedTemplate.grouping;
|
|
2503
|
+
console.log('📋 [DEBUG] Using template grouping:', effectiveGrouping);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// For grouped reports, fetch data with reasonable limit
|
|
2507
|
+
const groupedResult = await dataSource({
|
|
2508
|
+
skip: 0,
|
|
2509
|
+
limit: 1000, // Maximum allowed limit
|
|
2510
|
+
sortInfo: null,
|
|
2511
|
+
filterValue: null,
|
|
2512
|
+
grouping: effectiveGrouping,
|
|
2513
|
+
template: dynamicTemplate // Pass dynamic template if created
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
console.log('🎯 [DEBUG] Full API Response:', groupedResult);
|
|
2517
|
+
console.log('🎯 [DEBUG] groupedResult.data:', groupedResult.data);
|
|
2518
|
+
console.log('🎯 [DEBUG] groupedResult.data.grouped:', groupedResult.data?.grouped);
|
|
2519
|
+
console.log('🎯 [DEBUG] groupedResult.data.groupedData:', groupedResult.data?.groupedData);
|
|
2520
|
+
|
|
2521
|
+
if (groupedResult.data?.grouped && groupedResult.data?.groupedData?.groups) {
|
|
2522
|
+
console.log('✅ Processing grouped report');
|
|
2523
|
+
setGroupedData(groupedResult.data); // Store entire response with grouped flag
|
|
2524
|
+
setPreviewData([]); // Clear regular preview data
|
|
2525
|
+
// Get the count from the fetched grouped data
|
|
2526
|
+
console.log('🎯 [DEBUG] Before assignment - groupedResult.data.groupedData.totalRecords:', groupedResult.data.groupedData?.totalRecords);
|
|
2527
|
+
fetchedDataCount = groupedResult.data.groupedData?.totalRecords || 0;
|
|
2528
|
+
console.log('🎯 [DEBUG] After assignment - fetchedDataCount:', fetchedDataCount);
|
|
2529
|
+
console.log('🔢 [DEBUG] Grouped data count:', {
|
|
2530
|
+
totalRecords: groupedResult.data.groupedData?.totalRecords,
|
|
2531
|
+
fetchedDataCount,
|
|
2532
|
+
hasGroups: !!groupedResult.data.groupedData?.groups
|
|
2533
|
+
});
|
|
2534
|
+
} else {
|
|
2535
|
+
// Fallback to regular display if server doesn't support grouping
|
|
2536
|
+
console.log('⚠️ Fallback to regular display');
|
|
2537
|
+
const fallbackData = groupedResult.data?.data || [];
|
|
2538
|
+
console.log('🎯 [DEBUG] Fallback data length:', fallbackData.length);
|
|
2539
|
+
setPreviewData(fallbackData);
|
|
2540
|
+
fetchedDataCount = fallbackData.length;
|
|
2541
|
+
}
|
|
2542
|
+
} else {
|
|
2543
|
+
// Regular non-grouped report
|
|
2544
|
+
const firstPageResult = await dataSource({
|
|
2545
|
+
skip: 0,
|
|
2546
|
+
limit: pageSize,
|
|
2547
|
+
sortInfo: null,
|
|
2548
|
+
filterValue: null,
|
|
2549
|
+
});
|
|
2317
2550
|
|
|
2318
|
-
|
|
2319
|
-
|
|
2551
|
+
if (firstPageResult.data && firstPageResult.data.length > 0) {
|
|
2552
|
+
setPreviewData(firstPageResult.data);
|
|
2553
|
+
fetchedDataCount = firstPageResult.data.length;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2320
2556
|
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const
|
|
2557
|
+
// Create grid columns for regular reports only
|
|
2558
|
+
if (!hasGrouping) {
|
|
2559
|
+
const dataToProcess = previewData.length > 0 ? previewData : [];
|
|
2560
|
+
|
|
2561
|
+
if (dataToProcess.length > 0) {
|
|
2562
|
+
// Create grid columns dynamically from actual data with smart formatting
|
|
2563
|
+
const firstRow = dataToProcess[0];
|
|
2564
|
+
const dynamicColumns = Object.keys(firstRow).map((key) => {
|
|
2324
2565
|
// Find the column metadata from selected columns to get type information
|
|
2325
2566
|
const columnMetadata = selectedColumns.find((col) => {
|
|
2326
2567
|
// Handle both direct column names and aliased names
|
|
@@ -2333,7 +2574,7 @@ const GenericReportImproved = ({
|
|
|
2333
2574
|
});
|
|
2334
2575
|
|
|
2335
2576
|
// Check if any value in this column is JSON
|
|
2336
|
-
const hasJsonValues =
|
|
2577
|
+
const hasJsonValues = dataToProcess.some((row) => {
|
|
2337
2578
|
return isJsonValue(row[key]);
|
|
2338
2579
|
});
|
|
2339
2580
|
|
|
@@ -2428,11 +2669,11 @@ const GenericReportImproved = ({
|
|
|
2428
2669
|
return formattedValue;
|
|
2429
2670
|
},
|
|
2430
2671
|
};
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2672
|
+
});
|
|
2673
|
+
setGridColumns(dynamicColumns);
|
|
2674
|
+
} else {
|
|
2675
|
+
// Fallback to selected columns if no data
|
|
2676
|
+
const columns = selectedColumns.map((col) => {
|
|
2436
2677
|
// Find column type from table columns with enhanced detection
|
|
2437
2678
|
let columnType = 'varchar';
|
|
2438
2679
|
const tableColumn = tableColumns.find(
|
|
@@ -2468,7 +2709,7 @@ const GenericReportImproved = ({
|
|
|
2468
2709
|
}
|
|
2469
2710
|
|
|
2470
2711
|
return {
|
|
2471
|
-
name: col.displayName ||
|
|
2712
|
+
name: col.displayName || `${col.table}.${col.column}`,
|
|
2472
2713
|
header: col.displayName || formatName(col.column),
|
|
2473
2714
|
defaultFlex: 1,
|
|
2474
2715
|
minWidth: 100,
|
|
@@ -2499,16 +2740,19 @@ const GenericReportImproved = ({
|
|
|
2499
2740
|
return formattedValue;
|
|
2500
2741
|
},
|
|
2501
2742
|
};
|
|
2502
|
-
|
|
2503
|
-
|
|
2743
|
+
});
|
|
2744
|
+
setGridColumns(columns);
|
|
2745
|
+
}
|
|
2504
2746
|
}
|
|
2505
2747
|
|
|
2506
2748
|
if (currentWizardStep < wizardSteps.length - 1) {
|
|
2507
2749
|
setCurrentWizardStep(wizardSteps.length - 1);
|
|
2508
2750
|
}
|
|
2509
2751
|
|
|
2510
|
-
// Show success message with
|
|
2511
|
-
|
|
2752
|
+
// Show success message with available data count
|
|
2753
|
+
console.log('🎯 [DEBUG] Final fetchedDataCount before toast:', fetchedDataCount);
|
|
2754
|
+
console.log('🎯 [DEBUG] Type of fetchedDataCount:', typeof fetchedDataCount);
|
|
2755
|
+
toast.success(`Found ${fetchedDataCount} records`);
|
|
2512
2756
|
} catch (error) {
|
|
2513
2757
|
toast.error('Failed to execute report');
|
|
2514
2758
|
console.error('Error executing report:', error);
|
|
@@ -2597,12 +2841,54 @@ const GenericReportImproved = ({
|
|
|
2597
2841
|
}
|
|
2598
2842
|
}
|
|
2599
2843
|
|
|
2844
|
+
// Restore grouping configuration
|
|
2845
|
+
if (config.grouping) {
|
|
2846
|
+
setGroupingConfig({
|
|
2847
|
+
enabled: config.isGrouped || false,
|
|
2848
|
+
groupByField: config.grouping.groupByField || '',
|
|
2849
|
+
groupDisplayName: config.grouping.groupDisplayName || '',
|
|
2850
|
+
sortBy: config.grouping.sortBy || '',
|
|
2851
|
+
sortDirection: config.grouping.sortDirection || 'asc',
|
|
2852
|
+
rowsPerGroup: config.grouping.rowsPerGroup || 10,
|
|
2853
|
+
showEmptyRows: config.grouping.showEmptyRows || false,
|
|
2854
|
+
showGroupTotals: config.grouping.showGroupTotals || true,
|
|
2855
|
+
expandable: config.grouping.expandable || true,
|
|
2856
|
+
groupHeaderStyle: config.grouping.groupHeaderStyle || 'primary',
|
|
2857
|
+
emptyRowStyle: config.grouping.emptyRowStyle || 'light'
|
|
2858
|
+
});
|
|
2859
|
+
} else {
|
|
2860
|
+
// Reset grouping config if not present in saved report
|
|
2861
|
+
setGroupingConfig({
|
|
2862
|
+
enabled: false,
|
|
2863
|
+
groupByField: '',
|
|
2864
|
+
groupDisplayName: '',
|
|
2865
|
+
sortBy: '',
|
|
2866
|
+
sortDirection: 'asc',
|
|
2867
|
+
rowsPerGroup: 10,
|
|
2868
|
+
showEmptyRows: false,
|
|
2869
|
+
showGroupTotals: true,
|
|
2870
|
+
expandable: true,
|
|
2871
|
+
groupHeaderStyle: 'primary',
|
|
2872
|
+
emptyRowStyle: 'light'
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// Restore custom calculated fields if present
|
|
2877
|
+
if (config.customCalculatedFields) {
|
|
2878
|
+
setCustomCalculatedFields(config.customCalculatedFields);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// Restore custom URLs if present
|
|
2882
|
+
if (config.customUrls) {
|
|
2883
|
+
setCustomUrls(config.customUrls);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2600
2886
|
setReportName(report.label);
|
|
2601
2887
|
setIsPublic(report.is_public || false);
|
|
2602
2888
|
setLoadedReportId(report.id); // Track the loaded report ID for overwrite functionality
|
|
2603
2889
|
setShowTemplates(false);
|
|
2604
|
-
// Set to the preview step (index
|
|
2605
|
-
setCurrentWizardStep(
|
|
2890
|
+
// Set to the preview step (index 5) since the report is already configured
|
|
2891
|
+
setCurrentWizardStep(5);
|
|
2606
2892
|
|
|
2607
2893
|
toast.success(`Loaded report: ${report.label}`);
|
|
2608
2894
|
} catch (error) {
|
|
@@ -2614,6 +2900,13 @@ const GenericReportImproved = ({
|
|
|
2614
2900
|
// Load business template configuration
|
|
2615
2901
|
const loadBusinessTemplate = async (template) => {
|
|
2616
2902
|
try {
|
|
2903
|
+
console.log('🏗️ [DEBUG] Loading business template:', {
|
|
2904
|
+
templateId: template.id,
|
|
2905
|
+
templateName: template.name,
|
|
2906
|
+
hasGrouping: template.grouping?.enabled,
|
|
2907
|
+
groupingConfig: template.grouping
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2617
2910
|
setSelectedTemplate(template);
|
|
2618
2911
|
|
|
2619
2912
|
// Set main table
|
|
@@ -2701,6 +2994,44 @@ const GenericReportImproved = ({
|
|
|
2701
2994
|
}
|
|
2702
2995
|
}
|
|
2703
2996
|
|
|
2997
|
+
// Apply template grouping configuration
|
|
2998
|
+
if (template.grouping && template.grouping.enabled) {
|
|
2999
|
+
setGroupingConfig({
|
|
3000
|
+
enabled: template.grouping.enabled || false,
|
|
3001
|
+
groupByField: template.grouping.groupByField || '',
|
|
3002
|
+
groupDisplayName: template.grouping.groupDisplayName || '',
|
|
3003
|
+
sortBy: template.grouping.sortBy || '',
|
|
3004
|
+
sortDirection: template.grouping.sortDirection || 'asc',
|
|
3005
|
+
rowsPerGroup: template.grouping.rowsPerGroup || 10,
|
|
3006
|
+
showEmptyRows: template.grouping.showEmptyRows || false,
|
|
3007
|
+
showGroupTotals: template.grouping.showGroupTotals || true,
|
|
3008
|
+
expandable: template.grouping.expandable !== false, // Default true
|
|
3009
|
+
groupHeaderStyle: template.grouping.groupHeaderStyle || 'primary',
|
|
3010
|
+
emptyRowStyle: template.grouping.emptyRowStyle || 'light'
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
console.log('🏗️ [DEBUG] Applied template grouping config:', {
|
|
3014
|
+
enabled: template.grouping.enabled,
|
|
3015
|
+
groupByField: template.grouping.groupByField,
|
|
3016
|
+
groupDisplayName: template.grouping.groupDisplayName
|
|
3017
|
+
});
|
|
3018
|
+
} else {
|
|
3019
|
+
// Reset grouping config if template doesn't have grouping
|
|
3020
|
+
setGroupingConfig({
|
|
3021
|
+
enabled: false,
|
|
3022
|
+
groupByField: '',
|
|
3023
|
+
groupDisplayName: '',
|
|
3024
|
+
sortBy: '',
|
|
3025
|
+
sortDirection: 'asc',
|
|
3026
|
+
rowsPerGroup: 10,
|
|
3027
|
+
showEmptyRows: false,
|
|
3028
|
+
showGroupTotals: true,
|
|
3029
|
+
expandable: true,
|
|
3030
|
+
groupHeaderStyle: 'primary',
|
|
3031
|
+
emptyRowStyle: 'light'
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
|
|
2704
3035
|
// Set report name based on template
|
|
2705
3036
|
setReportName(template.name);
|
|
2706
3037
|
setIsPublic(false);
|
|
@@ -3080,6 +3411,8 @@ const GenericReportImproved = ({
|
|
|
3080
3411
|
return renderColumnSelection();
|
|
3081
3412
|
case 'filters':
|
|
3082
3413
|
return renderFilters();
|
|
3414
|
+
case 'grouping':
|
|
3415
|
+
return renderGrouping();
|
|
3083
3416
|
case 'preview':
|
|
3084
3417
|
return renderPreview();
|
|
3085
3418
|
default:
|
|
@@ -6078,9 +6411,22 @@ const GenericReportImproved = ({
|
|
|
6078
6411
|
|
|
6079
6412
|
console.log('📁 Generated filename:', fileName);
|
|
6080
6413
|
|
|
6414
|
+
// Priority: custom export URL > template export URL > default export URL
|
|
6415
|
+
const finalExportUrl = customUrls.exportUrl || selectedTemplate?.exportUrl || exportUrl;
|
|
6416
|
+
|
|
6417
|
+
console.log('🔍 [DEBUG] Export API Call:', {
|
|
6418
|
+
selectedTemplate: selectedTemplate?.id,
|
|
6419
|
+
customExportUrl: customUrls.exportUrl,
|
|
6420
|
+
templateExportUrl: selectedTemplate?.exportUrl,
|
|
6421
|
+
defaultExportUrl: exportUrl,
|
|
6422
|
+
finalExportUrl,
|
|
6423
|
+
usingCustomExportUrl: !!customUrls.exportUrl,
|
|
6424
|
+
usingTemplateExportUrl: !!selectedTemplate?.exportUrl
|
|
6425
|
+
});
|
|
6426
|
+
|
|
6081
6427
|
// Use Download component which handles error checking and blob download properly
|
|
6082
|
-
console.log('📡 Making request to:',
|
|
6083
|
-
const result = await Download(
|
|
6428
|
+
console.log('📡 Making request to:', finalExportUrl);
|
|
6429
|
+
const result = await Download(finalExportUrl, 'POST', exportData);
|
|
6084
6430
|
|
|
6085
6431
|
console.log('Download component returned result:', result);
|
|
6086
6432
|
|
|
@@ -6185,6 +6531,183 @@ const GenericReportImproved = ({
|
|
|
6185
6531
|
}
|
|
6186
6532
|
};
|
|
6187
6533
|
|
|
6534
|
+
// Handle grouped report export to Excel
|
|
6535
|
+
const handleRegularExport = async () => {
|
|
6536
|
+
if (previewData.length === 0) {
|
|
6537
|
+
toast.error('No data available to export');
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
|
|
6541
|
+
try {
|
|
6542
|
+
// Use the existing client-side Excel export functionality
|
|
6543
|
+
await exportReport('excel');
|
|
6544
|
+
} catch (error) {
|
|
6545
|
+
console.error('Regular export failed:', error);
|
|
6546
|
+
toast.error('Export failed. Please try again.');
|
|
6547
|
+
}
|
|
6548
|
+
};
|
|
6549
|
+
|
|
6550
|
+
const handleGroupedExport = async () => {
|
|
6551
|
+
if (!selectedTemplate) {
|
|
6552
|
+
toast.error('No template selected for export');
|
|
6553
|
+
return;
|
|
6554
|
+
}
|
|
6555
|
+
|
|
6556
|
+
// Debug logging to understand the groupedData state
|
|
6557
|
+
console.log('🔍 [DEBUG] Export function called with:', {
|
|
6558
|
+
groupedData: groupedData,
|
|
6559
|
+
hasGroupedData: !!groupedData,
|
|
6560
|
+
hasGroups: !!(groupedData && groupedData.groups),
|
|
6561
|
+
groupsLength: groupedData && groupedData.groups ? groupedData.groups.length : 'undefined',
|
|
6562
|
+
selectedColumns: selectedColumns,
|
|
6563
|
+
selectedTemplate: selectedTemplate?.name
|
|
6564
|
+
});
|
|
6565
|
+
|
|
6566
|
+
// Check the correct path: groupedData.groupedData.groups
|
|
6567
|
+
const actualGroups = groupedData?.groupedData?.groups;
|
|
6568
|
+
if (!groupedData || !actualGroups || actualGroups.length === 0) {
|
|
6569
|
+
console.error('❌ [DEBUG] Export failed - no grouped data:', {
|
|
6570
|
+
groupedData: groupedData,
|
|
6571
|
+
hasGroupedData: !!groupedData,
|
|
6572
|
+
hasGroupedDataProperty: !!(groupedData && groupedData.groupedData),
|
|
6573
|
+
hasGroups: !!(groupedData && groupedData.groupedData && groupedData.groupedData.groups),
|
|
6574
|
+
groupsLength: actualGroups ? actualGroups.length : 'undefined'
|
|
6575
|
+
});
|
|
6576
|
+
toast.error('No grouped data available for export');
|
|
6577
|
+
return;
|
|
6578
|
+
}
|
|
6579
|
+
|
|
6580
|
+
try {
|
|
6581
|
+
toast.info('Generating Excel export...');
|
|
6582
|
+
|
|
6583
|
+
// Create workbook and worksheet
|
|
6584
|
+
const workbook = XLSX.utils.book_new();
|
|
6585
|
+
const worksheetData = [];
|
|
6586
|
+
|
|
6587
|
+
// Add headers
|
|
6588
|
+
const headers = selectedColumns.map(col => col.displayName || col.column);
|
|
6589
|
+
worksheetData.push(headers);
|
|
6590
|
+
|
|
6591
|
+
// Helper function to get cell value (same logic as SectionGroupedReport)
|
|
6592
|
+
const getCellValue = (row, column) => {
|
|
6593
|
+
if (row.__empty_row__) return '';
|
|
6594
|
+
|
|
6595
|
+
let value;
|
|
6596
|
+
|
|
6597
|
+
// Try multiple field access patterns
|
|
6598
|
+
if (column.displayName && row[`\`${column.displayName}\``] !== undefined) {
|
|
6599
|
+
value = row[`\`${column.displayName}\``];
|
|
6600
|
+
} else if (column.displayName && row[column.displayName] !== undefined) {
|
|
6601
|
+
value = row[column.displayName];
|
|
6602
|
+
} else if (column.table && row[`${column.table}_${column.column}`] !== undefined) {
|
|
6603
|
+
value = row[`${column.table}_${column.column}`];
|
|
6604
|
+
} else if (column.table && row[`\`${column.table}_${column.column}\``] !== undefined) {
|
|
6605
|
+
value = row[`\`${column.table}_${column.column}\``];
|
|
6606
|
+
} else if (row[column.column] !== undefined) {
|
|
6607
|
+
value = row[column.column];
|
|
6608
|
+
} else if (row[`\`${column.column}\``] !== undefined) {
|
|
6609
|
+
value = row[`\`${column.column}\``];
|
|
6610
|
+
} else if (column.id && row[column.id] !== undefined) {
|
|
6611
|
+
value = row[column.id];
|
|
6612
|
+
}
|
|
6613
|
+
|
|
6614
|
+
// Format the value
|
|
6615
|
+
if (value === null || value === undefined) return '';
|
|
6616
|
+
|
|
6617
|
+
// Handle dates
|
|
6618
|
+
if (column.type === 'date' || column.type === 'datetime') {
|
|
6619
|
+
if (value && moment(value).isValid()) {
|
|
6620
|
+
return column.type === 'date'
|
|
6621
|
+
? moment(value).format('DD/MM/YYYY')
|
|
6622
|
+
: moment(value).format('DD/MM/YYYY HH:mm:ss');
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
// Handle Comments field - strip HTML and formatting
|
|
6627
|
+
if (column.displayName === 'Comments' && value) {
|
|
6628
|
+
return value.replace(/• /g, '').replace(/<[^>]*>/g, '').trim();
|
|
6629
|
+
}
|
|
6630
|
+
|
|
6631
|
+
// Handle ABN (Australian Business Number) columns for export
|
|
6632
|
+
if ((column.displayName && column.displayName.toLowerCase().includes('abn')) ||
|
|
6633
|
+
(column.column && column.column.toLowerCase().includes('abn'))) {
|
|
6634
|
+
const cleanABN = String(value).replace(/\s/g, '');
|
|
6635
|
+
if (cleanABN.length === 11 && /^\d{11}$/.test(cleanABN)) {
|
|
6636
|
+
return `${cleanABN.slice(0,2)} ${cleanABN.slice(2,5)} ${cleanABN.slice(5,8)} ${cleanABN.slice(8,11)}`;
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6640
|
+
// Handle ACN (Australian Company Number) columns for export
|
|
6641
|
+
if ((column.displayName && column.displayName.toLowerCase().includes('acn')) ||
|
|
6642
|
+
(column.column && column.column.toLowerCase().includes('acn'))) {
|
|
6643
|
+
const cleanACN = String(value).replace(/\s/g, '');
|
|
6644
|
+
if (cleanACN.length === 9 && /^\d{9}$/.test(cleanACN)) {
|
|
6645
|
+
return `${cleanACN.slice(0,3)} ${cleanACN.slice(3,6)} ${cleanACN.slice(6,9)}`;
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
|
|
6649
|
+
return String(value);
|
|
6650
|
+
};
|
|
6651
|
+
|
|
6652
|
+
// Add data for each group
|
|
6653
|
+
actualGroups.forEach((group, groupIndex) => {
|
|
6654
|
+
// Add group header row
|
|
6655
|
+
const groupDisplayName = selectedTemplate?.grouping?.groupDisplayName || 'Group';
|
|
6656
|
+
const groupHeaderRow = Array(headers.length).fill('');
|
|
6657
|
+
groupHeaderRow[0] = `${groupDisplayName}: ${group.groupDisplayName}`;
|
|
6658
|
+
worksheetData.push(groupHeaderRow);
|
|
6659
|
+
|
|
6660
|
+
// Add data rows for this group
|
|
6661
|
+
const allRows = [...group.rows, ...(group.emptyRows || [])];
|
|
6662
|
+
allRows.forEach(row => {
|
|
6663
|
+
const dataRow = selectedColumns.map(column => getCellValue(row, column));
|
|
6664
|
+
worksheetData.push(dataRow);
|
|
6665
|
+
});
|
|
6666
|
+
|
|
6667
|
+
// Add separator row between groups (except for last group)
|
|
6668
|
+
if (groupIndex < actualGroups.length - 1) {
|
|
6669
|
+
worksheetData.push(Array(headers.length).fill(''));
|
|
6670
|
+
}
|
|
6671
|
+
});
|
|
6672
|
+
|
|
6673
|
+
// Create worksheet from data
|
|
6674
|
+
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
|
|
6675
|
+
|
|
6676
|
+
// Set column widths based on content
|
|
6677
|
+
const colWidths = selectedColumns.map(column => {
|
|
6678
|
+
switch (column.displayName) {
|
|
6679
|
+
case 'Comments': return { wch: 40 };
|
|
6680
|
+
case 'Project':
|
|
6681
|
+
case 'Project Name': return { wch: 25 };
|
|
6682
|
+
case 'User':
|
|
6683
|
+
case 'Assigned User': return { wch: 20 };
|
|
6684
|
+
case 'Start Date':
|
|
6685
|
+
case 'End Date':
|
|
6686
|
+
case 'Database Updated': return { wch: 15 };
|
|
6687
|
+
default: return { wch: 18 };
|
|
6688
|
+
}
|
|
6689
|
+
});
|
|
6690
|
+
worksheet['!cols'] = colWidths;
|
|
6691
|
+
|
|
6692
|
+
|
|
6693
|
+
// Add worksheet to workbook
|
|
6694
|
+
const sheetName = selectedTemplate.name || 'Grouped Report';
|
|
6695
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
6696
|
+
|
|
6697
|
+
// Generate file and download
|
|
6698
|
+
const filename = `${selectedTemplate.name.replace(/[^a-zA-Z0-9]/g, '_')}_grouped_report_${new Date().toISOString().split('T')[0]}.xlsx`;
|
|
6699
|
+
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
|
6700
|
+
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
6701
|
+
|
|
6702
|
+
saveAs(blob, filename);
|
|
6703
|
+
toast.success('Excel export completed successfully!');
|
|
6704
|
+
|
|
6705
|
+
} catch (error) {
|
|
6706
|
+
console.error('Export error:', error);
|
|
6707
|
+
toast.error(`Export failed: ${error.message}`);
|
|
6708
|
+
}
|
|
6709
|
+
};
|
|
6710
|
+
|
|
6188
6711
|
// Save report configuration (with save/save-as functionality)
|
|
6189
6712
|
const saveReport = async (saveAs = false) => {
|
|
6190
6713
|
if (!reportName.trim()) {
|
|
@@ -6262,6 +6785,10 @@ const GenericReportImproved = ({
|
|
|
6262
6785
|
joins: joins,
|
|
6263
6786
|
filters: filterCriteria,
|
|
6264
6787
|
sorting: sortCriteria,
|
|
6788
|
+
grouping: groupingConfig.enabled ? groupingConfig : null,
|
|
6789
|
+
isGrouped: groupingConfig.enabled,
|
|
6790
|
+
customCalculatedFields: customCalculatedFields,
|
|
6791
|
+
customUrls: customUrls,
|
|
6265
6792
|
},
|
|
6266
6793
|
};
|
|
6267
6794
|
|
|
@@ -6314,6 +6841,252 @@ const GenericReportImproved = ({
|
|
|
6314
6841
|
}
|
|
6315
6842
|
};
|
|
6316
6843
|
|
|
6844
|
+
// Render grouping configuration
|
|
6845
|
+
const renderGrouping = () => {
|
|
6846
|
+
return (
|
|
6847
|
+
<div className={styles.groupingSection}>
|
|
6848
|
+
<div className={styles.sectionContent}>
|
|
6849
|
+
<div className={styles.groupingToggle}>
|
|
6850
|
+
<label className={styles.toggleLabel}>
|
|
6851
|
+
<input
|
|
6852
|
+
type="checkbox"
|
|
6853
|
+
checked={groupingConfig.enabled}
|
|
6854
|
+
onChange={(e) => setGroupingConfig({
|
|
6855
|
+
...groupingConfig,
|
|
6856
|
+
enabled: e.target.checked,
|
|
6857
|
+
// Reset fields when disabled
|
|
6858
|
+
...(e.target.checked ? {} : {
|
|
6859
|
+
groupByField: '',
|
|
6860
|
+
groupDisplayName: ''
|
|
6861
|
+
})
|
|
6862
|
+
})}
|
|
6863
|
+
className={styles.toggleInput}
|
|
6864
|
+
/>
|
|
6865
|
+
<span className={styles.toggleCheckbox}></span>
|
|
6866
|
+
<span className={styles.toggleText}>Enable Grouping</span>
|
|
6867
|
+
</label>
|
|
6868
|
+
<p className={styles.helpText}>
|
|
6869
|
+
Group data by a field to organize results into sections (e.g., "Projects by Facility")
|
|
6870
|
+
</p>
|
|
6871
|
+
</div>
|
|
6872
|
+
|
|
6873
|
+
{groupingConfig.enabled && (
|
|
6874
|
+
<div className={styles.groupingOptions}>
|
|
6875
|
+
{/* Group By Field Selection */}
|
|
6876
|
+
<div className={styles.formGroup}>
|
|
6877
|
+
<label className={styles.formLabel}>Group By Field *</label>
|
|
6878
|
+
<select
|
|
6879
|
+
value={groupingConfig.groupByField}
|
|
6880
|
+
onChange={(e) => {
|
|
6881
|
+
const selectedOption = e.target.selectedOptions[0];
|
|
6882
|
+
const fieldDisplayName = selectedOption?.text || '';
|
|
6883
|
+
// Extract table name for display
|
|
6884
|
+
const tableMatch = fieldDisplayName.match(/^([^.]+)\./);
|
|
6885
|
+
const tableName = tableMatch ? tableMatch[1] : 'Group';
|
|
6886
|
+
|
|
6887
|
+
setGroupingConfig({
|
|
6888
|
+
...groupingConfig,
|
|
6889
|
+
groupByField: e.target.value,
|
|
6890
|
+
groupDisplayName: groupingConfig.groupDisplayName || tableName,
|
|
6891
|
+
sortBy: e.target.value // Auto-set sort field
|
|
6892
|
+
});
|
|
6893
|
+
}}
|
|
6894
|
+
className={styles.formSelect}
|
|
6895
|
+
>
|
|
6896
|
+
<option value="">Select field to group by...</option>
|
|
6897
|
+
{getAvailableFilterColumns().map((col, index) => {
|
|
6898
|
+
const fieldValue = `${col.table}.${col.column}`;
|
|
6899
|
+
const displayName = `${col.tableName}.${col.columnName}`;
|
|
6900
|
+
return (
|
|
6901
|
+
<option key={index} value={fieldValue}>
|
|
6902
|
+
{displayName}
|
|
6903
|
+
</option>
|
|
6904
|
+
);
|
|
6905
|
+
})}
|
|
6906
|
+
</select>
|
|
6907
|
+
<p className={styles.fieldHelp}>
|
|
6908
|
+
Choose the field that contains the categories you want to group by
|
|
6909
|
+
</p>
|
|
6910
|
+
</div>
|
|
6911
|
+
|
|
6912
|
+
{/* Group Display Name */}
|
|
6913
|
+
<div className={styles.formGroup}>
|
|
6914
|
+
<label className={styles.formLabel}>Group Display Name</label>
|
|
6915
|
+
<input
|
|
6916
|
+
type="text"
|
|
6917
|
+
value={groupingConfig.groupDisplayName}
|
|
6918
|
+
onChange={(e) => setGroupingConfig({
|
|
6919
|
+
...groupingConfig,
|
|
6920
|
+
groupDisplayName: e.target.value
|
|
6921
|
+
})}
|
|
6922
|
+
placeholder="e.g., Facility, Department, Category"
|
|
6923
|
+
className={styles.formInput}
|
|
6924
|
+
/>
|
|
6925
|
+
<p className={styles.fieldHelp}>
|
|
6926
|
+
What to call each group in the report (e.g., "Facility: Main Building")
|
|
6927
|
+
</p>
|
|
6928
|
+
</div>
|
|
6929
|
+
|
|
6930
|
+
{/* Advanced Group Options */}
|
|
6931
|
+
<div className={styles.advancedGroupOptions}>
|
|
6932
|
+
<h4 className={styles.sectionTitle}>Advanced Options</h4>
|
|
6933
|
+
|
|
6934
|
+
<div className={styles.optionsGrid}>
|
|
6935
|
+
<div className={styles.formGroup}>
|
|
6936
|
+
<label className={styles.formLabel}>Rows Per Group</label>
|
|
6937
|
+
<input
|
|
6938
|
+
type="number"
|
|
6939
|
+
min="1"
|
|
6940
|
+
max="100"
|
|
6941
|
+
value={groupingConfig.rowsPerGroup}
|
|
6942
|
+
onChange={(e) => setGroupingConfig({
|
|
6943
|
+
...groupingConfig,
|
|
6944
|
+
rowsPerGroup: parseInt(e.target.value) || 10
|
|
6945
|
+
})}
|
|
6946
|
+
className={styles.formInput}
|
|
6947
|
+
/>
|
|
6948
|
+
</div>
|
|
6949
|
+
|
|
6950
|
+
<div className={styles.formGroup}>
|
|
6951
|
+
<label className={styles.formLabel}>Group Header Style</label>
|
|
6952
|
+
<select
|
|
6953
|
+
value={groupingConfig.groupHeaderStyle}
|
|
6954
|
+
onChange={(e) => setGroupingConfig({
|
|
6955
|
+
...groupingConfig,
|
|
6956
|
+
groupHeaderStyle: e.target.value
|
|
6957
|
+
})}
|
|
6958
|
+
className={styles.formSelect}
|
|
6959
|
+
>
|
|
6960
|
+
<option value="primary">Primary (Blue)</option>
|
|
6961
|
+
<option value="secondary">Secondary (Gray)</option>
|
|
6962
|
+
<option value="success">Success (Green)</option>
|
|
6963
|
+
<option value="warning">Warning (Orange)</option>
|
|
6964
|
+
</select>
|
|
6965
|
+
</div>
|
|
6966
|
+
|
|
6967
|
+
<div className={styles.formGroup}>
|
|
6968
|
+
<label className={styles.formLabel}>Sort Direction</label>
|
|
6969
|
+
<select
|
|
6970
|
+
value={groupingConfig.sortDirection}
|
|
6971
|
+
onChange={(e) => setGroupingConfig({
|
|
6972
|
+
...groupingConfig,
|
|
6973
|
+
sortDirection: e.target.value
|
|
6974
|
+
})}
|
|
6975
|
+
className={styles.formSelect}
|
|
6976
|
+
>
|
|
6977
|
+
<option value="asc">Ascending (A-Z)</option>
|
|
6978
|
+
<option value="desc">Descending (Z-A)</option>
|
|
6979
|
+
</select>
|
|
6980
|
+
</div>
|
|
6981
|
+
</div>
|
|
6982
|
+
|
|
6983
|
+
<div className={styles.checkboxGrid}>
|
|
6984
|
+
<label className={styles.checkboxLabel}>
|
|
6985
|
+
<input
|
|
6986
|
+
type="checkbox"
|
|
6987
|
+
checked={groupingConfig.showEmptyRows}
|
|
6988
|
+
onChange={(e) => setGroupingConfig({
|
|
6989
|
+
...groupingConfig,
|
|
6990
|
+
showEmptyRows: e.target.checked
|
|
6991
|
+
})}
|
|
6992
|
+
/>
|
|
6993
|
+
<span className={styles.checkboxText}>Show Empty Rows</span>
|
|
6994
|
+
</label>
|
|
6995
|
+
|
|
6996
|
+
<label className={styles.checkboxLabel}>
|
|
6997
|
+
<input
|
|
6998
|
+
type="checkbox"
|
|
6999
|
+
checked={groupingConfig.showGroupTotals}
|
|
7000
|
+
onChange={(e) => setGroupingConfig({
|
|
7001
|
+
...groupingConfig,
|
|
7002
|
+
showGroupTotals: e.target.checked
|
|
7003
|
+
})}
|
|
7004
|
+
/>
|
|
7005
|
+
<span className={styles.checkboxText}>Show Group Totals</span>
|
|
7006
|
+
</label>
|
|
7007
|
+
|
|
7008
|
+
<label className={styles.checkboxLabel}>
|
|
7009
|
+
<input
|
|
7010
|
+
type="checkbox"
|
|
7011
|
+
checked={groupingConfig.expandable}
|
|
7012
|
+
onChange={(e) => setGroupingConfig({
|
|
7013
|
+
...groupingConfig,
|
|
7014
|
+
expandable: e.target.checked
|
|
7015
|
+
})}
|
|
7016
|
+
/>
|
|
7017
|
+
<span className={styles.checkboxText}>Collapsible Groups</span>
|
|
7018
|
+
</label>
|
|
7019
|
+
</div>
|
|
7020
|
+
</div>
|
|
7021
|
+
|
|
7022
|
+
{/* Grouping Preview */}
|
|
7023
|
+
{groupingConfig.groupByField && (
|
|
7024
|
+
<div className={styles.groupingPreview}>
|
|
7025
|
+
<h4 className={styles.sectionTitle}>Configuration Preview</h4>
|
|
7026
|
+
<div className={styles.previewBox}>
|
|
7027
|
+
<p><strong>Group By:</strong> {groupingConfig.groupByField}</p>
|
|
7028
|
+
<p><strong>Display Name:</strong> {groupingConfig.groupDisplayName || 'Group'}</p>
|
|
7029
|
+
<p><strong>Sort:</strong> {groupingConfig.sortDirection === 'asc' ? 'Ascending' : 'Descending'}</p>
|
|
7030
|
+
<p>
|
|
7031
|
+
<strong>Options:</strong>
|
|
7032
|
+
{groupingConfig.showEmptyRows && ' Empty Rows'}
|
|
7033
|
+
{groupingConfig.showGroupTotals && ' Group Totals'}
|
|
7034
|
+
{groupingConfig.expandable && ' Collapsible'}
|
|
7035
|
+
</p>
|
|
7036
|
+
</div>
|
|
7037
|
+
</div>
|
|
7038
|
+
)}
|
|
7039
|
+
</div>
|
|
7040
|
+
)}
|
|
7041
|
+
|
|
7042
|
+
{/* Custom URLs Configuration */}
|
|
7043
|
+
<div className={styles.customUrlsSection}>
|
|
7044
|
+
<h4 className={styles.sectionTitle}>Custom API Endpoints (Optional)</h4>
|
|
7045
|
+
<p className={styles.helpText}>
|
|
7046
|
+
Override default API endpoints for specialized report handling
|
|
7047
|
+
</p>
|
|
7048
|
+
|
|
7049
|
+
<div className={styles.optionsGrid}>
|
|
7050
|
+
<div className={styles.formGroup}>
|
|
7051
|
+
<label className={styles.formLabel}>Custom Execute URL</label>
|
|
7052
|
+
<input
|
|
7053
|
+
type="text"
|
|
7054
|
+
value={customUrls.executeUrl}
|
|
7055
|
+
onChange={(e) => setCustomUrls({
|
|
7056
|
+
...customUrls,
|
|
7057
|
+
executeUrl: e.target.value
|
|
7058
|
+
})}
|
|
7059
|
+
placeholder="/api/custom-reports/execute"
|
|
7060
|
+
className={styles.formInput}
|
|
7061
|
+
/>
|
|
7062
|
+
<p className={styles.fieldHelp}>
|
|
7063
|
+
Custom endpoint for report execution (overrides template and default URLs)
|
|
7064
|
+
</p>
|
|
7065
|
+
</div>
|
|
7066
|
+
|
|
7067
|
+
<div className={styles.formGroup}>
|
|
7068
|
+
<label className={styles.formLabel}>Custom Export URL</label>
|
|
7069
|
+
<input
|
|
7070
|
+
type="text"
|
|
7071
|
+
value={customUrls.exportUrl}
|
|
7072
|
+
onChange={(e) => setCustomUrls({
|
|
7073
|
+
...customUrls,
|
|
7074
|
+
exportUrl: e.target.value
|
|
7075
|
+
})}
|
|
7076
|
+
placeholder="/api/custom-reports/export"
|
|
7077
|
+
className={styles.formInput}
|
|
7078
|
+
/>
|
|
7079
|
+
<p className={styles.fieldHelp}>
|
|
7080
|
+
Custom endpoint for report exports (for server-side exports)
|
|
7081
|
+
</p>
|
|
7082
|
+
</div>
|
|
7083
|
+
</div>
|
|
7084
|
+
</div>
|
|
7085
|
+
</div>
|
|
7086
|
+
</div>
|
|
7087
|
+
);
|
|
7088
|
+
};
|
|
7089
|
+
|
|
6317
7090
|
// Render preview
|
|
6318
7091
|
const renderPreview = () => {
|
|
6319
7092
|
return (
|
|
@@ -6330,34 +7103,200 @@ const GenericReportImproved = ({
|
|
|
6330
7103
|
>
|
|
6331
7104
|
{isLoading ? 'Loading...' : 'Generate Report'}
|
|
6332
7105
|
</button>
|
|
7106
|
+
|
|
7107
|
+
{/* View Toggle Button - only show if both grouped and regular data are available */}
|
|
7108
|
+
{(groupingConfig.enabled || selectedTemplate?.grouping?.enabled) && (
|
|
7109
|
+
<div className={styles.viewToggle}>
|
|
7110
|
+
<span className={styles.viewLabel}>View:</span>
|
|
7111
|
+
<button
|
|
7112
|
+
className={`${styles.btn} ${styles.btnSecondary} ${
|
|
7113
|
+
groupedData ? styles.active : ''
|
|
7114
|
+
}`}
|
|
7115
|
+
onClick={() => {
|
|
7116
|
+
if (!groupedData && previewData.length > 0) {
|
|
7117
|
+
// Switch to grouped view
|
|
7118
|
+
executeReport();
|
|
7119
|
+
}
|
|
7120
|
+
}}
|
|
7121
|
+
disabled={isLoading}
|
|
7122
|
+
title="Show grouped report"
|
|
7123
|
+
>
|
|
7124
|
+
📊 Grouped
|
|
7125
|
+
</button>
|
|
7126
|
+
<button
|
|
7127
|
+
className={`${styles.btn} ${styles.btnSecondary} ${
|
|
7128
|
+
!groupedData ? styles.active : ''
|
|
7129
|
+
}`}
|
|
7130
|
+
onClick={() => {
|
|
7131
|
+
if (groupedData) {
|
|
7132
|
+
// Switch to regular table view
|
|
7133
|
+
setGroupedData(null);
|
|
7134
|
+
if (previewData.length === 0) {
|
|
7135
|
+
executeReport();
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
}}
|
|
7139
|
+
disabled={isLoading}
|
|
7140
|
+
title="Show regular table"
|
|
7141
|
+
>
|
|
7142
|
+
📋 Table
|
|
7143
|
+
</button>
|
|
7144
|
+
</div>
|
|
7145
|
+
)}
|
|
6333
7146
|
</div>
|
|
6334
7147
|
|
|
6335
|
-
{
|
|
7148
|
+
{/* Render grouped report if available */}
|
|
7149
|
+
{(() => {
|
|
7150
|
+
console.log('🎯 [DEBUG] Render check - groupedData:', groupedData);
|
|
7151
|
+
console.log('🎯 [DEBUG] Render check - groupedData?.grouped:', groupedData?.grouped);
|
|
7152
|
+
console.log('🎯 [DEBUG] Render check - selectedColumns:', selectedColumns);
|
|
7153
|
+
return groupedData && groupedData.grouped;
|
|
7154
|
+
})() && (
|
|
7155
|
+
<div className={styles.groupedReportContainer}>
|
|
7156
|
+
<GroupedReportRenderer
|
|
7157
|
+
data={{
|
|
7158
|
+
...groupedData.groupedData,
|
|
7159
|
+
grouped: true
|
|
7160
|
+
}}
|
|
7161
|
+
config={
|
|
7162
|
+
groupingConfig.enabled
|
|
7163
|
+
? groupingConfig
|
|
7164
|
+
: selectedTemplate?.grouping || {}
|
|
7165
|
+
}
|
|
7166
|
+
columns={selectedColumns}
|
|
7167
|
+
onExport={(data) => {
|
|
7168
|
+
handleGroupedExport();
|
|
7169
|
+
}}
|
|
7170
|
+
/>
|
|
7171
|
+
</div>
|
|
7172
|
+
)}
|
|
7173
|
+
|
|
7174
|
+
{/* Render regular grid if no grouping */}
|
|
7175
|
+
{!groupedData && previewData.length > 0 && (
|
|
6336
7176
|
<>
|
|
6337
7177
|
<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
|
-
|
|
7178
|
+
<div className="grouped-report">
|
|
7179
|
+
<div className="grouped-report-section">
|
|
7180
|
+
<div className="grouped-report-header">
|
|
7181
|
+
<div className="grouped-report-summary">
|
|
7182
|
+
<h3 className="group-title">Report Results</h3>
|
|
7183
|
+
<span className="group-stats">({previewData.length} records)</span>
|
|
7184
|
+
</div>
|
|
7185
|
+
<div className="grouped-report-actions">
|
|
7186
|
+
<button
|
|
7187
|
+
className="btn btn-export"
|
|
7188
|
+
onClick={() => handleRegularExport()}
|
|
7189
|
+
disabled={isLoading || previewData.length === 0}
|
|
7190
|
+
title="Export report data as Excel file"
|
|
7191
|
+
>
|
|
7192
|
+
Export
|
|
7193
|
+
</button>
|
|
7194
|
+
</div>
|
|
7195
|
+
</div>
|
|
7196
|
+
<div className="grouped-table-container">
|
|
7197
|
+
<table className="grouped-table">
|
|
7198
|
+
<thead>
|
|
7199
|
+
<tr>
|
|
7200
|
+
{(() => {
|
|
7201
|
+
const headerColumns = previewData.length > 0 ? Object.keys(previewData[0]).map(key => ({name: key, header: key.replace(/\./g, ' ')})) : gridColumns;
|
|
7202
|
+
return headerColumns.map((col, index) => (
|
|
7203
|
+
<th key={index} className="table-header">
|
|
7204
|
+
{col.header || col.name}
|
|
7205
|
+
</th>
|
|
7206
|
+
));
|
|
7207
|
+
})()}
|
|
7208
|
+
</tr>
|
|
7209
|
+
</thead>
|
|
7210
|
+
<tbody>
|
|
7211
|
+
{(() => {
|
|
7212
|
+
console.log(`🎯 [DEBUG] Rendering ${previewData.length} table rows`);
|
|
7213
|
+
// Always use response keys as fallback instead of gridColumns that might have wrong names
|
|
7214
|
+
const actualColumns = previewData.length > 0 ? Object.keys(previewData[0]).map(key => ({name: key, header: key.replace(/\./g, ' ')})) : gridColumns;
|
|
7215
|
+
console.log(`🎯 [DEBUG] Using columns:`, actualColumns);
|
|
7216
|
+
return previewData.map((row, rowIndex) => (
|
|
7217
|
+
<tr key={rowIndex} className="data-row">
|
|
7218
|
+
{actualColumns.map((col, colIndex) => (
|
|
7219
|
+
<td key={colIndex} className="table-cell">
|
|
7220
|
+
{(() => {
|
|
7221
|
+
const value = row[col.name];
|
|
7222
|
+
|
|
7223
|
+
// Handle null/undefined
|
|
7224
|
+
if (value === null || value === undefined) {
|
|
7225
|
+
return <span style={{color: '#6c757d', fontStyle: 'italic'}}>—</span>;
|
|
7226
|
+
}
|
|
7227
|
+
|
|
7228
|
+
// Handle JSON strings
|
|
7229
|
+
if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
|
|
7230
|
+
try {
|
|
7231
|
+
const parsed = JSON.parse(value);
|
|
7232
|
+
return (
|
|
7233
|
+
<div style={{fontSize: '0.85em', lineHeight: '1.3'}}>
|
|
7234
|
+
{Object.entries(parsed).map(([key, val]) => (
|
|
7235
|
+
<div key={key} style={{marginBottom: '2px'}}>
|
|
7236
|
+
<strong>{key.replace(/_/g, ' ')}:</strong> {val || '—'}
|
|
7237
|
+
</div>
|
|
7238
|
+
))}
|
|
7239
|
+
</div>
|
|
7240
|
+
);
|
|
7241
|
+
} catch (e) {
|
|
7242
|
+
return value;
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
// Handle ABN (Australian Business Number) columns
|
|
7247
|
+
if (col.name.toLowerCase().includes('abn') && value) {
|
|
7248
|
+
const cleanABN = String(value).replace(/\s/g, ''); // Remove spaces, convert to string
|
|
7249
|
+
if (cleanABN.length === 11 && /^\d{11}$/.test(cleanABN)) {
|
|
7250
|
+
// Format as XX XXX XXX XXX
|
|
7251
|
+
const formatted = `${cleanABN.slice(0,2)} ${cleanABN.slice(2,5)} ${cleanABN.slice(5,8)} ${cleanABN.slice(8,11)}`;
|
|
7252
|
+
return <span style={{fontFamily: 'monospace', fontWeight: '500', color: '#495057'}}>{formatted}</span>;
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
|
|
7256
|
+
// Handle ACN (Australian Company Number) columns
|
|
7257
|
+
if (col.name.toLowerCase().includes('acn') && value) {
|
|
7258
|
+
const cleanACN = String(value).replace(/\s/g, ''); // Remove spaces, convert to string
|
|
7259
|
+
if (cleanACN.length === 9 && /^\d{9}$/.test(cleanACN)) {
|
|
7260
|
+
// Format as XXX XXX XXX
|
|
7261
|
+
const formatted = `${cleanACN.slice(0,3)} ${cleanACN.slice(3,6)} ${cleanACN.slice(6,9)}`;
|
|
7262
|
+
return <span style={{fontFamily: 'monospace', fontWeight: '500', color: '#495057'}}>{formatted}</span>;
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
// Handle status/boolean-like columns
|
|
7267
|
+
if (col.name.toLowerCase().includes('status')) {
|
|
7268
|
+
if (value === 1 || value === '1' || value === true) {
|
|
7269
|
+
return <span style={{color: '#28a745', fontWeight: '500'}}>Active</span>;
|
|
7270
|
+
} else if (value === 0 || value === '0' || value === false) {
|
|
7271
|
+
return <span style={{color: '#6c757d', fontWeight: '500'}}>Inactive</span>;
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
|
|
7275
|
+
// Handle other boolean values
|
|
7276
|
+
if (typeof value === 'boolean') {
|
|
7277
|
+
return value ?
|
|
7278
|
+
<span style={{color: '#28a745', fontWeight: '500'}}>Yes</span> :
|
|
7279
|
+
<span style={{color: '#6c757d', fontWeight: '500'}}>No</span>;
|
|
7280
|
+
}
|
|
7281
|
+
|
|
7282
|
+
// Handle numeric values
|
|
7283
|
+
if (typeof value === 'number' && !col.name.toLowerCase().includes('status')) {
|
|
7284
|
+
return value.toLocaleString();
|
|
7285
|
+
}
|
|
7286
|
+
|
|
7287
|
+
// Default string handling
|
|
7288
|
+
return value;
|
|
7289
|
+
})()}
|
|
7290
|
+
</td>
|
|
7291
|
+
))}
|
|
7292
|
+
</tr>
|
|
7293
|
+
));
|
|
7294
|
+
})()}
|
|
7295
|
+
</tbody>
|
|
7296
|
+
</table>
|
|
7297
|
+
</div>
|
|
7298
|
+
</div>
|
|
7299
|
+
</div>
|
|
6361
7300
|
</div>
|
|
6362
7301
|
|
|
6363
7302
|
<div className={styles.reportActions}>
|
|
@@ -6390,66 +7329,6 @@ const GenericReportImproved = ({
|
|
|
6390
7329
|
</div>
|
|
6391
7330
|
</div>
|
|
6392
7331
|
|
|
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
7332
|
</div>
|
|
6454
7333
|
</>
|
|
6455
7334
|
)}
|