@visns-studio/visns-components 5.12.12 → 5.12.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 CHANGED
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.12.12",
90
+ "version": "5.12.14",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -10,6 +10,64 @@ import CustomFetch from '../Fetch';
10
10
  import MultiSelect from '../MultiSelect';
11
11
  import styles from '../styles/GenericEditableTable.module.scss';
12
12
 
13
+ // Simple Tooltip Component
14
+ const Tooltip = ({ children, text, position = 'top' }) => {
15
+ const [isVisible, setIsVisible] = useState(false);
16
+ const [coords, setCoords] = useState({ x: 0, y: 0 });
17
+ const elementRef = useRef(null);
18
+
19
+ const handleMouseEnter = () => {
20
+ if (!elementRef.current) return;
21
+ const rect = elementRef.current.getBoundingClientRect();
22
+ const scrollLeft =
23
+ window.pageXOffset || document.documentElement.scrollLeft;
24
+ const scrollTop =
25
+ window.pageYOffset || document.documentElement.scrollTop;
26
+
27
+ let x = rect.left + scrollLeft + rect.width / 2;
28
+ let y = rect.top + scrollTop;
29
+
30
+ if (position === 'bottom') {
31
+ y = rect.bottom + scrollTop + 5;
32
+ } else if (position === 'top') {
33
+ y = y - 5;
34
+ }
35
+
36
+ setCoords({ x, y });
37
+ setIsVisible(true);
38
+ };
39
+
40
+ const handleMouseLeave = () => {
41
+ setIsVisible(false);
42
+ };
43
+
44
+ return (
45
+ <span
46
+ ref={elementRef}
47
+ onMouseEnter={handleMouseEnter}
48
+ onMouseLeave={handleMouseLeave}
49
+ className={styles.tooltipContainer}
50
+ >
51
+ {children}
52
+ {isVisible &&
53
+ ReactDOM.createPortal(
54
+ <div
55
+ className={`${styles.tooltip} ${
56
+ styles[`tooltip-${position}`]
57
+ }`}
58
+ style={{
59
+ left: coords.x,
60
+ top: coords.y,
61
+ }}
62
+ >
63
+ {text}
64
+ </div>,
65
+ document.body
66
+ )}
67
+ </span>
68
+ );
69
+ };
70
+
13
71
  // Updated ColourPicker with an expanded pastel palette
14
72
  const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
15
73
  const [colorPickerShow, setColorPickerShow] = useState(false);
