@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.
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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. **
|
|
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.
|
|
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
|
|
149
|
+
const rawValue = data[column.id];
|
|
150
|
+
const formattedValue = formatCellContent(rawValue, { ...column, type: 'boolean' });
|
|
149
151
|
return (
|
|
150
|
-
<CellWithTooltip value={
|
|
151
|
-
<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
|
-
|
|
206
|
-
|
|
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(
|
|
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
|
|
2545
|
-
skip:
|
|
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 (
|
|
2552
|
-
setPreviewData(
|
|
2553
|
-
fetchedDataCount =
|
|
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
|
-
//
|
|
2753
|
-
console.log('🎯 [DEBUG] Final 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
|
|
2970
|
-
if (template.quickFilters) {
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
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
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
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
|
-
//
|
|
4645
|
-
const
|
|
4646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7044
|
-
<
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
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.
|
|
7068
|
-
<
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
7288
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|