@visns-studio/visns-components 5.11.2 → 5.11.4
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 +55 -0
- package/package.json +1 -1
- package/src/components/crm/AssociationManager.jsx +431 -0
- package/src/components/crm/BusinessCardOcr.jsx +190 -6
- package/src/components/crm/ClientAssociationManager.jsx +362 -0
- package/src/components/crm/MergeEntity.jsx +1049 -0
- package/src/components/crm/MultiSelect.jsx +14 -3
- package/src/components/crm/Navigation.jsx +67 -8
- package/src/components/crm/cells/CellWithTooltip.jsx +35 -28
- package/src/components/crm/generic/GenericDetail.jsx +25 -0
- package/src/components/crm/generic/GenericReport.jsx +1457 -434
- package/src/components/crm/generic/styles/GenericReport.module.scss +598 -24
- package/src/components/crm/styles/AssociationManager.module.scss +364 -0
- package/src/components/crm/styles/BusinessCardOcr.module.scss +362 -94
- package/src/components/crm/styles/ClientAssociationManager.module.scss +290 -0
- package/src/components/crm/styles/MergeEntity.module.scss +690 -0
- package/src/components/crm/styles/MultiSelect.module.scss +18 -0
- package/src/components/crm/styles/Navigation.module.scss +148 -16
- package/src/components/crm/styles/global-datagrid.css +9 -0
- package/src/index.js +6 -0
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
FileText as Newspaper,
|
|
28
28
|
ChevronDown,
|
|
29
29
|
Rss,
|
|
30
|
+
Globe,
|
|
31
|
+
Lock,
|
|
30
32
|
} from 'lucide-react';
|
|
31
33
|
import CustomFetch from '../Fetch';
|
|
32
34
|
import Download from '../Download';
|
|
@@ -111,6 +113,58 @@ const getTableDisplayName = (tableName, definition) => {
|
|
|
111
113
|
return formatName(tableName);
|
|
112
114
|
};
|
|
113
115
|
|
|
116
|
+
// Get unique table display name with disambiguation if needed
|
|
117
|
+
const getUniqueTableDisplayName = (tableName, allTables, definition) => {
|
|
118
|
+
const baseDisplayName = getTableDisplayName(tableName, definition);
|
|
119
|
+
|
|
120
|
+
// Find all tables with the same display name
|
|
121
|
+
const conflictingTables = allTables.filter((table) => {
|
|
122
|
+
const otherTableName = typeof table === 'string' ? table : table.name;
|
|
123
|
+
const otherDisplayName = getTableDisplayName(
|
|
124
|
+
otherTableName,
|
|
125
|
+
definition
|
|
126
|
+
);
|
|
127
|
+
return (
|
|
128
|
+
otherDisplayName === baseDisplayName && otherTableName !== tableName
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// If no conflicts, return the base name
|
|
133
|
+
if (conflictingTables.length === 0) {
|
|
134
|
+
return baseDisplayName;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If there are conflicts, add disambiguation
|
|
138
|
+
// Show the actual table name in parentheses
|
|
139
|
+
return `${baseDisplayName} (${tableName})`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Get structured table display name data for better styling
|
|
143
|
+
const getStructuredTableDisplayName = (tableName, allTables, definition) => {
|
|
144
|
+
const baseDisplayName = getTableDisplayName(tableName, definition);
|
|
145
|
+
|
|
146
|
+
// Find all tables with the same display name
|
|
147
|
+
const conflictingTables = allTables.filter((table) => {
|
|
148
|
+
const otherTableName = typeof table === 'string' ? table : table.name;
|
|
149
|
+
const otherDisplayName = getTableDisplayName(
|
|
150
|
+
otherTableName,
|
|
151
|
+
definition
|
|
152
|
+
);
|
|
153
|
+
return (
|
|
154
|
+
otherDisplayName === baseDisplayName && otherTableName !== tableName
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
main: baseDisplayName,
|
|
160
|
+
disambiguation: conflictingTables.length > 0 ? tableName : null,
|
|
161
|
+
full:
|
|
162
|
+
conflictingTables.length > 0
|
|
163
|
+
? `${baseDisplayName} (${tableName})`
|
|
164
|
+
: baseDisplayName,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
114
168
|
// Get table description from definition
|
|
115
169
|
const getTableDescription = (tableName, definition) => {
|
|
116
170
|
if (definition && definition.tables) {
|
|
@@ -462,9 +516,57 @@ const formatSmartValue = (value, columnName, columnType) => {
|
|
|
462
516
|
}
|
|
463
517
|
}
|
|
464
518
|
|
|
519
|
+
// Handle currency formatting for monetary values
|
|
520
|
+
const lowerColumnName = columnName.toLowerCase();
|
|
521
|
+
if (
|
|
522
|
+
(lowerColumnName.includes('value') ||
|
|
523
|
+
lowerColumnName.includes('price') ||
|
|
524
|
+
lowerColumnName.includes('cost') ||
|
|
525
|
+
lowerColumnName.includes('amount') ||
|
|
526
|
+
lowerColumnName.includes('fee') ||
|
|
527
|
+
lowerColumnName.includes('rate') ||
|
|
528
|
+
lowerColumnName.includes('salary') ||
|
|
529
|
+
lowerColumnName.includes('wage') ||
|
|
530
|
+
lowerColumnName.includes('revenue') ||
|
|
531
|
+
lowerColumnName.includes('income') ||
|
|
532
|
+
lowerColumnName.includes('budget') ||
|
|
533
|
+
lowerColumnName.includes('total') ||
|
|
534
|
+
lowerColumnName.includes('subtotal') ||
|
|
535
|
+
lowerColumnName.includes('tax') ||
|
|
536
|
+
lowerColumnName.includes('discount') ||
|
|
537
|
+
lowerColumnName.includes('payment') ||
|
|
538
|
+
lowerColumnName.includes('invoice') ||
|
|
539
|
+
lowerColumnName.includes('bill') ||
|
|
540
|
+
lowerColumnName.includes('expense') ||
|
|
541
|
+
lowerColumnName.includes('profit') ||
|
|
542
|
+
lowerColumnName.includes('loss') ||
|
|
543
|
+
lowerColumnName.includes('balance') ||
|
|
544
|
+
lowerColumnName.includes('debt') ||
|
|
545
|
+
lowerColumnName.includes('credit') ||
|
|
546
|
+
lowerColumnName.includes('refund') ||
|
|
547
|
+
lowerColumnName.includes('commission') ||
|
|
548
|
+
lowerColumnName.includes('bonus') ||
|
|
549
|
+
lowerColumnName.includes('penalty') ||
|
|
550
|
+
lowerColumnName.includes('fine') ||
|
|
551
|
+
columnType === 'decimal' ||
|
|
552
|
+
columnType === 'money' ||
|
|
553
|
+
columnType === 'currency') &&
|
|
554
|
+
!isNaN(parseFloat(value)) &&
|
|
555
|
+
isFinite(value)
|
|
556
|
+
) {
|
|
557
|
+
const numericValue = parseFloat(value);
|
|
558
|
+
|
|
559
|
+
// Format as Australian currency
|
|
560
|
+
return new Intl.NumberFormat('en-AU', {
|
|
561
|
+
style: 'currency',
|
|
562
|
+
currency: 'AUD',
|
|
563
|
+
minimumFractionDigits: 2,
|
|
564
|
+
maximumFractionDigits: 2,
|
|
565
|
+
}).format(numericValue);
|
|
566
|
+
}
|
|
567
|
+
|
|
465
568
|
// Convert to string for comparison
|
|
466
569
|
const strValue = String(value).toLowerCase();
|
|
467
|
-
const lowerColumnName = columnName.toLowerCase();
|
|
468
570
|
|
|
469
571
|
// Handle boolean values (0/1) in status columns
|
|
470
572
|
if (
|
|
@@ -745,7 +847,7 @@ const wizardSteps = [
|
|
|
745
847
|
id: 'table',
|
|
746
848
|
title: 'Select Your Data',
|
|
747
849
|
icon: Data,
|
|
748
|
-
description: 'Choose
|
|
850
|
+
description: 'Choose the data source you want to view',
|
|
749
851
|
guidance: {
|
|
750
852
|
summary: 'Pick the main data source for your report',
|
|
751
853
|
howTo: [
|
|
@@ -823,13 +925,37 @@ const wizardSteps = [
|
|
|
823
925
|
},
|
|
824
926
|
];
|
|
825
927
|
|
|
826
|
-
|
|
928
|
+
// Icon mapping for business templates (moved from hardcoded templates)
|
|
929
|
+
const getTemplateIcon = (iconName) => {
|
|
930
|
+
const iconMap = {
|
|
931
|
+
Calendar,
|
|
932
|
+
Person,
|
|
933
|
+
Rss,
|
|
934
|
+
Data,
|
|
935
|
+
Money,
|
|
936
|
+
Map,
|
|
937
|
+
Tag,
|
|
938
|
+
File,
|
|
939
|
+
ShippingBoxV1,
|
|
940
|
+
Newspaper,
|
|
941
|
+
// Add more as needed
|
|
942
|
+
};
|
|
943
|
+
return iconMap[iconName] || Data;
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const GenericReportImproved = ({
|
|
947
|
+
setting = {},
|
|
948
|
+
definition = null,
|
|
949
|
+
businessTemplates = [],
|
|
950
|
+
}) => {
|
|
827
951
|
// Extract settings with defaults
|
|
828
952
|
const { tableUrl, columnUrl } = setting;
|
|
829
953
|
|
|
830
954
|
// UI State
|
|
831
955
|
const [currentWizardStep, setCurrentWizardStep] = useState(0);
|
|
832
956
|
const [showTemplates, setShowTemplates] = useState(true);
|
|
957
|
+
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
|
958
|
+
const [showBusinessTemplates, setShowBusinessTemplates] = useState(false);
|
|
833
959
|
|
|
834
960
|
// State for database schema
|
|
835
961
|
const [tables, setTables] = useState([]);
|
|
@@ -872,6 +998,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
872
998
|
const [sortCriteria, setSortCriteria] = useState([]);
|
|
873
999
|
const [showSortingSection, setShowSortingSection] = useState(false);
|
|
874
1000
|
const [showFilteringSection, setShowFilteringSection] = useState(false);
|
|
1001
|
+
const [showSelectedFields, setShowSelectedFields] = useState(false);
|
|
875
1002
|
|
|
876
1003
|
// Enhanced filter criteria with visual builder support
|
|
877
1004
|
const [filterCriteria, setFilterCriteria] = useState({
|
|
@@ -902,6 +1029,16 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
902
1029
|
// State for showing hidden fields
|
|
903
1030
|
const [showHiddenFields, setShowHiddenFields] = useState(false);
|
|
904
1031
|
|
|
1032
|
+
// State for calculated field modal
|
|
1033
|
+
const [showCalculatedFieldModal, setShowCalculatedFieldModal] =
|
|
1034
|
+
useState(false);
|
|
1035
|
+
const [calculatedFieldForm, setCalculatedFieldForm] = useState({
|
|
1036
|
+
displayName: '',
|
|
1037
|
+
formula: '',
|
|
1038
|
+
type: 'decimal',
|
|
1039
|
+
});
|
|
1040
|
+
const [customCalculatedFields, setCustomCalculatedFields] = useState([]);
|
|
1041
|
+
|
|
905
1042
|
// Note: Removed table search functionality to simplify user experience
|
|
906
1043
|
|
|
907
1044
|
// Extract additional settings with defaults
|
|
@@ -912,39 +1049,43 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
912
1049
|
exportUrl = '/ajax/reportBuilder/export',
|
|
913
1050
|
} = setting;
|
|
914
1051
|
|
|
915
|
-
// Simple icon helper
|
|
1052
|
+
// Simple icon helper for SweetAlert2 - returns HTML strings with SVG icons
|
|
916
1053
|
const getIcon = (iconName, color = '#6b7280', size = 16) => {
|
|
917
1054
|
const icons = {
|
|
918
|
-
chart:
|
|
919
|
-
rocket:
|
|
920
|
-
file:
|
|
921
|
-
cogs:
|
|
922
|
-
list:
|
|
923
|
-
database:
|
|
924
|
-
columns:
|
|
925
|
-
link:
|
|
926
|
-
filter:
|
|
927
|
-
download:
|
|
928
|
-
star:
|
|
929
|
-
lightbulb:
|
|
930
|
-
users:
|
|
931
|
-
cart:
|
|
932
|
-
money:
|
|
933
|
-
tasks:
|
|
934
|
-
settings:
|
|
935
|
-
clock:
|
|
936
|
-
question:
|
|
937
|
-
warning:
|
|
938
|
-
eye:
|
|
1055
|
+
chart: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>`,
|
|
1056
|
+
rocket: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M12 2c5 3 8 5 8 9a6 6 0 1 1-12 0c0-4 3-6 8-9Z"/><path d="m16 6-4 14-4-14"/><circle cx="12" cy="12" r="2"/></svg>`,
|
|
1057
|
+
file: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>`,
|
|
1058
|
+
cogs: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
|
1059
|
+
list: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>`,
|
|
1060
|
+
database: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="m3 5 0 14c0 1.67 4.03 3 9 3s9-1.33 9-3V5"/><path d="m3 12c0 1.67 4.03 3 9 3s9-1.33 9-3"/></svg>`,
|
|
1061
|
+
columns: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>`,
|
|
1062
|
+
link: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
|
|
1063
|
+
filter: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><polygon points="22,3 2,3 10,12.46 10,19 14,21 14,12.46"/></svg>`,
|
|
1064
|
+
download: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7,10 12,15 17,10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>`,
|
|
1065
|
+
star: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M12 2c5 3 8 5 8 9a6 6 0 1 1-12 0c0-4 3-6 8-9Z"/><path d="m16 6-4 14-4-14"/><circle cx="12" cy="12" r="2"/></svg>`,
|
|
1066
|
+
lightbulb: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M12 2c5 3 8 5 8 9a6 6 0 1 1-12 0c0-4 3-6 8-9Z"/><path d="m16 6-4 14-4-14"/><circle cx="12" cy="12" r="2"/></svg>`,
|
|
1067
|
+
users: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
|
|
1068
|
+
cart: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57L20.71 7H5.12"/></svg>`,
|
|
1069
|
+
money: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><line x1="12" x2="12" y1="2" y2="22"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>`,
|
|
1070
|
+
tasks: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M12 2c5 3 8 5 8 9a6 6 0 1 1-12 0c0-4 3-6 8-9Z"/><path d="m16 6-4 14-4-14"/><circle cx="12" cy="12" r="2"/></svg>`,
|
|
1071
|
+
settings: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
|
1072
|
+
clock: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><circle cx="12" cy="12" r="10"/><polyline points="12,6 12,12 16,14"/></svg>`,
|
|
1073
|
+
question: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`,
|
|
1074
|
+
warning: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,
|
|
1075
|
+
eye: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle; display: inline-block;"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
|
939
1076
|
};
|
|
940
|
-
|
|
941
|
-
return
|
|
1077
|
+
|
|
1078
|
+
return icons[iconName] || icons.chart;
|
|
942
1079
|
};
|
|
943
1080
|
|
|
944
1081
|
// Show comprehensive help guide
|
|
945
1082
|
const showGuidedHelp = () => {
|
|
946
1083
|
Swal.fire({
|
|
947
|
-
title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${getIcon(
|
|
1084
|
+
title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${getIcon(
|
|
1085
|
+
'chart',
|
|
1086
|
+
'#3b82f6',
|
|
1087
|
+
24
|
|
1088
|
+
)}Complete Report Builder Guide</div>`,
|
|
948
1089
|
html: `
|
|
949
1090
|
<div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">
|
|
950
1091
|
|
|
@@ -1077,19 +1218,35 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
1077
1218
|
|
|
1078
1219
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
1079
1220
|
<div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 6px; padding: 8px;">
|
|
1080
|
-
<strong style="color: #15803d;">${getIcon(
|
|
1221
|
+
<strong style="color: #15803d;">${getIcon(
|
|
1222
|
+
'users',
|
|
1223
|
+
'#15803d',
|
|
1224
|
+
12
|
|
1225
|
+
)}Client List:</strong>
|
|
1081
1226
|
<br><small style="color: #16a34a;">Clients table + Contact details</small>
|
|
1082
1227
|
</div>
|
|
1083
1228
|
<div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 6px; padding: 8px;">
|
|
1084
|
-
<strong style="color: #1d4ed8;">${getIcon(
|
|
1229
|
+
<strong style="color: #1d4ed8;">${getIcon(
|
|
1230
|
+
'cart',
|
|
1231
|
+
'#1d4ed8',
|
|
1232
|
+
12
|
|
1233
|
+
)}Recent Orders:</strong>
|
|
1085
1234
|
<br><small style="color: #2563eb;">Orders + Customer names + Date filter</small>
|
|
1086
1235
|
</div>
|
|
1087
1236
|
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 8px;">
|
|
1088
|
-
<strong style="color: #d97706;">${getIcon(
|
|
1237
|
+
<strong style="color: #d97706;">${getIcon(
|
|
1238
|
+
'money',
|
|
1239
|
+
'#d97706',
|
|
1240
|
+
12
|
|
1241
|
+
)}Financial Summary:</strong>
|
|
1089
1242
|
<br><small style="color: #f59e0b;">Invoices + Payments + Date grouping</small>
|
|
1090
1243
|
</div>
|
|
1091
1244
|
<div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 6px; padding: 8px;">
|
|
1092
|
-
<strong style="color: #db2777;">${getIcon(
|
|
1245
|
+
<strong style="color: #db2777;">${getIcon(
|
|
1246
|
+
'tasks',
|
|
1247
|
+
'#db2777',
|
|
1248
|
+
12
|
|
1249
|
+
)}Active Projects:</strong>
|
|
1093
1250
|
<br><small style="color: #ec4899;">Projects + Status filter + Team members</small>
|
|
1094
1251
|
</div>
|
|
1095
1252
|
</div>
|
|
@@ -1104,19 +1261,35 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
1104
1261
|
|
|
1105
1262
|
<div style="background: #fef2f2; border: 1px solid #dc2626; border-radius: 8px; padding: 12px;">
|
|
1106
1263
|
<div style="margin-bottom: 8px;">
|
|
1107
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1264
|
+
<strong style="color: #dc2626;">${getIcon(
|
|
1265
|
+
'clock',
|
|
1266
|
+
'#dc2626',
|
|
1267
|
+
12
|
|
1268
|
+
)}Report too slow?</strong>
|
|
1108
1269
|
<span style="color: #991b1b;"> Add date filters or reduce columns</span>
|
|
1109
1270
|
</div>
|
|
1110
1271
|
<div style="margin-bottom: 8px;">
|
|
1111
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1272
|
+
<strong style="color: #dc2626;">${getIcon(
|
|
1273
|
+
'question',
|
|
1274
|
+
'#dc2626',
|
|
1275
|
+
12
|
|
1276
|
+
)}Missing data?</strong>
|
|
1112
1277
|
<span style="color: #991b1b;"> Check relationships and join conditions</span>
|
|
1113
1278
|
</div>
|
|
1114
1279
|
<div style="margin-bottom: 8px;">
|
|
1115
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1280
|
+
<strong style="color: #dc2626;">${getIcon(
|
|
1281
|
+
'warning',
|
|
1282
|
+
'#dc2626',
|
|
1283
|
+
12
|
|
1284
|
+
)}Export failing?</strong>
|
|
1116
1285
|
<span style="color: #991b1b;"> Try smaller date ranges or fewer rows</span>
|
|
1117
1286
|
</div>
|
|
1118
1287
|
<div>
|
|
1119
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1288
|
+
<strong style="color: #dc2626;">${getIcon(
|
|
1289
|
+
'eye',
|
|
1290
|
+
'#dc2626',
|
|
1291
|
+
12
|
|
1292
|
+
)}No data showing?</strong>
|
|
1120
1293
|
<span style="color: #991b1b;"> Review your filter conditions</span>
|
|
1121
1294
|
</div>
|
|
1122
1295
|
</div>
|
|
@@ -1124,14 +1297,14 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
1124
1297
|
</div>
|
|
1125
1298
|
`,
|
|
1126
1299
|
width: 800,
|
|
1127
|
-
confirmButtonText:
|
|
1300
|
+
confirmButtonText: 'Close',
|
|
1128
1301
|
confirmButtonColor: '#6b7280',
|
|
1129
1302
|
showCancelButton: false,
|
|
1130
1303
|
showDenyButton: false,
|
|
1131
1304
|
customClass: {
|
|
1132
1305
|
popup: 'comprehensive-help-modal',
|
|
1133
|
-
htmlContainer: 'help-content-scrollable'
|
|
1134
|
-
}
|
|
1306
|
+
htmlContainer: 'help-content-scrollable',
|
|
1307
|
+
},
|
|
1135
1308
|
});
|
|
1136
1309
|
};
|
|
1137
1310
|
|
|
@@ -1829,27 +2002,128 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
1829
2002
|
|
|
1830
2003
|
// Column selection handler
|
|
1831
2004
|
const handleColumnToggle = (column, tableName = selectedTable) => {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2005
|
+
// Prevent toggling of calculated fields
|
|
2006
|
+
if (column.isCalculated) {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const isSelected = selectedColumns.some((col) => {
|
|
2011
|
+
if (col.isCalculated && column.isCalculated) {
|
|
2012
|
+
// Both are calculated fields, match by column name
|
|
2013
|
+
return col.column === column.name;
|
|
2014
|
+
} else if (!col.isCalculated && !column.isCalculated) {
|
|
2015
|
+
// Both are regular fields, match by table.column
|
|
2016
|
+
return (
|
|
2017
|
+
`${col.table}.${col.column}` ===
|
|
2018
|
+
`${tableName}.${column.name}`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
return false; // Different types don't match
|
|
2022
|
+
});
|
|
1836
2023
|
|
|
1837
2024
|
if (isSelected) {
|
|
1838
2025
|
setSelectedColumns(
|
|
1839
|
-
selectedColumns.filter(
|
|
1840
|
-
(col
|
|
1841
|
-
|
|
2026
|
+
selectedColumns.filter((col) => {
|
|
2027
|
+
if (col.isCalculated && column.isCalculated) {
|
|
2028
|
+
return col.column !== column.name;
|
|
2029
|
+
} else if (!col.isCalculated && !column.isCalculated) {
|
|
2030
|
+
return (
|
|
2031
|
+
`${col.table}.${col.column}` !==
|
|
2032
|
+
`${tableName}.${column.name}`
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
return true; // Keep different types
|
|
2036
|
+
})
|
|
1842
2037
|
);
|
|
1843
2038
|
} else {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2039
|
+
// Check if this is a calculated field and preserve its properties
|
|
2040
|
+
const newColumn = {
|
|
2041
|
+
table: column.isCalculated ? null : tableName,
|
|
2042
|
+
column: column.name,
|
|
2043
|
+
displayName: column.displayName || formatName(column.name),
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// Add calculated field properties if applicable
|
|
2047
|
+
if (column.isCalculated) {
|
|
2048
|
+
newColumn.isCalculated = true;
|
|
2049
|
+
newColumn.formula = column.formula;
|
|
2050
|
+
newColumn.type = column.type;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
setSelectedColumns([...selectedColumns, newColumn]);
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
// Calculated field handlers
|
|
2058
|
+
const handleCalculatedFieldSubmit = () => {
|
|
2059
|
+
if (
|
|
2060
|
+
!calculatedFieldForm.displayName.trim() ||
|
|
2061
|
+
!calculatedFieldForm.formula.trim()
|
|
2062
|
+
) {
|
|
2063
|
+
toast.error('Please provide both a display name and formula');
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// Create a unique ID for the calculated field
|
|
2068
|
+
const fieldId = calculatedFieldForm.displayName
|
|
2069
|
+
.toLowerCase()
|
|
2070
|
+
.replace(/[^a-z0-9]/g, '_');
|
|
2071
|
+
|
|
2072
|
+
// Check if field with this ID already exists
|
|
2073
|
+
const existingField = customCalculatedFields.find(
|
|
2074
|
+
(field) => field.id === fieldId
|
|
2075
|
+
);
|
|
2076
|
+
if (existingField) {
|
|
2077
|
+
toast.error('A calculated field with this name already exists');
|
|
2078
|
+
return;
|
|
1852
2079
|
}
|
|
2080
|
+
|
|
2081
|
+
// Create the new calculated field
|
|
2082
|
+
const newCalculatedField = {
|
|
2083
|
+
id: fieldId,
|
|
2084
|
+
displayName: calculatedFieldForm.displayName,
|
|
2085
|
+
formula: calculatedFieldForm.formula,
|
|
2086
|
+
type: calculatedFieldForm.type,
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
// Add to custom calculated fields
|
|
2090
|
+
setCustomCalculatedFields([
|
|
2091
|
+
...customCalculatedFields,
|
|
2092
|
+
newCalculatedField,
|
|
2093
|
+
]);
|
|
2094
|
+
|
|
2095
|
+
// Automatically add to selected columns
|
|
2096
|
+
const newColumn = {
|
|
2097
|
+
table: null,
|
|
2098
|
+
column: fieldId,
|
|
2099
|
+
displayName: calculatedFieldForm.displayName,
|
|
2100
|
+
isCalculated: true,
|
|
2101
|
+
formula: calculatedFieldForm.formula,
|
|
2102
|
+
type: calculatedFieldForm.type,
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
setSelectedColumns([...selectedColumns, newColumn]);
|
|
2106
|
+
|
|
2107
|
+
// Reset form and close modal
|
|
2108
|
+
setCalculatedFieldForm({
|
|
2109
|
+
displayName: '',
|
|
2110
|
+
formula: '',
|
|
2111
|
+
type: 'decimal',
|
|
2112
|
+
});
|
|
2113
|
+
setShowCalculatedFieldModal(false);
|
|
2114
|
+
|
|
2115
|
+
toast.success(
|
|
2116
|
+
`Calculated field "${calculatedFieldForm.displayName}" added successfully`
|
|
2117
|
+
);
|
|
2118
|
+
};
|
|
2119
|
+
|
|
2120
|
+
const handleCalculatedFieldCancel = () => {
|
|
2121
|
+
setCalculatedFieldForm({
|
|
2122
|
+
displayName: '',
|
|
2123
|
+
formula: '',
|
|
2124
|
+
type: 'decimal',
|
|
2125
|
+
});
|
|
2126
|
+
setShowCalculatedFieldModal(false);
|
|
1853
2127
|
};
|
|
1854
2128
|
|
|
1855
2129
|
// Apply smart suggestion to the report configuration
|
|
@@ -1928,11 +2202,27 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
1928
2202
|
// Use the same payload format as GenericReport (with query wrapper)
|
|
1929
2203
|
const queryConfig = {
|
|
1930
2204
|
mainTable: selectedTable,
|
|
1931
|
-
columns: selectedColumns.map((col) =>
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2205
|
+
columns: selectedColumns.map((col) => {
|
|
2206
|
+
const column = {
|
|
2207
|
+
table: col.table,
|
|
2208
|
+
column: col.column,
|
|
2209
|
+
alias: col.alias || col.displayName,
|
|
2210
|
+
isCalculated: col.isCalculated || false,
|
|
2211
|
+
formula: col.formula,
|
|
2212
|
+
displayName: col.displayName,
|
|
2213
|
+
type: col.type,
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
// Log calculated fields specifically
|
|
2217
|
+
if (col.isCalculated) {
|
|
2218
|
+
console.log(
|
|
2219
|
+
'Frontend sending calculated field:',
|
|
2220
|
+
column
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
return column;
|
|
2225
|
+
}),
|
|
1936
2226
|
joins: joins.map((join) => ({
|
|
1937
2227
|
joinType: join.joinType,
|
|
1938
2228
|
targetTable: join.targetTable,
|
|
@@ -2280,7 +2570,23 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2280
2570
|
}
|
|
2281
2571
|
|
|
2282
2572
|
if (config.filters) {
|
|
2283
|
-
|
|
2573
|
+
// Ensure filters have the proper structure
|
|
2574
|
+
if (config.filters.groups) {
|
|
2575
|
+
setFilterCriteria(config.filters);
|
|
2576
|
+
} else {
|
|
2577
|
+
// Legacy format - convert to new structure
|
|
2578
|
+
setFilterCriteria({
|
|
2579
|
+
operator: 'AND',
|
|
2580
|
+
groups: [
|
|
2581
|
+
{
|
|
2582
|
+
operator: 'AND',
|
|
2583
|
+
filters: Array.isArray(config.filters)
|
|
2584
|
+
? config.filters
|
|
2585
|
+
: [],
|
|
2586
|
+
},
|
|
2587
|
+
],
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2284
2590
|
}
|
|
2285
2591
|
|
|
2286
2592
|
if (config.sorting) {
|
|
@@ -2305,6 +2611,111 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2305
2611
|
}
|
|
2306
2612
|
};
|
|
2307
2613
|
|
|
2614
|
+
// Load business template configuration
|
|
2615
|
+
const loadBusinessTemplate = async (template) => {
|
|
2616
|
+
try {
|
|
2617
|
+
setSelectedTemplate(template);
|
|
2618
|
+
|
|
2619
|
+
// Set main table
|
|
2620
|
+
if (template.mainTable) {
|
|
2621
|
+
setSelectedTable(template.mainTable);
|
|
2622
|
+
setAvailableTables([template.mainTable]);
|
|
2623
|
+
await fetchTableColumns(template.mainTable);
|
|
2624
|
+
await fetchDetectedJoins(template.mainTable);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// Apply suggested joins
|
|
2628
|
+
if (template.suggestedJoins && template.suggestedJoins.length > 0) {
|
|
2629
|
+
setJoins(template.suggestedJoins);
|
|
2630
|
+
|
|
2631
|
+
// Add joined tables to available tables and fetch their columns
|
|
2632
|
+
for (const join of template.suggestedJoins) {
|
|
2633
|
+
if (!availableTables.includes(join.targetTable)) {
|
|
2634
|
+
setAvailableTables((prev) => [
|
|
2635
|
+
...prev,
|
|
2636
|
+
join.targetTable,
|
|
2637
|
+
]);
|
|
2638
|
+
}
|
|
2639
|
+
// Fetch columns for joined tables
|
|
2640
|
+
await fetchJoinColumns(
|
|
2641
|
+
join.targetTable,
|
|
2642
|
+
template.suggestedJoins.indexOf(join)
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Set default columns
|
|
2648
|
+
if (template.defaultColumns) {
|
|
2649
|
+
// Convert template column format to the format expected by the component
|
|
2650
|
+
const formattedColumns = template.defaultColumns.map((col) => ({
|
|
2651
|
+
table: col.table,
|
|
2652
|
+
column: col.column,
|
|
2653
|
+
displayName: col.displayName || col.column,
|
|
2654
|
+
required: col.required || false,
|
|
2655
|
+
}));
|
|
2656
|
+
|
|
2657
|
+
// Add calculated fields as additional columns if they exist
|
|
2658
|
+
if (template.calculatedFields) {
|
|
2659
|
+
const calculatedColumns = template.calculatedFields.map(
|
|
2660
|
+
(calc) => ({
|
|
2661
|
+
table: null, // No table for calculated fields
|
|
2662
|
+
column: calc.id,
|
|
2663
|
+
displayName: calc.displayName,
|
|
2664
|
+
required: false,
|
|
2665
|
+
isCalculated: true,
|
|
2666
|
+
formula: calc.formula,
|
|
2667
|
+
type: calc.type,
|
|
2668
|
+
})
|
|
2669
|
+
);
|
|
2670
|
+
formattedColumns.push(...calculatedColumns);
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
setSelectedColumns(formattedColumns);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// Apply quick filters as default filter criteria
|
|
2677
|
+
if (template.quickFilters) {
|
|
2678
|
+
const defaultFilters = template.quickFilters
|
|
2679
|
+
.filter(
|
|
2680
|
+
(filter) =>
|
|
2681
|
+
filter.value && filter.value !== 'current_user'
|
|
2682
|
+
) // Skip user-specific filters for now
|
|
2683
|
+
.map((filter) => ({
|
|
2684
|
+
id: filter.id,
|
|
2685
|
+
field: filter.field,
|
|
2686
|
+
operator: filter.operator || '=',
|
|
2687
|
+
value: filter.value,
|
|
2688
|
+
label: filter.label,
|
|
2689
|
+
}));
|
|
2690
|
+
|
|
2691
|
+
if (defaultFilters.length > 0) {
|
|
2692
|
+
setFilterCriteria({
|
|
2693
|
+
operator: 'AND',
|
|
2694
|
+
groups: [
|
|
2695
|
+
{
|
|
2696
|
+
operator: 'AND',
|
|
2697
|
+
filters: defaultFilters,
|
|
2698
|
+
},
|
|
2699
|
+
],
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// Set report name based on template
|
|
2705
|
+
setReportName(template.name);
|
|
2706
|
+
setIsPublic(false);
|
|
2707
|
+
setLoadedReportId(null); // This is a new report from template
|
|
2708
|
+
|
|
2709
|
+
setShowTemplates(false);
|
|
2710
|
+
setCurrentWizardStep(0); // Start from the beginning to let user review
|
|
2711
|
+
|
|
2712
|
+
toast.success(`Loaded template: ${template.name}`);
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
toast.error('Failed to load template configuration');
|
|
2715
|
+
console.error('Error loading template:', error);
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
|
|
2308
2719
|
// Render saved reports dashboard
|
|
2309
2720
|
const renderTemplateSelection = () => {
|
|
2310
2721
|
return (
|
|
@@ -2333,6 +2744,59 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2333
2744
|
</div>
|
|
2334
2745
|
</div>
|
|
2335
2746
|
|
|
2747
|
+
{/* Business Templates Section */}
|
|
2748
|
+
<div className={styles.businessTemplatesSection}>
|
|
2749
|
+
<h3>Business Report Templates</h3>
|
|
2750
|
+
<p>
|
|
2751
|
+
Pre-configured reports for common business scenarios
|
|
2752
|
+
</p>
|
|
2753
|
+
<div className={styles.templatesGrid}>
|
|
2754
|
+
{businessTemplates.map((template) => {
|
|
2755
|
+
const TemplateIcon = getTemplateIcon(
|
|
2756
|
+
template.icon
|
|
2757
|
+
);
|
|
2758
|
+
return (
|
|
2759
|
+
<div
|
|
2760
|
+
key={template.id}
|
|
2761
|
+
className={styles.templateCard}
|
|
2762
|
+
onClick={() =>
|
|
2763
|
+
loadBusinessTemplate(template)
|
|
2764
|
+
}
|
|
2765
|
+
>
|
|
2766
|
+
<div className={styles.templateIcon}>
|
|
2767
|
+
<TemplateIcon size={24} />
|
|
2768
|
+
</div>
|
|
2769
|
+
<div className={styles.templateContent}>
|
|
2770
|
+
<h4>{template.name}</h4>
|
|
2771
|
+
<p>{template.description}</p>
|
|
2772
|
+
<div
|
|
2773
|
+
className={styles.templateTags}
|
|
2774
|
+
>
|
|
2775
|
+
{template.tags.map(
|
|
2776
|
+
(tag, index) => (
|
|
2777
|
+
<span
|
|
2778
|
+
key={index}
|
|
2779
|
+
className={
|
|
2780
|
+
styles.templateTag
|
|
2781
|
+
}
|
|
2782
|
+
>
|
|
2783
|
+
{tag}
|
|
2784
|
+
</span>
|
|
2785
|
+
)
|
|
2786
|
+
)}
|
|
2787
|
+
</div>
|
|
2788
|
+
</div>
|
|
2789
|
+
<div
|
|
2790
|
+
className={styles.templateCategory}
|
|
2791
|
+
>
|
|
2792
|
+
{template.category}
|
|
2793
|
+
</div>
|
|
2794
|
+
</div>
|
|
2795
|
+
);
|
|
2796
|
+
})}
|
|
2797
|
+
</div>
|
|
2798
|
+
</div>
|
|
2799
|
+
|
|
2336
2800
|
{/* Saved Reports List */}
|
|
2337
2801
|
{isLoadingReports ? (
|
|
2338
2802
|
<div className={styles.loading}>
|
|
@@ -2369,9 +2833,21 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2369
2833
|
styles.reportVisibility
|
|
2370
2834
|
}
|
|
2371
2835
|
>
|
|
2372
|
-
{report.is_public
|
|
2373
|
-
|
|
2374
|
-
|
|
2836
|
+
{report.is_public ? (
|
|
2837
|
+
<Globe
|
|
2838
|
+
size={14}
|
|
2839
|
+
style={{
|
|
2840
|
+
color: '#10b981',
|
|
2841
|
+
}}
|
|
2842
|
+
/>
|
|
2843
|
+
) : (
|
|
2844
|
+
<Lock
|
|
2845
|
+
size={14}
|
|
2846
|
+
style={{
|
|
2847
|
+
color: '#6b7280',
|
|
2848
|
+
}}
|
|
2849
|
+
/>
|
|
2850
|
+
)}
|
|
2375
2851
|
</span>
|
|
2376
2852
|
<span
|
|
2377
2853
|
className={
|
|
@@ -2395,7 +2871,15 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2395
2871
|
styles.reportTable
|
|
2396
2872
|
}
|
|
2397
2873
|
>
|
|
2398
|
-
|
|
2874
|
+
<Data
|
|
2875
|
+
size={16}
|
|
2876
|
+
style={{
|
|
2877
|
+
marginRight:
|
|
2878
|
+
'4px',
|
|
2879
|
+
verticalAlign:
|
|
2880
|
+
'middle',
|
|
2881
|
+
}}
|
|
2882
|
+
/>
|
|
2399
2883
|
{formatName(
|
|
2400
2884
|
typeof report.detail ===
|
|
2401
2885
|
'string'
|
|
@@ -2626,8 +3110,9 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2626
3110
|
{tables.map((table) => {
|
|
2627
3111
|
const TableIcon = getTableIcon(table.name);
|
|
2628
3112
|
const isSelected = selectedTable === table.name;
|
|
2629
|
-
const displayName =
|
|
3113
|
+
const displayName = getUniqueTableDisplayName(
|
|
2630
3114
|
table.name,
|
|
3115
|
+
tables,
|
|
2631
3116
|
definition
|
|
2632
3117
|
);
|
|
2633
3118
|
const description = getTableDescription(
|
|
@@ -2709,22 +3194,62 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2709
3194
|
);
|
|
2710
3195
|
}
|
|
2711
3196
|
|
|
3197
|
+
// Get all available tables for disambiguation
|
|
3198
|
+
const allAvailableTables = [
|
|
3199
|
+
selectedTable,
|
|
3200
|
+
...joins.map((join) => join.targetTable),
|
|
3201
|
+
];
|
|
3202
|
+
|
|
2712
3203
|
// Get all available columns (main table + joined tables)
|
|
2713
3204
|
const allAvailableColumns = [
|
|
2714
3205
|
{
|
|
2715
3206
|
tableName: selectedTable,
|
|
2716
|
-
displayName:
|
|
3207
|
+
displayName: getUniqueTableDisplayName(
|
|
3208
|
+
selectedTable,
|
|
3209
|
+
allAvailableTables.map((t) => ({ name: t })),
|
|
3210
|
+
definition
|
|
3211
|
+
),
|
|
2717
3212
|
columns: tableColumns,
|
|
2718
3213
|
isMainTable: true,
|
|
2719
3214
|
},
|
|
2720
3215
|
...joins.map((join) => ({
|
|
2721
3216
|
tableName: join.targetTable,
|
|
2722
|
-
displayName:
|
|
3217
|
+
displayName: getUniqueTableDisplayName(
|
|
3218
|
+
join.targetTable,
|
|
3219
|
+
allAvailableTables.map((t) => ({ name: t })),
|
|
3220
|
+
definition
|
|
3221
|
+
),
|
|
2723
3222
|
columns: join.availableColumns || [],
|
|
2724
3223
|
isMainTable: false,
|
|
2725
3224
|
})),
|
|
2726
3225
|
];
|
|
2727
3226
|
|
|
3227
|
+
// Add calculated fields if template is selected or custom fields exist
|
|
3228
|
+
const templateCalculatedFields =
|
|
3229
|
+
selectedTemplate && selectedTemplate.calculatedFields
|
|
3230
|
+
? selectedTemplate.calculatedFields
|
|
3231
|
+
: [];
|
|
3232
|
+
const allCalculatedFields = [
|
|
3233
|
+
...templateCalculatedFields,
|
|
3234
|
+
...customCalculatedFields,
|
|
3235
|
+
];
|
|
3236
|
+
|
|
3237
|
+
if (allCalculatedFields.length > 0) {
|
|
3238
|
+
const calculatedFields = {
|
|
3239
|
+
tableName: 'calculated',
|
|
3240
|
+
displayName: 'Calculated Fields',
|
|
3241
|
+
columns: allCalculatedFields.map((calc) => ({
|
|
3242
|
+
name: calc.id,
|
|
3243
|
+
type: calc.type,
|
|
3244
|
+
formula: calc.formula,
|
|
3245
|
+
displayName: calc.displayName,
|
|
3246
|
+
isCalculated: true,
|
|
3247
|
+
})),
|
|
3248
|
+
isCalculated: true,
|
|
3249
|
+
};
|
|
3250
|
+
allAvailableColumns.push(calculatedFields);
|
|
3251
|
+
}
|
|
3252
|
+
|
|
2728
3253
|
// Use enhanced categorization with smart filtering for main table
|
|
2729
3254
|
const mainTableCategories = categorizeFields(
|
|
2730
3255
|
tableColumns,
|
|
@@ -2796,20 +3321,43 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2796
3321
|
onClick={() => {
|
|
2797
3322
|
const allVisibleColumns = [];
|
|
2798
3323
|
allAvailableColumns.forEach((tableData) => {
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3324
|
+
if (tableData.isCalculated) {
|
|
3325
|
+
// Handle calculated fields
|
|
3326
|
+
const calculatedColumns =
|
|
3327
|
+
tableData.columns.map((col) => ({
|
|
3328
|
+
table: null, // No table for calculated fields
|
|
3329
|
+
column: col.name,
|
|
3330
|
+
displayName:
|
|
3331
|
+
col.displayName ||
|
|
3332
|
+
formatName(col.name),
|
|
3333
|
+
isCalculated: true,
|
|
3334
|
+
formula: col.formula,
|
|
3335
|
+
type: col.type,
|
|
3336
|
+
}));
|
|
3337
|
+
allVisibleColumns.push(
|
|
3338
|
+
...calculatedColumns
|
|
3339
|
+
);
|
|
3340
|
+
} else {
|
|
3341
|
+
// Handle regular table columns
|
|
3342
|
+
const visibleColumns = tableData.columns
|
|
3343
|
+
.filter(
|
|
3344
|
+
(col) =>
|
|
3345
|
+
!shouldHideField(
|
|
3346
|
+
col.name,
|
|
3347
|
+
col.type
|
|
3348
|
+
)
|
|
3349
|
+
)
|
|
3350
|
+
.map((col) => ({
|
|
3351
|
+
table: tableData.tableName,
|
|
3352
|
+
column: col.name,
|
|
3353
|
+
displayName: formatName(
|
|
3354
|
+
col.name
|
|
3355
|
+
),
|
|
3356
|
+
}));
|
|
3357
|
+
allVisibleColumns.push(
|
|
3358
|
+
...visibleColumns
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
2813
3361
|
});
|
|
2814
3362
|
setSelectedColumns(allVisibleColumns);
|
|
2815
3363
|
}}
|
|
@@ -2847,6 +3395,20 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2847
3395
|
{tableData.displayName}
|
|
2848
3396
|
{!tableData.isMainTable && ' (Connected)'}
|
|
2849
3397
|
</h3>
|
|
3398
|
+
{tableData.isCalculated && (
|
|
3399
|
+
<button
|
|
3400
|
+
className={
|
|
3401
|
+
styles.addCalculatedFieldButton
|
|
3402
|
+
}
|
|
3403
|
+
onClick={() =>
|
|
3404
|
+
setShowCalculatedFieldModal(true)
|
|
3405
|
+
}
|
|
3406
|
+
type="button"
|
|
3407
|
+
>
|
|
3408
|
+
<CirclePlus size={16} />
|
|
3409
|
+
Add Calculated Field
|
|
3410
|
+
</button>
|
|
3411
|
+
)}
|
|
2850
3412
|
<span className={styles.tableColumnCount}>
|
|
2851
3413
|
{
|
|
2852
3414
|
tableData.columns.filter((col) =>
|
|
@@ -2991,37 +3553,100 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
2991
3553
|
<label
|
|
2992
3554
|
key={column.name}
|
|
2993
3555
|
className={
|
|
2994
|
-
|
|
3556
|
+
column.isCalculated
|
|
3557
|
+
? styles.calculatedColumnItem
|
|
3558
|
+
: styles.columnItem
|
|
2995
3559
|
}
|
|
2996
3560
|
>
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3561
|
+
{column.isCalculated ? (
|
|
3562
|
+
<>
|
|
3563
|
+
<div
|
|
3564
|
+
className={
|
|
3565
|
+
styles.calculatedColumnHeader
|
|
3566
|
+
}
|
|
3567
|
+
>
|
|
3568
|
+
<input
|
|
3569
|
+
type="checkbox"
|
|
3570
|
+
checked={
|
|
3571
|
+
true
|
|
3572
|
+
}
|
|
3573
|
+
disabled={
|
|
3574
|
+
true
|
|
3575
|
+
}
|
|
3576
|
+
readOnly
|
|
3577
|
+
/>
|
|
3578
|
+
<span
|
|
3579
|
+
className={
|
|
3580
|
+
styles.calculatedColumnName
|
|
3581
|
+
}
|
|
3582
|
+
>
|
|
3583
|
+
{column.displayName ||
|
|
3584
|
+
formatName(
|
|
3585
|
+
column.name
|
|
3586
|
+
)}
|
|
3587
|
+
<span
|
|
3588
|
+
className={
|
|
3589
|
+
styles.calculatedBadge
|
|
3590
|
+
}
|
|
3591
|
+
>
|
|
3592
|
+
<Data
|
|
3593
|
+
size={
|
|
3594
|
+
16
|
|
3595
|
+
}
|
|
3596
|
+
style={{
|
|
3597
|
+
marginRight:
|
|
3598
|
+
'4px',
|
|
3599
|
+
verticalAlign:
|
|
3600
|
+
'middle',
|
|
3601
|
+
}}
|
|
3602
|
+
/>
|
|
3603
|
+
Calculated
|
|
3604
|
+
</span>
|
|
3605
|
+
</span>
|
|
3606
|
+
</div>
|
|
3607
|
+
<div
|
|
3608
|
+
className={
|
|
3609
|
+
styles.calculatedColumnFormula
|
|
3610
|
+
}
|
|
3611
|
+
>
|
|
3612
|
+
{column.formula}
|
|
3613
|
+
</div>
|
|
3614
|
+
</>
|
|
3615
|
+
) : (
|
|
3616
|
+
<>
|
|
3617
|
+
<input
|
|
3618
|
+
type="checkbox"
|
|
3619
|
+
checked={
|
|
3620
|
+
isSelected
|
|
3621
|
+
}
|
|
3622
|
+
onChange={() =>
|
|
3623
|
+
handleColumnToggle(
|
|
3624
|
+
column,
|
|
3625
|
+
tableData.tableName
|
|
3626
|
+
)
|
|
3627
|
+
}
|
|
3628
|
+
/>
|
|
3629
|
+
<span
|
|
3630
|
+
className={
|
|
3631
|
+
styles.columnName
|
|
3632
|
+
}
|
|
3633
|
+
>
|
|
3634
|
+
{column.displayName ||
|
|
3635
|
+
formatName(
|
|
3636
|
+
column.name
|
|
3637
|
+
)}
|
|
3638
|
+
</span>
|
|
3639
|
+
<span
|
|
3640
|
+
className={
|
|
3641
|
+
styles.columnType
|
|
3642
|
+
}
|
|
3643
|
+
>
|
|
3644
|
+
{getFriendlyTerm(
|
|
3645
|
+
column.type
|
|
3646
|
+
)}
|
|
3647
|
+
</span>
|
|
3648
|
+
</>
|
|
3649
|
+
)}
|
|
3025
3650
|
</label>
|
|
3026
3651
|
);
|
|
3027
3652
|
})}
|
|
@@ -3033,45 +3658,131 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
3033
3658
|
|
|
3034
3659
|
{selectedColumns.length > 0 && (
|
|
3035
3660
|
<div className={styles.selectedSummary}>
|
|
3036
|
-
<
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3661
|
+
<div className={styles.selectedSummaryHeader}>
|
|
3662
|
+
<strong>
|
|
3663
|
+
{selectedColumns.length} fields selected
|
|
3664
|
+
</strong>
|
|
3665
|
+
<button
|
|
3666
|
+
className={styles.expandToggle}
|
|
3667
|
+
onClick={() =>
|
|
3668
|
+
setShowSelectedFields(!showSelectedFields)
|
|
3669
|
+
}
|
|
3670
|
+
type="button"
|
|
3671
|
+
>
|
|
3672
|
+
<ChevronDown
|
|
3673
|
+
size={16}
|
|
3674
|
+
style={{
|
|
3675
|
+
transform: showSelectedFields
|
|
3676
|
+
? 'rotate(180deg)'
|
|
3677
|
+
: 'rotate(0deg)',
|
|
3678
|
+
transition: 'transform 0.2s ease',
|
|
3679
|
+
}}
|
|
3680
|
+
/>
|
|
3681
|
+
</button>
|
|
3682
|
+
</div>
|
|
3683
|
+
{showSelectedFields && (
|
|
3684
|
+
<div className={styles.selectedFieldsList}>
|
|
3685
|
+
{selectedColumns.map((col) => (
|
|
3686
|
+
<div
|
|
3687
|
+
key={`${col.table}-${col.column}`}
|
|
3688
|
+
className={styles.selectedField}
|
|
3689
|
+
>
|
|
3690
|
+
{col.isCalculated ? (
|
|
3691
|
+
<>
|
|
3692
|
+
<span
|
|
3693
|
+
className={
|
|
3694
|
+
styles.selectedFieldColumn
|
|
3695
|
+
}
|
|
3696
|
+
>
|
|
3697
|
+
{col.displayName ||
|
|
3698
|
+
col.column}
|
|
3699
|
+
</span>
|
|
3700
|
+
<span
|
|
3701
|
+
className={
|
|
3702
|
+
styles.calculatedBadge
|
|
3703
|
+
}
|
|
3704
|
+
style={{
|
|
3705
|
+
marginLeft: '8px',
|
|
3706
|
+
fontSize: '0.7rem',
|
|
3707
|
+
}}
|
|
3708
|
+
>
|
|
3709
|
+
<Data
|
|
3710
|
+
size={12}
|
|
3711
|
+
style={{
|
|
3712
|
+
marginRight: '2px',
|
|
3713
|
+
verticalAlign:
|
|
3714
|
+
'middle',
|
|
3715
|
+
}}
|
|
3716
|
+
/>
|
|
3717
|
+
Calculated
|
|
3718
|
+
</span>
|
|
3719
|
+
</>
|
|
3720
|
+
) : (
|
|
3721
|
+
<>
|
|
3722
|
+
<span
|
|
3723
|
+
className={
|
|
3724
|
+
styles.selectedFieldTable
|
|
3725
|
+
}
|
|
3726
|
+
>
|
|
3727
|
+
{col.table}
|
|
3728
|
+
</span>
|
|
3729
|
+
<span
|
|
3730
|
+
className={
|
|
3731
|
+
styles.selectedFieldDivider
|
|
3732
|
+
}
|
|
3733
|
+
>
|
|
3734
|
+
.
|
|
3735
|
+
</span>
|
|
3736
|
+
<span
|
|
3737
|
+
className={
|
|
3738
|
+
styles.selectedFieldColumn
|
|
3739
|
+
}
|
|
3740
|
+
>
|
|
3741
|
+
{col.displayName ||
|
|
3742
|
+
col.column}
|
|
3743
|
+
</span>
|
|
3744
|
+
</>
|
|
3745
|
+
)}
|
|
3746
|
+
</div>
|
|
3747
|
+
))}
|
|
3748
|
+
</div>
|
|
3749
|
+
)}
|
|
3750
|
+
</div>
|
|
3751
|
+
)}
|
|
3752
|
+
</div>
|
|
3753
|
+
);
|
|
3754
|
+
};
|
|
3755
|
+
|
|
3756
|
+
// Render relationships (simplified)
|
|
3757
|
+
const renderRelationships = () => {
|
|
3758
|
+
const tableSuggestions = detectedJoins[selectedTable] || [];
|
|
3759
|
+
const isLoading = isLoadingDetectedJoins[selectedTable] || false;
|
|
3760
|
+
|
|
3761
|
+
return (
|
|
3762
|
+
<div className={styles.relationshipsSection}>
|
|
3763
|
+
<div className={styles.helpBox}>
|
|
3764
|
+
<Info size={20} />
|
|
3765
|
+
<div>
|
|
3766
|
+
<h4>What are relationships?</h4>
|
|
3767
|
+
<p>
|
|
3768
|
+
Relationships allow you to include information from
|
|
3769
|
+
related data. For example, if you're reporting on
|
|
3770
|
+
invoices, you might want to include the client's
|
|
3771
|
+
name from the clients table.
|
|
3772
|
+
</p>
|
|
3773
|
+
</div>
|
|
3774
|
+
</div>
|
|
3775
|
+
|
|
3776
|
+
{/* Current Joins */}
|
|
3777
|
+
{joins.length > 0 && (
|
|
3778
|
+
<div className={styles.currentJoins}>
|
|
3779
|
+
<h3>Connected Data</h3>
|
|
3780
|
+
{joins.map((join, index) => (
|
|
3781
|
+
<div key={index} className={styles.joinItem}>
|
|
3782
|
+
<div className={styles.joinInfo}>
|
|
3783
|
+
<span className={styles.joinDescription}>
|
|
3784
|
+
{getTableDisplayName(
|
|
3785
|
+
selectedTable,
|
|
3075
3786
|
definition
|
|
3076
3787
|
)}{' '}
|
|
3077
3788
|
→{' '}
|
|
@@ -3597,6 +4308,117 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
3597
4308
|
handleRemoveFilterCriterion(groupIndex, filterIndex);
|
|
3598
4309
|
};
|
|
3599
4310
|
|
|
4311
|
+
// Apply template-based quick filter
|
|
4312
|
+
const applyTemplateQuickFilter = (quickFilter) => {
|
|
4313
|
+
let filterValue = quickFilter.value;
|
|
4314
|
+
|
|
4315
|
+
// Handle dynamic date ranges
|
|
4316
|
+
switch (quickFilter.value) {
|
|
4317
|
+
case 'current_week':
|
|
4318
|
+
filterValue = {
|
|
4319
|
+
start: moment().startOf('week').format('YYYY-MM-DD'),
|
|
4320
|
+
end: moment().endOf('week').format('YYYY-MM-DD'),
|
|
4321
|
+
};
|
|
4322
|
+
break;
|
|
4323
|
+
case 'next_30_days':
|
|
4324
|
+
filterValue = {
|
|
4325
|
+
start: moment().format('YYYY-MM-DD'),
|
|
4326
|
+
end: moment().add(30, 'days').format('YYYY-MM-DD'),
|
|
4327
|
+
};
|
|
4328
|
+
break;
|
|
4329
|
+
case 'last_30_days':
|
|
4330
|
+
filterValue = {
|
|
4331
|
+
start: moment().subtract(30, 'days').format('YYYY-MM-DD'),
|
|
4332
|
+
end: moment().format('YYYY-MM-DD'),
|
|
4333
|
+
};
|
|
4334
|
+
break;
|
|
4335
|
+
case 'last_quarter':
|
|
4336
|
+
filterValue = {
|
|
4337
|
+
start: moment()
|
|
4338
|
+
.subtract(1, 'quarter')
|
|
4339
|
+
.startOf('quarter')
|
|
4340
|
+
.format('YYYY-MM-DD'),
|
|
4341
|
+
end: moment()
|
|
4342
|
+
.subtract(1, 'quarter')
|
|
4343
|
+
.endOf('quarter')
|
|
4344
|
+
.format('YYYY-MM-DD'),
|
|
4345
|
+
};
|
|
4346
|
+
break;
|
|
4347
|
+
case 'last_year':
|
|
4348
|
+
filterValue = {
|
|
4349
|
+
start: moment()
|
|
4350
|
+
.subtract(1, 'year')
|
|
4351
|
+
.startOf('year')
|
|
4352
|
+
.format('YYYY-MM-DD'),
|
|
4353
|
+
end: moment()
|
|
4354
|
+
.subtract(1, 'year')
|
|
4355
|
+
.endOf('year')
|
|
4356
|
+
.format('YYYY-MM-DD'),
|
|
4357
|
+
};
|
|
4358
|
+
break;
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
const newFilter = {
|
|
4362
|
+
id: `template_${quickFilter.id}_${Date.now()}`,
|
|
4363
|
+
field: quickFilter.field,
|
|
4364
|
+
operator:
|
|
4365
|
+
quickFilter.operator ||
|
|
4366
|
+
(quickFilter.type === 'date_range' ? 'BETWEEN' : '='),
|
|
4367
|
+
value: filterValue,
|
|
4368
|
+
label: quickFilter.label,
|
|
4369
|
+
type: quickFilter.type,
|
|
4370
|
+
};
|
|
4371
|
+
|
|
4372
|
+
// Add to filter criteria
|
|
4373
|
+
const updatedFilterCriteria = JSON.parse(
|
|
4374
|
+
JSON.stringify(filterCriteria)
|
|
4375
|
+
);
|
|
4376
|
+
|
|
4377
|
+
if (updatedFilterCriteria.groups.length === 0) {
|
|
4378
|
+
updatedFilterCriteria.groups.push({
|
|
4379
|
+
operator: 'AND',
|
|
4380
|
+
filters: [],
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
updatedFilterCriteria.groups[0].filters.push(newFilter);
|
|
4385
|
+
setFilterCriteria(updatedFilterCriteria);
|
|
4386
|
+
|
|
4387
|
+
toast.success(`Applied filter: ${quickFilter.label}`);
|
|
4388
|
+
};
|
|
4389
|
+
|
|
4390
|
+
// Render template quick filters
|
|
4391
|
+
const renderTemplateQuickFilters = () => {
|
|
4392
|
+
if (
|
|
4393
|
+
!selectedTemplate ||
|
|
4394
|
+
!selectedTemplate.quickFilters ||
|
|
4395
|
+
selectedTemplate.quickFilters.length === 0
|
|
4396
|
+
) {
|
|
4397
|
+
return null;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
return (
|
|
4401
|
+
<div className={styles.templateQuickFilters}>
|
|
4402
|
+
<h4>Quick Filters for {selectedTemplate.name}</h4>
|
|
4403
|
+
<div className={styles.quickFiltersGrid}>
|
|
4404
|
+
{selectedTemplate.quickFilters.map((quickFilter) => (
|
|
4405
|
+
<button
|
|
4406
|
+
key={quickFilter.id}
|
|
4407
|
+
className={styles.quickFilterBtn}
|
|
4408
|
+
onClick={() =>
|
|
4409
|
+
applyTemplateQuickFilter(quickFilter)
|
|
4410
|
+
}
|
|
4411
|
+
title={`Apply ${quickFilter.label} filter`}
|
|
4412
|
+
>
|
|
4413
|
+
<Filter size={14} />
|
|
4414
|
+
{quickFilter.label}
|
|
4415
|
+
</button>
|
|
4416
|
+
))}
|
|
4417
|
+
</div>
|
|
4418
|
+
</div>
|
|
4419
|
+
);
|
|
4420
|
+
};
|
|
4421
|
+
|
|
3600
4422
|
// Add quick filter based on common patterns
|
|
3601
4423
|
const addQuickFilter = (type) => {
|
|
3602
4424
|
const availableColumns = [...selectedColumns];
|
|
@@ -3771,6 +4593,25 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
3771
4593
|
|
|
3772
4594
|
// Advanced filter rendering (from original GenericReport)
|
|
3773
4595
|
const renderFilters = () => {
|
|
4596
|
+
// Ensure filterCriteria has proper structure
|
|
4597
|
+
if (!filterCriteria || !filterCriteria.groups) {
|
|
4598
|
+
console.error(
|
|
4599
|
+
'FilterCriteria is not properly structured, reinitializing...'
|
|
4600
|
+
);
|
|
4601
|
+
setFilterCriteria({
|
|
4602
|
+
operator: 'AND',
|
|
4603
|
+
groups: [
|
|
4604
|
+
{
|
|
4605
|
+
operator: 'AND',
|
|
4606
|
+
filters: [],
|
|
4607
|
+
},
|
|
4608
|
+
],
|
|
4609
|
+
});
|
|
4610
|
+
return (
|
|
4611
|
+
<div className={styles.loading}>Initializing filters...</div>
|
|
4612
|
+
);
|
|
4613
|
+
}
|
|
4614
|
+
|
|
3774
4615
|
const availableColumns = getAvailableFilterColumns();
|
|
3775
4616
|
|
|
3776
4617
|
// Smart suggestions for sorting and filtering based on table type and relationships
|
|
@@ -4338,6 +5179,9 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
4338
5179
|
|
|
4339
5180
|
return (
|
|
4340
5181
|
<div className={styles.filtersSection}>
|
|
5182
|
+
{/* Template Quick Filters */}
|
|
5183
|
+
{renderTemplateQuickFilters()}
|
|
5184
|
+
|
|
4341
5185
|
{/* Smart Suggestions Section */}
|
|
4342
5186
|
{smartSuggestions.length > 0 && (
|
|
4343
5187
|
<div className={styles.reportSection}>
|
|
@@ -4725,306 +5569,350 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
4725
5569
|
</div>
|
|
4726
5570
|
|
|
4727
5571
|
{/* Filter Groups */}
|
|
4728
|
-
{filterCriteria.groups
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
<div className={styles.
|
|
4735
|
-
<
|
|
4736
|
-
|
|
4737
|
-
<
|
|
4738
|
-
|
|
4739
|
-
<
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
styles.operatorButton
|
|
4743
|
-
} ${
|
|
4744
|
-
group.operator === 'AND'
|
|
4745
|
-
? styles.operatorButtonSelected
|
|
4746
|
-
: ''
|
|
4747
|
-
}`}
|
|
4748
|
-
onClick={() =>
|
|
4749
|
-
handleFilterGroupOperatorChange(
|
|
4750
|
-
groupIndex,
|
|
4751
|
-
'AND'
|
|
4752
|
-
)
|
|
4753
|
-
}
|
|
4754
|
-
>
|
|
4755
|
-
ALL
|
|
4756
|
-
</button>
|
|
4757
|
-
<button
|
|
4758
|
-
type="button"
|
|
4759
|
-
className={`${
|
|
4760
|
-
styles.operatorButton
|
|
4761
|
-
} ${
|
|
4762
|
-
group.operator === 'OR'
|
|
4763
|
-
? styles.operatorButtonSelected
|
|
4764
|
-
: ''
|
|
4765
|
-
}`}
|
|
4766
|
-
onClick={() =>
|
|
4767
|
-
handleFilterGroupOperatorChange(
|
|
4768
|
-
groupIndex,
|
|
4769
|
-
'OR'
|
|
4770
|
-
)
|
|
5572
|
+
{filterCriteria.groups &&
|
|
5573
|
+
filterCriteria.groups.map((group, groupIndex) => (
|
|
5574
|
+
<div
|
|
5575
|
+
key={`group-${groupIndex}`}
|
|
5576
|
+
className={styles.filterGroup}
|
|
5577
|
+
>
|
|
5578
|
+
<div className={styles.filterGroupHeader}>
|
|
5579
|
+
<div className={styles.filterGroupTitle}>
|
|
5580
|
+
<h4>Filter Group {groupIndex + 1}</h4>
|
|
5581
|
+
<div className={styles.filterGroupOperator}>
|
|
5582
|
+
<label>Match:</label>
|
|
5583
|
+
<div
|
|
5584
|
+
className={
|
|
5585
|
+
styles.operatorButtonGroup
|
|
4771
5586
|
}
|
|
4772
5587
|
>
|
|
4773
|
-
|
|
4774
|
-
|
|
5588
|
+
<button
|
|
5589
|
+
type="button"
|
|
5590
|
+
className={`${
|
|
5591
|
+
styles.operatorButton
|
|
5592
|
+
} ${
|
|
5593
|
+
group.operator === 'AND'
|
|
5594
|
+
? styles.operatorButtonSelected
|
|
5595
|
+
: ''
|
|
5596
|
+
}`}
|
|
5597
|
+
onClick={() =>
|
|
5598
|
+
handleFilterGroupOperatorChange(
|
|
5599
|
+
groupIndex,
|
|
5600
|
+
'AND'
|
|
5601
|
+
)
|
|
5602
|
+
}
|
|
5603
|
+
>
|
|
5604
|
+
ALL
|
|
5605
|
+
</button>
|
|
5606
|
+
<button
|
|
5607
|
+
type="button"
|
|
5608
|
+
className={`${
|
|
5609
|
+
styles.operatorButton
|
|
5610
|
+
} ${
|
|
5611
|
+
group.operator === 'OR'
|
|
5612
|
+
? styles.operatorButtonSelected
|
|
5613
|
+
: ''
|
|
5614
|
+
}`}
|
|
5615
|
+
onClick={() =>
|
|
5616
|
+
handleFilterGroupOperatorChange(
|
|
5617
|
+
groupIndex,
|
|
5618
|
+
'OR'
|
|
5619
|
+
)
|
|
5620
|
+
}
|
|
5621
|
+
>
|
|
5622
|
+
ANY
|
|
5623
|
+
</button>
|
|
5624
|
+
</div>
|
|
4775
5625
|
</div>
|
|
4776
5626
|
</div>
|
|
4777
|
-
</div>
|
|
4778
5627
|
|
|
4779
|
-
|
|
4780
|
-
<button
|
|
4781
|
-
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
4782
|
-
onClick={() =>
|
|
4783
|
-
handleAddFilterCriterion(groupIndex)
|
|
4784
|
-
}
|
|
4785
|
-
disabled={selectedColumns.length === 0}
|
|
4786
|
-
>
|
|
4787
|
-
Add Filter
|
|
4788
|
-
</button>
|
|
4789
|
-
{filterCriteria.groups.length > 1 && (
|
|
5628
|
+
<div className={styles.filterGroupActions}>
|
|
4790
5629
|
<button
|
|
4791
|
-
className={styles.
|
|
5630
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
4792
5631
|
onClick={() =>
|
|
4793
|
-
|
|
5632
|
+
handleAddFilterCriterion(groupIndex)
|
|
4794
5633
|
}
|
|
4795
|
-
|
|
5634
|
+
disabled={selectedColumns.length === 0}
|
|
4796
5635
|
>
|
|
4797
|
-
|
|
5636
|
+
Add Filter
|
|
4798
5637
|
</button>
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
</div>
|
|
4802
|
-
|
|
4803
|
-
{/* Filter Criteria in Group */}
|
|
4804
|
-
{group.filters.length > 0 ? (
|
|
4805
|
-
<div className={styles.filterCriteriaContainer}>
|
|
4806
|
-
{group.filters.map((criterion, filterIndex) => (
|
|
4807
|
-
<div
|
|
4808
|
-
key={`filter-${groupIndex}-${filterIndex}`}
|
|
4809
|
-
className={styles.filterCriterion}
|
|
4810
|
-
>
|
|
4811
|
-
<div
|
|
4812
|
-
className={
|
|
4813
|
-
styles.filterCriterionHeader
|
|
4814
|
-
}
|
|
4815
|
-
>
|
|
4816
|
-
<h4>
|
|
4817
|
-
Filter {filterIndex + 1}:{' '}
|
|
4818
|
-
{criterion.table &&
|
|
4819
|
-
criterion.column
|
|
4820
|
-
? `${formatName(
|
|
4821
|
-
criterion.table
|
|
4822
|
-
)}.${formatName(
|
|
4823
|
-
criterion.column
|
|
4824
|
-
)}`
|
|
4825
|
-
: `Criterion #${
|
|
4826
|
-
filterIndex + 1
|
|
4827
|
-
}`}
|
|
4828
|
-
</h4>
|
|
5638
|
+
{filterCriteria.groups &&
|
|
5639
|
+
filterCriteria.groups.length > 1 && (
|
|
4829
5640
|
<button
|
|
4830
5641
|
className={styles.removeJoinBtn}
|
|
4831
5642
|
onClick={() =>
|
|
4832
|
-
|
|
4833
|
-
groupIndex
|
|
4834
|
-
filterIndex
|
|
5643
|
+
handleRemoveFilterGroup(
|
|
5644
|
+
groupIndex
|
|
4835
5645
|
)
|
|
4836
5646
|
}
|
|
4837
|
-
title="Remove Filter
|
|
5647
|
+
title="Remove Filter Group"
|
|
4838
5648
|
>
|
|
4839
5649
|
×
|
|
4840
5650
|
</button>
|
|
4841
|
-
|
|
5651
|
+
)}
|
|
5652
|
+
</div>
|
|
5653
|
+
</div>
|
|
4842
5654
|
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
5655
|
+
{/* Filter Criteria in Group */}
|
|
5656
|
+
{group.filters.length > 0 ? (
|
|
5657
|
+
<div className={styles.filterCriteriaContainer}>
|
|
5658
|
+
{group.filters.map(
|
|
5659
|
+
(criterion, filterIndex) => (
|
|
5660
|
+
<div
|
|
5661
|
+
key={`filter-${groupIndex}-${filterIndex}`}
|
|
5662
|
+
className={
|
|
5663
|
+
styles.filterCriterion
|
|
5664
|
+
}
|
|
5665
|
+
>
|
|
5666
|
+
<div
|
|
4852
5667
|
className={
|
|
4853
|
-
styles.
|
|
5668
|
+
styles.filterCriterionHeader
|
|
4854
5669
|
}
|
|
4855
|
-
|
|
4856
|
-
|
|
5670
|
+
>
|
|
5671
|
+
<h4>
|
|
5672
|
+
Filter {filterIndex + 1}
|
|
5673
|
+
:{' '}
|
|
5674
|
+
{criterion.table &&
|
|
4857
5675
|
criterion.column
|
|
4858
|
-
? `${
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
e.target.value.substring(
|
|
4872
|
-
0,
|
|
4873
|
-
dotIndex
|
|
4874
|
-
);
|
|
4875
|
-
const column =
|
|
4876
|
-
e.target.value.substring(
|
|
4877
|
-
dotIndex +
|
|
4878
|
-
1
|
|
4879
|
-
);
|
|
4880
|
-
handleFilterCriterionChange(
|
|
4881
|
-
groupIndex,
|
|
4882
|
-
filterIndex,
|
|
4883
|
-
'table',
|
|
4884
|
-
table
|
|
4885
|
-
);
|
|
4886
|
-
handleFilterCriterionChange(
|
|
4887
|
-
groupIndex,
|
|
4888
|
-
filterIndex,
|
|
4889
|
-
'column',
|
|
4890
|
-
column
|
|
4891
|
-
);
|
|
4892
|
-
}
|
|
5676
|
+
? `${formatName(
|
|
5677
|
+
criterion.table
|
|
5678
|
+
)}.${formatName(
|
|
5679
|
+
criterion.column
|
|
5680
|
+
)}`
|
|
5681
|
+
: `Criterion #${
|
|
5682
|
+
filterIndex +
|
|
5683
|
+
1
|
|
5684
|
+
}`}
|
|
5685
|
+
</h4>
|
|
5686
|
+
<button
|
|
5687
|
+
className={
|
|
5688
|
+
styles.removeJoinBtn
|
|
4893
5689
|
}
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
{col.tableName}.
|
|
4906
|
-
{col.columnName}
|
|
4907
|
-
</option>
|
|
4908
|
-
)
|
|
4909
|
-
)}
|
|
4910
|
-
</select>
|
|
4911
|
-
</div>
|
|
4912
|
-
|
|
4913
|
-
{/* Operator Selection */}
|
|
4914
|
-
<div className={styles.filterField}>
|
|
4915
|
-
<label>Operator:</label>
|
|
4916
|
-
<select
|
|
4917
|
-
className={
|
|
4918
|
-
styles.filterSelect
|
|
4919
|
-
}
|
|
4920
|
-
value={criterion.operator}
|
|
4921
|
-
onChange={(e) =>
|
|
4922
|
-
handleFilterCriterionChange(
|
|
4923
|
-
groupIndex,
|
|
4924
|
-
filterIndex,
|
|
4925
|
-
'operator',
|
|
4926
|
-
e.target.value
|
|
4927
|
-
)
|
|
4928
|
-
}
|
|
4929
|
-
>
|
|
4930
|
-
<option value="=">
|
|
4931
|
-
equals
|
|
4932
|
-
</option>
|
|
4933
|
-
<option value="!=">
|
|
4934
|
-
does not equal
|
|
4935
|
-
</option>
|
|
4936
|
-
<option value="LIKE">
|
|
4937
|
-
contains
|
|
4938
|
-
</option>
|
|
4939
|
-
<option value="NOT LIKE">
|
|
4940
|
-
does not contain
|
|
4941
|
-
</option>
|
|
4942
|
-
<option value=">">
|
|
4943
|
-
greater than
|
|
4944
|
-
</option>
|
|
4945
|
-
<option value="<">
|
|
4946
|
-
less than
|
|
4947
|
-
</option>
|
|
4948
|
-
<option value=">=">
|
|
4949
|
-
greater than or equal to
|
|
4950
|
-
</option>
|
|
4951
|
-
<option value="<=">
|
|
4952
|
-
less than or equal to
|
|
4953
|
-
</option>
|
|
4954
|
-
<option value="IN">
|
|
4955
|
-
is one of
|
|
4956
|
-
</option>
|
|
4957
|
-
<option value="NOT IN">
|
|
4958
|
-
is not one of
|
|
4959
|
-
</option>
|
|
4960
|
-
<option value="BETWEEN">
|
|
4961
|
-
is between
|
|
4962
|
-
</option>
|
|
4963
|
-
<option value="IS NULL">
|
|
4964
|
-
is empty
|
|
4965
|
-
</option>
|
|
4966
|
-
<option value="IS NOT NULL">
|
|
4967
|
-
is not empty
|
|
4968
|
-
</option>
|
|
4969
|
-
</select>
|
|
4970
|
-
</div>
|
|
5690
|
+
onClick={() =>
|
|
5691
|
+
handleRemoveFilterCriterion(
|
|
5692
|
+
groupIndex,
|
|
5693
|
+
filterIndex
|
|
5694
|
+
)
|
|
5695
|
+
}
|
|
5696
|
+
title="Remove Filter Criterion"
|
|
5697
|
+
>
|
|
5698
|
+
×
|
|
5699
|
+
</button>
|
|
5700
|
+
</div>
|
|
4971
5701
|
|
|
4972
|
-
{/* Value Input */}
|
|
4973
|
-
{![
|
|
4974
|
-
'IS NULL',
|
|
4975
|
-
'IS NOT NULL',
|
|
4976
|
-
].includes(criterion.operator) && (
|
|
4977
5702
|
<div
|
|
4978
5703
|
className={
|
|
4979
|
-
styles.
|
|
5704
|
+
styles.filterCriterionFields
|
|
4980
5705
|
}
|
|
4981
5706
|
>
|
|
4982
|
-
|
|
4983
|
-
<
|
|
4984
|
-
type="text"
|
|
5707
|
+
{/* Table.Column Selection */}
|
|
5708
|
+
<div
|
|
4985
5709
|
className={
|
|
4986
|
-
styles.
|
|
4987
|
-
}
|
|
4988
|
-
value={
|
|
4989
|
-
criterion.value ||
|
|
4990
|
-
''
|
|
5710
|
+
styles.filterField
|
|
4991
5711
|
}
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5712
|
+
>
|
|
5713
|
+
<label>Column:</label>
|
|
5714
|
+
<select
|
|
5715
|
+
className={
|
|
5716
|
+
styles.filterSelect
|
|
5717
|
+
}
|
|
5718
|
+
value={
|
|
5719
|
+
criterion.table &&
|
|
5720
|
+
criterion.column
|
|
5721
|
+
? `${criterion.table}.${criterion.column}`
|
|
5722
|
+
: ''
|
|
5723
|
+
}
|
|
5724
|
+
onChange={(e) => {
|
|
5725
|
+
if (
|
|
5726
|
+
e.target
|
|
5727
|
+
.value
|
|
5728
|
+
) {
|
|
5729
|
+
const dotIndex =
|
|
5730
|
+
e.target.value.indexOf(
|
|
5731
|
+
'.'
|
|
5732
|
+
);
|
|
5733
|
+
if (
|
|
5734
|
+
dotIndex !==
|
|
5735
|
+
-1
|
|
5736
|
+
) {
|
|
5737
|
+
const table =
|
|
5738
|
+
e.target.value.substring(
|
|
5739
|
+
0,
|
|
5740
|
+
dotIndex
|
|
5741
|
+
);
|
|
5742
|
+
const column =
|
|
5743
|
+
e.target.value.substring(
|
|
5744
|
+
dotIndex +
|
|
5745
|
+
1
|
|
5746
|
+
);
|
|
5747
|
+
handleFilterCriterionChange(
|
|
5748
|
+
groupIndex,
|
|
5749
|
+
filterIndex,
|
|
5750
|
+
'table',
|
|
5751
|
+
table
|
|
5752
|
+
);
|
|
5753
|
+
handleFilterCriterionChange(
|
|
5754
|
+
groupIndex,
|
|
5755
|
+
filterIndex,
|
|
5756
|
+
'column',
|
|
5757
|
+
column
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
}}
|
|
5762
|
+
>
|
|
5763
|
+
<option value="">
|
|
5764
|
+
Select a
|
|
5765
|
+
column...
|
|
5766
|
+
</option>
|
|
5767
|
+
{availableColumns.map(
|
|
5768
|
+
(col) => (
|
|
5769
|
+
<option
|
|
5770
|
+
key={`${col.table}.${col.column}`}
|
|
5771
|
+
value={`${col.table}.${col.column}`}
|
|
5772
|
+
>
|
|
5773
|
+
{
|
|
5774
|
+
col.tableName
|
|
5775
|
+
}
|
|
5776
|
+
.
|
|
5777
|
+
{
|
|
5778
|
+
col.columnName
|
|
5779
|
+
}
|
|
5780
|
+
</option>
|
|
5781
|
+
)
|
|
5782
|
+
)}
|
|
5783
|
+
</select>
|
|
5784
|
+
</div>
|
|
5785
|
+
|
|
5786
|
+
{/* Operator Selection */}
|
|
5787
|
+
<div
|
|
5788
|
+
className={
|
|
5789
|
+
styles.filterField
|
|
5010
5790
|
}
|
|
5011
|
-
|
|
5791
|
+
>
|
|
5792
|
+
<label>Operator:</label>
|
|
5793
|
+
<select
|
|
5794
|
+
className={
|
|
5795
|
+
styles.filterSelect
|
|
5796
|
+
}
|
|
5797
|
+
value={
|
|
5798
|
+
criterion.operator
|
|
5799
|
+
}
|
|
5800
|
+
onChange={(e) =>
|
|
5801
|
+
handleFilterCriterionChange(
|
|
5802
|
+
groupIndex,
|
|
5803
|
+
filterIndex,
|
|
5804
|
+
'operator',
|
|
5805
|
+
e.target
|
|
5806
|
+
.value
|
|
5807
|
+
)
|
|
5808
|
+
}
|
|
5809
|
+
>
|
|
5810
|
+
<option value="=">
|
|
5811
|
+
equals
|
|
5812
|
+
</option>
|
|
5813
|
+
<option value="!=">
|
|
5814
|
+
does not equal
|
|
5815
|
+
</option>
|
|
5816
|
+
<option value="LIKE">
|
|
5817
|
+
contains
|
|
5818
|
+
</option>
|
|
5819
|
+
<option value="NOT LIKE">
|
|
5820
|
+
does not contain
|
|
5821
|
+
</option>
|
|
5822
|
+
<option value=">">
|
|
5823
|
+
greater than
|
|
5824
|
+
</option>
|
|
5825
|
+
<option value="<">
|
|
5826
|
+
less than
|
|
5827
|
+
</option>
|
|
5828
|
+
<option value=">=">
|
|
5829
|
+
greater than or
|
|
5830
|
+
equal to
|
|
5831
|
+
</option>
|
|
5832
|
+
<option value="<=">
|
|
5833
|
+
less than or
|
|
5834
|
+
equal to
|
|
5835
|
+
</option>
|
|
5836
|
+
<option value="IN">
|
|
5837
|
+
is one of
|
|
5838
|
+
</option>
|
|
5839
|
+
<option value="NOT IN">
|
|
5840
|
+
is not one of
|
|
5841
|
+
</option>
|
|
5842
|
+
<option value="BETWEEN">
|
|
5843
|
+
is between
|
|
5844
|
+
</option>
|
|
5845
|
+
<option value="IS NULL">
|
|
5846
|
+
is empty
|
|
5847
|
+
</option>
|
|
5848
|
+
<option value="IS NOT NULL">
|
|
5849
|
+
is not empty
|
|
5850
|
+
</option>
|
|
5851
|
+
</select>
|
|
5852
|
+
</div>
|
|
5853
|
+
|
|
5854
|
+
{/* Value Input */}
|
|
5855
|
+
{![
|
|
5856
|
+
'IS NULL',
|
|
5857
|
+
'IS NOT NULL',
|
|
5858
|
+
].includes(
|
|
5859
|
+
criterion.operator
|
|
5860
|
+
) && (
|
|
5861
|
+
<div
|
|
5862
|
+
className={
|
|
5863
|
+
styles.filterField
|
|
5864
|
+
}
|
|
5865
|
+
>
|
|
5866
|
+
<label>
|
|
5867
|
+
Value:
|
|
5868
|
+
</label>
|
|
5869
|
+
<input
|
|
5870
|
+
type="text"
|
|
5871
|
+
className={
|
|
5872
|
+
styles.filterInput
|
|
5873
|
+
}
|
|
5874
|
+
value={
|
|
5875
|
+
criterion.value ||
|
|
5876
|
+
''
|
|
5877
|
+
}
|
|
5878
|
+
onChange={(e) =>
|
|
5879
|
+
handleFilterCriterionChange(
|
|
5880
|
+
groupIndex,
|
|
5881
|
+
filterIndex,
|
|
5882
|
+
'value',
|
|
5883
|
+
e.target
|
|
5884
|
+
.value
|
|
5885
|
+
)
|
|
5886
|
+
}
|
|
5887
|
+
placeholder={
|
|
5888
|
+
criterion.operator ===
|
|
5889
|
+
'BETWEEN'
|
|
5890
|
+
? 'Enter values separated by comma (e.g., 10,100)'
|
|
5891
|
+
: criterion.operator ===
|
|
5892
|
+
'IN' ||
|
|
5893
|
+
criterion.operator ===
|
|
5894
|
+
'NOT IN'
|
|
5895
|
+
? 'Enter values separated by comma'
|
|
5896
|
+
: 'Enter filter value'
|
|
5897
|
+
}
|
|
5898
|
+
/>
|
|
5899
|
+
</div>
|
|
5900
|
+
)}
|
|
5012
5901
|
</div>
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
</
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
))}
|
|
5902
|
+
</div>
|
|
5903
|
+
)
|
|
5904
|
+
)}
|
|
5905
|
+
</div>
|
|
5906
|
+
) : (
|
|
5907
|
+
<div className={styles.emptyFilterGroup}>
|
|
5908
|
+
<p>
|
|
5909
|
+
No filters in this group. Click "Add
|
|
5910
|
+
Filter" to add one.
|
|
5911
|
+
</p>
|
|
5912
|
+
</div>
|
|
5913
|
+
)}
|
|
5914
|
+
</div>
|
|
5915
|
+
))}
|
|
5028
5916
|
|
|
5029
5917
|
<div className={styles.skipOption}>
|
|
5030
5918
|
<p>
|
|
@@ -5099,7 +5987,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5099
5987
|
// Check for large datasets when exporting to PDF
|
|
5100
5988
|
if (format === 'pdf' && previewData.length > 1000) {
|
|
5101
5989
|
const result = await Swal.fire({
|
|
5102
|
-
title: '
|
|
5990
|
+
title: 'Large Dataset Warning',
|
|
5103
5991
|
html: `
|
|
5104
5992
|
<div style="text-align: left;">
|
|
5105
5993
|
<p>You are trying to export <strong>${previewData.length} rows</strong> to PDF.</p>
|
|
@@ -5139,6 +6027,10 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5139
6027
|
table: col.table,
|
|
5140
6028
|
column: col.column,
|
|
5141
6029
|
alias: col.alias || col.displayName,
|
|
6030
|
+
isCalculated: col.isCalculated || false,
|
|
6031
|
+
formula: col.formula,
|
|
6032
|
+
displayName: col.displayName,
|
|
6033
|
+
type: col.type,
|
|
5142
6034
|
})),
|
|
5143
6035
|
joins: joins.map((join) => ({
|
|
5144
6036
|
joinType: join.joinType,
|
|
@@ -5158,7 +6050,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5158
6050
|
|
|
5159
6051
|
// DEBUG: Log the complete request payload
|
|
5160
6052
|
console.group('🐛 PDF Export Debug');
|
|
5161
|
-
console.log('
|
|
6053
|
+
console.log('Export Request Details:');
|
|
5162
6054
|
console.log('URL:', exportUrl);
|
|
5163
6055
|
console.log('Format:', format);
|
|
5164
6056
|
console.log('Report Name:', reportName);
|
|
@@ -5190,13 +6082,13 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5190
6082
|
console.log('📡 Making request to:', exportUrl);
|
|
5191
6083
|
const result = await Download(exportUrl, 'POST', exportData);
|
|
5192
6084
|
|
|
5193
|
-
console.log('
|
|
6085
|
+
console.log('Download component returned result:', result);
|
|
5194
6086
|
|
|
5195
6087
|
if (result && result.data) {
|
|
5196
|
-
console.log('
|
|
5197
|
-
console.log('
|
|
6088
|
+
console.log('Result has data, triggering download');
|
|
6089
|
+
console.log('Result data type:', typeof result.data);
|
|
5198
6090
|
console.log(
|
|
5199
|
-
'
|
|
6091
|
+
'Result data instanceof Blob:',
|
|
5200
6092
|
result.data instanceof Blob
|
|
5201
6093
|
);
|
|
5202
6094
|
|
|
@@ -5235,18 +6127,13 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5235
6127
|
if (error.response.data instanceof Blob) {
|
|
5236
6128
|
// If error response is a blob, try to read it as text
|
|
5237
6129
|
try {
|
|
5238
|
-
console.log(
|
|
5239
|
-
'🔍 Attempting to parse blob error response'
|
|
5240
|
-
);
|
|
6130
|
+
console.log('Attempting to parse blob error response');
|
|
5241
6131
|
const errorText = await error.response.data.text();
|
|
5242
|
-
console.log('
|
|
6132
|
+
console.log('Blob content as text:', errorText);
|
|
5243
6133
|
|
|
5244
6134
|
try {
|
|
5245
6135
|
const errorData = JSON.parse(errorText);
|
|
5246
|
-
console.log(
|
|
5247
|
-
'📄 Parsed JSON error data:',
|
|
5248
|
-
errorData
|
|
5249
|
-
);
|
|
6136
|
+
console.log('Parsed JSON error data:', errorData);
|
|
5250
6137
|
errorMessage =
|
|
5251
6138
|
errorData.message ||
|
|
5252
6139
|
errorData.error ||
|
|
@@ -5267,13 +6154,13 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5267
6154
|
}
|
|
5268
6155
|
} else if (error.response.data && error.response.data.message) {
|
|
5269
6156
|
console.log(
|
|
5270
|
-
'
|
|
6157
|
+
'Using error.response.data.message:',
|
|
5271
6158
|
error.response.data.message
|
|
5272
6159
|
);
|
|
5273
6160
|
errorMessage = error.response.data.message;
|
|
5274
6161
|
} else if (error.response.data && error.response.data.error) {
|
|
5275
6162
|
console.log(
|
|
5276
|
-
'
|
|
6163
|
+
'Using error.response.data.error:',
|
|
5277
6164
|
error.response.data.error
|
|
5278
6165
|
);
|
|
5279
6166
|
errorMessage = error.response.data.error;
|
|
@@ -5323,7 +6210,7 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5323
6210
|
${
|
|
5324
6211
|
isExistingReport
|
|
5325
6212
|
? `<div style="background: #e3f2fd; border: 1px solid #2196f3; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
|
5326
|
-
<strong
|
|
6213
|
+
<strong><TriangleAlert size={16} style={{ marginRight: '4px', verticalAlign: 'middle' }} />This will overwrite the existing report:</strong><br>
|
|
5327
6214
|
"${reportName}"
|
|
5328
6215
|
</div>`
|
|
5329
6216
|
: ''
|
|
@@ -5588,6 +6475,127 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5588
6475
|
|
|
5589
6476
|
{/* Main content */}
|
|
5590
6477
|
<div className={styles.content}>{renderContent()}</div>
|
|
6478
|
+
|
|
6479
|
+
{/* Calculated Field Modal */}
|
|
6480
|
+
{showCalculatedFieldModal && (
|
|
6481
|
+
<div className={styles.modalOverlay}>
|
|
6482
|
+
<div className={styles.calculatedFieldModal}>
|
|
6483
|
+
<div className={styles.modalHeader}>
|
|
6484
|
+
<h3>Add Calculated Field</h3>
|
|
6485
|
+
<button
|
|
6486
|
+
className={styles.modalCloseButton}
|
|
6487
|
+
onClick={handleCalculatedFieldCancel}
|
|
6488
|
+
>
|
|
6489
|
+
✕
|
|
6490
|
+
</button>
|
|
6491
|
+
</div>
|
|
6492
|
+
|
|
6493
|
+
<div className={styles.modalBody}>
|
|
6494
|
+
<div className={styles.formGroup}>
|
|
6495
|
+
<label htmlFor="displayName">
|
|
6496
|
+
Display Name *
|
|
6497
|
+
</label>
|
|
6498
|
+
<input
|
|
6499
|
+
id="displayName"
|
|
6500
|
+
type="text"
|
|
6501
|
+
placeholder="e.g., Total Revenue"
|
|
6502
|
+
value={calculatedFieldForm.displayName}
|
|
6503
|
+
onChange={(e) =>
|
|
6504
|
+
setCalculatedFieldForm({
|
|
6505
|
+
...calculatedFieldForm,
|
|
6506
|
+
displayName: e.target.value,
|
|
6507
|
+
})
|
|
6508
|
+
}
|
|
6509
|
+
className={styles.formInput}
|
|
6510
|
+
/>
|
|
6511
|
+
</div>
|
|
6512
|
+
|
|
6513
|
+
<div className={styles.formGroup}>
|
|
6514
|
+
<label htmlFor="fieldType">Data Type</label>
|
|
6515
|
+
<select
|
|
6516
|
+
id="fieldType"
|
|
6517
|
+
value={calculatedFieldForm.type}
|
|
6518
|
+
onChange={(e) =>
|
|
6519
|
+
setCalculatedFieldForm({
|
|
6520
|
+
...calculatedFieldForm,
|
|
6521
|
+
type: e.target.value,
|
|
6522
|
+
})
|
|
6523
|
+
}
|
|
6524
|
+
className={styles.formSelect}
|
|
6525
|
+
>
|
|
6526
|
+
<option value="decimal">
|
|
6527
|
+
Decimal/Number
|
|
6528
|
+
</option>
|
|
6529
|
+
<option value="integer">Integer</option>
|
|
6530
|
+
<option value="string">Text/String</option>
|
|
6531
|
+
<option value="date">Date</option>
|
|
6532
|
+
</select>
|
|
6533
|
+
</div>
|
|
6534
|
+
|
|
6535
|
+
<div className={styles.formGroup}>
|
|
6536
|
+
<label htmlFor="formula">SQL Formula *</label>
|
|
6537
|
+
<textarea
|
|
6538
|
+
id="formula"
|
|
6539
|
+
placeholder="e.g., SUM(orders.amount) or COUNT(DISTINCT clients.id)"
|
|
6540
|
+
value={calculatedFieldForm.formula}
|
|
6541
|
+
onChange={(e) =>
|
|
6542
|
+
setCalculatedFieldForm({
|
|
6543
|
+
...calculatedFieldForm,
|
|
6544
|
+
formula: e.target.value,
|
|
6545
|
+
})
|
|
6546
|
+
}
|
|
6547
|
+
className={styles.formulaTextarea}
|
|
6548
|
+
rows={4}
|
|
6549
|
+
/>
|
|
6550
|
+
<div className={styles.formulaHelp}>
|
|
6551
|
+
<p>
|
|
6552
|
+
<strong>Examples:</strong>
|
|
6553
|
+
</p>
|
|
6554
|
+
<ul>
|
|
6555
|
+
<li>
|
|
6556
|
+
<code>SUM(orders.amount)</code> -
|
|
6557
|
+
Total of amounts
|
|
6558
|
+
</li>
|
|
6559
|
+
<li>
|
|
6560
|
+
<code>
|
|
6561
|
+
COUNT(DISTINCT clients.id)
|
|
6562
|
+
</code>{' '}
|
|
6563
|
+
- Unique client count
|
|
6564
|
+
</li>
|
|
6565
|
+
<li>
|
|
6566
|
+
<code>
|
|
6567
|
+
ROUND(AVG(leads.value), 2)
|
|
6568
|
+
</code>{' '}
|
|
6569
|
+
- Average lead value
|
|
6570
|
+
</li>
|
|
6571
|
+
<li>
|
|
6572
|
+
<code>
|
|
6573
|
+
DATEDIFF(end_date, start_date)
|
|
6574
|
+
</code>{' '}
|
|
6575
|
+
- Date difference
|
|
6576
|
+
</li>
|
|
6577
|
+
</ul>
|
|
6578
|
+
</div>
|
|
6579
|
+
</div>
|
|
6580
|
+
</div>
|
|
6581
|
+
|
|
6582
|
+
<div className={styles.modalFooter}>
|
|
6583
|
+
<button
|
|
6584
|
+
className={styles.cancelButton}
|
|
6585
|
+
onClick={handleCalculatedFieldCancel}
|
|
6586
|
+
>
|
|
6587
|
+
Cancel
|
|
6588
|
+
</button>
|
|
6589
|
+
<button
|
|
6590
|
+
className={styles.addButton}
|
|
6591
|
+
onClick={handleCalculatedFieldSubmit}
|
|
6592
|
+
>
|
|
6593
|
+
Add Calculated Field
|
|
6594
|
+
</button>
|
|
6595
|
+
</div>
|
|
6596
|
+
</div>
|
|
6597
|
+
</div>
|
|
6598
|
+
)}
|
|
5591
6599
|
</div>
|
|
5592
6600
|
);
|
|
5593
6601
|
};
|
|
@@ -5595,6 +6603,21 @@ const GenericReportImproved = ({ setting = {}, definition = null }) => {
|
|
|
5595
6603
|
GenericReportImproved.propTypes = {
|
|
5596
6604
|
setting: PropTypes.object,
|
|
5597
6605
|
definition: PropTypes.object,
|
|
6606
|
+
businessTemplates: PropTypes.arrayOf(
|
|
6607
|
+
PropTypes.shape({
|
|
6608
|
+
id: PropTypes.string.isRequired,
|
|
6609
|
+
name: PropTypes.string.isRequired,
|
|
6610
|
+
description: PropTypes.string.isRequired,
|
|
6611
|
+
category: PropTypes.string,
|
|
6612
|
+
icon: PropTypes.string,
|
|
6613
|
+
tags: PropTypes.arrayOf(PropTypes.string),
|
|
6614
|
+
mainTable: PropTypes.string.isRequired,
|
|
6615
|
+
suggestedJoins: PropTypes.array,
|
|
6616
|
+
defaultColumns: PropTypes.array,
|
|
6617
|
+
quickFilters: PropTypes.array,
|
|
6618
|
+
calculatedFields: PropTypes.array,
|
|
6619
|
+
})
|
|
6620
|
+
),
|
|
5598
6621
|
};
|
|
5599
6622
|
|
|
5600
6623
|
export default GenericReportImproved;
|