@visns-studio/visns-components 5.1.22 → 5.1.24

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.22",
80
+ "version": "5.1.24",
81
81
  "description": "Various packages to assist in the development of our Custom Applications.",
82
82
  "main": "src/index.js",
83
83
  "files": [
@@ -15,7 +15,6 @@ import styles from './styles/GenericEditableTable.module.scss';
15
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
- const colorPickerRef = useRef(null);
19
18
  const buttonRef = useRef(null);
20
19
  const popoverRef = useRef(null);
21
20
 
@@ -54,20 +53,23 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
54
53
  ? buttonRef.current.getBoundingClientRect()
55
54
  : {};
56
55
 
56
+ const handleClear = () => {
57
+ // Passing an empty string to represent "no colour selected"
58
+ onChange('', columnId, keyCounter);
59
+ setColorPickerShow(false);
60
+ };
61
+
57
62
  return (
58
63
  <div className={styles.cpicker}>
59
- <div className={styles.cpicker__btn} ref={colorPickerRef}>
64
+ <div className={styles.cpicker__btn}>
60
65
  <button
61
66
  ref={buttonRef}
62
- style={
63
- value && value !== 'null' ? { background: value } : {}
64
- }
67
+ style={value ? { background: value } : {}}
65
68
  onClick={(e) => {
66
69
  e.preventDefault();
67
70
  setColorPickerShow(!colorPickerShow);
68
71
  }}
69
72
  >
70
- {/* Using the Akar Water icon with increased size and some margin */}
71
73
  <Water size={20} style={{ color: 'black' }} />
72
74
  </button>
73
75
 
@@ -87,14 +89,29 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
87
89
  className={styles.colour_cover}
88
90
  onClick={() => setColorPickerShow(false)}
89
91
  />
90
- <CompactPicker
91
- color={value || ''}
92
- colors={pastelColors}
93
- onChangeComplete={(color) => {
94
- onChange(color.hex, columnId, keyCounter);
95
- setColorPickerShow(false);
96
- }}
97
- />
92
+ <div className={styles.pickerContainer}>
93
+ <CompactPicker
94
+ color={value || ''}
95
+ colors={pastelColors}
96
+ onChangeComplete={(color) => {
97
+ onChange(
98
+ color.hex,
99
+ columnId,
100
+ keyCounter
101
+ );
102
+ setColorPickerShow(false);
103
+ }}
104
+ />
105
+ {/* Extra clear button added below the CompactPicker */}
106
+ <div className={styles.clearContainer}>
107
+ <button
108
+ onClick={handleClear}
109
+ className={styles.clearButton}
110
+ >
111
+ Clear
112
+ </button>
113
+ </div>
114
+ </div>
98
115
  </div>,
99
116
  document.body
100
117
  )}
