@visns-studio/visns-components 5.5.6 → 5.5.8

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.
@@ -177,6 +177,47 @@ const isJsonValue = (value) => {
177
177
  return typeof value === 'object' && value !== null;
178
178
  };
179
179
 
180
+ // Helper function to convert the enhanced filter structure to a flat array for the backend
181
+ const flattenFilterCriteria = (filterCriteria) => {
182
+ // If it's already a flat array (legacy format), return it as is
183
+ if (Array.isArray(filterCriteria)) {
184
+ return filterCriteria;
185
+ }
186
+
187
+ // If it's the new structure with groups and operators
188
+ if (filterCriteria.groups && Array.isArray(filterCriteria.groups)) {
189
+ const flattenedFilters = [];
190
+
191
+ // Process each group
192
+ filterCriteria.groups.forEach((group, groupIndex) => {
193
+ // Only add filters from non-empty groups
194
+ if (group.filters && group.filters.length > 0) {
195
+ // Add all filters from this group
196
+ group.filters.forEach((filter) => {
197
+ // Add a special property to indicate which group this filter belongs to
198
+ // and the operator to use within this group
199
+ const enhancedFilter = {
200
+ ...filter,
201
+ _group: groupIndex,
202
+ _group_operator: group.operator,
203
+ };
204
+ flattenedFilters.push(enhancedFilter);
205
+ });
206
+ }
207
+ });
208
+
209
+ // If we have multiple groups, add the main operator as a property to the first filter
210
+ if (filterCriteria.groups.length > 1 && flattenedFilters.length > 0) {
211
+ flattenedFilters[0]._main_operator = filterCriteria.operator;
212
+ }
213
+
214
+ return flattenedFilters;
215
+ }
216
+
217
+ // If it's neither a flat array nor the new structure, return an empty array
218
+ return [];
219
+ };
220
+
180
221
  const GenericReport = ({ setting = {} }) => {
181
222
  // Extract settings with defaults
182
223
  const { tableUrl, columnUrl } = setting;
@@ -219,8 +260,10 @@ const GenericReport = ({ setting = {} }) => {
219
260
  <li style="margin-bottom: 4px; font-size: 14px !important;">You can sort by multiple columns (e.g., sort by last name, then by first name)</li>
220
261
  <li style="margin-bottom: 4px; font-size: 14px !important;">Choose between ascending (A-Z, 0-9) or descending (Z-A, 9-0) order</li>
221
262
  <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Filtering</strong>: Click the "Filtering" header to expand the section, then add filter criteria</li>
222
- <li style="margin-bottom: 4px; font-size: 14px !important;">Use operators like equals, contains, greater than, etc. to narrow down results</li>
223
- <li style="margin-bottom: 4px; font-size: 14px !important;">Combine multiple filters for precise data selection (all filters are combined with AND logic)</li>
263
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Use operators like "contains" or "is exactly equal to" to narrow down results</li>
264
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Create multiple filter groups and choose whether to join them with ALL or ANY logic</li>
265
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Within each filter group, choose whether conditions are joined with ALL (all must match) or ANY (any can match)</li>
266
+ <li style="margin-bottom: 4px; font-size: 14px !important;">This powerful combination allows for complex queries like "(A AND B) OR (C AND D)"</li>
224
267
  <li style="font-size: 14px !important;">Sorting and filtering settings are saved with your report for future use</li>
225
268
  </ul>
226
269
 
@@ -233,11 +276,69 @@ const GenericReport = ({ setting = {} }) => {
233
276
  <li style="margin-bottom: 4px; font-size: 14px !important;">Consider adding date fields to enable time-based filtering</li>
234
277
  <li style="font-size: 14px !important;">Save frequently used reports for quick access</li>
235
278
  </ul>
279
+ </div>
280
+ `,
281
+ width: 550,
282
+ confirmButtonText: 'Got it!',
283
+ confirmButtonColor: '#2563eb',
284
+ customClass: {
285
+ title: 'swal-title',
286
+ htmlContainer: 'swal-html-container',
287
+ confirmButton: 'swal-confirm-button',
288
+ },
289
+ });
290
+ };
236
291
 
292
+ // Help function specifically for filtering
293
+ const showFilteringHelp = () => {
294
+ Swal.fire({
295
+ title: 'Understanding Filtering',
296
+ html: `
297
+ <div style="text-align: left; margin-bottom: 20px; font-size: 14px; color: #4b5563;">
298
+ <h4 style="font-size: 16px; color: #1f2937; margin-bottom: 10px; font-weight: 600;">Filtering Basics:</h4>
299
+ <p style="margin-bottom: 8px; line-height: 1.4;">Filtering allows you to narrow down your report results to show only the data you're interested in.</p>
237
300
 
301
+ <h4 style="font-size: 16px; color: #1f2937; margin-bottom: 10px; margin-top: 16px; font-weight: 600;">Filter Groups:</h4>
302
+ <p style="margin-bottom: 8px; line-height: 1.4;">You can create multiple filter groups to build complex conditions:</p>
303
+ <ul style="padding-left: 18px; line-height: 1.4; font-size: 14px !important; margin-bottom: 14px;">
304
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Each filter group can contain one or more conditions</li>
305
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Within a group, you can choose whether ALL conditions must match or ANY condition can match</li>
306
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Between groups, you can also choose ALL groups must match or ANY group can match</li>
307
+ </ul>
308
+
309
+ <h4 style="font-size: 16px; color: #1f2937; margin-bottom: 10px; margin-top: 16px; font-weight: 600;">Filter Operators:</h4>
310
+ <p style="margin-bottom: 8px; line-height: 1.4;">Each filter condition uses an operator to compare values:</p>
311
+ <ul style="padding-left: 18px; line-height: 1.4; font-size: 14px !important; margin-bottom: 14px;">
312
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Is exactly equal to</strong>: Matches records where the field exactly matches your value</li>
313
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Is not equal to</strong>: Matches records where the field does not match your value</li>
314
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Contains</strong>: Matches records where the field contains your value anywhere within it</li>
315
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Does not contain</strong>: Matches records where the field does not contain your value</li>
316
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Is greater than</strong>: Matches records where the field is greater than your value (for numbers and dates)</li>
317
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Is less than</strong>: Matches records where the field is less than your value (for numbers and dates)</li>
318
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Is empty</strong>: Matches records where the field has no value</li>
319
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Is not empty</strong>: Matches records where the field has any value</li>
320
+ </ul>
321
+
322
+ <h4 style="font-size: 16px; color: #1f2937; margin-bottom: 10px; margin-top: 16px; font-weight: 600;">Examples:</h4>
323
+ <p style="margin-bottom: 8px; line-height: 1.4;">Here are some examples of how to use filtering:</p>
324
+ <ul style="padding-left: 18px; line-height: 1.4; font-size: 14px !important; margin-bottom: 14px;">
325
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Simple filter</strong>: Find all clients where Name contains "ABC"</li>
326
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Multiple conditions (ALL)</strong>: Find clients where Name contains "ABC" AND State is "NSW"</li>
327
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Multiple conditions (ANY)</strong>: Find clients where Name contains "ABC" OR Name contains "XYZ"</li>
328
+ <li style="margin-bottom: 4px; font-size: 14px !important;"><strong>Complex filter</strong>: Find clients where (Name contains "ABC" AND State is "NSW") OR (Name contains "XYZ" AND State is "VIC")</li>
329
+ </ul>
330
+
331
+ <h4 style="font-size: 16px; color: #1f2937; margin-bottom: 10px; margin-top: 16px; font-weight: 600;">Tips for Effective Filtering:</h4>
332
+ <ul style="padding-left: 18px; line-height: 1.4; font-size: 14px !important; margin-bottom: 14px;">
333
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Start with simple filters and add complexity as needed</li>
334
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Use "Contains" for partial text matching (e.g., searching for part of a name)</li>
335
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Use date filters to find records within specific time periods</li>
336
+ <li style="margin-bottom: 4px; font-size: 14px !important;">Group related conditions together in the same filter group</li>
337
+ <li style="font-size: 14px !important;">Test your filters by executing the query to make sure they work as expected</li>
338
+ </ul>
238
339
  </div>
239
340
  `,
240
- width: 550,
341
+ width: 650,
241
342
  confirmButtonText: 'Got it!',
242
343
  confirmButtonColor: '#2563eb',
243
344
  customClass: {
@@ -351,7 +452,18 @@ const GenericReport = ({ setting = {} }) => {
351
452
 
352
453
  // State for sorting and filtering
353
454
  const [sortCriteria, setSortCriteria] = useState([]);
354
- const [filterCriteria, setFilterCriteria] = useState([]);
455
+
456
+ // Enhanced filter criteria with support for AND/OR logic
457
+ // Structure: { operator: 'AND'|'OR', groups: [{ operator: 'AND'|'OR', filters: [...] }] }
458
+ const [filterCriteria, setFilterCriteria] = useState({
459
+ operator: 'AND',
460
+ groups: [
461
+ {
462
+ operator: 'AND',
463
+ filters: [],
464
+ },
465
+ ],
466
+ });
355
467
 
356
468
  // State for report execution
357
469
  const [totalResults, setTotalResults] = useState(0);
@@ -889,8 +1001,8 @@ const GenericReport = ({ setting = {} }) => {
889
1001
  }
890
1002
  };
891
1003
 
892
- // Add a new filter criterion
893
- const handleAddFilterCriterion = () => {
1004
+ // Add a new filter criterion to a specific group
1005
+ const handleAddFilterCriterion = (groupIndex = 0) => {
894
1006
  // Default to the first selected column if available
895
1007
  const defaultColumn =
896
1008
  selectedColumns.length > 0
@@ -908,25 +1020,132 @@ const GenericReport = ({ setting = {} }) => {
908
1020
  value: '',
909
1021
  };
910
1022
 
911
- setFilterCriteria([...filterCriteria, newFilterCriterion]);
1023
+ // Create a deep copy of the current filter criteria
1024
+ const updatedFilterCriteria = JSON.parse(
1025
+ JSON.stringify(filterCriteria)
1026
+ );
1027
+
1028
+ // Add the new filter to the specified group
1029
+ updatedFilterCriteria.groups[groupIndex].filters.push(
1030
+ newFilterCriterion
1031
+ );
1032
+
1033
+ setFilterCriteria(updatedFilterCriteria);
1034
+ setShowFilteringSection(true);
1035
+ };
1036
+
1037
+ // Add a new filter group
1038
+ const handleAddFilterGroup = () => {
1039
+ // Create a deep copy of the current filter criteria
1040
+ const updatedFilterCriteria = JSON.parse(
1041
+ JSON.stringify(filterCriteria)
1042
+ );
1043
+
1044
+ // Add a new empty filter group
1045
+ updatedFilterCriteria.groups.push({
1046
+ operator: 'AND',
1047
+ filters: [],
1048
+ });
1049
+
1050
+ setFilterCriteria(updatedFilterCriteria);
912
1051
  setShowFilteringSection(true);
913
1052
  };
914
1053
 
915
1054
  // Update a filter criterion
916
- const handleFilterCriterionChange = (index, field, value) => {
917
- const updatedFilterCriteria = [...filterCriteria];
918
- updatedFilterCriteria[index][field] = value;
1055
+ const handleFilterCriterionChange = (
1056
+ groupIndex,
1057
+ filterIndex,
1058
+ field,
1059
+ value
1060
+ ) => {
1061
+ // Create a deep copy of the current filter criteria
1062
+ const updatedFilterCriteria = JSON.parse(
1063
+ JSON.stringify(filterCriteria)
1064
+ );
1065
+
1066
+ // Update the specified field of the filter
1067
+ updatedFilterCriteria.groups[groupIndex].filters[filterIndex][field] =
1068
+ value;
1069
+
919
1070
  setFilterCriteria(updatedFilterCriteria);
920
1071
  };
921
1072
 
922
1073
  // Remove a filter criterion
923
- const handleRemoveFilterCriterion = (index) => {
924
- const updatedFilterCriteria = [...filterCriteria];
925
- updatedFilterCriteria.splice(index, 1);
1074
+ const handleRemoveFilterCriterion = (groupIndex, filterIndex) => {
1075
+ // Create a deep copy of the current filter criteria
1076
+ const updatedFilterCriteria = JSON.parse(
1077
+ JSON.stringify(filterCriteria)
1078
+ );
1079
+
1080
+ // Remove the filter from the specified group
1081
+ updatedFilterCriteria.groups[groupIndex].filters.splice(filterIndex, 1);
1082
+
926
1083
  setFilterCriteria(updatedFilterCriteria);
927
1084
 
928
- // Hide the section if no filter criteria remain
929
- if (updatedFilterCriteria.length === 0) {
1085
+ // Check if all groups are empty
1086
+ const allGroupsEmpty = updatedFilterCriteria.groups.every(
1087
+ (group) => group.filters.length === 0
1088
+ );
1089
+
1090
+ // Hide the section if all groups are empty
1091
+ if (allGroupsEmpty) {
1092
+ setShowFilteringSection(false);
1093
+ }
1094
+ };
1095
+
1096
+ // Update a filter group's operator (AND/OR)
1097
+ const handleFilterGroupOperatorChange = (groupIndex, newOperator) => {
1098
+ // Create a deep copy of the current filter criteria
1099
+ const updatedFilterCriteria = JSON.parse(
1100
+ JSON.stringify(filterCriteria)
1101
+ );
1102
+
1103
+ // Update the operator for the specified group
1104
+ updatedFilterCriteria.groups[groupIndex].operator = newOperator;
1105
+
1106
+ setFilterCriteria(updatedFilterCriteria);
1107
+ };
1108
+
1109
+ // Update the main filter operator (AND/OR between groups)
1110
+ const handleMainFilterOperatorChange = (newOperator) => {
1111
+ // Create a deep copy of the current filter criteria
1112
+ const updatedFilterCriteria = JSON.parse(
1113
+ JSON.stringify(filterCriteria)
1114
+ );
1115
+
1116
+ // Update the main operator
1117
+ updatedFilterCriteria.operator = newOperator;
1118
+
1119
+ setFilterCriteria(updatedFilterCriteria);
1120
+ };
1121
+
1122
+ // Remove a filter group
1123
+ const handleRemoveFilterGroup = (groupIndex) => {
1124
+ // Create a deep copy of the current filter criteria
1125
+ const updatedFilterCriteria = JSON.parse(
1126
+ JSON.stringify(filterCriteria)
1127
+ );
1128
+
1129
+ // Remove the group
1130
+ updatedFilterCriteria.groups.splice(groupIndex, 1);
1131
+
1132
+ // If no groups remain, add an empty one
1133
+ if (updatedFilterCriteria.groups.length === 0) {
1134
+ updatedFilterCriteria.groups.push({
1135
+ operator: 'AND',
1136
+ filters: [],
1137
+ });
1138
+ }
1139
+
1140
+ setFilterCriteria(updatedFilterCriteria);
1141
+
1142
+ // Check if all groups are empty
1143
+ const allGroupsEmpty = updatedFilterCriteria.groups.every(
1144
+ (group) => group.filters.length === 0
1145
+ );
1146
+
1147
+ // Hide the section if all groups are empty
1148
+ if (allGroupsEmpty) {
930
1149
  setShowFilteringSection(false);
931
1150
  }
932
1151
  };
@@ -1134,11 +1353,36 @@ const GenericReport = ({ setting = {} }) => {
1134
1353
  }
1135
1354
 
1136
1355
  // Set filtering criteria
1137
- if (detail.filters && Array.isArray(detail.filters)) {
1138
- setFilterCriteria(detail.filters);
1139
- // Show filtering section if there are filter criteria
1140
- if (detail.filters.length > 0) {
1141
- setShowFilteringSection(true);
1356
+ if (detail.filters) {
1357
+ // Check if it's the new structure with groups and operators
1358
+ if (
1359
+ detail.filters.groups &&
1360
+ Array.isArray(detail.filters.groups)
1361
+ ) {
1362
+ setFilterCriteria(detail.filters);
1363
+ // Show filtering section if there are filter criteria in any group
1364
+ const hasFilters = detail.filters.groups.some(
1365
+ (group) => group.filters && group.filters.length > 0
1366
+ );
1367
+ if (hasFilters) {
1368
+ setShowFilteringSection(true);
1369
+ }
1370
+ } else if (Array.isArray(detail.filters)) {
1371
+ // Handle legacy format - convert to new structure
1372
+ const newFilterCriteria = {
1373
+ operator: 'AND',
1374
+ groups: [
1375
+ {
1376
+ operator: 'AND',
1377
+ filters: detail.filters,
1378
+ },
1379
+ ],
1380
+ };
1381
+ setFilterCriteria(newFilterCriteria);
1382
+ // Show filtering section if there are filter criteria
1383
+ if (detail.filters.length > 0) {
1384
+ setShowFilteringSection(true);
1385
+ }
1142
1386
  }
1143
1387
  }
1144
1388
 
@@ -1222,11 +1466,37 @@ const GenericReport = ({ setting = {} }) => {
1222
1466
  }
1223
1467
 
1224
1468
  // Set filtering criteria
1225
- if (detail.filters && Array.isArray(detail.filters)) {
1226
- setFilterCriteria(detail.filters);
1227
- // Show filtering section if there are filter criteria
1228
- if (detail.filters.length > 0) {
1229
- setShowFilteringSection(true);
1469
+ if (detail.filters) {
1470
+ // Check if it's the new structure with groups and operators
1471
+ if (
1472
+ detail.filters.groups &&
1473
+ Array.isArray(detail.filters.groups)
1474
+ ) {
1475
+ setFilterCriteria(detail.filters);
1476
+ // Show filtering section if there are filter criteria in any group
1477
+ const hasFilters = detail.filters.groups.some(
1478
+ (group) =>
1479
+ group.filters && group.filters.length > 0
1480
+ );
1481
+ if (hasFilters) {
1482
+ setShowFilteringSection(true);
1483
+ }
1484
+ } else if (Array.isArray(detail.filters)) {
1485
+ // Handle legacy format - convert to new structure
1486
+ const newFilterCriteria = {
1487
+ operator: 'AND',
1488
+ groups: [
1489
+ {
1490
+ operator: 'AND',
1491
+ filters: detail.filters,
1492
+ },
1493
+ ],
1494
+ };
1495
+ setFilterCriteria(newFilterCriteria);
1496
+ // Show filtering section if there are filter criteria
1497
+ if (detail.filters.length > 0) {
1498
+ setShowFilteringSection(true);
1499
+ }
1230
1500
  }
1231
1501
  }
1232
1502
 
@@ -1268,7 +1538,8 @@ const GenericReport = ({ setting = {} }) => {
1268
1538
  sourceColumn: join.sourceColumn,
1269
1539
  targetColumn: join.targetColumn,
1270
1540
  })),
1271
- filters: filterCriteria,
1541
+ // Convert the enhanced filter structure to a flat array that the backend expects
1542
+ filters: flattenFilterCriteria(filterCriteria),
1272
1543
  sorting: sortCriteria,
1273
1544
  };
1274
1545
 
@@ -1445,7 +1716,8 @@ const GenericReport = ({ setting = {} }) => {
1445
1716
  sourceColumn: join.sourceColumn,
1446
1717
  targetColumn: join.targetColumn,
1447
1718
  })),
1448
- filters: filterCriteria,
1719
+ filters: filterCriteria, // Save the enhanced filter structure with groups and operators
1720
+ // Note: We're saving the full structure here, not flattening it, so we can load it properly later
1449
1721
  sorting: sortCriteria,
1450
1722
  };
1451
1723
 
@@ -1517,6 +1789,14 @@ const GenericReport = ({ setting = {} }) => {
1517
1789
  <Question size={18} />
1518
1790
  <span>Data types</span>
1519
1791
  </button>
1792
+ <button
1793
+ className={styles.helpButton}
1794
+ onClick={showFilteringHelp}
1795
+ title="Learn about filtering"
1796
+ >
1797
+ <Question size={18} />
1798
+ <span>Filtering</span>
1799
+ </button>
1520
1800
  </div>
1521
1801
  </div>
1522
1802
  <div className={styles.titleInfo}>
@@ -2832,14 +3112,26 @@ const GenericReport = ({ setting = {} }) => {
2832
3112
  }
2833
3113
  >
2834
3114
  <h3>Filtering</h3>
2835
- <div
2836
- className={`${styles.toggleBtn} ${
2837
- showFilteringSection
2838
- ? styles.toggleBtnActive
2839
- : ''
2840
- }`}
2841
- >
2842
- {showFilteringSection ? '▲' : '▼'}
3115
+ <div className={styles.sectionHeaderActions}>
3116
+ <button
3117
+ className={styles.helpButton}
3118
+ onClick={(e) => {
3119
+ e.stopPropagation();
3120
+ showFilteringHelp();
3121
+ }}
3122
+ title="Learn about filtering"
3123
+ >
3124
+ <Question size={16} />
3125
+ </button>
3126
+ <div
3127
+ className={`${styles.toggleBtn} ${
3128
+ showFilteringSection
3129
+ ? styles.toggleBtnActive
3130
+ : ''
3131
+ }`}
3132
+ >
3133
+ {showFilteringSection ? '▲' : '▼'}
3134
+ </div>
2843
3135
  </div>
2844
3136
  </div>
2845
3137
 
@@ -2847,164 +3139,334 @@ const GenericReport = ({ setting = {} }) => {
2847
3139
  <>
2848
3140
  <div
2849
3141
  className={
2850
- styles.addManualJoinContainer
3142
+ styles.filterOperatorContainer
2851
3143
  }
2852
3144
  >
2853
- <button
2854
- className={styles.btn}
2855
- onClick={handleAddFilterCriterion}
2856
- disabled={
2857
- selectedColumns.length === 0
3145
+ <div
3146
+ className={
3147
+ styles.mainFilterOperator
2858
3148
  }
2859
3149
  >
2860
- Add Filter Criterion
2861
- </button>
2862
- </div>
3150
+ <label>Match:</label>
3151
+ <div
3152
+ className={
3153
+ styles.operatorOptions
3154
+ }
3155
+ >
3156
+ <div
3157
+ className={`${
3158
+ styles.operatorOption
3159
+ } ${
3160
+ filterCriteria.operator ===
3161
+ 'AND'
3162
+ ? styles.operatorOptionSelected
3163
+ : ''
3164
+ }`}
3165
+ onClick={() =>
3166
+ handleMainFilterOperatorChange(
3167
+ 'AND'
3168
+ )
3169
+ }
3170
+ >
3171
+ <div
3172
+ className={
3173
+ styles.operatorName
3174
+ }
3175
+ >
3176
+ ALL
3177
+ </div>
3178
+ <div
3179
+ className={
3180
+ styles.operatorDescription
3181
+ }
3182
+ >
3183
+ All filter groups must
3184
+ match
3185
+ </div>
3186
+ </div>
3187
+ <div
3188
+ className={`${
3189
+ styles.operatorOption
3190
+ } ${
3191
+ filterCriteria.operator ===
3192
+ 'OR'
3193
+ ? styles.operatorOptionSelected
3194
+ : ''
3195
+ }`}
3196
+ onClick={() =>
3197
+ handleMainFilterOperatorChange(
3198
+ 'OR'
3199
+ )
3200
+ }
3201
+ >
3202
+ <div
3203
+ className={
3204
+ styles.operatorName
3205
+ }
3206
+ >
3207
+ ANY
3208
+ </div>
3209
+ <div
3210
+ className={
3211
+ styles.operatorDescription
3212
+ }
3213
+ >
3214
+ Any filter group can
3215
+ match
3216
+ </div>
3217
+ </div>
3218
+ </div>
3219
+ </div>
2863
3220
 
2864
- {filterCriteria.length > 0 && (
2865
3221
  <div
2866
3222
  className={
2867
- styles.joinTablesContainer
3223
+ styles.filterActionButtons
2868
3224
  }
2869
3225
  >
2870
- {filterCriteria.map(
2871
- (criterion, index) => (
3226
+ <button
3227
+ className={styles.btn}
3228
+ onClick={handleAddFilterGroup}
3229
+ disabled={
3230
+ selectedColumns.length === 0
3231
+ }
3232
+ >
3233
+ Add Filter Group
3234
+ </button>
3235
+ </div>
3236
+ </div>
3237
+
3238
+ {filterCriteria.groups.map(
3239
+ (group, groupIndex) => (
3240
+ <div
3241
+ key={`group-${groupIndex}`}
3242
+ className={styles.filterGroup}
3243
+ >
3244
+ <div
3245
+ className={
3246
+ styles.filterGroupHeader
3247
+ }
3248
+ >
2872
3249
  <div
2873
- key={index}
2874
3250
  className={
2875
- styles.joinSection
3251
+ styles.filterGroupTitle
2876
3252
  }
2877
3253
  >
3254
+ <h4>
3255
+ Filter Group{' '}
3256
+ {groupIndex + 1}
3257
+ </h4>
2878
3258
  <div
2879
3259
  className={
2880
- styles.joinHeader
3260
+ styles.filterGroupOperator
2881
3261
  }
2882
3262
  >
2883
- <h4>
2884
- Filter on{' '}
2885
- {criterion.table &&
2886
- criterion.column
2887
- ? `${formatName(
2888
- criterion.table
2889
- )}.${formatName(
2890
- criterion.column
2891
- )}`
2892
- : `Criterion #${
2893
- index +
2894
- 1
2895
- }`}
2896
- </h4>
2897
- <button
3263
+ <label>
3264
+ Match:
3265
+ </label>
3266
+ <div
2898
3267
  className={
2899
- styles.removeJoinBtn
3268
+ styles.operatorButtonGroup
2900
3269
  }
2901
- onClick={() =>
2902
- handleRemoveFilterCriterion(
2903
- index
2904
- )
2905
- }
2906
- title="Remove Filter Criterion"
2907
3270
  >
2908
- ×
2909
- </button>
3271
+ <button
3272
+ type="button"
3273
+ className={`${
3274
+ styles.operatorButton
3275
+ } ${
3276
+ group.operator ===
3277
+ 'AND'
3278
+ ? styles.operatorButtonSelected
3279
+ : ''
3280
+ }`}
3281
+ onClick={() =>
3282
+ handleFilterGroupOperatorChange(
3283
+ groupIndex,
3284
+ 'AND'
3285
+ )
3286
+ }
3287
+ >
3288
+ ALL
3289
+ </button>
3290
+ <button
3291
+ type="button"
3292
+ className={`${
3293
+ styles.operatorButton
3294
+ } ${
3295
+ group.operator ===
3296
+ 'OR'
3297
+ ? styles.operatorButtonSelected
3298
+ : ''
3299
+ }`}
3300
+ onClick={() =>
3301
+ handleFilterGroupOperatorChange(
3302
+ groupIndex,
3303
+ 'OR'
3304
+ )
3305
+ }
3306
+ >
3307
+ ANY
3308
+ </button>
3309
+ </div>
2910
3310
  </div>
3311
+ </div>
2911
3312
 
2912
- <div
3313
+ <div
3314
+ className={
3315
+ styles.filterGroupActions
3316
+ }
3317
+ >
3318
+ <button
2913
3319
  className={
2914
- styles.joinGrid
3320
+ styles.btn
3321
+ }
3322
+ onClick={() =>
3323
+ handleAddFilterCriterion(
3324
+ groupIndex
3325
+ )
3326
+ }
3327
+ disabled={
3328
+ selectedColumns.length ===
3329
+ 0
2915
3330
  }
2916
3331
  >
2917
- <div
3332
+ Add Filter
3333
+ </button>
3334
+ {filterCriteria.groups
3335
+ .length > 1 && (
3336
+ <button
2918
3337
  className={
2919
- styles.joinGridColumn
3338
+ styles.removeJoinBtn
2920
3339
  }
3340
+ onClick={() =>
3341
+ handleRemoveFilterGroup(
3342
+ groupIndex
3343
+ )
3344
+ }
3345
+ title="Remove Filter Group"
2921
3346
  >
3347
+ ×
3348
+ </button>
3349
+ )}
3350
+ </div>
3351
+ </div>
3352
+
3353
+ {group.filters.length > 0 ? (
3354
+ <div
3355
+ className={
3356
+ styles.joinTablesContainer
3357
+ }
3358
+ >
3359
+ {group.filters.map(
3360
+ (
3361
+ criterion,
3362
+ filterIndex
3363
+ ) => (
2922
3364
  <div
3365
+ key={`filter-${groupIndex}-${filterIndex}`}
2923
3366
  className={
2924
- styles.joinField
3367
+ styles.joinSection
2925
3368
  }
2926
3369
  >
2927
- <label>
2928
- Column:
2929
- </label>
2930
- <select
3370
+ <div
2931
3371
  className={
2932
- styles.selectControl
3372
+ styles.joinHeader
2933
3373
  }
2934
- value={`${criterion.table}.${criterion.column}`}
2935
- onChange={(
2936
- e
2937
- ) => {
2938
- const [
2939
- table,
2940
- column,
2941
- ] =
2942
- e.target.value.split(
2943
- '.'
2944
- );
2945
- handleFilterCriterionChange(
2946
- index,
2947
- 'table',
2948
- table
2949
- );
2950
- handleFilterCriterionChange(
2951
- index,
2952
- 'column',
2953
- column
2954
- );
2955
- }}
2956
3374
  >
2957
- <option value="">
2958
- Select
2959
- Column
2960
- </option>
2961
- {/* Main table columns */}
2962
- <optgroup
2963
- label={`${formatName(
2964
- selectedTable
2965
- )} Columns`}
2966
- >
2967
- {tableColumns.map(
2968
- (
2969
- column
2970
- ) => (
2971
- <option
2972
- key={`${selectedTable}.${column.name}`}
2973
- value={`${selectedTable}.${column.name}`}
2974
- >
2975
- {formatName(
2976
- column.name
2977
- )}
2978
- </option>
3375
+ <h4>
3376
+ Filter
3377
+ on{' '}
3378
+ {criterion.table &&
3379
+ criterion.column
3380
+ ? `${formatName(
3381
+ criterion.table
3382
+ )}.${formatName(
3383
+ criterion.column
3384
+ )}`
3385
+ : `Criterion #${
3386
+ filterIndex +
3387
+ 1
3388
+ }`}
3389
+ </h4>
3390
+ <button
3391
+ className={
3392
+ styles.removeJoinBtn
3393
+ }
3394
+ onClick={() =>
3395
+ handleRemoveFilterCriterion(
3396
+ groupIndex,
3397
+ filterIndex
2979
3398
  )
2980
- )}
2981
- </optgroup>
3399
+ }
3400
+ title="Remove Filter Criterion"
3401
+ >
3402
+ ×
3403
+ </button>
3404
+ </div>
2982
3405
 
2983
- {/* Joined tables columns */}
2984
- {joins.map(
2985
- (
2986
- join,
2987
- joinIndex
2988
- ) =>
2989
- join.targetTable &&
2990
- join.availableColumns &&
2991
- join
2992
- .availableColumns
2993
- .length >
2994
- 0 ? (
3406
+ <div
3407
+ className={
3408
+ styles.joinGrid
3409
+ }
3410
+ >
3411
+ <div
3412
+ className={
3413
+ styles.joinGridColumn
3414
+ }
3415
+ >
3416
+ <div
3417
+ className={
3418
+ styles.joinField
3419
+ }
3420
+ >
3421
+ <label>
3422
+ Column:
3423
+ </label>
3424
+ <select
3425
+ className={
3426
+ styles.selectControl
3427
+ }
3428
+ value={`${criterion.table}.${criterion.column}`}
3429
+ onChange={(
3430
+ e
3431
+ ) => {
3432
+ const [
3433
+ table,
3434
+ column,
3435
+ ] =
3436
+ e.target.value.split(
3437
+ '.'
3438
+ );
3439
+ handleFilterCriterionChange(
3440
+ groupIndex,
3441
+ filterIndex,
3442
+ 'table',
3443
+ table
3444
+ );
3445
+ handleFilterCriterionChange(
3446
+ groupIndex,
3447
+ filterIndex,
3448
+ 'column',
3449
+ column
3450
+ );
3451
+ }}
3452
+ >
3453
+ <option value="">
3454
+ Select
3455
+ Column
3456
+ </option>
3457
+ {/* Main table columns */}
2995
3458
  <optgroup
2996
- key={`join-${joinIndex}`}
2997
3459
  label={`${formatName(
2998
- join.targetTable
3460
+ selectedTable
2999
3461
  )} Columns`}
3000
3462
  >
3001
- {join.availableColumns.map(
3463
+ {tableColumns.map(
3002
3464
  (
3003
3465
  column
3004
3466
  ) => (
3005
3467
  <option
3006
- key={`${join.targetTable}.${column.name}`}
3007
- value={`${join.targetTable}.${column.name}`}
3468
+ key={`${selectedTable}.${column.name}`}
3469
+ value={`${selectedTable}.${column.name}`}
3008
3470
  >
3009
3471
  {formatName(
3010
3472
  column.name
@@ -3013,144 +3475,194 @@ const GenericReport = ({ setting = {} }) => {
3013
3475
  )
3014
3476
  )}
3015
3477
  </optgroup>
3016
- ) : null
3017
- )}
3018
- </select>
3019
- </div>
3020
- </div>
3021
3478
 
3022
- <div
3023
- className={
3024
- styles.joinGridColumn
3025
- }
3026
- >
3027
- <div
3028
- className={
3029
- styles.joinField
3030
- }
3031
- >
3032
- <label>
3033
- Operator:
3034
- </label>
3035
- <select
3036
- className={
3037
- styles.selectControl
3038
- }
3039
- value={
3040
- criterion.operator
3041
- }
3042
- onChange={(
3043
- e
3044
- ) =>
3045
- handleFilterCriterionChange(
3046
- index,
3047
- 'operator',
3048
- e
3049
- .target
3050
- .value
3051
- )
3052
- }
3053
- >
3054
- <option value="=">
3055
- Equals
3056
- (=)
3057
- </option>
3058
- <option value="!=">
3059
- Not
3060
- Equals
3061
- (!=)
3062
- </option>
3063
- <option value="&gt;">
3064
- Greater
3065
- Than
3066
- (&gt;)
3067
- </option>
3068
- <option value="&lt;">
3069
- Less
3070
- Than
3071
- (&lt;)
3072
- </option>
3073
- <option value="&gt;=">
3074
- Greater
3075
- Than
3076
- or
3077
- Equal
3078
- (&gt;=)
3079
- </option>
3080
- <option value="&lt;=">
3081
- Less
3082
- Than
3083
- or
3084
- Equal
3085
- (&lt;=)
3086
- </option>
3087
- <option value="LIKE">
3088
- Contains
3089
- (LIKE)
3090
- </option>
3091
- <option value="NOT LIKE">
3092
- Does
3093
- Not
3094
- Contain
3095
- (NOT
3096
- LIKE)
3097
- </option>
3098
- <option value="IS NULL">
3099
- Is
3100
- Empty
3101
- (NULL)
3102
- </option>
3103
- <option value="IS NOT NULL">
3104
- Is
3105
- Not
3106
- Empty
3107
- (NOT
3108
- NULL)
3109
- </option>
3110
- </select>
3111
- </div>
3479
+ {/* Joined tables columns */}
3480
+ {joins.map(
3481
+ (
3482
+ join,
3483
+ joinIndex
3484
+ ) =>
3485
+ join.targetTable &&
3486
+ join.availableColumns &&
3487
+ join
3488
+ .availableColumns
3489
+ .length >
3490
+ 0 ? (
3491
+ <optgroup
3492
+ key={`join-${joinIndex}`}
3493
+ label={`${formatName(
3494
+ join.targetTable
3495
+ )} Columns`}
3496
+ >
3497
+ {join.availableColumns.map(
3498
+ (
3499
+ column
3500
+ ) => (
3501
+ <option
3502
+ key={`${join.targetTable}.${column.name}`}
3503
+ value={`${join.targetTable}.${column.name}`}
3504
+ >
3505
+ {formatName(
3506
+ column.name
3507
+ )}
3508
+ </option>
3509
+ )
3510
+ )}
3511
+ </optgroup>
3512
+ ) : null
3513
+ )}
3514
+ </select>
3515
+ </div>
3516
+ </div>
3112
3517
 
3113
- {criterion.operator !==
3114
- 'IS NULL' &&
3115
- criterion.operator !==
3116
- 'IS NOT NULL' && (
3117
3518
  <div
3118
3519
  className={
3119
- styles.joinField
3520
+ styles.joinGridColumn
3120
3521
  }
3121
3522
  >
3122
- <label>
3123
- Value:
3124
- </label>
3125
- <input
3126
- type="text"
3523
+ <div
3127
3524
  className={
3128
- styles.formControl
3525
+ styles.joinField
3129
3526
  }
3130
- value={
3131
- criterion.value
3132
- }
3133
- onChange={(
3134
- e
3135
- ) =>
3136
- handleFilterCriterionChange(
3137
- index,
3138
- 'value',
3527
+ >
3528
+ <label>
3529
+ Operator:
3530
+ </label>
3531
+ <select
3532
+ className={
3533
+ styles.selectControl
3534
+ }
3535
+ value={
3536
+ criterion.operator
3537
+ }
3538
+ onChange={(
3139
3539
  e
3140
- .target
3141
- .value
3142
- )
3143
- }
3144
- placeholder="Enter filter value"
3145
- />
3540
+ ) =>
3541
+ handleFilterCriterionChange(
3542
+ groupIndex,
3543
+ filterIndex,
3544
+ 'operator',
3545
+ e
3546
+ .target
3547
+ .value
3548
+ )
3549
+ }
3550
+ >
3551
+ <option value="=">
3552
+ Is
3553
+ exactly
3554
+ equal
3555
+ to
3556
+ </option>
3557
+ <option value="!=">
3558
+ Is
3559
+ not
3560
+ equal
3561
+ to
3562
+ </option>
3563
+ <option value="&gt;">
3564
+ Is
3565
+ greater
3566
+ than
3567
+ </option>
3568
+ <option value="&lt;">
3569
+ Is
3570
+ less
3571
+ than
3572
+ </option>
3573
+ <option value="&gt;=">
3574
+ Is
3575
+ greater
3576
+ than
3577
+ or
3578
+ equal
3579
+ to
3580
+ </option>
3581
+ <option value="&lt;=">
3582
+ Is
3583
+ less
3584
+ than
3585
+ or
3586
+ equal
3587
+ to
3588
+ </option>
3589
+ <option value="LIKE">
3590
+ Contains
3591
+ </option>
3592
+ <option value="NOT LIKE">
3593
+ Does
3594
+ not
3595
+ contain
3596
+ </option>
3597
+ <option value="IS NULL">
3598
+ Is
3599
+ empty
3600
+ </option>
3601
+ <option value="IS NOT NULL">
3602
+ Is
3603
+ not
3604
+ empty
3605
+ </option>
3606
+ </select>
3607
+ </div>
3608
+
3609
+ {criterion.operator !==
3610
+ 'IS NULL' &&
3611
+ criterion.operator !==
3612
+ 'IS NOT NULL' && (
3613
+ <div
3614
+ className={
3615
+ styles.joinField
3616
+ }
3617
+ >
3618
+ <label>
3619
+ Value:
3620
+ </label>
3621
+ <input
3622
+ type="text"
3623
+ className={
3624
+ styles.formControl
3625
+ }
3626
+ value={
3627
+ criterion.value
3628
+ }
3629
+ onChange={(
3630
+ e
3631
+ ) =>
3632
+ handleFilterCriterionChange(
3633
+ groupIndex,
3634
+ filterIndex,
3635
+ 'value',
3636
+ e
3637
+ .target
3638
+ .value
3639
+ )
3640
+ }
3641
+ placeholder="Enter filter value"
3642
+ />
3643
+ </div>
3644
+ )}
3146
3645
  </div>
3147
- )}
3148
- </div>
3149
- </div>
3646
+ </div>
3647
+ </div>
3648
+ )
3649
+ )}
3150
3650
  </div>
3151
- )
3152
- )}
3153
- </div>
3651
+ ) : (
3652
+ <div
3653
+ className={
3654
+ styles.emptyFilterGroup
3655
+ }
3656
+ >
3657
+ <p>
3658
+ No filters in this
3659
+ group. Click "Add
3660
+ Filter" to add one.
3661
+ </p>
3662
+ </div>
3663
+ )}
3664
+ </div>
3665
+ )
3154
3666
  )}
3155
3667
  </>
3156
3668
  )}