@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.
@@ -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 what type of information you want to report on',
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
- const GenericReportImproved = ({ setting = {}, definition = null }) => {
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 using Unicode symbols for SweetAlert2
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 `<span style="color: ${color}; font-size: ${size}px; margin-right: 6px; vertical-align: middle; display: inline-block;">${icons[iconName] || '📊'}</span>`;
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('chart', '#3b82f6', 24)}Complete Report Builder Guide</div>`,
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('users', '#15803d', 12)}Client List:</strong>
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('cart', '#1d4ed8', 12)}Recent Orders:</strong>
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('money', '#d97706', 12)}Financial Summary:</strong>
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('tasks', '#db2777', 12)}Active Projects:</strong>
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('clock', '#dc2626', 12)}Report too slow?</strong>
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('question', '#dc2626', 12)}Missing data?</strong>
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('warning', '#dc2626', 12)}Export failing?</strong>
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('eye', '#dc2626', 12)}No data showing?</strong>
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: "Close",
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
- const columnId = `${tableName}.${column.name}`;
1833
- const isSelected = selectedColumns.some(
1834
- (col) => `${col.table}.${col.column}` === columnId
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) => `${col.table}.${col.column}` !== columnId
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
- setSelectedColumns([
1845
- ...selectedColumns,
1846
- {
1847
- table: tableName,
1848
- column: column.name,
1849
- displayName: formatName(column.name),
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
- table: col.table,
1933
- column: col.column,
1934
- alias: col.alias || col.displayName,
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
- setFilterCriteria(config.filters);
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 = getTableDisplayName(
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: getTableDisplayName(selectedTable, definition),
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: getTableDisplayName(join.targetTable, definition),
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
- const visibleColumns = tableData.columns
2800
- .filter(
2801
- (col) =>
2802
- !shouldHideField(
2803
- col.name,
2804
- col.type
2805
- )
2806
- )
2807
- .map((col) => ({
2808
- table: tableData.tableName,
2809
- column: col.name,
2810
- displayName: formatName(col.name),
2811
- }));
2812
- allVisibleColumns.push(...visibleColumns);
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
- styles.columnItem
3556
+ column.isCalculated
3557
+ ? styles.calculatedColumnItem
3558
+ : styles.columnItem
2995
3559
  }
2996
3560
  >
2997
- <input
2998
- type="checkbox"
2999
- checked={isSelected}
3000
- onChange={() =>
3001
- handleColumnToggle(
3002
- column,
3003
- tableData.tableName
3004
- )
3005
- }
3006
- />
3007
- <span
3008
- className={
3009
- styles.columnName
3010
- }
3011
- >
3012
- {formatName(
3013
- column.name
3014
- )}
3015
- </span>
3016
- <span
3017
- className={
3018
- styles.columnType
3019
- }
3020
- >
3021
- {getFriendlyTerm(
3022
- column.type
3023
- )}
3024
- </span>
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
- <strong>
3037
- {selectedColumns.length} fields selected
3038
- </strong>
3039
- </div>
3040
- )}
3041
- </div>
3042
- );
3043
- };
3044
-
3045
- // Render relationships (simplified)
3046
- const renderRelationships = () => {
3047
- const tableSuggestions = detectedJoins[selectedTable] || [];
3048
- const isLoading = isLoadingDetectedJoins[selectedTable] || false;
3049
-
3050
- return (
3051
- <div className={styles.relationshipsSection}>
3052
- <div className={styles.helpBox}>
3053
- <Info size={20} />
3054
- <div>
3055
- <h4>What are relationships?</h4>
3056
- <p>
3057
- Relationships allow you to include information from
3058
- related data. For example, if you're reporting on
3059
- invoices, you might want to include the client's
3060
- name from the clients table.
3061
- </p>
3062
- </div>
3063
- </div>
3064
-
3065
- {/* Current Joins */}
3066
- {joins.length > 0 && (
3067
- <div className={styles.currentJoins}>
3068
- <h3>Connected Data</h3>
3069
- {joins.map((join, index) => (
3070
- <div key={index} className={styles.joinItem}>
3071
- <div className={styles.joinInfo}>
3072
- <span className={styles.joinDescription}>
3073
- {getTableDisplayName(
3074
- selectedTable,
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.map((group, groupIndex) => (
4729
- <div
4730
- key={`group-${groupIndex}`}
4731
- className={styles.filterGroup}
4732
- >
4733
- <div className={styles.filterGroupHeader}>
4734
- <div className={styles.filterGroupTitle}>
4735
- <h4>Filter Group {groupIndex + 1}</h4>
4736
- <div className={styles.filterGroupOperator}>
4737
- <label>Match:</label>
4738
- <div className={styles.operatorButtonGroup}>
4739
- <button
4740
- type="button"
4741
- className={`${
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
- ANY
4774
- </button>
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
- <div className={styles.filterGroupActions}>
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.removeJoinBtn}
5630
+ className={`${styles.btn} ${styles.btnSecondary}`}
4792
5631
  onClick={() =>
4793
- handleRemoveFilterGroup(groupIndex)
5632
+ handleAddFilterCriterion(groupIndex)
4794
5633
  }
