@visns-studio/visns-components 5.1.23 → 5.1.25

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.23",
80
+ "version": "5.1.25",
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
  )}
@@ -107,14 +124,16 @@ const GenericEditableTable = ({
107
124
  schedulingConfig,
108
125
  data,
109
126
  dataId,
110
- modalOpen,
111
127
  preloadedOptions,
112
128
  setData,
113
129
  }) => {
114
- const { columns, rows, form } = schedulingConfig;
130
+ const { columns, rows, form, title } = schedulingConfig;
115
131
  const hasCategory = rows && rows.categoryKey; // Only group if defined
116
132
 
117
133
  const [localData, setLocalData] = useState([]);
134
+ // For new (unsaved) entries: if grouped, store per group; if not, a single object.
135
+ const [newEntries, setNewEntries] = useState({});
136
+ const [newEntry, setNewEntry] = useState({});
118
137
 
119
138
  // Determine if a total column exists and its index.
120
139
  const totalColumnIndex = columns.findIndex((col) => col.type === 'total');
@@ -135,7 +154,6 @@ const GenericEditableTable = ({
135
154
  const calculateTotal = (entry, keys) => {
136
155
  let total = 0;
137
156
  if (Array.isArray(keys)) {
138
- // Legacy format: simply sum up the values.
139
157
  total = keys.reduce(
140
158
  (sum, key) => sum + (parseFloat(entry[key]) || 0),
141
159
  0
@@ -148,7 +166,6 @@ const GenericEditableTable = ({
148
166
  ) {
149
167
  const priceKeys = keys.price;
150
168
  const unitKeys = keys.unit;
151
- // If there's one unit key, use its value for all price keys.
152
169
  if (unitKeys.length === 1) {
153
170
  const unitValue = parseFloat(entry[unitKeys[0]]) || 0;
154
171
  total = priceKeys.reduce(
@@ -156,9 +173,7 @@ const GenericEditableTable = ({
156
173
  sum + (parseFloat(entry[key]) || 0) * unitValue,
157
174
  0
158
175
  );
159
- }
160
- // Otherwise, if both arrays have the same length, multiply pair‑wise.
161
- else if (priceKeys.length === unitKeys.length) {
176
+ } else if (priceKeys.length === unitKeys.length) {
162
177
  total = priceKeys.reduce(
163
178
  (sum, key, index) =>
164
179
  sum +
@@ -219,8 +234,10 @@ const GenericEditableTable = ({
219
234
 
220
235
  if (!grouped[groupKey]) {
221
236
  grouped[groupKey] = {
222
- id: categoryId || 'no_category',
223
- category: categoryType || '',
237
+ id: groupKey,
238
+ category:
239
+ categoryType ||
240
+ (groupKey === 'no_category' ? 'No Category' : ''),
224
241
  entries: [],
225
242
  };
226
243
  }
@@ -276,7 +293,7 @@ const GenericEditableTable = ({
276
293
  .catch(() => toast.error('Failed to copy to clipboard'));
277
294
  };
278
295
 
279
- // Update a field’s value (keyCounter corresponds to the row index in sorted data)
296
+ // Update an existing field’s value
280
297
  const handleFieldChange = (value, fieldId, keyCounter) => {
281
298
  setLocalData((prev) => {
282
299
  const updatedDetail = [...prev];
@@ -383,6 +400,7 @@ const GenericEditableTable = ({
383
400
  });
384
401
  };
385
402
 
403
+ // Render existing row field (same as before)
386
404
  const renderScheduledTableField = (entry, column, keyCounter) => {
387
405
  if (!entry || !column) return null;
388
406
 
@@ -507,6 +525,222 @@ const GenericEditableTable = ({
507
525
  }
508
526
  };
509
527
 
528
+ // --- New row functions for inline creation ---
529
+
530
+ // Update new entry fields (for grouped or ungrouped)
531
+ const handleNewFieldChange = (value, fieldId, groupId = null) => {
532
+ if (hasCategory) {
533
+ setNewEntries((prev) => ({
534
+ ...prev,
535
+ [groupId]: {
536
+ ...prev[groupId],
537
+ [fieldId]: value,
538
+ },
539
+ }));
540
+ } else {
541
+ setNewEntry((prev) => ({
542
+ ...prev,
543
+ [fieldId]: value,
544
+ }));
545
+ }
546
+ };
547
+
548
+ // Add the new row to localData and update the backend
549
+ const handleAddNewEntry = (groupId = null) => {
550
+ const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
551
+ if (!newRowData || Object.keys(newRowData).length === 0) {
552
+ toast.error('Please fill in some data before adding.');
553
+ return;
554
+ }
555
+ // Determine a new sort_order
556
+ const maxSortOrder = localData.reduce(
557
+ (max, row) => Math.max(max, row.sort_order || 0),
558
+ 0
559
+ );
560
+ const newRow = {
561
+ ...newRowData,
562
+ id: `new-${Date.now()}`, // temporary ID; adjust as needed
563
+ sort_order: maxSortOrder + 1,
564
+ };
565
+
566
+ // For grouped data, prepopulate the category field if needed
567
+ if (hasCategory && groupId !== 'no_category') {
568
+ newRow[rows.categoryKey.id] = {
569
+ id: groupId,
570
+ [rows.categoryKey.label]: groupId,
571
+ };
572
+ }
573
+
574
+ setLocalData((prev) => {
575
+ const updated = [...prev, newRow];
576
+ debouncedUpdate({ [rows.key]: updated });
577
+ return updated;
578
+ });
579
+ // Reset the new entry state for the corresponding group or overall
580
+ if (hasCategory) {
581
+ setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
582
+ } else {
583
+ setNewEntry({});
584
+ }
585
+ toast.success('Row added');
586
+ };
587
+
588
+ // Render a new row field (similar to renderScheduledTableField)
589
+ const renderNewRowField = (newRowData, column, groupId = null) => {
590
+ const value = newRowData ? newRowData[column.id] : '';
591
+ switch (column.type) {
592
+ case 'colour':
593
+ return (
594
+ <ColourPicker
595
+ value={value}
596
+ columnId={column.id}
597
+ keyCounter={0}
598
+ onChange={(val) =>
599
+ handleNewFieldChange(val, column.id, groupId)
600
+ }
601
+ />
602
+ );
603
+ case 'currency':
604
+ case 'number':
605
+ return (
606
+ <input
607
+ type="text"
608
+ value={value || ''}
609
+ onChange={(e) =>
610
+ handleNewFieldChange(
611
+ e.target.value,
612
+ column.id,
613
+ groupId
614
+ )
615
+ }
616
+ style={{ textAlign: 'right' }}
617
+ />
618
+ );
619
+ case 'date':
620
+ return (
621
+ <input
622
+ type="date"
623
+ value={value || ''}
624
+ onChange={(e) =>
625
+ handleNewFieldChange(
626
+ e.target.value,
627
+ column.id,
628
+ groupId
629
+ )
630
+ }
631
+ style={{ textAlign: 'right' }}
632
+ />
633
+ );
634
+ case 'dropdown':
635
+ return (
636
+ <select
637
+ value={value || ''}
638
+ onChange={(e) =>
639
+ handleNewFieldChange(
640
+ e.target.value,
641
+ column.id,
642
+ groupId
643
+ )
644
+ }
645
+ >
646
+ {column.options.map((option) => (
647
+ <option key={option.id} value={option.id}>
648
+ {option.label}
649
+ </option>
650
+ ))}
651
+ </select>
652
+ );
653
+ case 'dropdown-ajax': {
654
+ const options = preloadedOptions[column.id] || [];
655
+ return (
656
+ <select
657
+ value={value || ''}
658
+ onChange={(e) =>
659
+ handleNewFieldChange(
660
+ e.target.value,
661
+ column.id,
662
+ groupId
663
+ )
664
+ }
665
+ >
666
+ <option>Select an Option</option>
667
+ {options.map((option) => (
668
+ <option key={option.id} value={option.id}>
669
+ {option.label}
670
+ </option>
671
+ ))}
672
+ </select>
673
+ );
674
+ }
675
+ case 'multi-dropdown-ajax': {
676
+ const multiOptions = preloadedOptions[column.id] || [];
677
+ return (
678
+ <MultiSelect
679
+ settings={{ id: column.id, compact: true }}
680
+ className={styles.selectFullWidth}
681
+ multi={false}
682
+ onChange={(value) =>
683
+ handleNewFieldChange(value, column.id, groupId)
684
+ }
685
+ inputValue={value || ''}
686
+ options={multiOptions}
687
+ />
688
+ );
689
+ }
690
+ case 'total': {
691
+ const subtotal = calculateTotal(newRowData || {}, column.keys);
692
+ return new Intl.NumberFormat('en-AU', {
693
+ style: 'currency',
694
+ currency: 'AUD',
695
+ }).format(subtotal);
696
+ }
697
+ default:
698
+ return (
699
+ <input
700
+ type="text"
701
+ value={value || ''}
702
+ onChange={(e) =>
703
+ handleNewFieldChange(
704
+ e.target.value,
705
+ column.id,
706
+ groupId
707
+ )
708
+ }
709
+ />
710
+ );
711
+ }
712
+ };
713
+
714
+ // Render the new row form (for grouped or ungrouped data)
715
+ const renderNewRowForm = (groupId = null) => {
716
+ const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
717
+ return (
718
+ <tr className={styles.newRow}>
719
+ {columns.map((column) => (
720
+ <td
721
+ key={column.id}
722
+ style={{
723
+ textAlign:
724
+ column.type === 'currency' ? 'right' : 'left',
725
+ }}
726
+ >
727
+ {renderNewRowField(newRowData, column, groupId)}
728
+ </td>
729
+ ))}
730
+ <td>
731
+ <div className={styles.tdactions}>
732
+ <button
733
+ className={styles.btn_compact}
734
+ onClick={() => handleAddNewEntry(groupId)}
735
+ >
736
+ Add
737
+ </button>
738
+ </div>
739
+ </td>
740
+ </tr>
741
+ );
742
+ };
743
+
510
744
  // Render header cell with a label and copy icon
511
745
  const renderHeaderCell = (column, group = null) => (
512
746
  <th
@@ -539,7 +773,7 @@ const GenericEditableTable = ({
539
773
  return (
540
774
  <div className={styles.gridtxt}>
541
775
  <div className={styles.gridtxt__header}>
542
- <span>{schedulingConfig.title || 'Estimation Schedule'}</span>
776
+ <span>{title || 'Estimation Schedule'}</span>
543
777
  </div>
544
778
  {hasCategory ? (
545
779
  groupedCategories.length > 0 ? (
@@ -635,6 +869,8 @@ const GenericEditableTable = ({
635
869
  </td>
636
870
  </tr>
637
871
  ))}
872
+ {/* Render the inline new row for this category */}
873
+ {renderNewRowForm(group.id)}
638
874
  {/* Render subtotal row for group only if a total column exists */}
639
875
  {totalColumnIndex !== -1 && (
640
876
  <tr className={styles.subtotalRow}>
@@ -764,6 +1000,8 @@ const GenericEditableTable = ({
764
1000
  </td>
765
1001
  </tr>
766
1002
  ))}
1003
+ {/* Render the inline new row at the bottom */}
1004
+ {renderNewRowForm()}
767
1005
  {/* Render overall subtotal row only if a total column exists */}
768
1006
  {totalColumnIndex !== -1 && (
769
1007
  <tr className={styles.subtotalRow}>
@@ -812,17 +1050,6 @@ const GenericEditableTable = ({
812
1050
  reloading the data.
813
1051
  </div>
814
1052
  )}
815
-
816
- <div className={styles.polActions}>
817
- <button
818
- className={styles.btn}
819
- onClick={() => {
820
- modalOpen('create', 0);
821
- }}
822
- >
823
- Add
824
- </button>
825
- </div>
826
1053
  </div>
827
1054
  );
828
1055
  };
@@ -144,12 +144,35 @@
144
144
  outline: none;
145
145
  transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
146
146
  font-size: 1.25em;
147
+
148
+ &:hover {
149
+ color: var(--primary-color);
150
+ background: var(--highlight-color);
151
+ border: 1px solid rgba(var(--highlight-rgb), 1.05);
152
+ }
147
153
  }
148
154
 
149
- .btn:hover {
150
- color: var(--primary-color);
151
- background: var(--highlight-color);
152
- border: 1px solid rgba(var(--highlight-rgb), 1.05);
155
+ .btn_compact {
156
+ width: max-content;
157
+ display: inline-block;
158
+ position: relative;
159
+ padding: 0.4rem 0.75rem; /* Reduced padding for a compact look */
160
+ cursor: pointer;
161
+ font-size: 1rem; /* Slightly smaller font size */
162
+ color: var(--tertiary-color);
163
+ text-decoration: none;
164
+ overflow: hidden;
165
+ background: var(--primary-color);
166
+ border: 1px solid rgba(var(--primary-color--rgb), 1.1);
167
+ border-radius: var(--br);
168
+ outline: none;
169
+ transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
170
+
171
+ &:hover {
172
+ color: var(--primary-color);
173
+ background: var(--highlight-color);
174
+ border: 1px solid rgba(var(--highlight-rgb), 1.05);
175
+ }
153
176
  }
154
177
 
155
178
  .tdactions {
@@ -223,40 +246,79 @@
223
246
  }
224
247
 
225
248
  .cpicker {
226
- width: 100%;
249
+ position: relative;
227
250
  display: flex;
228
- flex-wrap: nowrap;
229
251
  align-items: center;
230
252
 
231
253
  &__btn {
232
- flex: 1;
233
-
234
254
  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;
255
+ width: 28px;
256
+ height: 28px;
257
+ background-color: #f0f0f0;
258
+ border: 1px solid #ccc;
259
+ border-radius: 6px;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
243
263
  cursor: pointer;
244
- transition: all 0.3s ease;
264
+ transition: background 0.2s;
245
265
 
246
266
  &:hover {
247
- background-color: #999;
248
- border-color: #666;
267
+ background-color: #e0e0e0;
249
268
  }
250
269
 
251
270
  &:focus {
252
271
  outline: none;
253
- box-shadow: 0 0 0 2px rgba(72, 168, 58, 0.5);
272
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
254
273
  }
274
+ }
275
+ }
276
+ }
255
277
 
256
- &:active {
257
- background-color: #888;
258
- transform: scale(0.98);
259
- }
278
+ .colour_popover {
279
+ position: absolute;
280
+ background: white;
281
+ border-radius: 8px;
282
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
283
+ padding: 10px;
284
+ min-width: 200px;
285
+ display: flex;
286
+ flex-direction: column;
287
+ align-items: center;
288
+ z-index: 1000;
289
+
290
+ .clearContainer {
291
+ width: 100%;
292
+ display: flex;
293
+ justify-content: center;
294
+ margin-top: 8px;
295
+ }
296
+
297
+ .clearButton {
298
+ width: 100%; /* Make button span the entire width */
299
+ background: rgba(
300
+ var(--primary-rgb),
301
+ 0.1
302
+ ); /* Soft transparent background */
303
+ color: var(--primary-color);
304
+ border: none;
305
+ border-top: 1px solid rgba(var(--primary-rgb), 0.2); /* Subtle separator */
306
+ border-radius: 0 0 8px 8px; /* Match colour picker's bottom corners */
307
+ font-size: 13px;
308
+ padding: 8px;
309
+ cursor: pointer;
310
+ transition: background 0.2s, border 0.2s, box-shadow 0.2s;
311
+ font-weight: 500;
312
+ text-align: center;
313
+ box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.05);
314
+
315
+ &:hover {
316
+ background: rgba(var(--primary-rgb), 0.15);
317
+ }
318
+
319
+ &:active {
320
+ background: rgba(var(--primary-rgb), 0.25);
321
+ box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.15);
260
322
  }
261
323
  }
262
324
  }
@@ -287,3 +349,12 @@
287
349
  .react-select__indicators {
288
350
  padding: 0 0.25rem;
289
351
  }
352
+
353
+ .newRow {
354
+ background-color: rgba(
355
+ var(--primary-rgb),
356
+ 0.15
357
+ ); /* Slightly lighter background */
358
+ border-top: 1px dashed rgba(var(--primary-rgb), 0.3);
359
+ border-bottom: 1px dashed rgba(var(--primary-rgb), 0.3);
360
+ }