@visns-studio/visns-components 5.10.11 → 5.11.1

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.
@@ -3,7 +3,6 @@ import '../../styles/global.css';
3
3
  import React, {
4
4
  useCallback,
5
5
  useEffect,
6
- useMemo,
7
6
  useRef,
8
7
  useState,
9
8
  } from 'react';
@@ -13,19 +12,17 @@ import { saveAs } from 'file-saver';
13
12
  import _ from 'lodash';
14
13
  import parse from 'html-react-parser';
15
14
  import moment from 'moment';
16
- import numeral from 'numeral';
17
- import { CloudDownload } from 'akar-icons';
18
15
  import Popup from 'reactjs-popup';
19
16
 
20
17
  import CustomFetch from '../Fetch';
21
18
  import Download from '../Download';
22
- import Field from '../Field';
23
19
  import Form from '../Form';
24
20
  import Table from '../DataGrid';
25
21
  import TableFilter from '../TableFilter';
22
+ import GenericGrid from './GenericGrid';
23
+ import GenericReportForm from './GenericReportForm';
26
24
 
27
25
  import styles from './styles/GenericIndex.module.scss';
28
- import reportStyles from './styles/ReportForm.module.scss';
29
26
 
30
27
  function useWindowHeight() {
31
28
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
@@ -226,1253 +223,6 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
226
223
  return { config, setConfig, subnav, setSubnav };
227
224
  }
228
225
 
