@visns-studio/visns-components 5.13.15 → 5.13.17

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
@@ -88,7 +88,7 @@
88
88
  "react-dom": "^17.0.0 || ^18.0.0"
89
89
  },
90
90
  "name": "@visns-studio/visns-components",
91
- "version": "5.13.15",
91
+ "version": "5.13.17",
92
92
  "description": "Various packages to assist in the development of our Custom Applications.",
93
93
  "main": "src/index.js",
94
94
  "files": [
@@ -18,6 +18,62 @@ import styles from '../styles/GenericDashboard.module.scss';
18
18
 
19
19
  const toSnakeCase = (str) => str.replace(/[\s-]+/g, '_').toLowerCase();
20
20
 
21
+ const renderProgressBar = (value, options = {}) => {
22
+ const percentage = parseFloat(value) || 0;
23
+ const { showPercentage = true, color = 'primary' } = options;
24
+
25
+ const colorMap = {
26
+ primary: '#007bff',
27
+ success: '#28a745',
28
+ warning: '#ffc107',
29
+ danger: '#dc3545',
30
+ info: '#17a2b8'
31
+ };
32
+
33
+ const backgroundColor = colorMap[color] || colorMap.primary;
34
+
35
+ return (
36
+ <div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
37
+ <div
38
+ style={{
39
+ width: '100px',
40
+ height: '20px',
41
+ backgroundColor: '#e9ecef',
42
+ borderRadius: '10px',
43
+ overflow: 'hidden',
44
+ marginRight: showPercentage ? '8px' : '0'
45
+ }}
46
+ >
47
+ <div
48
+ style={{
49
+ width: `${Math.min(Math.max(percentage, 0), 100)}%`,
50
+ height: '100%',
51
+ backgroundColor: backgroundColor,
52
+ borderRadius: '10px',
53
+ transition: 'width 0.3s ease'
54
+ }}
55
+ />
56
+ </div>
57
+ {showPercentage && (
58
+ <span style={{ fontSize: '12px', color: '#6c757d', minWidth: '35px' }}>
59
+ {percentage.toFixed(1)}%
60
+ </span>
61
+ )}
62
+ </div>
63
+ );
64
+ };
65
+
66
+ const renderCellContent = (value, column, widget) => {
67
+ const columnFormatting = widget.columnFormatting || {};
68
+ const formatting = columnFormatting[column];
69
+
70
+ if (formatting && formatting.type === 'progressBar') {
71
+ return renderProgressBar(value, formatting);
72
+ }
73
+
74
+ return value;
75
+ };
76
+
21
77
  function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
22
78
  const swapy = useRef(null);
23
79
  const container = useRef(null);
@@ -1060,11 +1116,11 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1060
1116
  <td
1061
1117
  key={`table-data-${rowIndex}-${columnIndex}`}
1062
1118
  >
1063
- {
1064
- row[
1065
- column
1066
- ]
1067
- }
1119
+ {renderCellContent(
1120
+ row[column],
1121
+ column,
1122
+ widget
1123
+ )}
1068
1124
  </td>
1069
1125
  )
1070
1126
  )}
