@visns-studio/visns-components 5.13.16 → 5.13.18

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.16",
91
+ "version": "5.13.18",
92
92
  "description": "Various packages to assist in the development of our Custom Applications.",
93
93
  "main": "src/index.js",
94
94
  "files": [
@@ -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
@@ -359,6 +375,14 @@ const GenericEditableTable = ({
359
375
  setSelectedRows(new Set());
360
376
  }, [dataSets, activeDatasetIndex, hasMarginRow, rows.marginRow]);
361
377
 
378
+ // Initialize newEntry with default values when columns change
379
+ useEffect(() => {
380
+ const defaultValues = getDefaultRowValues();
381
+ if (!hasCategory) {
382
+ setNewEntry(defaultValues);
383
+ }
384
+ }, [columns, hasCategory]);
385
+
362
386
  // Helper to sort by sort_order
363
387
  const sortBySortOrder = (dataArray) =>
364
388
  [...dataArray].sort((a, b) => a.sort_order - b.sort_order);
@@ -400,6 +424,73 @@ const GenericEditableTable = ({
400
424
  return undefined;
401
425
  };
402
426
 
427
+ // Helper function to get default values for new row
428
+ const getDefaultRowValues = () => {
429
+ const defaultValues = {};
430
+ columns.forEach((column) => {
431
+ switch (column.type) {
432
+ case 'dropdown':
433
+ if (column.options && column.options.length > 0) {
434
+ defaultValues[column.id] = column.options[0].id;
435
+ }
436
+ break;
437
+ case 'dropdown-ajax':
438
+ // Reset to empty for ajax dropdowns
439
+ defaultValues[column.id] = '';
440
+ break;
441
+ case 'currency':
442
+ case 'number':
443
+ case 'text':
444
+ case 'date':
445
+ case 'html5_date':
446
+ // Reset to empty string for input fields
447
+ defaultValues[column.id] = '';
448
+ break;
449
+ case 'multi-dropdown-ajax':
450
+ // Reset to empty for multi-select
451
+ defaultValues[column.id] = '';
452
+ break;
453
+ case 'total':
454
+ // Don't reset total fields as they are calculated
455
+ break;
456
+ case 'colour':
457
+ // Reset colour fields
458
+ defaultValues[column.id] = '';
459
+ break;
460
+ default:
461
+ // Reset any other field types to empty string
462
+ defaultValues[column.id] = '';
463
+ break;
464
+ }
465
+ });
466
+ return defaultValues;
467
+ };
468
+
469
+ // Helper function to get add row background color (same logic as getRowBackground)
470
+ const getAddRowBackground = (newRowData, groupId = null) => {
471
+ if (!newRowData) newRowData = {};
472
+
473
+ // Ensure default status values are applied for color calculation
474
+ const enrichedRowData = { ...newRowData };
475
+ columns.forEach((column) => {
476
+ if (column.type === 'dropdown' && column.options && column.options.length > 0) {
477
+ if (!enrichedRowData[column.id]) {
478
+ enrichedRowData[column.id] = column.options[0].id;
479
+ }
480
+ }
481
+ });
482
+
483
+ // Check if there's a colour picked from the colours column
484
+ const coloursValue = enrichedRowData.colours || enrichedRowData.row_colour;
485
+ if (coloursValue) {
486
+ return `${coloursValue}40`; // Add 40% opacity
487
+ }
488
+
489
+ // Check for status color if no manual color is set
490
+ const statusColor = getStatusColor(enrichedRowData);
491
+ return statusColor ? `${statusColor}40` : undefined; // Add 40% opacity
492
+ };
493
+
403
494
  // Calculate total project cost (sum of all entries excluding margin row)
404
495
  const calculateTotalProjectCost = (dataEntries) => {
405
496
  const totalColIndex = columns.findIndex((c) => c.type === 'total');
@@ -658,11 +749,12 @@ const GenericEditableTable = ({
658
749
  setDataSets(updatedDataSets);
659
750
  debouncedUpdate(updatedDataSets);
660
751
 
661
- // Reset new-entry state
752
+ // Reset new-entry state with default values
753
+ const defaultValues = getDefaultRowValues();
662
754
  if (hasCategory) {
663
- setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
755
+ setNewEntries((prev) => ({ ...prev, [groupId]: defaultValues }));
664
756
  } else {
665
- setNewEntry({});
757
+ setNewEntry(defaultValues);
666
758
  }
667
759
  toast.success('Row added');
668
760
  };
@@ -1101,10 +1193,11 @@ const GenericEditableTable = ({
1101
1193
  case 'currency':
1102
1194
  return (
1103
1195
  <CurrencyInput
1196
+ key={`new-currency-${column.id}-${groupId || 'default'}-${value || 'empty'}`}
1104
1197
  id={`new-currency-${column.id}-${groupId || 'default'}`}
1105
1198
  name={column.id}
1106
1199
  placeholder="$0.00"
1107
- defaultValue={value || ''}
1200
+ value={value || ''}
1108
1201
  decimalsLimit={2}
1109
1202
  prefix="$"
1110
1203
  decimalSeparator="."
@@ -1165,9 +1258,10 @@ const GenericEditableTable = ({
1165
1258
  />
1166
1259
  );
1167
1260
  case 'dropdown':
1261
+ const defaultValue = value || (column.options && column.options.length > 0 ? column.options[0].id : '');
1168
1262
  return (
1169
1263
  <select
1170
- value={value || ''}
1264
+ value={defaultValue}
1171
1265
  onChange={(e) =>
1172
1266
  handleNewFieldChange(
1173
1267
  e.target.value,
@@ -1312,95 +1406,99 @@ const GenericEditableTable = ({
1312
1406
  <td></td>
1313
1407
  </tr>
1314
1408
 
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>
1409
+ {/* Margin - Show only if user has permission */}
1410
+ {hasMarginPermission() && (
1411
+ <>
1412
+ <tr style={{ backgroundColor: '#f8f9fa' }}>
1413
+ <td colSpan={6} style={{ padding: '8px' }}>
1414
+ {rows?.marginLabel || 'LKD'} Margin (in %)
1415
+ </td>
1416
+ <td
1417
+ colSpan={2}
1418
+ style={{ padding: '8px', textAlign: 'center' }}
1419
+ >
1420
+ <input
1421
+ type="number"
1422
+ value={marginPercentage}
1423
+ onChange={(e) =>
1424
+ handleMarginPercentageChange(
1425
+ parseFloat(e.target.value) || 0
1426
+ )
1427
+ }
1428
+ placeholder="0"
1429
+ style={{
1430
+ padding: '4px',
1431
+ border: '1px solid #ccc',
1432
+ borderRadius: '3px',
1433
+ textAlign: 'center',
1434
+ marginRight: '4px',
1435
+ }}
1436
+ />
1437
+ </td>
1438
+ <td
1439
+ colSpan={2}
1440
+ style={{ padding: '8px', textAlign: 'right' }}
1441
+ >
1442
+ {marginAmount.toLocaleString('en-AU', {
1443
+ style: 'currency',
1444
+ currency: 'AUD',
1445
+ })}
1446
+ </td>
1447
+ <td></td>
1448
+ </tr>
1353
1449
 
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>
1450
+ {/* Total Project Cost */}
1451
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1452
+ <td colSpan={6} style={{ padding: '8px' }}>
1453
+ Total {rows?.marginLabel || 'LKD'} Project Cost
1454
+ </td>
1455
+ <td
1456
+ colSpan={4}
1457
+ style={{ padding: '8px', textAlign: 'right' }}
1458
+ >
1459
+ {totalLkdProjectCost.toLocaleString('en-AU', {
1460
+ style: 'currency',
1461
+ currency: 'AUD',
1462
+ })}
1463
+ </td>
1464
+ <td></td>
1465
+ </tr>
1370
1466
 
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>
1467
+ {/* Total GST */}
1468
+ <tr style={{ backgroundColor: '#e9ecef' }}>
1469
+ <td colSpan={6} style={{ padding: '8px' }}>
1470
+ Total GST
1471
+ </td>
1472
+ <td
1473
+ colSpan={4}
1474
+ style={{ padding: '8px', textAlign: 'right' }}
1475
+ >
1476
+ {totalLkdGst.toLocaleString('en-AU', {
1477
+ style: 'currency',
1478
+ currency: 'AUD',
1479
+ })}
1480
+ </td>
1481
+ <td></td>
1482
+ </tr>
1387
1483
 
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>
1484
+ {/* Total Project Cost (Including GST) */}
1485
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1486
+ <td colSpan={6} style={{ padding: '8px' }}>
1487
+ Total {rows?.marginLabel || 'LKD'} Project Cost (Including GST)
1488
+ </td>
1489
+ <td
1490
+ colSpan={4}
1491
+ style={{ padding: '8px', textAlign: 'right' }}
1492
+ >
1493
+ {totalLkdWithGst.toLocaleString('en-AU', {
1494
+ style: 'currency',
1495
+ currency: 'AUD',
1496
+ })}
1497
+ </td>
1498
+ <td></td>
1499
+ </tr>
1500
+ </>
1501
+ )}
1404
1502
  </>
1405
1503
  );
1406
1504
  };
@@ -1409,7 +1507,12 @@ const GenericEditableTable = ({
1409
1507
  const renderNewRowForm = (groupId = null) => {
1410
1508
  const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
1411
1509
  return (
1412
- <tr className={styles.newRow}>
1510
+ <tr
1511
+ className={styles.newRow}
1512
+ style={{
1513
+ background: getAddRowBackground(newRowData, groupId),
1514
+ }}
1515
+ >
1413
1516
  <td className={styles.checkboxColumn}></td>
1414
1517
  {columns.map((column) => (
1415
1518
  <td