@visns-studio/visns-components 5.14.9 → 5.14.11

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