@visns-studio/visns-components 5.1.24 → 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.
|
|
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": [
|
|
@@ -124,14 +124,16 @@ const GenericEditableTable = ({
|
|
|
124
124
|
schedulingConfig,
|
|
125
125
|
data,
|
|
126
126
|
dataId,
|
|
127
|
-
modalOpen,
|
|
128
127
|
preloadedOptions,
|
|
129
128
|
setData,
|
|
130
129
|
}) => {
|
|
131
|
-
const { columns, rows, form } = schedulingConfig;
|
|
130
|
+
const { columns, rows, form, title } = schedulingConfig;
|
|
132
131
|
const hasCategory = rows && rows.categoryKey; // Only group if defined
|
|
133
132
|
|
|
134
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({});
|
|
135
137
|
|
|
136
138
|
// Determine if a total column exists and its index.
|
|
137
139
|
const totalColumnIndex = columns.findIndex((col) => col.type === 'total');
|
|
@@ -152,7 +154,6 @@ const GenericEditableTable = ({
|
|
|
152
154
|
const calculateTotal = (entry, keys) => {
|
|
153
155
|
let total = 0;
|
|
154
156
|
if (Array.isArray(keys)) {
|
|
155
|
-
// Legacy format: simply sum up the values.
|
|
156
157
|
total = keys.reduce(
|
|
157
158
|
(sum, key) => sum + (parseFloat(entry[key]) || 0),
|
|
158
159
|
0
|
|
@@ -165,7 +166,6 @@ const GenericEditableTable = ({
|
|
|
165
166
|
) {
|
|
166
167
|
const priceKeys = keys.price;
|
|
167
168
|
const unitKeys = keys.unit;
|
|
168
|
-
// If there's one unit key, use its value for all price keys.
|
|
169
169
|
if (unitKeys.length === 1) {
|
|
170
170
|
const unitValue = parseFloat(entry[unitKeys[0]]) || 0;
|
|
171
171
|
total = priceKeys.reduce(
|
|
@@ -173,9 +173,7 @@ const GenericEditableTable = ({
|
|
|
173
173
|
sum + (parseFloat(entry[key]) || 0) * unitValue,
|
|
174
174
|
0
|
|
175
175
|
);
|
|
176
|
-
}
|
|
177
|
-
// Otherwise, if both arrays have the same length, multiply pair‑wise.
|
|
178
|
-
else if (priceKeys.length === unitKeys.length) {
|
|
176
|
+
} else if (priceKeys.length === unitKeys.length) {
|
|
179
177
|
total = priceKeys.reduce(
|
|
180
178
|
(sum, key, index) =>
|
|
181
179
|
sum +
|
|
@@ -236,8 +234,10 @@ const GenericEditableTable = ({
|
|
|
236
234
|
|
|
237
235
|
if (!grouped[groupKey]) {
|
|
238
236
|
grouped[groupKey] = {
|
|
239
|
-
id:
|
|
240
|
-
category:
|
|
237
|
+
id: groupKey,
|
|
238
|
+
category:
|
|
239
|
+
categoryType ||
|
|
240
|
+
(groupKey === 'no_category' ? 'No Category' : ''),
|
|
241
241
|
entries: [],
|
|
242
242
|
};
|
|
243
243
|
}
|
|
@@ -293,7 +293,7 @@ const GenericEditableTable = ({
|
|
|
293
293
|
.catch(() => toast.error('Failed to copy to clipboard'));
|
|
294
294
|
};
|
|
295
295
|
|
|
296
|
-
// Update
|
|
296
|
+
// Update an existing field’s value
|
|
297
297
|
const handleFieldChange = (value, fieldId, keyCounter) => {
|
|
298
298
|
setLocalData((prev) => {
|
|
299
299
|
const updatedDetail = [...prev];
|
|
@@ -400,6 +400,7 @@ const GenericEditableTable = ({
|
|
|
400
400
|
});
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
// Render existing row field (same as before)
|
|
403
404
|
const renderScheduledTableField = (entry, column, keyCounter) => {
|
|
404
405
|
if (!entry || !column) return null;
|
|
405
406
|
|
|
@@ -524,6 +525,222 @@ const GenericEditableTable = ({
|
|
|
524
525
|
}
|
|
525
526
|
};
|
|
526
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
|
+
|
|
527
744
|
// Render header cell with a label and copy icon
|
|
528
745
|
const renderHeaderCell = (column, group = null) => (
|
|
529
746
|
<th
|
|
@@ -556,7 +773,7 @@ const GenericEditableTable = ({
|
|
|
556
773
|
return (
|
|
557
774
|
<div className={styles.gridtxt}>
|
|
558
775
|
<div className={styles.gridtxt__header}>
|
|
559
|
-
<span>{
|
|
776
|
+
<span>{title || 'Estimation Schedule'}</span>
|
|
560
777
|
</div>
|
|
561
778
|
{hasCategory ? (
|
|
562
779
|
groupedCategories.length > 0 ? (
|
|
@@ -652,6 +869,8 @@ const GenericEditableTable = ({
|
|
|
652
869
|
</td>
|
|
653
870
|
</tr>
|
|
654
871
|
))}
|
|
872
|
+
{/* Render the inline new row for this category */}
|
|
873
|
+
{renderNewRowForm(group.id)}
|
|
655
874
|
{/* Render subtotal row for group only if a total column exists */}
|
|
656
875
|
{totalColumnIndex !== -1 && (
|
|
657
876
|
<tr className={styles.subtotalRow}>
|
|
@@ -781,6 +1000,8 @@ const GenericEditableTable = ({
|
|
|
781
1000
|
</td>
|
|
782
1001
|
</tr>
|
|
783
1002
|
))}
|
|
1003
|
+
{/* Render the inline new row at the bottom */}
|
|
1004
|
+
{renderNewRowForm()}
|
|
784
1005
|
{/* Render overall subtotal row only if a total column exists */}
|
|
785
1006
|
{totalColumnIndex !== -1 && (
|
|
786
1007
|
<tr className={styles.subtotalRow}>
|
|
@@ -829,17 +1050,6 @@ const GenericEditableTable = ({
|
|
|
829
1050
|
reloading the data.
|
|
830
1051
|
</div>
|
|
831
1052
|
)}
|
|
832
|
-
|
|
833
|
-
<div className={styles.polActions}>
|
|
834
|
-
<button
|
|
835
|
-
className={styles.btn}
|
|
836
|
-
onClick={() => {
|
|
837
|
-
modalOpen('create', 0);
|
|
838
|
-
}}
|
|
839
|
-
>
|
|
840
|
-
Add
|
|
841
|
-
</button>
|
|
842
|
-
</div>
|
|
843
1053
|
</div>
|
|
844
1054
|
);
|
|
845
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
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 {
|
|
@@ -326,3 +349,12 @@
|
|
|
326
349
|
.react-select__indicators {
|
|
327
350
|
padding: 0 0.25rem;
|
|
328
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
|
+
}
|