@visns-studio/visns-components 5.1.24 → 5.1.26
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.26",
|
|
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 +
|
|
@@ -228,16 +226,27 @@ const GenericEditableTable = ({
|
|
|
228
226
|
|
|
229
227
|
dataArray.forEach((entry, index) => {
|
|
230
228
|
const categoryInfo = entry[rows.categoryKey.id];
|
|
231
|
-
const
|
|
229
|
+
const categoryLabel = categoryInfo
|
|
232
230
|
? categoryInfo[rows.categoryKey.label]
|
|
233
231
|
: null;
|
|
234
232
|
const categoryId = categoryInfo ? categoryInfo.id : null;
|
|
235
233
|
const groupKey = categoryId || 'no_category';
|
|
234
|
+
// Use the category's sort_order if provided; otherwise, default to a high number (e.g. 9999)
|
|
235
|
+
|
|
236
|
+
console.info(categoryInfo);
|
|
237
|
+
|
|
238
|
+
const catSortOrder =
|
|
239
|
+
categoryInfo && typeof categoryInfo.sort_order !== 'undefined'
|
|
240
|
+
? categoryInfo.sort_order
|
|
241
|
+
: 9999;
|
|
236
242
|
|
|
237
243
|
if (!grouped[groupKey]) {
|
|
238
244
|
grouped[groupKey] = {
|
|
239
|
-
id:
|
|
240
|
-
category:
|
|
245
|
+
id: groupKey,
|
|
246
|
+
category:
|
|
247
|
+
categoryLabel ||
|
|
248
|
+
(groupKey === 'no_category' ? 'No Category' : ''),
|
|
249
|
+
catSortOrder, // assign the sort order (or fallback)
|
|
241
250
|
entries: [],
|
|
242
251
|
};
|
|
243
252
|
}
|
|
@@ -247,15 +256,23 @@ const GenericEditableTable = ({
|
|
|
247
256
|
});
|
|
248
257
|
});
|
|
249
258
|
|
|
259
|
+
// Sort entries within each group by their universal sort_order
|
|
250
260
|
Object.values(grouped).forEach((group) => {
|
|
251
261
|
group.entries.sort((a, b) => a.sort_order - b.sort_order);
|
|
252
262
|
});
|
|
253
263
|
|
|
254
|
-
//
|
|
264
|
+
// Now sort the groups by the cost category's sort_order (catSortOrder)
|
|
255
265
|
const groupedArray = Object.values(grouped);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
groupedArray.sort((a, b) => {
|
|
267
|
+
// Optionally, place 'no_category' first (or adjust as needed)
|
|
268
|
+
if (a.id === 'no_category') return -1;
|
|
269
|
+
if (b.id === 'no_category') return 1;
|
|
270
|
+
return a.catSortOrder - b.catSortOrder;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
console.info(groupedArray);
|
|
274
|
+
|
|
275
|
+
return groupedArray;
|
|
259
276
|
};
|
|
260
277
|
|
|
261
278
|
const groupedCategories = useMemo(() => {
|
|
@@ -293,7 +310,7 @@ const GenericEditableTable = ({
|
|
|
293
310
|
.catch(() => toast.error('Failed to copy to clipboard'));
|
|
294
311
|
};
|
|
295
312
|
|
|
296
|
-
// Update
|
|
313
|
+
// Update an existing field’s value
|
|
297
314
|
const handleFieldChange = (value, fieldId, keyCounter) => {
|
|
298
315
|
setLocalData((prev) => {
|
|
299
316
|
const updatedDetail = [...prev];
|
|
@@ -400,6 +417,7 @@ const GenericEditableTable = ({
|
|
|
400
417
|
});
|
|
401
418
|
};
|
|
402
419
|
|
|
420
|
+
// Render existing row field (same as before)
|
|
403
421
|
const renderScheduledTableField = (entry, column, keyCounter) => {
|
|
404
422
|
if (!entry || !column) return null;
|
|
405
423
|
|
|
@@ -524,6 +542,222 @@ const GenericEditableTable = ({
|
|
|
524
542
|
}
|
|
525
543
|
};
|
|
526
544
|
|
|
545
|
+
// --- New row functions for inline creation ---
|
|
546
|
+
|
|
547
|
+
// Update new entry fields (for grouped or ungrouped)
|
|
548
|
+
const handleNewFieldChange = (value, fieldId, groupId = null) => {
|
|
549
|
+
if (hasCategory) {
|
|
550
|
+
setNewEntries((prev) => ({
|
|
551
|
+
...prev,
|
|
552
|
+
[groupId]: {
|
|
553
|
+
...prev[groupId],
|
|
554
|
+
[fieldId]: value,
|
|
555
|
+
},
|
|
556
|
+
}));
|
|
557
|
+
} else {
|
|
558
|
+
setNewEntry((prev) => ({
|
|
559
|
+
...prev,
|
|
560
|
+
[fieldId]: value,
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// Add the new row to localData and update the backend
|
|
566
|
+
const handleAddNewEntry = (groupId = null) => {
|
|
567
|
+
const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
|
|
568
|
+
if (!newRowData || Object.keys(newRowData).length === 0) {
|
|
569
|
+
toast.error('Please fill in some data before adding.');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// Determine a new sort_order
|
|
573
|
+
const maxSortOrder = localData.reduce(
|
|
574
|
+
(max, row) => Math.max(max, row.sort_order || 0),
|
|
575
|
+
0
|
|
576
|
+
);
|
|
577
|
+
const newRow = {
|
|
578
|
+
...newRowData,
|
|
579
|
+
id: `new-${Date.now()}`, // temporary ID; adjust as needed
|
|
580
|
+
sort_order: maxSortOrder + 1,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// For grouped data, prepopulate the category field if needed
|
|
584
|
+
if (hasCategory && groupId !== 'no_category') {
|
|
585
|
+
newRow[rows.categoryKey.id] = {
|
|
586
|
+
id: groupId,
|
|
587
|
+
[rows.categoryKey.label]: groupId,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
setLocalData((prev) => {
|
|
592
|
+
const updated = [...prev, newRow];
|
|
593
|
+
debouncedUpdate({ [rows.key]: updated });
|
|
594
|
+
return updated;
|
|
595
|
+
});
|
|
596
|
+
// Reset the new entry state for the corresponding group or overall
|
|
597
|
+
if (hasCategory) {
|
|
598
|
+
setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
|
|
599
|
+
} else {
|
|
600
|
+
setNewEntry({});
|
|
601
|
+
}
|
|
602
|
+
toast.success('Row added');
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Render a new row field (similar to renderScheduledTableField)
|
|
606
|
+
const renderNewRowField = (newRowData, column, groupId = null) => {
|
|
607
|
+
const value = newRowData ? newRowData[column.id] : '';
|
|
608
|
+
switch (column.type) {
|
|
609
|
+
case 'colour':
|
|
610
|
+
return (
|
|
611
|
+
<ColourPicker
|
|
612
|
+
value={value}
|
|
613
|
+
columnId={column.id}
|
|
614
|
+
keyCounter={0}
|
|
615
|
+
onChange={(val) =>
|
|
616
|
+
handleNewFieldChange(val, column.id, groupId)
|
|
617
|
+
}
|
|
618
|
+
/>
|
|
619
|
+
);
|
|
620
|
+
case 'currency':
|
|
621
|
+
case 'number':
|
|
622
|
+
return (
|
|
623
|
+
<input
|
|
624
|
+
type="text"
|
|
625
|
+
value={value || ''}
|
|
626
|
+
onChange={(e) =>
|
|
627
|
+
handleNewFieldChange(
|
|
628
|
+
e.target.value,
|
|
629
|
+
column.id,
|
|
630
|
+
groupId
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
style={{ textAlign: 'right' }}
|
|
634
|
+
/>
|
|
635
|
+
);
|
|
636
|
+
case 'date':
|
|
637
|
+
return (
|
|
638
|
+
<input
|
|
639
|
+
type="date"
|
|
640
|
+
value={value || ''}
|
|
641
|
+
onChange={(e) =>
|
|
642
|
+
handleNewFieldChange(
|
|
643
|
+
e.target.value,
|
|
644
|
+
column.id,
|
|
645
|
+
groupId
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
style={{ textAlign: 'right' }}
|
|
649
|
+
/>
|
|
650
|
+
);
|
|
651
|
+
case 'dropdown':
|
|
652
|
+
return (
|
|
653
|
+
<select
|
|
654
|
+
value={value || ''}
|
|
655
|
+
onChange={(e) =>
|
|
656
|
+
handleNewFieldChange(
|
|
657
|
+
e.target.value,
|
|
658
|
+
column.id,
|
|
659
|
+
groupId
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
>
|
|
663
|
+
{column.options.map((option) => (
|
|
664
|
+
<option key={option.id} value={option.id}>
|
|
665
|
+
{option.label}
|
|
666
|
+
</option>
|
|
667
|
+
))}
|
|
668
|
+
</select>
|
|
669
|
+
);
|
|
670
|
+
case 'dropdown-ajax': {
|
|
671
|
+
const options = preloadedOptions[column.id] || [];
|
|
672
|
+
return (
|
|
673
|
+
<select
|
|
674
|
+
value={value || ''}
|
|
675
|
+
onChange={(e) =>
|
|
676
|
+
handleNewFieldChange(
|
|
677
|
+
e.target.value,
|
|
678
|
+
column.id,
|
|
679
|
+
groupId
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
>
|
|
683
|
+
<option>Select an Option</option>
|
|
684
|
+
{options.map((option) => (
|
|
685
|
+
<option key={option.id} value={option.id}>
|
|
686
|
+
{option.label}
|
|
687
|
+
</option>
|
|
688
|
+
))}
|
|
689
|
+
</select>
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
case 'multi-dropdown-ajax': {
|
|
693
|
+
const multiOptions = preloadedOptions[column.id] || [];
|
|
694
|
+
return (
|
|
695
|
+
<MultiSelect
|
|
696
|
+
settings={{ id: column.id, compact: true }}
|
|
697
|
+
className={styles.selectFullWidth}
|
|
698
|
+
multi={false}
|
|
699
|
+
onChange={(value) =>
|
|
700
|
+
handleNewFieldChange(value, column.id, groupId)
|
|
701
|
+
}
|
|
702
|
+
inputValue={value || ''}
|
|
703
|
+
options={multiOptions}
|
|
704
|
+
/>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
case 'total': {
|
|
708
|
+
const subtotal = calculateTotal(newRowData || {}, column.keys);
|
|
709
|
+
return new Intl.NumberFormat('en-AU', {
|
|
710
|
+
style: 'currency',
|
|
711
|
+
currency: 'AUD',
|
|
712
|
+
}).format(subtotal);
|
|
713
|
+
}
|
|
714
|
+
default:
|
|
715
|
+
return (
|
|
716
|
+
<input
|
|
717
|
+
type="text"
|
|
718
|
+
value={value || ''}
|
|
719
|
+
onChange={(e) =>
|
|
720
|
+
handleNewFieldChange(
|
|
721
|
+
e.target.value,
|
|
722
|
+
column.id,
|
|
723
|
+
groupId
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
/>
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Render the new row form (for grouped or ungrouped data)
|
|
732
|
+
const renderNewRowForm = (groupId = null) => {
|
|
733
|
+
const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
|
|
734
|
+
return (
|
|
735
|
+
<tr className={styles.newRow}>
|
|
736
|
+
{columns.map((column) => (
|
|
737
|
+
<td
|
|
738
|
+
key={column.id}
|
|
739
|
+
style={{
|
|
740
|
+
textAlign:
|
|
741
|
+
column.type === 'currency' ? 'right' : 'left',
|
|
742
|
+
}}
|
|
743
|
+
>
|
|
744
|
+
{renderNewRowField(newRowData, column, groupId)}
|
|
745
|
+
</td>
|
|
746
|
+
))}
|
|
747
|
+
<td>
|
|
748
|
+
<div className={styles.tdactions}>
|
|
749
|
+
<button
|
|
750
|
+
className={styles.btn_compact}
|
|
751
|
+
onClick={() => handleAddNewEntry(groupId)}
|
|
752
|
+
>
|
|
753
|
+
Add
|
|
754
|
+
</button>
|
|
755
|
+
</div>
|
|
756
|
+
</td>
|
|
757
|
+
</tr>
|
|
758
|
+
);
|
|
759
|
+
};
|
|
760
|
+
|
|
527
761
|
// Render header cell with a label and copy icon
|
|
528
762
|
const renderHeaderCell = (column, group = null) => (
|
|
529
763
|
<th
|
|
@@ -556,7 +790,7 @@ const GenericEditableTable = ({
|
|
|
556
790
|
return (
|
|
557
791
|
<div className={styles.gridtxt}>
|
|
558
792
|
<div className={styles.gridtxt__header}>
|
|
559
|
-
<span>{
|
|
793
|
+
<span>{title || 'Estimation Schedule'}</span>
|
|
560
794
|
</div>
|
|
561
795
|
{hasCategory ? (
|
|
562
796
|
groupedCategories.length > 0 ? (
|
|
@@ -652,6 +886,8 @@ const GenericEditableTable = ({
|
|
|
652
886
|
</td>
|
|
653
887
|
</tr>
|
|
654
888
|
))}
|
|
889
|
+
{/* Render the inline new row for this category */}
|
|
890
|
+
{renderNewRowForm(group.id)}
|
|
655
891
|
{/* Render subtotal row for group only if a total column exists */}
|
|
656
892
|
{totalColumnIndex !== -1 && (
|
|
657
893
|
<tr className={styles.subtotalRow}>
|
|
@@ -781,6 +1017,8 @@ const GenericEditableTable = ({
|
|
|
781
1017
|
</td>
|
|
782
1018
|
</tr>
|
|
783
1019
|
))}
|
|
1020
|
+
{/* Render the inline new row at the bottom */}
|
|
1021
|
+
{renderNewRowForm()}
|
|
784
1022
|
{/* Render overall subtotal row only if a total column exists */}
|
|
785
1023
|
{totalColumnIndex !== -1 && (
|
|
786
1024
|
<tr className={styles.subtotalRow}>
|
|
@@ -829,17 +1067,6 @@ const GenericEditableTable = ({
|
|
|
829
1067
|
reloading the data.
|
|
830
1068
|
</div>
|
|
831
1069
|
)}
|
|
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
1070
|
</div>
|
|
844
1071
|
);
|
|
845
1072
|
};
|
|
@@ -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
|
+
}
|