@visns-studio/visns-components 6.1.4 → 6.1.5

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.
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { toast } from 'react-toastify';
4
4
  import moment from 'moment';
@@ -34,6 +34,7 @@ import {
34
34
  } from 'lucide-react';
35
35
  import CustomFetch from '../Fetch';
36
36
  import Download from '../Download';
37
+ import debugLog from '../utils/debugLog';
37
38
  import { saveAs } from 'file-saver';
38
39
  import * as XLSX from 'xlsx';
39
40
  import Swal from 'sweetalert2';
@@ -287,6 +288,31 @@ const formatName = (name) => {
287
288
  .join(' ');
288
289
  };
289
290
 
291
+ // A saved report's `detail` arrives as either a JSON string or an object,
292
+ // and older rows can hold malformed JSON — never let that reach the renderer.
293
+ const parseReportDetail = (detail) => {
294
+ if (!detail) return null;
295
+ if (typeof detail !== 'string') {
296
+ return typeof detail === 'object' ? detail : null;
297
+ }
298
+ try {
299
+ const parsed = JSON.parse(detail);
300
+ return parsed && typeof parsed === 'object' ? parsed : null;
301
+ } catch (error) {
302
+ debugLog('Could not parse saved report detail:', error);
303
+ return null;
304
+ }
305
+ };
306
+
307
+ // Escape values interpolated into SweetAlert HTML strings
308
+ const escapeHtml = (value) =>
309
+ String(value ?? '')
310
+ .replace(/&/g, '&')
311
+ .replace(/</g, '&lt;')
312
+ .replace(/>/g, '&gt;')
313
+ .replace(/"/g, '&quot;')
314
+ .replace(/'/g, '&#39;');
315
+
290
316
  // Enhanced function to format pivot field names with context from end table
291
317
  const formatPivotFieldName = (fieldName, pivotTableName, joins, definition = null) => {
292
318
  if (!fieldName || !pivotTableName) return formatName(fieldName);
@@ -747,8 +773,9 @@ const formatSmartValue = (value, columnName, columnType) => {
747
773
 
748
774
  // Smart field filtering - hide technical fields by default
749
775
  const shouldHideField = (fieldName, fieldType) => {
750
- const name = fieldName.toLowerCase();
751
- const type = fieldType.toLowerCase();
776
+ if (!fieldName) return false;
777
+ const name = String(fieldName).toLowerCase();
778
+ const type = String(fieldType || '').toLowerCase();
752
779
 
753
780
  // Hide foreign keys and primary key IDs (confusing for non-dev users)
754
781
  if (name.endsWith('_id') || name === 'id') return true;
@@ -828,9 +855,10 @@ const categorizeFields = (columns, showHiddenFields = false) => {
828
855
  other: [],
829
856
  };
830
857
 
831
- columns.forEach((column) => {
832
- const name = column.name.toLowerCase();
833
- const type = column.type.toLowerCase();
858
+ (columns || []).forEach((column) => {
859
+ if (!column || !column.name) return;
860
+ const name = String(column.name).toLowerCase();
861
+ const type = String(column.type || '').toLowerCase();
834
862
 
835
863
  // Check if field should be hidden
836
864
  if (shouldHideField(column.name, column.type)) {
@@ -1022,8 +1050,14 @@ const GenericReportImproved = ({
1022
1050
  const [selectedTable, setSelectedTable] = useState(null);
1023
1051
  const [tableColumns, setTableColumns] = useState([]);
1024
1052
  const [isLoadingTables, setIsLoadingTables] = useState(false);
1053
+ const [isLoadingColumns, setIsLoadingColumns] = useState(false);
1025
1054
  const [isLoadingJoinColumns, setIsLoadingJoinColumns] = useState(false);
1026
1055
 
1056
+ // Human-readable failure messages per fetch step (null = no failure)
1057
+ const [tablesError, setTablesError] = useState(null);
1058
+ const [columnsError, setColumnsError] = useState(null);
1059
+ const [reportsError, setReportsError] = useState(null);
1060
+
1027
1061
  // State for report configuration
1028
1062
  const [selectedColumns, setSelectedColumns] = useState([]);
1029
1063
  const [availableTables, setAvailableTables] = useState([]);
@@ -1126,11 +1160,7 @@ const GenericReportImproved = ({
1126
1160
  enabled: false,
1127
1161
  selectedField: '',
1128
1162
  });
1129
-
1130
- // Debug logging for uniqueFieldConfig changes
1131
- useEffect(() => {
1132
- console.log('🔍 [EFFECT] uniqueFieldConfig changed:', uniqueFieldConfig);
1133
- }, [uniqueFieldConfig]);
1163
+
1134
1164
  const [excludedRows, setExcludedRows] = useState(new Set());
1135
1165
  const [showRowExclusionModal, setShowRowExclusionModal] = useState(false);
1136
1166
  const [exportType, setExportType] = useState('regular');
@@ -1509,11 +1539,46 @@ const GenericReportImproved = ({
1509
1539
  hasAutoExecuted,
1510
1540
  ]);
1511
1541
 
1542
+ // Escape closes whichever modal is open
1543
+ useEffect(() => {
1544
+ if (!showCalculatedFieldModal && !showRowExclusionModal) return undefined;
1545
+
1546
+ const handleKeyDown = (event) => {
1547
+ if (event.key !== 'Escape') return;
1548
+ if (showCalculatedFieldModal) {
1549
+ handleCalculatedFieldCancel();
1550
+ } else {
1551
+ setShowRowExclusionModal(false);
1552
+ }
1553
+ };
1554
+
1555
+ document.addEventListener('keydown', handleKeyDown);
1556
+ return () => document.removeEventListener('keydown', handleKeyDown);
1557
+ }, [showCalculatedFieldModal, showRowExclusionModal]);
1558
+
1559
+ // Move focus into a modal when it opens so keyboard users start inside it
1560
+ const calculatedFieldModalRef = React.useRef(null);
1561
+ const rowExclusionModalRef = React.useRef(null);
1562
+
1563
+ useEffect(() => {
1564
+ if (showCalculatedFieldModal) {
1565
+ calculatedFieldModalRef.current?.focus();
1566
+ }
1567
+ }, [showCalculatedFieldModal]);
1568
+
1569
+ useEffect(() => {
1570
+ if (showRowExclusionModal) {
1571
+ rowExclusionModalRef.current?.focus();
1572
+ }
1573
+ }, [showRowExclusionModal]);
1574
+
1512
1575
  // Delete saved report
1513
1576
  const deleteReport = async (reportId, reportLabel) => {
1514
1577
  const result = await Swal.fire({
1515
1578
  title: 'Delete Report',
1516
- html: `Are you sure you want to delete "<strong>${reportLabel}</strong>"?<br><br>This action cannot be undone.`,
1579
+ html: `Are you sure you want to delete "<strong>${escapeHtml(
1580
+ reportLabel
1581
+ )}</strong>"?<br><br>This action cannot be undone.`,
1517
1582
  icon: 'warning',
1518
1583
  showCancelButton: true,
1519
1584
  confirmButtonText: 'Yes, Delete',
@@ -1545,18 +1610,13 @@ const GenericReportImproved = ({
1545
1610
  }
1546
1611
  } catch (error) {
1547
1612
  toast.error('Failed to delete report');
1548
- console.error('Delete error:', error);
1613
+ debugLog('Delete error:', error);
1549
1614
  }
1550
1615
  }
1551
1616
  };
1552
1617
 
1553
1618
  // Start again function to reset wizard to home
1554
1619
  const startAgain = () => {
1555
- console.log('🔄 [DEBUG] StartAgain called - BEFORE reset:', {
1556
- selectedTemplate: selectedTemplate?.id,
1557
- customUrls: customUrls,
1558
- groupingConfig: groupingConfig,
1559
- });
1560
1620
 
1561
1621
  // Reset wizard state
1562
1622
  setCurrentWizardStep(0);
@@ -1664,12 +1724,6 @@ const GenericReportImproved = ({
1664
1724
  // Reset UI state
1665
1725
  setShowHiddenFields(false);
1666
1726
 
1667
- console.log('✅ [DEBUG] StartAgain completed - AFTER reset:', {
1668
- selectedTemplate: null,
1669
- customUrls: { executeUrl: '', exportUrl: '' },
1670
- groupingConfig: { enabled: false },
1671
- });
1672
-
1673
1727
  toast.info(
1674
1728
  'Report builder reset. You can start creating a new report.'
1675
1729
  );
@@ -1711,12 +1765,16 @@ const GenericReportImproved = ({
1711
1765
  // Fetch database tables from the API
1712
1766
  const fetchDatabaseTables = async () => {
1713
1767
  if (!tableUrl) {
1714
- console.warn('No tableUrl provided to fetch database tables');
1768
+ debugLog('No tableUrl provided to fetch database tables');
1715
1769
  setIsLoadingTables(false);
1770
+ setTablesError(
1771
+ 'No data source endpoint is configured for this report builder.'
1772
+ );
1716
1773
  return;
1717
1774
  }
1718
1775
 
1719
1776
  setIsLoadingTables(true);
1777
+ setTablesError(null);
1720
1778
 
1721
1779
  // List of tables to hide from the report builder
1722
1780
  const hiddenTables = [
@@ -1738,6 +1796,7 @@ const GenericReportImproved = ({
1738
1796
  const result = await CustomFetch(tableUrl, 'POST', {});
1739
1797
 
1740
1798
  if (result.error) {
1799
+ setTablesError(result.error);
1741
1800
  toast.error(result.error);
1742
1801
  } else {
1743
1802
  // Handle response format like the working advanced mode
@@ -1746,7 +1805,10 @@ const GenericReportImproved = ({
1746
1805
 
1747
1806
  // Ensure tableData is an array
1748
1807
  if (!Array.isArray(tableData)) {
1749
- console.error('Unexpected table data format:', tableData);
1808
+ debugLog('Unexpected table data format:', tableData);
1809
+ setTablesError(
1810
+ 'The data source list came back in an unexpected format.'
1811
+ );
1750
1812
  toast.error('Unexpected response format from table API');
1751
1813
  return;
1752
1814
  }
@@ -1755,8 +1817,9 @@ const GenericReportImproved = ({
1755
1817
  let filteredTables = tableData
1756
1818
  .filter((table) => {
1757
1819
  const tableName =
1758
- typeof table === 'string' ? table : table.name;
1820
+ typeof table === 'string' ? table : table?.name;
1759
1821
  return (
1822
+ typeof tableName === 'string' &&
1760
1823
  !hiddenTables.includes(tableName) &&
1761
1824
  !tableName.includes('_')
1762
1825
  );
@@ -1786,16 +1849,18 @@ const GenericReportImproved = ({
1786
1849
  // Sort alphabetically
1787
1850
  filteredTables.sort((a, b) => a.name.localeCompare(b.name));
1788
1851
 
1789
- console.log('Loaded tables:', filteredTables);
1790
1852
  setTables(filteredTables);
1791
1853
 
1792
1854
  if (filteredTables.length === 0) {
1793
- console.warn('No tables available after filtering');
1855
+ debugLog('No tables available after filtering');
1794
1856
  }
1795
1857
  }
1796
1858
  } catch (error) {
1859
+ setTablesError(
1860
+ 'We could not load the available data sources. Please try again.'
1861
+ );
1797
1862
  toast.error('Failed to load database tables');
1798
- console.error('Error fetching tables:', error);
1863
+ debugLog('Error fetching tables:', error);
1799
1864
  } finally {
1800
1865
  setIsLoadingTables(false);
1801
1866
  }
@@ -1805,16 +1870,16 @@ const GenericReportImproved = ({
1805
1870
  const fetchTableColumns = async (tableName) => {
1806
1871
  if (!tableName || !columnUrl) return;
1807
1872
 
1808
- setIsLoadingTables(true);
1873
+ setIsLoadingColumns(true);
1874
+ setColumnsError(null);
1809
1875
 
1810
1876
  try {
1811
1877
  const result = await CustomFetch(columnUrl, 'POST', {
1812
1878
  table: tableName,
1813
1879
  });
1814
1880
 
1815
- console.log('Column API Response:', result); // Debug log
1816
-
1817
1881
  if (result.error) {
1882
+ setColumnsError(result.error);
1818
1883
  toast.error(result.error);
1819
1884
  } else {
1820
1885
  // Handle the specific response format from getTableColumns (like GenericReport)
@@ -1839,23 +1904,28 @@ const GenericReportImproved = ({
1839
1904
 
1840
1905
  if (Array.isArray(columnsData) && columnsData.length > 0) {
1841
1906
  setTableColumns(columnsData);
1842
- console.log('Loaded columns:', columnsData);
1843
1907
 
1844
1908
  // Note: Auto-selection of columns removed since user should choose relationships first,
1845
1909
  // then select columns in step 3
1846
1910
  } else {
1847
- console.error('Invalid columns data format:', result);
1911
+ debugLog('Invalid columns data format:', result);
1848
1912
  setTableColumns([]);
1913
+ setColumnsError(
1914
+ 'No fields were returned for this data source.'
1915
+ );
1849
1916
  toast.error(
1850
1917
  'Failed to load columns. Please check the table configuration.'
1851
1918
  );
1852
1919
  }
1853
1920
  }
1854
1921
  } catch (error) {
1922
+ setColumnsError(
1923
+ 'We could not load the fields for this data source. Please try again.'
1924
+ );
1855
1925
  toast.error('Failed to load table columns');
1856
- console.error('Error fetching columns:', error);
1926
+ debugLog('Error fetching columns:', error);
1857
1927
  } finally {
1858
- setIsLoadingTables(false);
1928
+ setIsLoadingColumns(false);
1859
1929
  }
1860
1930
  };
1861
1931
 
@@ -1864,22 +1934,29 @@ const GenericReportImproved = ({
1864
1934
  if (!reportsUrl) return;
1865
1935
 
1866
1936
  setIsLoadingReports(true);
1937
+ setReportsError(null);
1867
1938
 
1868
1939
  try {
1869
1940
  const result = await CustomFetch(reportsUrl, 'GET');
1870
1941
 
1871
1942
  if (result.error) {
1943
+ setReportsError(result.error);
1872
1944
  toast.error(result.error);
1873
- } else if (result.data?.success && result.data?.data) {
1874
- setSavedReports(result.data.data);
1875
- } else if (result.reports) {
1876
- setSavedReports(result.reports);
1877
- } else if (result.data) {
1878
- setSavedReports(result.data);
1945
+ return;
1879
1946
  }
1947
+
1948
+ const reportList =
1949
+ (result.data?.success && result.data?.data) ||
1950
+ result.reports ||
1951
+ result.data;
1952
+
1953
+ setSavedReports(Array.isArray(reportList) ? reportList : []);
1880
1954
  } catch (error) {
1955
+ setReportsError(
1956
+ 'We could not load your saved reports. Please try again.'
1957
+ );
1881
1958
  toast.error('Failed to load saved reports');
1882
- console.error('Error fetching reports:', error);
1959
+ debugLog('Error fetching reports:', error);
1883
1960
  } finally {
1884
1961
  setIsLoadingReports(false);
1885
1962
  }
@@ -1888,23 +1965,20 @@ const GenericReportImproved = ({
1888
1965
  // Fetch detected joins for a table
1889
1966
  const fetchDetectedJoins = async (tableName) => {
1890
1967
  if (!tableName || !detectedJoinsUrl) {
1891
- console.warn(
1968
+ debugLog(
1892
1969
  'Missing tableName or detectedJoinsUrl for fetchDetectedJoins'
1893
1970
  );
1894
1971
  return;
1895
1972
  }
1896
1973
 
1897
- setIsLoadingDetectedJoins({
1898
- ...isLoadingDetectedJoins,
1899
- [tableName]: true,
1900
- });
1974
+ setIsLoadingDetectedJoins((prev) => ({ ...prev, [tableName]: true }));
1901
1975
 
1902
1976
  try {
1903
1977
  const result = await CustomFetch(detectedJoinsUrl, 'POST', {
1904
1978
  table: tableName,
1905
1979
  });
1906
1980
 
1907
- console.log('Detected joins API response:', result); // Debug log
1981
+ // Debug log
1908
1982
 
1909
1983
  if (result.error) {
1910
1984
  toast.error(result.error);
@@ -1944,37 +2018,26 @@ const GenericReportImproved = ({
1944
2018
  );
1945
2019
  });
1946
2020
 
1947
- console.log(
1948
- `Found ${filteredJoins.length} detected joins for ${tableName}:`,
1949
- filteredJoins
1950
- );
1951
-
1952
- setDetectedJoins({
1953
- ...detectedJoins,
2021
+ setDetectedJoins((prev) => ({
2022
+ ...prev,
1954
2023
  [tableName]: filteredJoins,
1955
- });
2024
+ }));
1956
2025
  } else {
1957
- console.warn(
2026
+ debugLog(
1958
2027
  'No valid detected joins found or invalid response format:',
1959
2028
  result
1960
2029
  );
1961
- setDetectedJoins({
1962
- ...detectedJoins,
1963
- [tableName]: [],
1964
- });
2030
+ setDetectedJoins((prev) => ({ ...prev, [tableName]: [] }));
1965
2031
  }
1966
2032
  }
1967
2033
  } catch (error) {
1968
- console.error('Error fetching detected joins:', error);
1969
- setDetectedJoins({
1970
- ...detectedJoins,
1971
- [tableName]: [],
1972
- });
2034
+ debugLog('Error fetching detected joins:', error);
2035
+ setDetectedJoins((prev) => ({ ...prev, [tableName]: [] }));
1973
2036
  } finally {
1974
- setIsLoadingDetectedJoins({
1975
- ...isLoadingDetectedJoins,
2037
+ setIsLoadingDetectedJoins((prev) => ({
2038
+ ...prev,
1976
2039
  [tableName]: false,
1977
- });
2040
+ }));
1978
2041
  }
1979
2042
  };
1980
2043
 
@@ -2014,7 +2077,7 @@ const GenericReportImproved = ({
2014
2077
  // Fetch columns for a joined table (similar to GenericReport)
2015
2078
  const fetchJoinColumns = async (tableName, joinIndex) => {
2016
2079
  if (!tableName || !columnUrl) {
2017
- console.warn('Missing tableName or columnUrl for fetchJoinColumns');
2080
+ debugLog('Missing tableName or columnUrl for fetchJoinColumns');
2018
2081
  return;
2019
2082
  }
2020
2083
 
@@ -2055,11 +2118,8 @@ const GenericReportImproved = ({
2055
2118
  return updatedJoins;
2056
2119
  });
2057
2120
 
2058
- console.log(
2059
- `Loaded ${columnsData.length} columns for joined table ${tableName}`
2060
- );
2061
2121
  } else {
2062
- console.error(
2122
+ debugLog(
2063
2123
  'Invalid columns data format for joined table:',
2064
2124
  result
2065
2125
  );
@@ -2070,7 +2130,7 @@ const GenericReportImproved = ({
2070
2130
  }
2071
2131
  } catch (error) {
2072
2132
  toast.error(`Failed to load columns for joined table ${tableName}`);
2073
- console.error('Error fetching join columns:', error);
2133
+ debugLog('Error fetching join columns:', error);
2074
2134
  } finally {
2075
2135
  setIsLoadingJoinColumns(false);
2076
2136
  }
@@ -2106,13 +2166,8 @@ const GenericReportImproved = ({
2106
2166
  table: tableName,
2107
2167
  });
2108
2168
 
2109
- console.log(
2110
- `Manual join columns API response for ${tableName}:`,
2111
- result
2112
- );
2113
-
2114
2169
  if (result.error) {
2115
- console.error(`Error fetching ${type} columns:`, result.error);
2170
+ debugLog(`Error fetching ${type} columns:`, result.error);
2116
2171
  } else {
2117
2172
  // Handle the response format similar to fetchTableColumns
2118
2173
  let columnsData = [];
@@ -2143,24 +2198,20 @@ const GenericReportImproved = ({
2143
2198
 
2144
2199
  // Ensure columnsData is always an array
2145
2200
  if (!Array.isArray(columnsData)) {
2146
- console.error(
2201
+ debugLog(
2147
2202
  `Invalid columns data format for ${type} table:`,
2148
2203
  result
2149
2204
  );
2150
2205
  columnsData = [];
2151
2206
  }
2152
2207
 
2153
- console.log(
2154
- `Loaded ${columnsData.length} columns for ${type} table ${tableName}`
2155
- );
2156
-
2157
2208
  setManualJoinColumns((prev) => ({
2158
2209
  ...prev,
2159
2210
  [type]: columnsData,
2160
2211
  }));
2161
2212
  }
2162
2213
  } catch (error) {
2163
- console.error(`Error fetching ${type} columns:`, error);
2214
+ debugLog(`Error fetching ${type} columns:`, error);
2164
2215
  // Ensure state remains consistent even on error
2165
2216
  setManualJoinColumns((prev) => ({
2166
2217
  ...prev,
@@ -2220,8 +2271,8 @@ const GenericReportImproved = ({
2220
2271
  setSelectedColumns([]);
2221
2272
  setJoins([]);
2222
2273
  setAvailableTables([tableName]);
2223
- fetchTableColumns(tableName);
2224
- fetchDetectedJoins(tableName);
2274
+ // Columns and suggested joins are fetched by the selectedTable effect —
2275
+ // calling them here as well doubled every request.
2225
2276
 
2226
2277
  goToNextStep(); // Go to relationships step
2227
2278
  };
@@ -2390,25 +2441,24 @@ const GenericReportImproved = ({
2390
2441
  };
2391
2442
 
2392
2443
  setFilterCriteria((prev) => {
2393
- const updatedCriteria = { ...prev };
2444
+ const groups = prev?.groups?.length
2445
+ ? prev.groups
2446
+ : [{ operator: 'AND', filters: [] }];
2394
2447
 
2395
- // Ensure we have at least one group
2396
- if (
2397
- !updatedCriteria.groups ||
2398
- updatedCriteria.groups.length === 0
2399
- ) {
2400
- updatedCriteria.groups = [
2401
- {
2402
- operator: 'AND',
2403
- filters: [],
2404
- },
2405
- ];
2406
- }
2407
-
2408
- // Add to first group
2409
- updatedCriteria.groups[0].filters.push(newFilterCriterion);
2410
-
2411
- return updatedCriteria;
2448
+ return {
2449
+ operator: prev?.operator || 'AND',
2450
+ groups: groups.map((group, index) =>
2451
+ index === 0
2452
+ ? {
2453
+ ...group,
2454
+ filters: [
2455
+ ...(group.filters || []),
2456
+ newFilterCriterion,
2457
+ ],
2458
+ }
2459
+ : group
2460
+ ),
2461
+ };
2412
2462
  });
2413
2463
 
2414
2464
  toast.success(`Applied filter: ${suggestion.title}`);
@@ -2437,22 +2487,6 @@ const GenericReportImproved = ({
2437
2487
  selectedTemplate?.executeUrl ||
2438
2488
  executeUrl;
2439
2489
 
2440
- // Debug logging
2441
- console.log('🔍 [DEBUG] Grouped Report Detected:', {
2442
- selectedTemplate: selectedTemplate?.id,
2443
- isDynamicTemplate,
2444
- template,
2445
- customExecuteUrl: customUrls.executeUrl,
2446
- templateExecuteUrl: selectedTemplate?.executeUrl,
2447
- defaultExecuteUrl: executeUrl,
2448
- finalExecuteUrl: templateExecuteUrl,
2449
- isGroupedReport,
2450
- templateGroupingConfig: selectedTemplate?.grouping,
2451
- customGroupingConfig: groupingConfig,
2452
- usingCustomUrl: !!customUrls.executeUrl,
2453
- usingTemplateExecuteUrl: !!selectedTemplate?.executeUrl,
2454
- });
2455
-
2456
2490
  let payload;
2457
2491
 
2458
2492
  if (isDynamicTemplate) {
@@ -2479,20 +2513,8 @@ const GenericReportImproved = ({
2479
2513
  unique: groupedUniqueConfig,
2480
2514
  };
2481
2515
 
2482
- // Debug log for grouped reports
2483
- if (uniqueFieldConfig.enabled) {
2484
- console.group('🔍 [DEBUG] Grouped Report - Unique Field Configuration');
2485
- console.log('Grouped unique config:', groupedUniqueConfig);
2486
- console.groupEnd();
2487
- }
2488
2516
  } else {
2489
2517
  // For predefined templates, use the existing approach
2490
- console.log('🚨 [DEBUG] Using predefined template approach:', {
2491
- selectedTemplate: selectedTemplate,
2492
- selectedTemplateId: selectedTemplate?.id,
2493
- fallbackId: 'utilization_planning',
2494
- finalTemplateId: selectedTemplate?.id || 'utilization_planning',
2495
- });
2496
2518
 
2497
2519
  payload = {
2498
2520
  template_id:
@@ -2501,23 +2523,15 @@ const GenericReportImproved = ({
2501
2523
  };
2502
2524
  }
2503
2525
 
2504
- console.log('🚀 [DEBUG] Sending grouped report request:', {
2505
- url: templateExecuteUrl,
2506
- method: 'POST',
2507
- payload,
2508
- });
2509
-
2510
2526
  const result = await CustomFetch(
2511
2527
  templateExecuteUrl,
2512
2528
  'POST',
2513
2529
  payload
2514
2530
  );
2515
2531
 
2516
- console.log('📊 [DEBUG] Grouped report response:', result);
2517
-
2518
2532
  if (result.error) {
2519
- console.error(
2520
- 'Grouped report execution error:',
2533
+ debugLog(
2534
+ 'Grouped report execution error:',
2521
2535
  result.error
2522
2536
  );
2523
2537
  return { data: [], count: 0 };
@@ -2529,18 +2543,6 @@ const GenericReportImproved = ({
2529
2543
  const page = Math.floor(skip / limit) + 1;
2530
2544
 
2531
2545
  // Use the same payload format as GenericReport (with query wrapper)
2532
- // Debug the unique field state at query time
2533
- console.group('🔍 [DEBUG] DataSource - Unique Field State Check');
2534
- console.log('uniqueFieldConfig:', uniqueFieldConfig);
2535
- console.log('uniqueFieldConfig.enabled:', uniqueFieldConfig.enabled);
2536
- console.log('uniqueFieldConfig.selectedField:', uniqueFieldConfig.selectedField);
2537
- console.log('Condition evaluation:', uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField);
2538
-
2539
- // Check DOM state vs React state
2540
- const checkboxes = document.querySelectorAll('input[type="checkbox"]');
2541
- console.log('🔍 [DEBUG] All checkboxes in DOM:', Array.from(checkboxes).map((cb, i) => `${i}: ${cb.checked}`));
2542
- console.groupEnd();
2543
-
2544
2546
  const uniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
2545
2547
  enabled: true,
2546
2548
  field: uniqueFieldConfig.selectedField,
@@ -2564,13 +2566,6 @@ const GenericReportImproved = ({
2564
2566
  type: col.type,
2565
2567
  };
2566
2568
 
2567
- // Log calculated fields specifically
2568
- if (col.isCalculated) {
2569
- console.log(
2570
- 'Frontend sending calculated field:',
2571
- column
2572
- );
2573
- }
2574
2569
 
2575
2570
  return column;
2576
2571
  }),
@@ -2586,16 +2581,6 @@ const GenericReportImproved = ({
2586
2581
  unique: uniqueConfig,
2587
2582
  };
2588
2583
 
2589
- // Debug log for unique field configuration
2590
- if (uniqueFieldConfig.enabled) {
2591
- console.group('🔍 [DEBUG] Unique Field Configuration');
2592
- console.log('Unique config enabled:', uniqueFieldConfig.enabled);
2593
- console.log('Selected field:', uniqueFieldConfig.selectedField);
2594
- console.log('Query unique config:', uniqueConfig);
2595
- console.log('Expected SQL: SELECT DISTINCT', uniqueFieldConfig.selectedField, '...');
2596
- console.groupEnd();
2597
- }
2598
-
2599
2584
  // Add sorting from DataGrid if provided
2600
2585
  if (sortInfo) {
2601
2586
  queryConfig.sorting = [
@@ -2619,16 +2604,6 @@ const GenericReportImproved = ({
2619
2604
  selectedTemplate?.executeUrl ||
2620
2605
  executeUrl;
2621
2606
 
2622
- console.log('🔍 [DEBUG] Regular Report API Call:', {
2623
- selectedTemplate: selectedTemplate?.id,
2624
- customExecuteUrl: customUrls.executeUrl,
2625
- templateExecuteUrl: selectedTemplate?.executeUrl,
2626
- defaultExecuteUrl: executeUrl,
2627
- finalExecuteUrl,
2628
- usingCustomUrl: !!customUrls.executeUrl,
2629
- usingTemplateExecuteUrl: !!selectedTemplate?.executeUrl,
2630
- });
2631
-
2632
2607
  const result = await CustomFetch(
2633
2608
  finalExecuteUrl,
2634
2609
  'POST',
@@ -2636,7 +2611,7 @@ const GenericReportImproved = ({
2636
2611
  );
2637
2612
 
2638
2613
  if (result.error) {
2639
- console.error('Report execution error:', result.error);
2614
+ debugLog('Report execution error:', result.error);
2640
2615
  return { data: [], count: 0 };
2641
2616
  } else if (result.data?.success) {
2642
2617
  const fetchedData = result.data.data || [];
@@ -2647,20 +2622,13 @@ const GenericReportImproved = ({
2647
2622
  setTotalCount(totalRecords);
2648
2623
  setTotalResults(totalRecords);
2649
2624
 
2650
- console.info(
2651
- 'Fetched data:',
2652
- fetchedData.length,
2653
- 'of',
2654
- totalRecords
2655
- );
2656
-
2657
2625
  return { data: fetchedData, count: totalRecords };
2658
2626
  } else {
2659
- console.error('Unexpected response format:', result);
2627
+ debugLog('Unexpected response format:', result);
2660
2628
  return { data: [], count: 0 };
2661
2629
  }
2662
2630
  } catch (error) {
2663
- console.error('Error in dataSource:', error);
2631
+ debugLog('Error in dataSource:', error);
2664
2632
  return { data: [], count: 0 };
2665
2633
  }
2666
2634
  },
@@ -2678,20 +2646,11 @@ const GenericReportImproved = ({
2678
2646
  );
2679
2647
 
2680
2648
  // Execute report function for manual execution (creates grid columns)
2681
- const executeReport = async () => {
2682
- console.log('🎯 [DEBUG] executeReport called:', {
2683
- selectedTable,
2684
- selectedColumns: selectedColumns.length,
2685
- selectedTemplate: selectedTemplate?.id,
2686
- hasGrouping: selectedTemplate?.grouping?.enabled,
2687
- executeUrl,
2688
- setting,
2689
- });
2690
-
2691
- // CRITICAL DEBUG: Check uniqueFieldConfig state at execute time
2692
- console.log('🚨 [CRITICAL] uniqueFieldConfig at executeReport start:', uniqueFieldConfig);
2693
- console.log('🚨 [CRITICAL] uniqueFieldConfig.enabled:', uniqueFieldConfig.enabled);
2694
- console.log('🚨 [CRITICAL] uniqueFieldConfig.selectedField:', uniqueFieldConfig.selectedField);
2649
+ const executeReport = async ({ page, size } = {}) => {
2650
+ // Pagination handlers pass the page/size they just set: reading them from
2651
+ // state here would use the value from the render that queued the update.
2652
+ const pageToLoad = page || currentPage;
2653
+ const sizeToLoad = size || pageSize;
2695
2654
 
2696
2655
  if (!selectedTable || selectedColumns.length === 0) {
2697
2656
  toast.warning(
@@ -2708,20 +2667,7 @@ const GenericReportImproved = ({
2708
2667
  const hasTemplateGrouping = selectedTemplate?.grouping?.enabled;
2709
2668
  const hasCustomGrouping = groupingConfig.enabled;
2710
2669
  const hasGrouping = hasTemplateGrouping || hasCustomGrouping;
2711
- let fetchedDataCount = 0;
2712
-
2713
- console.log('📋 [DEBUG] Template analysis:', {
2714
- selectedTemplate,
2715
- hasTemplateGrouping,
2716
- hasCustomGrouping,
2717
- hasGrouping,
2718
- customGroupingConfig: groupingConfig,
2719
- templateGroupingConfig: selectedTemplate?.grouping,
2720
- });
2721
- console.log(
2722
- '🎯 [DEBUG] Initial fetchedDataCount:',
2723
- fetchedDataCount
2724
- );
2670
+ let fetchedRows = [];
2725
2671
 
2726
2672
  if (hasGrouping) {
2727
2673
  // Determine which grouping configuration to use
@@ -2753,27 +2699,16 @@ const GenericReportImproved = ({
2753
2699
  };
2754
2700
  effectiveGrouping = dynamicTemplate.grouping;
2755
2701
 
2756
- console.log(
2757
- '🏗️ [DEBUG] Created dynamic template for custom grouping:',
2758
- dynamicTemplate
2759
- );
2760
2702
  } else if (hasCustomGrouping && !groupingConfig.groupByField) {
2761
2703
  // Custom grouping enabled but no field selected - show error and fallback to regular report
2762
2704
  toast.error(
2763
2705
  'Please select a field to group by before executing the report.'
2764
2706
  );
2765
- console.log(
2766
- '⚠️ [DEBUG] Custom grouping enabled but no groupByField selected'
2767
- );
2768
2707
  setIsLoading(false);
2769
2708
  return;
2770
2709
  } else {
2771
2710
  // Use existing template grouping
2772
2711
  effectiveGrouping = selectedTemplate.grouping;
2773
- console.log(
2774
- '📋 [DEBUG] Using template grouping:',
2775
- effectiveGrouping
2776
- );
2777
2712
  }
2778
2713
 
2779
2714
  // For grouped reports, fetch data with reasonable limit
@@ -2786,76 +2721,48 @@ const GenericReportImproved = ({
2786
2721
  template: dynamicTemplate, // Pass dynamic template if created
2787
2722
  });
2788
2723
 
2789
- console.log('🎯 [DEBUG] Full API Response:', groupedResult);
2790
- console.log(
2791
- '🎯 [DEBUG] groupedResult.data:',
2792
- groupedResult.data
2793
- );
2794
- console.log(
2795
- '🎯 [DEBUG] groupedResult.data.grouped:',
2796
- groupedResult.data?.grouped
2797
- );
2798
- console.log(
2799
- '🎯 [DEBUG] groupedResult.data.groupedData:',
2800
- groupedResult.data?.groupedData
2801
- );
2802
-
2803
2724
  if (
2804
2725
  groupedResult.data?.grouped &&
2805
2726
  groupedResult.data?.groupedData?.groups
2806
2727
  ) {
2807
- console.log('✅ Processing grouped report');
2808
2728
  setGroupedData(groupedResult.data); // Store entire response with grouped flag
2809
2729
  setPreviewData([]); // Clear regular preview data
2810
- // Get the count from the fetched grouped data
2811
- console.log(
2812
- '🎯 [DEBUG] Before assignment - groupedResult.data.groupedData.totalRecords:',
2813
- groupedResult.data.groupedData?.totalRecords
2814
- );
2815
- fetchedDataCount =
2730
+
2731
+ // Keep the record counters in step with the grouped result
2732
+ const groupedTotal =
2816
2733
  groupedResult.data.groupedData?.totalRecords || 0;
2817
- console.log(
2818
- '🎯 [DEBUG] After assignment - fetchedDataCount:',
2819
- fetchedDataCount
2820
- );
2821
- console.log('🔢 [DEBUG] Grouped data count:', {
2822
- totalRecords:
2823
- groupedResult.data.groupedData?.totalRecords,
2824
- fetchedDataCount,
2825
- hasGroups: !!groupedResult.data.groupedData?.groups,
2826
- });
2734
+ setTotalResults(groupedTotal);
2735
+ setTotalCount(groupedTotal);
2827
2736
  } else {
2828
2737
  // Fallback to regular display if server doesn't support grouping
2829
- console.log('⚠️ Fallback to regular display');
2830
- const fallbackData = groupedResult.data?.data || [];
2831
- console.log(
2832
- '🎯 [DEBUG] Fallback data length:',
2833
- fallbackData.length
2834
- );
2738
+ const fallbackData = Array.isArray(groupedResult.data?.data)
2739
+ ? groupedResult.data.data
2740
+ : [];
2741
+ fetchedRows = fallbackData;
2835
2742
  setPreviewData(fallbackData);
2836
- fetchedDataCount = fallbackData.length;
2743
+ setTotalResults(fallbackData.length);
2744
+ setTotalCount(fallbackData.length);
2837
2745
  }
2838
2746
  } else {
2839
2747
  // Regular non-grouped report
2840
2748
  const currentPageResult = await dataSource({
2841
- skip: (currentPage - 1) * pageSize,
2842
- limit: pageSize,
2749
+ skip: (pageToLoad - 1) * sizeToLoad,
2750
+ limit: sizeToLoad,
2843
2751
  sortInfo: null,
2844
2752
  filterValue: null,
2845
2753
  });
2846
2754
 
2847
- if (
2848
- currentPageResult.data &&
2849
- currentPageResult.data.length > 0
2850
- ) {
2851
- setPreviewData(currentPageResult.data);
2852
- fetchedDataCount = currentPageResult.data.length;
2853
- }
2755
+ fetchedRows = Array.isArray(currentPageResult.data)
2756
+ ? currentPageResult.data
2757
+ : [];
2758
+ setPreviewData(fetchedRows);
2854
2759
  }
2855
2760
 
2856
2761
  // Create grid columns for regular reports only
2857
2762
  if (!hasGrouping) {
2858
- const dataToProcess = previewData.length > 0 ? previewData : [];
2763
+ // Use the rows we just fetched previewData still holds the
2764
+ // previous render's value at this point.
2765
+ const dataToProcess = fetchedRows;
2859
2766
 
2860
2767
  if (dataToProcess.length > 0) {
2861
2768
  // Create grid columns dynamically from actual data with smart formatting
@@ -2970,15 +2877,17 @@ const GenericReportImproved = ({
2970
2877
  };
2971
2878
  });
2972
2879
  setGridColumns(dynamicColumns);
2880
+ } else if (
2881
+ !Array.isArray(selectedColumns) ||
2882
+ selectedColumns.length === 0
2883
+ ) {
2884
+ debugLog(
2885
+ 'selectedColumns is not a valid array:',
2886
+ selectedColumns
2887
+ );
2888
+ setGridColumns([]);
2973
2889
  } else {
2974
2890
  // Fallback to selected columns if no data
2975
- console.log('🔧 [DEBUG] Creating fallback columns from selectedColumns:', selectedColumns);
2976
- if (!Array.isArray(selectedColumns) || selectedColumns.length === 0) {
2977
- console.warn('⚠️ selectedColumns is not a valid array:', selectedColumns);
2978
- setGridColumns([]);
2979
- return;
2980
- }
2981
-
2982
2891
  const columns = selectedColumns.map((col, index) => {
2983
2892
  try {
2984
2893
  // Find column type from table columns with enhanced detection
@@ -3049,13 +2958,23 @@ const GenericReportImproved = ({
3049
2958
 
3050
2959
  return formattedValue;
3051
2960
  } catch (renderError) {
3052
- console.warn('🚨 Cell render error:', renderError, 'for value:', value);
2961
+ debugLog(
2962
+ 'Cell render error:',
2963
+ renderError,
2964
+ 'for value:',
2965
+ value
2966
+ );
3053
2967
  return String(value || '');
3054
2968
  }
3055
2969
  },
3056
2970
  };
3057
2971
  } catch (colError) {
3058
- console.warn('🚨 Column mapping error:', colError, 'for column:', col);
2972
+ debugLog(
2973
+ 'Column mapping error:',
2974
+ colError,
2975
+ 'for column:',
2976
+ col
2977
+ );
3059
2978
  return {
3060
2979
  name: `error_col_${index}`,
3061
2980
  header: 'Error Column',
@@ -3074,25 +2993,8 @@ const GenericReportImproved = ({
3074
2993
  }
3075
2994
 
3076
2995
  // Report executed successfully
3077
- console.log('🎯 [DEBUG] Final fetchedDataCount:', fetchedDataCount);
3078
- console.log(
3079
- '🎯 [DEBUG] Type of fetchedDataCount:',
3080
- typeof fetchedDataCount
3081
- );
3082
2996
  } catch (error) {
3083
- console.group('🚨 [ERROR] executeReport failed');
3084
- console.error('Error executing report:', error);
3085
- console.log('Error name:', error.name);
3086
- console.log('Error message:', error.message);
3087
- console.log('Error stack:', error.stack);
3088
- console.log('Current state at error:', {
3089
- selectedTable,
3090
- selectedColumns: selectedColumns.length,
3091
- hasGrouping: selectedTemplate?.grouping?.enabled || groupingConfig.enabled,
3092
- currentPage,
3093
- pageSize,
3094
- });
3095
- console.groupEnd();
2997
+ debugLog('Error executing report:', error);
3096
2998
 
3097
2999
  // Only show toast error for actual failures, not for successful pagination
3098
3000
  if (!error.message?.includes('successful')) {
@@ -3124,23 +3026,24 @@ const GenericReportImproved = ({
3124
3026
  // Load saved report configuration
3125
3027
  const loadSavedReport = (report) => {
3126
3028
  try {
3127
- const config =
3128
- typeof report.detail === 'string'
3129
- ? JSON.parse(report.detail)
3130
- : report.detail;
3029
+ const config = parseReportDetail(report?.detail);
3030
+
3031
+ if (!config) {
3032
+ toast.error('This report has no saved configuration.');
3033
+ return;
3034
+ }
3131
3035
 
3132
3036
  if (config.mainTable) {
3133
3037
  setSelectedTable(config.mainTable);
3134
3038
  setAvailableTables([config.mainTable]);
3135
- fetchTableColumns(config.mainTable);
3136
- fetchDetectedJoins(config.mainTable);
3039
+ // The selectedTable effect fetches columns and suggested joins.
3137
3040
  }
3138
3041
 
3139
- if (config.columns) {
3042
+ if (Array.isArray(config.columns)) {
3140
3043
  setSelectedColumns(config.columns);
3141
3044
  }
3142
3045
 
3143
- if (config.joins) {
3046
+ if (Array.isArray(config.joins)) {
3144
3047
  setJoins(config.joins);
3145
3048
  // Fetch columns for each joined table
3146
3049
  config.joins.forEach((join, index) => {
@@ -3157,7 +3060,6 @@ const GenericReportImproved = ({
3157
3060
 
3158
3061
  // Restore unique field configuration if it exists
3159
3062
  if (config.unique) {
3160
- console.log('🔄 [RESTORE] Restoring unique field config:', config.unique);
3161
3063
  setUniqueFieldConfig(config.unique);
3162
3064
  }
3163
3065
 
@@ -3242,19 +3144,13 @@ const GenericReportImproved = ({
3242
3144
  toast.success(`Loaded report: ${report.label}`);
3243
3145
  } catch (error) {
3244
3146
  toast.error('Failed to load report configuration');
3245
- console.error('Error loading report:', error);
3147
+ debugLog('Error loading report:', error);
3246
3148
  }
3247
3149
  };
3248
3150
 
3249
3151
  // Load business template configuration
3250
3152
  const loadBusinessTemplate = async (template) => {
3251
3153
  try {
3252
- console.log('🏗️ [DEBUG] Loading business template:', {
3253
- templateId: template.id,
3254
- templateName: template.name,
3255
- hasGrouping: template.grouping?.enabled,
3256
- groupingConfig: template.grouping,
3257
- });
3258
3154
 
3259
3155
  setSelectedTemplate(template);
3260
3156
 
@@ -3385,11 +3281,6 @@ const GenericReportImproved = ({
3385
3281
  emptyRowStyle: template.grouping.emptyRowStyle || 'light',
3386
3282
  });
3387
3283
 
3388
- console.log('🏗️ [DEBUG] Applied template grouping config:', {
3389
- enabled: template.grouping.enabled,
3390
- groupByField: template.grouping.groupByField,
3391
- groupDisplayName: template.grouping.groupDisplayName,
3392
- });
3393
3284
  } else {
3394
3285
  // Reset grouping config if template doesn't have grouping
3395
3286
  setGroupingConfig({
@@ -3418,7 +3309,7 @@ const GenericReportImproved = ({
3418
3309
  toast.success(`Loaded template: ${template.name}`);
3419
3310
  } catch (error) {
3420
3311
  toast.error('Failed to load template configuration');
3421
- console.error('Error loading template:', error);
3312
+ debugLog('Error loading template:', error);
3422
3313
  }
3423
3314
  };
3424
3315
 
@@ -3450,69 +3341,109 @@ const GenericReportImproved = ({
3450
3341
  </div>
3451
3342
  </div>
3452
3343
 
3453
- {/* Business Templates Section */}
3454
- <div className={styles.businessTemplatesSection}>
3455
- <h3>Business Report Templates</h3>
3456
- <p>
3457
- Pre-configured reports for common business scenarios
3458
- </p>
3459
- <div className={styles.templatesGrid}>
3460
- {businessTemplates.map((template) => {
3461
- const TemplateIcon = getTemplateIcon(
3462
- template.icon
3463
- );
3464
- return (
3465
- <div
3466
- key={template.id}
3467
- className={styles.templateCard}
3468
- onClick={() =>
3469
- loadBusinessTemplate(template)
3470
- }
3471
- >
3472
- <div className={styles.templateIcon}>
3473
- <TemplateIcon size={24} />
3474
- </div>
3475
- <div className={styles.templateContent}>
3476
- <h4>{template.name}</h4>
3477
- <p>{template.description}</p>
3344
+ {/* Business Templates Section — hidden when the host app
3345
+ ships no templates, so the panel is never empty */}
3346
+ {businessTemplates.length > 0 && (
3347
+ <div className={styles.businessTemplatesSection}>
3348
+ <h3>Business Report Templates</h3>
3349
+ <p>
3350
+ Pre-configured reports for common business
3351
+ scenarios
3352
+ </p>
3353
+ <div className={styles.templatesGrid}>
3354
+ {businessTemplates.map((template) => {
3355
+ const TemplateIcon = getTemplateIcon(
3356
+ template.icon
3357
+ );
3358
+ const tags = Array.isArray(template.tags)
3359
+ ? template.tags
3360
+ : [];
3361
+ return (
3362
+ <div
3363
+ key={template.id}
3364
+ className={styles.templateCard}
3365
+ onClick={() =>
3366
+ loadBusinessTemplate(template)
3367
+ }
3368
+ >
3478
3369
  <div
3479
- className={styles.templateTags}
3370
+ className={styles.templateIcon}
3480
3371
  >
3481
- {template.tags.map(
3482
- (tag, index) => (
3483
- <span
3484
- key={index}
3485
- className={
3486
- styles.templateTag
3487
- }
3488
- >
3489
- {tag}
3490
- </span>
3491
- )
3372
+ <TemplateIcon size={24} />
3373
+ </div>
3374
+ <div
3375
+ className={
3376
+ styles.templateContent
3377
+ }
3378
+ >
3379
+ <h4>{template.name}</h4>
3380
+ <p>{template.description}</p>
3381
+ {tags.length > 0 && (
3382
+ <div
3383
+ className={
3384
+ styles.templateTags
3385
+ }
3386
+ >
3387
+ {tags.map(
3388
+ (tag, index) => (
3389
+ <span
3390
+ key={index}
3391
+ className={
3392
+ styles.templateTag
3393
+ }
3394
+ >
3395
+ {tag}
3396
+ </span>
3397
+ )
3398
+ )}
3399
+ </div>
3492
3400
  )}
3493
3401
  </div>
3402
+ {template.category && (
3403
+ <div
3404
+ className={
3405
+ styles.templateCategory
3406
+ }
3407
+ >
3408
+ {template.category}
3409
+ </div>
3410
+ )}
3494
3411
  </div>
3495
- <div
3496
- className={styles.templateCategory}
3497
- >
3498
- {template.category}
3499
- </div>
3500
- </div>
3501
- );
3502
- })}
3412
+ );
3413
+ })}
3414
+ </div>
3503
3415
  </div>
3504
- </div>
3416
+ )}
3505
3417
 
3506
3418
  {/* Saved Reports List */}
3507
3419
  {isLoadingReports ? (
3508
3420
  <div className={styles.loading}>
3509
3421
  Loading your saved reports...
3510
3422
  </div>
3423
+ ) : reportsError ? (
3424
+ <div className={styles.errorPanel} role="alert">
3425
+ <TriangleAlert size={20} />
3426
+ <div>
3427
+ <h3>Saved reports unavailable</h3>
3428
+ <p>{reportsError}</p>
3429
+ </div>
3430
+ <button
3431
+ type="button"
3432
+ className={styles.retryButton}
3433
+ onClick={fetchSavedReports}
3434
+ >
3435
+ Try again
3436
+ </button>
3437
+ </div>
3511
3438
  ) : savedReports.length > 0 ? (
3512
3439
  <div className={styles.savedReportsSection}>
3513
3440
  <h3>Your Saved Reports ({savedReports.length})</h3>
3514
3441
  <div className={styles.reportsGrid}>
3515
- {savedReports.map((report) => (
3442
+ {savedReports.map((report) => {
3443
+ const reportMainTable =
3444
+ parseReportDetail(report.detail)
3445
+ ?.mainTable || '';
3446
+ return (
3516
3447
  <div
3517
3448
  key={report.id}
3518
3449
  className={styles.reportCard}
@@ -3566,7 +3497,7 @@ const GenericReportImproved = ({
3566
3497
  </span>
3567
3498
  </div>
3568
3499
  </div>
3569
- {report.detail && (
3500
+ {reportMainTable && (
3570
3501
  <div
3571
3502
  className={
3572
3503
  styles.reportTableInfo
@@ -3587,13 +3518,7 @@ const GenericReportImproved = ({
3587
3518
  }}
3588
3519
  />
3589
3520
  {formatName(
3590
- typeof report.detail ===
3591
- 'string'
3592
- ? JSON.parse(
3593
- report.detail
3594
- ).mainTable
3595
- : report.detail
3596
- .mainTable
3521
+ reportMainTable
3597
3522
  )}
3598
3523
  </span>
3599
3524
  </div>
@@ -3608,6 +3533,7 @@ const GenericReportImproved = ({
3608
3533
  </div>
3609
3534
  <div className={styles.reportActions}>
3610
3535
  <button
3536
+ type="button"
3611
3537
  className={
3612
3538
  styles.deleteReportBtn
3613
3539
  }
@@ -3624,7 +3550,8 @@ const GenericReportImproved = ({
3624
3550
  </button>
3625
3551
  </div>
3626
3552
  </div>
3627
- ))}
3553
+ );
3554
+ })}
3628
3555
  </div>
3629
3556
  </div>
3630
3557
  ) : (
@@ -3664,10 +3591,24 @@ const GenericReportImproved = ({
3664
3591
  className={`${styles.wizardStep} ${
3665
3592
  isActive ? styles.active : ''
3666
3593
  } ${isComplete ? styles.complete : ''}`}
3594
+ role="button"
3595
+ tabIndex={index <= currentWizardStep ? 0 : -1}
3596
+ aria-current={isActive ? 'step' : undefined}
3597
+ aria-disabled={index > currentWizardStep}
3667
3598
  onClick={() =>
3668
3599
  index <= currentWizardStep &&
3669
3600
  setCurrentWizardStep(index)
3670
3601
  }
3602
+ onKeyDown={(event) => {
3603
+ if (
3604
+ (event.key === 'Enter' ||
3605
+ event.key === ' ') &&
3606
+ index <= currentWizardStep
3607
+ ) {
3608
+ event.preventDefault();
3609
+ setCurrentWizardStep(index);
3610
+ }
3611
+ }}
3671
3612
  >
3672
3613
  <div className={styles.stepIcon}>
3673
3614
  {isComplete && !isActive ? (
@@ -3739,6 +3680,7 @@ const GenericReportImproved = ({
3739
3680
  <div className={styles.wizardNavigation}>
3740
3681
  {currentWizardStep === 0 ? (
3741
3682
  <button
3683
+ type="button"
3742
3684
  className={`${styles.btn} ${styles.btnCancel}`}
3743
3685
  onClick={startAgain}
3744
3686
  >
@@ -3746,6 +3688,7 @@ const GenericReportImproved = ({
3746
3688
  </button>
3747
3689
  ) : (
3748
3690
  <button
3691
+ type="button"
3749
3692
  className={`${styles.btn} ${styles.btnPrevious}`}
3750
3693
  onClick={goToPreviousStep}
3751
3694
  >
@@ -3755,6 +3698,7 @@ const GenericReportImproved = ({
3755
3698
 
3756
3699
  {currentWizardStep === wizardSteps.length - 1 ? (
3757
3700
  <button
3701
+ type="button"
3758
3702
  className={`${styles.btn} ${styles.btnStartAgain}`}
3759
3703
  onClick={startAgain}
3760
3704
  >
@@ -3762,6 +3706,7 @@ const GenericReportImproved = ({
3762
3706
  </button>
3763
3707
  ) : (
3764
3708
  <button
3709
+ type="button"
3765
3710
  className={`${styles.btn} ${styles.btnPrimary}`}
3766
3711
  onClick={goToNextStep}
3767
3712
  disabled={!isStepComplete(currentStep.id)}
@@ -3803,6 +3748,21 @@ const GenericReportImproved = ({
3803
3748
  <div className={styles.loading}>
3804
3749
  Loading available tables...
3805
3750
  </div>
3751
+ ) : tablesError ? (
3752
+ <div className={styles.errorPanel} role="alert">
3753
+ <TriangleAlert size={20} />
3754
+ <div>
3755
+ <h3>Data sources unavailable</h3>
3756
+ <p>{tablesError}</p>
3757
+ </div>
3758
+ <button
3759
+ type="button"
3760
+ className={styles.retryButton}
3761
+ onClick={fetchDatabaseTables}
3762
+ >
3763
+ Try again
3764
+ </button>
3765
+ </div>
3806
3766
  ) : tables.length === 0 ? (
3807
3767
  <div className={styles.noTables}>
3808
3768
  <Data size={48} />
@@ -3834,9 +3794,21 @@ const GenericReportImproved = ({
3834
3794
  className={`${styles.tableCard} ${
3835
3795
  isSelected ? styles.selected : ''
3836
3796
  }`}
3797
+ role="button"
3798
+ tabIndex={0}
3799
+ aria-pressed={isSelected}
3837
3800
  onClick={() =>
3838
3801
  handleTableSelect(table.name)
3839
3802
  }
3803
+ onKeyDown={(event) => {
3804
+ if (
3805
+ event.key === 'Enter' ||
3806
+ event.key === ' '
3807
+ ) {
3808
+ event.preventDefault();
3809
+ handleTableSelect(table.name);
3810
+ }
3811
+ }}
3840
3812
  >
3841
3813
  <div className={styles.tableCardIcon}>
3842
3814
  <TableIcon size={24} />
@@ -3877,44 +3849,23 @@ const GenericReportImproved = ({
3877
3849
  );
3878
3850
  };
3879
3851
 
3880
- // Render column selection
3881
- const renderColumnSelection = () => {
3882
- // Add loading check at the beginning
3883
- if (isLoadingTables || isLoadingJoinColumns) {
3884
- return (
3885
- <div className={styles.loading}>
3886
- Loading table columns
3887
- {isLoadingJoinColumns ? ' from joined tables' : ''}...
3888
- </div>
3889
- );
3890
- }
3891
-
3892
- if (tableColumns.length === 0) {
3893
- return (
3894
- <div className={styles.noColumns}>
3895
- <Data size={48} />
3896
- <h3>No Columns Available</h3>
3897
- <p>
3898
- No columns found for the selected table. Please try
3899
- selecting a different table or check your configuration.
3900
- </p>
3901
- </div>
3902
- );
3903
- }
3852
+ // Field pickers for the column step. Memoised because the step re-renders on
3853
+ // every checkbox toggle and these lists only change when the schema does.
3854
+ const allAvailableColumns = useMemo(() => {
3855
+ if (!selectedTable) return [];
3904
3856
 
3905
- // Get all available tables for disambiguation
3857
+ // Table names used for display disambiguation
3906
3858
  const allAvailableTables = [
3907
3859
  selectedTable,
3908
3860
  ...joins.map((join) => join.targetTable),
3909
- ];
3861
+ ].map((name) => ({ name }));
3910
3862
 
3911
- // Get all available columns (main table + joined tables)
3912
- const allAvailableColumns = [
3863
+ const sections = [
3913
3864
  {
3914
3865
  tableName: selectedTable,
3915
3866
  displayName: getUniqueTableDisplayName(
3916
3867
  selectedTable,
3917
- allAvailableTables.map((t) => ({ name: t })),
3868
+ allAvailableTables,
3918
3869
  definition
3919
3870
  ),
3920
3871
  columns: tableColumns,
@@ -3924,7 +3875,7 @@ const GenericReportImproved = ({
3924
3875
  tableName: join.targetTable,
3925
3876
  displayName: getUniqueTableDisplayName(
3926
3877
  join.targetTable,
3927
- allAvailableTables.map((t) => ({ name: t })),
3878
+ allAvailableTables,
3928
3879
  definition
3929
3880
  ),
3930
3881
  columns: join.availableColumns || [],
@@ -3932,18 +3883,13 @@ const GenericReportImproved = ({
3932
3883
  })),
3933
3884
  ];
3934
3885
 
3935
- // Add calculated fields if template is selected or custom fields exist
3936
- const templateCalculatedFields =
3937
- selectedTemplate && selectedTemplate.calculatedFields
3938
- ? selectedTemplate.calculatedFields
3939
- : [];
3940
3886
  const allCalculatedFields = [
3941
- ...templateCalculatedFields,
3887
+ ...(selectedTemplate?.calculatedFields || []),
3942
3888
  ...customCalculatedFields,
3943
3889
  ];
3944
3890
 
3945
3891
  if (allCalculatedFields.length > 0) {
3946
- const calculatedFields = {
3892
+ sections.push({
3947
3893
  tableName: 'calculated',
3948
3894
  displayName: 'Calculated Fields',
3949
3895
  columns: allCalculatedFields.map((calc) => ({
@@ -3954,16 +3900,77 @@ const GenericReportImproved = ({
3954
3900
  isCalculated: true,
3955
3901
  })),
3956
3902
  isCalculated: true,
3957
- };
3958
- allAvailableColumns.push(calculatedFields);
3903
+ });
3959
3904
  }
3960
3905
 
3961
- // Use enhanced categorization with smart filtering for main table
3962
- const mainTableCategories = categorizeFields(
3963
- tableColumns,
3964
- showHiddenFields
3965
- );
3966
- const mainHiddenCount = mainTableCategories.hidden.length;
3906
+ return sections;
3907
+ }, [
3908
+ selectedTable,
3909
+ tableColumns,
3910
+ joins,
3911
+ definition,
3912
+ selectedTemplate,
3913
+ customCalculatedFields,
3914
+ ]);
3915
+
3916
+ const mainTableCategories = useMemo(
3917
+ () => categorizeFields(tableColumns, showHiddenFields),
3918
+ [tableColumns, showHiddenFields]
3919
+ );
3920
+
3921
+ // categorizeFields only collects hidden fields when they are being shown,
3922
+ // so the toggle's counter has to be derived from the raw column list.
3923
+ const mainHiddenCount = useMemo(
3924
+ () =>
3925
+ (tableColumns || []).filter((col) =>
3926
+ shouldHideField(col?.name, col?.type)
3927
+ ).length,
3928
+ [tableColumns]
3929
+ );
3930
+
3931
+ // Render column selection
3932
+ const renderColumnSelection = () => {
3933
+ // Add loading check at the beginning
3934
+ if (isLoadingColumns || isLoadingJoinColumns) {
3935
+ return (
3936
+ <div className={styles.loading}>
3937
+ Loading table columns
3938
+ {isLoadingJoinColumns ? ' from joined tables' : ''}...
3939
+ </div>
3940
+ );
3941
+ }
3942
+
3943
+ if (columnsError) {
3944
+ return (
3945
+ <div className={styles.errorPanel} role="alert">
3946
+ <TriangleAlert size={20} />
3947
+ <div>
3948
+ <h3>Fields unavailable</h3>
3949
+ <p>{columnsError}</p>
3950
+ </div>
3951
+ <button
3952
+ type="button"
3953
+ className={styles.retryButton}
3954
+ onClick={() => fetchTableColumns(selectedTable)}
3955
+ >
3956
+ Try again
3957
+ </button>
3958
+ </div>
3959
+ );
3960
+ }
3961
+
3962
+ if (tableColumns.length === 0) {
3963
+ return (
3964
+ <div className={styles.noColumns}>
3965
+ <Data size={48} />
3966
+ <h3>No Columns Available</h3>
3967
+ <p>
3968
+ No columns found for the selected table. Please try
3969
+ selecting a different table or check your configuration.
3970
+ </p>
3971
+ </div>
3972
+ );
3973
+ }
3967
3974
 
3968
3975
  const categoryInfo = {
3969
3976
  essential: {
@@ -4025,6 +4032,7 @@ const GenericReportImproved = ({
4025
4032
  </p>
4026
4033
  <div className={styles.quickActions}>
4027
4034
  <button
4035
+ type="button"
4028
4036
  className={styles.linkButton}
4029
4037
  onClick={() => {
4030
4038
  const allVisibleColumns = [];
@@ -4073,6 +4081,7 @@ const GenericReportImproved = ({
4073
4081
  Select All Visible
4074
4082
  </button>
4075
4083
  <button
4084
+ type="button"
4076
4085
  className={styles.linkButton}
4077
4086
  onClick={() => setSelectedColumns([])}
4078
4087
  >
@@ -4080,6 +4089,7 @@ const GenericReportImproved = ({
4080
4089
  </button>
4081
4090
  {mainHiddenCount > 0 && (
4082
4091
  <button
4092
+ type="button"
4083
4093
  className={styles.linkButton}
4084
4094
  onClick={() =>
4085
4095
  setShowHiddenFields(!showHiddenFields)
@@ -4484,19 +4494,12 @@ const GenericReportImproved = ({
4484
4494
  type="checkbox"
4485
4495
  checked={uniqueFieldConfig.enabled}
4486
4496
  onChange={(e) => {
4487
- console.log('🔍 [DEBUG] Unique field toggled:', e.target.checked);
4488
- console.log('🔍 [DEBUG] Current uniqueFieldConfig before change:', uniqueFieldConfig);
4489
4497
  const newConfig = {
4490
4498
  ...uniqueFieldConfig,
4491
4499
  enabled: e.target.checked,
4492
4500
  selectedField: e.target.checked ? uniqueFieldConfig.selectedField : ''
4493
4501
  };
4494
- console.log('🔍 [DEBUG] New unique field config:', newConfig);
4495
4502
  setUniqueFieldConfig(newConfig);
4496
- // Log immediately after setState
4497
- setTimeout(() => {
4498
- console.log('🔍 [DEBUG] State after setTimeout:', uniqueFieldConfig);
4499
- }, 100);
4500
4503
  }}
4501
4504
  />
4502
4505
  <span className={styles.checkboxText}>
@@ -4512,27 +4515,21 @@ const GenericReportImproved = ({
4512
4515
 
4513
4516
  {uniqueFieldConfig.enabled && (
4514
4517
  <div className={styles.uniqueFieldSelector}>
4515
- <label className={styles.fieldLabel}>
4518
+ <label
4519
+ className={styles.fieldLabel}
4520
+ htmlFor="unique-field-select"
4521
+ >
4516
4522
  Select field to make unique:
4517
4523
  </label>
4518
4524
  <select
4525
+ id="unique-field-select"
4519
4526
  value={uniqueFieldConfig.selectedField}
4520
4527
  onChange={(e) => {
4521
- console.log('🔍 [DEBUG] Unique field selected:', e.target.value);
4522
- console.log('🔍 [DEBUG] Available dropdown options:', Array.from(e.target.options).map(opt => ({value: opt.value, text: opt.text})));
4523
- console.log('🔍 [DEBUG] Selected option text:', e.target.options[e.target.selectedIndex]?.text);
4524
- console.log('🔍 [DEBUG] Current uniqueFieldConfig before field change:', uniqueFieldConfig);
4525
- console.log('🔍 [DEBUG] Current selectedColumns:', selectedColumns);
4526
4528
  const newConfig = {
4527
4529
  ...uniqueFieldConfig,
4528
4530
  selectedField: e.target.value
4529
4531
  };
4530
- console.log('🔍 [DEBUG] Updated unique field config with selected field:', newConfig);
4531
4532
  setUniqueFieldConfig(newConfig);
4532
- // Log immediately after setState
4533
- setTimeout(() => {
4534
- console.log('🔍 [DEBUG] Field state after setTimeout:', uniqueFieldConfig);
4535
- }, 100);
4536
4533
  }}
4537
4534
  className={styles.fieldSelect}
4538
4535
  required
@@ -4564,7 +4561,9 @@ const GenericReportImproved = ({
4564
4561
  // Render relationships (simplified)
4565
4562
  const renderRelationships = () => {
4566
4563
  const tableSuggestions = detectedJoins[selectedTable] || [];
4567
- const isLoading = isLoadingDetectedJoins[selectedTable] || false;
4564
+ // Named apart from the report-execution `isLoading` state it shadowed
4565
+ const isLoadingSuggestions =
4566
+ isLoadingDetectedJoins[selectedTable] || false;
4568
4567
 
4569
4568
  return (
4570
4569
  <div className={styles.relationshipsSection}>
@@ -4605,6 +4604,7 @@ const GenericReportImproved = ({
4605
4604
  </span>
4606
4605
  </div>
4607
4606
  <button
4607
+ type="button"
4608
4608
  className={styles.removeJoinBtn}
4609
4609
  onClick={() => removeJoin(index)}
4610
4610
  >
@@ -4615,8 +4615,14 @@ const GenericReportImproved = ({
4615
4615
  </div>
4616
4616
  )}
4617
4617
 
4618
+ {isLoadingSuggestions && (
4619
+ <div className={styles.loading}>
4620
+ Finding possible connections...
4621
+ </div>
4622
+ )}
4623
+
4618
4624
  {/* Detected Relationships */}
4619
- {tableSuggestions.length > 0 && (
4625
+ {!isLoadingSuggestions && tableSuggestions.length > 0 && (
4620
4626
  <div className={styles.detectedJoins}>
4621
4627
  <h3>Detected Connections</h3>
4622
4628
  <p>
@@ -4639,18 +4645,21 @@ const GenericReportImproved = ({
4639
4645
  definition
4640
4646
  )}
4641
4647
  </span>
4642
- <div
4643
- className={
4644
- styles.confidenceRating
4645
- }
4646
- >
4647
- {'⭐'.repeat(
4648
- Math.floor(
4649
- suggestion.confidence /
4650
- 20
4651
- )
4652
- )}
4653
- </div>
4648
+ {suggestion.confidence != null && (
4649
+ <span
4650
+ className={
4651
+ styles.confidenceRating
4652
+ }
4653
+ title={`Confidence: ${Math.round(
4654
+ suggestion.confidence
4655
+ )}%`}
4656
+ >
4657
+ {Math.round(
4658
+ suggestion.confidence
4659
+ )}
4660
+ % match
4661
+ </span>
4662
+ )}
4654
4663
  </div>
4655
4664
  <p
4656
4665
  className={
@@ -4668,6 +4677,7 @@ const GenericReportImproved = ({
4668
4677
  </p>
4669
4678
  </div>
4670
4679
  <button
4680
+ type="button"
4671
4681
  className={`${styles.connectIconBtn} ${
4672
4682
  joins.some(
4673
4683
  (j) =>
@@ -4712,9 +4722,13 @@ const GenericReportImproved = ({
4712
4722
  </div>
4713
4723
  )}
4714
4724
 
4715
- {isLoading && (
4716
- <div className={styles.loading}>
4717
- Finding possible connections...
4725
+ {!isLoadingSuggestions && tableSuggestions.length === 0 && (
4726
+ <div className={styles.emptyState}>
4727
+ <p>
4728
+ No connections were detected for{' '}
4729
+ {getTableDisplayName(selectedTable, definition)}.
4730
+ You can add one manually below, or skip this step.
4731
+ </p>
4718
4732
  </div>
4719
4733
  )}
4720
4734
 
@@ -4738,6 +4752,7 @@ const GenericReportImproved = ({
4738
4752
 
4739
4753
  {!showManualJoinForm ? (
4740
4754
  <button
4755
+ type="button"
4741
4756
  className={`${styles.btn} ${styles.btnSecondary}`}
4742
4757
  onClick={() => {
4743
4758
  setShowManualJoinForm(true);
@@ -4915,6 +4930,7 @@ const GenericReportImproved = ({
4915
4930
 
4916
4931
  <div className={styles.manualJoinActions}>
4917
4932
  <button
4933
+ type="button"
4918
4934
  className={`${styles.btn} ${styles.btnPrimary}`}
4919
4935
  onClick={addManualJoin}
4920
4936
  disabled={
@@ -4927,6 +4943,7 @@ const GenericReportImproved = ({
4927
4943
  Add Relationship
4928
4944
  </button>
4929
4945
  <button
4946
+ type="button"
4930
4947
  className={`${styles.btn} ${styles.btnSecondary}`}
4931
4948
  onClick={() => {
4932
4949
  setShowManualJoinForm(false);
@@ -4957,6 +4974,7 @@ const GenericReportImproved = ({
4957
4974
  {getTableDisplayName(selectedTable, definition)} table.
4958
4975
  </p>
4959
4976
  <button
4977
+ type="button"
4960
4978
  className={`${styles.btn} ${styles.btnSecondary}`}
4961
4979
  onClick={goToNextStep}
4962
4980
  >
@@ -5107,9 +5125,23 @@ const GenericReportImproved = ({
5107
5125
 
5108
5126
  // Legacy functions for backward compatibility
5109
5127
  const addFilter = (filter) => {
5110
- const updatedCriteria = { ...filterCriteria };
5111
- updatedCriteria.groups[0].filters.push(filter);
5112
- setFilterCriteria(updatedCriteria);
5128
+ setFilterCriteria((prev) => {
5129
+ const groups = prev?.groups?.length
5130
+ ? prev.groups
5131
+ : [{ operator: 'AND', filters: [] }];
5132
+
5133
+ return {
5134
+ operator: prev?.operator || 'AND',
5135
+ groups: groups.map((group, index) =>
5136
+ index === 0
5137
+ ? {
5138
+ ...group,
5139
+ filters: [...(group.filters || []), filter],
5140
+ }
5141
+ : group
5142
+ ),
5143
+ };
5144
+ });
5113
5145
  };
5114
5146
 
5115
5147
  const removeFilter = (groupIndex, filterIndex) => {
@@ -5241,6 +5273,7 @@ const GenericReportImproved = ({
5241
5273
  <div className={styles.quickFiltersGrid}>
5242
5274
  {selectedTemplate.quickFilters.map((quickFilter) => (
5243
5275
  <button
5276
+ type="button"
5244
5277
  key={quickFilter.id}
5245
5278
  className={styles.quickFilterBtn}
5246
5279
  onClick={() =>
@@ -5389,13 +5422,27 @@ const GenericReportImproved = ({
5389
5422
 
5390
5423
  // Update filter value
5391
5424
  const updateFilter = (groupIndex, filterIndex, field, value) => {
5392
- const updatedCriteria = { ...filterCriteria };
5393
- updatedCriteria.groups[groupIndex].filters[filterIndex][field] = value;
5394
- setFilterCriteria(updatedCriteria);
5425
+ setFilterCriteria((prev) => ({
5426
+ ...prev,
5427
+ groups: (prev.groups || []).map((group, gIndex) =>
5428
+ gIndex === groupIndex
5429
+ ? {
5430
+ ...group,
5431
+ filters: (group.filters || []).map(
5432
+ (filter, fIndex) =>
5433
+ fIndex === filterIndex
5434
+ ? { ...filter, [field]: value }
5435
+ : filter
5436
+ ),
5437
+ }
5438
+ : group
5439
+ ),
5440
+ }));
5395
5441
  };
5396
5442
 
5397
- // Get available columns for filters
5398
- const getAvailableFilterColumns = () => {
5443
+ // Get available columns for filters. Memoised: the filter step, the sort
5444
+ // step and the grouping step each ask for this list on every render.
5445
+ const availableFilterColumns = useMemo(() => {
5399
5446
  const allColumns = [];
5400
5447
 
5401
5448
  // Add main table columns
@@ -5437,13 +5484,15 @@ const GenericReportImproved = ({
5437
5484
  });
5438
5485
 
5439
5486
  return allColumns;
5440
- };
5487
+ }, [tableColumns, joins, selectedTable, definition]);
5488
+
5489
+ const getAvailableFilterColumns = () => availableFilterColumns;
5441
5490
 
5442
5491
  // Advanced filter rendering (from original GenericReport)
5443
5492
  const renderFilters = () => {
5444
5493
  // Ensure filterCriteria has proper structure
5445
5494
  if (!filterCriteria || !filterCriteria.groups) {
5446
- console.error(
5495
+ debugLog(
5447
5496
  'FilterCriteria is not properly structured, reinitializing...'
5448
5497
  );
5449
5498
  setFilterCriteria({
@@ -6017,14 +6066,6 @@ const GenericReportImproved = ({
6017
6066
 
6018
6067
  const smartSuggestions = getSmartSuggestions();
6019
6068
 
6020
- // Debug logging to check if suggestions are generated
6021
- console.log('Smart suggestions debug:', {
6022
- selectedTable,
6023
- tableColumns: tableColumns?.length,
6024
- smartSuggestions: smartSuggestions?.length,
6025
- suggestions: smartSuggestions,
6026
- });
6027
-
6028
6069
  return (
6029
6070
  <div className={styles.filtersSection}>
6030
6071
  {/* Template Quick Filters */}
@@ -6089,6 +6130,7 @@ const GenericReportImproved = ({
6089
6130
  className={styles.suggestionActions}
6090
6131
  >
6091
6132
  <button
6133
+ type="button"
6092
6134
  className={`${styles.btn} ${styles.btnPrimary}`}
6093
6135
  onClick={() =>
6094
6136
  applySuggestion(suggestion)
@@ -6137,6 +6179,7 @@ const GenericReportImproved = ({
6137
6179
  </h3>
6138
6180
  <div className={styles.sectionHeaderActions}>
6139
6181
  <button
6182
+ type="button"
6140
6183
  className={`${styles.btn} ${styles.btnSecondary}`}
6141
6184
  onClick={(e) => {
6142
6185
  e.stopPropagation();
@@ -6181,6 +6224,7 @@ const GenericReportImproved = ({
6181
6224
  : 'Configure column'}
6182
6225
  </h4>
6183
6226
  <button
6227
+ type="button"
6184
6228
  className={
6185
6229
  styles.removeSortBtn
6186
6230
  }
@@ -6205,8 +6249,13 @@ const GenericReportImproved = ({
6205
6249
  styles.sortingField
6206
6250
  }
6207
6251
  >
6208
- <label>Column:</label>
6252
+ <label
6253
+ htmlFor={`sort-column-${index}`}
6254
+ >
6255
+ Column:
6256
+ </label>
6209
6257
  <select
6258
+ id={`sort-column-${index}`}
6210
6259
  className={
6211
6260
  styles.sortingSelect
6212
6261
  }
@@ -6321,8 +6370,13 @@ const GenericReportImproved = ({
6321
6370
  styles.sortingField
6322
6371
  }
6323
6372
  >
6324
- <label>Direction:</label>
6373
+ <label
6374
+ htmlFor={`sort-direction-${index}`}
6375
+ >
6376
+ Direction:
6377
+ </label>
6325
6378
  <select
6379
+ id={`sort-direction-${index}`}
6326
6380
  className={
6327
6381
  styles.sortingSelect
6328
6382
  }
@@ -6407,6 +6461,7 @@ const GenericReportImproved = ({
6407
6461
 
6408
6462
  <div className={styles.filterActionButtons}>
6409
6463
  <button
6464
+ type="button"
6410
6465
  className={`${styles.btn} ${styles.btnPrimary}`}
6411
6466
  onClick={handleAddFilterGroup}
6412
6467
  disabled={selectedColumns.length === 0}
@@ -6475,6 +6530,7 @@ const GenericReportImproved = ({
6475
6530
 
6476
6531
  <div className={styles.filterGroupActions}>
6477
6532
  <button
6533
+ type="button"
6478
6534
  className={`${styles.btn} ${styles.btnSecondary}`}
6479
6535
  onClick={() =>
6480
6536
  handleAddFilterCriterion(groupIndex)
@@ -6486,6 +6542,7 @@ const GenericReportImproved = ({
6486
6542
  {filterCriteria.groups &&
6487
6543
  filterCriteria.groups.length > 1 && (
6488
6544
  <button
6545
+ type="button"
6489
6546
  className={styles.removeJoinBtn}
6490
6547
  onClick={() =>
6491
6548
  handleRemoveFilterGroup(
@@ -6532,6 +6589,7 @@ const GenericReportImproved = ({
6532
6589
  }`}
6533
6590
  </h4>
6534
6591
  <button
6592
+ type="button"
6535
6593
  className={
6536
6594
  styles.removeJoinBtn
6537
6595
  }
@@ -6558,8 +6616,13 @@ const GenericReportImproved = ({
6558
6616
  styles.filterField
6559
6617
  }
6560
6618
  >
6561
- <label>Column:</label>
6619
+ <label
6620
+ htmlFor={`filter-column-${groupIndex}-${filterIndex}`}
6621
+ >
6622
+ Column:
6623
+ </label>
6562
6624
  <select
6625
+ id={`filter-column-${groupIndex}-${filterIndex}`}
6563
6626
  className={
6564
6627
  styles.filterSelect
6565
6628
  }
@@ -6637,8 +6700,13 @@ const GenericReportImproved = ({
6637
6700
  styles.filterField
6638
6701
  }
6639
6702
  >
6640
- <label>Operator:</label>
6703
+ <label
6704
+ htmlFor={`filter-operator-${groupIndex}-${filterIndex}`}
6705
+ >
6706
+ Operator:
6707
+ </label>
6641
6708
  <select
6709
+ id={`filter-operator-${groupIndex}-${filterIndex}`}
6642
6710
  className={
6643
6711
  styles.filterSelect
6644
6712
  }
@@ -6711,10 +6779,13 @@ const GenericReportImproved = ({
6711
6779
  styles.filterField
6712
6780
  }
6713
6781
  >
6714
- <label>
6782
+ <label
6783
+ htmlFor={`filter-value-${groupIndex}-${filterIndex}`}
6784
+ >
6715
6785
  Value:
6716
6786
  </label>
6717
6787
  <input
6788
+ id={`filter-value-${groupIndex}-${filterIndex}`}
6718
6789
  type="text"
6719
6790
  className={
6720
6791
  styles.filterInput
@@ -6768,6 +6839,7 @@ const GenericReportImproved = ({
6768
6839
  records.
6769
6840
  </p>
6770
6841
  <button
6842
+ type="button"
6771
6843
  className={`${styles.btn} ${styles.btnSecondary}`}
6772
6844
  onClick={goToNextStep}
6773
6845
  >
@@ -6906,41 +6978,11 @@ const GenericReportImproved = ({
6906
6978
  excludedRows: Array.from(excludedRows),
6907
6979
  };
6908
6980
 
6909
- // Debug log for exports
6910
- if (uniqueFieldConfig.enabled) {
6911
- console.group('🔍 [DEBUG] Export - Unique Field Configuration');
6912
- console.log('Export format:', format);
6913
- console.log('Export unique config:', exportUniqueConfig);
6914
- console.log('Query config:', queryConfig);
6915
- console.groupEnd();
6916
- }
6917
-
6918
6981
  const exportData = {
6919
6982
  query: queryConfig,
6920
6983
  format: format === 'excel' ? 'xlsx' : format, // Convert excel to xlsx
6921
6984
  };
6922
6985
 
6923
- // DEBUG: Log the complete request payload
6924
- console.group('🐛 PDF Export Debug');
6925
- console.log('Export Request Details:');
6926
- console.log('URL:', exportUrl);
6927
- console.log('Format:', format);
6928
- console.log('Report Name:', reportName);
6929
- console.log('Selected Table:', selectedTable);
6930
- console.log('Selected Columns:', selectedColumns);
6931
- console.log('Joins:', joins);
6932
- console.log('Filter Criteria:', filterCriteria);
6933
- console.log(
6934
- 'Flattened Filters:',
6935
- flattenFilterCriteria(filterCriteria)
6936
- );
6937
- console.log('Sort Criteria:', sortCriteria);
6938
- console.log(
6939
- '🚀 Final Payload:',
6940
- JSON.stringify(exportData, null, 2)
6941
- );
6942
- console.groupEnd();
6943
-
6944
6986
  // Generate filename with timestamp
6945
6987
  const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
6946
6988
  const fileName = `${reportName.replace(
@@ -6948,109 +6990,57 @@ const GenericReportImproved = ({
6948
6990
  '_'
6949
6991
  )}_${timestamp}.${format === 'excel' ? 'xlsx' : format}`;
6950
6992
 
6951
- console.log('📁 Generated filename:', fileName);
6952
-
6953
6993
  // Priority: custom export URL > template export URL > default export URL
6954
6994
  const finalExportUrl =
6955
6995
  customUrls.exportUrl ||
6956
6996
  selectedTemplate?.exportUrl ||
6957
6997
  exportUrl;
6958
6998
 
6959
- console.log('🔍 [DEBUG] Export API Call:', {
6960
- selectedTemplate: selectedTemplate?.id,
6961
- customExportUrl: customUrls.exportUrl,
6962
- templateExportUrl: selectedTemplate?.exportUrl,
6963
- defaultExportUrl: exportUrl,
6964
- finalExportUrl,
6965
- usingCustomExportUrl: !!customUrls.exportUrl,
6966
- usingTemplateExportUrl: !!selectedTemplate?.exportUrl,
6967
- });
6968
-
6969
6999
  // Use Download component which handles error checking and blob download properly
6970
- console.log('📡 Making request to:', finalExportUrl);
6971
7000
  const result = await Download(finalExportUrl, 'POST', exportData);
6972
7001
 
6973
- console.log('Download component returned result:', result);
6974
-
6975
7002
  if (result && result.data) {
6976
- console.log('Result has data, triggering download');
6977
- console.log('Result data type:', typeof result.data);
6978
- console.log(
6979
- 'Result data instanceof Blob:',
6980
- result.data instanceof Blob
6981
- );
6982
-
6983
- // Use saveAs to trigger download
6984
7003
  saveAs(result.data, fileName);
6985
7004
  toast.success(`Report exported as ${format.toUpperCase()}`);
6986
7005
  } else {
6987
- console.error(' No result data received');
6988
- console.log('Full result object:', result);
7006
+ debugLog('Export returned no data', result);
6989
7007
  toast.error(
6990
7008
  `Failed to export report as ${format.toUpperCase()}`
6991
7009
  );
6992
7010
  }
6993
7011
  } catch (error) {
6994
- // Handle binary error responses properly
6995
- console.group('❌ Export Error Debug');
6996
- console.error('Error object:', error);
6997
- console.log('Error name:', error.name);
6998
- console.log('Error message:', error.message);
6999
- console.log('Error stack:', error.stack);
7000
-
7012
+ // Errors may arrive as binary blobs unwrap them for a readable message
7001
7013
  let errorMessage = `Failed to export report as ${format.toUpperCase()}`;
7002
7014
 
7003
7015
  if (error.response) {
7004
- console.log('📡 Error response received');
7005
- console.log('Response status:', error.response.status);
7006
- console.log('Response statusText:', error.response.statusText);
7007
- console.log('Response headers:', error.response.headers);
7008
- console.log('Response data type:', typeof error.response.data);
7009
- console.log(
7010
- 'Response data instanceof Blob:',
7011
- error.response.data instanceof Blob
7012
- );
7013
- console.log('Raw response data:', error.response.data);
7014
-
7015
7016
  if (error.response.data instanceof Blob) {
7016
7017
  // If error response is a blob, try to read it as text
7017
7018
  try {
7018
- console.log('Attempting to parse blob error response');
7019
7019
  const errorText = await error.response.data.text();
7020
- console.log('Blob content as text:', errorText);
7021
7020
 
7022
7021
  try {
7023
7022
  const errorData = JSON.parse(errorText);
7024
- console.log('Parsed JSON error data:', errorData);
7025
7023
  errorMessage =
7026
7024
  errorData.message ||
7027
7025
  errorData.error ||
7028
7026
  errorMessage;
7029
7027
  } catch (jsonError) {
7030
- console.error(
7031
- ' Failed to parse JSON from blob:',
7028
+ debugLog(
7029
+ 'Export error blob was not JSON:',
7032
7030
  jsonError
7033
7031
  );
7034
7032
  errorMessage = errorText || errorMessage;
7035
7033
  }
7036
7034
  } catch (parseError) {
7037
7035
  // If parsing fails, use default message
7038
- console.error(
7039
- 'Could not parse error response blob:',
7036
+ debugLog(
7037
+ 'Could not read export error response:',
7040
7038
  parseError
7041
7039
  );
7042
7040
  }
7043
7041
  } else if (error.response.data && error.response.data.message) {
7044
- console.log(
7045
- 'Using error.response.data.message:',
7046
- error.response.data.message
7047
- );
7048
7042
  errorMessage = error.response.data.message;
7049
7043
  } else if (error.response.data && error.response.data.error) {
7050
- console.log(
7051
- 'Using error.response.data.error:',
7052
- error.response.data.error
7053
- );
7054
7044
  errorMessage = error.response.data.error;
7055
7045
  }
7056
7046
 
@@ -7061,27 +7051,29 @@ const GenericReportImproved = ({
7061
7051
  • Memory limitations on the server
7062
7052
  • Try reducing the dataset or using Excel/CSV format`;
7063
7053
  }
7064
- } else {
7065
- console.log('❌ No error.response available');
7066
7054
  }
7067
7055
 
7068
- console.log('🔔 Final error message:', errorMessage);
7069
- console.groupEnd();
7070
-
7071
7056
  toast.error(errorMessage);
7072
- console.error('Export error:', error);
7057
+ debugLog('Export error:', error);
7073
7058
  }
7074
7059
  };
7075
7060
 
7076
7061
  // Handle row exclusion modal
7077
- const handleExportWithExclusion = (exportType = 'regular') => {
7078
- if (previewData.length === 0) {
7062
+ const handleExportWithExclusion = (requestedType = 'regular') => {
7063
+ const hasRows =
7064
+ requestedType === 'grouped'
7065
+ ? (groupedData?.groupedData?.groups || []).some(
7066
+ (group) => (group.rows || []).length > 0
7067
+ )
7068
+ : previewData.length > 0;
7069
+
7070
+ if (!hasRows) {
7079
7071
  toast.error('No data available to export');
7080
7072
  return;
7081
7073
  }
7082
7074
 
7083
7075
  setShowRowExclusionModal(true);
7084
- setExportType(exportType);
7076
+ setExportType(requestedType);
7085
7077
  };
7086
7078
 
7087
7079
  const confirmExportWithExclusion = async () => {
@@ -7121,21 +7113,12 @@ const GenericReportImproved = ({
7121
7113
  }
7122
7114
 
7123
7115
  try {
7124
- // Temporarily replace previewData with filtered data for export
7125
- const originalPreviewData = previewData;
7126
- setPreviewData(filteredData);
7127
-
7128
- // Use the existing client-side Excel export functionality
7116
+ // Excluded rows are sent to the server as part of the query payload
7117
+ // (see queryConfig.excludedRows), so the preview state stays untouched.
7129
7118
  await exportReport('excel');
7130
-
7131
- // Restore original data
7132
- setPreviewData(originalPreviewData);
7133
7119
  } catch (error) {
7134
- console.error('Regular export failed:', error);
7120
+ debugLog('Regular export failed:', error);
7135
7121
  toast.error('Export failed. Please try again.');
7136
-
7137
- // Restore original data on error as well
7138
- setPreviewData(previewData);
7139
7122
  }
7140
7123
  };
7141
7124
 
@@ -7145,44 +7128,25 @@ const GenericReportImproved = ({
7145
7128
 
7146
7129
  // Perform the actual grouped export
7147
7130
  const performGroupedExport = async () => {
7148
- if (!selectedTemplate) {
7149
- toast.error('No template selected for export');
7150
- return;
7151
- }
7131
+ // A grouped report can come from a business template or from the
7132
+ // wizard's own grouping step, so fall back through both.
7133
+ const exportTitle =
7134
+ selectedTemplate?.name ||
7135
+ reportName ||
7136
+ (selectedTable ? `${formatName(selectedTable)} Report` : 'Report');
7152
7137
 
7153
- // Debug logging to understand the groupedData state
7154
- console.log('🔍 [DEBUG] Export function called with:', {
7155
- groupedData: groupedData,
7156
- hasGroupedData: !!groupedData,
7157
- hasGroups: !!(groupedData && groupedData.groups),
7158
- groupsLength:
7159
- groupedData && groupedData.groups
7160
- ? groupedData.groups.length
7161
- : 'undefined',
7162
- selectedColumns: selectedColumns,
7163
- selectedTemplate: selectedTemplate?.name,
7164
- });
7165
-
7166
- // Check the correct path: groupedData.groupedData.groups
7167
7138
  const actualGroups = groupedData?.groupedData?.groups;
7168
- if (!groupedData || !actualGroups || actualGroups.length === 0) {
7169
- console.error(' [DEBUG] Export failed - no grouped data:', {
7170
- groupedData: groupedData,
7171
- hasGroupedData: !!groupedData,
7172
- hasGroupedDataProperty: !!(
7173
- groupedData && groupedData.groupedData
7174
- ),
7175
- hasGroups: !!(
7176
- groupedData &&
7177
- groupedData.groupedData &&
7178
- groupedData.groupedData.groups
7179
- ),
7180
- groupsLength: actualGroups ? actualGroups.length : 'undefined',
7181
- });
7139
+ if (!Array.isArray(actualGroups) || actualGroups.length === 0) {
7140
+ debugLog('Grouped export skipped no grouped data:', groupedData);
7182
7141
  toast.error('No grouped data available for export');
7183
7142
  return;
7184
7143
  }
7185
7144
 
7145
+ if (!Array.isArray(selectedColumns) || selectedColumns.length === 0) {
7146
+ toast.error('No fields selected to export');
7147
+ return;
7148
+ }
7149
+
7186
7150
  try {
7187
7151
  toast.info('Generating Excel export...');
7188
7152
 
@@ -7245,7 +7209,7 @@ const GenericReportImproved = ({
7245
7209
 
7246
7210
  // Handle Comments field - strip HTML and formatting
7247
7211
  if (column.displayName === 'Comments' && value) {
7248
- return value
7212
+ return String(value)
7249
7213
  .replace(/• /g, '')
7250
7214
  .replace(/<[^>]*>/g, '')
7251
7215
  .trim();
@@ -7290,13 +7254,18 @@ const GenericReportImproved = ({
7290
7254
  actualGroups.forEach((group, groupIndex) => {
7291
7255
  // Add group header row
7292
7256
  const groupDisplayName =
7293
- selectedTemplate?.grouping?.groupDisplayName || 'Group';
7257
+ selectedTemplate?.grouping?.groupDisplayName ||
7258
+ groupingConfig.groupDisplayName ||
7259
+ 'Group';
7294
7260
  const groupHeaderRow = Array(headers.length).fill('');
7295
7261
  groupHeaderRow[0] = `${groupDisplayName}: ${group.groupDisplayName}`;
7296
7262
  worksheetData.push(groupHeaderRow);
7297
7263
 
7298
7264
  // Add data rows for this group (excluding excluded rows)
7299
- const allRows = [...group.rows, ...(group.emptyRows || [])];
7265
+ const allRows = [
7266
+ ...(group.rows || []),
7267
+ ...(group.emptyRows || []),
7268
+ ];
7300
7269
  allRows.forEach((row, rowIndex) => {
7301
7270
  // Create a unique identifier for the row
7302
7271
  const rowId = row.id || `group-${groupIndex}-row-${rowIndex}`;
@@ -7343,14 +7312,17 @@ const GenericReportImproved = ({
7343
7312
  worksheet['!cols'] = colWidths;
7344
7313
 
7345
7314
  // Add worksheet to workbook
7346
- const sheetName = selectedTemplate.name || 'Grouped Report';
7315
+ // Excel sheet names are capped at 31 chars and reject [ ] : * ? / \\
7316
+ const sheetName =
7317
+ exportTitle.replace(/[[\]:*?/\\]/g, ' ').slice(0, 31) ||
7318
+ 'Grouped Report';
7347
7319
  XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
7348
7320
 
7349
7321
  // Generate file and download
7350
- const filename = `${selectedTemplate.name.replace(
7322
+ const filename = `${exportTitle.replace(
7351
7323
  /[^a-zA-Z0-9]/g,
7352
7324
  '_'
7353
- )}_grouped_report_${new Date().toISOString().split('T')[0]}.xlsx`;
7325
+ )}_grouped_report_${moment().format('YYYY-MM-DD')}.xlsx`;
7354
7326
  const excelBuffer = XLSX.write(workbook, {
7355
7327
  bookType: 'xlsx',
7356
7328
  type: 'array',
@@ -7362,7 +7334,7 @@ const GenericReportImproved = ({
7362
7334
  saveAs(blob, filename);
7363
7335
  toast.success('Excel export completed successfully!');
7364
7336
  } catch (error) {
7365
- console.error('Export error:', error);
7337
+ debugLog('Export error:', error);
7366
7338
  toast.error(`Export failed: ${error.message}`);
7367
7339
  }
7368
7340
  };
@@ -7391,9 +7363,9 @@ const GenericReportImproved = ({
7391
7363
  <div style="text-align: left;">
7392
7364
  ${
7393
7365
  isExistingReport
7394
- ? `<div style="background: #e3f2fd; border: 1px solid #2196f3; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
7395
- <strong><TriangleAlert size={16} style={{ marginRight: '4px', verticalAlign: 'middle' }} />This will overwrite the existing report:</strong><br>
7396
- "${reportName}"
7366
+ ? `<div style="background: var(--bg-color, #f7f7f8); border: 1px solid var(--border-color, #e1e1e1); border-radius: var(--br, 5px); padding: 12px; margin-bottom: 16px;">
7367
+ <strong>This will overwrite the existing report:</strong><br>
7368
+ "${escapeHtml(reportName)}"
7397
7369
  </div>`
7398
7370
  : ''
7399
7371
  }
@@ -7401,14 +7373,16 @@ const GenericReportImproved = ({
7401
7373
  <input
7402
7374
  id="report-name"
7403
7375
  type="text"
7404
- value="${
7376
+ value="${escapeHtml(
7405
7377
  saveAs
7406
7378
  ? `${reportName} (Copy)`
7407
7379
  : reportName ||
7408
- `${formatName(
7409
- selectedTable
7410
- )} Report - ${moment().format('YYYY-MM-DD')}`
7411
- }"
7380
+ `${formatName(
7381
+ selectedTable
7382
+ )} Report - ${moment().format(
7383
+ 'YYYY-MM-DD'
7384
+ )}`
7385
+ )}"
7412
7386
  style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 16px;"
7413
7387
  />
7414
7388
  <label style="display: flex; align-items: center; gap: 8px;">
@@ -7426,9 +7400,16 @@ const GenericReportImproved = ({
7426
7400
  preConfirm: () => {
7427
7401
  const reportNameInput = document.getElementById('report-name');
7428
7402
  const isPublicInput = document.getElementById('is-public');
7403
+ const name = (reportNameInput?.value || '').trim();
7404
+
7405
+ if (!name) {
7406
+ Swal.showValidationMessage('Please enter a report name');
7407
+ return false;
7408
+ }
7409
+
7429
7410
  return {
7430
- name: reportNameInput.value,
7431
- isPublic: isPublicInput.checked,
7411
+ name,
7412
+ isPublic: !!isPublicInput?.checked,
7432
7413
  };
7433
7414
  },
7434
7415
  });
@@ -7498,7 +7479,7 @@ const GenericReportImproved = ({
7498
7479
  }
7499
7480
  } catch (error) {
7500
7481
  toast.error('Failed to save report');
7501
- console.error('Save error:', error);
7482
+ debugLog('Save error:', error);
7502
7483
  }
7503
7484
  }
7504
7485
  };
@@ -7543,10 +7524,14 @@ const GenericReportImproved = ({
7543
7524
  <div className={styles.groupingOptions}>
7544
7525
  {/* Group By Field Selection */}
7545
7526
  <div className={styles.formGroup}>
7546
- <label className={styles.formLabel}>
7527
+ <label
7528
+ className={styles.formLabel}
7529
+ htmlFor="grouping-field"
7530
+ >
7547
7531
  Group By Field *
7548
7532
  </label>
7549
7533
  <select
7534
+ id="grouping-field"
7550
7535
  value={groupingConfig.groupByField}
7551
7536
  onChange={(e) => {
7552
7537
  const selectedOption =
@@ -7599,10 +7584,14 @@ const GenericReportImproved = ({
7599
7584
 
7600
7585
  {/* Group Display Name */}
7601
7586
  <div className={styles.formGroup}>
7602
- <label className={styles.formLabel}>
7587
+ <label
7588
+ className={styles.formLabel}
7589
+ htmlFor="grouping-display-name"
7590
+ >
7603
7591
  Group Display Name
7604
7592
  </label>
7605
7593
  <input
7594
+ id="grouping-display-name"
7606
7595
  type="text"
7607
7596
  value={groupingConfig.groupDisplayName}
7608
7597
  onChange={(e) =>
@@ -7628,10 +7617,14 @@ const GenericReportImproved = ({
7628
7617
 
7629
7618
  <div className={styles.optionsGrid}>
7630
7619
  <div className={styles.formGroup}>
7631
- <label className={styles.formLabel}>
7620
+ <label
7621
+ className={styles.formLabel}
7622
+ htmlFor="grouping-rows-per-group"
7623
+ >
7632
7624
  Rows Per Group
7633
7625
  </label>
7634
7626
  <input
7627
+ id="grouping-rows-per-group"
7635
7628
  type="number"
7636
7629
  min="1"
7637
7630
  max="100"
@@ -7650,10 +7643,14 @@ const GenericReportImproved = ({
7650
7643
  </div>
7651
7644
 
7652
7645
  <div className={styles.formGroup}>
7653
- <label className={styles.formLabel}>
7646
+ <label
7647
+ className={styles.formLabel}
7648
+ htmlFor="grouping-header-style"
7649
+ >
7654
7650
  Group Header Style
7655
7651
  </label>
7656
7652
  <select
7653
+ id="grouping-header-style"
7657
7654
  value={
7658
7655
  groupingConfig.groupHeaderStyle
7659
7656
  }
@@ -7682,10 +7679,14 @@ const GenericReportImproved = ({
7682
7679
  </div>
7683
7680
 
7684
7681
  <div className={styles.formGroup}>
7685
- <label className={styles.formLabel}>
7682
+ <label
7683
+ className={styles.formLabel}
7684
+ htmlFor="grouping-sort-direction"
7685
+ >
7686
7686
  Sort Direction
7687
7687
  </label>
7688
7688
  <select
7689
+ id="grouping-sort-direction"
7689
7690
  value={groupingConfig.sortDirection}
7690
7691
  onChange={(e) =>
7691
7692
  setGroupingConfig({
@@ -7815,10 +7816,14 @@ const GenericReportImproved = ({
7815
7816
 
7816
7817
  <div className={styles.optionsGrid}>
7817
7818
  <div className={styles.formGroup}>
7818
- <label className={styles.formLabel}>
7819
+ <label
7820
+ className={styles.formLabel}
7821
+ htmlFor="custom-execute-url"
7822
+ >
7819
7823
  Custom Execute URL
7820
7824
  </label>
7821
7825
  <input
7826
+ id="custom-execute-url"
7822
7827
  type="text"
7823
7828
  value={customUrls.executeUrl}
7824
7829
  onChange={(e) =>
@@ -7837,10 +7842,14 @@ const GenericReportImproved = ({
7837
7842
  </div>
7838
7843
 
7839
7844
  <div className={styles.formGroup}>
7840
- <label className={styles.formLabel}>
7845
+ <label
7846
+ className={styles.formLabel}
7847
+ htmlFor="custom-export-url"
7848
+ >
7841
7849
  Custom Export URL
7842
7850
  </label>
7843
7851
  <input
7852
+ id="custom-export-url"
7844
7853
  type="text"
7845
7854
  value={customUrls.exportUrl}
7846
7855
  onChange={(e) =>
@@ -7865,17 +7874,30 @@ const GenericReportImproved = ({
7865
7874
  );
7866
7875
  };
7867
7876
 
7877
+ // Column set for the preview table. The response keys are authoritative —
7878
+ // gridColumns is only a fallback for the "no rows yet" case.
7879
+ const previewColumns = useMemo(() => {
7880
+ if (previewData.length > 0 && previewData[0]) {
7881
+ return Object.keys(previewData[0]).map((key) => ({
7882
+ name: key,
7883
+ header: key.replace(/\./g, ' '),
7884
+ }));
7885
+ }
7886
+ return Array.isArray(gridColumns) ? gridColumns : [];
7887
+ }, [previewData, gridColumns]);
7888
+
7868
7889
  // Render preview
7869
7890
  const renderPreview = () => {
7870
7891
  return (
7871
7892
  <div className={styles.previewSection}>
7872
7893
  <div className={styles.previewActions}>
7873
7894
  <button
7895
+ type="button"
7874
7896
  className={`${styles.btn} ${styles.btnPrimary}`}
7875
7897
  onClick={() => {
7876
7898
  resetPagination();
7877
7899
  // Don't reset hasAutoExecuted to avoid triggering useEffect
7878
- executeReport();
7900
+ executeReport({ page: 1 });
7879
7901
  }}
7880
7902
  disabled={isLoading}
7881
7903
  >
@@ -7888,6 +7910,7 @@ const GenericReportImproved = ({
7888
7910
  <div className={styles.viewToggle}>
7889
7911
  <span className={styles.viewLabel}>View:</span>
7890
7912
  <button
7913
+ type="button"
7891
7914
  className={`${styles.btn} ${
7892
7915
  styles.btnSecondary
7893
7916
  } ${groupedData ? styles.active : ''}`}
@@ -7903,9 +7926,10 @@ const GenericReportImproved = ({
7903
7926
  disabled={isLoading}
7904
7927
  title="Show grouped report"
7905
7928
  >
7906
- 📊 Grouped
7929
+ Grouped
7907
7930
  </button>
7908
7931
  <button
7932
+ type="button"
7909
7933
  className={`${styles.btn} ${
7910
7934
  styles.btnSecondary
7911
7935
  } ${!groupedData ? styles.active : ''}`}
@@ -7921,28 +7945,20 @@ const GenericReportImproved = ({
7921
7945
  disabled={isLoading}
7922
7946
  title="Show regular table"
7923
7947
  >
7924
- 📋 Table
7948
+ Table
7925
7949
  </button>
7926
7950
  </div>
7927
7951
  )}
7928
7952
  </div>
7929
7953
 
7954
+ {isLoading && (
7955
+ <div className={styles.loading}>
7956
+ Running your report...
7957
+ </div>
7958
+ )}
7959
+
7930
7960
  {/* Render grouped report if available */}
7931
- {(() => {
7932
- console.log(
7933
- '🎯 [DEBUG] Render check - groupedData:',
7934
- groupedData
7935
- );
7936
- console.log(
7937
- '🎯 [DEBUG] Render check - groupedData?.grouped:',
7938
- groupedData?.grouped
7939
- );
7940
- console.log(
7941
- '🎯 [DEBUG] Render check - selectedColumns:',
7942
- selectedColumns
7943
- );
7944
- return groupedData && groupedData.grouped;
7945
- })() && (
7961
+ {!isLoading && groupedData?.grouped && groupedData.groupedData && (
7946
7962
  <div className={styles.groupedReportContainer}>
7947
7963
  <GroupedReportRenderer
7948
7964
  data={{
@@ -7955,15 +7971,13 @@ const GenericReportImproved = ({
7955
7971
  : selectedTemplate?.grouping || {}
7956
7972
  }
7957
7973
  columns={selectedColumns}
7958
- onExport={(data) => {
7959
- handleGroupedExport();
7960
- }}
7974
+ onExport={handleGroupedExport}
7961
7975
  />
7962
7976
  </div>
7963
7977
  )}
7964
7978
 
7965
7979
  {/* Render regular grid if no grouping */}
7966
- {!groupedData && previewData.length > 0 && (
7980
+ {!isLoading && !groupedData && previewData.length > 0 && (
7967
7981
  <>
7968
7982
  <div className={styles.gridContainer}>
7969
7983
  <div className="grouped-report">
@@ -7979,6 +7993,7 @@ const GenericReportImproved = ({
7979
7993
  </div>
7980
7994
  <div className="grouped-report-actions">
7981
7995
  <button
7996
+ type="button"
7982
7997
  className="btn btn-export"
7983
7998
  onClick={() =>
7984
7999
  handleRegularExport()
@@ -7997,67 +8012,27 @@ const GenericReportImproved = ({
7997
8012
  <table className="grouped-table">
7998
8013
  <thead>
7999
8014
  <tr>
8000
- {(() => {
8001
- const headerColumns =
8002
- previewData.length >
8003
- 0
8004
- ? Object.keys(
8005
- previewData[0]
8006
- ).map(
8007
- (
8008
- key
8009
- ) => ({
8010
- name: key,
8011
- header: key.replace(
8012
- /\./g,
8013
- ' '
8014
- ),
8015
- })
8016
- )
8017
- : gridColumns;
8018
- return headerColumns.map(
8019
- (col, index) => (
8020
- <th
8021
- key={index}
8022
- className="table-header"
8023
- >
8024
- {col.header ||
8025
- col.name}
8026
- </th>
8027
- )
8028
- );
8029
- })()}
8015
+ {previewColumns.map(
8016
+ (col, index) => (
8017
+ <th
8018
+ key={index}
8019
+ className="table-header"
8020
+ >
8021
+ {col.header ||
8022
+ col.name}
8023
+ </th>
8024
+ )
8025
+ )}
8030
8026
  </tr>
8031
8027
  </thead>
8032
8028
  <tbody>
8033
- {(() => {
8034
- console.log(
8035
- `🎯 [DEBUG] Rendering ${previewData.length} table rows`
8036
- );
8037
- // Always use response keys as fallback instead of gridColumns that might have wrong names
8038
- const actualColumns =
8039
- previewData.length > 0
8040
- ? Object.keys(
8041
- previewData[0]
8042
- ).map((key) => ({
8043
- name: key,
8044
- header: key.replace(
8045
- /\./g,
8046
- ' '
8047
- ),
8048
- }))
8049
- : gridColumns;
8050
- console.log(
8051
- `🎯 [DEBUG] Using columns:`,
8052
- actualColumns
8053
- );
8054
- return previewData.map(
8055
- (row, rowIndex) => (
8029
+ {previewData.map(
8030
+ (row, rowIndex) => (
8056
8031
  <tr
8057
8032
  key={rowIndex}
8058
8033
  className="data-row"
8059
8034
  >
8060
- {actualColumns.map(
8035
+ {previewColumns.map(
8061
8036
  (
8062
8037
  col,
8063
8038
  colIndex
@@ -8522,8 +8497,7 @@ const GenericReportImproved = ({
8522
8497
  )}
8523
8498
  </tr>
8524
8499
  )
8525
- );
8526
- })()}
8500
+ )}
8527
8501
  </tbody>
8528
8502
  </table>
8529
8503
  </div>
@@ -8566,8 +8540,11 @@ const GenericReportImproved = ({
8566
8540
  styles.pageSizeSelector
8567
8541
  }
8568
8542
  >
8569
- <label>Show:</label>
8543
+ <label htmlFor="preview-page-size">
8544
+ Show:
8545
+ </label>
8570
8546
  <select
8547
+ id="preview-page-size"
8571
8548
  value={pageSize}
8572
8549
  onChange={(e) => {
8573
8550
  const newPageSize =
@@ -8578,13 +8555,15 @@ const GenericReportImproved = ({
8578
8555
  setPageSize(
8579
8556
  newPageSize
8580
8557
  );
8581
- setCurrentPage(1); // Reset to first page
8582
- // Re-execute the report with new page size
8558
+ setCurrentPage(1);
8583
8559
  if (
8584
8560
  previewData.length >
8585
8561
  0
8586
8562
  ) {
8587
- executeReport();
8563
+ executeReport({
8564
+ page: 1,
8565
+ size: newPageSize,
8566
+ });
8588
8567
  }
8589
8568
  }}
8590
8569
  className={
@@ -8616,6 +8595,7 @@ const GenericReportImproved = ({
8616
8595
  }
8617
8596
  >
8618
8597
  <button
8598
+ type="button"
8619
8599
  onClick={() => {
8620
8600
  if (
8621
8601
  currentPage > 1
@@ -8626,7 +8606,9 @@ const GenericReportImproved = ({
8626
8606
  setCurrentPage(
8627
8607
  newPage
8628
8608
  );
8629
- executeReport();
8609
+ executeReport({
8610
+ page: newPage,
8611
+ });
8630
8612
  }
8631
8613
  }}
8632
8614
  disabled={
@@ -8694,12 +8676,17 @@ const GenericReportImproved = ({
8694
8676
  if (startPage > 1) {
8695
8677
  pages.push(
8696
8678
  <button
8679
+ type="button"
8697
8680
  key={1}
8698
8681
  onClick={() => {
8699
8682
  setCurrentPage(
8700
8683
  1
8701
8684
  );
8702
- executeReport();
8685
+ executeReport(
8686
+ {
8687
+ page: 1,
8688
+ }
8689
+ );
8703
8690
  }}
8704
8691
  className={`${
8705
8692
  styles.pageBtn
@@ -8739,12 +8726,17 @@ const GenericReportImproved = ({
8739
8726
  ) {
8740
8727
  pages.push(
8741
8728
  <button
8729
+ type="button"
8742
8730
  key={i}
8743
8731
  onClick={() => {
8744
8732
  setCurrentPage(
8745
8733
  i
8746
8734
  );
8747
- executeReport();
8735
+ executeReport(
8736
+ {
8737
+ page: i,
8738
+ }
8739
+ );
8748
8740
  }}
8749
8741
  className={`${
8750
8742
  styles.pageBtn
@@ -8783,6 +8775,7 @@ const GenericReportImproved = ({
8783
8775
  }
8784
8776
  pages.push(
8785
8777
  <button
8778
+ type="button"
8786
8779
  key={
8787
8780
  totalPages
8788
8781
  }
@@ -8790,7 +8783,11 @@ const GenericReportImproved = ({
8790
8783
  setCurrentPage(
8791
8784
  totalPages
8792
8785
  );
8793
- executeReport();
8786
+ executeReport(
8787
+ {
8788
+ page: totalPages,
8789
+ }
8790
+ );
8794
8791
  }}
8795
8792
  className={`${
8796
8793
  styles.pageBtn
@@ -8813,6 +8810,7 @@ const GenericReportImproved = ({
8813
8810
  </div>
8814
8811
 
8815
8812
  <button
8813
+ type="button"
8816
8814
  onClick={() => {
8817
8815
  const totalPages =
8818
8816
  Math.ceil(
@@ -8829,7 +8827,9 @@ const GenericReportImproved = ({
8829
8827
  setCurrentPage(
8830
8828
  newPage
8831
8829
  );
8832
- executeReport();
8830
+ executeReport({
8831
+ page: newPage,
8832
+ });
8833
8833
  }
8834
8834
  }}
8835
8835
  disabled={
@@ -8870,6 +8870,7 @@ const GenericReportImproved = ({
8870
8870
  </p>
8871
8871
  <div className={styles.saveButtons}>
8872
8872
  <button
8873
+ type="button"
8873
8874
  className={`${styles.btn} ${styles.btnSuccess}`}
8874
8875
  onClick={() => saveReport(false)}
8875
8876
  style={{ marginRight: '10px' }}
@@ -8881,6 +8882,7 @@ const GenericReportImproved = ({
8881
8882
  </button>
8882
8883
  {loadedReportId && (
8883
8884
  <button
8885
+ type="button"
8884
8886
  className={`${styles.btn} ${styles.btnSecondary}`}
8885
8887
  onClick={() => saveReport(true)}
8886
8888
  >
@@ -8939,6 +8941,7 @@ const GenericReportImproved = ({
8939
8941
  <div className={styles.headerLeft}>
8940
8942
  <h1>Report Builder</h1>
8941
8943
  <button
8944
+ type="button"
8942
8945
  className={styles.helpButton}
8943
8946
  onClick={showGuidedHelp}
8944
8947
  >
@@ -8953,13 +8956,31 @@ const GenericReportImproved = ({
8953
8956
 
8954
8957
  {/* Calculated Field Modal */}
8955
8958
  {showCalculatedFieldModal && (
8956
- <div className={styles.modalOverlay}>
8957
- <div className={styles.calculatedFieldModal}>
8959
+ <div
8960
+ className={styles.modalOverlay}
8961
+ onMouseDown={(e) => {
8962
+ if (e.target === e.currentTarget) {
8963
+ handleCalculatedFieldCancel();
8964
+ }
8965
+ }}
8966
+ >
8967
+ <div
8968
+ className={styles.calculatedFieldModal}
8969
+ role="dialog"
8970
+ aria-modal="true"
8971
+ aria-labelledby="calculated-field-modal-title"
8972
+ ref={calculatedFieldModalRef}
8973
+ tabIndex={-1}
8974
+ >
8958
8975
  <div className={styles.modalHeader}>
8959
- <h3>Add Calculated Field</h3>
8976
+ <h3 id="calculated-field-modal-title">
8977
+ Add Calculated Field
8978
+ </h3>
8960
8979
  <button
8980
+ type="button"
8961
8981
  className={styles.modalCloseButton}
8962
8982
  onClick={handleCalculatedFieldCancel}
8983
+ aria-label="Close"
8963
8984
  >
8964
8985
 
8965
8986
  </button>
@@ -9056,12 +9077,14 @@ const GenericReportImproved = ({
9056
9077
 
9057
9078
  <div className={styles.modalFooter}>
9058
9079
  <button
9080
+ type="button"
9059
9081
  className={styles.cancelButton}
9060
9082
  onClick={handleCalculatedFieldCancel}
9061
9083
  >
9062
9084
  Cancel
9063
9085
  </button>
9064
9086
  <button
9087
+ type="button"
9065
9088
  className={styles.addButton}
9066
9089
  onClick={handleCalculatedFieldSubmit}
9067
9090
  >
@@ -9074,14 +9097,32 @@ const GenericReportImproved = ({
9074
9097
 
9075
9098
  {/* Row Exclusion Modal */}
9076
9099
  {showRowExclusionModal && (
9077
- <div className={styles.modalOverlay}>
9078
- <div className={styles.rowExclusionModal}>
9100
+ <div
9101
+ className={styles.modalOverlay}
9102
+ onMouseDown={(e) => {
9103
+ if (e.target === e.currentTarget) {
9104
+ setShowRowExclusionModal(false);
9105
+ }
9106
+ }}
9107
+ >
9108
+ <div
9109
+ className={styles.rowExclusionModal}
9110
+ role="dialog"
9111
+ aria-modal="true"
9112
+ aria-labelledby="row-exclusion-modal-title"
9113
+ ref={rowExclusionModalRef}
9114
+ tabIndex={-1}
9115
+ >
9079
9116
  <div className={styles.modalHeader}>
9080
- <h3>Select Rows to Exclude</h3>
9117
+ <h3 id="row-exclusion-modal-title">
9118
+ Select Rows to Exclude
9119
+ </h3>
9081
9120
  <p>Choose which rows you don't want to include in the export</p>
9082
9121
  <button
9122
+ type="button"
9083
9123
  className={styles.modalCloseButton}
9084
9124
  onClick={() => setShowRowExclusionModal(false)}
9125
+ aria-label="Close"
9085
9126
  >
9086
9127
 
9087
9128
  </button>
@@ -9171,12 +9212,14 @@ const GenericReportImproved = ({
9171
9212
 
9172
9213
  <div className={styles.modalFooter}>
9173
9214
  <button
9215
+ type="button"
9174
9216
  className={styles.cancelButton}
9175
9217
  onClick={() => setShowRowExclusionModal(false)}
9176
9218
  >
9177
9219
  Cancel
9178
9220
  </button>
9179
9221
  <button
9222
+ type="button"
9180
9223
  className={styles.confirmButton}
9181
9224
  onClick={confirmExportWithExclusion}
9182
9225
  >