@visns-studio/visns-components 5.0.11 → 5.0.13

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.
package/package.json CHANGED
@@ -75,7 +75,7 @@
75
75
  "react-dom": "^17.0.0 || ^18.0.0"
76
76
  },
77
77
  "name": "@visns-studio/visns-components",
78
- "version": "5.0.11",
78
+ "version": "5.0.13",
79
79
  "description": "Various packages to assist in the development of our Custom Applications.",
80
80
  "main": "src/index.js",
81
81
  "files": [
@@ -207,6 +207,12 @@ function Field({
207
207
  filter.orderBy = s.orderBy;
208
208
  }
209
209
 
210
+ if (a.where) {
211
+ forEach(a.where, (w) => {
212
+ filter.where.push(w);
213
+ });
214
+ }
215
+
210
216
  CustomFetch(s.child.url, 'POST', filter, function (result) {
211
217
  childDropdownCallback({
212
218
  id: s.child.id,
@@ -314,7 +320,7 @@ function Field({
314
320
  <span>{settings.label}</span>
315
321
  </div>
316
322
  );
317
- } else if (['richeditor'].includes(settings.type)) {
323
+ } else if (['richeditor', 'textarea'].includes(settings.type)) {
318
324
  return settings.label;
319
325
  }
320
326
  }
@@ -1664,6 +1670,7 @@ function Field({
1664
1670
  'signature',
1665
1671
  'table_radio',
1666
1672
  'table_text',
1673
+ 'textarea',
1667
1674
  'time',
1668
1675
  'toggle',
1669
1676
  ].includes(settings.type)
@@ -1882,7 +1889,9 @@ function Field({
1882
1889
  <div className={formItemClass} style={formItemStyle}>
1883
1890
  <label
1884
1891
  className={
1885
- settings.type === 'richeditor' ? '' : styles.fi__label
1892
+ ['richeditor', 'textarea'].includes(settings.type)
1893
+ ? ''
1894
+ : styles.fi__label
1886
1895
  }
1887
1896
  >
1888
1897
  {renderLabel()}
@@ -621,8 +621,6 @@ function Form({
621
621
  }
622
622
  });
623
623
 
624
- console.info(_inputClass);
625
-
626
624
  setInputClass(_inputClass);
627
625
  }
628
626
 
@@ -672,7 +670,14 @@ function Form({
672
670
 
673
671
  if (!_.isEmpty(formData)) {
674
672
  Object.keys(formData).forEach((item) => {
675
- if (formData[item] instanceof File) {
673
+ const value = formData[item];
674
+
675
+ // Check if it's a single File or an array of File objects
676
+ if (
677
+ value instanceof File ||
678
+ (Array.isArray(value) &&
679
+ value.every((file) => file instanceof File))
680
+ ) {
676
681
  fileUpload = true;
677
682
  fileKey = item;
678
683
 
@@ -842,8 +847,6 @@ function Form({
842
847
  const fetchData = async () => {
843
848
  let _formData = { ...formData };
844
849
 
845
- console.info('aaa', formType, columnId);
846
-
847
850
  if (formSettings.fields.length > 0) {
848
851
  formSettings.fields.forEach((item) => {
849
852
  const { id, value, type, parent, urlParam } = item;
@@ -1,11 +1,9 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import CustomFetch from './Fetch';
3
- import SelectList from './SelectList';
4
3
  import Select, { createFilter, components } from 'react-select';
5
4
  import CreatableSelect from 'react-select/creatable';
6
5
  import styles from './styles/AsyncSelect.module.scss';
7
6
 
8
- // Custom Option component to add a second line of text
9
7
  const CustomOption = (props) => (
10
8
  <components.Option {...props}>
11
9
  <div>
@@ -34,34 +32,49 @@ function MultiSelect({
34
32
  const [selectOptions, setSelectOptions] = useState([]);
35
33
  const [selectValue, setSelectValue] = useState([]);
36
34
 
37
- useEffect(() => {
38
- const _options = Array.isArray(options)
39
- ? options.map((a) => ({
40
- value: a.id,
41
- label: a.label || a.name || a.description || 'Unknown',
42
- description: a.description || '', // Include description field
43
- ...a,
44
- }))
45
- : [];
46
- setSelectOptions(_options);
35
+ // Memoize transformed options
36
+ const memoizedOptions = useMemo(() => {
37
+ return options
38
+ .filter((a) => a?.label || a?.name || a?.description) // Filter out options with no label, name, or description
39
+ .map((a) => ({
40
+ value: a?.id,
41
+ label: a?.label || a?.name || a?.description || 'Unknown',
42
+ description: a?.description || '',
43
+ ...a,
44
+ }));
47
45
  }, [options]);
48
46
 
49
- useEffect(() => {
50
- const normalizeInputValue = (value) => {
51
- // Ensure value is an array, wrapping single items if necessary
52
- return Array.isArray(value) ? value : value ? [value] : [];
53
- };
54
-
55
- const _values = normalizeInputValue(inputValue).map((a) => ({
56
- value: a.id,
57
- label: a.label || a.name || a.description || 'Unknown',
58
- description: a.description || '', // Include description field
59
- ...a,
60
- }));
47
+ // Memoize transformed input values
48
+ const memoizedInputValue = useMemo(() => {
49
+ const normalizeInputValue = (value) =>
50
+ Array.isArray(value) ? value : value ? [value] : [];
61
51
 
62
- setSelectValue(_values);
52
+ return normalizeInputValue(inputValue)
53
+ .filter((a) => a?.label || a?.name || a?.description) // Filter out values with no label, name, or description
54
+ .map((a) => ({
55
+ value: a?.id,
56
+ label: a?.label || a?.name || a?.description || 'Unknown',
57
+ description: a?.description || '',
58
+ ...a,
59
+ }));
63
60
  }, [inputValue]);
64
61
 
62
+ // Set options when memoizedOptions changes
63
+ useEffect(() => {
64
+ if (JSON.stringify(selectOptions) !== JSON.stringify(memoizedOptions)) {
65
+ setSelectOptions(memoizedOptions);
66
+ }
67
+ }, [memoizedOptions, selectOptions]);
68
+
69
+ // Set value when memoizedInputValue changes
70
+ useEffect(() => {
71
+ if (
72
+ JSON.stringify(selectValue) !== JSON.stringify(memoizedInputValue)
73
+ ) {
74
+ setSelectValue(memoizedInputValue);
75
+ }
76
+ }, [memoizedInputValue, selectValue]);
77
+
65
78
  const handleCreate = async (inputValue) => {
66
79
  if (creatableConfig.url && creatableConfig.method) {
67
80
  const res = await CustomFetch(
@@ -74,17 +87,15 @@ function MultiSelect({
74
87
  const newOption = {
75
88
  value: res.data.id,
76
89
  label: res.data.label || inputValue,
77
- description: res.data.description || 'Newly created item', // Default description for new items
90
+ description: res.data.description || 'Newly created item',
78
91
  ...res.data,
79
92
  };
80
93
 
81
- // Update options and trigger onChange with the new option
82
94
  setSelectOptions((prevOptions) => [...prevOptions, newOption]);
83
95
  setSelectValue((prevValues) =>
84
96
  multi ? [...prevValues, newOption] : [newOption]
85
97
  );
86
98
 
87
- // Trigger onChange with the new option
88
99
  onChange(
89
100
  multi ? [...selectValue, newOption] : newOption,
90
101
  { action: 'create-option' },
@@ -103,7 +114,7 @@ function MultiSelect({
103
114
  isSearchable
104
115
  isMulti={multi}
105
116
  filterOption={createFilter({ ignoreAccents: false })}
106
- components={{ Option: CustomOption }} // Use the custom Option component
117
+ components={{ Option: CustomOption }}
107
118
  options={selectOptions}
108
119
  onChange={(inputValue, action) => {
109
120
  onChange(inputValue, action, settings.id);
@@ -125,8 +136,8 @@ function MultiSelect({
125
136
  }),
126
137
  }}
127
138
  value={selectValue}
128
- onCreateOption={isCreatable ? handleCreate : undefined} // Only add if creatable
129
- placeholder={placeholder ? placeholder : 'Select...'}
139
+ onCreateOption={isCreatable ? handleCreate : undefined}
140
+ placeholder={placeholder || 'Select...'}
130
141
  />
131
142
  );
132
143
  }
@@ -28,7 +28,15 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
28
28
  let content =
29
29
  providers && providers.length > 0 ? (
30
30
  <div className="formItem fwItem lastItem forgotten">
31
- <span>OR</span>
31
+ <span
32
+ style={{
33
+ display: 'block',
34
+ textAlign: 'center',
35
+ width: '100%',
36
+ }}
37
+ >
38
+ OR
39
+ </span>
32
40
  </div>
33
41
  ) : null;
34
42
 
@@ -1,6 +1,6 @@
1
1
  import '../../styles/global.css';
2
2
 
3
- import React, { useCallback, useEffect, useRef, useState } from 'react';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
4
  import { Link, useParams } from 'react-router-dom';
5
5
  import moment from 'moment';
6
6
  import parse from 'html-react-parser';
@@ -28,8 +28,8 @@ import Breadcrumb from '../Breadcrumb';
28
28
  import CustomFetch from '../Fetch';
29
29
  import Form from '../Form';
30
30
  import GenericDynamic from './GenericDynamic';
31
+ import GenericEditableTable from './GenericEditableTable';
31
32
  import QrCode from '../QrCode';
32
- import SortableList from '../sorting/List';
33
33
  import Table from '../DataGrid';
34
34
  import TableFilter from '../TableFilter';
35
35
 
@@ -55,9 +55,11 @@ function GenericDetail({
55
55
 
56
56
  /** General States */
57
57
  const [activeStage, setActiveStage] = useState('');
58
+ const [activeTabConfig, setActiveTabConfig] = useState(null);
58
59
  const [config, setConfig] = useState({});
59
60
  const [data, setData] = useState({});
60
61
  const [dataReload, setDataReload] = useState(0);
62
+ const [preloadedOptions, setPreloadedOptions] = useState({});
61
63
  const [rowsSelected, setRowsSelected] = useState({});
62
64
  const [subnav, setSubnav] = useState([]);
63
65
  const [total, setTotal] = useState(0);
@@ -1321,9 +1323,15 @@ function GenericDetail({
1321
1323
  return null;
1322
1324
  }
1323
1325
  case 'scheduling':
1324
- return renderSchedulingTable(
1325
- activeTabConfig,
1326
- routeParams[urlParam]
1326
+ return (
1327
+ <GenericEditableTable
1328
+ schedulingConfig={activeTabConfig}
1329
+ data={data}
1330
+ dataId={routeParams[urlParam]}
1331
+ handleReload={handleReload}
1332
+ preloadedOptions={preloadedOptions}
1333
+ userProfile={userProfile}
1334
+ />
1327
1335
  );
1328
1336
  default:
1329
1337
  return null;
@@ -1333,298 +1341,50 @@ function GenericDetail({
1333
1341
  }
1334
1342
  };
1335
1343
 
1336
- const renderSchedulingTable = (config, dataId) => {
1337
- const { columns, rows } = config;
1338
- const scheduleData = data[rows.key]; // Access schedule data directly from the rows
1339
-
1340
- // Group categories by their type (e.g., Concrete Work, Design Services)
1341
- const groupedCategories = groupCategoriesByType(scheduleData, rows);
1342
-
1343
- // Count the number of total columns
1344
- const totalColumns = columns.filter((column) => column.total).length;
1345
-
1346
- return (
1347
- <div className={styles.gridtxt}>
1348
- <div className={styles.gridtxt__header}>
1349
- <span>{config.title || 'Estimation Schedule'}</span>
1350
- </div>
1344
+ /** General Hooks */
1345
+ useEffect(() => {
1346
+ if (tabs && tabs.length > 0) {
1347
+ const activeTab = subnav.find((s) => s.show === true);
1351
1348
 
1352
- {groupedCategories.length > 0 ? (
1353
- <table className={styles.schedulingTable}>
1354
- <thead>
1355
- <tr>
1356
- {columns.map((column) => (
1357
- <th
1358
- key={column.id}
1359
- style={{
1360
- width: column.width || 'auto',
1361
- textAlign:
1362
- column.type === 'currency'
1363
- ? 'right'
1364
- : 'left',
1365
- }}
1366
- >
1367
- {column.label}
1368
- </th>
1369
- ))}
1370
- <th style={{ width: '5%' }}></th>
1371
- </tr>
1372
- </thead>
1373
- <tbody>
1374
- {groupedCategories.map((group) => (
1375
- <React.Fragment key={group.category}>
1376
- {/* Category Row */}
1377
- <tr className={styles.categoryRow}>
1378
- <td colSpan={columns.length + 1}>
1379
- <strong>{group.category}</strong>
1380
- </td>
1381
- </tr>
1382
-
1383
- {/* Data Rows */}
1384
- {group.entries.map((entry, idx) => (
1385
- <tr
1386
- key={`row-${group.category}-${idx}`}
1387
- >
1388
- {columns.map((column) => (
1389
- <td
1390
- key={`column-${column.id}-${idx}`}
1391
- style={{
1392
- textAlign:
1393
- column.type ===
1394
- 'currency'
1395
- ? 'right'
1396
- : 'left',
1397
- }}
1398
- >
1399
- {column.type === 'currency'
1400
- ? formatCurrency(
1401
- entry[column.id]
1402
- )
1403
- : entry[column.id]
1404
- ?.label ||
1405
- entry[column.id] ||
1406
- ''}
1407
- </td>
1408
- ))}
1409
- <td>
1410
- {/* Edit Icon */}
1411
- <span
1412
- data-tooltip-id="system-tooltip"
1413
- data-tooltip-content="Edit"
1414
- style={{
1415
- cursor: 'pointer',
1416
- marginRight: '10px',
1417
- }}
1418
- onClick={() =>
1419
- modalOpen(
1420
- 'update',
1421
- entry.id
1422
- )
1423
- }
1424
- >
1425
- <Pencil
1426
- size={18}
1427
- strokeWidth={2}
1428
- />
1429
- </span>
1430
-
1431
- {/* Delete Icon */}
1432
- <span
1433
- data-tooltip-id="system-tooltip"
1434
- data-tooltip-content="Delete"
1435
- style={{
1436
- cursor: 'pointer',
1437
- }}
1438
- onClick={() =>
1439
- handleDeleteRow(
1440
- entry,
1441
- config,
1442
- dataId
1443
- )
1444
- }
1445
- >
1446
- <TrashCan
1447
- size={18}
1448
- strokeWidth={2}
1449
- />
1450
- </span>
1451
- </td>
1452
- </tr>
1453
- ))}
1454
-
1455
- {/* Total Cost Row */}
1456
- <tr className={styles.totalRow}>
1457
- <td
1458
- colSpan={
1459
- columns.length - totalColumns
1460
- }
1461
- >
1462
- <strong>Total Cost</strong>
1463
- </td>
1464
- {columns.map((c) =>
1465
- c.total ? (
1466
- <td
1467
- key={c.id}
1468
- style={{
1469
- textAlign: 'right',
1470
- }}
1471
- >
1472
- {calculateTotalCost(
1473
- group.entries,
1474
- c.id
1475
- )}
1476
- </td>
1477
- ) : null
1478
- )}
1479
- <td></td>
1480
- </tr>
1481
- </React.Fragment>
1482
- ))}
1483
- </tbody>
1484
- </table>
1485
- ) : (
1486
- <div className={styles.noDataMessage}>
1487
- <p>No scheduling data has been added.</p>
1488
- </div>
1489
- )}
1490
-
1491
- {config.form && (
1492
- <div className={styles.polActions}>
1493
- <button
1494
- className={styles.btn}
1495
- onClick={() => modalOpen('create', 0)}
1496
- >
1497
- Add
1498
- </button>
1499
- </div>
1500
- )}
1501
- </div>
1502
- );
1503
- };
1349
+ if (activeTab) {
1350
+ const newActiveTabConfig = tabs.find(
1351
+ (t) => t.id === activeTab.id
1352
+ );
1353
+ setActiveTabConfig(newActiveTabConfig); // Update state
1354
+ }
1355
+ }
1356
+ }, [subnav, tabs]);
1504
1357
 
1505
- const handleDeleteRow = (d, c, dataId) => {
1506
- confirmAlert({
1507
- title: 'Delete Item',
1508
- message:
1509
- d.description && d.description !== ''
1510
- ? d.description
1511
- : `Are you sure you want to delete ${
1512
- d.name ? d.name : d.label ? d.label : 'this item'
1513
- }?`,
1514
- buttons: [
1515
- {
1516
- label: 'Yes',
1517
- onClick: () => {
1518
- let deleteForm = {
1519
- url: '',
1520
- method: 'DELETE',
1521
- payload: {},
1522
- };
1523
-
1524
- deleteForm.url = `${c.form.url}/json/delete`;
1525
- deleteForm.method = 'POST';
1526
- deleteForm.payload = {
1527
- id: d[c.form.primaryKey],
1528
- dataId: dataId,
1529
- key: c.form.jsonKey || '',
1530
- };
1531
-
1532
- CustomFetch(
1533
- deleteForm.url,
1534
- deleteForm.method,
1535
- deleteForm.payload,
1536
- (result) => {
1537
- if (result.error === '') {
1538
- handleReload();
1539
-
1540
- toast.success(
1541
- `The selected item has been deleted.`
1542
- );
1543
- } else {
1544
- toast.error(
1545
- <div>{parse(result.error)}</div>
1546
- );
1547
- }
1548
- }
1358
+ useEffect(() => {
1359
+ const fetchDropdownOptions = async () => {
1360
+ if (activeTabConfig?.columns) {
1361
+ const ajaxColumns = activeTabConfig.columns.filter(
1362
+ (column) =>
1363
+ ['dropdown-ajax', 'multi-dropdown-ajax'].includes(
1364
+ column.type
1365
+ ) && column.url
1366
+ );
1367
+
1368
+ const options = {};
1369
+ for (const column of ajaxColumns) {
1370
+ try {
1371
+ const res = await CustomFetch(column.url, 'POST');
1372
+ options[column.id] = res.data.data || res.data || []; // Assume API returns options in the expected format
1373
+ } catch (error) {
1374
+ console.error(
1375
+ `Error fetching options for ${column.id}:`,
1376
+ error
1549
1377
  );
1550
- },
1551
- },
1552
- {
1553
- label: 'No',
1554
- onClick: () => {},
1555
- },
1556
- ],
1557
- });
1558
- };
1559
-
1560
- // Function to group categories by their type (e.g., Concrete Work, Miscellaneous, etc.)
1561
- const groupCategoriesByType = (data, rows) => {
1562
- const grouped = {};
1563
-
1564
- if (data) {
1565
- data.forEach((category) => {
1566
- const categoryType =
1567
- category[rows.categoryKey.id]?.[rows.categoryKey.label] ||
1568
- 'Miscellaneous'; // Default to 'Miscellaneous' if no category is found
1569
-
1570
- const categoryId =
1571
- category[rows.categoryKey.id]?.id ||
1572
- Number.MAX_SAFE_INTEGER; // Default to max integer if no id is found
1573
-
1574
- if (!grouped[categoryId]) {
1575
- grouped[categoryId] = {
1576
- id: categoryId,
1577
- category: categoryType,
1578
- entries: [],
1579
- };
1378
+ }
1580
1379
  }
1581
- grouped[categoryId].entries.push(category);
1582
- });
1583
- }
1584
-
1585
- return Object.values(grouped).sort((a, b) => a.id - b.id); // Sort categories by id
1586
- };
1587
1380
 
1588
- // Function to calculate the total cost for a group
1589
- const calculateTotalCost = (entries, id) => {
1590
- if (!entries || entries.length === 0 || !id) {
1591
- return '$0.00'; // Return $0.00 if entries are empty or id is not provided
1592
- }
1593
-
1594
- let total = 0;
1595
-
1596
- entries.forEach((entry) => {
1597
- if (entry && entry[id]) {
1598
- // Check if the entry and the specific field exist
1599
- // Parse the cost from the field and remove non-numeric characters
1600
- const cost =
1601
- parseFloat(
1602
- entry[id].toString().replace(/[^0-9.-]+/g, '')
1603
- ) || 0;
1604
- total += cost;
1381
+ setPreloadedOptions(options);
1605
1382
  }
1606
- });
1607
-
1608
- // Format the total cost with commas and return it
1609
- return `$${total.toLocaleString('en-AU', {
1610
- minimumFractionDigits: 2,
1611
- maximumFractionDigits: 2,
1612
- })}`;
1613
- };
1383
+ };
1614
1384
 
1615
- // Function to format currency values with commas and a currency symbol
1616
- const formatCurrency = (value) => {
1617
- if (value) {
1618
- const cost = parseFloat(value.replace(/[^0-9.-]+/g, '')) || 0; // Remove any non-numeric characters
1619
- return `$${cost.toLocaleString('en-AU', {
1620
- minimumFractionDigits: 2,
1621
- maximumFractionDigits: 2,
1622
- })}`;
1623
- }
1624
- return ''; // Return empty if value is not provided
1625
- };
1385
+ fetchDropdownOptions();
1386
+ }, [activeTabConfig]); // Depend on activeTabConfig
1626
1387
 
1627
- /** General Hooks */
1628
1388
  useEffect(() => {
1629
1389
  const handleResize = () => {
1630
1390
  setWindowHeight(window.innerHeight);
@@ -0,0 +1,341 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import debounce from 'lodash.debounce';
3
+ import { toast } from 'react-toastify';
4
+ import Popup from 'reactjs-popup';
5
+ import { confirmAlert } from 'react-confirm-alert';
6
+ import { TrashCan, ArrowUp, ArrowDown } from 'akar-icons';
7
+
8
+ import CustomFetch from '../Fetch';
9
+ import Form from '../Form';
10
+ import MultiSelect from '../MultiSelect';
11
+
12
+ import 'react-confirm-alert/src/react-confirm-alert.css';
13
+ import styles from './styles/GenericEditableTable.module.scss';
14
+
15
+ const GenericEditableTable = ({
16
+ schedulingConfig,
17
+ data,
18
+ dataId,
19
+ handleReload,
20
+ preloadedOptions,
21
+ userProfile,
22
+ }) => {
23
+ const { columns, rows, form } = schedulingConfig;
24
+
25
+ const [localData, setLocalData] = useState([]);
26
+ const [groupedCategories, setGroupedCategories] = useState([]);
27
+
28
+ useEffect(() => {
29
+ const enrichedData = data[rows.key]?.map((entry, index) => ({
30
+ ...entry,
31
+ sort_order: entry.sort_order ?? index + 1,
32
+ }));
33
+ setLocalData(enrichedData || []);
34
+ }, [data, rows.key]);
35
+
36
+ const debouncedUpdate = useMemo(
37
+ () =>
38
+ debounce(async (updatedData) => {
39
+ try {
40
+ const res = await CustomFetch(
41
+ `${form.url}/${dataId}`,
42
+ 'PUT',
43
+ updatedData
44
+ );
45
+ if (!res.error) {
46
+ toast.success('Template updated successfully');
47
+ } else {
48
+ toast.error(res.error);
49
+ }
50
+ } catch (error) {
51
+ console.error('Error updating field:', error);
52
+ }
53
+ }, 1000),
54
+ []
55
+ );
56
+
57
+ const groupCategoriesByType = (data) => {
58
+ const grouped = {};
59
+ data.forEach((category) => {
60
+ const categoryType =
61
+ category[rows.categoryKey.id]?.[rows.categoryKey.label] ||
62
+ 'Miscellaneous';
63
+
64
+ const categoryId =
65
+ category[rows.categoryKey.id]?.id || Number.MAX_SAFE_INTEGER;
66
+
67
+ if (!grouped[categoryId]) {
68
+ grouped[categoryId] = {
69
+ id: categoryId,
70
+ category: categoryType,
71
+ entries: [],
72
+ };
73
+ }
74
+ grouped[categoryId].entries.push(category);
75
+ });
76
+
77
+ Object.values(grouped).forEach((group) => {
78
+ group.entries.sort((a, b) => a.sort_order - b.sort_order);
79
+ });
80
+
81
+ return Object.values(grouped);
82
+ };
83
+
84
+ const handleFieldChange = (value, fieldId, keyCounter) => {
85
+ setLocalData((prev) => {
86
+ const updatedDetail = [...prev];
87
+ updatedDetail[keyCounter] = {
88
+ ...updatedDetail[keyCounter],
89
+ [fieldId]: value,
90
+ };
91
+
92
+ debouncedUpdate({ detail: updatedDetail });
93
+
94
+ return updatedDetail;
95
+ });
96
+ };
97
+
98
+ const handleSortChange = (groupId, entryId, direction) => {
99
+ setLocalData((prev) => {
100
+ const updatedDetail = [...prev];
101
+ const group = groupCategoriesByType(updatedDetail).find(
102
+ (group) => group.id === groupId
103
+ );
104
+
105
+ if (group) {
106
+ const index = group.entries.findIndex(
107
+ (entry) => entry.id === entryId
108
+ );
109
+
110
+ if (
111
+ (direction === 'up' && index > 0) ||
112
+ (direction === 'down' && index < group.entries.length - 1)
113
+ ) {
114
+ const swapIndex =
115
+ direction === 'up' ? index - 1 : index + 1;
116
+ [
117
+ group.entries[index].sort_order,
118
+ group.entries[swapIndex].sort_order,
119
+ ] = [
120
+ group.entries[swapIndex].sort_order,
121
+ group.entries[index].sort_order,
122
+ ];
123
+
124
+ const reordered = updatedDetail.map((item) => {
125
+ const entry = group.entries.find(
126
+ (e) => e.id === item.id
127
+ );
128
+ return entry
129
+ ? { ...item, sort_order: entry.sort_order }
130
+ : item;
131
+ });
132
+
133
+ debouncedUpdate({ detail: reordered });
134
+ return reordered;
135
+ }
136
+ }
137
+
138
+ return updatedDetail;
139
+ });
140
+ };
141
+
142
+ const renderScheduledTableField = (entry, column, keyCounter) => {
143
+ if (!entry || !column) return null;
144
+
145
+ switch (column.type) {
146
+ case 'currency':
147
+ case 'number':
148
+ return (
149
+ <input
150
+ type="text"
151
+ value={entry[column.id] || ''}
152
+ onChange={(e) =>
153
+ handleFieldChange(
154
+ e.target.value,
155
+ column.id,
156
+ keyCounter
157
+ )
158
+ }
159
+ style={{ textAlign: 'right' }}
160
+ />
161
+ );
162
+ case 'dropdown':
163
+ return (
164
+ <select
165
+ value={entry[column.id] || ''}
166
+ onChange={(e) =>
167
+ handleFieldChange(
168
+ e.target.value,
169
+ column.id,
170
+ keyCounter
171
+ )
172
+ }
173
+ >
174
+ {column.options.map((option) => (
175
+ <option key={option.id} value={option.id}>
176
+ {option.label}
177
+ </option>
178
+ ))}
179
+ </select>
180
+ );
181
+ case 'dropdown-ajax':
182
+ const options = preloadedOptions[column.id] || [];
183
+ return (
184
+ <select
185
+ value={entry[column.id] || ''}
186
+ onChange={(e) =>
187
+ handleFieldChange(
188
+ e.target.value,
189
+ column.id,
190
+ keyCounter
191
+ )
192
+ }
193
+ >
194
+ <option>Select an Option</option>
195
+ {options.map((option) => (
196
+ <option key={option.id} value={option.id}>
197
+ {option.label}
198
+ </option>
199
+ ))}
200
+ </select>
201
+ );
202
+ case 'multi-dropdown-ajax':
203
+ const multiOptions = preloadedOptions[column.id] || [];
204
+ return (
205
+ <MultiSelect
206
+ settings={{
207
+ id: column.id,
208
+ }}
209
+ className={styles.selectFullWidth}
210
+ multi={false}
211
+ onChange={(value) =>
212
+ handleFieldChange(value, column.id, keyCounter)
213
+ }
214
+ inputValue={entry[column.id] || ''}
215
+ options={multiOptions}
216
+ />
217
+ );
218
+ default:
219
+ return (
220
+ <input
221
+ type="text"
222
+ defaultValue={entry[column.id] || ''}
223
+ onChange={(e) =>
224
+ handleFieldChange(
225
+ e.target.value,
226
+ column.id,
227
+ keyCounter
228
+ )
229
+ }
230
+ />
231
+ );
232
+ }
233
+ };
234
+
235
+ useEffect(() => {
236
+ setGroupedCategories(groupCategoriesByType(localData));
237
+ }, [localData]);
238
+
239
+ return (
240
+ <div className={styles.gridtxt}>
241
+ <div className={styles.gridtxt__header}>
242
+ <span>{schedulingConfig.title || 'Estimation Schedule'}</span>
243
+ </div>
244
+ {groupedCategories.length > 0 ? (
245
+ <table className={styles.schedulingTable}>
246
+ <thead>
247
+ <tr>
248
+ {columns.map((column) => (
249
+ <th
250
+ key={column.id}
251
+ style={{ width: column.width || 'auto' }}
252
+ >
253
+ {column.label}
254
+ </th>
255
+ ))}
256
+ <th style={{ width: '5%' }}></th>
257
+ </tr>
258
+ </thead>
259
+ <tbody>
260
+ {groupedCategories.map((group) => (
261
+ <React.Fragment key={group.id}>
262
+ <tr className={styles.categoryRow}>
263
+ <td colSpan={columns.length + 1}>
264
+ {group.category}
265
+ </td>
266
+ </tr>
267
+ {group.entries.map((entry, idx) => (
268
+ <tr key={entry.id}>
269
+ {columns.map((column) => (
270
+ <td
271
+ key={column.id}
272
+ style={{
273
+ width:
274
+ column.width || 'auto',
275
+ textAlign:
276
+ column.type ===
277
+ 'currency'
278
+ ? 'right'
279
+ : 'left',
280
+ }}
281
+ >
282
+ {renderScheduledTableField(
283
+ entry,
284
+ column,
285
+ entry.keyCounter
286
+ )}
287
+ </td>
288
+ ))}
289
+ <td>
290
+ {idx > 0 && (
291
+ <ArrowUp
292
+ size={18}
293
+ onClick={() =>
294
+ handleSortChange(
295
+ group.id,
296
+ entry.id,
297
+ 'up'
298
+ )
299
+ }
300
+ style={{
301
+ cursor: 'pointer',
302
+ }}
303
+ />
304
+ )}
305
+ {idx < group.entries.length - 1 && (
306
+ <ArrowDown
307
+ size={18}
308
+ onClick={() =>
309
+ handleSortChange(
310
+ group.id,
311
+ entry.id,
312
+ 'down'
313
+ )
314
+ }
315
+ style={{
316
+ cursor: 'pointer',
317
+ }}
318
+ />
319
+ )}
320
+ <TrashCan
321
+ size={18}
322
+ onClick={() =>
323
+ handleDeleteRow(entry)
324
+ }
325
+ style={{ cursor: 'pointer' }}
326
+ />
327
+ </td>
328
+ </tr>
329
+ ))}
330
+ </React.Fragment>
331
+ ))}
332
+ </tbody>
333
+ </table>
334
+ ) : (
335
+ <div>No data available.</div>
336
+ )}
337
+ </div>
338
+ );
339
+ };
340
+
341
+ export default GenericEditableTable;
@@ -106,5 +106,9 @@
106
106
  background: rgba(#f4f4f4, 0.65);
107
107
  margin: 5px;
108
108
  border-radius: 5px;
109
+
110
+ p {
111
+ font-size: 0.85rem;
112
+ }
109
113
  }
110
114
  }
@@ -233,6 +233,10 @@
233
233
  background: rgba(#f4f4f4, 0.65);
234
234
  margin: 5px;
235
235
  border-radius: 5px;
236
+
237
+ p {
238
+ font-size: 0.85rem;
239
+ }
236
240
  }
237
241
  }
238
242
 
@@ -277,6 +277,10 @@
277
277
  border: 1px solid rgba(var(--highlight-color-rgb), 0.85);
278
278
  }
279
279
 
280
+ .formItem textarea {
281
+ line-height: 1.65 !important;
282
+ }
283
+
280
284
  input[type='file'] {
281
285
  background: var(--item-color) !important;
282
286
  border-radius: var (--br) !important;
@@ -0,0 +1,136 @@
1
+ .schedulingTable {
2
+ width: 100%;
3
+ border-collapse: collapse;
4
+ margin: 1em 0;
5
+
6
+ th,
7
+ td {
8
+ padding: 0.75rem;
9
+ text-align: left;
10
+ border: 1px solid rgba(var(--primary-rgb), 0.1);
11
+ }
12
+
13
+ th {
14
+ background-color: var(--secondary-color);
15
+ color: var(--item-color);
16
+ font-weight: bold;
17
+ }
18
+
19
+ td {
20
+ background-color: rgba(var(--primary-rgb), 0.05);
21
+ }
22
+ }
23
+
24
+ .categoryRow {
25
+ background-color: rgba(var(--primary-rgb), 0.1);
26
+ text-align: left;
27
+ font-weight: bold;
28
+ font-size: 1.2rem;
29
+ color: var(--primary-color);
30
+ }
31
+
32
+ .totalRow {
33
+ background-color: rgba(var(--primary-rgb), 0.05);
34
+ font-weight: bold;
35
+ text-align: right;
36
+ }
37
+
38
+ .noDataMessage {
39
+ text-align: center;
40
+ color: var(--secondary-color);
41
+ font-size: 1.125em;
42
+ padding: 1.5em;
43
+ margin: 2em auto;
44
+ background: #f9f9f9;
45
+ border: 1px solid #ddd;
46
+ border-radius: 6px;
47
+ max-width: 600px;
48
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
49
+ font-style: italic;
50
+ }
51
+
52
+ .polActions {
53
+ position: fixed;
54
+ bottom: 15px;
55
+ right: 20px;
56
+ width: auto;
57
+
58
+ button {
59
+ display: block;
60
+ padding: 0.35em 1em;
61
+ margin: 0 0.15em;
62
+ }
63
+ }
64
+
65
+ .gridtxt {
66
+ &__header {
67
+ width: 100%;
68
+ display: block;
69
+ background: rgba(var(--primary-rgb), 0.05);
70
+ box-sizing: border-box;
71
+ padding: 10px 20px;
72
+ border-radius: var(--br);
73
+ margin-bottom: 0;
74
+ font-weight: 700;
75
+ text-align: center;
76
+ color: var(--paragraph-color);
77
+
78
+ span {
79
+ font-weight: 700;
80
+ }
81
+ }
82
+
83
+ > ul {
84
+ width: 100%;
85
+ display: flex;
86
+ justify-content: flex-start;
87
+ flex-wrap: wrap;
88
+ list-style: none;
89
+ padding: 0.85rem;
90
+ margin: 0;
91
+
92
+ li {
93
+ flex: 0 0 calc(50% - 10px);
94
+ padding: 0.65rem;
95
+ border: 1px solid rgba(var(--primary-rgb), 0.095);
96
+ color: var(--paragraph-color);
97
+ background: rgba(var(--primary-rgb), 0.015);
98
+ margin: 3px;
99
+ border-radius: var(--br);
100
+ line-height: 1.45;
101
+ font-size: 0.85rem;
102
+
103
+ &.fw-grid-item {
104
+ flex: 0 0 calc(100% - 10px);
105
+ }
106
+
107
+ &.notecolor {
108
+ border: 1px solid var(--secondary-color);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ .btn {
115
+ width: max-content;
116
+ display: inline-block;
117
+ position: relative;
118
+ padding: 0.65rem 1rem;
119
+ cursor: pointer;
120
+ font-size: 1rem;
121
+ color: var(--tertiary-color);
122
+ text-decoration: none;
123
+ overflow: hidden;
124
+ background: var(--primary-color);
125
+ border: 1px solid rgba(var(--primary-color--rgb), 1.1);
126
+ border-radius: var(--br);
127
+ outline: none;
128
+ transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
129
+ font-size: 1.25em;
130
+ }
131
+
132
+ .btn:hover {
133
+ color: var(--primary-color);
134
+ background: var(--highlight-color);
135
+ border: 1px solid rgba(var(--highlight-rgb), 1.05);
136
+ }
@@ -270,6 +270,10 @@
270
270
  border: 1px solid rgba(var(--primary-color-rgb), 0.85);
271
271
  }
272
272
 
273
+ .formItem textarea {
274
+ line-height: 1.65 !important;
275
+ }
276
+
273
277
  input[type='file'] {
274
278
  background: var(--item-color) !important;
275
279
  border-radius: var (--br) !important;
@@ -144,7 +144,7 @@ select:not(:placeholder-shown) + .fi__span {
144
144
  background: var(--tertiary-color) !important;
145
145
  font-weight: 400 !important;
146
146
  transition: all 0.5s cubic-bezier(0.5, 0.5, 0, 1) !important;
147
- z-index: 999 !important;
147
+ z-index: 1 !important;
148
148
  color: rgba(var(--paragraph-rgb), 0.65) !important;
149
149
  }
150
150
 
@@ -221,6 +221,10 @@ select:not(:placeholder-shown) + .fi__span {
221
221
  border: 1px solid rgba(var(--paragraph-color-rgb), 0.85);
222
222
  }
223
223
 
224
+ .formItem textarea {
225
+ line-height: 1.65 !important;
226
+ }
227
+
224
228
  input[type='file'] {
225
229
  background: white !important;
226
230
  border-radius: var(--br) !important;
@@ -92,13 +92,13 @@
92
92
  &.active a,
93
93
  &:hover span,
94
94
  &.active span {
95
- background: rgba(var(--secondary-rgb), 1);
96
- color: var(--tertiary-color);
95
+ background: rgba(var(--nav-active-rgb), 1);
96
+ color: var(--highlight-color);
97
97
  }
98
98
 
99
99
  &:hover svg,
100
100
  &.active svg {
101
- color: var(--tertiary-color);
101
+ color: var(--highlight-color);
102
102
  }
103
103
 
104
104
  > ul {
@@ -69,6 +69,10 @@
69
69
  background: rgba(#f4f4f4, 0.65);
70
70
  margin: 5px;
71
71
  border-radius: 5px;
72
+
73
+ p {
74
+ font-size: 0.85rem;
75
+ }
72
76
  }
73
77
  }
74
78
 
@@ -10,7 +10,9 @@
10
10
 
11
11
  html,
12
12
  body {
13
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
14
+ Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
15
+ 'Segoe UI Symbol';
14
16
  height: 100%;
15
17
  text-rendering: geometricPrecision;
16
18
  font-stretch: 75% 125%;
@@ -19,7 +21,9 @@ body {
19
21
  body {
20
22
  background: var(--bg-color);
21
23
  font-weight: var(--para-weight);
22
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
25
+ Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
26
+ 'Segoe UI Symbol';
23
27
  font-style: normal;
24
28
  overflow-x: hidden;
25
29
  overscroll-behavior: none;
@@ -113,6 +117,11 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
113
117
  padding: 0 !important;
114
118
  }
115
119
 
120
+ .popup-overlay {
121
+ background: rgba(0, 0, 0, 0.65) !important;
122
+ backdrop-filter: blur(3px);
123
+ }
124
+
116
125
  .AutocompletePlace-results {
117
126
  z-index: 999;
118
127
  width: 500px;
@@ -191,4 +200,4 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
191
200
 
192
201
  .visns-select__menu {
193
202
  z-index: 999 !important;
194
- }
203
+ }