@visns-studio/visns-components 5.8.7 → 5.8.9

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.
@@ -1,12 +1,19 @@
1
1
  import '../../styles/global.css';
2
2
 
3
- import React, { useCallback, useEffect, useRef, useState } from 'react';
3
+ import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
4
10
  import { useParams, Outlet } from 'react-router-dom';
5
11
  import { toast } from 'react-toastify';
6
12
  import { saveAs } from 'file-saver';
7
13
  import _ from 'lodash';
8
14
  import parse from 'html-react-parser';
9
15
  import moment from 'moment';
16
+ import numeral from 'numeral';
10
17
  import { CloudDownload } from 'akar-icons';
11
18
 
12
19
  import CustomFetch from '../Fetch';
@@ -33,6 +40,8 @@ function useWindowHeight() {
33
40
  function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
34
41
  const [config, setConfig] = useState({});
35
42
  const [subnav, setSubnav] = useState(filters || []);
43
+ // Add a ref to track previous active item to prevent unnecessary updates
44
+ const prevActiveItemRef = useRef(null);
36
45
 
37
46
  useEffect(() => {
38
47
  const newConfig = {
@@ -114,6 +123,21 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
114
123
  if (activeItemResult) {
115
124
  const activeItem = activeItemResult.item;
116
125
 
126
+ // Create a string representation of the active item for comparison
127
+ const activeItemString = JSON.stringify({
128
+ id: activeItem.id,
129
+ isChild: activeItemResult.isChild,
130
+ parentId: activeItemResult.parent?.id,
131
+ });
132
+
133
+ // Skip update if the active item hasn't changed
134
+ if (prevActiveItemRef.current === activeItemString) {
135
+ return;
136
+ }
137
+
138
+ // Update the ref with current active item
139
+ prevActiveItemRef.current = activeItemString;
140
+
117
141
  // Find the tab config for this item
118
142
  const activeTabConfig = tabs.find(
119
143
  (t) => t.id === activeItem.id
@@ -121,6 +145,9 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
121
145
 
122
146
  if (activeTabConfig) {
123
147
  // We found a matching tab config, use it
148
+ // Get current style to preserve it
149
+ const currentStyle = config.style || {};
150
+
124
151
  const updatedConfig = {
125
152
  ...config,
126
153
  id: activeTabConfig.id,
@@ -130,7 +157,7 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
130
157
  settings: activeTabConfig.settings,
131
158
  // Preserve the style from the original config or use the tab config's style
132
159
  style:
133
- config.style ||
160
+ currentStyle ||
134
161
  activeTabConfig.style ||
135
162
  setting.style ||
136
163
  {},
@@ -149,6 +176,9 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
149
176
  // If this is a child item but we couldn't find a tab config,
150
177
  // try to use the base config but update the filter
151
178
 
179
+ // Get current style to preserve it
180
+ const currentStyle = config.style || {};
181
+
152
182
  // Create a new config based on the base settings
153
183
  const baseConfig = {
154
184
  export: setting.export || {},
@@ -160,7 +190,7 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
160
190
  setRowsSelected,
161
191
  settings: setting.settings,
162
192
  // Make sure we have a style property
163
- style: config.style || setting.style || {},
193
+ style: currentStyle || setting.style || {},
164
194
  tableSetting,
165
195
  type: setting.type ? setting.type : 'table',
166
196
  // Preserve functions from the original config or setting
@@ -189,11 +219,1070 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
189
219
  }
190
220
  }
191
221
  }
192
- }, [subnav, tabs, config, setting, setRowsSelected, tableSetting]);
222
+ }, [subnav, tabs, setting, setRowsSelected, tableSetting]);
193
223
 
194
224
  return { config, setConfig, subnav, setSubnav };
195
225
  }
196
226
 
227
+ const Grid = ({ config, userProfile }) => {
228
+ const [gridData, setGridData] = useState([]);
229
+ const [sortedData, setSortedData] = useState([]);
230
+ const [filteredData, setFilteredData] = useState([]);
231
+ const [filterValues, setFilterValues] = useState({});
232
+ const [settingsFilterValues, setSettingsFilterValues] = useState({});
233
+ const [loading, setLoading] = useState(false);
234
+ const [filtersCollapsed, setFiltersCollapsed] = useState(false);
235
+ const [sortConfig, setSortConfig] = useState({
236
+ key: null,
237
+ direction: 'asc',
238
+ });
239
+ const prevFetchParamsRef = useRef(null);
240
+
241
+ // Extract data and settings from config using useMemo to prevent unnecessary re-renders
242
+ const {
243
+ settings,
244
+ columns = [],
245
+ data = [],
246
+ } = useMemo(
247
+ () => ({
248
+ settings: config.settings || {},
249
+ columns: config.columns || [],
250
+ data: config.data || [],
251
+ }),
252
+ [config.settings, config.columns, config.data]
253
+ );
254
+
255
+ // Create table headers from columns
256
+ const headers = useMemo(
257
+ () => columns.map((col) => col.header || col.name || ''),
258
+ [columns]
259
+ );
260
+
261
+ // Format cell content based on column type or value type
262
+ const formatCellContent = (value, column) => {
263
+ if (value === null || value === undefined) return '';
264
+ if (value === 0) return '0'; // Explicitly handle zero values
265
+ if (value === '') return '';
266
+
267
+ // Try to infer the type if not provided
268
+ const valueType = column.type || typeof value;
269
+
270
+ switch (valueType) {
271
+ case 'date':
272
+ return moment(value).isValid()
273
+ ? moment(value).format(column.format || 'DD/MM/YYYY')
274
+ : value;
275
+ case 'datetime':
276
+ return moment(value).isValid()
277
+ ? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
278
+ : value;
279
+ case 'currency':
280
+ return numeral(value).format(column.format || '$0,0.00');
281
+ case 'number':
282
+ // Check if it's actually a number before formatting
283
+ return !isNaN(parseFloat(value)) && isFinite(value)
284
+ ? numeral(value).format(column.format || '0,0')
285
+ : value;
286
+ case 'boolean':
287
+ if (
288
+ value === 1 ||
289
+ value === true ||
290
+ value === 'true' ||
291
+ value === 'yes' ||
292
+ value === 'Yes'
293
+ ) {
294
+ return 'Yes';
295
+ }
296
+ if (
297
+ value === 0 ||
298
+ value === false ||
299
+ value === 'false' ||
300
+ value === 'no' ||
301
+ value === 'No'
302
+ ) {
303
+ return 'No';
304
+ }
305
+ return value;
306
+ case 'year':
307
+ // Special handling for year values
308
+ return value.toString();
309
+ case 'quarter':
310
+ // Special handling for quarter values (Q1, Q2, etc.)
311
+ if (value && value.toString().startsWith('Q')) {
312
+ return value;
313
+ }
314
+ return value;
315
+ default:
316
+ return value;
317
+ }
318
+ };
319
+
320
+ // State for headers from API response
321
+ const [gridHeaders, setGridHeaders] = useState([]);
322
+
323
+ // Function to sort data
324
+ const sortData = useCallback(
325
+ (data, key, direction) => {
326
+ if (!key) return [...data];
327
+
328
+ return [...data].sort((a, b) => {
329
+ // Find the header config for this key to determine type
330
+ const headerConfig = gridHeaders.find((h) => h.key === key);
331
+ const type = headerConfig?.type || 'text';
332
+
333
+ // Get values, handling null/undefined
334
+ let aValue =
335
+ a[key] === null || a[key] === undefined ? '' : a[key];
336
+ let bValue =
337
+ b[key] === null || b[key] === undefined ? '' : b[key];
338
+
339
+ // Sort based on type
340
+ switch (type) {
341
+ case 'number':
342
+ case 'year':
343
+ aValue = parseFloat(aValue) || 0;
344
+ bValue = parseFloat(bValue) || 0;
345
+ break;
346
+ case 'date':
347
+ // Convert to timestamp for comparison
348
+ aValue = aValue ? new Date(aValue).getTime() : 0;
349
+ bValue = bValue ? new Date(bValue).getTime() : 0;
350
+ break;
351
+ default:
352
+ // For text, convert to lowercase strings
353
+ aValue = String(aValue).toLowerCase();
354
+ bValue = String(bValue).toLowerCase();
355
+ }
356
+
357
+ // Compare values
358
+ if (aValue < bValue) {
359
+ return direction === 'asc' ? -1 : 1;
360
+ }
361
+ if (aValue > bValue) {
362
+ return direction === 'asc' ? 1 : -1;
363
+ }
364
+ return 0;
365
+ });
366
+ },
367
+ [gridHeaders]
368
+ );
369
+
370
+ // Handle sort request
371
+ const requestSort = useCallback((key) => {
372
+ // If clicking the same column, toggle direction
373
+ // Otherwise, start with ascending sort for the new column
374
+ setSortConfig((prevConfig) => {
375
+ const newDirection =
376
+ prevConfig.key === key && prevConfig.direction === 'asc'
377
+ ? 'desc'
378
+ : 'asc';
379
+
380
+ return { key, direction: newDirection };
381
+ });
382
+ }, []);
383
+
384
+ // Function to filter data based on filter values
385
+ const filterData = useCallback(
386
+ (data) => {
387
+ // If no filter values are set, return the original data
388
+ if (Object.keys(filterValues).length === 0) {
389
+ return data;
390
+ }
391
+
392
+ // Filter the data based on the filter values
393
+ return data.filter((row) => {
394
+ // Check each filter
395
+ return Object.entries(filterValues).every(([key, value]) => {
396
+ // Skip empty filter values
397
+ if (!value) return true;
398
+
399
+ // Get the row value
400
+ const rowValue = row[key];
401
+ if (rowValue === null || rowValue === undefined)
402
+ return false;
403
+
404
+ // Find the header config for this key
405
+ const headerConfig = gridHeaders.find((h) => h.key === key);
406
+ if (!headerConfig || !headerConfig.filter) return true;
407
+
408
+ const { type, operator } = headerConfig.filter;
409
+
410
+ // Apply filter based on type and operator
411
+ switch (type) {
412
+ case 'string':
413
+ const rowStr = String(rowValue).toLowerCase();
414
+ const filterStr = String(value).toLowerCase();
415
+
416
+ switch (operator) {
417
+ case 'equals':
418
+ return rowStr === filterStr;
419
+ case 'contains':
420
+ return rowStr.includes(filterStr);
421
+ case 'startsWith':
422
+ return rowStr.startsWith(filterStr);
423
+ case 'endsWith':
424
+ return rowStr.endsWith(filterStr);
425
+ default:
426
+ return rowStr.includes(filterStr);
427
+ }
428
+
429
+ case 'number':
430
+ const rowNum = parseFloat(rowValue);
431
+ const filterNum = parseFloat(value);
432
+
433
+ if (isNaN(rowNum) || isNaN(filterNum)) return false;
434
+
435
+ switch (operator) {
436
+ case 'equals':
437
+ return rowNum === filterNum;
438
+ case 'greaterThan':
439
+ return rowNum > filterNum;
440
+ case 'lessThan':
441
+ return rowNum < filterNum;
442
+ case 'greaterThanOrEqual':
443
+ return rowNum >= filterNum;
444
+ case 'lessThanOrEqual':
445
+ return rowNum <= filterNum;
446
+ default:
447
+ return rowNum === filterNum;
448
+ }
449
+
450
+ case 'date':
451
+ const rowDate = new Date(rowValue);
452
+ const filterDate = new Date(value);
453
+
454
+ if (
455
+ isNaN(rowDate.getTime()) ||
456
+ isNaN(filterDate.getTime())
457
+ )
458
+ return false;
459
+
460
+ switch (operator) {
461
+ case 'equals':
462
+ return (
463
+ rowDate.toDateString() ===
464
+ filterDate.toDateString()
465
+ );
466
+ case 'after':
467
+ return rowDate > filterDate;
468
+ case 'before':
469
+ return rowDate < filterDate;
470
+ default:
471
+ return (
472
+ rowDate.toDateString() ===
473
+ filterDate.toDateString()
474
+ );
475
+ }
476
+
477
+ default:
478
+ return true;
479
+ }
480
+ });
481
+ });
482
+ },
483
+ [filterValues, gridHeaders]
484
+ );
485
+
486
+ // Handle filter change
487
+ const handleFilterChange = useCallback((key, value) => {
488
+ setFilterValues((prev) => ({
489
+ ...prev,
490
+ [key]: value,
491
+ }));
492
+ }, []);
493
+
494
+ // Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
495
+ useEffect(() => {
496
+ if (gridData.length > 0) {
497
+ // First apply filters
498
+ const filtered = filterData(gridData);
499
+ setFilteredData(filtered);
500
+
501
+ // Then sort the filtered data
502
+ const sorted = sortData(
503
+ filtered,
504
+ sortConfig.key,
505
+ sortConfig.direction
506
+ );
507
+ setSortedData(sorted);
508
+ } else {
509
+ setFilteredData([]);
510
+ setSortedData([]);
511
+ }
512
+ }, [gridData, filterValues, sortConfig, sortData, filterData]);
513
+
514
+ // Load data from API if gridSetting.fetch is provided
515
+ useEffect(() => {
516
+ const fetchData = async () => {
517
+ // Only fetch if we have a URL
518
+ if (!settings?.fetch?.url) {
519
+ if (data && data.length > 0) {
520
+ // Use provided data if no fetch setting
521
+ setGridData(data);
522
+ }
523
+ return;
524
+ }
525
+
526
+ const url = settings.fetch.url;
527
+ const method = settings.fetch.method || 'POST';
528
+
529
+ // Combine base params with settings filter values
530
+ const params = {
531
+ ...(settings.fetch.params || {}),
532
+ ...settingsFilterValues,
533
+ };
534
+
535
+ // Create a string representation of the fetch parameters for comparison
536
+ const fetchParamsString = JSON.stringify({ url, method, params });
537
+
538
+ // Skip fetch if the parameters haven't changed
539
+ if (prevFetchParamsRef.current === fetchParamsString) {
540
+ return;
541
+ }
542
+
543
+ // Update the ref with current parameters
544
+ prevFetchParamsRef.current = fetchParamsString;
545
+
546
+ setLoading(true);
547
+ try {
548
+ const response = await CustomFetch(url, method, params);
549
+
550
+ if (response.data.data) {
551
+ // Check if the response has the expected structure with header and rows
552
+ if (response.data.data.header && response.data.data.rows) {
553
+ // Set the headers from the response with the new structure
554
+ const headerEntries = Object.entries(
555
+ response.data.data.header
556
+ );
557
+ setGridHeaders(
558
+ headerEntries.map(([key, config]) => ({
559
+ key,
560
+ label: config.label || key,
561
+ sort: config.sort || key,
562
+ type: config.type || 'text',
563
+ filter: config.filter || null,
564
+ onClick: config.onClick || null,
565
+ }))
566
+ );
567
+
568
+ // Initialize filter values
569
+ const initialFilterValues = {};
570
+ headerEntries.forEach(([key, config]) => {
571
+ if (config.filter) {
572
+ initialFilterValues[key] = '';
573
+ }
574
+ });
575
+ setFilterValues(initialFilterValues);
576
+
577
+ // Set the rows data
578
+ setGridData(response.data.data.rows || []);
579
+ // Also set the initial sorted data
580
+ setSortedData(response.data.data.rows || []);
581
+ } else {
582
+ // Fallback to the previous behavior if the structure is different
583
+ setGridData(response.data.data.data || []);
584
+ setSortedData(response.data.data.data || []);
585
+ }
586
+ }
587
+ } catch (error) {
588
+ console.error('Error fetching grid data:', error);
589
+ } finally {
590
+ setLoading(false);
591
+ }
592
+ };
593
+
594
+ fetchData();
595
+ }, [
596
+ // Only depend on the specific properties we need, not the entire objects
597
+ settings?.fetch?.url,
598
+ settings?.fetch?.method,
599
+ // Use JSON.stringify for the params to detect deep changes
600
+ JSON.stringify(settings?.fetch?.params),
601
+ // Only depend on data if we're not fetching
602
+ !settings?.fetch?.url ? JSON.stringify(data) : null,
603
+ ]);
604
+
605
+ // Generate year options for dropdown
606
+ const generateYearOptions = useCallback((startYear) => {
607
+ const currentYear = new Date().getFullYear();
608
+ const years = [];
609
+
610
+ // Start from the specified year and go up to current year
611
+ for (let year = startYear; year <= currentYear + 1; year++) {
612
+ years.push({ value: year.toString(), label: year.toString() });
613
+ }
614
+
615
+ return years;
616
+ }, []);
617
+
618
+ // Initialize settings filters
619
+ useEffect(() => {
620
+ if (settings?.filters && settings.filters.length > 0) {
621
+ const initialValues = {};
622
+
623
+ settings.filters.forEach((filter) => {
624
+ if (filter.type === 'dropdown' && filter.id === 'year') {
625
+ // For year dropdown with 'now' value, set to current year
626
+ if (filter.value === 'now') {
627
+ initialValues[filter.id] = new Date()
628
+ .getFullYear()
629
+ .toString();
630
+ } else {
631
+ initialValues[filter.id] = filter.value || '';
632
+ }
633
+ } else {
634
+ initialValues[filter.id] = filter.value || '';
635
+ }
636
+ });
637
+
638
+ setSettingsFilterValues(initialValues);
639
+ }
640
+ }, [settings?.filters]);
641
+
642
+ // Handle settings filter change
643
+ const handleSettingsFilterChange = useCallback(
644
+ (filterId, value) => {
645
+ setSettingsFilterValues((prev) => ({
646
+ ...prev,
647
+ [filterId]: value,
648
+ }));
649
+
650
+ // Update fetch params with the new filter value
651
+ if (settings?.fetch?.params) {
652
+ const updatedParams = {
653
+ ...settings.fetch.params,
654
+ [filterId]: value,
655
+ };
656
+
657
+ // Update the settings object
658
+ if (config.settings && config.settings.fetch) {
659
+ config.settings.fetch.params = updatedParams;
660
+ }
661
+
662
+ // Trigger a re-fetch with the new params
663
+ prevFetchParamsRef.current = null;
664
+ }
665
+ },
666
+ [settings, config]
667
+ );
668
+
669
+ // Apply custom styling from userProfile if available
670
+ const getCustomStyles = () => {
671
+ const customStyles = {};
672
+
673
+ if (userProfile?.settings?.style) {
674
+ const { style } = userProfile.settings;
675
+
676
+ if (style.hoverColor) {
677
+ customStyles.hoverColor = {
678
+ '--hover-color': style.hoverColor,
679
+ };
680
+ }
681
+ }
682
+
683
+ return customStyles;
684
+ };
685
+
686
+ // CSS for sort indicators
687
+ const getSortIndicatorStyle = (key) => {
688
+ if (sortConfig.key !== key) {
689
+ return { opacity: 0.3 }; // Dim the icon when not sorting by this column
690
+ }
691
+ return {}; // Full opacity when sorting by this column
692
+ };
693
+
694
+ return (
695
+ <div className={styles.gridContainer}>
696
+ {/* Settings Filters */}
697
+ {settings?.filters && settings.filters.length > 0 && (
698
+ <div className={styles.settingsFiltersContainer}>
699
+ <div className={styles.settingsFiltersHeader}>
700
+ <div
701
+ className={styles.headerLeft}
702
+ onClick={() =>
703
+ setFiltersCollapsed(!filtersCollapsed)
704
+ }
705
+ >
706
+ <div className={styles.settingsFiltersTitle}>
707
+ <svg
708
+ width="16"
709
+ height="16"
710
+ viewBox="0 0 24 24"
711
+ fill="none"
712
+ xmlns="http://www.w3.org/2000/svg"
713
+ >
714
+ <path
715
+ d="M4 6H20M7 12H17M9 18H15"
716
+ stroke="currentColor"
717
+ strokeWidth="2"
718
+ strokeLinecap="round"
719
+ strokeLinejoin="round"
720
+ />
721
+ </svg>
722
+ Filters
723
+ {/* Show active filters count when collapsed */}
724
+ {filtersCollapsed && (
725
+ <div
726
+ className={
727
+ styles.activeFiltersIndicator
728
+ }
729
+ >
730
+ {Object.values(
731
+ settingsFilterValues
732
+ ).some((value) => value !== '') && (
733
+ <>
734
+ <span>Active:</span>
735
+ <span
736
+ className={
737
+ styles.filterBadge
738
+ }
739
+ >
740
+ {
741
+ Object.values(
742
+ settingsFilterValues
743
+ ).filter(
744
+ (value) =>
745
+ value !== ''
746
+ ).length
747
+ }
748
+ </span>
749
+ </>
750
+ )}
751
+ </div>
752
+ )}
753
+ </div>
754
+
755
+ {/* Toggle icon */}
756
+ <svg
757
+ className={`${styles.toggleIcon} ${
758
+ filtersCollapsed ? styles.collapsed : ''
759
+ }`}
760
+ width="16"
761
+ height="16"
762
+ viewBox="0 0 24 24"
763
+ fill="none"
764
+ xmlns="http://www.w3.org/2000/svg"
765
+ >
766
+ <path
767
+ d="M19 9l-7 7-7-7"
768
+ stroke="currentColor"
769
+ strokeWidth="2"
770
+ strokeLinecap="round"
771
+ strokeLinejoin="round"
772
+ />
773
+ </svg>
774
+ </div>
775
+
776
+ {/* Clear filters button */}
777
+ <button
778
+ className={styles.clearFiltersButton}
779
+ onClick={(e) => {
780
+ e.stopPropagation(); // Prevent toggling when clicking the button
781
+ // Initialize with default values
782
+ const initialValues = {};
783
+ settings.filters.forEach((filter) => {
784
+ if (
785
+ filter.type === 'dropdown' &&
786
+ filter.id === 'year' &&
787
+ filter.value === 'now'
788
+ ) {
789
+ initialValues[filter.id] = new Date()
790
+ .getFullYear()
791
+ .toString();
792
+ } else {
793
+ initialValues[filter.id] =
794
+ filter.value || '';
795
+ }
796
+ });
797
+ setSettingsFilterValues(initialValues);
798
+
799
+ // Reset fetch params
800
+ prevFetchParamsRef.current = null;
801
+ }}
802
+ >
803
+ <svg
804
+ viewBox="0 0 24 24"
805
+ fill="none"
806
+ xmlns="http://www.w3.org/2000/svg"
807
+ >
808
+ <path
809
+ d="M19 7l-7 7-7-7"
810
+ stroke="currentColor"
811
+ strokeWidth="2"
812
+ strokeLinecap="round"
813
+ strokeLinejoin="round"
814
+ transform="rotate(90, 12, 12)"
815
+ />
816
+ </svg>
817
+ Clear
818
+ </button>
819
+ </div>
820
+
821
+ <div
822
+ className={`${styles.settingsFiltersContent} ${
823
+ filtersCollapsed ? styles.collapsed : ''
824
+ }`}
825
+ >
826
+ {settings.filters.map((filter, index) => (
827
+ <div
828
+ key={`settings-filter-${index}`}
829
+ className={styles.settingsFilterItem}
830
+ >
831
+ <label className={styles.settingsFilterLabel}>
832
+ {filter.label}
833
+ </label>
834
+ {filter.type === 'dropdown' &&
835
+ filter.id === 'year' ? (
836
+ <select
837
+ className={
838
+ styles.settingsFilterDropdown
839
+ }
840
+ value={
841
+ settingsFilterValues[filter.id] ||
842
+ ''
843
+ }
844
+ onChange={(e) =>
845
+ handleSettingsFilterChange(
846
+ filter.id,
847
+ e.target.value
848
+ )
849
+ }
850
+ >
851
+ {generateYearOptions(
852
+ filter.startYear || 2000
853
+ ).map((option) => (
854
+ <option
855
+ key={option.value}
856
+ value={option.value}
857
+ >
858
+ {option.label}
859
+ </option>
860
+ ))}
861
+ </select>
862
+ ) : filter.type === 'dropdown' ? (
863
+ <select
864
+ className={
865
+ styles.settingsFilterDropdown
866
+ }
867
+ value={
868
+ settingsFilterValues[filter.id] ||
869
+ ''
870
+ }
871
+ onChange={(e) =>
872
+ handleSettingsFilterChange(
873
+ filter.id,
874
+ e.target.value
875
+ )
876
+ }
877
+ >
878
+ {filter.options &&
879
+ filter.options.map((option) => (
880
+ <option
881
+ key={option.value}
882
+ value={option.value}
883
+ >
884
+ {option.label}
885
+ </option>
886
+ ))}
887
+ </select>
888
+ ) : filter.type === 'text' ? (
889
+ <input
890
+ type="text"
891
+ className={styles.settingsFilterInput}
892
+ value={
893
+ settingsFilterValues[filter.id] ||
894
+ ''
895
+ }
896
+ onChange={(e) =>
897
+ handleSettingsFilterChange(
898
+ filter.id,
899
+ e.target.value
900
+ )
901
+ }
902
+ placeholder={filter.placeholder || ''}
903
+ />
904
+ ) : null}
905
+ </div>
906
+ ))}
907
+ </div>
908
+ </div>
909
+ )}
910
+
911
+ {loading ? (
912
+ <div className={styles.loadingContainer}>
913
+ <div className={styles.loadingSpinner}></div>
914
+ <p>Loading data...</p>
915
+ </div>
916
+ ) : (
917
+ <table className={styles.gridTable}>
918
+ <thead>
919
+ <tr>
920
+ {/* Use gridHeaders if available, otherwise fall back to headers from columns */}
921
+ {gridHeaders.length > 0
922
+ ? gridHeaders.map((header, index) => (
923
+ <th
924
+ key={`header-${index}`}
925
+ className={`${styles.gridHeader} ${styles.sortableHeader}`}
926
+ onClick={() =>
927
+ requestSort(header.key)
928
+ }
929
+ style={{ cursor: 'pointer' }}
930
+ >
931
+ <div className={styles.headerContent}>
932
+ {header.label}
933
+ <span
934
+ className={
935
+ styles.sortIndicator
936
+ }
937
+ style={getSortIndicatorStyle(
938
+ header.key
939
+ )}
940
+ >
941
+ {sortConfig.key ===
942
+ header.key &&
943
+ sortConfig.direction === 'asc'
944
+ ? ' ▲'
945
+ : ' ▼'}
946
+ </span>
947
+ </div>
948
+ </th>
949
+ ))
950
+ : headers.map((header, index) => (
951
+ <th
952
+ key={`header-${index}`}
953
+ className={styles.gridHeader}
954
+ >
955
+ {header}
956
+ </th>
957
+ ))}
958
+ </tr>
959
+ {/* Filter row */}
960
+ {gridHeaders.some((header) => header.filter) && (
961
+ <tr className={styles.filterRow}>
962
+ {gridHeaders.map((header, index) => (
963
+ <th
964
+ key={`filter-${index}`}
965
+ className={styles.filterCell}
966
+ >
967
+ {header.filter ? (
968
+ <input
969
+ type={
970
+ header.filter.type ===
971
+ 'date'
972
+ ? 'date'
973
+ : 'text'
974
+ }
975
+ className={styles.filterInput}
976
+ placeholder={`Filter ${header.label}...`}
977
+ value={
978
+ filterValues[header.key] ||
979
+ ''
980
+ }
981
+ onChange={(e) =>
982
+ handleFilterChange(
983
+ header.key,
984
+ e.target.value
985
+ )
986
+ }
987
+ />
988
+ ) : null}
989
+ </th>
990
+ ))}
991
+ </tr>
992
+ )}
993
+ </thead>
994
+ <tbody>
995
+ {sortedData && sortedData.length > 0 ? (
996
+ sortedData.map((row, rowIndex) => (
997
+ <tr
998
+ key={`row-${rowIndex}`}
999
+ className={styles.gridRow}
1000
+ style={getCustomStyles().hoverColor}
1001
+ >
1002
+ {/* Use gridHeaders if available, otherwise fall back to columns */}
1003
+ {gridHeaders.length > 0
1004
+ ? gridHeaders.map(
1005
+ (header, colIndex) => (
1006
+ <td
1007
+ key={`cell-${rowIndex}-${colIndex}`}
1008
+ className={`${
1009
+ styles.gridCell
1010
+ } ${
1011
+ header.onClick &&
1012
+ !row[header.key]
1013
+ ? styles.clickableCell
1014
+ : ''
1015
+ }`}
1016
+ onClick={
1017
+ header.onClick &&
1018
+ !row[header.key]
1019
+ ? async () => {
1020
+ try {
1021
+ // Replace {id} in the URL with the row's ID
1022
+ const url =
1023
+ header.onClick.url.replace(
1024
+ '{id}',
1025
+ row.id
1026
+ );
1027
+
1028
+ // Show loading toast
1029
+ toast.info(
1030
+ 'Updating...',
1031
+ {
1032
+ autoClose: 1000,
1033
+ }
1034
+ );
1035
+
1036
+ // Extract field and value from params
1037
+ const {
1038
+ field,
1039
+ value,
1040
+ ...otherParams
1041
+ } =
1042
+ header
1043
+ .onClick
1044
+ .params;
1045
+
1046
+ // Create a new params object with the field as the key
1047
+ const params =
1048
+ {
1049
+ ...otherParams,
1050
+ _method:
1051
+ header
1052
+ .onClick
1053
+ .method, // Add _method for PUT/DELETE requests
1054
+ };
1055
+
1056
+ // Set the field as the key with the value
1057
+ // If value is "now", replace it with current date in ISO format
1058
+ params[
1059
+ field
1060
+ ] =
1061
+ value ===
1062
+ 'now'
1063
+ ? new Date().toISOString()
1064
+ : value;
1065
+
1066
+ // Make the API call
1067
+ const response =
1068
+ await CustomFetch(
1069
+ url,
1070
+ header
1071
+ .onClick
1072
+ .method,
1073
+ params
1074
+ );
1075
+
1076
+ if (
1077
+ response
1078
+ .data
1079
+ .error
1080
+ ) {
1081
+ toast.error(
1082
+ response
1083
+ .data
1084
+ .error
1085
+ );
1086
+ } else {
1087
+ toast.success(
1088
+ 'Updated successfully',
1089
+ {
1090
+ autoClose: 1000,
1091
+ }
1092
+ );
1093
+ // Refresh the data
1094
+ prevFetchParamsRef.current =
1095
+ null;
1096
+ // Force a re-fetch
1097
+ const fetchUrl =
1098
+ settings
1099
+ .fetch
1100
+ .url;
1101
+ const fetchMethod =
1102
+ settings
1103
+ .fetch
1104
+ .method ||
1105
+ 'POST';
1106
+ const fetchParams =
1107
+ {
1108
+ ...(settings
1109
+ .fetch
1110
+ .params ||
1111
+ {}),
1112
+ ...settingsFilterValues,
1113
+ };
1114
+
1115
+ setLoading(
1116
+ true
1117
+ );
1118
+ try {
1119
+ const fetchResponse =
1120
+ await CustomFetch(
1121
+ fetchUrl,
1122
+ fetchMethod,
1123
+ fetchParams
1124
+ );
1125
+ if (
1126
+ fetchResponse
1127
+ .data
1128
+ .data
1129
+ ) {
1130
+ if (
1131
+ fetchResponse
1132
+ .data
1133
+ .data
1134
+ .header &&
1135
+ fetchResponse
1136
+ .data
1137
+ .data
1138
+ .rows
1139
+ ) {
1140
+ setGridData(
1141
+ fetchResponse
1142
+ .data
1143
+ .data
1144
+ .rows ||
1145
+ []
1146
+ );
1147
+ setSortedData(
1148
+ fetchResponse
1149
+ .data
1150
+ .data
1151
+ .rows ||
1152
+ []
1153
+ );
1154
+ } else {
1155
+ setGridData(
1156
+ fetchResponse
1157
+ .data
1158
+ .data
1159
+ .data ||
1160
+ []
1161
+ );
1162
+ setSortedData(
1163
+ fetchResponse
1164
+ .data
1165
+ .data
1166
+ .data ||
1167
+ []
1168
+ );
1169
+ }
1170
+ }
1171
+ } catch (error) {
1172
+ console.error(
1173
+ 'Error refreshing data:',
1174
+ error
1175
+ );
1176
+ } finally {
1177
+ setLoading(
1178
+ false
1179
+ );
1180
+ }
1181
+ }
1182
+ } catch (error) {
1183
+ console.error(
1184
+ 'Error updating cell:',
1185
+ error
1186
+ );
1187
+ toast.error(
1188
+ 'An error occurred while updating'
1189
+ );
1190
+ }
1191
+ }
1192
+ : undefined
1193
+ }
1194
+ >
1195
+ {row[header.key] ? (
1196
+ header.onClick ? (
1197
+ <span
1198
+ className={
1199
+ styles.cellWithValue
1200
+ }
1201
+ >
1202
+ {formatCellContent(
1203
+ row[
1204
+ header
1205
+ .key
1206
+ ],
1207
+ {
1208
+ type:
1209
+ header.type ||
1210
+ (typeof row[
1211
+ header
1212
+ .key
1213
+ ] ===
1214
+ 'number'
1215
+ ? 'number'
1216
+ : ''),
1217
+ }
1218
+ )}
1219
+ </span>
1220
+ ) : (
1221
+ formatCellContent(
1222
+ row[
1223
+ header.key
1224
+ ],
1225
+ {
1226
+ type:
1227
+ header.type ||
1228
+ (typeof row[
1229
+ header
1230
+ .key
1231
+ ] ===
1232
+ 'number'
1233
+ ? 'number'
1234
+ : ''),
1235
+ }
1236
+ )
1237
+ )
1238
+ ) : header.onClick ? (
1239
+ <span
1240
+ className={
1241
+ styles.placeholderText
1242
+ }
1243
+ >
1244
+ {header.onClick
1245
+ .placeholder ||
1246
+ 'Click to set'}
1247
+ </span>
1248
+ ) : (
1249
+ ''
1250
+ )}
1251
+ </td>
1252
+ )
1253
+ )
1254
+ : columns.map((col, colIndex) => (
1255
+ <td
1256
+ key={`cell-${rowIndex}-${colIndex}`}
1257
+ className={styles.gridCell}
1258
+ >
1259
+ {formatCellContent(
1260
+ row[col.name],
1261
+ col
1262
+ )}
1263
+ </td>
1264
+ ))}
1265
+ </tr>
1266
+ ))
1267
+ ) : (
1268
+ <tr>
1269
+ <td
1270
+ colSpan={
1271
+ gridHeaders.length || headers.length
1272
+ }
1273
+ className={styles.noData}
1274
+ >
1275
+ No data available
1276
+ </td>
1277
+ </tr>
1278
+ )}
1279
+ </tbody>
1280
+ </table>
1281
+ )}
1282
+ </div>
1283
+ );
1284
+ };
1285
+
197
1286
  const ReportForm = ({ config, userProfile }) => {
198
1287
  const [formData, setFormData] = useState({});
199
1288
 
@@ -410,8 +1499,6 @@ function GenericIndex({
410
1499
 
411
1500
  const { data, method, message, url } = checkboxUpdateConfig;
412
1501
 
413
- console.info('Selected rows:', rowsSelectedRef.current);
414
-
415
1502
  if (!Object.keys(rowsSelectedRef.current).length) {
416
1503
  toast.warning(message.warning);
417
1504
  return;
@@ -585,6 +1672,8 @@ function GenericIndex({
585
1672
 
586
1673
  const renderContentBasedOnType = useCallback(() => {
587
1674
  switch (config.type) {
1675
+ case 'grid':
1676
+ return <Grid config={config} userProfile={userProfile} />;
588
1677
  case 'table':
589
1678
  return renderTable();
590
1679
  case 'report':
@@ -664,7 +1753,13 @@ function GenericIndex({
664
1753
  </div>
665
1754
  );
666
1755
  }
667
- }, [layout, subnav, setSubnav, setConfig, renderTable, handleFilterChange]);
1756
+ }, [
1757
+ layout,
1758
+ subnav,
1759
+ renderContentBasedOnType,
1760
+ handleFilterChange,
1761
+ setting.tabs,
1762
+ ]);
668
1763
 
669
1764
  useEffect(() => {
670
1765
  if (setting.page?.tableInfo?.hasOwnProperty('url')) {
@@ -696,6 +1791,14 @@ function GenericIndex({
696
1791
 
697
1792
  // Add an effect to update the config when the active tab changes
698
1793
  useEffect(() => {
1794
+ // Skip if no subnav or tabs
1795
+ if (!subnav?.length || !setting.tabs?.length) {
1796
+ return;
1797
+ }
1798
+
1799
+ // Create a ref to track if we've already processed this combination
1800
+ const activeFilterRef = useRef(null);
1801
+
699
1802
  // Find the active filter and its child
700
1803
  const activeFilter = subnav?.find((filter) => {
701
1804
  if (filter.isParent) {
@@ -707,6 +1810,11 @@ function GenericIndex({
707
1810
  return filter.show === true;
708
1811
  });
709
1812
 
1813
+ // Skip if no active filter
1814
+ if (!activeFilter) {
1815
+ return;
1816
+ }
1817
+
710
1818
  if (
711
1819
  activeFilter?.isParent &&
712
1820
  activeFilter.children &&
@@ -722,6 +1830,20 @@ function GenericIndex({
722
1830
  const firstChild = activeFilter.children[0];
723
1831
  const firstChildId = firstChild.id;
724
1832
 
1833
+ // Create a string representation for comparison
1834
+ const activeFilterString = JSON.stringify({
1835
+ parentId: activeFilter.id,
1836
+ childId: firstChildId,
1837
+ });
1838
+
1839
+ // Skip if we've already processed this combination
1840
+ if (activeFilterRef.current === activeFilterString) {
1841
+ return;
1842
+ }
1843
+
1844
+ // Update the ref
1845
+ activeFilterRef.current = activeFilterString;
1846
+
725
1847
  // Find the tab config for the first child
726
1848
  const firstChildTabConfig = setting.tabs?.find(
727
1849
  (tab) => tab.id === firstChildId
@@ -729,18 +1851,25 @@ function GenericIndex({
729
1851
 
730
1852
  if (firstChildTabConfig) {
731
1853
  // Update the config with the first child's tab config
732
- setConfig((prevConfig) => ({
733
- ...prevConfig,
734
- id: firstChildTabConfig.id,
735
- functions:
736
- firstChildTabConfig.functions ||
737
- prevConfig.functions ||
738
- {},
739
- }));
1854
+ setConfig((prevConfig) => {
1855
+ // Skip update if the ID is already set correctly
1856
+ if (prevConfig.id === firstChildTabConfig.id) {
1857
+ return prevConfig;
1858
+ }
1859
+
1860
+ return {
1861
+ ...prevConfig,
1862
+ id: firstChildTabConfig.id,
1863
+ functions:
1864
+ firstChildTabConfig.functions ||
1865
+ prevConfig.functions ||
1866
+ {},
1867
+ };
1868
+ });
740
1869
  }
741
1870
  }
742
1871
  }
743
- }, [subnav, setting.tabs, setConfig]);
1872
+ }, [subnav, setting.tabs]);
744
1873
 
745
1874
  return (
746
1875
  <>
@@ -765,10 +1894,12 @@ function GenericIndex({
765
1894
  {h.label}]{' '}
766
1895
  </span>
767
1896
  ))}
768
- <span>
769
- [<strong>{total}</strong> Total{' '}
770
- {parse(setting.page?.title || '')}]
771
- </span>
1897
+ {total > 0 && (
1898
+ <span>
1899
+ [<strong>{total}</strong> Total{' '}
1900
+ {parse(setting.page?.title || '')}]
1901
+ </span>
1902
+ )}
772
1903
  </div>
773
1904
  </div>
774
1905
  </div>
@@ -826,16 +1957,6 @@ function GenericIndex({
826
1957
  (tab) => tab.id === firstChild.id
827
1958
  );
828
1959
 
829
- // Log for debugging
830
- console.log(
831
- 'No active child found, using first child:',
832
- firstChild.id
833
- );
834
- console.log(
835
- 'First child tab config:',
836
- firstChildTabConfig
837
- );
838
-
839
1960
  return {
840
1961
  activeTabId: firstChild.id,
841
1962
  activeTabConfig: firstChildTabConfig,