@visns-studio/visns-components 5.1.19 → 5.1.20

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
@@ -77,7 +77,7 @@
77
77
  "react-dom": "^17.0.0 || ^18.0.0"
78
78
  },
79
79
  "name": "@visns-studio/visns-components",
80
- "version": "5.1.19",
80
+ "version": "5.1.20",
81
81
  "description": "Various packages to assist in the development of our Custom Applications.",
82
82
  "main": "src/index.js",
83
83
  "files": [
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
3
3
  import debounce from 'lodash.debounce';
4
4
  import { toast } from 'react-toastify';
5
5
  import { confirmAlert } from 'react-confirm-alert';
6
- import { TrashCan, ArrowUp, ArrowDown } from 'akar-icons';
6
+ import { TrashCan, ArrowUp, ArrowDown, Copy, Water } from 'akar-icons';
7
7
  import { CompactPicker } from 'react-color';
8
8
 
9
9
  import CustomFetch from '../Fetch';
@@ -11,25 +11,37 @@ import MultiSelect from '../MultiSelect';
11
11
 
12
12
  import 'react-confirm-alert/src/react-confirm-alert.css';
13
13
  import styles from './styles/GenericEditableTable.module.scss';
14
- import { set } from 'lodash';
15
14
 
15
+ // Updated ColourPicker with an expanded pastel palette and a refined Akar icon
16
16
  const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
17
17
  const [colorPickerShow, setColorPickerShow] = useState(false);
18
18
  const colorPickerRef = useRef(null);
19
- const buttonRef = useRef(null); // Reference to the button for positioning
20
- const popoverRef = useRef(null); // Reference to the popover
19
+ const buttonRef = useRef(null);
20
+ const popoverRef = useRef(null);
21
+
22
+ // Expanded custom palette of pastel colours
23
+ const pastelColors = [
24
+ '#FFB3BA', // pastel red
25
+ '#FFDFBA', // pastel orange
26
+ '#FFFFBA', // pastel yellow
27
+ '#BAFFC9', // pastel green
28
+ '#BAE1FF', // pastel blue
29
+ '#BFFCC6', // additional pastel green
30
+ '#E2BAFF', // pastel purple
31
+ '#C3B1E1', // pastel lavender
32
+ '#F0E6EF', // pastel pink
33
+ '#D0E8F2', // pastel light blue
34
+ ];
21
35
 
22
- // Handle clicks outside of the color picker to close it
23
36
  useEffect(() => {
24
37
  const handleClickOutside = (event) => {
25
- // Check if click is outside both the button and the popover
26
38
  if (
27
39
  popoverRef.current &&
28
40
  !popoverRef.current.contains(event.target) &&
29
41
  buttonRef.current &&
30
42
  !buttonRef.current.contains(event.target)
31
43
  ) {
32
- setColorPickerShow(false); // Close the color picker
44
+ setColorPickerShow(false);
33
45
  }
34
46
  };
35
47
 
@@ -40,33 +52,34 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
40
52
 
41
53
  const buttonRect = buttonRef.current
42
54
  ? buttonRef.current.getBoundingClientRect()
43
- : {}; // Get button position
55
+ : {};
44
56
 
45
57
  return (
46
58
  <div className={styles.cpicker}>
47
59
  <div className={styles.cpicker__btn} ref={colorPickerRef}>
48
60
  <button
49
- ref={buttonRef} // Attach ref to the button
61
+ ref={buttonRef}
50
62
  style={
51
63
  value && value !== 'null' ? { background: value } : {}
52
64
  }
53
65
  onClick={(e) => {
54
66
  e.preventDefault();
55
- setColorPickerShow(!colorPickerShow); // Toggle visibility
67
+ setColorPickerShow(!colorPickerShow);
56
68
  }}
57
69
  >
58
- &#9744;
70
+ {/* Using the Akar Water icon with increased size and some margin */}
71
+ <Water size={20} style={{ color: 'black' }} />
59
72
  </button>
60
73
 
61
74
  {colorPickerShow &&
62
75
  ReactDOM.createPortal(
63
76
  <div
64
- ref={popoverRef} // Attach ref to the popover
77
+ ref={popoverRef}
65
78
  className={styles.colour_popover}
66
79
  style={{
67
80
  position: 'absolute',
68
- left: buttonRect.left, // Position it based on button's position
69
- top: buttonRect.bottom + 5, // Position below the button with a small offset
81
+ left: buttonRect.left,
82
+ top: buttonRect.bottom + 5,
70
83
  zIndex: 9999,
71
84
  }}
72
85
  >
@@ -76,13 +89,14 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
76
89
  />
77
90
  <CompactPicker
78
91
  color={value || ''}
92
+ colors={pastelColors}
79
93
  onChangeComplete={(color) => {
80
94
  onChange(color.hex, columnId, keyCounter);
81
95
  setColorPickerShow(false);
82
96
  }}
83
97
  />
84
98
  </div>,
85
- document.body // Render the popover inside the body
99
+ document.body
86
100
  )}
87
101
  </div>
88
102
  </div>
@@ -98,16 +112,30 @@ const GenericEditableTable = ({
98
112
  setData,
99
113
  }) => {
100
114
  const { columns, rows, form } = schedulingConfig;
115
+ const hasCategory = rows && rows.categoryKey; // Only group if defined
101
116
 
102
117
  const [localData, setLocalData] = useState([]);
103
- const [groupedCategories, setGroupedCategories] = useState([]);
104
118
 
119
+ // Helper to sort by sort_order without mutating the original array
120
+ const sortBySortOrder = (dataArray) =>
121
+ [...dataArray].sort((a, b) => a.sort_order - b.sort_order);
122
+
123
+ // Helper: if there is a column of type 'colour', return that value from the entry.
124
+ const getRowBackground = (entry) => {
125
+ const colourColumn = columns.find((col) => col.type === 'colour');
126
+ return colourColumn && entry[colourColumn.id]
127
+ ? entry[colourColumn.id]
128
+ : undefined;
129
+ };
130
+
131
+ // On load, enrich the data and sort by sort_order
105
132
  useEffect(() => {
106
133
  const enrichedData = data[rows.key]?.map((entry, index) => ({
107
134
  ...entry,
108
135
  sort_order: entry.sort_order ?? index + 1,
109
136
  }));
110
- setLocalData(enrichedData || []);
137
+ const sortedData = enrichedData ? sortBySortOrder(enrichedData) : [];
138
+ setLocalData(sortedData);
111
139
  }, [data, rows.key]);
112
140
 
113
141
  const debouncedUpdate = useMemo(
@@ -123,7 +151,6 @@ const GenericEditableTable = ({
123
151
  if (res.data?.data) {
124
152
  setData(res.data.data);
125
153
  }
126
- // toast.success('Template updated successfully');
127
154
  } else {
128
155
  toast.error(res.error);
129
156
  }
@@ -131,18 +158,20 @@ const GenericEditableTable = ({
131
158
  console.error('Error updating field:', error);
132
159
  }
133
160
  }, 1000),
134
- []
161
+ [form.url, dataId, setData]
135
162
  );
136
163
 
137
- const groupCategoriesByType = (data) => {
164
+ // Group data by category (if defined) and sort each group by sort_order
165
+ const groupCategoriesByType = (dataArray) => {
166
+ if (!rows.categoryKey) return [];
138
167
  const grouped = {};
139
168
 
140
- data.forEach((category, categoryKey) => {
141
- const categoryType =
142
- category[rows.categoryKey.id]?.[rows.categoryKey.label] || null;
143
-
144
- const categoryId = category[rows.categoryKey.id]?.id || null;
145
-
169
+ dataArray.forEach((entry, index) => {
170
+ const categoryInfo = entry[rows.categoryKey.id];
171
+ const categoryType = categoryInfo
172
+ ? categoryInfo[rows.categoryKey.label]
173
+ : null;
174
+ const categoryId = categoryInfo ? categoryInfo.id : null;
146
175
  const groupKey = categoryId || 'no_category';
147
176
 
148
177
  if (!grouped[groupKey]) {
@@ -153,8 +182,8 @@ const GenericEditableTable = ({
153
182
  };
154
183
  }
155
184
  grouped[groupKey].entries.push({
156
- ...category,
157
- keyCounter: categoryKey,
185
+ ...entry,
186
+ keyCounter: index,
158
187
  });
159
188
  });
160
189
 
@@ -169,6 +198,33 @@ const GenericEditableTable = ({
169
198
  );
170
199
  };
171
200
 
201
+ const groupedCategories = useMemo(() => {
202
+ if (hasCategory) {
203
+ return groupCategoriesByType(localData);
204
+ }
205
+ return [];
206
+ }, [localData, hasCategory]);
207
+
208
+ // Copy all values from a column to the clipboard
209
+ const handleColumnCopy = (column) => {
210
+ const values = localData.map((entry) => entry[column.id]);
211
+ const textToCopy = values
212
+ .filter((val) => val !== undefined && val !== null)
213
+ .join('\n');
214
+
215
+ navigator.clipboard
216
+ .writeText(textToCopy)
217
+ .then(() =>
218
+ toast.success(
219
+ `Copied ${values.length} value${
220
+ values.length !== 1 ? 's' : ''
221
+ } from "${column.label}"`
222
+ )
223
+ )
224
+ .catch(() => toast.error('Failed to copy to clipboard'));
225
+ };
226
+
227
+ // Update a field’s value (keyCounter corresponds to the row index in sorted data)
172
228
  const handleFieldChange = (value, fieldId, keyCounter) => {
173
229
  setLocalData((prev) => {
174
230
  const updatedDetail = [...prev];
@@ -178,16 +234,17 @@ const GenericEditableTable = ({
178
234
  };
179
235
 
180
236
  debouncedUpdate({ [rows.key]: updatedDetail });
181
-
182
237
  return updatedDetail;
183
238
  });
184
239
  };
185
240
 
186
- const handleSortChange = (groupId, entryId, direction) => {
241
+ // For grouped data, sort handler remains unchanged
242
+ const handleSortChangeGrouped = (groupId, entryId, direction) => {
243
+ if (!hasCategory) return;
187
244
  setLocalData((prev) => {
188
245
  const updatedDetail = [...prev];
189
246
  const group = groupCategoriesByType(updatedDetail).find(
190
- (group) => group.id === groupId
247
+ (grp) => grp.id === groupId
191
248
  );
192
249
 
193
250
  if (group) {
@@ -210,11 +267,11 @@ const GenericEditableTable = ({
210
267
  ];
211
268
 
212
269
  const reordered = updatedDetail.map((item) => {
213
- const entry = group.entries.find(
270
+ const found = group.entries.find(
214
271
  (e) => e.id === item.id
215
272
  );
216
- return entry
217
- ? { ...item, sort_order: entry.sort_order }
273
+ return found
274
+ ? { ...item, sort_order: found.sort_order }
218
275
  : item;
219
276
  });
220
277
 
@@ -222,11 +279,34 @@ const GenericEditableTable = ({
222
279
  return reordered;
223
280
  }
224
281
  }
225
-
226
282
  return updatedDetail;
227
283
  });
228
284
  };
229
285
 
286
+ // For ungrouped data, sort before swapping items.
287
+ const handleSortChangeUngrouped = (entryId, direction) => {
288
+ setLocalData((prev) => {
289
+ const sortedData = sortBySortOrder(prev);
290
+ const index = sortedData.findIndex((entry) => entry.id === entryId);
291
+
292
+ if (
293
+ (direction === 'up' && index === 0) ||
294
+ (direction === 'down' && index === sortedData.length - 1)
295
+ ) {
296
+ return prev;
297
+ }
298
+
299
+ const swapIndex = direction === 'up' ? index - 1 : index + 1;
300
+ [sortedData[index].sort_order, sortedData[swapIndex].sort_order] = [
301
+ sortedData[swapIndex].sort_order,
302
+ sortedData[index].sort_order,
303
+ ];
304
+
305
+ debouncedUpdate({ [rows.key]: sortedData });
306
+ return sortedData;
307
+ });
308
+ };
309
+
230
310
  const handleDeleteRow = (entry) => {
231
311
  confirmAlert({
232
312
  title: 'Confirm to Delete',
@@ -314,7 +394,7 @@ const GenericEditableTable = ({
314
394
  ))}
315
395
  </select>
316
396
  );
317
- case 'dropdown-ajax':
397
+ case 'dropdown-ajax': {
318
398
  const options = preloadedOptions[column.id] || [];
319
399
  return (
320
400
  <select
@@ -335,13 +415,12 @@ const GenericEditableTable = ({
335
415
  ))}
336
416
  </select>
337
417
  );
338
- case 'multi-dropdown-ajax':
418
+ }
419
+ case 'multi-dropdown-ajax': {
339
420
  const multiOptions = preloadedOptions[column.id] || [];
340
421
  return (
341
422
  <MultiSelect
342
- settings={{
343
- id: column.id,
344
- }}
423
+ settings={{ id: column.id }}
345
424
  className={styles.selectFullWidth}
346
425
  multi={false}
347
426
  onChange={(value) =>
@@ -351,19 +430,17 @@ const GenericEditableTable = ({
351
430
  options={multiOptions}
352
431
  />
353
432
  );
354
- case 'total':
433
+ }
434
+ case 'total': {
355
435
  const subtotal = column.keys.reduce(
356
436
  (sum, key) => sum + (parseFloat(entry[key]) || 0),
357
437
  0
358
438
  );
359
-
360
- // Format the subtotal as currency
361
- const formattedSubtotal = new Intl.NumberFormat('en-AU', {
439
+ return new Intl.NumberFormat('en-AU', {
362
440
  style: 'currency',
363
441
  currency: 'AUD',
364
442
  }).format(subtotal);
365
-
366
- return formattedSubtotal;
443
+ }
367
444
  default:
368
445
  return (
369
446
  <input
@@ -381,151 +458,298 @@ const GenericEditableTable = ({
381
458
  }
382
459
  };
383
460
 
384
- useEffect(() => {
385
- setGroupedCategories(groupCategoriesByType(localData));
386
- }, [localData]);
461
+ // Render header cell with a label and copy icon
462
+ const renderHeaderCell = (column) => (
463
+ <th key={column.id} style={{ width: column.width || 'auto' }}>
464
+ <div
465
+ style={{
466
+ display: 'flex',
467
+ alignItems: 'center',
468
+ justifyContent: 'space-between',
469
+ }}
470
+ >
471
+ <span>{column.label}</span>
472
+ {column.copy && (
473
+ <span
474
+ onClick={(e) => {
475
+ e.stopPropagation();
476
+ handleColumnCopy(column);
477
+ }}
478
+ style={{ cursor: 'pointer' }}
479
+ >
480
+ <Copy size={16} />
481
+ </span>
482
+ )}
483
+ </div>
484
+ </th>
485
+ );
387
486
 
388
487
  return (
389
488
  <div className={styles.gridtxt}>
390
489
  <div className={styles.gridtxt__header}>
391
490
  <span>{schedulingConfig.title || 'Estimation Schedule'}</span>
392
491
  </div>
393
- {groupedCategories.length > 0 ? (
394
- <table className={styles.schedulingTable}>
395
- <thead>
396
- <tr>
397
- {columns.map((column) => (
398
- <th
399
- key={column.id}
400
- style={{ width: column.width || 'auto' }}
401
- >
402
- {column.label}
403
- </th>
404
- ))}
405
- <th style={{ width: '5%' }}></th>
406
- </tr>
407
- </thead>
408
- <tbody>
409
- {groupedCategories.map((group) => (
410
- <React.Fragment key={group.id}>
411
- <tr className={styles.categoryRow}>
412
- <td colSpan={columns.length + 1}>
413
- {group.category}
414
- </td>
415
- </tr>
416
- {group.entries.map((entry, idx) => (
417
- <tr key={entry.id}>
418
- {columns.map((column) => (
419
- <td
420
- key={column.id}
421
- style={{
422
- width:
423
- column.width || 'auto',
424
- textAlign:
425
- column.type ===
426
- 'currency'
427
- ? 'right'
428
- : 'left',
429
- }}
430
- >
431
- {renderScheduledTableField(
432
- entry,
433
- column,
434
- entry.keyCounter
435
- )}
436
- </td>
437
- ))}
438
- <td>
439
- <div className={styles.tdactions}>
440
- {idx > 0 && (
441
- <ArrowUp
442
- size={16}
443
- onClick={() =>
444
- handleSortChange(
445
- group.id,
446
- entry.id,
447
- 'up'
448
- )
449
- }
450
- style={{
451
- cursor: 'pointer',
452
- }}
453
- />
454
- )}
455
- {idx <
456
- group.entries.length -
457
- 1 && (
458
- <ArrowDown
492
+ {hasCategory ? (
493
+ groupedCategories.length > 0 ? (
494
+ <table className={styles.schedulingTable}>
495
+ <thead>
496
+ <tr>
497
+ {columns.map((column) =>
498
+ renderHeaderCell(column)
499
+ )}
500
+ <th style={{ width: '5%' }}></th>
501
+ </tr>
502
+ </thead>
503
+ <tbody>
504
+ {groupedCategories.map((group) => (
505
+ <React.Fragment key={group.id}>
506
+ <tr className={styles.categoryRow}>
507
+ <td colSpan={columns.length + 1}>
508
+ {group.category}
509
+ </td>
510
+ </tr>
511
+ {group.entries.map((entry, idx) => (
512
+ <tr
513
+ key={entry.id}
514
+ style={{
515
+ background:
516
+ getRowBackground(entry),
517
+ }}
518
+ >
519
+ {columns.map((column) => (
520
+ <td
521
+ key={column.id}
522
+ style={{
523
+ width:
524
+ column.width ||
525
+ 'auto',
526
+ textAlign:
527
+ column.type ===
528
+ 'currency'
529
+ ? 'right'
530
+ : 'left',
531
+ }}
532
+ >
533
+ {renderScheduledTableField(
534
+ entry,
535
+ column,
536
+ entry.keyCounter
537
+ )}
538
+ </td>
539
+ ))}
540
+ <td>
541
+ <div
542
+ className={styles.tdactions}
543
+ >
544
+ {idx > 0 && (
545
+ <ArrowUp
546
+ size={16}
547
+ onClick={() =>
548
+ handleSortChangeGrouped(
549
+ group.id,
550
+ entry.id,
551
+ 'up'
552
+ )
553
+ }
554
+ style={{
555
+ cursor: 'pointer',
556
+ }}
557
+ />
558
+ )}
559
+ {idx <
560
+ group.entries.length -
561
+ 1 && (
562
+ <ArrowDown
563
+ size={16}
564
+ onClick={() =>
565
+ handleSortChangeGrouped(
566
+ group.id,
567
+ entry.id,
568
+ 'down'
569
+ )
570
+ }
571
+ style={{
572
+ cursor: 'pointer',
573
+ }}
574
+ />
575
+ )}
576
+ <TrashCan
459
577
  size={16}
460
578
  onClick={() =>
461
- handleSortChange(
462
- group.id,
463
- entry.id,
464
- 'down'
579
+ handleDeleteRow(
580
+ entry
465
581
  )
466
582
  }
467
583
  style={{
468
584
  cursor: 'pointer',
469
585
  }}
470
586
  />
471
- )}
472
- <TrashCan
473
- size={16}
474
- onClick={() =>
475
- handleDeleteRow(entry)
476
- }
477
- style={{
478
- cursor: 'pointer',
479
- }}
480
- />
481
- </div>
482
- </td>
587
+ </div>
588
+ </td>
589
+ </tr>
590
+ ))}
591
+ <tr className={styles.subtotalRow}>
592
+ {columns.map((column, index) => (
593
+ <td key={column.id}>
594
+ {index === columns.length - 1
595
+ ? group.entries
596
+ .reduce(
597
+ (sum, entry) => {
598
+ const subtotal =
599
+ Array.isArray(
600
+ column.keys
601
+ )
602
+ ? column.keys.reduce(
603
+ (
604
+ rowSum,
605
+ key
606
+ ) =>
607
+ rowSum +
608
+ (parseFloat(
609
+ entry[
610
+ key
611
+ ]
612
+ ) ||
613
+ 0),
614
+ 0
615
+ )
616
+ : 0;
617
+ return (
618
+ sum +
619
+ subtotal
620
+ );
621
+ },
622
+ 0
623
+ )
624
+ .toLocaleString(
625
+ 'en-AU',
626
+ {
627
+ style: 'currency',
628
+ currency:
629
+ 'AUD',
630
+ }
631
+ )
632
+ : index ===
633
+ columns.length - 2
634
+ ? 'Subtotal'
635
+ : ''}
636
+ </td>
637
+ ))}
638
+ <td></td>
483
639
  </tr>
640
+ </React.Fragment>
641
+ ))}
642
+ </tbody>
643
+ </table>
644
+ ) : (
645
+ <div className={styles.noDataMessage}>
646
+ No data available. Please check your filters or try
647
+ reloading the data.
648
+ </div>
649
+ )
650
+ ) : localData.length > 0 ? (
651
+ <table className={styles.schedulingTable}>
652
+ <thead>
653
+ <tr>
654
+ {columns.map((column) => renderHeaderCell(column))}
655
+ <th style={{ width: '5%' }}></th>
656
+ </tr>
657
+ </thead>
658
+ <tbody>
659
+ {sortBySortOrder(localData).map((entry, idx) => (
660
+ <tr
661
+ key={entry.id}
662
+ style={{ background: getRowBackground(entry) }}
663
+ >
664
+ {columns.map((column) => (
665
+ <td
666
+ key={column.id}
667
+ style={{
668
+ width: column.width || 'auto',
669
+ textAlign:
670
+ column.type === 'currency'
671
+ ? 'right'
672
+ : 'left',
673
+ }}
674
+ >
675
+ {renderScheduledTableField(
676
+ entry,
677
+ column,
678
+ idx
679
+ )}
680
+ </td>
484
681
  ))}
485
- <tr className={styles.subtotalRow}>
486
- {columns.map((column, index) => (
487
- <td key={column.id}>
488
- {index === columns.length - 1
489
- ? // Total column: Calculate the sum of each row's total
490
- group.entries
491
- .reduce((sum, entry) => {
492
- // For each row, sum the values based on column.keys
493
- const subtotal =
494
- Array.isArray(
495
- column.keys
496
- ) // Ensure column.keys is an array
497
- ? column.keys.reduce(
498
- (
499
- rowSum,
500
- key
501
- ) =>
502
- rowSum +
503
- (parseFloat(
504
- entry[
505
- key
506
- ]
507
- ) ||
508
- 0),
509
- 0
510
- )
511
- : 0; // Default to 0 if column.keys is not an array
512
- return sum + subtotal;
513
- }, 0)
514
- .toLocaleString('en-AU', {
515
- style: 'currency',
516
- currency: 'AUD',
517
- })
518
- : index === columns.length - 2
519
- ? // The column immediately before 'total' shows 'Subtotal'
520
- 'Subtotal'
521
- : // All other columns are left empty
522
- ''}
523
- </td>
524
- ))}
525
- <td></td>
526
- </tr>
527
- </React.Fragment>
682
+ <td>
683
+ <div className={styles.tdactions}>
684
+ {idx > 0 && (
685
+ <ArrowUp
686
+ size={16}
687
+ onClick={() =>
688
+ handleSortChangeUngrouped(
689
+ entry.id,
690
+ 'up'
691
+ )
692
+ }
693
+ style={{ cursor: 'pointer' }}
694
+ />
695
+ )}
696
+ {idx <
697
+ sortBySortOrder(localData).length -
698
+ 1 && (
699
+ <ArrowDown
700
+ size={16}
701
+ onClick={() =>
702
+ handleSortChangeUngrouped(
703
+ entry.id,
704
+ 'down'
705
+ )
706
+ }
707
+ style={{ cursor: 'pointer' }}
708
+ />
709
+ )}
710
+ <TrashCan
711
+ size={16}
712
+ onClick={() =>
713
+ handleDeleteRow(entry)
714
+ }
715
+ style={{ cursor: 'pointer' }}
716
+ />
717
+ </div>
718
+ </td>
719
+ </tr>
528
720
  ))}
721
+ <tr className={styles.subtotalRow}>
722
+ {columns.map((column, index) => (
723
+ <td key={column.id}>
724
+ {index === columns.length - 1
725
+ ? sortBySortOrder(localData)
726
+ .reduce((sum, entry) => {
727
+ const subtotal =
728
+ Array.isArray(column.keys)
729
+ ? column.keys.reduce(
730
+ (rowSum, key) =>
731
+ rowSum +
732
+ (parseFloat(
733
+ entry[
734
+ key
735
+ ]
736
+ ) || 0),
737
+ 0
738
+ )
739
+ : 0;
740
+ return sum + subtotal;
741
+ }, 0)
742
+ .toLocaleString('en-AU', {
743
+ style: 'currency',
744
+ currency: 'AUD',
745
+ })
746
+ : index === columns.length - 2
747
+ ? 'Subtotal'
748
+ : ''}
749
+ </td>
750
+ ))}
751
+ <td></td>
752
+ </tr>
529
753
  </tbody>
530
754
  </table>
531
755
  ) : (
@@ -1,21 +1,21 @@
1
1
  .schedulingTable {
2
2
  width: 100%;
3
3
  border-collapse: collapse;
4
- margin: 1em 0;
4
+ margin: 0.5em 0; // Reduced margin
5
5
 
6
6
  th,
7
7
  td {
8
- padding: 0.25rem;
8
+ padding: 0.2rem; // Reduced padding for cells
9
9
  text-align: left;
10
10
  border: 1px solid rgba(var(--primary-rgb), 0.1);
11
11
  }
12
12
 
13
13
  th {
14
- padding: 0.75rem 1rem; /* Add more padding for better spacing */
15
- font-size: 1.1rem; /* Increase font size */
14
+ padding: 0.5rem 0.75rem; // Less padding than before
15
+ font-size: 1rem; // Slightly smaller font size
16
16
  font-weight: bold;
17
- background-color: var(--primary-color); /* Apply a background color */
18
- color: white; /* Change text color to white */
17
+ background-color: var(--primary-color);
18
+ color: white;
19
19
  text-align: center;
20
20
  }
21
21
 
@@ -25,16 +25,13 @@
25
25
  }
26
26
 
27
27
  .categoryRow {
28
- background-color: rgba(
29
- var(--primary-rgb),
30
- 0.15
31
- ); /* Lighter background for category */
32
- font-size: 1.1rem; /* Increase font size */
28
+ background-color: rgba(var(--primary-rgb), 0.15);
29
+ font-size: 1rem; // Reduced font size
33
30
  font-weight: bold;
34
31
  text-align: left;
35
- padding: 0.75rem 1rem; /* Add padding for better spacing */
36
- border-bottom: 2px solid var(--primary-color); /* Add border under category row */
37
- color: var(--primary-color); /* Set text color to primary color */
32
+ padding: 0.5rem 0.75rem; // Reduced padding
33
+ border-bottom: 2px solid var(--primary-color);
34
+ color: var(--primary-color);
38
35
  }
39
36
 
40
37
  .totalRow {
@@ -46,9 +43,9 @@
46
43
  .noDataMessage {
47
44
  text-align: center;
48
45
  color: var(--secondary-color);
49
- font-size: 1.125em;
50
- padding: 1.5em;
51
- margin: 2em auto;
46
+ font-size: 1rem;
47
+ padding: 1em;
48
+ margin: 1em auto;
52
49
  background: #f9f9f9;
53
50
  border: 1px solid #ddd;
54
51
  border-radius: 6px;
@@ -186,16 +183,13 @@
186
183
  }
187
184
 
188
185
  .subtotalRow {
189
- background-color: rgba(
190
- var(--secondary-rgb),
191
- 0.15
192
- ); /* Lighter background for subtotal */
186
+ background-color: rgba(var(--secondary-rgb), 0.15);
193
187
  font-weight: bold;
194
- text-align: right; /* Align to the right like total column */
195
- padding: 1em; /* Add padding for better spacing */
196
- font-size: 0.9rem; /* Increase font size */
197
- color: var(--primary-color); /* Set text color to primary color */
198
- border-top: 2px solid var(--primary-color); /* Add border top to separate subtotal row */
188
+ text-align: right;
189
+ padding: 0.5em; // Reduced padding
190
+ font-size: 0.8rem; // Reduced font size
191
+ color: var(--primary-color);
192
+ border-top: 2px solid var(--primary-color);
199
193
  }
200
194
 
201
195
  .colour {
@@ -210,9 +204,9 @@
210
204
  &__popover {
211
205
  position: absolute;
212
206
  z-index: 9999;
213
- left: 50%; /* Position to the center or adjust accordingly */
214
- top: 50%; /* Adjust top based on your layout */
215
- transform: translate(-50%, -50%); /* Center it */
207
+ left: 50%;
208
+ top: 50%;
209
+ transform: translate(-50%, -50%);
216
210
  }
217
211
  }
218
212
 
@@ -226,30 +220,30 @@
226
220
  flex: 1;
227
221
 
228
222
  button {
229
- width: 40px; /* Keep it compact */
230
- height: 40px; /* Square button */
231
- font-size: 1.5rem; /* Adjusted for a better icon size */
223
+ width: 40px;
224
+ height: 40px;
225
+ font-size: 1.5rem;
232
226
  font-weight: 600;
233
- color: #fff; /* White icon color */
234
- background-color: #b0b0b0; /* Neutral grey base */
235
- border: 2px solid #999; /* Light border for definition */
236
- border-radius: 8px; /* Rounded corners */
227
+ color: #fff;
228
+ background-color: #b0b0b0;
229
+ border: 2px solid #999;
230
+ border-radius: 8px;
237
231
  cursor: pointer;
238
- transition: all 0.3s ease; /* Smooth transition for hover effect */
232
+ transition: all 0.3s ease;
239
233
 
240
234
  &:hover {
241
- background-color: #999; /* Darken the background on hover */
242
- border-color: #666; /* Darken the border on hover */
235
+ background-color: #999;
236
+ border-color: #666;
243
237
  }
244
238
 
245
239
  &:focus {
246
- outline: none; /* Remove default focus outline */
247
- box-shadow: 0 0 0 2px rgba(72, 168, 58, 0.5); /* Custom focus outline */
240
+ outline: none;
241
+ box-shadow: 0 0 0 2px rgba(72, 168, 58, 0.5);
248
242
  }
249
243
 
250
244
  &:active {
251
- background-color: #888; /* Lighter grey on click */
252
- transform: scale(0.98); /* Shrink on click */
245
+ background-color: #888;
246
+ transform: scale(0.98);
253
247
  }
254
248
  }
255
249
  }