@visns-studio/visns-components 5.6.3 → 5.6.5

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,96 @@ const isJsonValue = (value) => {
177
177
  return typeof value === 'object' && value !== null;
178
178
  };
179
179
 
180
+ // Helper function to validate filter criteria
181
+ const validateFilterCriteria = (filterCriteria) => {
182
+ // If it's already a flat array (legacy format)
183
+ if (Array.isArray(filterCriteria)) {
184
+ // Check each filter in the array
185
+ for (let i = 0; i < filterCriteria.length; i++) {
186
+ const filter = filterCriteria[i];
187
+
188
+ // Skip validation for IS NULL and IS NOT NULL operators which don't need a value
189
+ if (
190
+ filter.operator === 'IS NULL' ||
191
+ filter.operator === 'IS NOT NULL'
192
+ ) {
193
+ continue;
194
+ }
195
+
196
+ // Check if any required field is missing
197
+ if (
198
+ !filter.table ||
199
+ !filter.column ||
200
+ !filter.operator ||
201
+ filter.value === undefined ||
202
+ filter.value === null ||
203
+ filter.value === ''
204
+ ) {
205
+ return {
206
+ valid: false,
207
+ message: `Filter #${
208
+ i + 1
209
+ } is missing required fields (table, column, operator, or value)`,
210
+ };
211
+ }
212
+ }
213
+ return { valid: true };
214
+ }
215
+
216
+ // If it's the new structure with groups and operators
217
+ if (filterCriteria.groups && Array.isArray(filterCriteria.groups)) {
218
+ // Process each group
219
+ for (
220
+ let groupIndex = 0;
221
+ groupIndex < filterCriteria.groups.length;
222
+ groupIndex++
223
+ ) {
224
+ const group = filterCriteria.groups[groupIndex];
225
+
226
+ // Only validate non-empty groups
227
+ if (group.filters && group.filters.length > 0) {
228
+ // Check all filters in this group
229
+ for (
230
+ let filterIndex = 0;
231
+ filterIndex < group.filters.length;
232
+ filterIndex++
233
+ ) {
234
+ const filter = group.filters[filterIndex];
235
+
236
+ // Skip validation for IS NULL and IS NOT NULL operators which don't need a value
237
+ if (
238
+ filter.operator === 'IS NULL' ||
239
+ filter.operator === 'IS NOT NULL'
240
+ ) {
241
+ continue;
242
+ }
243
+
244
+ // Check if any required field is missing
245
+ if (
246
+ !filter.table ||
247
+ !filter.column ||
248
+ !filter.operator ||
249
+ filter.value === undefined ||
250
+ filter.value === null ||
251
+ filter.value === ''
252
+ ) {
253
+ return {
254
+ valid: false,
255
+ message: `Filter #${filterIndex + 1} in group #${
256
+ groupIndex + 1
257
+ } is missing required fields (table, column, operator, or value)`,
258
+ };
259
+ }
260
+ }
261
+ }
262
+ }
263
+ return { valid: true };
264
+ }
265
+
266
+ // If it's neither a flat array nor the new structure, consider it valid (empty)
267
+ return { valid: true };
268
+ };
269
+
180
270
  // Helper function to convert the enhanced filter structure to a flat array for the backend