@@ -642,28 +642,30 @@ function GenericDetail({
642
642
  if (!url) return toast.error('Export URL not found');
643
643
 
644
644
  let exportUrl = url;
645
-
645
+
646
646
  // Replace {id} placeholder with actual ID value
647
647
  if (key && data[key]) {
648
648
  exportUrl = url.replace('{id}', data[key]);
649
649
  } else if (url.includes('{id}') && data.id) {
650
650
  exportUrl = url.replace('{id}', data.id);
651
651
  }
652
-
652
+
653
653
  // Legacy support: append key value to URL if no placeholder found
654
654
  if (!url.includes('{id}') && key && data[key]) {
655
655
  exportUrl = `${url}/${data[key]}`;
656
656
  }
657
-
657
+
658
658
  if (method === 'POST') {
659
659
  // Create a form to submit POST request
660
660
  const form = document.createElement('form');
661
661
  form.method = 'POST';
662
662
  form.action = exportUrl;
663
663
  form.target = '_blank';
664
-
664
+
665
665
  // Add CSRF token if available
666
- const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
666
+ const csrfToken = document
667
+ .querySelector('meta[name="csrf-token"]')
668
+ ?.getAttribute('content');
667
669
  if (csrfToken) {
668
670
  const csrfInput = document.createElement('input');
669
671
  csrfInput.type = 'hidden';
@@ -671,7 +673,7 @@ function GenericDetail({
671
673
  csrfInput.value = csrfToken;
672
674
  form.appendChild(csrfInput);
673
675
  }
674
-
676
+
675
677
  document.body.appendChild(form);
676
678
  form.submit();
677
679
  document.body.removeChild(form);
@@ -1434,9 +1436,9 @@ function GenericDetail({
1434
1436
  return renderTotal();
1435
1437
  default:
1436
1438
  if (id === 'proposalDynamic') {
1437
- return size === 'full' && truncatedValue
1438
- ? <div className={styles.proposalDynamicContainer}>
1439
- <iframe
1439
+ return size === 'full' && truncatedValue ? (
1440
+ <div className={styles.proposalDynamicContainer}>
1441
+ <iframe
1440
1442
  srcDoc={`
1441
1443
  <!DOCTYPE html>
1442
1444
  <html>
@@ -1493,18 +1495,21 @@ function GenericDetail({
1493
1495
  width: '100%',
1494
1496
  minHeight: '400px',
1495
1497
  border: 'none',
1496
- backgroundColor: 'transparent'
1498
+ backgroundColor: 'transparent',
1497
1499
  }}
1498
1500
  onLoad={(e) => {
1499
1501
  const iframe = e.target;
1500
- const doc = iframe.contentDocument || iframe.contentWindow.document;
1502
+ const doc =
1503
+ iframe.contentDocument ||
1504
+ iframe.contentWindow.document;
1501
1505
  const height = doc.body.scrollHeight;
1502
1506
  iframe.style.height = height + 'px';
1503
1507
  }}
1504
1508
  />
1505
- </div>
1506
- : <div className={styles.proposalDynamicContainer}>
1507
- <iframe
1509
+ </div>
1510
+ ) : (
1511
+ <div className={styles.proposalDynamicContainer}>
1512
+ <iframe
1508
1513
  srcDoc={`
1509
1514
  <!DOCTYPE html>
1510
1515
  <html>
@@ -1531,16 +1536,19 @@ function GenericDetail({
1531
1536
  width: '100%',
1532
1537
  minHeight: '200px',
1533
1538
  border: 'none',
1534
- backgroundColor: 'transparent'
1539
+ backgroundColor: 'transparent',
1535
1540
  }}
1536
1541
  onLoad={(e) => {
1537
1542
  const iframe = e.target;
1538
- const doc = iframe.contentDocument || iframe.contentWindow.document;
1543
+ const doc =
1544
+ iframe.contentDocument ||
1545
+ iframe.contentWindow.document;
1539
1546
  const height = doc.body.scrollHeight;
1540
1547
  iframe.style.height = height + 'px';
1541
1548
  }}
1542
1549
  />
1543
- </div>;
1550
+ </div>
1551
+ );
1544
1552
  }
1545
1553
  return size === 'full' && truncatedValue
1546
1554
  ? parse(`<br /><p>${truncatedValue}</p>`)
@@ -2672,7 +2680,8 @@ function GenericDetail({
2672
2680
  )
2673
2681
  }
2674
2682
  >
2675
- {activeTabConfig.export.label || 'Export'}
2683
+ {activeTabConfig.export
2684
+ .label || 'Export'}
2676
2685
  </button>
2677
2686
  )}
2678
2687
 
@@ -3004,21 +3013,27 @@ function GenericDetail({
3004
3013
  // Replace URL parameters like {key} with actual values
3005
3014
  const replaceUrlParams = (url) => {
3006
3015
  if (!url) return url;
3007
-
3016
+
3008
3017
  // Handle the new urlKey parameter
3009
- const urlKey = activeTabConfig.urlKey || 'dataId';
3010
-
3018
+ const urlKey =
3019
+ activeTabConfig.urlKey || 'dataId';
3020
+
3011
3021
  let replacementValue;
3012
3022
  if (urlKey === 'dataId') {
3013
3023
  // Use routeParams for dataId (backward compatibility)
3014
- replacementValue = routeParams[urlParam] || '';
3024
+ replacementValue =
3025
+ routeParams[urlParam] || '';
3015
3026
  } else {
3016
3027
  // Use data object for other keys, supporting dot notation
3017
- replacementValue = urlKey.split('.').reduce((obj, key) => {
3018
- return obj && obj[key] !== undefined ? obj[key] : '';
3019
- }, data);
3028
+ replacementValue = urlKey
3029
+ .split('.')
3030
+ .reduce((obj, key) => {
3031
+ return obj && obj[key] !== undefined
3032
+ ? obj[key]
3033
+ : '';
3034
+ }, data);
3020
3035
  }
3021
-
3036
+
3022
3037
  // Replace both {key} and {dataId} patterns for backward compatibility
3023
3038
  return url
3024
3039
  .replace(/{key}/g, replacementValue)
@@ -3050,7 +3065,9 @@ function GenericDetail({
3050
3065
  entityData={data}
3051
3066
  routeParams={routeParams}
3052
3067
  fetchData={handleReload}
3053
- showDescription={activeTabConfig.showDescription}
3068
+ showDescription={
3069
+ activeTabConfig.showDescription
3070
+ }
3054
3071
  />
3055
3072
  );
3056
3073
  case 'scheduling':
@@ -3062,6 +3079,7 @@ function GenericDetail({
3062
3079
  modalOpen={modalOpen}
3063
3080
  preloadedOptions={preloadedOptions}
3064
3081
  setData={setData}
3082
+ userProfile={userProfile}
3065
3083
  />
3066
3084
  );
3067
3085
  case 'custom':
@@ -210,6 +210,7 @@ const GenericEditableTable = ({
210
210
  dataId,
211
211
  preloadedOptions,
212
212
  setData, // callback to update the parent state
213
+ userProfile, // user profile for permission checks
213
214
  }) => {
214
215
  const isInitialLoad = useRef(true);
215
216
  const { columns, rows, form, title } = schedulingConfig;
@@ -217,6 +218,21 @@ const GenericEditableTable = ({
217
218
  const hasColourColumn = columns.some((col) => col.id === 'colours');
218
219
  const hasMarginRow = rows && rows.marginRow;
219
220
 
221
+ // Check if user has permission to view margin rows
222
+ const hasMarginPermission = () => {
223
+ if (!rows?.marginRoles || !Array.isArray(rows.marginRoles)) {
224
+ return true; // Show by default if no marginRoles specified
225
+ }
226
+
227
+ if (!userProfile?.roles) {
228
+ return false; // Hide if marginRoles exist but no user profile
229
+ }
230
+
231
+ return userProfile.roles.some(role =>
232
+ rows.marginRoles.includes(role.name)
233
+ );
234
+ };
235
+
220
236
  // dataSets: each item => { created_at: string, data: array of rows }
221
237
  const [dataSets, setDataSets] = useState([]);
222
238
  // Index of the currently active dataset
@@ -1312,95 +1328,99 @@ const GenericEditableTable = ({
1312
1328
  <td></td>
1313
1329
  </tr>
1314
1330
 
1315
- {/* LKD Margin */}
1316
- <tr style={{ backgroundColor: '#f8f9fa' }}>
1317
- <td colSpan={6} style={{ padding: '8px' }}>
1318
- LKD Margin (in %)
1319
- </td>
1320
- <td
1321
- colSpan={2}
1322
- style={{ padding: '8px', textAlign: 'center' }}
1323
- >
1324
- <input
1325
- type="number"
1326
- value={marginPercentage}
1327
- onChange={(e) =>
1328
- handleMarginPercentageChange(
1329
- parseFloat(e.target.value) || 0
1330
- )
1331
- }
1332
- placeholder="0"
1333
- style={{
1334
- padding: '4px',
1335
- border: '1px solid #ccc',
1336
- borderRadius: '3px',
1337
- textAlign: 'center',
1338
- marginRight: '4px',
1339
- }}
1340
- />
1341
- </td>
1342
- <td
1343
- colSpan={2}
1344
- style={{ padding: '8px', textAlign: 'right' }}
1345
- >
1346
- {marginAmount.toLocaleString('en-AU', {
1347
- style: 'currency',
1348
- currency: 'AUD',
1349
- })}
1350
- </td>
1351
- <td></td>
1352
- </tr>
1331
+ {/* Margin - Show only if user has permission */}
1332
+ {hasMarginPermission() && (
1333
+ <>
1334
+ <tr style={{ backgroundColor: '#f8f9fa' }}>
1335
+ <td colSpan={6} style={{ padding: '8px' }}>
1336
+ {rows?.marginLabel || 'LKD'} Margin (in %)
1337
+ </td>
1338
+ <td
1339
+ colSpan={2}
1340
+ style={{ padding: '8px', textAlign: 'center' }}
1341
+ >
1342
+ <input
1343
+ type="number"
1344
+ value={marginPercentage}
1345
+ onChange={(e) =>
1346
+ handleMarginPercentageChange(
1347
+ parseFloat(e.target.value) || 0
1348
+ )
1349
+ }
1350
+ placeholder="0"
1351
+ style={{
1352
+ padding: '4px',
1353
+ border: '1px solid #ccc',
1354
+ borderRadius: '3px',
1355
+ textAlign: 'center',
1356
+ marginRight: '4px',
1357
+ }}
1358
+ />
1359
+ </td>
1360
+ <td
1361
+ colSpan={2}
1362
+ style={{ padding: '8px', textAlign: 'right' }}
1363
+ >
1364
+ {marginAmount.toLocaleString('en-AU', {
1365
+ style: 'currency',
1366
+ currency: 'AUD',
1367
+ })}
1368
+ </td>
1369
+ <td></td>
1370
+ </tr>
1353
1371
 
1354
- {/* Total LKD Project Cost */}
1355
- <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1356
- <td colSpan={6} style={{ padding: '8px' }}>
1357
- Total LKD Project Cost
1358
- </td>
1359
- <td
1360
- colSpan={4}
1361
- style={{ padding: '8px', textAlign: 'right' }}
1362
- >
1363
- {totalLkdProjectCost.toLocaleString('en-AU', {
1364
- style: 'currency',
1365
- currency: 'AUD',
1366
- })}
1367
- </td>
1368
- <td></td>
1369
- </tr>
1372
+ {/* Total Project Cost */}
1373
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1374
+ <td colSpan={6} style={{ padding: '8px' }}>
1375
+ Total {rows?.marginLabel || 'LKD'} Project Cost
1376
+ </td>
1377
+ <td
1378
+ colSpan={4}
1379
+ style={{ padding: '8px', textAlign: 'right' }}
1380
+ >
1381
+ {totalLkdProjectCost.toLocaleString('en-AU', {
1382
+ style: 'currency',
1383
+ currency: 'AUD',
1384
+ })}
1385
+ </td>
1386
+ <td></td>
1387
+ </tr>
1370
1388
 
1371
- {/* Total GST */}
1372
- <tr style={{ backgroundColor: '#e9ecef' }}>
1373
- <td colSpan={6} style={{ padding: '8px' }}>
1374
- Total GST
1375
- </td>
1376
- <td
1377
- colSpan={4}
1378
- style={{ padding: '8px', textAlign: 'right' }}
1379
- >
1380
- {totalLkdGst.toLocaleString('en-AU', {
1381
- style: 'currency',
1382
- currency: 'AUD',
1383
- })}
1384
- </td>
1385
- <td></td>
1386
- </tr>
1389
+ {/* Total GST */}
1390
+ <tr style={{ backgroundColor: '#e9ecef' }}>
1391
+ <td colSpan={6} style={{ padding: '8px' }}>
1392
+ Total GST
1393
+ </td>
1394
+ <td
1395
+ colSpan={4}
1396
+ style={{ padding: '8px', textAlign: 'right' }}
1397
+ >
1398
+ {totalLkdGst.toLocaleString('en-AU', {
1399
+ style: 'currency',
1400
+ currency: 'AUD',
1401
+ })}
1402
+ </td>
1403
+ <td></td>
1404
+ </tr>
1387
1405
 
1388
- {/* Total LKD Project Cost (Including GST) */}
1389
- <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1390
- <td colSpan={6} style={{ padding: '8px' }}>
1391
- Total LKD Project Cost (Including GST)
1392
- </td>
1393
- <td
1394
- colSpan={4}
1395
- style={{ padding: '8px', textAlign: 'right' }}
1396
- >
1397
- {totalLkdWithGst.toLocaleString('en-AU', {
1398
- style: 'currency',
1399
- currency: 'AUD',
1400
- })}
1401
- </td>
1402
- <td></td>
1403
- </tr>
1406
+ {/* Total Project Cost (Including GST) */}
1407
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1408
+ <td colSpan={6} style={{ padding: '8px' }}>
1409
+ Total {rows?.marginLabel || 'LKD'} Project Cost (Including GST)
1410
+ </td>
1411
+ <td
1412
+ colSpan={4}
1413
+ style={{ padding: '8px', textAlign: 'right' }}
1414
+ >
1415
+ {totalLkdWithGst.toLocaleString('en-AU', {
1416
+ style: 'currency',
1417
+ currency: 'AUD',
1418
+ })}
1419
+ </td>
1420
+ <td></td>
1421
+ </tr>
1422
+ </>
1423
+ )}
1404
1424
  </>
1405
1425
  );
1406
1426
  };