@@ -57,38 +115,47 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
57
115
  return (
58
116
  <div className={styles.cpicker}>
59
117
  <div className={styles.cpicker__btn}>
60
- <button
61
- ref={buttonRef}
62
- style={value ? {
63
- background: value,
64
- border: '1px solid rgba(0,0,0,0.2)',
65
- borderRadius: '3px',
66
- width: '20px',
67
- height: '20px',
68
- padding: '0',
69
- cursor: 'pointer',
70
- display: 'flex',
71
- alignItems: 'center',
72
- justifyContent: 'center'
73
- } : {
74
- background: 'transparent',
75
- border: '1px solid rgba(0,0,0,0.2)',
76
- borderRadius: '3px',
77
- width: '20px',
78
- height: '20px',
79
- padding: '0',
80
- cursor: 'pointer',
81
- display: 'flex',
82
- alignItems: 'center',
83
- justifyContent: 'center'
84
- }}
85
- onClick={(e) => {
86
- e.preventDefault();
87
- setColorPickerShow(!colorPickerShow);
88
- }}
89
- >
90
- <Droplets size={16} style={{ color: value ? 'white' : 'black' }} />
91
- </button>
118
+ <Tooltip text="Select color">
119
+ <button
120
+ ref={buttonRef}
121
+ style={
122
+ value
123
+ ? {
124
+ background: value,
125
+ border: '1px solid rgba(0,0,0,0.2)',
126
+ borderRadius: '3px',
127
+ width: '20px',
128
+ height: '20px',
129
+ padding: '0',
130
+ cursor: 'pointer',
131
+ display: 'flex',
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ }
135
+ : {
136
+ background: 'transparent',
137
+ border: '1px solid rgba(0,0,0,0.2)',
138
+ borderRadius: '3px',
139
+ width: '20px',
140
+ height: '20px',
141
+ padding: '0',
142
+ cursor: 'pointer',
143
+ display: 'flex',
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ }
147
+ }
148
+ onClick={(e) => {
149
+ e.preventDefault();
150
+ setColorPickerShow(!colorPickerShow);
151
+ }}
152
+ >
153
+ <Droplets
154
+ size={18}
155
+ style={{ color: value ? 'white' : 'black' }}
156
+ />
157
+ </button>
158
+ </Tooltip>
92
159
 
93
160
  {colorPickerShow &&
94
161
  ReactDOM.createPortal(
@@ -146,7 +213,7 @@ const GenericEditableTable = ({
146
213
  const isInitialLoad = useRef(true);
147
214
  const { columns, rows, form, title } = schedulingConfig;
148
215
  const hasCategory = rows && rows.categoryKey; // Only group if defined
149
- const hasColourColumn = columns.some(col => col.id === 'colours');
216
+ const hasColourColumn = columns.some((col) => col.id === 'colours');
150
217
 
151
218
  // dataSets: each item => { created_at: string, data: array of rows }
152
219
  const [dataSets, setDataSets] = useState([]);
@@ -158,6 +225,9 @@ const GenericEditableTable = ({
158
225
  const [newEntries, setNewEntries] = useState({});
159
226
  const [newEntry, setNewEntry] = useState({});
160
227
 
228
+ // Row selection state for copy functionality
229
+ const [selectedRows, setSelectedRows] = useState(new Set());
230
+
161
231
  // On mount or when 'data' changes, read from data[schedulingConfig.rows.key] and handle both new & old formats
162
232
  useEffect(() => {
163
233
  const raw = data && data[schedulingConfig.rows.key];
@@ -220,6 +290,8 @@ const GenericEditableTable = ({
220
290
  } else {
221
291
  setLocalData([]);
222
292
  }
293
+ // Clear selected rows when dataset changes
294
+ setSelectedRows(new Set());
223
295
  }, [dataSets, activeDatasetIndex]);
224
296
 
225
297
  // Helper to sort by sort_order
@@ -504,11 +576,50 @@ const GenericEditableTable = ({
504
576
  return hasCategory ? groupCategoriesByType(localData) : [];
505
577
  }, [localData, hasCategory]);
506
578
 
507
- // 5) Column copy
579
+ // 5) Row selection handlers
580
+ const handleRowSelection = (rowId, isChecked) => {
581
+ setSelectedRows((prev) => {
582
+ const newSelected = new Set(prev);
583
+ if (isChecked) {
584
+ newSelected.add(rowId);
585
+ } else {
586
+ newSelected.delete(rowId);
587
+ }
588
+ return newSelected;
589
+ });
590
+ };
591
+
592
+ const handleSelectAll = (isChecked, dataToSelect = null) => {
593
+ if (isChecked) {
594
+ const rowIds = dataToSelect
595
+ ? dataToSelect.map((entry) => entry.id)
596
+ : localData.map((entry) => entry.id);
597
+ setSelectedRows(new Set(rowIds));
598
+ } else {
599
+ setSelectedRows(new Set());
600
+ }
601
+ };
602
+
603
+ const isAllSelected = (dataToCheck = null) => {
604
+ const dataSet = dataToCheck || localData;
605
+ return (
606
+ dataSet.length > 0 &&
607
+ dataSet.every((entry) => selectedRows.has(entry.id))
608
+ );
609
+ };
610
+
611
+ // 6) Enhanced column copy with row selection
508
612
  const handleColumnCopy = (column, group = null) => {
509
- let values = group
510
- ? group.entries.map((entry) => entry[column.id])
511
- : localData.map((entry) => entry[column.id]);
613
+ let dataToProcess = group ? group.entries : localData;
614
+
615
+ // Filter by selected rows if any are selected
616
+ if (selectedRows.size > 0) {
617
+ dataToProcess = dataToProcess.filter((entry) =>
618
+ selectedRows.has(entry.id)
619
+ );
620
+ }
621
+
622
+ let values = dataToProcess.map((entry) => entry[column.id]);
512
623
 
513
624
  const textToCopy = values
514
625
  .filter((val) => val !== undefined && val !== null)
@@ -516,19 +627,23 @@ const GenericEditableTable = ({
516
627
 
517
628
  navigator.clipboard
518
629
  .writeText(textToCopy)
519
- .then(() =>
630
+ .then(() => {
631
+ const selectionInfo =
632
+ selectedRows.size > 0
633
+ ? ` (${selectedRows.size} selected rows)`
634
+ : ' (all rows)';
520
635
  toast.success(
521
636
  `Copied ${values.length} value${
522
637
  values.length !== 1 ? 's' : ''
523
638
  } from "${column.label}"${
524
639
  group ? ` (Group: ${group.category})` : ''
525
- }`
526
- )
527
- )
640
+ }${selectionInfo}`
641
+ );
642
+ })
528
643
  .catch(() => toast.error('Failed to copy to clipboard'));
529
644
  };
530
645
 
531
- // 6) Sorting: grouped
646
+ // 7) Sorting: grouped
532
647
  const handleSortChangeGrouped = (groupId, entryId, direction) => {
533
648
  if (!hasCategory) return;
534
649
  setLocalData((prev) => {
@@ -580,7 +695,7 @@ const GenericEditableTable = ({
580
695
  });
581
696
  };
582
697
 
583
- // 7) Sorting: ungrouped
698
+ // 8) Sorting: ungrouped
584
699
  const handleSortChangeUngrouped = (entryId, direction) => {
585
700
  setLocalData((prev) => {
586
701
  const sortedData = sortBySortOrder(prev);
@@ -611,7 +726,7 @@ const GenericEditableTable = ({
611
726
  });
612
727
  };
613
728
 
614
- // 8) Deletion
729
+ // 9) Deletion
615
730
  const handleDeleteRow = (entry) => {
616
731
  confirmDialog({
617
732
  title: 'Confirm to Delete',
@@ -784,7 +899,7 @@ const GenericEditableTable = ({
784
899
  // Render new row field
785
900
  const renderNewRowField = (newRowData, column, groupId = null) => {
786
901
  const value = newRowData ? newRowData[column.id] : '';
787
-
902
+
788
903
  // Special handling for colours column regardless of type
789
904
  if (column.id === 'colours') {
790
905
  return (
@@ -800,7 +915,7 @@ const GenericEditableTable = ({
800
915
  />
801
916
  );
802
917
  }
803
-
918
+
804
919
  switch (column.type) {
805
920
  case 'colour':
806
921
  // For other colour columns, picker is rendered in the action column
@@ -921,6 +1036,7 @@ const GenericEditableTable = ({
921
1036
  const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
922
1037
  return (
923
1038
  <tr className={styles.newRow}>
1039
+ <td className={styles.checkboxColumn}></td>
924
1040
  {columns.map((column) => (
925
1041
  <td
926
1042
  key={column.id}
@@ -946,6 +1062,51 @@ const GenericEditableTable = ({
946
1062
  );
947
1063
  };
948
1064
 
1065
+ // Checkbox column header
1066
+ const renderSelectAllCheckbox = (group = null) => (
1067
+ <th
1068
+ key="select-all"
1069
+ className={styles.checkboxColumn}
1070
+ onClick={() => {
1071
+ const currentlyAllSelected = isAllSelected(group?.entries);
1072
+ handleSelectAll(!currentlyAllSelected, group?.entries);
1073
+ }}
1074
+ style={{ cursor: 'pointer' }}
1075
+ title={
1076
+ group ? `Select all in ${group.category}` : 'Select all rows'
1077
+ }
1078
+ >
1079
+ <input
1080
+ type="checkbox"
1081
+ checked={isAllSelected(group?.entries)}
1082
+ onChange={() => {}} // Empty handler since we handle click on the cell
1083
+ style={{ cursor: 'pointer', pointerEvents: 'none' }}
1084
+ tabIndex={-1}
1085
+ />
1086
+ </th>
1087
+ );
1088
+
1089
+ // Row checkbox cell
1090
+ const renderRowCheckbox = (entry) => (
1091
+ <td
1092
+ key={`checkbox-${entry.id}`}
1093
+ className={styles.checkboxColumn}
1094
+ onClick={() => {
1095
+ const isCurrentlySelected = selectedRows.has(entry.id);
1096
+ handleRowSelection(entry.id, !isCurrentlySelected);
1097
+ }}
1098
+ style={{ cursor: 'pointer' }}
1099
+ >
1100
+ <input
1101
+ type="checkbox"
1102
+ checked={selectedRows.has(entry.id)}
1103
+ onChange={() => {}} // Empty handler since we handle click on the cell
1104
+ style={{ cursor: 'pointer', pointerEvents: 'none' }}
1105
+ tabIndex={-1}
1106
+ />
1107
+ </td>
1108
+ );
1109
+
949
1110
  // Header cell with optional copy icon
950
1111
  const renderHeaderCell = (column, group = null) => (
951
1112
  <th
@@ -961,15 +1122,17 @@ const GenericEditableTable = ({
961
1122
  >
962
1123
  <span>{column.label}</span>
963
1124
  {column.copy && (
964
- <span
965
- onClick={(e) => {
966
- e.stopPropagation();
967
- handleColumnCopy(column, group);
968
- }}
969
- style={{ cursor: 'pointer' }}
970
- >
971
- <Copy size={16} />
972
- </span>
1125
+ <Tooltip text="Copy column data to clipboard">
1126
+ <span
1127
+ onClick={(e) => {
1128
+ e.stopPropagation();
1129
+ handleColumnCopy(column, group);
1130
+ }}
1131
+ style={{ cursor: 'pointer' }}
1132
+ >
1133
+ <Copy size={20} />
1134
+ </span>
1135
+ </Tooltip>
973
1136
  )}
974
1137
  </div>
975
1138
  </th>
@@ -1027,11 +1190,12 @@ const GenericEditableTable = ({
1027
1190
  {groupedCategories.map((group) => (
1028
1191
  <React.Fragment key={group.id}>
1029
1192
  <tr className={styles.categoryRow}>
1030
- <td colSpan={columns.length + 1}>
1193
+ <td colSpan={columns.length + 2}>
1031
1194
  {group.category}
1032
1195
  </td>
1033
1196
  </tr>
1034
1197
  <tr>
1198
+ {renderSelectAllCheckbox(group)}
1035
1199
  {columns.map((column) =>
1036
1200
  renderHeaderCell(column, group)
1037
1201
  )}
@@ -1046,6 +1210,7 @@ const GenericEditableTable = ({
1046
1210
  getRowBackground(entry),
1047
1211
  }}
1048
1212
  >
1213
+ {renderRowCheckbox(entry)}
1049
1214
  {columns.map((column) => (
1050
1215
  <td
1051
1216
  key={column.id}
@@ -1070,55 +1235,70 @@ const GenericEditableTable = ({
1070
1235
  >
1071
1236
  {!hasColourColumn && (
1072
1237
  <ColourPicker
1073
- value={entry.row_colour || ''}
1074
- columnId={'row_colour'}
1075
- keyCounter={entry.keyCounter}
1076
- onChange={handleFieldChange}
1238
+ value={
1239
+ entry.row_colour ||
1240
+ ''
1241
+ }
1242
+ columnId={
1243
+ 'row_colour'
1244
+ }
1245
+ keyCounter={
1246
+ entry.keyCounter
1247
+ }
1248
+ onChange={
1249
+ handleFieldChange
1250
+ }
1077
1251
  />
1078
1252
  )}
1079
1253
  {idx > 0 && (
1080
- <ArrowUp
1081
- size={16}
1082
- onClick={() =>
1083
- handleSortChangeGrouped(
1084
- group.id,
1085
- entry.id,
1086
- 'up'
1087
- )
1088
- }
1089
- style={{
1090
- cursor: 'pointer',
1091
- }}
1092
- />
1254
+ <Tooltip text="Move up">
1255
+ <ArrowUp
1256
+ size={16}
1257
+ onClick={() =>
1258
+ handleSortChangeGrouped(
1259
+ group.id,
1260
+ entry.id,
1261
+ 'up'
1262
+ )
1263
+ }
1264
+ style={{
1265
+ cursor: 'pointer',
1266
+ }}
1267
+ />
1268
+ </Tooltip>
1093
1269
  )}
1094
1270
  {idx <
1095
1271
  group.entries.length -
1096
1272
  1 && (
1097
- <ArrowDown
1273
+ <Tooltip text="Move down">
1274
+ <ArrowDown
1275
+ size={16}
1276
+ onClick={() =>
1277
+ handleSortChangeGrouped(
1278
+ group.id,
1279
+ entry.id,
1280
+ 'down'
1281
+ )
1282
+ }
1283
+ style={{
1284
+ cursor: 'pointer',
1285
+ }}
1286
+ />
1287
+ </Tooltip>
1288
+ )}
1289
+ <Tooltip text="Delete row">
1290
+ <Trash2
1098
1291
  size={16}
1099
1292
  onClick={() =>
1100
- handleSortChangeGrouped(
1101
- group.id,
1102
- entry.id,
1103
- 'down'
1293
+ handleDeleteRow(
1294
+ entry
1104
1295
  )
1105
1296
  }
1106
1297
  style={{
1107
1298
  cursor: 'pointer',
1108
1299
  }}
1109
1300
  />
1110
- )}
1111
- <Trash2
1112
- size={16}
1113
- onClick={() =>
1114
- handleDeleteRow(
1115
- entry
1116
- )
1117
- }
1118
- style={{
1119
- cursor: 'pointer',
1120
- }}
1121
- />
1301
+ </Tooltip>
1122
1302
  </div>
1123
1303
  </td>
1124
1304
  </tr>
@@ -1130,6 +1310,7 @@ const GenericEditableTable = ({
1130
1310
  (col) => col.type === 'total'
1131
1311
  ) !== -1 && (
1132
1312
  <tr className={styles.subtotalRow}>
1313
+ <td></td>
1133
1314
  {columns.map((col, index) => {
1134
1315
  const totalColIndex =
1135
1316
  columns.findIndex(
@@ -1192,126 +1373,153 @@ const GenericEditableTable = ({
1192
1373
  <table className={styles.schedulingTable}>
1193
1374
  <thead>
1194
1375
  <tr>
1376
+ {renderSelectAllCheckbox()}
1195
1377
  {columns.map((column) => renderHeaderCell(column))}
1196
1378
  <th style={{ width: '5%' }}></th>
1197
1379
  </tr>
1198
1380
  </thead>
1199
1381
  <tbody>
1200
- {localData.length > 0 && sortBySortOrder(localData).map((entry, idx) => (
1201
- <tr
1202
- key={entry.id}
1203
- style={{ background: getRowBackground(entry) }}
1204
- >
1205
- {columns.map((column) => (
1206
- <td
1207
- key={column.id}
1208
- style={{
1209
- width: column.width || 'auto',
1210
- textAlign:
1211
- column.type === 'currency'
1212
- ? 'right'
1213
- : 'left',
1214
- }}
1215
- >
1216
- {renderScheduledTableField(
1217
- entry,
1218
- column,
1219
- idx
1220
- )}
1382
+ {localData.length > 0 &&
1383
+ sortBySortOrder(localData).map((entry, idx) => (
1384
+ <tr
1385
+ key={entry.id}
1386
+ style={{
1387
+ background: getRowBackground(entry),
1388
+ }}
1389
+ >
1390
+ {renderRowCheckbox(entry)}
1391
+ {columns.map((column) => (
1392
+ <td
1393
+ key={column.id}
1394
+ style={{
1395
+ width: column.width || 'auto',
1396
+ textAlign:
1397
+ column.type === 'currency'
1398
+ ? 'right'
1399
+ : 'left',
1400
+ }}
1401
+ >
1402
+ {renderScheduledTableField(
1403
+ entry,
1404
+ column,
1405
+ idx
1406
+ )}
1407
+ </td>
1408
+ ))}
1409
+ <td>
1410
+ <div className={styles.tdactions}>
1411
+ {!hasColourColumn && (
1412
+ <ColourPicker
1413
+ value={
1414
+ entry.row_colour || ''
1415
+ }
1416
+ columnId={'row_colour'}
1417
+ keyCounter={idx}
1418
+ onChange={handleFieldChange}
1419
+ />
1420
+ )}
1421
+ {idx > 0 && (
1422
+ <Tooltip text="Move up">
1423
+ <ArrowUp
1424
+ size={16}
1425
+ onClick={() =>
1426
+ handleSortChangeUngrouped(
1427
+ entry.id,
1428
+ 'up'
1429
+ )
1430
+ }
1431
+ style={{
1432
+ cursor: 'pointer',
1433
+ }}
1434
+ />
1435
+ </Tooltip>
1436
+ )}
1437
+ {idx <
1438
+ sortBySortOrder(localData)
1439
+ .length -
1440
+ 1 && (
1441
+ <Tooltip text="Move down">
1442
+ <ArrowDown
1443
+ size={16}
1444
+ onClick={() =>
1445
+ handleSortChangeUngrouped(
1446
+ entry.id,
1447
+ 'down'
1448
+ )
1449
+ }
1450
+ style={{
1451
+ cursor: 'pointer',
1452
+ }}
1453
+ />
1454
+ </Tooltip>
1455
+ )}
1456
+ <Tooltip text="Delete row">
1457
+ <Trash2
1458
+ size={16}
1459
+ onClick={() =>
1460
+ handleDeleteRow(entry)
1461
+ }
1462
+ style={{
1463
+ cursor: 'pointer',
1464
+ }}
1465
+ />
1466
+ </Tooltip>
1467
+ </div>
1221
1468
  </td>
1222
- ))}
1223
- <td>
1224
- <div className={styles.tdactions}>
1225
- {!hasColourColumn && (
1226
- <ColourPicker
1227
- value={entry.row_colour || ''}
1228
- columnId={'row_colour'}
1229
- keyCounter={idx}
1230
- onChange={handleFieldChange}
1231
- />
1232
- )}
1233
- {idx > 0 && (
1234
- <ArrowUp
1235
- size={16}
1236
- onClick={() =>
1237
- handleSortChangeUngrouped(
1238
- entry.id,
1239
- 'up'
1240
- )
1241
- }
1242
- style={{ cursor: 'pointer' }}
1243
- />
1244
- )}
1245
- {idx <
1246
- sortBySortOrder(localData).length -
1247
- 1 && (
1248
- <ArrowDown
1249
- size={16}
1250
- onClick={() =>
1251
- handleSortChangeUngrouped(
1252
- entry.id,
1253
- 'down'
1254
- )
1255
- }
1256
- style={{ cursor: 'pointer' }}
1257
- />
1258
- )}
1259
- <Trash2
1260
- size={16}
1261
- onClick={() =>
1262
- handleDeleteRow(entry)
1263
- }
1264
- style={{ cursor: 'pointer' }}
1265
- />
1266
- </div>
1267
- </td>
1268
- </tr>
1269
- ))}
1469
+ </tr>
1470
+ ))}
1270
1471
 
1271
1472
  {renderNewRowForm()}
1272
1473
 
1273
- {localData.length > 0 && columns.findIndex((col) => col.type === 'total') !==
1274
- -1 && (
1275
- <tr className={styles.subtotalRow}>
1276
- {columns.map((col, index) => {
1277
- const totalColIndex = columns.findIndex(
1278
- (c) => c.type === 'total'
1279
- );
1280
- if (
1281
- index === totalColIndex - 1 &&
1282
- index > 0
1283
- ) {
1284
- return <td key={col.id}>Subtotal</td>;
1285
- }
1286
- if (index === totalColIndex) {
1287
- const overallTotal = sortBySortOrder(
1288
- localData
1289
- ).reduce(
1290
- (sum, entry) =>
1291
- sum +
1292
- calculateTotal(
1293
- entry,
1294
- columns[totalColIndex].keys
1295
- ),
1296
- 0
1297
- );
1298
- return (
1299
- <td key={col.id}>
1300
- {overallTotal.toLocaleString(
1301
- 'en-AU',
1302
- {
1303
- style: 'currency',
1304
- currency: 'AUD',
1305
- }
1306
- )}
1307
- </td>
1474
+ {localData.length > 0 &&
1475
+ columns.findIndex((col) => col.type === 'total') !==
1476
+ -1 && (
1477
+ <tr className={styles.subtotalRow}>
1478
+ <td></td>
1479
+ {columns.map((col, index) => {
1480
+ const totalColIndex = columns.findIndex(
1481
+ (c) => c.type === 'total'
1308
1482
  );
1309
- }
1310
- return <td key={col.id} />;
1311
- })}
1312
- <td></td>
1313
- </tr>
1314
- )}
1483
+ if (
1484
+ index === totalColIndex - 1 &&
1485
+ index > 0
1486
+ ) {
1487
+ return (
1488
+ <td key={col.id}>Subtotal</td>
1489
+ );
1490
+ }
1491
+ if (index === totalColIndex) {
1492
+ const overallTotal =
1493
+ sortBySortOrder(
1494
+ localData
1495
+ ).reduce(
1496
+ (sum, entry) =>
1497
+ sum +
1498
+ calculateTotal(
1499
+ entry,
1500
+ columns[
1501
+ totalColIndex
1502
+ ].keys
1503
+ ),
1504
+ 0
1505
+ );
1506
+ return (
1507
+ <td key={col.id}>
1508
+ {overallTotal.toLocaleString(
1509
+ 'en-AU',
1510
+ {
1511
+ style: 'currency',
1512
+ currency: 'AUD',
1513
+ }
1514
+ )}
1515
+ </td>
1516
+ );
1517
+ }
1518
+ return <td key={col.id} />;
1519
+ })}
1520
+ <td></td>
1521
+ </tr>
1522
+ )}
1315
1523
  </tbody>
1316
1524
  </table>
1317
1525
  )}
@@ -5,12 +5,12 @@
5
5
 
6
6
  th,
7
7
  td {
8
- padding: 0.08rem 0.25rem; // Ultra-compact padding for cells
8
+ padding: 0.4rem 0.5rem; // Increased padding for better readability
9
9
  text-align: left;
10
10
  border: 1px solid rgba(var(--primary-rgb), 0.1);
11
- line-height: 1; // Minimal line height
11
+ line-height: 1.3; // Increased line height for better readability
12
12
  vertical-align: middle; // Ensure vertical alignment
13
- height: 22px; // Fixed row height
13
+ height: 40px; // Increased row height
14
14
  }
15
15
 
16
16
  // Additional high-specificity selectors to override any global styles
@@ -18,14 +18,14 @@
18
18
  td select,
19
19
  th input,
20
20
  th select {
21
- padding: 0.05rem 0.2rem !important;
21
+ padding: 0.3rem 0.5rem !important;
22
22
  margin: 0 !important;
23
- font-size: 0.7rem !important;
24
- height: 18px !important;
25
- line-height: 1 !important;
23
+ font-size: 0.9rem !important;
24
+ height: 32px !important;
25
+ line-height: 1.2 !important;
26
26
  border: 1px solid rgba(var(--primary-rgb), 0.1) !important;
27
27
  box-sizing: border-box !important;
28
- border-radius: 2px !important;
28
+ border-radius: 4px !important;
29
29
  width: 100% !important;
30
30
  appearance: none !important;
31
31
  background: white !important;
@@ -35,9 +35,9 @@
35
35
  th select {
36
36
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path d="M6 9L1 4h10z" fill="%23666"/></svg>') !important;
37
37
  background-repeat: no-repeat !important;
38
- background-position: right 0.2rem center !important;
39
- background-size: 8px !important;
40
- padding: 0.05rem 1rem 0.05rem 0.2rem !important;
38
+ background-position: right 0.5rem center !important;
39
+ background-size: 10px !important;
40
+ padding: 0.3rem 1.5rem 0.3rem 0.5rem !important;
41
41
  cursor: pointer !important;
42
42
 
43
43
  &::-ms-expand {
@@ -46,13 +46,13 @@
46
46
  }
47
47
 
48
48
  th {
49
- padding: 0.1rem 0.25rem; // Ultra-compact header padding
50
- font-size: 0.75rem; // Smaller font size
49
+ padding: 0.5rem 0.5rem; // Increased header padding
50
+ font-size: 1rem; // Larger font size for headers
51
51
  font-weight: bold;
52
52
  background-color: var(--primary-color);
53
53
  color: white;
54
54
  text-align: center;
55
- height: 24px; // Fixed header height
55
+ height: 45px; // Increased header height
56
56
  }
57
57
 
58
58
  td {
@@ -64,17 +64,17 @@
64
64
  }
65
65
  }
66
66
 
67
- /* Ultra-compact styling for native input and select elements - override global styles */
67
+ /* Updated styling for native input and select elements */
68
68
  input,
69
69
  select {
70
- padding: 0.05rem 0.2rem !important; // Force minimal padding, override global styles
70
+ padding: 0.3rem 0.5rem !important; // Increased padding for better usability
71
71
  margin: 0 !important;
72
- font-size: 0.7rem !important; // Smaller font
73
- height: 18px !important; // Minimal height
74
- line-height: 1 !important;
72
+ font-size: 0.9rem !important; // Larger font size for readability
73
+ height: 32px !important; // Increased height
74
+ line-height: 1.2 !important;
75
75
  border: 1px solid rgba(var(--primary-rgb), 0.1) !important;
76
76
  box-sizing: border-box !important;
77
- border-radius: 2px !important; // Smaller border radius
77
+ border-radius: 4px !important; // Larger border radius
78
78
  width: 100% !important;
79
79
  appearance: none !important; // Remove default dropdown arrow for consistency
80
80
  background: white !important;
@@ -84,9 +84,9 @@
84
84
  &:not([type]) {
85
85
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path d="M6 9L1 4h10z" fill="%23666"/></svg>') !important;
86
86
  background-repeat: no-repeat !important;
87
- background-position: right 0.2rem center !important;
88
- background-size: 8px !important;
89
- padding-right: 1rem !important;
87
+ background-position: right 0.5rem center !important;
88
+ background-size: 10px !important;
89
+ padding-right: 1.5rem !important;
90
90
  }
91
91
  }
92
92
 
@@ -96,9 +96,9 @@
96
96
  background: white !important;
97
97
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path d="M6 9L1 4h10z" fill="%23666"/></svg>') !important;
98
98
  background-repeat: no-repeat !important;
99
- background-position: right 0.2rem center !important;
100
- background-size: 8px !important;
101
- padding: 0.05rem 1rem 0.05rem 0.2rem !important; // Force specific padding values
99
+ background-position: right 0.5rem center !important;
100
+ background-size: 10px !important;
101
+ padding: 0.3rem 1.5rem 0.3rem 0.5rem !important; // Increased padding values
102
102
  cursor: pointer !important;
103
103
 
104
104
  // Remove default arrow on different browsers
@@ -233,19 +233,19 @@
233
233
  width: max-content;
234
234
  display: inline-block;
235
235
  position: relative;
236
- padding: 0.2rem 0.4rem; /* Ultra-minimal padding */
236
+ padding: 0.4rem 0.8rem; /* Increased padding */
237
237
  cursor: pointer;
238
- font-size: 0.75rem; /* Even smaller font size */
238
+ font-size: 0.9rem; /* Larger font size */
239
239
  color: var(--tertiary-color);
240
240
  text-decoration: none;
241
241
  overflow: hidden;
242
242
  background: var(--primary-color);
243
243
  border: 1px solid rgba(var(--primary-color--rgb), 1.1);
244
- border-radius: 2px; /* Smaller border radius */
244
+ border-radius: 4px; /* Larger border radius */
245
245
  outline: none;
246
246
  transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
247
- height: 20px; /* Match input height */
248
- line-height: 1;
247
+ height: 34px; /* Match input height */
248
+ line-height: 1.2;
249
249
  display: flex;
250
250
  align-items: center;
251
251
  justify-content: center;
@@ -263,19 +263,19 @@
263
263
  flex-wrap: nowrap;
264
264
  justify-content: flex-start;
265
265
  align-items: center;
266
- gap: 0.05rem; // Reduced gap
266
+ gap: 0.3rem; // Increased gap
267
267
  background: transparent !important; /* Ensure no background color interference */
268
268
 
269
269
  span {
270
270
  display: flex;
271
271
  align-items: center;
272
272
  position: relative;
273
- width: 14px; // Smaller icons
274
- height: 14px;
273
+ width: 16px; // Smaller icons for sort and trash
274
+ height: 16px;
275
275
 
276
276
  svg {
277
- width: 14px;
278
- height: 14px;
277
+ width: 16px;
278
+ height: 16px;
279
279
  }
280
280
  }
281
281
  }
@@ -293,8 +293,8 @@
293
293
  margin: 0;
294
294
  outline: none;
295
295
  transition: all 0.2s ease;
296
- width: 16px; // Smaller color picker button
297
- height: 16px;
296
+ width: 24px; // Larger color picker button
297
+ height: 24px;
298
298
 
299
299
  &:hover {
300
300
  opacity: 0.8;
@@ -302,8 +302,8 @@
302
302
  }
303
303
 
304
304
  svg {
305
- width: 12px; // Smaller icon
306
- height: 12px;
305
+ width: 18px; // Larger icon
306
+ height: 18px;
307
307
  }
308
308
  }
309
309
  }
@@ -665,19 +665,19 @@
665
665
  border-top: 1px dashed rgba(var(--primary-rgb), 0.3);
666
666
  border-bottom: 1px dashed rgba(var(--primary-rgb), 0.3);
667
667
 
668
- /* Ultra-compact styling for new row inputs and selects - force override globals */
668
+ /* Updated styling for new row inputs and selects */
669
669
  input,
670
670
  select {
671
- height: 18px !important; // Consistent height
672
- padding: 0.05rem 0.2rem !important; // Force override global padding
673
- font-size: 0.7rem !important;
671
+ height: 32px !important; // Increased height
672
+ padding: 0.3rem 0.5rem !important; // Increased padding
673
+ font-size: 0.9rem !important;
674
674
  appearance: none !important;
675
675
  background: white !important;
676
676
  margin: 0 !important;
677
- line-height: 1 !important;
677
+ line-height: 1.2 !important;
678
678
  border: 1px solid rgba(var(--primary-rgb), 0.1) !important;
679
679
  box-sizing: border-box !important;
680
- border-radius: 2px !important;
680
+ border-radius: 4px !important;
681
681
  width: 100% !important;
682
682
 
683
683
  // Ensure selects have dropdown arrow
@@ -685,9 +685,9 @@
685
685
  &:not([type]) {
686
686
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path d="M6 9L1 4h10z" fill="%23666"/></svg>') !important;
687
687
  background-repeat: no-repeat !important;
688
- background-position: right 0.2rem center !important;
689
- background-size: 8px !important;
690
- padding-right: 1rem !important;
688
+ background-position: right 0.5rem center !important;
689
+ background-size: 10px !important;
690
+ padding-right: 1.5rem !important;
691
691
  }
692
692
  }
693
693
 
@@ -696,9 +696,9 @@
696
696
  background: white !important;
697
697
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path d="M6 9L1 4h10z" fill="%23666"/></svg>') !important;
698
698
  background-repeat: no-repeat !important;
699
- background-position: right 0.2rem center !important;
700
- background-size: 8px !important;
701
- padding: 0.05rem 1rem 0.05rem 0.2rem !important; // Force specific padding
699
+ background-position: right 0.5rem center !important;
700
+ background-size: 10px !important;
701
+ padding: 0.3rem 1.5rem 0.3rem 0.5rem !important; // Updated padding
702
702
  cursor: pointer !important;
703
703
 
704
704
  &::-ms-expand {
@@ -707,6 +707,122 @@
707
707
  }
708
708
  }
709
709
 
710
+ /* Tooltip styles */
711
+ .tooltipContainer {
712
+ position: relative;
713
+ display: inline-flex;
714
+ align-items: center;
715
+ justify-content: center;
716
+ }
717
+
718
+ .tooltip {
719
+ position: absolute;
720
+ background: rgba(0, 0, 0, 0.9);
721
+ color: white;
722
+ padding: 6px 10px;
723
+ border-radius: 4px;
724
+ font-size: 0.75rem;
725
+ font-weight: 500;
726
+ white-space: nowrap;
727
+ z-index: 10000;
728
+ pointer-events: none;
729
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
730
+ transform: translateX(-50%);
731
+
732
+ &::before {
733
+ content: '';
734
+ position: absolute;
735
+ left: 50%;
736
+ border: 5px solid transparent;
737
+ transform: translateX(-50%);
738
+ }
739
+
740
+ &.tooltip-top {
741
+ transform: translateX(-50%) translateY(-100%);
742
+ margin-top: -5px;
743
+
744
+ &::before {
745
+ top: 100%;
746
+ border-top-color: rgba(0, 0, 0, 0.9);
747
+ }
748
+ }
749
+
750
+ &.tooltip-bottom {
751
+ transform: translateX(-50%) translateY(0%);
752
+ margin-top: 5px;
753
+
754
+ &::before {
755
+ bottom: 100%;
756
+ border-bottom-color: rgba(0, 0, 0, 0.9);
757
+ }
758
+ }
759
+ }
760
+
761
+ /* Checkbox styling */
762
+ .schedulingTable input[type="checkbox"] {
763
+ cursor: pointer;
764
+ width: 14px !important;
765
+ height: 14px !important;
766
+ min-width: 14px !important;
767
+ min-height: 14px !important;
768
+ max-width: 14px !important;
769
+ max-height: 14px !important;
770
+ margin: 0 !important;
771
+ padding: 0 !important;
772
+ accent-color: var(--primary-color);
773
+ vertical-align: middle;
774
+ flex-shrink: 0;
775
+ display: inline-block;
776
+
777
+ &:hover {
778
+ transform: scale(1.05);
779
+ }
780
+ }
781
+
782
+ /* Checkbox column specific styling */
783
+ .checkboxColumn {
784
+ width: 30px !important;
785
+ max-width: 30px !important;
786
+ min-width: 30px !important;
787
+ padding: 0.2rem !important;
788
+ text-align: center !important;
789
+ vertical-align: middle !important;
790
+ cursor: pointer !important;
791
+ user-select: none !important;
792
+
793
+ /* Ensure checkbox container doesn't stretch */
794
+ display: table-cell !important;
795
+
796
+ input[type="checkbox"] {
797
+ display: inline-block !important;
798
+ vertical-align: middle !important;
799
+ pointer-events: none !important; /* Prevent direct checkbox interaction */
800
+ position: relative !important;
801
+ top: 0 !important;
802
+ left: 0 !important;
803
+ transform: none !important;
804
+ margin: 0 auto !important;
805
+ }
806
+ }
807
+
808
+ /* Specific styling for header checkbox to ensure proper alignment */
809
+ .schedulingTable th.checkboxColumn {
810
+ position: relative !important;
811
+
812
+ input[type="checkbox"] {
813
+ position: absolute !important;
814
+ top: 50% !important;
815
+ left: 50% !important;
816
+ transform: translate(-50%, -50%) !important;
817
+ margin: 0 !important;
818
+ display: block !important;
819
+
820
+ &:hover {
821
+ transform: translate(-50%, -50%) scale(1.05) !important;
822
+ }
823
+ }
824
+ }
825
+
710
826
  .datasetSelector {
711
827
  display: flex;
712
828
  align-items: center;