4795
- title="Remove Filter Group"
5634
+ disabled={selectedColumns.length === 0}
4796
5635
  >
4797
- ×
5636
+ Add Filter
4798
5637
  </button>
4799
- )}
4800
- </div>
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
- handleRemoveFilterCriterion(
4833
- groupIndex,
4834
- filterIndex
5643
+ handleRemoveFilterGroup(
5644
+ groupIndex
4835
5645
  )
4836
5646
  }
4837
- title="Remove Filter Criterion"
5647
+ title="Remove Filter Group"
4838
5648
  >
4839
5649
  ×
4840
5650
  </button>
4841
- </div>
5651
+ )}
5652
+ </div>
5653
+ </div>
4842
5654
 
4843
- <div
4844
- className={
4845
- styles.filterCriterionFields
4846
- }
4847
- >
4848
- {/* Table.Column Selection */}
4849
- <div className={styles.filterField}>
4850
- <label>Column:</label>
4851
- <select
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.filterSelect
5668
+ styles.filterCriterionHeader
4854
5669
  }
4855
- value={
4856
- criterion.table &&
5670
+ >
5671
+ <h4>
5672
+ Filter {filterIndex + 1}
5673
+ :{' '}
5674
+ {criterion.table &&
4857
5675
  criterion.column
4858
- ? `${criterion.table}.${criterion.column}`
4859
- : ''
4860
- }
4861
- onChange={(e) => {
4862
- if (e.target.value) {
4863
- const dotIndex =
4864
- e.target.value.indexOf(
4865
- '.'
4866
- );
4867
- if (
4868
- dotIndex !== -1
4869
- ) {
4870
- const table =
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
- <option value="">
4897
- Select a column...
4898
- </option>
4899
- {availableColumns.map(
4900
- (col) => (
4901
- <option
4902
- key={`${col.table}.${col.column}`}
4903
- value={`${col.table}.${col.column}`}
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.filterField
5704
+ styles.filterCriterionFields
4980
5705
  }
4981
5706
  >
4982
- <label>Value:</label>
4983
- <input
4984
- type="text"
5707
+ {/* Table.Column Selection */}
5708
+ <div
4985
5709
  className={
4986
- styles.filterInput
4987
- }
4988
- value={
4989
- criterion.value ||
4990
- ''
5710
+ styles.filterField
4991
5711
  }
4992
- onChange={(e) =>
4993
- handleFilterCriterionChange(
4994
- groupIndex,
4995
- filterIndex,
4996
- 'value',
4997
- e.target.value
4998
- )
4999
- }
5000
- placeholder={
5001
- criterion.operator ===
5002
- 'BETWEEN'
5003
- ? 'Enter values separated by comma (e.g., 10,100)'
5004
- : criterion.operator ===
5005
- 'IN' ||
5006
- criterion.operator ===
5007
- 'NOT IN'
5008
- ? 'Enter values separated by comma'
5009
- : 'Enter filter value'
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
- </div>
5015
- </div>
5016
- ))}
5017
- </div>
5018
- ) : (
5019
- <div className={styles.emptyFilterGroup}>
5020
- <p>
5021
- No filters in this group. Click "Add Filter"
5022
- to add one.
5023
- </p>
5024
- </div>
5025
- )}
5026
- </div>
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: '⚠️ Large Dataset Warning',
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('📋 Export Request Details:');
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('Download component returned result:', result);
6085
+ console.log('Download component returned result:', result);
5194
6086
 
5195
6087
  if (result && result.data) {
5196
- console.log('Result has data, triggering download');
5197
- console.log('📄 Result data type:', typeof result.data);
6088
+ console.log('Result has data, triggering download');
6089
+ console.log('Result data type:', typeof result.data);
5198
6090
  console.log(
5199
- '📄 Result data instanceof Blob:',
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('📄 Blob content as text:', errorText);
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
- '📄 Using error.response.data.message:',
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
- '📄 Using error.response.data.error:',
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>⚠️ This will overwrite the existing report:</strong><br>
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;