@visns-studio/visns-components 5.14.10 → 5.14.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2889,7 +2889,7 @@ Response:
2889
2889
 
2890
2890
  ### Report Builder Component
2891
2891
 
2892
- #### GenericReport (Enhanced v5.10.11+)
2892
+ #### GenericReport (Enhanced v5.12.2+)
2893
2893
 
2894
2894
  A powerful and enhanced report building component that provides an intuitive wizard-based interface for creating custom reports. The component has been significantly improved with new features and better user experience.
2895
2895
 
@@ -2909,16 +2909,19 @@ A powerful and enhanced report building component that provides an intuitive wiz
2909
2909
 
2910
2910
  **Major Enhancements:**
2911
2911
 
2912
- - **5-Step Wizard Interface**: Guided workflow with visual progress indicators
2912
+ - **6-Step Wizard Interface**: Guided workflow with visual progress indicators
2913
2913
  - **Business Templates**: Pre-built report templates for common business scenarios
2914
+ - **Custom Grouping**: Create grouped reports with flexible configuration options
2915
+ - **Smart Data Formatting**: Intelligent formatting for JSON, boolean, ABN/ACN, and numeric data
2916
+ - **Custom API Endpoints**: Override default URLs with template or custom endpoints
2914
2917
  - **Calculated Fields**: Create custom formulas and calculations within reports
2915
2918
  - **Smart Column Selection**: Automatic suggestions based on table relationships
2916
2919
  - **Enhanced Filtering**: Advanced filter options with AND/OR logic and grouping
2917
- - **Live Preview**: Real-time preview of report data as you build
2920
+ - **Live Preview**: Real-time preview of report data with grouped/table view toggle
2918
2921
  - **Intelligent Currency Formatting**: Automatic detection and formatting of monetary fields
2919
2922
  - **Expandable Field Summary**: View selected fields in a collapsible overview
2920
- - **Template Management**: Save, load, and share report templates
2921
- - **Multiple Export Formats**: Export to Excel, CSV, PDF with formatting options
2923
+ - **Template Management**: Save, load, and share report templates with custom configurations
2924
+ - **Multiple Export Formats**: Export to Excel, CSV, PDF with proper formatting preservation
2922
2925
 
2923
2926
  **New Features in Recent Updates:**
2924
2927
 
@@ -2960,13 +2963,54 @@ A powerful and enhanced report building component that provides an intuitive wiz
2960
2963
  - Expandable selected fields summary
2961
2964
  - Step-by-step guidance with tooltips and help text
2962
2965
 
2963
- **The 5-Step Wizard Process:**
2966
+ 5. **Custom Grouping & Reporting (v5.12.0+)**:
2967
+ ```jsx
2968
+ // Create custom grouped reports with flexible configuration
2969
+ {
2970
+ enabled: true,
2971
+ groupByField: 'facilities.label',
2972
+ groupDisplayName: 'Facility',
2973
+ sortBy: 'facilities.label',
2974
+ sortDirection: 'asc',
2975
+ rowsPerGroup: 10,
2976
+ showGroupTotals: true,
2977
+ groupHeaderStyle: 'primary'
2978
+ }
2979
+ ```
2980
+ - Build grouped reports with custom field selection
2981
+ - Flexible grouping options with configurable display
2982
+ - Support for both predefined templates and custom grouping
2983
+ - Toggle between grouped and regular table views
2984
+
2985
+ 6. **Smart Data Formatting (v5.12.1+)**:
2986
+ - **JSON Data**: Automatically parses and displays JSON strings in readable key-value format
2987
+ - **Boolean Values**: Shows "Yes/No" with color coding instead of true/false
2988
+ - **Status Columns**: Detects status fields and displays "Active/Inactive" with colors
2989
+ - **ABN/ACN Numbers**: Formats Australian Business/Company Numbers (12 345 678 901 / 123 456 789)
2990
+ - **Numeric Values**: Applies locale-specific number formatting with thousands separators
2991
+ - **Null Values**: Shows em dash (—) with italic styling for empty values
2992
+
2993
+ 7. **Custom API Endpoints (v5.12.2+)**:
2994
+ ```jsx
2995
+ // Override default API endpoints for specialized handling
2996
+ customUrls: {
2997
+ executeUrl: '/api/custom-reports/execute',
2998
+ exportUrl: '/api/custom-reports/export'
2999
+ }
3000
+ ```
3001
+ - **URL Priority**: Custom > Template > Default
3002
+ - **Template Support**: Respects `executeUrl` and `exportUrl` from business templates
3003
+ - **Flexible Configuration**: Override URLs at report level for specialized requirements
3004
+ - **Persistent Settings**: Custom URLs save with reports for reuse
3005
+
3006
+ **The 6-Step Wizard Process:**
2964
3007
 
2965
3008
  1. **Select Data Source**: Choose main table and review business templates
2966
3009
  2. **Choose Columns**: Select specific data fields with drag-and-drop reordering
2967
3010
  3. **Add Relationships**: Connect related tables with automatic join suggestions
2968
3011
  4. **Apply Filters**: Set conditions and date ranges with advanced logic
2969
- 5. **Generate & Export**: Preview results and export in multiple formats
3012
+ 5. **Configure Grouping**: Set up custom grouping options and API endpoints (optional)
3013
+ 6. **Generate & Export**: Preview results and export in multiple formats
2970
3014
 
2971
3015
  **API Integration:**
2972
3016
 
package/package.json CHANGED
@@ -89,7 +89,7 @@
89
89
  "react-dom": "^17.0.0 || ^18.0.0"
90
90
  },
91
91
  "name": "@visns-studio/visns-components",
92
- "version": "5.14.10",
92
+ "version": "5.14.11",
93
93
  "description": "Various packages to assist in the development of our Custom Applications.",
94
94
  "main": "src/index.js",