@@ -116,6 +133,9 @@ const GenericEditableTable = ({
116
133
 
117
134
  const [localData, setLocalData] = useState([]);
118
135
 
136
+ // Determine if a total column exists and its index.
137
+ const totalColumnIndex = columns.findIndex((col) => col.type === 'total');
138
+
119
139
  // Helper to sort by sort_order without mutating the original array
120
140
  const sortBySortOrder = (dataArray) =>
121
141
  [...dataArray].sort((a, b) => a.sort_order - b.sort_order);
@@ -128,6 +148,46 @@ const GenericEditableTable = ({
128
148
  : undefined;
129
149
  };
130
150
 
151
+ // New helper to calculate total using the updated keys structure
152
+ const calculateTotal = (entry, keys) => {
153
+ let total = 0;
154
+ if (Array.isArray(keys)) {
155
+ // Legacy format: simply sum up the values.
156
+ total = keys.reduce(
157
+ (sum, key) => sum + (parseFloat(entry[key]) || 0),
158
+ 0
159
+ );
160
+ } else if (
161
+ keys &&
162
+ typeof keys === 'object' &&
163
+ keys.price &&
164
+ keys.unit
165
+ ) {
166
+ const priceKeys = keys.price;
167
+ const unitKeys = keys.unit;
168
+ // If there's one unit key, use its value for all price keys.
169
+ if (unitKeys.length === 1) {
170
+ const unitValue = parseFloat(entry[unitKeys[0]]) || 0;
171
+ total = priceKeys.reduce(
172
+ (sum, key) =>
173
+ sum + (parseFloat(entry[key]) || 0) * unitValue,
174
+ 0
175
+ );
176
+ }
177
+ // Otherwise, if both arrays have the same length, multiply pair‑wise.
178
+ else if (priceKeys.length === unitKeys.length) {
179
+ total = priceKeys.reduce(
180
+ (sum, key, index) =>
181
+ sum +
182
+ (parseFloat(entry[key]) || 0) *
183
+ (parseFloat(entry[unitKeys[index]]) || 0),
184
+ 0
185
+ );
186
+ }
187
+ }
188
+ return total;
189
+ };
190
+
131
191
  // On load, enrich the data and sort by sort_order
132
192
  useEffect(() => {
133
193
  const enrichedData = data[rows.key]?.map((entry, index) => ({
@@ -441,10 +501,7 @@ const GenericEditableTable = ({
441
501
  );
442
502
  }
443
503
  case 'total': {
444
- const subtotal = column.keys.reduce(
445
- (sum, key) => sum + (parseFloat(entry[key]) || 0),
446
- 0
447
- );
504
+ const subtotal = calculateTotal(entry, column.keys);
448
505
  return new Intl.NumberFormat('en-AU', {
449
506
  style: 'currency',
450
507
  currency: 'AUD',
@@ -595,6 +652,54 @@ const GenericEditableTable = ({
595
652
  </td>
596
653
  </tr>
597
654
  ))}
655
+ {/* Render subtotal row for group only if a total column exists */}
656
+ {totalColumnIndex !== -1 && (
657
+ <tr className={styles.subtotalRow}>
658
+ {columns.map((col, index) => {
659
+ if (
660
+ index ===
661
+ totalColumnIndex - 1 &&
662
+ totalColumnIndex > 0
663
+ ) {
664
+ return (
665
+ <td key={col.id}>
666
+ Subtotal
667
+ </td>
668
+ );
669
+ }
670
+ if (
671
+ index === totalColumnIndex
672
+ ) {
673
+ const groupTotal =
674
+ group.entries.reduce(
675
+ (sum, entry) =>
676
+ sum +
677
+ calculateTotal(
678
+ entry,
679
+ columns[
680
+ totalColumnIndex
681
+ ].keys
682
+ ),
683
+ 0
684
+ );
685
+ return (
686
+ <td key={col.id}>
687
+ {groupTotal.toLocaleString(
688
+ 'en-AU',
689
+ {
690
+ style: 'currency',
691
+ currency:
692
+ 'AUD',
693
+ }
694
+ )}
695
+ </td>
696
+ );
697
+ }
698
+ return <td key={col.id} />;
699
+ })}
700
+ <td></td>
701
+ </tr>
702
+ )}
598
703
  </React.Fragment>
599
704
  ))}
600
705
  </tbody>
@@ -676,38 +781,46 @@ const GenericEditableTable = ({
676
781
  </td>
677
782
  </tr>
678
783
  ))}
679
- <tr className={styles.subtotalRow}>
680
- {columns.map((column, index) => (
681
- <td key={column.id}>
682
- {index === columns.length - 1
683
- ? sortBySortOrder(localData)
684
- .reduce((sum, entry) => {
685
- const subtotal =
686
- Array.isArray(column.keys)
687
- ? column.keys.reduce(
688
- (rowSum, key) =>
689
- rowSum +
690
- (parseFloat(
691
- entry[
692
- key
693
- ]
694
- ) || 0),
695
- 0
696
- )
697
- : 0;
698
- return sum + subtotal;
699
- }, 0)
700
- .toLocaleString('en-AU', {
701
- style: 'currency',
702
- currency: 'AUD',
703
- })
704
- : index === columns.length - 2
705
- ? 'Subtotal'
706
- : ''}
707
- </td>
708
- ))}
709
- <td></td>
710
- </tr>
784
+ {/* Render overall subtotal row only if a total column exists */}
785
+ {totalColumnIndex !== -1 && (
786
+ <tr className={styles.subtotalRow}>
787
+ {columns.map((col, index) => {
788
+ if (
789
+ index === totalColumnIndex - 1 &&
790
+ totalColumnIndex > 0
791
+ ) {
792
+ return <td key={col.id}>Subtotal</td>;
793
+ }
794
+ if (index === totalColumnIndex) {
795
+ const overallTotal = sortBySortOrder(
796
+ localData
797
+ ).reduce(
798
+ (sum, entry) =>
799
+ sum +
800
+ calculateTotal(
801
+ entry,
802
+ columns[totalColumnIndex]
803
+ .keys
804
+ ),
805
+ 0
806
+ );
807
+ return (
808
+ <td key={col.id}>
809
+ {overallTotal.toLocaleString(
810
+ 'en-AU',
811
+ {
812
+ style: 'currency',
813
+ currency: 'AUD',
814
+ }
815
+ )}
816
+ </td>
817
+ );
818
+ }
819
+ return <td key={col.id} />;
820
+ })}
821
+ <td></td>
822
+ </tr>
823
+ )}
711
824
  </tbody>
712
825
  </table>
713
826
  ) : (
@@ -223,40 +223,79 @@
223
223
  }
224
224
 
225
225
  .cpicker {
226
- width: 100%;
226
+ position: relative;
227
227
  display: flex;
228
- flex-wrap: nowrap;
229
228
  align-items: center;
230
229
 
231
230
  &__btn {
232
- flex: 1;
233
-
234
231
  button {
235
- width: 32px;
236
- height: 32px;
237
- font-size: 1.5rem;
238
- font-weight: 600;
239
- color: #fff;
240
- background-color: #b0b0b0;
241
- border: 2px solid #999;
242
- border-radius: 8px;
232
+ width: 28px;
233
+ height: 28px;
234
+ background-color: #f0f0f0;
235
+ border: 1px solid #ccc;
236
+ border-radius: 6px;
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
243
240
  cursor: pointer;
244
- transition: all 0.3s ease;
241
+ transition: background 0.2s;
245
242
 
246
243
  &:hover {
247
- background-color: #999;
248
- border-color: #666;
244
+ background-color: #e0e0e0;
249
245
  }
250
246
 
251
247
  &:focus {
252
248
  outline: none;
253
- box-shadow: 0 0 0 2px rgba(72, 168, 58, 0.5);
249
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
254
250
  }
251
+ }
252
+ }
253
+ }
255
254
 
256
- &:active {
257
- background-color: #888;
258
- transform: scale(0.98);
259
- }
255
+ .colour_popover {
256
+ position: absolute;
257
+ background: white;
258
+ border-radius: 8px;
259
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
260
+ padding: 10px;
261
+ min-width: 200px;
262
+ display: flex;
263
+ flex-direction: column;
264
+ align-items: center;
265
+ z-index: 1000;
266
+
267
+ .clearContainer {
268
+ width: 100%;
269
+ display: flex;
270
+ justify-content: center;
271
+ margin-top: 8px;
272
+ }
273
+
274
+ .clearButton {
275
+ width: 100%; /* Make button span the entire width */
276
+ background: rgba(
277
+ var(--primary-rgb),
278
+ 0.1
279
+ ); /* Soft transparent background */
280
+ color: var(--primary-color);
281
+ border: none;
282
+ border-top: 1px solid rgba(var(--primary-rgb), 0.2); /* Subtle separator */
283
+ border-radius: 0 0 8px 8px; /* Match colour picker's bottom corners */
284
+ font-size: 13px;
285
+ padding: 8px;
286
+ cursor: pointer;
287
+ transition: background 0.2s, border 0.2s, box-shadow 0.2s;
288
+ font-weight: 500;
289
+ text-align: center;
290
+ box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.05);
291
+
292
+ &:hover {
293
+ background: rgba(var(--primary-rgb), 0.15);
294
+ }
295
+
296
+ &:active {
297
+ background: rgba(var(--primary-rgb), 0.25);
298
+ box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.15);
260
299
  }
261
300
  }
262
301
  }