181
271
  const flattenFilterCriteria = (filterCriteria) => {
182
272
  // If it's already a flat array (legacy format), return it as is
@@ -750,9 +840,40 @@ const GenericReport = ({ setting = {} }) => {
750
840
  setSelectedColumns(updatedColumns);
751
841
  };
752
842
 
843
+ // Start editing a column name
844
+ const handleStartEditColumn = (index) => {
845
+ const column = selectedColumns[index];
846
+ setEditingColumnIndex(index);
847
+ setEditingColumnName(column.alias || column.displayName);
848
+ };
849
+
850
+ // Save the edited column name
851
+ const handleSaveColumnName = () => {
852
+ if (editingColumnIndex !== null) {
853
+ const updatedColumns = [...selectedColumns];
854
+ updatedColumns[editingColumnIndex] = {
855
+ ...updatedColumns[editingColumnIndex],
856
+ alias: editingColumnName,
857
+ };
858
+ setSelectedColumns(updatedColumns);
859
+ setEditingColumnIndex(null);
860
+ setEditingColumnName('');
861
+ }
862
+ };
863
+
864
+ // Cancel column name editing
865
+ const handleCancelEditColumn = () => {
866
+ setEditingColumnIndex(null);
867
+ setEditingColumnName('');
868
+ };
869
+
753
870
  // State for drag and drop
754
871
  const [draggedItemIndex, setDraggedItemIndex] = useState(null);
755
872
 
873
+ // State for column renaming
874
+ const [editingColumnIndex, setEditingColumnIndex] = useState(null);
875
+ const [editingColumnName, setEditingColumnName] = useState('');
876
+
756
877
  // Handle drag start for column reordering
757
878
  const handleDragStart = (e, index) => {
758
879
  setDraggedItemIndex(index);
@@ -1336,7 +1457,7 @@ const GenericReport = ({ setting = {} }) => {
1336
1457
  }
1337
1458
  };
1338
1459
 
1339
- // Export report as CSV or Excel
1460
+ // Export report as CSV, Excel or PDF
1340
1461
  const handleExportReport = async (format = 'csv') => {
1341
1462
  if (selectedColumns.length === 0) {
1342
1463
  toast.error('Please select at least one column for your report');
@@ -1348,6 +1469,15 @@ const GenericReport = ({ setting = {} }) => {
1348
1469
  return;
1349
1470
  }
1350
1471
 
1472
+ // Validate filter criteria
1473
+ if (filterCriteria) {
1474
+ const validationResult = validateFilterCriteria(filterCriteria);
1475
+ if (!validationResult.valid) {
1476
+ toast.error(validationResult.message);
1477
+ return;
1478
+ }
1479
+ }
1480
+
1351
1481
  setIsLoading(true);
1352
1482
  const toastId = toast.loading(
1353
1483
  `Exporting report as ${format.toUpperCase()}...`
@@ -1368,8 +1498,8 @@ const GenericReport = ({ setting = {} }) => {
1368
1498
  sourceColumn: join.sourceColumn,
1369
1499
  targetColumn: join.targetColumn,
1370
1500
  })),
1371
- filters: [],
1372
- sorting: [],
1501
+ filters: flattenFilterCriteria(filterCriteria),
1502
+ sorting: sortCriteria,
1373
1503
  };
1374
1504
 
1375
1505
  // Prepare export request data
@@ -1395,16 +1525,66 @@ const GenericReport = ({ setting = {} }) => {
1395
1525
  const date = moment().format('YYYYMMDD');
1396
1526
  const fileName = `${date}_${reportName}.${format}`;
1397
1527
 
1398
- // Use FileSaver.js to save the file
1399
- saveAs(res.data, fileName);
1528
+ // For PDF (HTML) format, open in a new window for printing
1529
+ if (format === 'pdf') {
1530
+ // Convert blob to URL
1531
+ const blob = new Blob([res.data], { type: 'text/html' });
1532
+ const url = URL.createObjectURL(blob);
1533
+
1534
+ // Open in a new window
1535
+ const printWindow = window.open(url, '_blank');
1536
+
1537
+ // Add print instructions
1538
+ if (printWindow) {
1539
+ printWindow.onload = function () {
1540
+ // Add print button at the top
1541
+ const printButton =
1542
+ printWindow.document.createElement('button');
1543
+ printButton.innerHTML = 'Print PDF';
1544
+ printButton.style.position = 'fixed';
1545
+ printButton.style.top = '10px';
1546
+ printButton.style.right = '10px';
1547
+ printButton.style.zIndex = '9999';
1548
+ printButton.style.padding = '8px 16px';
1549
+ printButton.style.backgroundColor = '#4CAF50';
1550
+ printButton.style.color = 'white';
1551
+ printButton.style.border = 'none';
1552
+ printButton.style.borderRadius = '4px';
1553
+ printButton.style.cursor = 'pointer';
1554
+
1555
+ printButton.onclick = function () {
1556
+ // Hide the button before printing
1557
+ this.style.display = 'none';
1558
+ printWindow.print();
1559
+ this.style.display = 'block';
1560
+ };
1561
+
1562
+ printWindow.document.body.insertBefore(
1563
+ printButton,
1564
+ printWindow.document.body.firstChild
1565
+ );
1566
+ };
1567
+ }
1400
1568
 
1401
- toast.update(toastId, {
1402
- render: `Report exported successfully as ${format.toUpperCase()}`,
1403
- type: 'success',
1404
- isLoading: false,
1405
- autoClose: 5000,
1406
- closeButton: true,
1407
- });
1569
+ toast.update(toastId, {
1570
+ render: `Report opened in a new window. Use browser's print function to save as PDF.`,
1571
+ type: 'success',
1572
+ isLoading: false,
1573
+ autoClose: 5000,
1574
+ closeButton: true,
1575
+ });
1576
+ } else {
1577
+ // Use FileSaver.js to save the file for CSV and Excel
1578
+ saveAs(res.data, fileName);
1579
+
1580
+ toast.update(toastId, {
1581
+ render: `Report exported successfully as ${format.toUpperCase()}`,
1582
+ type: 'success',
1583
+ isLoading: false,
1584
+ autoClose: 5000,
1585
+ closeButton: true,
1586
+ });
1587
+ }
1408
1588
  } else {
1409
1589
  throw new Error('Invalid response format');
1410
1590
  }
@@ -1666,6 +1846,15 @@ const GenericReport = ({ setting = {} }) => {
1666
1846
  return;
1667
1847
  }
1668
1848
 
1849
+ // Validate filter criteria
1850
+ if (filterCriteria) {
1851
+ const validationResult = validateFilterCriteria(filterCriteria);
1852
+ if (!validationResult.valid) {
1853
+ toast.error(validationResult.message);
1854
+ return;
1855
+ }
1856
+ }
1857
+
1669
1858
  setIsLoading(true);
1670
1859
  toast.info('Executing query...');
1671
1860
 
@@ -1845,6 +2034,15 @@ const GenericReport = ({ setting = {} }) => {
1845
2034
  return;
1846
2035
  }
1847
2036
 
2037
+ // Validate filter criteria
2038
+ if (filterCriteria) {
2039
+ const validationResult = validateFilterCriteria(filterCriteria);
2040
+ if (!validationResult.valid) {
2041
+ toast.error(validationResult.message);
2042
+ return;
2043
+ }
2044
+ }
2045
+
1848
2046
  setIsLoading(true);
1849
2047
 
1850
2048
  try {
@@ -2248,7 +2446,12 @@ const GenericReport = ({ setting = {} }) => {
2248
2446
  )}
2249
2447
  </div>
2250
2448
 
2251
- <h3>Selected Columns</h3>
2449
+ <div className={styles.columnSectionHeader}>
2450
+ <h3>Selected Columns</h3>
2451
+ <span className={styles.instructionText}>
2452
+ Click the pencil icon (✎) to rename columns
2453
+ </span>
2454
+ </div>
2252
2455
  {selectedColumns.length > 0 ? (
2253
2456
  <div className={styles.selectedColumns}>
2254
2457
  {selectedColumns.map((column, index) => (
@@ -2276,10 +2479,86 @@ const GenericReport = ({ setting = {} }) => {
2276
2479
  >
2277
2480
  ⋮⋮
2278
2481
  </div>
2279
- <span>{column.displayName}</span>
2482
+ <span>
2483
+ {editingColumnIndex ===
2484
+ index ? (
2485
+ <div
2486
+ className={
2487
+ styles.inlineEditContainer
2488
+ }
2489
+ >
2490
+ <input
2491
+ type="text"
2492
+ className={
2493
+ styles.columnNameInput
2494
+ }
2495
+ value={
2496
+ editingColumnName
2497
+ }
2498
+ onChange={(e) =>
2499
+ setEditingColumnName(
2500
+ e.target
2501
+ .value
2502
+ )
2503
+ }
2504
+ autoFocus
2505
+ onKeyDown={(e) => {
2506
+ if (
2507
+ e.key ===
2508
+ 'Enter'
2509
+ ) {
2510
+ handleSaveColumnName();
2511
+ } else if (
2512
+ e.key ===
2513
+ 'Escape'
2514
+ ) {
2515
+ handleCancelEditColumn();
2516
+ }
2517
+ }}
2518
+ />
2519
+ <button
2520
+ className={
2521
+ styles.saveBtn
2522
+ }
2523
+ onClick={
2524
+ handleSaveColumnName
2525
+ }
2526
+ title="Save"
2527
+ >
2528
+
2529
+ </button>
2530
+ <button
2531
+ className={
2532
+ styles.cancelBtn
2533
+ }
2534
+ onClick={
2535
+ handleCancelEditColumn
2536
+ }
2537
+ title="Cancel"
2538
+ >
2539
+
2540
+ </button>
2541
+ </div>
2542
+ ) : (
2543
+ column.alias ||
2544
+ column.displayName
2545
+ )}
2546
+ </span>
2280
2547
  <div
2281
2548
  className={styles.columnActions}
2282
2549
  >
2550
+ <button
2551
+ className={styles.editBtn}
2552
+ onClick={() =>
2553
+ handleStartEditColumn(
2554
+ index
2555
+ )
2556
+ }
2557
+ aria-label="Edit column name"
2558
+ title="Edit name"
2559
+ >
2560
+
2561
+ </button>
2283
2562
  <button
2284
2563
  className={styles.removeBtn}
2285
2564
  onClick={() =>
@@ -2307,338 +2586,351 @@ const GenericReport = ({ setting = {} }) => {
2307
2586
  <div className={styles.reportSection}>
2308
2587
  <div
2309
2588
  className={styles.sectionHeader}
2310
- onClick={() =>
2311
- setShowJoinTables(!showJoinTables)
2312
- }
2313
- style={{ cursor: 'pointer' }}
2314
2589
  title={
2315
2590
  showJoinTables
2316
2591
  ? 'Hide join tables'
2317
2592
  : 'Show join tables'
2318
2593
  }
2319
2594
  >
2320
- <h3>Join Tables</h3>
2321
- <div
2322
- className={`${styles.toggleBtn} ${
2323
- showJoinTables
2324
- ? styles.toggleBtnActive
2325
- : ''
2326
- }`}
2595
+ <h3
2596
+ onClick={() =>
2597
+ setShowJoinTables(!showJoinTables)
2598
+ }
2599
+ style={{ cursor: 'pointer' }}
2327
2600
  >
2328
- {showJoinTables ? '▲' : '▼'}
2329
- </div>
2330
- </div>
2331
-
2332
- {showJoinTables && selectedTable && (
2333
- <>
2601
+ Join Tables
2602
+ </h3>
2603
+ <div className={styles.sectionHeaderActions}>
2604
+ <button
2605
+ className={styles.btn}
2606
+ onClick={handleAddJoin}
2607
+ >
2608
+ Add Manual Relationship
2609
+ </button>
2334
2610
  <div
2335
- className={
2336
- styles.addManualJoinContainer
2611
+ className={`${styles.toggleBtn} ${
2612
+ showJoinTables
2613
+ ? styles.toggleBtnActive
2614
+ : ''
2615
+ }`}
2616
+ onClick={() =>
2617
+ setShowJoinTables(!showJoinTables)
2337
2618
  }
2619
+ style={{ cursor: 'pointer' }}
2338
2620
  >
2339
- <button
2340
- className={styles.btn}
2341
- onClick={handleAddJoin}
2342
- >
2343
- Add Manual Relationship
2344
- </button>
2621
+ {showJoinTables ? '▲' : '▼'}
2345
2622
  </div>
2623
+ </div>
2624
+ </div>
2346
2625
 
2626
+ {showJoinTables && selectedTable && (
2627
+ <>
2347
2628
  {joins.length > 0 && (
2348
- <div
2349
- className={
2350
- styles.joinTablesContainer
2351
- }
2352
- >
2353
- {joins.map((join, index) => (
2354
- <div
2355
- key={index}
2356
- className={
2357
- styles.joinSection
2358
- }
2359
- >
2629
+ <div className={styles.filterGroup}>
2630
+ <div
2631
+ className={
2632
+ styles.joinTablesContainer
2633
+ }
2634
+ >
2635
+ {joins.map((join, index) => (
2360
2636
  <div
2637
+ key={index}
2361
2638
  className={
2362
- styles.joinHeader
2639
+ styles.joinSection
2363
2640
  }
2364
- >
2365
- <h4>
2366
- {join.targetTable
2367
- ? `${formatName(
2368
- join.sourceTable
2369
- )} ${join.joinType.replace(
2370
- ' JOIN',
2371
- ''
2372
- )} ${formatName(
2373
- join.targetTable
2374
- )}`
2375
- : `Join #${
2376
- index + 1
2377
- }`}
2378
- </h4>
2379
- <button
2380
- className={
2381
- styles.removeJoinBtn
2382
- }
2383
- onClick={() =>
2384
- handleRemoveJoin(
2385
- index
2386
- )
2387
- }
2388
- title="Remove Join"
2389
- >
2390
- ×
2391
- </button>
2392
- </div>
2393
-
2394
- <div
2395
- className={
2396
- styles.joinGrid
2641
+ data-join-index={
2642
+ index + 1
2397
2643
  }
2398
2644
  >
2399
2645
  <div
2400
2646
  className={
2401
- styles.joinGridColumn
2647
+ styles.joinHeader
2402
2648
  }
2403
2649
  >
2404
- <div
2405
- className={
2406
- styles.joinField
2650
+ <h4
2651
+ data-join-index={
2652
+ index + 1
2407
2653
  }
2408
2654
  >
2409
- <label>
2410
- Join Type:
2411
- </label>
2412
- <select
2413
- className={
2414
- styles.selectControl
2415
- }
2416
- value={
2417
- join.joinType
2418
- }
2419
- onChange={(
2420
- e
2421
- ) =>
2422
- handleJoinChange(
2423
- index,
2424
- 'joinType',
2425
- e
2426
- .target
2427
- .value
2428
- )
2429
- }
2430
- >
2431
- <option value="INNER JOIN">
2432
- INNER
2433
- JOIN
2434
- </option>
2435
- <option value="LEFT JOIN">
2436
- LEFT
2437
- JOIN
2438
- </option>
2439
- <option value="RIGHT JOIN">
2440
- RIGHT
2441
- JOIN
2442
- </option>
2443
- <option value="FULL JOIN">
2444
- FULL
2445
- JOIN
2446
- </option>
2447
- </select>
2448
- </div>
2449
-
2450
- <div
2655
+ {join.targetTable
2656
+ ? `${formatName(
2657
+ join.sourceTable
2658
+ )} ${join.joinType.replace(
2659
+ ' JOIN',
2660
+ ''
2661
+ )} ${formatName(
2662
+ join.targetTable
2663
+ )}`
2664
+ : `Join #${
2665
+ index +
2666
+ 1
2667
+ }`}
2668
+ </h4>
2669
+ <button
2451
2670
  className={
2452
- styles.joinField
2671
+ styles.removeJoinBtn
2453
2672
  }
2454
- >
2455
- <label>
2456
- Source
2457
- Table:
2458
- </label>
2459
- <input
2460
- type="text"
2461
- className={
2462
- styles.formControl
2463
- }
2464
- value={formatName(
2465
- join.sourceTable
2466
- )}
2467
- readOnly
2468
- />
2469
- </div>
2470
-
2471
- <div
2472
- className={
2473
- styles.joinField
2673
+ onClick={() =>
2674
+ handleRemoveJoin(
2675
+ index
2676
+ )
2474
2677
  }
2678
+ title="Remove Join"
2475
2679
  >
2476
- <label>
2477
- Source
2478
- Column:
2479
- </label>
2480
- <select
2481
- className={
2482
- styles.selectControl
2483
- }
2484
- value={
2485
- join.sourceColumn
2486
- }
2487
- onChange={(
2488
- e
2489
- ) =>
2490
- handleJoinChange(
2491
- index,
2492
- 'sourceColumn',
2493
- e
2494
- .target
2495
- .value
2496
- )
2497
- }
2498
- >
2499
- <option value="">
2500
- Select
2501
- Column
2502
- </option>
2503
- {tableColumns.map(
2504
- (
2505
- column
2506
- ) => (
2507
- <option
2508
- key={
2509
- column.name
2510
- }
2511
- value={
2512
- column.name
2513
- }
2514
- >
2515
- {formatName(
2516
- column.name
2517
- )}
2518
- </option>
2519
- )
2520
- )}
2521
- </select>
2522
- </div>
2680
+ ×
2681
+ </button>
2523
2682
  </div>
2524
2683
 
2525
2684
  <div
2526
2685
  className={
2527
- styles.joinGridColumn
2686
+ styles.joinGrid
2528
2687
  }
2529
2688
  >
2530
2689
  <div
2531
2690
  className={
2532
- styles.joinField
2691
+ styles.joinGridColumn
2533
2692
  }
2534
2693
  >
2535
- <label>
2536
- Target
2537
- Table:
2538
- </label>
2539
- <select
2694
+ <div
2540
2695
  className={
2541
- styles.selectControl
2542
- }
2543
- value={
2544
- join.targetTable
2545
- }
2546
- onChange={(
2547
- e
2548
- ) =>
2549
- handleJoinChange(
2550
- index,
2551
- 'targetTable',
2552
- e
2553
- .target
2554
- .value
2555
- )
2696
+ styles.joinField
2556
2697
  }
2557
2698
  >
2558
- <option value="">
2559
- Select
2560
- Table
2561
- </option>
2562
- {availableTables.map(
2563
- (
2564
- table
2565
- ) => (
2566
- <option
2567
- key={
2568
- table.name
2569
- }
2570
- value={
2571
- table.name
2572
- }
2573
- >
2574
- {formatName(
2575
- table.name
2576
- )}
2577
- </option>
2578
- )
2579
- )}
2580
- </select>
2581
- </div>
2582
-
2583
- {join.targetTable &&
2584
- join.availableColumns && (
2585
- <div
2699
+ <label>
2700
+ Join
2701
+ Type:
2702
+ </label>
2703
+ <select
2586
2704
  className={
2587
- styles.joinField
2705
+ styles.selectControl
2588
2706
  }
2589
- >
2590
- <label>
2591
- Target
2592
- Column:
2593
- </label>
2594
- <select
2595
- className={
2596
- styles.selectControl
2597
- }
2598
- value={
2599
- join.targetColumn
2600
- }
2601
- onChange={(
2707
+ value={
2708
+ join.joinType
2709
+ }
2710
+ onChange={(
2711
+ e
2712
+ ) =>
2713
+ handleJoinChange(
2714
+ index,
2715
+ 'joinType',
2602
2716
  e
2603
- ) =>
2604
- handleJoinChange(
2605
- index,
2606
- 'targetColumn',
2607
- e
2608
- .target
2609
- .value
2610
- )
2717
+ .target
2718
+ .value
2719
+ )
2720
+ }
2721
+ >
2722
+ <option value="INNER JOIN">
2723
+ INNER
2724
+ JOIN
2725
+ </option>
2726
+ <option value="LEFT JOIN">
2727
+ LEFT
2728
+ JOIN
2729
+ </option>
2730
+ <option value="RIGHT JOIN">
2731
+ RIGHT
2732
+ JOIN
2733
+ </option>
2734
+ <option value="FULL JOIN">
2735
+ FULL
2736
+ JOIN
2737
+ </option>
2738
+ </select>
2739
+ </div>
2740
+
2741
+ <div
2742
+ className={
2743
+ styles.joinField
2744
+ }
2745
+ >
2746
+ <label>
2747
+ Source
2748
+ Table:
2749
+ </label>
2750
+ <input
2751
+ type="text"
2752
+ className={
2753
+ styles.formControl
2754
+ }
2755
+ value={formatName(
2756
+ join.sourceTable
2757
+ )}
2758
+ readOnly
2759
+ />
2760
+ </div>
2761
+
2762
+ <div
2763
+ className={
2764
+ styles.joinField
2765
+ }
2766
+ >
2767
+ <label>
2768
+ Source
2769
+ Column:
2770
+ </label>
2771
+ <select
2772
+ className={
2773
+ styles.selectControl
2774
+ }
2775
+ value={
2776
+ join.sourceColumn
2777
+ }
2778
+ onChange={(
2779
+ e
2780
+ ) =>
2781
+ handleJoinChange(
2782
+ index,
2783
+ 'sourceColumn',
2784
+ e
2785
+ .target
2786
+ .value
2787
+ )
2788
+ }
2789
+ >
2790
+ <option value="">
2791
+ Select
2792
+ Column
2793
+ </option>
2794
+ {tableColumns.map(
2795
+ (
2796
+ column
2797
+ ) => (
2798
+ <option
2799
+ key={
2800
+ column.name
2801
+ }
2802
+ value={
2803
+ column.name
2804
+ }
2805
+ >
2806
+ {formatName(
2807
+ column.name
2808
+ )}
2809
+ </option>
2810
+ )
2811
+ )}
2812
+ </select>
2813
+ </div>
2814
+ </div>
2815
+
2816
+ <div
2817
+ className={
2818
+ styles.joinGridColumn
2819
+ }
2820
+ >
2821
+ <div
2822
+ className={
2823
+ styles.joinField
2824
+ }
2825
+ >
2826
+ <label>
2827
+ Target
2828
+ Table:
2829
+ </label>
2830
+ <select
2831
+ className={
2832
+ styles.selectControl
2833
+ }
2834
+ value={
2835
+ join.targetTable
2836
+ }
2837
+ onChange={(
2838
+ e
2839
+ ) =>
2840
+ handleJoinChange(
2841
+ index,
2842
+ 'targetTable',
2843
+ e
2844
+ .target
2845
+ .value
2846
+ )
2847
+ }
2848
+ >
2849
+ <option value="">
2850
+ Select
2851
+ Table
2852
+ </option>
2853
+ {availableTables.map(
2854
+ (
2855
+ table
2856
+ ) => (
2857
+ <option
2858
+ key={
2859
+ table.name
2860
+ }
2861
+ value={
2862
+ table.name
2863
+ }
2864
+ >
2865
+ {formatName(
2866
+ table.name
2867
+ )}
2868
+ </option>
2869
+ )
2870
+ )}
2871
+ </select>
2872
+ </div>
2873
+
2874
+ {join.targetTable &&
2875
+ join.availableColumns && (
2876
+ <div
2877
+ className={
2878
+ styles.joinField
2611
2879
  }
2612
2880
  >
2613
- <option value="">
2614
- Select
2615
- Column
2616
- </option>
2617
- {join.availableColumns.map(
2618
- (
2619
- column
2620
- ) => (
2621
- <option
2622
- key={
2623
- column.name
2624
- }
2625
- value={
2626
- column.name
2627
- }
2628
- >
2629
- {formatName(
2630
- column.name
2631
- )}
2632
- </option>
2633
- )
2634
- )}
2635
- </select>
2636
- </div>
2637
- )}
2881
+ <label>
2882
+ Target
2883
+ Column:
2884
+ </label>
2885
+ <select
2886
+ className={
2887
+ styles.selectControl
2888
+ }
2889
+ value={
2890
+ join.targetColumn
2891
+ }
2892
+ onChange={(
2893
+ e
2894
+ ) =>
2895
+ handleJoinChange(
2896
+ index,
2897
+ 'targetColumn',
2898
+ e
2899
+ .target
2900
+ .value
2901
+ )
2902
+ }
2903
+ >
2904
+ <option value="">
2905
+ Select
2906
+ Column
2907
+ </option>
2908
+ {join.availableColumns.map(
2909
+ (
2910
+ column
2911
+ ) => (
2912
+ <option
2913
+ key={
2914
+ column.name
2915
+ }
2916
+ value={
2917
+ column.name
2918
+ }
2919
+ >
2920
+ {formatName(
2921
+ column.name
2922
+ )}
2923
+ </option>
2924
+ )
2925
+ )}
2926
+ </select>
2927
+ </div>
2928
+ )}
2929
+ </div>
2638
2930
  </div>
2639
2931
  </div>
2640
- </div>
2641
- ))}
2932
+ ))}
2933
+ </div>
2642
2934
  </div>
2643
2935
  )}
2644
2936
  </>
@@ -2992,249 +3284,298 @@ const GenericReport = ({ setting = {} }) => {
2992
3284
  <div className={styles.reportSection}>
2993
3285
  <div
2994
3286
  className={styles.sectionHeader}
2995
- onClick={() =>
2996
- setShowSortingSection(!showSortingSection)
2997
- }
2998
- style={{ cursor: 'pointer' }}
2999
3287
  title={
3000
3288
  showSortingSection
3001
3289
  ? 'Hide sorting options'
3002
3290
  : 'Show sorting options'
3003
3291
  }
3004
3292
  >
3005
- <h3>Sorting</h3>
3006
- <div
3007
- className={`${styles.toggleBtn} ${
3008
- showSortingSection
3009
- ? styles.toggleBtnActive
3010
- : ''
3011
- }`}
3293
+ <h3
3294
+ onClick={() =>
3295
+ setShowSortingSection(
3296
+ !showSortingSection
3297
+ )
3298
+ }
3299
+ style={{ cursor: 'pointer' }}
3012
3300
  >
3013
- {showSortingSection ? '▲' : '▼'}
3014
- </div>
3015
- </div>
3016
-
3017
- {showSortingSection && (
3018
- <>
3301
+ Sorting
3302
+ </h3>
3303
+ <div className={styles.sectionHeaderActions}>
3304
+ <button
3305
+ className={styles.btn}
3306
+ onClick={handleAddSortCriterion}
3307
+ disabled={selectedColumns.length === 0}
3308
+ >
3309
+ Add Sort Criterion
3310
+ </button>
3019
3311
  <div
3020
- className={
3021
- styles.addManualJoinContainer
3312
+ className={`${styles.toggleBtn} ${
3313
+ showSortingSection
3314
+ ? styles.toggleBtnActive
3315
+ : ''
3316
+ }`}
3317
+ onClick={() =>
3318
+ setShowSortingSection(
3319
+ !showSortingSection
3320
+ )
3022
3321
  }
3322
+ style={{ cursor: 'pointer' }}
3023
3323
  >
3024
- <button
3025
- className={styles.btn}
3026
- onClick={handleAddSortCriterion}
3027
- disabled={
3028
- selectedColumns.length === 0
3029
- }
3030
- >
3031
- Add Sort Criterion
3032
- </button>
3324
+ {showSortingSection ? '▲' : '▼'}
3033
3325
  </div>
3326
+ </div>
3327
+ </div>
3034
3328
 
3329
+ {showSortingSection && (
3330
+ <>
3035
3331
  {sortCriteria.length > 0 && (
3036
- <div
3037
- className={
3038
- styles.joinTablesContainer
3039
- }
3040
- >
3041
- {sortCriteria.map(
3042
- (criterion, index) => (
3043
- <div
3044
- key={index}
3045
- className={
3046
- styles.joinSection
3047
- }
3048
- >
3332
+ <div className={styles.filterGroup}>
3333
+ <div
3334
+ className={
3335
+ styles.joinTablesContainer
3336
+ }
3337
+ >
3338
+ {sortCriteria.map(
3339
+ (criterion, index) => (
3049
3340
  <div
3341
+ key={index}
3050
3342
  className={
3051
- styles.joinHeader
3343
+ styles.joinSection
3344
+ }
3345
+ data-sort-index={
3346
+ index + 1
3052
3347
  }
3053
3348
  >
3054
- <h4>
3055
- Sort by{' '}
3056
- {criterion.table &&
3057
- criterion.column
3058
- ? `${formatName(
3059
- criterion.table
3060
- )}.${formatName(
3061
- criterion.column
3062
- )}`
3063
- : `Criterion #${
3064
- index +
3065
- 1
3066
- }`}
3067
- </h4>
3068
- <button
3349
+ <div
3069
3350
  className={
3070
- styles.removeJoinBtn
3071
- }
3072
- onClick={() =>
3073
- handleRemoveSortCriterion(
3074
- index
3075
- )
3351
+ styles.joinHeader
3076
3352
  }
3077
- title="Remove Sort Criterion"
3078
3353
  >
3079
- ×
3080
- </button>
3081
- </div>
3354
+ <h4
3355
+ data-sort-index={
3356
+ index +
3357
+ 1
3358
+ }
3359
+ >
3360
+ by{' '}
3361
+ {criterion.table &&
3362
+ criterion.column
3363
+ ? `${formatName(
3364
+ criterion.table
3365
+ )}.${formatName(
3366
+ criterion.column
3367
+ )}`
3368
+ : `Criterion #${
3369
+ index +
3370
+ 1
3371
+ }`}
3372
+ </h4>
3373
+ <button
3374
+ className={
3375
+ styles.removeJoinBtn
3376
+ }
3377
+ onClick={() =>
3378
+ handleRemoveSortCriterion(
3379
+ index
3380
+ )
3381
+ }
3382
+ title="Remove Sort Criterion"
3383
+ >
3384
+ ×
3385
+ </button>
3386
+ </div>
3082
3387
 
3083
- <div
3084
- className={
3085
- styles.joinGrid
3086
- }
3087
- >
3088
3388
  <div
3089
3389
  className={
3090
- styles.joinGridColumn
3390
+ styles.joinGrid
3091
3391
  }
3092
3392
  >
3093
3393
  <div
3094
3394
  className={
3095
- styles.joinField
3395
+ styles.joinGridColumn
3096
3396
  }
3097
3397
  >
3098
- <label>
3099
- Column:
3100
- </label>
3101
- <select
3398
+ <div
3102
3399
  className={
3103
- styles.selectControl
3400
+ styles.joinField
3104
3401
  }
3105
- value={`${criterion.table}.${criterion.column}`}
3106
- onChange={(
3107
- e
3108
- ) => {
3109
- const [
3110
- table,
3111
- column,
3112
- ] =
3113
- e.target.value.split(
3114
- '.'
3115
- );
3116
- handleSortCriterionChange(
3117
- index,
3118
- 'table',
3119
- table
3120
- );
3121
- handleSortCriterionChange(
3122
- index,
3123
- 'column',
3124
- column
3125
- );
3126
- }}
3127
3402
  >
3128
- <option value="">
3129
- Select
3130
- Column
3131
- </option>
3132
- {/* Main table columns */}
3133
- <optgroup
3134
- label={`${formatName(
3135
- selectedTable
3136
- )} Columns`}
3403
+ <label>
3404
+ Column:
3405
+ </label>
3406
+ <select
3407
+ className={
3408
+ styles.selectControl
3409
+ }
3410
+ value={
3411
+ criterion.table &&
3412
+ criterion.column
3413
+ ? `${criterion.table}.${criterion.column}`
3414
+ : ''
3415
+ }
3416
+ onChange={(
3417
+ e
3418
+ ) => {
3419
+ if (
3420
+ e
3421
+ .target
3422
+ .value
3423
+ ) {
3424
+ // Split by the first occurrence of '.' to handle table names that might contain dots
3425
+ const dotIndex =
3426
+ e.target.value.indexOf(
3427
+ '.'
3428
+ );
3429
+ if (
3430
+ dotIndex !==
3431
+ -1
3432
+ ) {
3433
+ const table =
3434
+ e.target.value.substring(
3435
+ 0,
3436
+ dotIndex
3437
+ );
3438
+ const column =
3439
+ e.target.value.substring(
3440
+ dotIndex +
3441
+ 1
3442
+ );
3443
+
3444
+ // First set the column to empty to avoid any issues with the value attribute
3445
+ handleSortCriterionChange(
3446
+ index,
3447
+ 'column',
3448
+ ''
3449
+ );
3450
+
3451
+ // Then set the table
3452
+ handleSortCriterionChange(
3453
+ index,
3454
+ 'table',
3455
+ table
3456
+ );
3457
+
3458
+ // Finally set the column
3459
+ handleSortCriterionChange(
3460
+ index,
3461
+ 'column',
3462
+ column
3463
+ );
3464
+ }
3465
+ }
3466
+ }}
3137
3467
  >
3138
- {tableColumns.map(
3468
+ <option value="">
3469
+ Select
3470
+ Column
3471
+ </option>
3472
+ {/* Main table columns */}
3473
+ <optgroup
3474
+ label={`${formatName(
3475
+ selectedTable
3476
+ )} Columns`}
3477
+ >
3478
+ {tableColumns.map(
3479
+ (
3480
+ column
3481
+ ) => (
3482
+ <option
3483
+ key={`${selectedTable}.${column.name}`}
3484
+ value={`${selectedTable}.${column.name}`}
3485
+ >
3486
+ {formatName(
3487
+ column.name
3488
+ )}
3489
+ </option>
3490
+ )
3491
+ )}
3492
+ </optgroup>
3493
+
3494
+ {/* Joined tables columns */}
3495
+ {joins.map(
3139
3496
  (
3140
- column
3141
- ) => (
3142
- <option
3143
- key={`${selectedTable}.${column.name}`}
3144
- value={`${selectedTable}.${column.name}`}
3145
- >
3146
- {formatName(
3147
- column.name
3148
- )}
3149
- </option>
3150
- )
3497
+ join,
3498
+ joinIndex
3499
+ ) =>
3500
+ join.targetTable &&
3501
+ join.availableColumns &&
3502
+ join
3503
+ .availableColumns
3504
+ .length >
3505
+ 0 ? (
3506
+ <optgroup
3507
+ key={`join-${joinIndex}`}
3508
+ label={`${formatName(
3509
+ join.targetTable
3510
+ )} Columns`}
3511
+ >
3512
+ {join.availableColumns.map(
3513
+ (
3514
+ column
3515
+ ) => (
3516
+ <option
3517
+ key={`${join.targetTable}.${column.name}`}
3518
+ value={`${join.targetTable}.${column.name}`}
3519
+ >
3520
+ {formatName(
3521
+ column.name
3522
+ )}
3523
+ </option>
3524
+ )
3525
+ )}
3526
+ </optgroup>
3527
+ ) : null
3151
3528
  )}
3152
- </optgroup>
3153
-
3154
- {/* Joined tables columns */}
3155
- {joins.map(
3156
- (
3157
- join,
3158
- joinIndex
3159
- ) =>
3160
- join.targetTable &&
3161
- join.availableColumns &&
3162
- join
3163
- .availableColumns
3164
- .length >
3165
- 0 ? (
3166
- <optgroup
3167
- key={`join-${joinIndex}`}
3168
- label={`${formatName(
3169
- join.targetTable
3170
- )} Columns`}
3171
- >
3172
- {join.availableColumns.map(
3173
- (
3174
- column
3175
- ) => (
3176
- <option
3177
- key={`${join.targetTable}.${column.name}`}
3178
- value={`${join.targetTable}.${column.name}`}
3179
- >
3180
- {formatName(
3181
- column.name
3182
- )}
3183
- </option>
3184
- )
3185
- )}
3186
- </optgroup>
3187
- ) : null
3188
- )}
3189
- </select>
3529
+ </select>
3530
+ </div>
3190
3531
  </div>
3191
- </div>
3192
3532
 
3193
- <div
3194
- className={
3195
- styles.joinGridColumn
3196
- }
3197
- >
3198
3533
  <div
3199
3534
  className={
3200
- styles.joinField
3535
+ styles.joinGridColumn
3201
3536
  }
3202
3537
  >
3203
- <label>
3204
- Direction:
3205
- </label>
3206
- <select
3538
+ <div
3207
3539
  className={
3208
- styles.selectControl
3209
- }
3210
- value={
3211
- criterion.direction
3212
- }
3213
- onChange={(
3214
- e
3215
- ) =>
3216
- handleSortCriterionChange(
3217
- index,
3218
- 'direction',
3219
- e
3220
- .target
3221
- .value
3222
- )
3540
+ styles.joinField
3223
3541
  }
3224
3542
  >
3225
- <option value="ASC">
3226
- Ascending
3227
- </option>
3228
- <option value="DESC">
3229
- Descending
3230
- </option>
3231
- </select>
3543
+ <label>
3544
+ Direction:
3545
+ </label>
3546
+ <select
3547
+ className={
3548
+ styles.selectControl
3549
+ }
3550
+ value={
3551
+ criterion.direction
3552
+ }
3553
+ onChange={(
3554
+ e
3555
+ ) =>
3556
+ handleSortCriterionChange(
3557
+ index,
3558
+ 'direction',
3559
+ e
3560
+ .target
3561
+ .value
3562
+ )
3563
+ }
3564
+ >
3565
+ <option value="ASC">
3566
+ Ascending
3567
+ </option>
3568
+ <option value="DESC">
3569
+ Descending
3570
+ </option>
3571
+ </select>
3572
+ </div>
3232
3573
  </div>
3233
3574
  </div>
3234
3575
  </div>
3235
- </div>
3236
- )
3237
- )}
3576
+ )
3577
+ )}
3578
+ </div>
3238
3579
  </div>
3239
3580
  )}
3240
3581
  </>
@@ -3512,14 +3853,22 @@ const GenericReport = ({ setting = {} }) => {
3512
3853
  className={
3513
3854
  styles.joinSection
3514
3855
  }
3856
+ data-filter-index={
3857
+ filterIndex +
3858
+ 1
3859
+ }
3515
3860
  >
3516
3861
  <div
3517
3862
  className={
3518
3863
  styles.joinHeader
3519
3864
  }
3520
3865
  >
3521
- <h4>
3522
- Filter
3866
+ <h4
3867
+ data-filter-index={
3868
+ filterIndex +
3869
+ 1
3870
+ }
3871
+ >
3523
3872
  on{' '}
3524
3873
  {criterion.table &&
3525
3874
  criterion.column
@@ -3571,29 +3920,81 @@ const GenericReport = ({ setting = {} }) => {
3571
3920
  className={
3572
3921
  styles.selectControl
3573
3922
  }
3574
- value={`${criterion.table}.${criterion.column}`}
3923
+ value={
3924
+ criterion.table &&
3925
+ criterion.column
3926
+ ? `${criterion.table}.${criterion.column}`
3927
+ : ''
3928
+ }
3575
3929
  onChange={(
3576
3930
  e
3577
3931
  ) => {
3578
- const [
3579
- table,
3580
- column,
3581
- ] =
3582
- e.target.value.split(
3583
- '.'
3584
- );
3585
- handleFilterCriterionChange(
3586
- groupIndex,
3587
- filterIndex,
3588
- 'table',
3589
- table
3590
- );
3591
- handleFilterCriterionChange(
3592
- groupIndex,
3593
- filterIndex,
3594
- 'column',
3595
- column
3596
- );
3932
+ if (
3933
+ e
3934
+ .target
3935
+ .value
3936
+ ) {
3937
+ // Split by the first occurrence of '.' to handle table names that might contain dots
3938
+ const dotIndex =
3939
+ e.target.value.indexOf(
3940
+ '.'
3941
+ );
3942
+ if (
3943
+ dotIndex !==
3944
+ -1
3945
+ ) {
3946
+ const table =
3947
+ e.target.value.substring(
3948
+ 0,
3949
+ dotIndex
3950
+ );
3951
+ const column =
3952
+ e.target.value.substring(
3953
+ dotIndex +
3954
+ 1
3955
+ );
3956
+
3957
+ console.log(
3958
+ 'Filter dropdown selection:',
3959
+ {
3960
+ value: e
3961
+ .target
3962
+ .value,
3963
+ table,
3964
+ column,
3965
+ groupIndex,
3966
+ filterIndex,
3967
+ }
3968
+ );
3969
+
3970
+ // Create a temporary copy of the filter criteria
3971
+ const updatedFilterCriteria =
3972
+ JSON.parse(
3973
+ JSON.stringify(
3974
+ filterCriteria
3975
+ )
3976
+ );
3977
+
3978
+ // Update both properties at once
3979
+ updatedFilterCriteria.groups[
3980
+ groupIndex
3981
+ ].filters[
3982
+ filterIndex
3983
+ ].table =
3984
+ table;
3985
+ updatedFilterCriteria.groups[
3986
+ groupIndex
3987
+ ].filters[
3988
+ filterIndex
3989
+ ].column =
3990
+ column;
3991
+
3992
+ // Set the state once with both updates
3993
+ setFilterCriteria(
3994
+ updatedFilterCriteria
3995
+ );
3996
+ }
3997
+ }
3597
3998
  }}
3598
3999
  >
3599
4000
  <option value="">
@@ -3890,6 +4291,7 @@ const GenericReport = ({ setting = {} }) => {
3890
4291
  {
3891
4292
  keyInfo.percentage
3892
4293
  }
4294
+
3893
4295
  %
3894
4296
  </span>
3895
4297
  </div>
@@ -4033,6 +4435,17 @@ const GenericReport = ({ setting = {} }) => {
4033
4435
  >
4034
4436
  Export as Excel
4035
4437
  </button>
4438
+ <button
4439
+ className={`${styles.btn} ${styles.btnSecondary}`}
4440
+ onClick={() => handleExportReport('pdf')}
4441
+ disabled={
4442
+ isLoading ||
4443
+ selectedColumns.length === 0 ||
4444
+ !reportName
4445
+ }
4446
+ >
4447
+ Export as PDF
4448
+ </button>
4036
4449
  <button
4037
4450
  className={`${styles.btn} ${styles.btnPrimary}`}
4038
4451
  onClick={handleSaveReport}