95
95
  "files": [
@@ -9,6 +9,7 @@ import {
9
9
  getRelationshipPath,
10
10
  DEFAULT_INTELLIGENT_SORTING_CONFIG,
11
11
  } from '../../utils/relationshipSortingUtils';
12
+ import { formatCellContent } from '../generic/shared/formatters';
12
13
 
13
14
  // Stage Toggle Utility Functions
14
15
  const buildToggleUrl = (urlTemplate, stageItem, rowData) => {
@@ -145,10 +146,11 @@ export const renderBooleanColumn = ({
145
146
  name: `${column.type}.${column.id}`,
146
147
  sortable: true,
147
148
  render: ({ data }) => {
148
- const value = data[column.id] ? 'Yes' : 'No';
149
+ const rawValue = data[column.id];
150
+ const formattedValue = formatCellContent(rawValue, { ...column, type: 'boolean' });
149
151
  return (
150
- <CellWithTooltip value={data[column.id]} columnType="boolean">
151
- <span>{value}</span>
152
+ <CellWithTooltip value={rawValue} columnType="boolean">
153
+ <span>{formattedValue}</span>
152
154
  </CellWithTooltip>
153
155
  );
154
156
  },
@@ -202,9 +204,17 @@ export const renderDateColumn = ({
202
204
  moment(data[column.id]).isValid() &&
203
205
  moment(data[column.id]).format('YYYY-MM-DD') !== '1970-01-01'
204
206
  ) {
205
- const value = moment(data[column.id]).format(
206
- column.format || 'DD-MM-YYYY'
207
- );
207
+ // Smart date/datetime detection
208
+ const momentValue = moment(data[column.id]);
209
+ const hasTime = momentValue.format('HH:mm:ss') !== '00:00:00';
210
+
211
+ let format = column.format;
212
+ if (!format) {
213
+ // Auto-detect format based on whether there's a time component
214
+ format = hasTime ? 'DD-MM-YYYY HH:mm' : 'DD-MM-YYYY';
215
+ }
216
+
217
+ const value = momentValue.format(format);
208
218
  let columnStyle = {};
209
219
 
210
220
  if (
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
4
4
  import moment from 'moment';
5
5
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
6
6
  import GroupedReportRenderer from './GroupedReportRenderer';
7
+ import { formatCellContent } from './shared/formatters';
7
8
  import {
8
9
  HelpCircle as Question,
9
10
  Link as LinkChain,
@@ -30,6 +31,7 @@ import {
30
31
  Rss,
31
32
  Globe,
32
33
  Lock,
34
+ Database,
33
35
  } from 'lucide-react';
34
36
  import CustomFetch from '../Fetch';
35
37
  import Download from '../Download';
@@ -1042,7 +1044,7 @@ const GenericReportImproved = ({
1042
1044
 
1043
1045
  // State for pagination - now using server-side pagination
1044
1046
  const [currentPage, setCurrentPage] = useState(1);
1045
- const [pageSize, setPageSize] = useState(25); // Default to 25 for better UX
1047
+ const [pageSize, setPageSize] = useState(10); // Default to 10 to reduce scrolling
1046
1048
  const [totalPages, setTotalPages] = useState(0);
1047
1049
  const [totalCount, setTotalCount] = useState(0); // Total records from server
1048
1050
 
@@ -1416,13 +1418,30 @@ const GenericReportImproved = ({
1416
1418
  currentWizardStep,
1417
1419
  ]);
1418
1420
 
1421
+ // Reset auto-execute flag when user navigates away from preview step
1422
+ useEffect(() => {
1423
+ if (currentWizardStep !== 5) {
1424
+ setHasAutoExecuted(false);
1425
+ }
1426
+ }, [currentWizardStep]);
1427
+
1428
+ // Reset auto-execute flag when report configuration changes
1429
+ useEffect(() => {
1430
+ setHasAutoExecuted(false);
1431
+ }, [
1432
+ selectedTable,
1433
+ selectedColumns.length,
1434
+ filterCriteria,
1435
+ joins.length,
1436
+ groupingConfig,
1437
+ ]);
1438
+
1419
1439
  // Auto-execute report when user reaches step 5 (preview step)
1420
1440
  useEffect(() => {
1421
1441
  if (
1422
1442
  currentWizardStep === 5 && // Step 6 is index 5 (preview step)
1423
1443
  selectedTable &&
1424
1444
  selectedColumns.length > 0 &&
1425
- previewData.length === 0 && // Only if no data loaded yet
1426
1445
  !isLoading &&
1427
1446
  !hasAutoExecuted // Prevent infinite loops
1428
1447
  ) {
@@ -1433,7 +1452,6 @@ const GenericReportImproved = ({
1433
1452
  currentWizardStep,
1434
1453
  selectedTable,
1435
1454
  selectedColumns.length,
1436
- previewData.length,
1437
1455
  isLoading,
1438
1456
  hasAutoExecuted,
1439
1457
  ]);
@@ -2541,16 +2559,16 @@ const GenericReportImproved = ({
2541
2559
  }
2542
2560
  } else {
2543
2561
  // Regular non-grouped report
2544
- const firstPageResult = await dataSource({
2545
- skip: 0,
2562
+ const currentPageResult = await dataSource({
2563
+ skip: (currentPage - 1) * pageSize,
2546
2564
  limit: pageSize,
2547
2565
  sortInfo: null,
2548
2566
  filterValue: null,
2549
2567
  });
2550
2568
 
2551
- if (firstPageResult.data && firstPageResult.data.length > 0) {
2552
- setPreviewData(firstPageResult.data);
2553
- fetchedDataCount = firstPageResult.data.length;
2569
+ if (currentPageResult.data && currentPageResult.data.length > 0) {
2570
+ setPreviewData(currentPageResult.data);
2571
+ fetchedDataCount = currentPageResult.data.length;
2554
2572
  }
2555
2573
  }
2556
2574
 
@@ -2749,10 +2767,9 @@ const GenericReportImproved = ({
2749
2767
  setCurrentWizardStep(wizardSteps.length - 1);
2750
2768
  }
2751
2769
 
2752
- // Show success message with available data count
2753
- console.log('🎯 [DEBUG] Final fetchedDataCount before toast:', fetchedDataCount);
2770
+ // Report executed successfully
2771
+ console.log('🎯 [DEBUG] Final fetchedDataCount:', fetchedDataCount);
2754
2772
  console.log('🎯 [DEBUG] Type of fetchedDataCount:', typeof fetchedDataCount);
2755
- toast.success(`Found ${fetchedDataCount} records`);
2756
2773
  } catch (error) {
2757
2774
  toast.error('Failed to execute report');
2758
2775
  console.error('Error executing report:', error);
@@ -2966,32 +2983,49 @@ const GenericReportImproved = ({
2966
2983
  setSelectedColumns(formattedColumns);
2967
2984
  }
2968
2985
 
2969
- // Apply quick filters as default filter criteria
2970
- if (template.quickFilters) {
2971
- const defaultFilters = template.quickFilters
2972
- .filter(
2973
- (filter) =>
2974
- filter.value && filter.value !== 'current_user'
2975
- ) // Skip user-specific filters for now
2976
- .map((filter) => ({
2977
- id: filter.id,
2978
- field: filter.field,
2979
- operator: filter.operator || '=',
2980
- value: filter.value,
2981
- label: filter.label,
2982
- }));
2986
+ // Apply template quick filters as default filters
2987
+ if (template.quickFilters && template.quickFilters.length > 0) {
2988
+ const updatedFilterCriteria = {
2989
+ operator: 'AND',
2990
+ groups: [
2991
+ {
2992
+ operator: 'AND',
2993
+ filters: [],
2994
+ },
2995
+ ],
2996
+ };
2983
2997
 
2984
- if (defaultFilters.length > 0) {
2985
- setFilterCriteria({
2986
- operator: 'AND',
2987
- groups: [
2988
- {
2989
- operator: 'AND',
2990
- filters: defaultFilters,
2991
- },
2992
- ],
2993
- });
2994
- }
2998
+ template.quickFilters.forEach((quickFilter) => {
2999
+ let filterValue = convertDynamicDateValue(quickFilter.value);
3000
+
3001
+ // Parse the field to extract table and column
3002
+ const fieldParts = quickFilter.field.split('.');
3003
+ const table = fieldParts[0];
3004
+ const column = fieldParts.slice(1).join('.'); // Handle cases like "leads.user.name"
3005
+
3006
+ // Convert date range objects to comma-separated string for BETWEEN operator
3007
+ if (quickFilter.type === 'date_range' && typeof filterValue === 'object' && filterValue.start && filterValue.end) {
3008
+ filterValue = `${filterValue.start},${filterValue.end}`;
3009
+ }
3010
+
3011
+ const newFilter = {
3012
+ id: `template_${quickFilter.id}_${Date.now()}`,
3013
+ field: quickFilter.field,
3014
+ table: table,
3015
+ column: column,
3016
+ operator:
3017
+ quickFilter.operator ||
3018
+ (quickFilter.type === 'date_range' ? 'BETWEEN' : '='),
3019
+ value: filterValue,
3020
+ label: quickFilter.label,
3021
+ type: quickFilter.type,
3022
+ };
3023
+
3024
+ updatedFilterCriteria.groups[0].filters.push(newFilter);
3025
+ });
3026
+
3027
+ setFilterCriteria(updatedFilterCriteria);
3028
+
2995
3029
  }
2996
3030
 
2997
3031
  // Apply template grouping configuration
@@ -4641,32 +4675,26 @@ const GenericReportImproved = ({
4641
4675
  handleRemoveFilterCriterion(groupIndex, filterIndex);
4642
4676
  };
4643
4677
 
4644
- // Apply template-based quick filter
4645
- const applyTemplateQuickFilter = (quickFilter) => {
4646
- let filterValue = quickFilter.value;
4647
-
4648
- // Handle dynamic date ranges
4649
- switch (quickFilter.value) {
4678
+ // Helper function to convert dynamic date values to actual date ranges
4679
+ const convertDynamicDateValue = (value) => {
4680
+ switch (value) {
4650
4681
  case 'current_week':
4651
- filterValue = {
4682
+ return {
4652
4683
  start: moment().startOf('week').format('YYYY-MM-DD'),
4653
4684
  end: moment().endOf('week').format('YYYY-MM-DD'),
4654
4685
  };
4655
- break;
4656
4686
  case 'next_30_days':
4657
- filterValue = {
4687
+ return {
4658
4688
  start: moment().format('YYYY-MM-DD'),
4659
4689
  end: moment().add(30, 'days').format('YYYY-MM-DD'),
4660
4690
  };
4661
- break;
4662
4691
  case 'last_30_days':
4663
- filterValue = {
4692
+ return {
4664
4693
  start: moment().subtract(30, 'days').format('YYYY-MM-DD'),
4665
4694
  end: moment().format('YYYY-MM-DD'),
4666
4695
  };
4667
- break;
4668
4696
  case 'last_quarter':
4669
- filterValue = {
4697
+ return {
4670
4698
  start: moment()
4671
4699
  .subtract(1, 'quarter')
4672
4700
  .startOf('quarter')
@@ -4676,9 +4704,8 @@ const GenericReportImproved = ({
4676
4704
  .endOf('quarter')
4677
4705
  .format('YYYY-MM-DD'),
4678
4706
  };
4679
- break;
4680
4707
  case 'last_year':
4681
- filterValue = {
4708
+ return {
4682
4709
  start: moment()
4683
4710
  .subtract(1, 'year')
4684
4711
  .startOf('year')
@@ -4688,12 +4715,44 @@ const GenericReportImproved = ({
4688
4715
  .endOf('year')
4689
4716
  .format('YYYY-MM-DD'),
4690
4717
  };
4691
- break;
4718
+ case 'today':
4719
+ return moment().format('YYYY-MM-DD');
4720
+ case 'yesterday':
4721
+ return moment().subtract(1, 'day').format('YYYY-MM-DD');
4722
+ case 'current_month':
4723
+ return {
4724
+ start: moment().startOf('month').format('YYYY-MM-DD'),
4725
+ end: moment().endOf('month').format('YYYY-MM-DD'),
4726
+ };
4727
+ case 'current_year':
4728
+ return {
4729
+ start: moment().startOf('year').format('YYYY-MM-DD'),
4730
+ end: moment().endOf('year').format('YYYY-MM-DD'),
4731
+ };
4732
+ default:
4733
+ return value; // Return original value if no conversion needed
4734
+ }
4735
+ };
4736
+
4737
+ // Apply template-based quick filter
4738
+ const applyTemplateQuickFilter = (quickFilter) => {
4739
+ let filterValue = convertDynamicDateValue(quickFilter.value);
4740
+
4741
+ // Parse the field to extract table and column
4742
+ const fieldParts = quickFilter.field.split('.');
4743
+ const table = fieldParts[0];
4744
+ const column = fieldParts.slice(1).join('.'); // Handle cases like "leads.user.name"
4745
+
4746
+ // Convert date range objects to comma-separated string for BETWEEN operator
4747
+ if (quickFilter.type === 'date_range' && typeof filterValue === 'object' && filterValue.start && filterValue.end) {
4748
+ filterValue = `${filterValue.start},${filterValue.end}`;
4692
4749
  }
4693
4750
 
4694
4751
  const newFilter = {
4695
4752
  id: `template_${quickFilter.id}_${Date.now()}`,
4696
4753
  field: quickFilter.field,
4754
+ table: table,
4755
+ column: column,
4697
4756
  operator:
4698
4757
  quickFilter.operator ||
4699
4758
  (quickFilter.type === 'date_range' ? 'BETWEEN' : '='),
@@ -5510,6 +5569,7 @@ const GenericReportImproved = ({
5510
5569
  suggestions: smartSuggestions,
5511
5570
  });
5512
5571
 
5572
+
5513
5573
  return (
5514
5574
  <div className={styles.filtersSection}>
5515
5575
  {/* Template Quick Filters */}
@@ -7039,49 +7099,51 @@ const GenericReportImproved = ({
7039
7099
  </div>
7040
7100
  )}
7041
7101
 
7042
- {/* Custom URLs Configuration */}
7043
- <div className={styles.customUrlsSection}>
7044
- <h4 className={styles.sectionTitle}>Custom API Endpoints (Optional)</h4>
7045
- <p className={styles.helpText}>
7046
- Override default API endpoints for specialized report handling
7047
- </p>
7048
-
7049
- <div className={styles.optionsGrid}>
7050
- <div className={styles.formGroup}>
7051
- <label className={styles.formLabel}>Custom Execute URL</label>
7052
- <input
7053
- type="text"
7054
- value={customUrls.executeUrl}
7055
- onChange={(e) => setCustomUrls({
7056
- ...customUrls,
7057
- executeUrl: e.target.value
7058
- })}
7059
- placeholder="/api/custom-reports/execute"
7060
- className={styles.formInput}
7061
- />
7062
- <p className={styles.fieldHelp}>
7063
- Custom endpoint for report execution (overrides template and default URLs)
7064
- </p>
7065
- </div>
7102
+ {/* Custom URLs Configuration - Only show when grouping is enabled */}
7103
+ {groupingConfig.enabled && (
7104
+ <div className={styles.customUrlsSection}>
7105
+ <h4 className={styles.sectionTitle}>Custom API Endpoints (Optional)</h4>
7106
+ <p className={styles.helpText}>
7107
+ Override default API endpoints for specialized report handling
7108
+ </p>
7066
7109
 
7067
- <div className={styles.formGroup}>
7068
- <label className={styles.formLabel}>Custom Export URL</label>
7069
- <input
7070
- type="text"
7071
- value={customUrls.exportUrl}
7072
- onChange={(e) => setCustomUrls({
7073
- ...customUrls,
7074
- exportUrl: e.target.value
7075
- })}
7076
- placeholder="/api/custom-reports/export"
7077
- className={styles.formInput}
7078
- />
7079
- <p className={styles.fieldHelp}>
7080
- Custom endpoint for report exports (for server-side exports)
7081
- </p>
7110
+ <div className={styles.optionsGrid}>
7111
+ <div className={styles.formGroup}>
7112
+ <label className={styles.formLabel}>Custom Execute URL</label>
7113
+ <input
7114
+ type="text"
7115
+ value={customUrls.executeUrl}
7116
+ onChange={(e) => setCustomUrls({
7117
+ ...customUrls,
7118
+ executeUrl: e.target.value
7119
+ })}
7120
+ placeholder="/api/custom-reports/execute"
7121
+ className={styles.formInput}
7122
+ />
7123
+ <p className={styles.fieldHelp}>
7124
+ Custom endpoint for report execution (overrides template and default URLs)
7125
+ </p>
7126
+ </div>
7127
+
7128
+ <div className={styles.formGroup}>
7129
+ <label className={styles.formLabel}>Custom Export URL</label>
7130
+ <input
7131
+ type="text"
7132
+ value={customUrls.exportUrl}
7133
+ onChange={(e) => setCustomUrls({
7134
+ ...customUrls,
7135
+ exportUrl: e.target.value
7136
+ })}
7137
+ placeholder="/api/custom-reports/export"
7138
+ className={styles.formInput}
7139
+ />
7140
+ <p className={styles.fieldHelp}>
7141
+ Custom endpoint for report exports (for server-side exports)
7142
+ </p>
7143
+ </div>
7082
7144
  </div>
7083
7145
  </div>
7084
- </div>
7146
+ )}
7085
7147
  </div>
7086
7148
  </div>
7087
7149
  );
@@ -7279,13 +7341,60 @@ const GenericReportImproved = ({
7279
7341
  <span style={{color: '#6c757d', fontWeight: '500'}}>No</span>;
7280
7342
  }
7281
7343
 
7282
- // Handle numeric values
7344
+ // Handle numeric values (but exclude potential boolean columns)
7283
7345
  if (typeof value === 'number' && !col.name.toLowerCase().includes('status')) {
7284
- return value.toLocaleString();
7346
+ const columnName = col.name.toLowerCase();
7347
+ // Don't format as numbers if they might be boolean values
7348
+ const mightBeBoolean = (value === 0 || value === 1) && (
7349
+ columnName.includes('is_') || columnName.includes('has_') || columnName.includes('confirmed') ||
7350
+ columnName.includes('active') || columnName.includes('enabled') || columnName.includes('completed') ||
7351
+ columnName.includes('stage') || columnName.includes('final') || columnName.includes('flag') ||
7352
+ columnName.includes('approved') || columnName.includes('verified') || columnName.includes('sent') ||
7353
+ columnName.endsWith('?')
7354
+ );
7355
+
7356
+ if (!mightBeBoolean) {
7357
+ return value.toLocaleString();
7358
+ }
7285
7359
  }
7286
7360
 
7287
- // Default string handling
7288
- return value;
7361
+ // Use formatCellContent for all other values including dates and smart boolean formatting
7362
+ const formattedValue = formatCellContent(value, {
7363
+ id: col.name,
7364
+ name: col.name,
7365
+ // Get type from template column definition if available, otherwise infer
7366
+ type: (() => {
7367
+ // First try to find type from template's defaultColumns
7368
+ if (selectedTemplate?.defaultColumns) {
7369
+ const templateColumn = selectedTemplate.defaultColumns.find(
7370
+ tc => `${tc.table}.${tc.column}` === col.name || tc.column === col.name
7371
+ );
7372
+ if (templateColumn?.type) {
7373
+ return templateColumn.type;
7374
+ }
7375
+ }
7376
+
7377
+ // Fallback to column name-based type inference
7378
+ const columnName = col.name.toLowerCase();
7379
+ // Date column detection
7380
+ if (columnName.includes('date') || columnName.includes('time') || columnName.includes('created_at') || columnName.includes('updated_at')) {
7381
+ return 'date';
7382
+ }
7383
+ // Boolean column detection - enhanced logic
7384
+ if (columnName.includes('is_') || columnName.includes('has_') || columnName.includes('confirmed') ||
7385
+ columnName.includes('active') || columnName.includes('enabled') || columnName.includes('completed') ||
7386
+ columnName.includes('stage') || columnName.includes('final') || columnName.includes('flag') ||
7387
+ columnName.includes('approved') || columnName.includes('verified') || columnName.includes('sent') ||
7388
+ columnName.endsWith('?') || // Handle columns like "Confirmed?"
7389
+ (typeof value === 'number' && (value === 0 || value === 1)) ||
7390
+ (typeof value === 'string' && (value === '0' || value === '1'))) {
7391
+ return 'boolean';
7392
+ }
7393
+ return undefined; // Let formatCellContent auto-detect
7394
+ })()
7395
+ });
7396
+
7397
+ return formattedValue;
7289
7398
  })()}
7290
7399
  </td>
7291
7400
  ))}
@@ -7295,6 +7404,148 @@ const GenericReportImproved = ({
7295
7404
  </tbody>
7296
7405
  </table>
7297
7406
  </div>
7407
+
7408
+ {/* Pagination Controls */}
7409
+ {previewData.length > 0 && (
7410
+ <div className={styles.paginationContainer}>
7411
+ <div className={styles.paginationInfo}>
7412
+ {(() => {
7413
+ const startRecord = Math.min((currentPage - 1) * pageSize + 1, totalCount);
7414
+ const endRecord = Math.min(currentPage * pageSize, totalCount);
7415
+ return `Showing ${startRecord}-${endRecord} of ${totalCount} records`;
7416
+ })()}
7417
+ </div>
7418
+
7419
+ <div className={styles.paginationControls}>
7420
+ <div className={styles.pageSizeSelector}>
7421
+ <label>Show:</label>
7422
+ <select
7423
+ value={pageSize}
7424
+ onChange={(e) => {
7425
+ const newPageSize = parseInt(e.target.value);
7426
+ setPageSize(newPageSize);
7427
+ setCurrentPage(1); // Reset to first page
7428
+ // Re-execute the report with new page size
7429
+ if (previewData.length > 0) {
7430
+ executeReport();
7431
+ }
7432
+ }}
7433
+ className={styles.pageSizeSelect}
7434
+ >
7435
+ <option value={10}>10</option>
7436
+ <option value={25}>25</option>
7437
+ <option value={50}>50</option>
7438
+ <option value={100}>100</option>
7439
+ <option value={250}>250</option>
7440
+ </select>
7441
+ <span>records</span>
7442
+ </div>
7443
+
7444
+ <div className={styles.pageNavigation}>
7445
+ <button
7446
+ onClick={() => {
7447
+ if (currentPage > 1) {
7448
+ const newPage = currentPage - 1;
7449
+ setCurrentPage(newPage);
7450
+ executeReport();
7451
+ }
7452
+ }}
7453
+ disabled={currentPage <= 1}
7454
+ className={`${styles.pageBtn} ${currentPage <= 1 ? styles.disabled : ''}`}
7455
+ >
7456
+ ‹ Previous
7457
+ </button>
7458
+
7459
+ <div className={styles.pageNumbers}>
7460
+ {(() => {
7461
+ const maxPagesToShow = 5;
7462
+ const totalPages = Math.ceil(totalCount / pageSize);
7463
+ let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
7464
+ let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
7465
+
7466
+ // Adjust start if we're near the end
7467
+ if (endPage - startPage < maxPagesToShow - 1) {
7468
+ startPage = Math.max(1, endPage - maxPagesToShow + 1);
7469
+ }
7470
+
7471
+ const pages = [];
7472
+
7473
+ // Add first page and ellipsis if needed
7474
+ if (startPage > 1) {
7475
+ pages.push(
7476
+ <button
7477
+ key={1}
7478
+ onClick={() => {
7479
+ setCurrentPage(1);
7480
+ executeReport();
7481
+ }}
7482
+ className={`${styles.pageBtn} ${1 === currentPage ? styles.active : ''}`}
7483
+ >
7484
+ 1
7485
+ </button>
7486
+ );
7487
+ if (startPage > 2) {
7488
+ pages.push(<span key="start-ellipsis" className={styles.ellipsis}>...</span>);
7489
+ }
7490
+ }
7491
+
7492
+ // Add visible page numbers
7493
+ for (let i = startPage; i <= endPage; i++) {
7494
+ pages.push(
7495
+ <button
7496
+ key={i}
7497
+ onClick={() => {
7498
+ setCurrentPage(i);
7499
+ executeReport();
7500
+ }}
7501
+ className={`${styles.pageBtn} ${i === currentPage ? styles.active : ''}`}
7502
+ >
7503
+ {i}
7504
+ </button>
7505
+ );
7506
+ }
7507
+
7508
+ // Add ellipsis and last page if needed
7509
+ if (endPage < totalPages) {
7510
+ if (endPage < totalPages - 1) {
7511
+ pages.push(<span key="end-ellipsis" className={styles.ellipsis}>...</span>);
7512
+ }
7513
+ pages.push(
7514
+ <button
7515
+ key={totalPages}
7516
+ onClick={() => {
7517
+ setCurrentPage(totalPages);
7518
+ executeReport();
7519
+ }}
7520
+ className={`${styles.pageBtn} ${totalPages === currentPage ? styles.active : ''}`}
7521
+ >
7522
+ {totalPages}
7523
+ </button>
7524
+ );
7525
+ }
7526
+
7527
+ return pages;
7528
+ })()}
7529
+ </div>
7530
+
7531
+ <button
7532
+ onClick={() => {
7533
+ const totalPages = Math.ceil(totalCount / pageSize);
7534
+ if (currentPage < totalPages) {
7535
+ const newPage = currentPage + 1;
7536
+ setCurrentPage(newPage);
7537
+ executeReport();
7538
+ }
7539
+ }}
7540
+ disabled={currentPage >= Math.ceil(totalCount / pageSize)}
7541
+ className={`${styles.pageBtn} ${currentPage >= Math.ceil(totalCount / pageSize) ? styles.disabled : ''}`}
7542
+ >
7543
+ Next ›
7544
+ </button>
7545
+ </div>
7546
+ </div>
7547
+ </div>
7548
+ )}
7298
7549
  </div>
7299
7550
  </div>
7300
7551
  </div>
@@ -7332,6 +7583,29 @@ const GenericReportImproved = ({
7332
7583
  </div>
7333
7584
  </>
7334
7585
  )}
7586
+
7587
+ {/* Show no data message when no results found */}
7588
+ {!groupedData && previewData.length === 0 && !isLoading && selectedTable && selectedColumns.length > 0 && (
7589
+ <div className={styles.noDataContainer}>
7590
+ <div className={styles.noDataMessage}>
7591
+ <div className={styles.noDataIcon}>
7592
+ <Database size={48} />
7593
+ </div>
7594
+ <h3 className={styles.noDataTitle}>No Data Found</h3>
7595
+ <p className={styles.noDataText}>
7596
+ No records match the selected filters and criteria.
7597
+ </p>
7598
+ <div className={styles.noDataSuggestions}>
7599
+ <p><strong>Try:</strong></p>
7600
+ <ul>
7601
+ <li>Adjusting or removing filters</li>
7602
+ <li>Selecting a broader date range</li>
7603
+ <li>Checking if data exists for the selected table</li>
7604
+ </ul>
7605
+ </div>
7606
+ </div>
7607
+ </div>
7608
+ )}
7335
7609
  </div>
7336
7610
  );
7337
7611
  };
@@ -2,6 +2,132 @@ import parse from 'html-react-parser';
2
2
  import moment from 'moment';
3
3
  import numeral from 'numeral';
4
4
 
5
+ /**
6
+ * Smart boolean/binary value interpreter based on column name and context
7
+ * @param {string|number} value - The value (0/1, true/false, etc.)
8
+ * @param {Object} column - Column configuration with id/name
9
+ * @returns {string} Human-friendly representation
10
+ */
11
+ const interpretBooleanValue = (value, column) => {
12
+ const columnName = (column.id || column.name || '').toLowerCase();
13
+ const isTrue = value === 1 || value === true || value === 'true' || value === 'yes' || value === 'Yes';
14
+ const isFalse = value === 0 || value === false || value === 'false' || value === 'no' || value === 'No';
15
+
16
+ if (!isTrue && !isFalse) return value; // Return original if not clearly boolean
17
+
18
+ // Contextual interpretations based on column name patterns
19
+ const interpretations = {
20
+ // Status/State patterns
21
+ 'active': { true: 'Active', false: 'Inactive' },
22
+ 'enabled': { true: 'Enabled', false: 'Disabled' },
23
+ 'status': { true: 'Active', false: 'Inactive' },
24
+ 'visible': { true: 'Visible', false: 'Hidden' },
25
+ 'published': { true: 'Published', false: 'Draft' },
26
+ 'public': { true: 'Public', false: 'Private' },
27
+ 'live': { true: 'Live', false: 'Offline' },
28
+
29
+ // Completion/Progress patterns
30
+ 'completed': { true: 'Completed', false: 'Pending' },
31
+ 'finished': { true: 'Finished', false: 'In Progress' },
32
+ 'done': { true: 'Done', false: 'To Do' },
33
+ 'complete': { true: 'Complete', false: 'Incomplete' },
34
+ 'resolved': { true: 'Resolved', false: 'Open' },
35
+ 'closed': { true: 'Closed', false: 'Open' },
36
+
37
+ // Approval/Verification patterns
38
+ 'approved': { true: 'Approved', false: 'Pending' },
39
+ 'verified': { true: 'Verified', false: 'Unverified' },
40
+ 'confirmed': { true: 'Confirmed', false: 'Unconfirmed' },
41
+ 'accepted': { true: 'Accepted', false: 'Rejected' },
42
+ 'valid': { true: 'Valid', false: 'Invalid' },
43
+
44
+ // Access/Permission patterns
45
+ 'allowed': { true: 'Allowed', false: 'Denied' },
46
+ 'permitted': { true: 'Permitted', false: 'Restricted' },
47
+ 'authorized': { true: 'Authorized', false: 'Unauthorized' },
48
+ 'locked': { true: 'Locked', false: 'Unlocked' },
49
+
50
+ // Notification/Communication patterns
51
+ 'sent': { true: 'Sent', false: 'Not Sent' },
52
+ 'delivered': { true: 'Delivered', false: 'Pending' },
53
+ 'read': { true: 'Read', false: 'Unread' },
54
+ 'notified': { true: 'Notified', false: 'Not Notified' },
55
+
56
+ // Business/Financial patterns
57
+ 'paid': { true: 'Paid', false: 'Unpaid' },
58
+ 'invoiced': { true: 'Invoiced', false: 'Not Invoiced' },
59
+ 'billed': { true: 'Billed', false: 'Unbilled' },
60
+ 'taxable': { true: 'Taxable', false: 'Tax Free' },
61
+
62
+ // Quality/Condition patterns
63
+ 'available': { true: 'Available', false: 'Unavailable' },
64
+ 'ready': { true: 'Ready', false: 'Not Ready' },
65
+ 'urgent': { true: 'Urgent', false: 'Normal' },
66
+ 'priority': { true: 'High Priority', false: 'Normal Priority' },
67
+ 'critical': { true: 'Critical', false: 'Normal' },
68
+
69
+ // Agreement/Contract patterns
70
+ 'signed': { true: 'Signed', false: 'Unsigned' },
71
+ 'executed': { true: 'Executed', false: 'Draft' },
72
+ 'expired': { true: 'Expired', false: 'Current' },
73
+
74
+ // Project/Work patterns
75
+ 'assigned': { true: 'Assigned', false: 'Unassigned' },
76
+ 'allocated': { true: 'Allocated', false: 'Available' },
77
+ 'booked': { true: 'Booked', false: 'Available' },
78
+ 'scheduled': { true: 'Scheduled', false: 'Unscheduled' },
79
+ 'stage': { true: 'Completed', false: 'Pending' },
80
+ 'final': { true: 'Completed', false: 'Incomplete' },
81
+ 'finished': { true: 'Finished', false: 'In Progress' },
82
+ 'done': { true: 'Done', false: 'To Do' },
83
+
84
+ // System/Technical patterns
85
+ 'online': { true: 'Online', false: 'Offline' },
86
+ 'connected': { true: 'Connected', false: 'Disconnected' },
87
+ 'synced': { true: 'Synced', false: 'Not Synced' },
88
+ 'backup': { true: 'Backed Up', false: 'Not Backed Up' },
89
+ };
90
+
91
+ // Try to find a match based on column name
92
+ for (const [pattern, values] of Object.entries(interpretations)) {
93
+ if (columnName.includes(pattern)) {
94
+ return isTrue ? values.true : values.false;
95
+ }
96
+ }
97
+
98
+ // Special handling for specific common patterns
99
+ if (columnName.includes('is_') || columnName.startsWith('has_') || columnName.endsWith('_flag')) {
100
+ // Extract the meaningful part
101
+ const meaningfulPart = columnName
102
+ .replace(/^(is_|has_)/, '')
103
+ .replace(/_flag$/, '')
104
+ .replace(/_/g, ' ')
105
+ .replace(/\b\w/g, l => l.toUpperCase()); // Title case
106
+
107
+ return isTrue ? meaningfulPart : `Not ${meaningfulPart}`;
108
+ }
109
+
110
+ // Default fallback - still better than raw 0/1
111
+ return isTrue ? 'Yes' : 'No';
112
+ };
113
+
114
+ /**
115
+ * Auto-detect appropriate date format based on value content
116
+ * @param {*} value - The date value to analyze
117
+ * @param {string} fallbackFormat - Default format if detection fails
118
+ * @returns {string} Appropriate moment.js format string
119
+ */
120
+ export const autoDetectDateFormat = (value, fallbackFormat = 'DD-MM-YYYY') => {
121
+ if (!value || !moment(value).isValid()) {
122
+ return fallbackFormat;
123
+ }
124
+
125
+ const momentValue = moment(value);
126
+ const hasTime = momentValue.format('HH:mm:ss') !== '00:00:00';
127
+
128
+ return hasTime ? 'DD-MM-YYYY HH:mm' : 'DD-MM-YYYY';
129
+ };
130
+
5
131
  /**
6
132
  * Format quarter columns with status, contacts, and reasons in tooltips
7
133
  * @param {*} value - The date value
@@ -29,7 +155,7 @@ const formatQuarterWithStatus = (value, column, row) => {
29
155
  // If status is 'sent' and we have a date
30
156
  if (status === 'sent' && value) {
31
157
  const formattedDate = moment(value).isValid()
32
- ? moment(value).format('DD/MM/YYYY')
158
+ ? moment(value).format('DD-MM-YYYY')
33
159
  : value;
34
160
 
35
161
  const contactCount = Array.isArray(contacts) ? contacts.length : 0;
@@ -71,7 +197,7 @@ const formatQuarterWithStatus = (value, column, row) => {
71
197
  // If we have a date but no clear status (legacy data)
72
198
  if (value) {
73
199
  const formattedDate = moment(value).isValid()
74
- ? moment(value).format('DD/MM/YYYY')
200
+ ? moment(value).format('DD-MM-YYYY')
75
201
  : value;
76
202
 
77
203
  const contactCount = Array.isArray(contacts) ? contacts.length : 0;
@@ -125,11 +251,11 @@ export const formatCellContent = (value, column, row = null) => {
125
251
  switch (valueType) {
126
252
  case 'date':
127
253
  return moment(value).isValid()
128
- ? moment(value).format(column.format || 'DD/MM/YYYY')
254
+ ? moment(value).format(column.format || autoDetectDateFormat(value, 'DD-MM-YYYY'))
129
255
  : value;
130
256
  case 'datetime':
131
257
  return moment(value).isValid()
132
- ? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
258
+ ? moment(value).format(column.format || autoDetectDateFormat(value, 'DD-MM-YYYY HH:mm'))
133
259
  : value;
134
260
  case 'currency':
135
261
  return numeral(value).format(column.format || '$0,0.00');
@@ -139,25 +265,7 @@ export const formatCellContent = (value, column, row = null) => {
139
265
  ? numeral(value).format(column.format || '0,0')
140
266
  : value;
141
267
  case 'boolean':
142
- if (
143
- value === 1 ||
144
- value === true ||
145
- value === 'true' ||
146
- value === 'yes' ||
147
- value === 'Yes'
148
- ) {
149
- return 'Yes';
150
- }
151
- if (
152
- value === 0 ||
153
- value === false ||
154
- value === 'false' ||
155
- value === 'no' ||
156
- value === 'No'
157
- ) {
158
- return 'No';
159
- }
160
- return value;
268
+ return interpretBooleanValue(value, column);
161
269
  case 'year':
162
270
  // Special handling for year values
163
271
  return value.toString();
@@ -174,6 +282,27 @@ export const formatCellContent = (value, column, row = null) => {
174
282
  // Parse rich text content
175
283
  return parse(String(value));
176
284
  default:
285
+ // Check if this might be a boolean value based on its content
286
+ if ((value === 0 || value === 1) && column.id) {
287
+ // Only apply smart boolean interpretation if the column name suggests it's boolean
288
+ const columnName = (column.id || column.name || '').toLowerCase();
289
+ const booleanIndicators = [
290
+ 'is_', 'has_', 'can_', 'should_', 'will_', 'did_',
291
+ 'active', 'enabled', 'completed', 'confirmed', 'approved', 'verified',
292
+ 'sent', 'delivered', 'paid', 'signed', 'expired', 'urgent', 'critical',
293
+ 'public', 'visible', 'available', 'ready', 'locked', 'online',
294
+ '_flag', '_status', '_state'
295
+ ];
296
+
297
+ const seemsBooleanish = booleanIndicators.some(indicator =>
298
+ columnName.includes(indicator)
299
+ );
300
+
301
+ if (seemsBooleanish) {
302
+ return interpretBooleanValue(value, column);
303
+ }
304
+ }
305
+
177
306
  // For text values, check if it contains HTML tags
178
307
  if (
179
308
  typeof value === 'string' &&
@@ -3503,3 +3503,331 @@
3503
3503
  border-radius: var(--radius);
3504
3504
  border: 1px solid var(--border-color);
3505
3505
  }
3506
+
3507
+ /* Enhanced Pagination Styles */
3508
+ .paginationContainer {
3509
+ display: flex;
3510
+ flex-direction: column;
3511
+ gap: 12px;
3512
+ padding: 16px 20px;
3513
+ background-color: var(--tertiary-color);
3514
+ border-top: 1px solid var(--border-color);
3515
+ border-radius: 0 0 var(--radius) var(--radius);
3516
+ }
3517
+
3518
+ .paginationInfo {
3519
+ display: flex;
3520
+ justify-content: center;
3521
+ align-items: center;
3522
+ font-size: 0.875rem;
3523
+ color: var(--paragraph-color);
3524
+ font-weight: 500;
3525
+ margin: 0;
3526
+ padding: 0;
3527
+ background: none;
3528
+ border: none;
3529
+ }
3530
+
3531
+ .paginationControls {
3532
+ display: flex;
3533
+ justify-content: space-between;
3534
+ align-items: center;
3535
+ gap: 16px;
3536
+ flex-wrap: wrap;
3537
+
3538
+ @media (max-width: 768px) {
3539
+ flex-direction: column;
3540
+ gap: 12px;
3541
+ }
3542
+ }
3543
+
3544
+ .pageSizeSelector {
3545
+ display: flex;
3546
+ align-items: center;
3547
+ gap: 8px;
3548
+ font-size: 0.875rem;
3549
+ color: var(--paragraph-color);
3550
+
3551
+ label {
3552
+ font-weight: 500;
3553
+ color: var(--header-color);
3554
+ margin: 0;
3555
+ }
3556
+
3557
+ span {
3558
+ font-weight: 400;
3559
+ color: var(--paragraph-color);
3560
+ }
3561
+ }
3562
+
3563
+ .pageSizeSelect {
3564
+ padding: 6px 8px;
3565
+ border: 1px solid var(--border-color);
3566
+ border-radius: var(--radius);
3567
+ background-color: var(--bg-color);
3568
+ color: var(--header-color);
3569
+ font-size: 0.875rem;
3570
+ cursor: pointer;
3571
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
3572
+
3573
+ &:focus {
3574
+ outline: none;
3575
+ border-color: var(--primary-color);
3576
+ box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
3577
+ }
3578
+
3579
+ &:hover {
3580
+ border-color: var(--primary-color);
3581
+ }
3582
+ }
3583
+
3584
+ .pageNavigation {
3585
+ display: flex;
3586
+ align-items: center;
3587
+ gap: 8px;
3588
+
3589
+ @media (max-width: 768px) {
3590
+ width: 100%;
3591
+ justify-content: center;
3592
+ }
3593
+ }
3594
+
3595
+ .pageNumbers {
3596
+ display: flex;
3597
+ align-items: center;
3598
+ gap: 4px;
3599
+
3600
+ @media (max-width: 480px) {
3601
+ gap: 2px;
3602
+ }
3603
+ }
3604
+
3605
+ .pageBtn {
3606
+ display: flex;
3607
+ align-items: center;
3608
+ justify-content: center;
3609
+ min-width: 36px;
3610
+ height: 36px;
3611
+ padding: 0 8px;
3612
+ border: 1px solid var(--border-color);
3613
+ background-color: var(--bg-color);
3614
+ color: var(--paragraph-color);
3615
+ font-size: 0.875rem;
3616
+ font-weight: 500;
3617
+ border-radius: var(--radius);
3618
+ cursor: pointer;
3619
+ transition: all 0.2s ease;
3620
+ user-select: none;
3621
+
3622
+ &:hover:not(.disabled) {
3623
+ background-color: var(--secondary-color);
3624
+ border-color: var(--primary-color);
3625
+ color: var(--header-color);
3626
+ transform: translateY(-1px);
3627
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
3628
+ }
3629
+
3630
+ &:active:not(.disabled) {
3631
+ transform: translateY(0);
3632
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
3633
+ }
3634
+
3635
+ &.active {
3636
+ background-color: var(--primary-color);
3637
+ border-color: var(--primary-color);
3638
+ color: white;
3639
+ font-weight: 600;
3640
+ box-shadow: 0 2px 4px rgba(var(--primary-rgb), 0.3);
3641
+
3642
+ &:hover {
3643
+ background-color: var(--primary-dark-color);
3644
+ transform: none;
3645
+ }
3646
+ }
3647
+
3648
+ &.disabled {
3649
+ background-color: var(--tertiary-color);
3650
+ border-color: var(--border-color);
3651
+ color: var(--disabled-color, #9CA3AF);
3652
+ cursor: not-allowed;
3653
+ opacity: 0.6;
3654
+
3655
+ &:hover {
3656
+ transform: none;
3657
+ box-shadow: none;
3658
+ background-color: var(--tertiary-color);
3659
+ border-color: var(--border-color);
3660
+ }
3661
+ }
3662
+
3663
+ @media (max-width: 480px) {
3664
+ min-width: 32px;
3665
+ height: 32px;
3666
+ font-size: 0.8rem;
3667
+ padding: 0 6px;
3668
+ }
3669
+ }
3670
+
3671
+ .ellipsis {
3672
+ display: flex;
3673
+ align-items: center;
3674
+ justify-content: center;
3675
+ min-width: 36px;
3676
+ height: 36px;
3677
+ color: var(--paragraph-color);
3678
+ font-size: 0.875rem;
3679
+ font-weight: 500;
3680
+ user-select: none;
3681
+
3682
+ @media (max-width: 480px) {
3683
+ min-width: 24px;
3684
+ height: 32px;
3685
+ font-size: 0.8rem;
3686
+ }
3687
+ }
3688
+
3689
+ /* Pagination animations */
3690
+ @keyframes paginationSlideIn {
3691
+ from {
3692
+ opacity: 0;
3693
+ transform: translateY(10px);
3694
+ }
3695
+ to {
3696
+ opacity: 1;
3697
+ transform: translateY(0);
3698
+ }
3699
+ }
3700
+
3701
+ .paginationContainer {
3702
+ animation: paginationSlideIn 0.3s ease-out;
3703
+ }
3704
+
3705
+ /* Loading state for pagination */
3706
+ .paginationContainer.loading {
3707
+ opacity: 0.6;
3708
+ pointer-events: none;
3709
+
3710
+ .pageBtn {
3711
+ cursor: wait;
3712
+ }
3713
+
3714
+ .pageSizeSelect {
3715
+ cursor: wait;
3716
+ }
3717
+ }
3718
+
3719
+ /* Enhanced accessibility */
3720
+ .pageBtn:focus-visible {
3721
+ outline: 2px solid var(--primary-color);
3722
+ outline-offset: 2px;
3723
+ }
3724
+
3725
+ .pageSizeSelect:focus-visible {
3726
+ outline: 2px solid var(--primary-color);
3727
+ outline-offset: 2px;
3728
+ }
3729
+
3730
+ /* No Data Message Styles */
3731
+ .noDataContainer {
3732
+ display: flex;
3733
+ justify-content: center;
3734
+ align-items: center;
3735
+ min-height: 400px;
3736
+ padding: 40px 20px;
3737
+ }
3738
+
3739
+ .noDataMessage {
3740
+ text-align: center;
3741
+ max-width: 500px;
3742
+ padding: 40px;
3743
+ background-color: var(--tertiary-color);
3744
+ border: 1px solid var(--border-color);
3745
+ border-radius: var(--radius);
3746
+ box-shadow: var(--box-shadow-small);
3747
+ }
3748
+
3749
+ .noDataIcon {
3750
+ margin-bottom: 20px;
3751
+ opacity: 0.7;
3752
+ color: var(--paragraph-color);
3753
+
3754
+ svg {
3755
+ width: 48px;
3756
+ height: 48px;
3757
+ }
3758
+ }
3759
+
3760
+ .noDataTitle {
3761
+ font-size: 1.5rem;
3762
+ font-weight: 600;
3763
+ color: var(--header-color);
3764
+ margin: 0 0 12px 0;
3765
+ }
3766
+
3767
+ .noDataText {
3768
+ font-size: 1rem;
3769
+ color: var(--paragraph-color);
3770
+ margin: 0 0 24px 0;
3771
+ line-height: 1.5;
3772
+ }
3773
+
3774
+ .noDataSuggestions {
3775
+ text-align: left;
3776
+ background-color: var(--bg-color);
3777
+ border: 1px solid var(--border-color);
3778
+ border-radius: var(--radius);
3779
+ padding: 20px;
3780
+ margin-top: 20px;
3781
+
3782
+ p {
3783
+ margin: 0 0 12px 0;
3784
+ font-weight: 600;
3785
+ color: var(--header-color);
3786
+ font-size: 0.9rem;
3787
+ }
3788
+
3789
+ ul {
3790
+ margin: 0;
3791
+ padding-left: 20px;
3792
+ color: var(--paragraph-color);
3793
+
3794
+ li {
3795
+ margin-bottom: 8px;
3796
+ font-size: 0.9rem;
3797
+ line-height: 1.4;
3798
+
3799
+ &:last-child {
3800
+ margin-bottom: 0;
3801
+ }
3802
+ }
3803
+ }
3804
+ }
3805
+
3806
+ /* Responsive adjustments for no data message */
3807
+ @media (max-width: 768px) {
3808
+ .noDataContainer {
3809
+ min-height: 300px;
3810
+ padding: 20px 15px;
3811
+ }
3812
+
3813
+ .noDataMessage {
3814
+ padding: 30px 20px;
3815
+ }
3816
+
3817
+ .noDataIcon {
3818
+ margin-bottom: 16px;
3819
+
3820
+ svg {
3821
+ width: 40px;
3822
+ height: 40px;
3823
+ }
3824
+ }
3825
+
3826
+ .noDataTitle {
3827
+ font-size: 1.3rem;
3828
+ }
3829
+
3830
+ .noDataText {
3831
+ font-size: 0.9rem;
3832
+ }
3833
+ }