@visns-studio/visns-components 5.0.12 → 5.0.14
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 +1 -1
- package/src/components/crm/Field.jsx +7 -0
- package/src/components/crm/Form.jsx +8 -5
- package/src/components/crm/MultiSelect.jsx +43 -32
- package/src/components/crm/auth/Login.jsx +9 -1
- package/src/components/crm/generic/GenericDetail.jsx +48 -289
- package/src/components/crm/generic/GenericEditableTable.jsx +407 -0
- package/src/components/crm/generic/styles/GenericEditableTable.module.scss +178 -0
- package/src/components/crm/styles/Navigation.module.scss +3 -3
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.
|
|
78
|
+
"version": "5.0.14",
|
|
79
79
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
80
80
|
"main": "src/index.js",
|
|
81
81
|
"files": [
|
|
@@ -82,6 +82,7 @@ function Field({
|
|
|
82
82
|
useEffect(() => {
|
|
83
83
|
const fetchOptions = () => {
|
|
84
84
|
let filter = {};
|
|
85
|
+
|
|
85
86
|
if (settings.where?.length > 0) {
|
|
86
87
|
filter = {
|
|
87
88
|
where: settings.where,
|
|
@@ -207,6 +208,12 @@ function Field({
|
|
|
207
208
|
filter.orderBy = s.orderBy;
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
if (a.where) {
|
|
212
|
+
forEach(a.where, (w) => {
|
|
213
|
+
filter.where.push(w);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
210
217
|
CustomFetch(s.child.url, 'POST', filter, function (result) {
|
|
211
218
|
childDropdownCallback({
|
|
212
219
|
id: s.child.id,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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',
|
|
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 }}
|
|
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}
|
|
129
|
-
placeholder={placeholder
|
|
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
|
|
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, {
|
|
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,14 @@ function GenericDetail({
|
|
|
1321
1323
|
return null;
|
|
1322
1324
|
}
|
|
1323
1325
|
case 'scheduling':
|
|
1324
|
-
return
|
|
1325
|
-
|
|
1326
|
-
|
|
1326
|
+
return (
|
|
1327
|
+
<GenericEditableTable
|
|
1328
|
+
schedulingConfig={activeTabConfig}
|
|
1329
|
+
data={data}
|
|
1330
|
+
dataId={routeParams[urlParam]}
|
|
1331
|
+
modalOpen={modalOpen}
|
|
1332
|
+
preloadedOptions={preloadedOptions}
|
|
1333
|
+
/>
|
|
1327
1334
|
);
|
|
1328
1335
|
default:
|
|
1329
1336
|
return null;
|
|
@@ -1333,298 +1340,50 @@ function GenericDetail({
|
|
|
1333
1340
|
}
|
|
1334
1341
|
};
|
|
1335
1342
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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>
|
|
1343
|
+
/** General Hooks */
|
|
1344
|
+
useEffect(() => {
|
|
1345
|
+
if (tabs && tabs.length > 0) {
|
|
1346
|
+
const activeTab = subnav.find((s) => s.show === true);
|
|
1351
1347
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
};
|
|
1348
|
+
if (activeTab) {
|
|
1349
|
+
const newActiveTabConfig = tabs.find(
|
|
1350
|
+
(t) => t.id === activeTab.id
|
|
1351
|
+
);
|
|
1352
|
+
setActiveTabConfig(newActiveTabConfig); // Update state
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}, [subnav, tabs]);
|
|
1504
1356
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
{
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
-
}
|
|
1357
|
+
useEffect(() => {
|
|
1358
|
+
const fetchDropdownOptions = async () => {
|
|
1359
|
+
if (activeTabConfig?.columns) {
|
|
1360
|
+
const ajaxColumns = activeTabConfig.columns.filter(
|
|
1361
|
+
(column) =>
|
|
1362
|
+
['dropdown-ajax', 'multi-dropdown-ajax'].includes(
|
|
1363
|
+
column.type
|
|
1364
|
+
) && column.url
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
const options = {};
|
|
1368
|
+
for (const column of ajaxColumns) {
|
|
1369
|
+
try {
|
|
1370
|
+
const res = await CustomFetch(column.url, 'POST');
|
|
1371
|
+
options[column.id] = res.data.data || res.data || []; // Assume API returns options in the expected format
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
console.error(
|
|
1374
|
+
`Error fetching options for ${column.id}:`,
|
|
1375
|
+
error
|
|
1549
1376
|
);
|
|
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
|
-
};
|
|
1377
|
+
}
|
|
1580
1378
|
}
|
|
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
1379
|
|
|
1588
|
-
|
|
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;
|
|
1380
|
+
setPreloadedOptions(options);
|
|
1605
1381
|
}
|
|
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
|
-
};
|
|
1382
|
+
};
|
|
1614
1383
|
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
};
|
|
1384
|
+
fetchDropdownOptions();
|
|
1385
|
+
}, [activeTabConfig]); // Depend on activeTabConfig
|
|
1626
1386
|
|
|
1627
|
-
/** General Hooks */
|
|
1628
1387
|
useEffect(() => {
|
|
1629
1388
|
const handleResize = () => {
|
|
1630
1389
|
setWindowHeight(window.innerHeight);
|
|
@@ -0,0 +1,407 @@
|
|
|
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
|
+
modalOpen,
|
|
20
|
+
preloadedOptions,
|
|
21
|
+
}) => {
|
|
22
|
+
const { columns, rows, form } = schedulingConfig;
|
|
23
|
+
|
|
24
|
+
const [localData, setLocalData] = useState([]);
|
|
25
|
+
const [groupedCategories, setGroupedCategories] = useState([]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const enrichedData = data[rows.key]?.map((entry, index) => ({
|
|
29
|
+
...entry,
|
|
30
|
+
sort_order: entry.sort_order ?? index + 1,
|
|
31
|
+
}));
|
|
32
|
+
setLocalData(enrichedData || []);
|
|
33
|
+
}, [data, rows.key]);
|
|
34
|
+
|
|
35
|
+
const debouncedUpdate = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
debounce(async (updatedData) => {
|
|
38
|
+
try {
|
|
39
|
+
const res = await CustomFetch(
|
|
40
|
+
`${form.url}/${dataId}`,
|
|
41
|
+
'PUT',
|
|
42
|
+
updatedData
|
|
43
|
+
);
|
|
44
|
+
if (!res.error) {
|
|
45
|
+
toast.success('Template updated successfully');
|
|
46
|
+
} else {
|
|
47
|
+
toast.error(res.error);
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error updating field:', error);
|
|
51
|
+
}
|
|
52
|
+
}, 1000),
|
|
53
|
+
[]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const groupCategoriesByType = (data) => {
|
|
57
|
+
const grouped = {};
|
|
58
|
+
|
|
59
|
+
data.forEach((category, categoryKey) => {
|
|
60
|
+
const categoryType =
|
|
61
|
+
category[rows.categoryKey.id]?.[rows.categoryKey.label] || null;
|
|
62
|
+
|
|
63
|
+
const categoryId = category[rows.categoryKey.id]?.id || null;
|
|
64
|
+
|
|
65
|
+
const groupKey = categoryId || 'no_category';
|
|
66
|
+
|
|
67
|
+
if (!grouped[groupKey]) {
|
|
68
|
+
grouped[groupKey] = {
|
|
69
|
+
id: categoryId || 'no_category',
|
|
70
|
+
category: categoryType || '',
|
|
71
|
+
entries: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
grouped[groupKey].entries.push({
|
|
75
|
+
...category,
|
|
76
|
+
keyCounter: categoryKey,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
Object.values(grouped).forEach((group) => {
|
|
81
|
+
group.entries.sort((a, b) => a.sort_order - b.sort_order);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Ensure 'no_category' group is at the top
|
|
85
|
+
const groupedArray = Object.values(grouped);
|
|
86
|
+
return groupedArray.sort((a, b) =>
|
|
87
|
+
a.id === 'no_category' ? -1 : b.id === 'no_category' ? 1 : 0
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleFieldChange = (value, fieldId, keyCounter) => {
|
|
92
|
+
setLocalData((prev) => {
|
|
93
|
+
const updatedDetail = [...prev];
|
|
94
|
+
updatedDetail[keyCounter] = {
|
|
95
|
+
...updatedDetail[keyCounter],
|
|
96
|
+
[fieldId]: value,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
debouncedUpdate({ [rows.key]: updatedDetail });
|
|
100
|
+
|
|
101
|
+
return updatedDetail;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleSortChange = (groupId, entryId, direction) => {
|
|
106
|
+
setLocalData((prev) => {
|
|
107
|
+
const updatedDetail = [...prev];
|
|
108
|
+
const group = groupCategoriesByType(updatedDetail).find(
|
|
109
|
+
(group) => group.id === groupId
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (group) {
|
|
113
|
+
const index = group.entries.findIndex(
|
|
114
|
+
(entry) => entry.id === entryId
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
(direction === 'up' && index > 0) ||
|
|
119
|
+
(direction === 'down' && index < group.entries.length - 1)
|
|
120
|
+
) {
|
|
121
|
+
const swapIndex =
|
|
122
|
+
direction === 'up' ? index - 1 : index + 1;
|
|
123
|
+
[
|
|
124
|
+
group.entries[index].sort_order,
|
|
125
|
+
group.entries[swapIndex].sort_order,
|
|
126
|
+
] = [
|
|
127
|
+
group.entries[swapIndex].sort_order,
|
|
128
|
+
group.entries[index].sort_order,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const reordered = updatedDetail.map((item) => {
|
|
132
|
+
const entry = group.entries.find(
|
|
133
|
+
(e) => e.id === item.id
|
|
134
|
+
);
|
|
135
|
+
return entry
|
|
136
|
+
? { ...item, sort_order: entry.sort_order }
|
|
137
|
+
: item;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
debouncedUpdate({ [rows.key]: reordered });
|
|
141
|
+
return reordered;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return updatedDetail;
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleDeleteRow = (entry) => {
|
|
150
|
+
confirmAlert({
|
|
151
|
+
title: 'Confirm to Delete',
|
|
152
|
+
message: 'Are you sure you want to delete this entry?',
|
|
153
|
+
buttons: [
|
|
154
|
+
{
|
|
155
|
+
label: 'Yes',
|
|
156
|
+
onClick: async () => {
|
|
157
|
+
setLocalData((prev) => {
|
|
158
|
+
const updatedDetail = prev.filter(
|
|
159
|
+
(item) => item.id !== entry.id
|
|
160
|
+
);
|
|
161
|
+
debouncedUpdate({ [rows.key]: updatedDetail });
|
|
162
|
+
return updatedDetail;
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: 'No',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const renderScheduledTableField = (entry, column, keyCounter) => {
|
|
174
|
+
if (!entry || !column) return null;
|
|
175
|
+
|
|
176
|
+
switch (column.type) {
|
|
177
|
+
case 'currency':
|
|
178
|
+
case 'number':
|
|
179
|
+
return (
|
|
180
|
+
<input
|
|
181
|
+
type="text"
|
|
182
|
+
value={entry[column.id] || ''}
|
|
183
|
+
onChange={(e) =>
|
|
184
|
+
handleFieldChange(
|
|
185
|
+
e.target.value,
|
|
186
|
+
column.id,
|
|
187
|
+
keyCounter
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
style={{ textAlign: 'right' }}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
case 'date':
|
|
194
|
+
return (
|
|
195
|
+
<input
|
|
196
|
+
type="date"
|
|
197
|
+
value={entry[column.id] || ''}
|
|
198
|
+
onChange={(e) =>
|
|
199
|
+
handleFieldChange(
|
|
200
|
+
e.target.value,
|
|
201
|
+
column.id,
|
|
202
|
+
keyCounter
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
style={{ textAlign: 'right' }}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
case 'dropdown':
|
|
209
|
+
return (
|
|
210
|
+
<select
|
|
211
|
+
value={entry[column.id] || ''}
|
|
212
|
+
onChange={(e) =>
|
|
213
|
+
handleFieldChange(
|
|
214
|
+
e.target.value,
|
|
215
|
+
column.id,
|
|
216
|
+
keyCounter
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
{column.options.map((option) => (
|
|
221
|
+
<option key={option.id} value={option.id}>
|
|
222
|
+
{option.label}
|
|
223
|
+
</option>
|
|
224
|
+
))}
|
|
225
|
+
</select>
|
|
226
|
+
);
|
|
227
|
+
case 'dropdown-ajax':
|
|
228
|
+
const options = preloadedOptions[column.id] || [];
|
|
229
|
+
return (
|
|
230
|
+
<select
|
|
231
|
+
value={entry[column.id] || ''}
|
|
232
|
+
onChange={(e) =>
|
|
233
|
+
handleFieldChange(
|
|
234
|
+
e.target.value,
|
|
235
|
+
column.id,
|
|
236
|
+
keyCounter
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
<option>Select an Option</option>
|
|
241
|
+
{options.map((option) => (
|
|
242
|
+
<option key={option.id} value={option.id}>
|
|
243
|
+
{option.label}
|
|
244
|
+
</option>
|
|
245
|
+
))}
|
|
246
|
+
</select>
|
|
247
|
+
);
|
|
248
|
+
case 'multi-dropdown-ajax':
|
|
249
|
+
const multiOptions = preloadedOptions[column.id] || [];
|
|
250
|
+
return (
|
|
251
|
+
<MultiSelect
|
|
252
|
+
settings={{
|
|
253
|
+
id: column.id,
|
|
254
|
+
}}
|
|
255
|
+
className={styles.selectFullWidth}
|
|
256
|
+
multi={false}
|
|
257
|
+
onChange={(value) =>
|
|
258
|
+
handleFieldChange(value, column.id, keyCounter)
|
|
259
|
+
}
|
|
260
|
+
inputValue={entry[column.id] || ''}
|
|
261
|
+
options={multiOptions}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
default:
|
|
265
|
+
return (
|
|
266
|
+
<input
|
|
267
|
+
type="text"
|
|
268
|
+
defaultValue={entry[column.id] || ''}
|
|
269
|
+
onChange={(e) =>
|
|
270
|
+
handleFieldChange(
|
|
271
|
+
e.target.value,
|
|
272
|
+
column.id,
|
|
273
|
+
keyCounter
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
/>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
setGroupedCategories(groupCategoriesByType(localData));
|
|
283
|
+
}, [localData]);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div className={styles.gridtxt}>
|
|
287
|
+
<div className={styles.gridtxt__header}>
|
|
288
|
+
<span>{schedulingConfig.title || 'Estimation Schedule'}</span>
|
|
289
|
+
</div>
|
|
290
|
+
{groupedCategories.length > 0 ? (
|
|
291
|
+
<table className={styles.schedulingTable}>
|
|
292
|
+
<thead>
|
|
293
|
+
<tr>
|
|
294
|
+
{columns.map((column) => (
|
|
295
|
+
<th
|
|
296
|
+
key={column.id}
|
|
297
|
+
style={{ width: column.width || 'auto' }}
|
|
298
|
+
>
|
|
299
|
+
{column.label}
|
|
300
|
+
</th>
|
|
301
|
+
))}
|
|
302
|
+
<th style={{ width: '5%' }}></th>
|
|
303
|
+
</tr>
|
|
304
|
+
</thead>
|
|
305
|
+
<tbody>
|
|
306
|
+
{groupedCategories.map((group) => (
|
|
307
|
+
<React.Fragment key={group.id}>
|
|
308
|
+
<tr className={styles.categoryRow}>
|
|
309
|
+
<td colSpan={columns.length + 1}>
|
|
310
|
+
{group.category}
|
|
311
|
+
</td>
|
|
312
|
+
</tr>
|
|
313
|
+
{group.entries.map((entry, idx) => (
|
|
314
|
+
<tr key={entry.id}>
|
|
315
|
+
{columns.map((column) => (
|
|
316
|
+
<td
|
|
317
|
+
key={column.id}
|
|
318
|
+
style={{
|
|
319
|
+
width:
|
|
320
|
+
column.width || 'auto',
|
|
321
|
+
textAlign:
|
|
322
|
+
column.type ===
|
|
323
|
+
'currency'
|
|
324
|
+
? 'right'
|
|
325
|
+
: 'left',
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
{renderScheduledTableField(
|
|
329
|
+
entry,
|
|
330
|
+
column,
|
|
331
|
+
entry.keyCounter
|
|
332
|
+
)}
|
|
333
|
+
</td>
|
|
334
|
+
))}
|
|
335
|
+
<td>
|
|
336
|
+
<div className={styles.tdactions}>
|
|
337
|
+
{idx > 0 && (
|
|
338
|
+
<ArrowUp
|
|
339
|
+
size={18}
|
|
340
|
+
onClick={() =>
|
|
341
|
+
handleSortChange(
|
|
342
|
+
group.id,
|
|
343
|
+
entry.id,
|
|
344
|
+
'up'
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
style={{
|
|
348
|
+
cursor: 'pointer',
|
|
349
|
+
}}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
{idx <
|
|
353
|
+
group.entries.length -
|
|
354
|
+
1 && (
|
|
355
|
+
<ArrowDown
|
|
356
|
+
size={18}
|
|
357
|
+
onClick={() =>
|
|
358
|
+
handleSortChange(
|
|
359
|
+
group.id,
|
|
360
|
+
entry.id,
|
|
361
|
+
'down'
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
style={{
|
|
365
|
+
cursor: 'pointer',
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
)}
|
|
369
|
+
<TrashCan
|
|
370
|
+
size={18}
|
|
371
|
+
onClick={() =>
|
|
372
|
+
handleDeleteRow(entry)
|
|
373
|
+
}
|
|
374
|
+
style={{
|
|
375
|
+
cursor: 'pointer',
|
|
376
|
+
}}
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
</td>
|
|
380
|
+
</tr>
|
|
381
|
+
))}
|
|
382
|
+
</React.Fragment>
|
|
383
|
+
))}
|
|
384
|
+
</tbody>
|
|
385
|
+
</table>
|
|
386
|
+
) : (
|
|
387
|
+
<div className={styles.noDataMessage}>
|
|
388
|
+
No data available. Please check your filters or try
|
|
389
|
+
reloading the data.
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
<div className={styles.polActions}>
|
|
394
|
+
<button
|
|
395
|
+
className={styles.btn}
|
|
396
|
+
onClick={() => {
|
|
397
|
+
modalOpen('create', 0);
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
Add
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export default GenericEditableTable;
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
}
|
|
137
|
+
|
|
138
|
+
.tdactions {
|
|
139
|
+
width: 100%;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-wrap: nowrap;
|
|
142
|
+
justify-content: flex-start;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.1rem;
|
|
145
|
+
|
|
146
|
+
span {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
position: relative;
|
|
150
|
+
width: 18px;
|
|
151
|
+
height: 18px;
|
|
152
|
+
|
|
153
|
+
svg {
|
|
154
|
+
width: 18px;
|
|
155
|
+
height: 18px;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.noDataMessage {
|
|
161
|
+
text-align: center;
|
|
162
|
+
color: var(--primary-color);
|
|
163
|
+
font-size: 1rem;
|
|
164
|
+
padding: 2rem;
|
|
165
|
+
margin: 2rem auto;
|
|
166
|
+
background-color: var(--secondary-bg-color, #f8f8f8);
|
|
167
|
+
border: 1px solid rgba(var(--primary-rgb), 0.15);
|
|
168
|
+
border-radius: 6px;
|
|
169
|
+
max-width: 80%;
|
|
170
|
+
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
font-family: var(--font-family, Arial, sans-serif);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.noDataMessage:hover {
|
|
176
|
+
background-color: rgba(var(--primary-rgb), 0.05);
|
|
177
|
+
border-color: rgba(var(--primary-rgb), 0.2);
|
|
178
|
+
}
|
|
@@ -92,13 +92,13 @@
|
|
|
92
92
|
&.active a,
|
|
93
93
|
&:hover span,
|
|
94
94
|
&.active span {
|
|
95
|
-
background: rgba(var(--
|
|
96
|
-
color: var(--
|
|
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(--
|
|
101
|
+
color: var(--highlight-color);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
> ul {
|