@visns-studio/visns-components 5.14.9 → 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.
@@ -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 === 4 && // Step 5 is index 4 (preview step)
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
- executeUrl,
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
- // Fetch first page to get data structure for grid columns
2311
- const firstPageResult = await dataSource({
2312
- skip: 0,
2313
- limit: pageSize,
2314
- sortInfo: null,
2315
- filterValue: null,
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
- if (firstPageResult.data && firstPageResult.data.length > 0) {
2319
- setPreviewData(firstPageResult.data);
2551
+ if (firstPageResult.data && firstPageResult.data.length > 0) {
2552
+ setPreviewData(firstPageResult.data);
2553
+ fetchedDataCount = firstPageResult.data.length;
2554
+ }
2555
+ }
2320
2556
 
2321
- // Create grid columns dynamically from actual data with smart formatting
2322
- const firstRow = firstPageResult.data[0];
2323
- const dynamicColumns = Object.keys(firstRow).map((key) => {
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 = firstPageResult.data.some((row) => {
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
- setGridColumns(dynamicColumns);
2433
- } else {
2434
- // Fallback to selected columns if no data
2435
- const columns = selectedColumns.map((col) => {
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 || formatName(col.column),
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
- setGridColumns(columns);
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 total records found
2511
- toast.success(`Found ${firstPageResult.count} records`);
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 4) since the report is already configured
2605
- setCurrentWizardStep(4);
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:', exportUrl);
6083
- const result = await Download(exportUrl, 'POST', exportData);
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
- {previewData.length > 0 && (
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
- <ReactDataGrid
6339
- columns={gridColumns}
6340
- dataSource={dataSource}
6341
- style={{ height: 800 }}
6342
- pagination
6343
- limit={pageSize}
6344
- onLimitChange={(newLimit) => {
6345
- setPageSize(newLimit);
6346
- }}
6347
- pageSizes={[10, 15, 25, 50]}
6348
- defaultLimit={25}
6349
- // Row height configuration for JSON content
6350
- rowHeight={null} // Allow dynamic row height
6351
- estimatedRowHeight={60} // Estimated height for performance
6352
- minRowHeight={40} // Minimum row height
6353
- // No maxRowHeight to allow full expansion for JSON content
6354
- // Enable text wrapping and proper sizing
6355
- showCellBorders="horizontal"
6356
- showZebraRows={false}
6357
- // Ensure proper rendering of content
6358
- virtualizeColumns={false}
6359
- enableRowspan={false}
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
  )}