229
- const Grid = ({ config, userProfile }) => {
230
- const [gridData, setGridData] = useState([]);
231
- const [sortedData, setSortedData] = useState([]);
232
- const [filteredData, setFilteredData] = useState([]);
233
- const [filterValues, setFilterValues] = useState({});
234
- const [settingsFilterValues, setSettingsFilterValues] = useState({});
235
- const [loading, setLoading] = useState(false);
236
- const [filtersCollapsed, setFiltersCollapsed] = useState(false);
237
- const [sortConfig, setSortConfig] = useState({
238
- key: null,
239
- direction: 'asc',
240
- });
241
- const prevFetchParamsRef = useRef(null);
242
-
243
- // Extract data and settings from config using useMemo to prevent unnecessary re-renders
244
- const {
245
- settings,
246
- columns = [],
247
- data = [],
248
- } = useMemo(
249
- () => ({
250
- settings: config.settings || {},
251
- columns: config.columns || [],
252
- data: config.data || [],
253
- }),
254
- [config.settings, config.columns, config.data]
255
- );
256
-
257
- // Create table headers from columns
258
- const headers = useMemo(
259
- () => columns.map((col) => col.header || col.name || ''),
260
- [columns]
261
- );
262
-
263
- // Format cell content based on column type or value type
264
- const formatCellContent = (value, column) => {
265
- if (value === null || value === undefined) return '';
266
- if (value === 0) return '0'; // Explicitly handle zero values
267
- if (value === '') return '';
268
-
269
- // Try to infer the type if not provided
270
- const valueType = column.type || typeof value;
271
-
272
- switch (valueType) {
273
- case 'date':
274
- return moment(value).isValid()
275
- ? moment(value).format(column.format || 'DD/MM/YYYY')
276
- : value;
277
- case 'datetime':
278
- return moment(value).isValid()
279
- ? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
280
- : value;
281
- case 'currency':
282
- return numeral(value).format(column.format || '$0,0.00');
283
- case 'number':
284
- // Check if it's actually a number before formatting
285
- return !isNaN(parseFloat(value)) && isFinite(value)
286
- ? numeral(value).format(column.format || '0,0')
287
- : value;
288
- case 'boolean':
289
- if (
290
- value === 1 ||
291
- value === true ||
292
- value === 'true' ||
293
- value === 'yes' ||
294
- value === 'Yes'
295
- ) {
296
- return 'Yes';
297
- }
298
- if (
299
- value === 0 ||
300
- value === false ||
301
- value === 'false' ||
302
- value === 'no' ||
303
- value === 'No'
304
- ) {
305
- return 'No';
306
- }
307
- return value;
308
- case 'year':
309
- // Special handling for year values
310
- return value.toString();
311
- case 'quarter':
312
- // Special handling for quarter values (Q1, Q2, etc.)
313
- if (value && value.toString().startsWith('Q')) {
314
- return value;
315
- }
316
- return value;
317
- case 'html':
318
- // Explicitly parse HTML content
319
- return parse(String(value));
320
- case 'richtext':
321
- // Parse rich text content
322
- return parse(String(value));
323
- default:
324
- // For text values, check if it contains HTML tags
325
- if (
326
- typeof value === 'string' &&
327
- (value.includes('<br') ||
328
- value.includes('<p>') ||
329
- value.includes('<div') ||
330
- value.includes('<span'))
331
- ) {
332
- // Ensure <br /> tags are properly handled
333
- const processedValue = String(value)
334
- .replace(/<br \/>/g, '<br>')
335
- .replace(/<br\/>/g, '<br>');
336
- return parse(processedValue);
337
- }
338
- return value;
339
- }
340
- };
341
-
342
- // State for headers from API response
343
- const [gridHeaders, setGridHeaders] = useState([]);
344
-
345
- // Function to sort data
346
- const sortData = useCallback(
347
- (data, key, direction) => {
348
- if (!key) return [...data];
349
-
350
- return [...data].sort((a, b) => {
351
- // Find the header config for this key to determine type
352
- const headerConfig = gridHeaders.find((h) => h.key === key);
353
- const type = headerConfig?.type || 'text';
354
-
355
- // Get values, handling null/undefined
356
- let aValue =
357
- a[key] === null || a[key] === undefined ? '' : a[key];
358
- let bValue =
359
- b[key] === null || b[key] === undefined ? '' : b[key];
360
-
361
- // Sort based on type
362
- switch (type) {
363
- case 'number':
364
- case 'year':
365
- aValue = parseFloat(aValue) || 0;
366
- bValue = parseFloat(bValue) || 0;
367
- break;
368
- case 'date':
369
- // Convert to timestamp for comparison
370
- aValue = aValue ? new Date(aValue).getTime() : 0;
371
- bValue = bValue ? new Date(bValue).getTime() : 0;
372
- break;
373
- default:
374
- // For text, convert to lowercase strings
375
- aValue = String(aValue).toLowerCase();
376
- bValue = String(bValue).toLowerCase();
377
- }
378
-
379
- // Compare values
380
- if (aValue < bValue) {
381
- return direction === 'asc' ? -1 : 1;
382
- }
383
- if (aValue > bValue) {
384
- return direction === 'asc' ? 1 : -1;
385
- }
386
- return 0;
387
- });
388
- },
389
- [gridHeaders]
390
- );
391
-
392
- // Handle sort request
393
- const requestSort = useCallback((key) => {
394
- // If clicking the same column, toggle direction
395
- // Otherwise, start with ascending sort for the new column
396
- setSortConfig((prevConfig) => {
397
- const newDirection =
398
- prevConfig.key === key && prevConfig.direction === 'asc'
399
- ? 'desc'
400
- : 'asc';
401
-
402
- return { key, direction: newDirection };
403
- });
404
- }, []);
405
-
406
- // Function to filter data based on filter values
407
- const filterData = useCallback(
408
- (data) => {
409
- // If no filter values are set, return the original data
410
- if (Object.keys(filterValues).length === 0) {
411
- return data;
412
- }
413
-
414
- // Filter the data based on the filter values
415
- return data.filter((row) => {
416
- // Check each filter
417
- return Object.entries(filterValues).every(([key, value]) => {
418
- // Skip empty filter values
419
- if (!value) return true;
420
-
421
- // Get the row value
422
- const rowValue = row[key];
423
- if (rowValue === null || rowValue === undefined)
424
- return false;
425
-
426
- // Find the header config for this key
427
- const headerConfig = gridHeaders.find((h) => h.key === key);
428
- if (!headerConfig || !headerConfig.filter) return true;
429
-
430
- const { type, operator } = headerConfig.filter;
431
-
432
- // Apply filter based on type and operator
433
- switch (type) {
434
- case 'string':
435
- const rowStr = String(rowValue).toLowerCase();
436
- const filterStr = String(value).toLowerCase();
437
-
438
- switch (operator) {
439
- case 'equals':
440
- return rowStr === filterStr;
441
- case 'contains':
442
- return rowStr.includes(filterStr);
443
- case 'startsWith':
444
- return rowStr.startsWith(filterStr);
445
- case 'endsWith':
446
- return rowStr.endsWith(filterStr);
447
- default:
448
- return rowStr.includes(filterStr);
449
- }
450
-
451
- case 'number':
452
- const rowNum = parseFloat(rowValue);
453
- const filterNum = parseFloat(value);
454
-
455
- if (isNaN(rowNum) || isNaN(filterNum)) return false;
456
-
457
- switch (operator) {
458
- case 'equals':
459
- return rowNum === filterNum;
460
- case 'greaterThan':
461
- return rowNum > filterNum;
462
- case 'lessThan':
463
- return rowNum < filterNum;
464
- case 'greaterThanOrEqual':
465
- return rowNum >= filterNum;
466
- case 'lessThanOrEqual':
467
- return rowNum <= filterNum;
468
- default:
469
- return rowNum === filterNum;
470
- }
471
-
472
- case 'date':
473
- const rowDate = new Date(rowValue);
474
- const filterDate = new Date(value);
475
-
476
- if (
477
- isNaN(rowDate.getTime()) ||
478
- isNaN(filterDate.getTime())
479
- )
480
- return false;
481
-
482
- switch (operator) {
483
- case 'equals':
484
- return (
485
- rowDate.toDateString() ===
486
- filterDate.toDateString()
487
- );
488
- case 'after':
489
- return rowDate > filterDate;
490
- case 'before':
491
- return rowDate < filterDate;
492
- default:
493
- return (
494
- rowDate.toDateString() ===
495
- filterDate.toDateString()
496
- );
497
- }
498
-
499
- default:
500
- return true;
501
- }
502
- });
503
- });
504
- },
505
- [filterValues, gridHeaders]
506
- );
507
-
508
- // Handle filter change
509
- const handleFilterChange = useCallback((key, value) => {
510
- setFilterValues((prev) => ({
511
- ...prev,
512
- [key]: value,
513
- }));
514
- }, []);
515
-
516
- // Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
517
- useEffect(() => {
518
- if (gridData.length > 0) {
519
- // First apply filters
520
- const filtered = filterData(gridData);
521
- setFilteredData(filtered);
522
-
523
- // Then sort the filtered data
524
- const sorted = sortData(
525
- filtered,
526
- sortConfig.key,
527
- sortConfig.direction
528
- );
529
- setSortedData(sorted);
530
- } else {
531
- setFilteredData([]);
532
- setSortedData([]);
533
- }
534
- }, [gridData, filterValues, sortConfig, sortData, filterData]);
535
-
536
- // Load data from API if gridSetting.fetch is provided
537
- useEffect(() => {
538
- const fetchData = async () => {
539
- // Only fetch if we have a URL
540
- if (!settings?.fetch?.url) {
541
- if (data && data.length > 0) {
542
- // Use provided data if no fetch setting
543
- setGridData(data);
544
- }
545
- return;
546
- }
547
-
548
- const url = settings.fetch.url;
549
- const method = settings.fetch.method || 'POST';
550
-
551
- // Combine base params with settings filter values
552
- const params = {
553
- ...(settings.fetch.params || {}),
554
- ...settingsFilterValues,
555
- };
556
-
557
- // Create a string representation of the fetch parameters for comparison
558
- const fetchParamsString = JSON.stringify({ url, method, params });
559
-
560
- // Skip fetch if the parameters haven't changed
561
- if (prevFetchParamsRef.current === fetchParamsString) {
562
- return;
563
- }
564
-
565
- // Update the ref with current parameters
566
- prevFetchParamsRef.current = fetchParamsString;
567
-
568
- setLoading(true);
569
- try {
570
- const response = await CustomFetch(url, method, params);
571
-
572
- if (response.data.data) {
573
- // Check if the response has the expected structure with header and rows
574
- if (response.data.data.header && response.data.data.rows) {
575
- // Set the headers from the response with the new structure
576
- const headerEntries = Object.entries(
577
- response.data.data.header
578
- );
579
- setGridHeaders(
580
- headerEntries.map(([key, config]) => ({
581
- key,
582
- label: config.label || key,
583
- sort: config.sort || key,
584
- type: config.type || 'text',
585
- filter: config.filter || null,
586
- onClick: config.onClick || null,
587
- }))
588
- );
589
-
590
- // Initialize filter values
591
- const initialFilterValues = {};
592
- headerEntries.forEach(([key, config]) => {
593
- if (config.filter) {
594
- initialFilterValues[key] = '';
595
- }
596
- });
597
- setFilterValues(initialFilterValues);
598
-
599
- // Set the rows data
600
- setGridData(response.data.data.rows || []);
601
- // Also set the initial sorted data
602
- setSortedData(response.data.data.rows || []);
603
- } else {
604
- // Fallback to the previous behavior if the structure is different
605
- setGridData(response.data.data.data || []);
606
- setSortedData(response.data.data.data || []);
607
- }
608
- }
609
- } catch (error) {
610
- console.error('Error fetching grid data:', error);
611
- } finally {
612
- setLoading(false);
613
- }
614
- };
615
-
616
- fetchData();
617
- }, [
618
- // Only depend on the specific properties we need, not the entire objects
619
- settings?.fetch?.url,
620
- settings?.fetch?.method,
621
- // Use JSON.stringify for the params to detect deep changes
622
- JSON.stringify(settings?.fetch?.params),
623
- // Only depend on data if we're not fetching
624
- !settings?.fetch?.url ? JSON.stringify(data) : null,
625
- ]);
626
-
627
- // Generate year options for dropdown
628
- const generateYearOptions = useCallback((startYear) => {
629
- const currentYear = new Date().getFullYear();
630
- const years = [];
631
-
632
- // Start from the specified year and go up to current year
633
- for (let year = startYear; year <= currentYear + 1; year++) {
634
- years.push({ value: year.toString(), label: year.toString() });
635
- }
636
-
637
- return years;
638
- }, []);
639
-
640
- // Initialize settings filters
641
- useEffect(() => {
642
- if (settings?.filters && settings.filters.length > 0) {
643
- const initialValues = {};
644
-
645
- settings.filters.forEach((filter) => {
646
- if (filter.type === 'dropdown' && filter.id === 'year') {
647
- // For year dropdown with 'now' value, set to current year
648
- if (filter.value === 'now') {
649
- initialValues[filter.id] = new Date()
650
- .getFullYear()
651
- .toString();
652
- } else {
653
- initialValues[filter.id] = filter.value || '';
654
- }
655
- } else {
656
- initialValues[filter.id] = filter.value || '';
657
- }
658
- });
659
-
660
- setSettingsFilterValues(initialValues);
661
- }
662
- }, [settings?.filters]);
663
-
664
- // Handle settings filter change
665
- const handleSettingsFilterChange = useCallback(
666
- (filterId, value) => {
667
- setSettingsFilterValues((prev) => ({
668
- ...prev,
669
- [filterId]: value,
670
- }));
671
-
672
- // Update fetch params with the new filter value
673
- if (settings?.fetch?.params) {
674
- const updatedParams = {
675
- ...settings.fetch.params,
676
- [filterId]: value,
677
- };
678
-
679
- // Update the settings object
680
- if (config.settings && config.settings.fetch) {
681
- config.settings.fetch.params = updatedParams;
682
- }
683
-
684
- // Trigger a re-fetch with the new params
685
- prevFetchParamsRef.current = null;
686
- }
687
- },
688
- [settings, config]
689
- );
690
-
691
- // Apply custom styling from userProfile if available
692
- const getCustomStyles = () => {
693
- const customStyles = {};
694
-
695
- if (userProfile?.settings?.style) {
696
- const { style } = userProfile.settings;
697
-
698
- if (style.hoverColor) {
699
- customStyles.hoverColor = {
700
- '--hover-color': style.hoverColor,
701
- };
702
- }
703
- }
704
-
705
- return customStyles;
706
- };
707
-
708
- // CSS for sort indicators
709
- const getSortIndicatorStyle = (key) => {
710
- if (sortConfig.key !== key) {
711
- return { opacity: 0.3 }; // Dim the icon when not sorting by this column
712
- }
713
- return {}; // Full opacity when sorting by this column
714
- };
715
-
716
- return (
717
- <div className={styles.gridContainer}>
718
- {/* Settings Filters */}
719
- {settings?.filters && settings.filters.length > 0 && (
720
- <div className={styles.settingsFiltersContainer}>
721
- <div className={styles.settingsFiltersHeader}>
722
- <div
723
- className={styles.headerLeft}
724
- onClick={() =>
725
- setFiltersCollapsed(!filtersCollapsed)
726
- }
727
- >
728
- <div className={styles.settingsFiltersTitle}>
729
- <svg
730
- width="16"
731
- height="16"
732
- viewBox="0 0 24 24"
733
- fill="none"
734
- xmlns="http://www.w3.org/2000/svg"
735
- >
736
- <path
737
- d="M4 6H20M7 12H17M9 18H15"
738
- stroke="currentColor"
739
- strokeWidth="2"
740
- strokeLinecap="round"
741
- strokeLinejoin="round"
742
- />
743
- </svg>
744
- Filters
745
- {/* Show active filters count when collapsed */}
746
- {filtersCollapsed && (
747
- <div
748
- className={
749
- styles.activeFiltersIndicator
750
- }
751
- >
752
- {Object.values(
753
- settingsFilterValues
754
- ).some((value) => value !== '') && (
755
- <>
756
- <span>Active:</span>
757
- <span
758
- className={
759
- styles.filterBadge
760
- }
761
- >
762
- {
763
- Object.values(
764
- settingsFilterValues
765
- ).filter(
766
- (value) =>
767
- value !== ''
768
- ).length
769
- }
770
- </span>
771
- </>
772
- )}
773
- </div>
774
- )}
775
- </div>
776
-
777
- {/* Toggle icon */}
778
- <svg
779
- className={`${styles.toggleIcon} ${
780
- filtersCollapsed ? styles.collapsed : ''
781
- }`}
782
- width="16"
783
- height="16"
784
- viewBox="0 0 24 24"
785
- fill="none"
786
- xmlns="http://www.w3.org/2000/svg"
787
- >
788
- <path
789
- d="M19 9l-7 7-7-7"
790
- stroke="currentColor"
791
- strokeWidth="2"
792
- strokeLinecap="round"
793
- strokeLinejoin="round"
794
- />
795
- </svg>
796
- </div>
797
-
798
- {/* Clear filters button */}
799
- <button
800
- className={styles.clearFiltersButton}
801
- onClick={(e) => {
802
- e.stopPropagation(); // Prevent toggling when clicking the button
803
- // Initialize with default values
804
- const initialValues = {};
805
- settings.filters.forEach((filter) => {
806
- if (
807
- filter.type === 'dropdown' &&
808
- filter.id === 'year' &&
809
- filter.value === 'now'
810
- ) {
811
- initialValues[filter.id] = new Date()
812
- .getFullYear()
813
- .toString();
814
- } else {
815
- initialValues[filter.id] =
816
- filter.value || '';
817
- }
818
- });
819
- setSettingsFilterValues(initialValues);
820
-
821
- // Reset fetch params
822
- prevFetchParamsRef.current = null;
823
- }}
824
- >
825
- <svg
826
- viewBox="0 0 24 24"
827
- fill="none"
828
- xmlns="http://www.w3.org/2000/svg"
829
- >
830
- <path
831
- d="M19 7l-7 7-7-7"
832
- stroke="currentColor"
833
- strokeWidth="2"
834
- strokeLinecap="round"
835
- strokeLinejoin="round"
836
- transform="rotate(90, 12, 12)"
837
- />
838
- </svg>
839
- Clear
840
- </button>
841
- </div>
842
-
843
- <div
844
- className={`${styles.settingsFiltersContent} ${
845
- filtersCollapsed ? styles.collapsed : ''
846
- }`}
847
- >
848
- {settings.filters.map((filter, index) => (
849
- <div
850
- key={`settings-filter-${index}`}
851
- className={styles.settingsFilterItem}
852
- >
853
- <label className={styles.settingsFilterLabel}>
854
- {filter.label}
855
- </label>
856
- {filter.type === 'dropdown' &&
857
- filter.id === 'year' ? (
858
- <select
859
- className={
860
- styles.settingsFilterDropdown
861
- }
862
- value={
863
- settingsFilterValues[filter.id] ||
864
- ''
865
- }
866
- onChange={(e) =>
867
- handleSettingsFilterChange(
868
- filter.id,
869
- e.target.value
870
- )
871
- }
872
- >
873
- {generateYearOptions(
874
- filter.startYear || 2000
875
- ).map((option) => (
876
- <option
877
- key={option.value}
878
- value={option.value}
879
- >
880
- {option.label}
881
- </option>
882
- ))}
883
- </select>
884
- ) : filter.type === 'dropdown' ? (
885
- <select
886
- className={
887
- styles.settingsFilterDropdown
888
- }
889
- value={
890
- settingsFilterValues[filter.id] ||
891
- ''
892
- }
893
- onChange={(e) =>
894
- handleSettingsFilterChange(
895
- filter.id,
896
- e.target.value
897
- )
898
- }
899
- >
900
- {filter.options &&
901
- filter.options.map((option) => (
902
- <option
903
- key={option.value}
904
- value={option.value}
905
- >
906
- {option.label}
907
- </option>
908
- ))}
909
- </select>
910
- ) : filter.type === 'text' ? (
911
- <input
912
- type="text"
913
- className={styles.settingsFilterInput}
914
- value={
915
- settingsFilterValues[filter.id] ||
916
- ''
917
- }
918
- onChange={(e) =>
919
- handleSettingsFilterChange(
920
- filter.id,
921
- e.target.value
922
- )
923
- }
924
- placeholder={filter.placeholder || ''}
925
- />
926
- ) : null}
927
- </div>
928
- ))}
929
- </div>
930
- </div>
931
- )}
932
-
933
- {loading ? (
934
- <div className={styles.loadingContainer}>
935
- <div className={styles.loadingSpinner}></div>
936
- <p>Loading data...</p>
937
- </div>
938
- ) : (
939
- <table className={styles.gridTable}>
940
- <thead>
941
- <tr>
942
- {/* Use gridHeaders if available, otherwise fall back to headers from columns */}
943
- {gridHeaders.length > 0
944
- ? gridHeaders.map((header, index) => (
945
- <th
946
- key={`header-${index}`}
947
- className={`${styles.gridHeader} ${styles.sortableHeader}`}
948
- onClick={() =>
949
- requestSort(header.key)
950
- }
951
- style={{ cursor: 'pointer' }}
952
- >
953
- <div className={styles.headerContent}>
954
- {header.label}
955
- <span
956
- className={
957
- styles.sortIndicator
958
- }
959
- style={getSortIndicatorStyle(
960
- header.key
961
- )}
962
- >
963
- {sortConfig.key ===
964
- header.key &&
965
- sortConfig.direction === 'asc'
966
- ? ' ▲'
967
- : ' ▼'}
968
- </span>
969
- </div>
970
- </th>
971
- ))
972
- : headers.map((header, index) => (
973
- <th
974
- key={`header-${index}`}
975
- className={styles.gridHeader}
976
- >
977
- {header}
978
- </th>
979
- ))}
980
- </tr>
981
- {/* Filter row */}
982
- {gridHeaders.some((header) => header.filter) && (
983
- <tr className={styles.filterRow}>
984
- {gridHeaders.map((header, index) => (
985
- <th
986
- key={`filter-${index}`}
987
- className={styles.filterCell}
988
- >
989
- {header.filter ? (
990
- <input
991
- type={
992
- header.filter.type ===
993
- 'date'
994
- ? 'date'
995
- : 'text'
996
- }
997
- className={styles.filterInput}
998
- placeholder={`Filter ${header.label}...`}
999
- value={
1000
- filterValues[header.key] ||
1001
- ''
1002
- }
1003
- onChange={(e) =>
1004
- handleFilterChange(
1005
- header.key,
1006
- e.target.value
1007
- )
1008
- }
1009
- />
1010
- ) : null}
1011
- </th>
1012
- ))}
1013
- </tr>
1014
- )}
1015
- </thead>
1016
- <tbody>
1017
- {sortedData && sortedData.length > 0 ? (
1018
- sortedData.map((row, rowIndex) => (
1019
- <tr
1020
- key={`row-${rowIndex}`}
1021
- className={styles.gridRow}
1022
- style={getCustomStyles().hoverColor}
1023
- >
1024
- {/* Use gridHeaders if available, otherwise fall back to columns */}
1025
- {gridHeaders.length > 0
1026
- ? gridHeaders.map(
1027
- (header, colIndex) => (
1028
- <td
1029
- key={`cell-${rowIndex}-${colIndex}`}
1030
- className={`${
1031
- styles.gridCell
1032
- } ${
1033
- header.onClick &&
1034
- !row[header.key]
1035
- ? styles.clickableCell
1036
- : ''
1037
- }`}
1038
- onClick={
1039
- header.onClick &&
1040
- !row[header.key]
1041
- ? async () => {
1042
- try {
1043
- // Replace {id} in the URL with the row's ID
1044
- const url =
1045
- header.onClick.url.replace(
1046
- '{id}',
1047
- row.id
1048
- );
1049
-
1050
- // Show loading toast
1051
- toast.info(
1052
- 'Updating...',
1053
- {
1054
- autoClose: 1000,
1055
- }
1056
- );
1057
-
1058
- // Extract field and value from params
1059
- const {
1060
- field,
1061
- value,
1062
- ...otherParams
1063
- } =
1064
- header
1065
- .onClick
1066
- .params;
1067
-
1068
- // Create a new params object with the field as the key
1069
- const params =
1070
- {
1071
- ...otherParams,
1072
- _method:
1073
- header
1074
- .onClick
1075
- .method, // Add _method for PUT/DELETE requests
1076
- };
1077
-
1078
- // Set the field as the key with the value
1079
- // If value is "now", replace it with current date in ISO format
1080
- params[
1081
- field
1082
- ] =
1083
- value ===
1084
- 'now'
1085
- ? new Date().toISOString()
1086
- : value;
1087
-
1088
- // Make the API call
1089
- const response =
1090
- await CustomFetch(
1091
- url,
1092
- header
1093
- .onClick
1094
- .method,
1095
- params
1096
- );
1097
-
1098
- if (
1099
- response
1100
- .data
1101
- .error
1102
- ) {
1103
- toast.error(
1104
- response
1105
- .data
1106
- .error
1107
- );
1108
- } else {
1109
- toast.success(
1110
- 'Updated successfully',
1111
- {
1112
- autoClose: 1000,
1113
- }
1114
- );
1115
- // Refresh the data
1116
- prevFetchParamsRef.current =
1117
- null;
1118
- // Force a re-fetch
1119
- const fetchUrl =
1120
- settings
1121
- .fetch
1122
- .url;
1123
- const fetchMethod =
1124
- settings
1125
- .fetch
1126
- .method ||
1127
- 'POST';
1128
- const fetchParams =
1129
- {
1130
- ...(settings
1131
- .fetch
1132
- .params ||
1133
- {}),
1134
- ...settingsFilterValues,
1135
- };
1136
-
1137
- setLoading(
1138
- true
1139
- );
1140
- try {
1141
- const fetchResponse =
1142
- await CustomFetch(
1143
- fetchUrl,
1144
- fetchMethod,
1145
- fetchParams
1146
- );
1147
- if (
1148
- fetchResponse
1149
- .data
1150
- .data
1151
- ) {
1152
- if (
1153
- fetchResponse
1154
- .data
1155
- .data
1156
- .header &&
1157
- fetchResponse
1158
- .data
1159
- .data
1160
- .rows
1161
- ) {
1162
- setGridData(
1163
- fetchResponse
1164
- .data
1165
- .data
1166
- .rows ||
1167
- []
1168
- );
1169
- setSortedData(
1170
- fetchResponse
1171
- .data
1172
- .data
1173
- .rows ||
1174
- []
1175
- );
1176
- } else {
1177
- setGridData(
1178
- fetchResponse
1179
- .data
1180
- .data
1181
- .data ||
1182
- []
1183
- );
1184
- setSortedData(
1185
- fetchResponse
1186
- .data
1187
- .data
1188
- .data ||
1189
- []
1190
- );
1191
- }
1192
- }
1193
- } catch (error) {
1194
- console.error(
1195
- 'Error refreshing data:',
1196
- error
1197
- );
1198
- } finally {
1199
- setLoading(
1200
- false
1201
- );
1202
- }
1203
- }
1204
- } catch (error) {
1205
- console.error(
1206
- 'Error updating cell:',
1207
- error
1208
- );
1209
- toast.error(
1210
- 'An error occurred while updating'
1211
- );
1212
- }
1213
- }
1214
- : undefined
1215
- }
1216
- >
1217
- {row[header.key] ? (
1218
- header.onClick ? (
1219
- <span
1220
- className={
1221
- styles.cellWithValue
1222
- }
1223
- >
1224
- {formatCellContent(
1225
- row[
1226
- header
1227
- .key
1228
- ],
1229
- {
1230
- type:
1231
- header.type ||
1232
- (typeof row[
1233
- header
1234
- .key
1235
- ] ===
1236
- 'number'
1237
- ? 'number'
1238
- : ''),
1239
- }
1240
- )}
1241
- </span>
1242
- ) : (
1243
- <span
1244
- className={
1245
- styles.cellContent
1246
- }
1247
- data-type={
1248
- header.type ||
1249
- (typeof row[
1250
- header
1251
- .key
1252
- ] ===
1253
- 'number'
1254
- ? 'number'
1255
- : '')
1256
- }
1257
- >
1258
- {formatCellContent(
1259
- row[
1260
- header
1261
- .key
1262
- ],
1263
- {
1264
- type:
1265
- header.type ||
1266
- (typeof row[
1267
- header
1268
- .key
1269
- ] ===
1270
- 'number'
1271
- ? 'number'
1272
- : ''),
1273
- }
1274
- )}
1275
- </span>
1276
- )
1277
- ) : header.onClick ? (
1278
- <span
1279
- className={
1280
- styles.placeholderText
1281
- }
1282
- >
1283
- {header.onClick
1284
- .placeholder ||
1285
- 'Click to set'}
1286
- </span>
1287
- ) : (
1288
- ''
1289
- )}
1290
- </td>
1291
- )
1292
- )
1293
- : columns.map((col, colIndex) => (
1294
- <td
1295
- key={`cell-${rowIndex}-${colIndex}`}
1296
- className={styles.gridCell}
1297
- >
1298
- <span
1299
- className={
1300
- styles.cellContent
1301
- }
1302
- data-type={
1303
- col.type ||
1304
- (typeof row[
1305
- col.name
1306
- ] === 'number'
1307
- ? 'number'
1308
- : '')
1309
- }
1310
- >
1311
- {formatCellContent(
1312
- row[col.name],
1313
- col
1314
- )}
1315
- </span>
1316
- </td>
1317
- ))}
1318
- </tr>
1319
- ))
1320
- ) : (
1321
- <tr>
1322
- <td
1323
- colSpan={
1324
- gridHeaders.length || headers.length
1325
- }
1326
- className={styles.noData}
1327
- >
1328
- No data available
1329
- </td>
1330
- </tr>
1331
- )}
1332
- </tbody>
1333
- </table>
1334
- )}
1335
- </div>
1336
- );
1337
- };
1338
-
1339
- const ReportForm = ({ config, userProfile }) => {
1340
- const [formData, setFormData] = useState({});
1341
-
1342
- const handleChange = (e) => {
1343
- const {
1344
- value,
1345
- dataset: { name },
1346
- } = e.target;
1347
-
1348
- setFormData((prevData) => ({ ...prevData, [name]: value }));
1349
- };
1350
-
1351
- const handleChangeDate = (date, id) => {
1352
- setFormData((prevData) => ({ ...prevData, [id]: date }));
1353
- };
1354
-
1355
- const handleChangeSelect = (inputValue, _, id) => {
1356
- setFormData((prevData) => ({ ...prevData, [id]: inputValue }));
1357
- };
1358
-
1359
- const handleExport = async (e) => {
1360
- try {
1361
- if (e) {
1362
- e.preventDefault();
1363
- }
1364
-
1365
- const missingFields = config.form.filters
1366
- .filter(
1367
- (item) => item.required === true && formData[item.id] === ''
1368
- )
1369
- .map((item) => item.label);
1370
-
1371
- if (missingFields.length > 0) {
1372
- const fieldNames = missingFields.join(', ');
1373
- const errorMessage = `Please fill in a value for the following field(s): ${fieldNames}.`;
1374
- toast.error(errorMessage);
1375
- } else {
1376
- toast.info('Preparing your export...');
1377
-
1378
- const res = await Download(
1379
- config.form.url,
1380
- config.form.method,
1381
- formData
1382
- );
1383
-
1384
- if (res.data.error) {
1385
- toast.error(res.data.error);
1386
- } else {
1387
- // Assuming the response contains a Blob or similar data
1388
- if (res.data && res.data instanceof Blob) {
1389
- const fileName = `${config.form.filename}`;
1390
- saveAs(res.data, fileName);
1391
- toast.success('Export completed successfully');
1392
- } else {
1393
- // Handle the case where the response is not a Blob
1394
- toast.error('Invalid file format received.');
1395
- }
1396
- }
1397
- }
1398
- } catch (err) {
1399
- console.error(err);
1400
- toast.error('An error occurred during export');
1401
- }
1402
- };
1403
-
1404
- useEffect(() => {
1405
- const tmpForm = config.form?.filters.reduce((acc, item) => {
1406
- acc[item.id] = '';
1407
- return acc;
1408
- }, {});
1409
- setFormData(tmpForm);
1410
- }, [config.form?.filters]);
1411
-
1412
- // Determine if the form has a title
1413
- const formTitle = config.form?.title || 'Export Report';
1414
- const formDescription =
1415
- config.form?.description || 'Select your export criteria below.';
1416
-
1417
- return (
1418
- <div className={reportStyles.reportFormContainer}>
1419
- <form className={reportStyles.reportForm}>
1420
- <h2 className={reportStyles.formTitle}>{formTitle}</h2>
1421
- <p className={reportStyles.formDescription}>
1422
- {formDescription}
1423
- </p>
1424
-
1425
- {config.form?.filters.map((item, key) => (
1426
- <div
1427
- key={`filter-container-${key}`}
1428
- className={`${reportStyles.formField} ${
1429
- item.fullWidth ? reportStyles.fullWidthField : ''
1430
- }`}
1431
- >
1432
- <Field
1433
- api={userProfile.settings.api}
1434
- autocompleteCallback={() => {}}
1435
- childDropdownCallback={() => {}}
1436
- fetchData={() => {}}
1437
- formData={formData}
1438
- inputClass={''}
1439
- inputValue={formData[item.id]}
1440
- key={`filter-fields-${key}`}
1441
- mapbox={{}}
1442
- onChange={handleChange}
1443
- onChangeCheckbox={() => {}}
1444
- onChangeDate={handleChangeDate}
1445
- onChangeNumberFormat={() => {}}
1446
- onChangeRicheditor={() => {}}
1447
- onChangeSelect={handleChangeSelect}
1448
- onChangeToggle={() => {}}
1449
- settings={item}
1450
- setFormData={setFormData}
1451
- style={userProfile.settings.style}
1452
- uploadProgress={0}
1453
- userProfile={userProfile}
1454
- />
1455
- </div>
1456
- ))}
1457
-
1458
- <div className={reportStyles.formActions}>
1459
- <button
1460
- className={reportStyles.exportButton}
1461
- onClick={handleExport}
1462
- type="button"
1463
- >
1464
- <CloudDownload
1465
- strokeWidth={2}
1466
- size={18}
1467
- style={{ marginRight: '8px' }}
1468
- />{' '}
1469
- Export
1470
- </button>
1471
- </div>
1472
- </form>
1473
- </div>
1474
- );
1475
- };
1476
226
 
1477
227
  function GenericIndex({
1478
228
  layout = 'full',
@@ -1856,15 +606,15 @@ function GenericIndex({
1856
606
  const renderContentBasedOnType = useCallback(() => {
1857
607
  switch (config.type) {
1858
608
  case 'grid':
1859
- return <Grid config={config} userProfile={userProfile} />;
609
+ return <GenericGrid config={config} userProfile={userProfile} />;
1860
610
  case 'table':
1861
611
  return renderTable();
1862
612
  case 'report':
1863
- return <ReportForm config={config} userProfile={userProfile} />;
613
+ return <GenericReportForm config={config} userProfile={userProfile} />;
1864
614
  default:
1865
615
  return renderTable(); // Fallback to table if no type is specified or recognized
1866
616
  }
1867
- }, [config, renderTable]);
617
+ }, [config, renderTable, userProfile]);
1868
618
 
1869
619
  // Custom handler for filter changes
1870
620
  const handleFilterChange = useCallback(