@visns-studio/visns-components 5.3.3 → 5.3.6
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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"akar-icons": "^1.9.31",
|
|
15
15
|
"array-move": "^4.0.0",
|
|
16
16
|
"awesome-debounce-promise": "^2.1.0",
|
|
17
|
-
"axios": "^1.8.
|
|
17
|
+
"axios": "^1.8.3",
|
|
18
18
|
"dayjs": "^1.11.13",
|
|
19
19
|
"fabric": "^6.5.4",
|
|
20
20
|
"file-saver": "^2.0.5",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
79
79
|
},
|
|
80
80
|
"name": "@visns-studio/visns-components",
|
|
81
|
-
"version": "5.3.
|
|
81
|
+
"version": "5.3.6",
|
|
82
82
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
83
83
|
"main": "src/index.js",
|
|
84
84
|
"files": [
|
|
@@ -112,10 +112,14 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (widget.api?.url && widget.api?.method) {
|
|
115
|
+
const requestParams = {
|
|
116
|
+
...widgetFilters,
|
|
117
|
+
...(widget.api.params || {}),
|
|
118
|
+
};
|
|
115
119
|
return CustomFetch(
|
|
116
120
|
widget.api.url,
|
|
117
121
|
widget.api.method,
|
|
118
|
-
|
|
122
|
+
requestParams
|
|
119
123
|
);
|
|
120
124
|
}
|
|
121
125
|
return null;
|
|
@@ -515,9 +519,64 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
515
519
|
<ResponsivePie data={widgetData} {...widget.props} />
|
|
516
520
|
);
|
|
517
521
|
case 'bar': {
|
|
518
|
-
|
|
519
|
-
|
|
522
|
+
// Handle both data structures
|
|
523
|
+
let barData = [];
|
|
524
|
+
let colors = {};
|
|
525
|
+
let keys = [];
|
|
526
|
+
|
|
527
|
+
// Handle new data structure (from your first example)
|
|
528
|
+
if (
|
|
529
|
+
widgetData.data &&
|
|
530
|
+
Array.isArray(widgetData.data) &&
|
|
531
|
+
widgetData.colors
|
|
532
|
+
) {
|
|
533
|
+
barData = widgetData.data;
|
|
534
|
+
colors = widgetData.colors;
|
|
535
|
+
// Extract keys from the first data item, excluding 'month' or any other index field
|
|
536
|
+
keys = Object.keys(barData[0] || {}).filter(
|
|
537
|
+
(key) => key !== 'month'
|
|
538
|
+
);
|
|
520
539
|
}
|
|
540
|
+
// Handle newer data structure (from your second example)
|
|
541
|
+
else if (
|
|
542
|
+
widgetData.data &&
|
|
543
|
+
Array.isArray(widgetData.data) &&
|
|
544
|
+
widgetData.keys
|
|
545
|
+
) {
|
|
546
|
+
barData = widgetData.data;
|
|
547
|
+
keys = widgetData.keys;
|
|
548
|
+
// Extract colors from the first data item
|
|
549
|
+
if (barData[0]) {
|
|
550
|
+
keys.forEach((key) => {
|
|
551
|
+
const colorKey = `${key}Color`;
|
|
552
|
+
if (barData[0][colorKey]) {
|
|
553
|
+
colors[key] = barData[0][colorKey];
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Handle legacy data structure
|
|
559
|
+
else {
|
|
560
|
+
barData =
|
|
561
|
+
widgetData.bar?.data ||
|
|
562
|
+
widgetData.data ||
|
|
563
|
+
widgetData;
|
|
564
|
+
if (widgetData.keys) {
|
|
565
|
+
keys = widgetData.keys;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Set up colors if they exist
|
|
570
|
+
if (Object.keys(colors).length > 0) {
|
|
571
|
+
widget.props.colors = ({ id }) => colors[id];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Set keys in props if they exist
|
|
575
|
+
if (keys.length > 0) {
|
|
576
|
+
widget.props.keys = keys;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Handle legend setup
|
|
521
580
|
let legendMapping = {};
|
|
522
581
|
if (widget.legend) {
|
|
523
582
|
legendMapping = widget.legend.reduce((acc, curr) => {
|
|
@@ -544,14 +603,14 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
544
603
|
};
|
|
545
604
|
widget.props.tooltip = CustomTooltip;
|
|
546
605
|
}
|
|
547
|
-
|
|
548
|
-
widgetData.bar?.data || widgetData.data || widgetData;
|
|
606
|
+
|
|
549
607
|
if (barData?.length > 0) {
|
|
550
608
|
return (
|
|
551
609
|
<div style={{ height: widget.height || '600px' }}>
|
|
552
610
|
<ResponsiveBar
|
|
553
611
|
key={`${widget.id}-${widget.size}`}
|
|
554
612
|
data={barData}
|
|
613
|
+
indexBy="month"
|
|
555
614
|
{...widget.props}
|
|
556
615
|
/>
|
|
557
616
|
</div>
|
|
@@ -12,24 +12,23 @@ import MultiSelect from '../MultiSelect';
|
|
|
12
12
|
import 'react-confirm-alert/src/react-confirm-alert.css';
|
|
13
13
|
import styles from './styles/GenericEditableTable.module.scss';
|
|
14
14
|
|
|
15
|
-
// Updated ColourPicker with an expanded pastel palette
|
|
15
|
+
// Updated ColourPicker with an expanded pastel palette
|
|
16
16
|
const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
17
17
|
const [colorPickerShow, setColorPickerShow] = useState(false);
|
|
18
18
|
const buttonRef = useRef(null);
|
|
19
19
|
const popoverRef = useRef(null);
|
|
20
20
|
|
|
21
|
-
// Expanded custom palette of pastel colours
|
|
22
21
|
const pastelColors = [
|
|
23
|
-
'#FFB3BA',
|
|
24
|
-
'#FFDFBA',
|
|
25
|
-
'#FFFFBA',
|
|
26
|
-
'#BAFFC9',
|
|
27
|
-
'#BAE1FF',
|
|
28
|
-
'#BFFCC6',
|
|
29
|
-
'#E2BAFF',
|
|
30
|
-
'#C3B1E1',
|
|
31
|
-
'#F0E6EF',
|
|
32
|
-
'#D0E8F2',
|
|
22
|
+
'#FFB3BA',
|
|
23
|
+
'#FFDFBA',
|
|
24
|
+
'#FFFFBA',
|
|
25
|
+
'#BAFFC9',
|
|
26
|
+
'#BAE1FF',
|
|
27
|
+
'#BFFCC6',
|
|
28
|
+
'#E2BAFF',
|
|
29
|
+
'#C3B1E1',
|
|
30
|
+
'#F0E6EF',
|
|
31
|
+
'#D0E8F2',
|
|
33
32
|
];
|
|
34
33
|
|
|
35
34
|
useEffect(() => {
|
|
@@ -43,7 +42,6 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
|
43
42
|
setColorPickerShow(false);
|
|
44
43
|
}
|
|
45
44
|
};
|
|
46
|
-
|
|
47
45
|
document.addEventListener('mousedown', handleClickOutside);
|
|
48
46
|
return () =>
|
|
49
47
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
@@ -54,7 +52,6 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
|
54
52
|
: {};
|
|
55
53
|
|
|
56
54
|
const handleClear = () => {
|
|
57
|
-
// Passing an empty string to represent "no colour selected"
|
|
58
55
|
onChange('', columnId, keyCounter);
|
|
59
56
|
setColorPickerShow(false);
|
|
60
57
|
};
|
|
@@ -102,7 +99,6 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
|
102
99
|
setColorPickerShow(false);
|
|
103
100
|
}}
|
|
104
101
|
/>
|
|
105
|
-
{/* Extra clear button added below the CompactPicker */}
|
|
106
102
|
<div className={styles.clearContainer}>
|
|
107
103
|
<button
|
|
108
104
|
onClick={handleClear}
|
|
@@ -122,27 +118,94 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
|
122
118
|
|
|
123
119
|
const GenericEditableTable = ({
|
|
124
120
|
schedulingConfig,
|
|
125
|
-
data,
|
|
121
|
+
data, // incoming data – may be a plain object or already an array of datasets
|
|
126
122
|
dataId,
|
|
127
123
|
preloadedOptions,
|
|
128
|
-
setData,
|
|
124
|
+
setData, // callback to update the parent state
|
|
129
125
|
}) => {
|
|
126
|
+
const isInitialLoad = useRef(true);
|
|
130
127
|
const { columns, rows, form, title } = schedulingConfig;
|
|
131
128
|
const hasCategory = rows && rows.categoryKey; // Only group if defined
|
|
132
129
|
|
|
130
|
+
// dataSets: each item => { created_at: string, data: array of rows }
|
|
131
|
+
const [dataSets, setDataSets] = useState([]);
|
|
132
|
+
// Index of the currently active dataset
|
|
133
|
+
const [activeDatasetIndex, setActiveDatasetIndex] = useState(0);
|
|
134
|
+
// Local copy of the rows for the active dataset
|
|
133
135
|
const [localData, setLocalData] = useState([]);
|
|
134
|
-
// For new (unsaved) entries
|
|
136
|
+
// For new (unsaved) entries in the active dataset
|
|
135
137
|
const [newEntries, setNewEntries] = useState({});
|
|
136
138
|
const [newEntry, setNewEntry] = useState({});
|
|
137
139
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
+
// On mount or when 'data' changes, read from data[schedulingConfig.rows.key] and handle both new & old formats
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const raw = data && data[schedulingConfig.rows.key];
|
|
143
|
+
if (!raw) {
|
|
144
|
+
setDataSets([]);
|
|
145
|
+
setActiveDatasetIndex(0);
|
|
146
|
+
isInitialLoad.current = false;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let newDataSets;
|
|
151
|
+
if (Array.isArray(raw)) {
|
|
152
|
+
if (
|
|
153
|
+
raw.length > 0 &&
|
|
154
|
+
typeof raw[0].created_at === 'string' &&
|
|
155
|
+
Array.isArray(raw[0].data)
|
|
156
|
+
) {
|
|
157
|
+
newDataSets = raw;
|
|
158
|
+
} else {
|
|
159
|
+
newDataSets = [
|
|
160
|
+
{
|
|
161
|
+
created_at: new Date().toISOString(),
|
|
162
|
+
data: raw,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
newDataSets = [
|
|
168
|
+
{
|
|
169
|
+
created_at: new Date().toISOString(),
|
|
170
|
+
data: raw,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
setDataSets(newDataSets);
|
|
175
|
+
|
|
176
|
+
if (isInitialLoad.current) {
|
|
177
|
+
// On initial load, always select the latest dataset
|
|
178
|
+
setActiveDatasetIndex(newDataSets.length - 1);
|
|
179
|
+
isInitialLoad.current = false;
|
|
180
|
+
} else {
|
|
181
|
+
// Otherwise, if the current index is valid, keep it; else, select the last
|
|
182
|
+
setActiveDatasetIndex((prevIndex) =>
|
|
183
|
+
prevIndex < newDataSets.length
|
|
184
|
+
? prevIndex
|
|
185
|
+
: newDataSets.length - 1
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}, [data, schedulingConfig.rows.key]);
|
|
189
|
+
|
|
190
|
+
// Whenever activeDatasetIndex changes, sync localData from the chosen dataset
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const activeDataSet = dataSets[activeDatasetIndex];
|
|
193
|
+
if (activeDataSet && Array.isArray(activeDataSet.data)) {
|
|
194
|
+
const enrichedData = activeDataSet.data.map((entry, index) => ({
|
|
195
|
+
...entry,
|
|
196
|
+
sort_order: entry.sort_order ?? index + 1,
|
|
197
|
+
}));
|
|
198
|
+
setLocalData(sortBySortOrder(enrichedData));
|
|
199
|
+
} else {
|
|
200
|
+
setLocalData([]);
|
|
201
|
+
}
|
|
202
|
+
}, [dataSets, activeDatasetIndex]);
|
|
140
203
|
|
|
141
|
-
// Helper to sort by sort_order
|
|
204
|
+
// Helper to sort by sort_order
|
|
142
205
|
const sortBySortOrder = (dataArray) =>
|
|
143
206
|
[...dataArray].sort((a, b) => a.sort_order - b.sort_order);
|
|
144
207
|
|
|
145
|
-
//
|
|
208
|
+
// Return background color if there's a column of type 'colour'
|
|
146
209
|
const getRowBackground = (entry) => {
|
|
147
210
|
const colourColumn = columns.find((col) => col.type === 'colour');
|
|
148
211
|
return colourColumn && entry[colourColumn.id]
|
|
@@ -150,7 +213,7 @@ const GenericEditableTable = ({
|
|
|
150
213
|
: undefined;
|
|
151
214
|
};
|
|
152
215
|
|
|
153
|
-
//
|
|
216
|
+
// Calculate total for columns of type 'total'
|
|
154
217
|
const calculateTotal = (entry, keys) => {
|
|
155
218
|
let total = 0;
|
|
156
219
|
if (Array.isArray(keys)) {
|
|
@@ -164,8 +227,7 @@ const GenericEditableTable = ({
|
|
|
164
227
|
keys.price &&
|
|
165
228
|
keys.unit
|
|
166
229
|
) {
|
|
167
|
-
const priceKeys = keys
|
|
168
|
-
const unitKeys = keys.unit;
|
|
230
|
+
const { price: priceKeys, unit: unitKeys } = keys;
|
|
169
231
|
if (unitKeys.length === 1) {
|
|
170
232
|
const unitValue = parseFloat(entry[unitKeys[0]]) || 0;
|
|
171
233
|
total = priceKeys.reduce(
|
|
@@ -186,24 +248,19 @@ const GenericEditableTable = ({
|
|
|
186
248
|
return total;
|
|
187
249
|
};
|
|
188
250
|
|
|
189
|
-
//
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
const enrichedData = data[rows.key]?.map((entry, index) => ({
|
|
192
|
-
...entry,
|
|
193
|
-
sort_order: entry.sort_order ?? index + 1,
|
|
194
|
-
}));
|
|
195
|
-
const sortedData = enrichedData ? sortBySortOrder(enrichedData) : [];
|
|
196
|
-
setLocalData(sortedData);
|
|
197
|
-
}, [data, rows.key]);
|
|
198
|
-
|
|
251
|
+
// Debounced update to save entire dataSets array
|
|
199
252
|
const debouncedUpdate = useMemo(
|
|
200
253
|
() =>
|
|
201
|
-
debounce(async (
|
|
254
|
+
debounce(async (updatedDataSets) => {
|
|
202
255
|
try {
|
|
256
|
+
// Pass the array of datasets under schedulingConfig.rows.key
|
|
257
|
+
const payload = {
|
|
258
|
+
[schedulingConfig.rows.key]: updatedDataSets,
|
|
259
|
+
};
|
|
203
260
|
const res = await CustomFetch(
|
|
204
261
|
`${form.url}/${dataId}`,
|
|
205
262
|
'PUT',
|
|
206
|
-
|
|
263
|
+
payload
|
|
207
264
|
);
|
|
208
265
|
if (!res.error) {
|
|
209
266
|
if (res.data?.data) {
|
|
@@ -216,10 +273,168 @@ const GenericEditableTable = ({
|
|
|
216
273
|
console.error('Error updating field:', error);
|
|
217
274
|
}
|
|
218
275
|
}, 1000),
|
|
219
|
-
[form.url, dataId, setData]
|
|
276
|
+
[form.url, dataId, setData, schedulingConfig.rows.key]
|
|
220
277
|
);
|
|
221
278
|
|
|
222
|
-
//
|
|
279
|
+
// 1) Handle field change for existing rows
|
|
280
|
+
const handleFieldChange = (value, fieldId, keyCounter) => {
|
|
281
|
+
setLocalData((prevLocalData) => {
|
|
282
|
+
const updatedLocalData = [...prevLocalData];
|
|
283
|
+
updatedLocalData[keyCounter] = {
|
|
284
|
+
...updatedLocalData[keyCounter],
|
|
285
|
+
[fieldId]: value,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// If there's date arithmetic logic
|
|
289
|
+
columns.forEach((col) => {
|
|
290
|
+
if (
|
|
291
|
+
col.add &&
|
|
292
|
+
(fieldId === col.id || fieldId === col.add.from)
|
|
293
|
+
) {
|
|
294
|
+
const fromVal = updatedLocalData[keyCounter][col.add.from];
|
|
295
|
+
const daysVal = parseFloat(
|
|
296
|
+
updatedLocalData[keyCounter][col.id]
|
|
297
|
+
);
|
|
298
|
+
if (fromVal && !isNaN(daysVal)) {
|
|
299
|
+
const newDate = new Date(fromVal);
|
|
300
|
+
if (!isNaN(newDate)) {
|
|
301
|
+
newDate.setDate(newDate.getDate() + daysVal);
|
|
302
|
+
const yyyy = newDate.getFullYear();
|
|
303
|
+
let mm = newDate.getMonth() + 1;
|
|
304
|
+
let dd = newDate.getDate();
|
|
305
|
+
if (mm < 10) mm = '0' + mm;
|
|
306
|
+
if (dd < 10) dd = '0' + dd;
|
|
307
|
+
updatedLocalData[keyCounter][
|
|
308
|
+
col.add.to
|
|
309
|
+
] = `${yyyy}-${mm}-${dd}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Update the active dataset
|
|
316
|
+
const updatedDataSets = [...dataSets];
|
|
317
|
+
updatedDataSets[activeDatasetIndex] = {
|
|
318
|
+
...updatedDataSets[activeDatasetIndex],
|
|
319
|
+
data: updatedLocalData,
|
|
320
|
+
};
|
|
321
|
+
setDataSets(updatedDataSets);
|
|
322
|
+
|
|
323
|
+
// Save
|
|
324
|
+
debouncedUpdate(updatedDataSets);
|
|
325
|
+
return updatedLocalData;
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// 2) Handle field change for *new* rows (before they're officially added)
|
|
330
|
+
const handleNewFieldChange = (value, fieldId, groupId = null) => {
|
|
331
|
+
if (hasCategory) {
|
|
332
|
+
setNewEntries((prev) => {
|
|
333
|
+
const updated = {
|
|
334
|
+
...prev,
|
|
335
|
+
[groupId]: { ...prev[groupId], [fieldId]: value },
|
|
336
|
+
};
|
|
337
|
+
columns.forEach((col) => {
|
|
338
|
+
if (
|
|
339
|
+
col.add &&
|
|
340
|
+
(fieldId === col.id || fieldId === col.add.from)
|
|
341
|
+
) {
|
|
342
|
+
const fromVal = updated[groupId][col.add.from];
|
|
343
|
+
const daysVal = parseFloat(updated[groupId][col.id]);
|
|
344
|
+
if (fromVal && !isNaN(daysVal)) {
|
|
345
|
+
const newDate = new Date(fromVal);
|
|
346
|
+
if (!isNaN(newDate)) {
|
|
347
|
+
newDate.setDate(newDate.getDate() + daysVal);
|
|
348
|
+
const yyyy = newDate.getFullYear();
|
|
349
|
+
let mm = newDate.getMonth() + 1;
|
|
350
|
+
let dd = newDate.getDate();
|
|
351
|
+
if (mm < 10) mm = '0' + mm;
|
|
352
|
+
if (dd < 10) dd = '0' + dd;
|
|
353
|
+
updated[groupId][
|
|
354
|
+
col.add.to
|
|
355
|
+
] = `${yyyy}-${mm}-${dd}`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return updated;
|
|
361
|
+
});
|
|
362
|
+
} else {
|
|
363
|
+
setNewEntry((prev) => {
|
|
364
|
+
const updated = { ...prev, [fieldId]: value };
|
|
365
|
+
columns.forEach((col) => {
|
|
366
|
+
if (
|
|
367
|
+
col.add &&
|
|
368
|
+
(fieldId === col.id || fieldId === col.add.from)
|
|
369
|
+
) {
|
|
370
|
+
const fromVal = updated[col.add.from];
|
|
371
|
+
const daysVal = parseFloat(updated[col.id]);
|
|
372
|
+
if (fromVal && !isNaN(daysVal)) {
|
|
373
|
+
const newDate = new Date(fromVal);
|
|
374
|
+
if (!isNaN(newDate)) {
|
|
375
|
+
newDate.setDate(newDate.getDate() + daysVal);
|
|
376
|
+
const yyyy = newDate.getFullYear();
|
|
377
|
+
let mm = newDate.getMonth() + 1;
|
|
378
|
+
let dd = newDate.getDate();
|
|
379
|
+
if (mm < 10) mm = '0' + mm;
|
|
380
|
+
if (dd < 10) dd = '0' + dd;
|
|
381
|
+
updated[col.add.to] = `${yyyy}-${mm}-${dd}`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
return updated;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// 3) Adding a new row to the active dataset
|
|
392
|
+
const handleAddNewEntry = (groupId = null) => {
|
|
393
|
+
const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
|
|
394
|
+
if (!newRowData || Object.keys(newRowData).length === 0) {
|
|
395
|
+
toast.error('Please fill in some data before adding.');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const maxSortOrder = localData.reduce(
|
|
399
|
+
(max, row) => Math.max(max, row.sort_order || 0),
|
|
400
|
+
0
|
|
401
|
+
);
|
|
402
|
+
const newRow = {
|
|
403
|
+
...newRowData,
|
|
404
|
+
id: `new-${Date.now()}`,
|
|
405
|
+
sort_order: maxSortOrder + 1,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// If there's a category column, fill it in
|
|
409
|
+
if (hasCategory && groupId !== 'no_category') {
|
|
410
|
+
newRow[rows.categoryKey.id] = {
|
|
411
|
+
id: groupId,
|
|
412
|
+
[rows.categoryKey.label]: groupId,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const updatedLocalData = [...localData, newRow];
|
|
417
|
+
setLocalData(updatedLocalData);
|
|
418
|
+
|
|
419
|
+
// Update the active dataset
|
|
420
|
+
const updatedDataSets = [...dataSets];
|
|
421
|
+
updatedDataSets[activeDatasetIndex] = {
|
|
422
|
+
...updatedDataSets[activeDatasetIndex],
|
|
423
|
+
data: updatedLocalData,
|
|
424
|
+
};
|
|
425
|
+
setDataSets(updatedDataSets);
|
|
426
|
+
debouncedUpdate(updatedDataSets);
|
|
427
|
+
|
|
428
|
+
// Reset new-entry state
|
|
429
|
+
if (hasCategory) {
|
|
430
|
+
setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
|
|
431
|
+
} else {
|
|
432
|
+
setNewEntry({});
|
|
433
|
+
}
|
|
434
|
+
toast.success('Row added');
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// 4) Group data by category (if defined)
|
|
223
438
|
const groupCategoriesByType = (dataArray) => {
|
|
224
439
|
if (!rows.categoryKey) return [];
|
|
225
440
|
const grouped = {};
|
|
@@ -231,8 +446,6 @@ const GenericEditableTable = ({
|
|
|
231
446
|
: null;
|
|
232
447
|
const categoryId = categoryInfo ? categoryInfo.id : null;
|
|
233
448
|
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
449
|
const catSortOrder =
|
|
237
450
|
categoryInfo && typeof categoryInfo.sort_order !== 'undefined'
|
|
238
451
|
? categoryInfo.sort_order
|
|
@@ -244,25 +457,21 @@ const GenericEditableTable = ({
|
|
|
244
457
|
category:
|
|
245
458
|
categoryLabel ||
|
|
246
459
|
(groupKey === 'no_category' ? 'No Category' : ''),
|
|
247
|
-
catSortOrder,
|
|
460
|
+
catSortOrder,
|
|
248
461
|
entries: [],
|
|
249
462
|
};
|
|
250
463
|
}
|
|
251
|
-
grouped[groupKey].entries.push({
|
|
252
|
-
...entry,
|
|
253
|
-
keyCounter: index,
|
|
254
|
-
});
|
|
464
|
+
grouped[groupKey].entries.push({ ...entry, keyCounter: index });
|
|
255
465
|
});
|
|
256
466
|
|
|
257
|
-
// Sort entries within each group
|
|
467
|
+
// Sort entries within each group
|
|
258
468
|
Object.values(grouped).forEach((group) => {
|
|
259
469
|
group.entries.sort((a, b) => a.sort_order - b.sort_order);
|
|
260
470
|
});
|
|
261
471
|
|
|
262
|
-
//
|
|
472
|
+
// Sort groups by catSortOrder
|
|
263
473
|
const groupedArray = Object.values(grouped);
|
|
264
474
|
groupedArray.sort((a, b) => {
|
|
265
|
-
// Optionally, place 'no_category' first (or adjust as needed)
|
|
266
475
|
if (a.id === 'no_category') return -1;
|
|
267
476
|
if (b.id === 'no_category') return 1;
|
|
268
477
|
return a.catSortOrder - b.catSortOrder;
|
|
@@ -272,21 +481,14 @@ const GenericEditableTable = ({
|
|
|
272
481
|
};
|
|
273
482
|
|
|
274
483
|
const groupedCategories = useMemo(() => {
|
|
275
|
-
|
|
276
|
-
return groupCategoriesByType(localData);
|
|
277
|
-
}
|
|
278
|
-
return [];
|
|
484
|
+
return hasCategory ? groupCategoriesByType(localData) : [];
|
|
279
485
|
}, [localData, hasCategory]);
|
|
280
486
|
|
|
281
|
-
//
|
|
487
|
+
// 5) Column copy
|
|
282
488
|
const handleColumnCopy = (column, group = null) => {
|
|
283
|
-
let values
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
values = group.entries.map((entry) => entry[column.id]);
|
|
287
|
-
} else {
|
|
288
|
-
values = localData.map((entry) => entry[column.id]);
|
|
289
|
-
}
|
|
489
|
+
let values = group
|
|
490
|
+
? group.entries.map((entry) => entry[column.id])
|
|
491
|
+
: localData.map((entry) => entry[column.id]);
|
|
290
492
|
|
|
291
493
|
const textToCopy = values
|
|
292
494
|
.filter((val) => val !== undefined && val !== null)
|
|
@@ -306,49 +508,7 @@ const GenericEditableTable = ({
|
|
|
306
508
|
.catch(() => toast.error('Failed to copy to clipboard'));
|
|
307
509
|
};
|
|
308
510
|
|
|
309
|
-
//
|
|
310
|
-
const handleFieldChange = (value, fieldId, keyCounter) => {
|
|
311
|
-
setLocalData((prev) => {
|
|
312
|
-
const updatedDetail = [...prev];
|
|
313
|
-
updatedDetail[keyCounter] = {
|
|
314
|
-
...updatedDetail[keyCounter],
|
|
315
|
-
[fieldId]: value,
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Loop through columns to see if any has an "add" property.
|
|
319
|
-
columns.forEach((col) => {
|
|
320
|
-
if (
|
|
321
|
-
col.add &&
|
|
322
|
-
(fieldId === col.id || fieldId === col.add.from)
|
|
323
|
-
) {
|
|
324
|
-
const fromVal = updatedDetail[keyCounter][col.add.from];
|
|
325
|
-
const daysVal = parseFloat(
|
|
326
|
-
updatedDetail[keyCounter][col.id]
|
|
327
|
-
);
|
|
328
|
-
if (fromVal && !isNaN(daysVal)) {
|
|
329
|
-
const newDate = new Date(fromVal);
|
|
330
|
-
if (!isNaN(newDate)) {
|
|
331
|
-
newDate.setDate(newDate.getDate() + daysVal);
|
|
332
|
-
// Format the date as YYYY-MM-DD
|
|
333
|
-
const yyyy = newDate.getFullYear();
|
|
334
|
-
let mm = newDate.getMonth() + 1;
|
|
335
|
-
let dd = newDate.getDate();
|
|
336
|
-
if (mm < 10) mm = '0' + mm;
|
|
337
|
-
if (dd < 10) dd = '0' + dd;
|
|
338
|
-
updatedDetail[keyCounter][
|
|
339
|
-
col.add.to
|
|
340
|
-
] = `${yyyy}-${mm}-${dd}`;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
debouncedUpdate({ [rows.key]: updatedDetail });
|
|
347
|
-
return updatedDetail;
|
|
348
|
-
});
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// For grouped data, sort handler remains unchanged
|
|
511
|
+
// 6) Sorting: grouped
|
|
352
512
|
const handleSortChangeGrouped = (groupId, entryId, direction) => {
|
|
353
513
|
if (!hasCategory) return;
|
|
354
514
|
setLocalData((prev) => {
|
|
@@ -356,12 +516,10 @@ const GenericEditableTable = ({
|
|
|
356
516
|
const group = groupCategoriesByType(updatedDetail).find(
|
|
357
517
|
(grp) => grp.id === groupId
|
|
358
518
|
);
|
|
359
|
-
|
|
360
519
|
if (group) {
|
|
361
520
|
const index = group.entries.findIndex(
|
|
362
521
|
(entry) => entry.id === entryId
|
|
363
522
|
);
|
|
364
|
-
|
|
365
523
|
if (
|
|
366
524
|
(direction === 'up' && index > 0) ||
|
|
367
525
|
(direction === 'down' && index < group.entries.length - 1)
|
|
@@ -376,6 +534,7 @@ const GenericEditableTable = ({
|
|
|
376
534
|
group.entries[index].sort_order,
|
|
377
535
|
];
|
|
378
536
|
|
|
537
|
+
// Rebuild array with swapped sort_order
|
|
379
538
|
const reordered = updatedDetail.map((item) => {
|
|
380
539
|
const found = group.entries.find(
|
|
381
540
|
(e) => e.id === item.id
|
|
@@ -385,7 +544,15 @@ const GenericEditableTable = ({
|
|
|
385
544
|
: item;
|
|
386
545
|
});
|
|
387
546
|
|
|
388
|
-
|
|
547
|
+
// Update the active dataset
|
|
548
|
+
const updatedDataSets = [...dataSets];
|
|
549
|
+
updatedDataSets[activeDatasetIndex] = {
|
|
550
|
+
...updatedDataSets[activeDatasetIndex],
|
|
551
|
+
data: reordered,
|
|
552
|
+
};
|
|
553
|
+
setDataSets(updatedDataSets);
|
|
554
|
+
debouncedUpdate(updatedDataSets);
|
|
555
|
+
|
|
389
556
|
return reordered;
|
|
390
557
|
}
|
|
391
558
|
}
|
|
@@ -393,7 +560,7 @@ const GenericEditableTable = ({
|
|
|
393
560
|
});
|
|
394
561
|
};
|
|
395
562
|
|
|
396
|
-
//
|
|
563
|
+
// 7) Sorting: ungrouped
|
|
397
564
|
const handleSortChangeUngrouped = (entryId, direction) => {
|
|
398
565
|
setLocalData((prev) => {
|
|
399
566
|
const sortedData = sortBySortOrder(prev);
|
|
@@ -405,18 +572,26 @@ const GenericEditableTable = ({
|
|
|
405
572
|
) {
|
|
406
573
|
return prev;
|
|
407
574
|
}
|
|
408
|
-
|
|
409
575
|
const swapIndex = direction === 'up' ? index - 1 : index + 1;
|
|
410
576
|
[sortedData[index].sort_order, sortedData[swapIndex].sort_order] = [
|
|
411
577
|
sortedData[swapIndex].sort_order,
|
|
412
578
|
sortedData[index].sort_order,
|
|
413
579
|
];
|
|
414
580
|
|
|
415
|
-
|
|
581
|
+
// Update the active dataset
|
|
582
|
+
const updatedDataSets = [...dataSets];
|
|
583
|
+
updatedDataSets[activeDatasetIndex] = {
|
|
584
|
+
...updatedDataSets[activeDatasetIndex],
|
|
585
|
+
data: sortedData,
|
|
586
|
+
};
|
|
587
|
+
setDataSets(updatedDataSets);
|
|
588
|
+
debouncedUpdate(updatedDataSets);
|
|
589
|
+
|
|
416
590
|
return sortedData;
|
|
417
591
|
});
|
|
418
592
|
};
|
|
419
593
|
|
|
594
|
+
// 8) Deletion
|
|
420
595
|
const handleDeleteRow = (entry) => {
|
|
421
596
|
confirmAlert({
|
|
422
597
|
title: 'Confirm to Delete',
|
|
@@ -429,19 +604,23 @@ const GenericEditableTable = ({
|
|
|
429
604
|
const updatedDetail = prev.filter(
|
|
430
605
|
(item) => item.id !== entry.id
|
|
431
606
|
);
|
|
432
|
-
|
|
607
|
+
const updatedDataSets = [...dataSets];
|
|
608
|
+
updatedDataSets[activeDatasetIndex] = {
|
|
609
|
+
...updatedDataSets[activeDatasetIndex],
|
|
610
|
+
data: updatedDetail,
|
|
611
|
+
};
|
|
612
|
+
setDataSets(updatedDataSets);
|
|
613
|
+
debouncedUpdate(updatedDataSets);
|
|
433
614
|
return updatedDetail;
|
|
434
615
|
});
|
|
435
616
|
},
|
|
436
617
|
},
|
|
437
|
-
{
|
|
438
|
-
label: 'No',
|
|
439
|
-
},
|
|
618
|
+
{ label: 'No' },
|
|
440
619
|
],
|
|
441
620
|
});
|
|
442
621
|
};
|
|
443
622
|
|
|
444
|
-
//
|
|
623
|
+
// RENDERING HELPERS
|
|
445
624
|
const renderScheduledTableField = (entry, column, keyCounter) => {
|
|
446
625
|
if (!entry || !column) return null;
|
|
447
626
|
|
|
@@ -538,8 +717,8 @@ const GenericEditableTable = ({
|
|
|
538
717
|
settings={{ id: column.id, compact: true }}
|
|
539
718
|
className={styles.selectFullWidth}
|
|
540
719
|
multi={false}
|
|
541
|
-
onChange={(
|
|
542
|
-
handleFieldChange(
|
|
720
|
+
onChange={(val) =>
|
|
721
|
+
handleFieldChange(val, column.id, keyCounter)
|
|
543
722
|
}
|
|
544
723
|
inputValue={entry[column.id] || ''}
|
|
545
724
|
options={multiOptions}
|
|
@@ -571,117 +750,7 @@ const GenericEditableTable = ({
|
|
|
571
750
|
}
|
|
572
751
|
};
|
|
573
752
|
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
// Updated handleNewFieldChange to support "add" functionality in new rows
|
|
577
|
-
const handleNewFieldChange = (value, fieldId, groupId = null) => {
|
|
578
|
-
if (hasCategory) {
|
|
579
|
-
setNewEntries((prev) => {
|
|
580
|
-
const updated = {
|
|
581
|
-
...prev,
|
|
582
|
-
[groupId]: {
|
|
583
|
-
...prev[groupId],
|
|
584
|
-
[fieldId]: value,
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
columns.forEach((col) => {
|
|
588
|
-
if (
|
|
589
|
-
col.add &&
|
|
590
|
-
(fieldId === col.id || fieldId === col.add.from)
|
|
591
|
-
) {
|
|
592
|
-
const fromVal = updated[groupId][col.add.from];
|
|
593
|
-
const daysVal = parseFloat(updated[groupId][col.id]);
|
|
594
|
-
if (fromVal && !isNaN(daysVal)) {
|
|
595
|
-
const newDate = new Date(fromVal);
|
|
596
|
-
if (!isNaN(newDate)) {
|
|
597
|
-
newDate.setDate(newDate.getDate() + daysVal);
|
|
598
|
-
const yyyy = newDate.getFullYear();
|
|
599
|
-
let mm = newDate.getMonth() + 1;
|
|
600
|
-
let dd = newDate.getDate();
|
|
601
|
-
if (mm < 10) mm = '0' + mm;
|
|
602
|
-
if (dd < 10) dd = '0' + dd;
|
|
603
|
-
updated[groupId][
|
|
604
|
-
col.add.to
|
|
605
|
-
] = `${yyyy}-${mm}-${dd}`;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
return updated;
|
|
611
|
-
});
|
|
612
|
-
} else {
|
|
613
|
-
setNewEntry((prev) => {
|
|
614
|
-
const updated = {
|
|
615
|
-
...prev,
|
|
616
|
-
[fieldId]: value,
|
|
617
|
-
};
|
|
618
|
-
columns.forEach((col) => {
|
|
619
|
-
if (
|
|
620
|
-
col.add &&
|
|
621
|
-
(fieldId === col.id || fieldId === col.add.from)
|
|
622
|
-
) {
|
|
623
|
-
const fromVal = updated[col.add.from];
|
|
624
|
-
const daysVal = parseFloat(updated[col.id]);
|
|
625
|
-
if (fromVal && !isNaN(daysVal)) {
|
|
626
|
-
const newDate = new Date(fromVal);
|
|
627
|
-
if (!isNaN(newDate)) {
|
|
628
|
-
newDate.setDate(newDate.getDate() + daysVal);
|
|
629
|
-
const yyyy = newDate.getFullYear();
|
|
630
|
-
let mm = newDate.getMonth() + 1;
|
|
631
|
-
let dd = newDate.getDate();
|
|
632
|
-
if (mm < 10) mm = '0' + mm;
|
|
633
|
-
if (dd < 10) dd = '0' + dd;
|
|
634
|
-
updated[col.add.to] = `${yyyy}-${mm}-${dd}`;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
return updated;
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
// Add the new row to localData and update the backend
|
|
645
|
-
const handleAddNewEntry = (groupId = null) => {
|
|
646
|
-
const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
|
|
647
|
-
if (!newRowData || Object.keys(newRowData).length === 0) {
|
|
648
|
-
toast.error('Please fill in some data before adding.');
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
// Determine a new sort_order
|
|
652
|
-
const maxSortOrder = localData.reduce(
|
|
653
|
-
(max, row) => Math.max(max, row.sort_order || 0),
|
|
654
|
-
0
|
|
655
|
-
);
|
|
656
|
-
const newRow = {
|
|
657
|
-
...newRowData,
|
|
658
|
-
id: `new-${Date.now()}`, // temporary ID; adjust as needed
|
|
659
|
-
sort_order: maxSortOrder + 1,
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
// For grouped data, prepopulate the category field if needed
|
|
663
|
-
if (hasCategory && groupId !== 'no_category') {
|
|
664
|
-
newRow[rows.categoryKey.id] = {
|
|
665
|
-
id: groupId,
|
|
666
|
-
[rows.categoryKey.label]: groupId,
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
setLocalData((prev) => {
|
|
671
|
-
const updated = [...prev, newRow];
|
|
672
|
-
debouncedUpdate({ [rows.key]: updated });
|
|
673
|
-
return updated;
|
|
674
|
-
});
|
|
675
|
-
// Reset the new entry state for the corresponding group or overall
|
|
676
|
-
if (hasCategory) {
|
|
677
|
-
setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
|
|
678
|
-
} else {
|
|
679
|
-
setNewEntry({});
|
|
680
|
-
}
|
|
681
|
-
toast.success('Row added');
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
// Render a new row field (similar to renderScheduledTableField)
|
|
753
|
+
// Render new row field
|
|
685
754
|
const renderNewRowField = (newRowData, column, groupId = null) => {
|
|
686
755
|
const value = newRowData ? newRowData[column.id] : '';
|
|
687
756
|
switch (column.type) {
|
|
@@ -775,8 +844,8 @@ const GenericEditableTable = ({
|
|
|
775
844
|
settings={{ id: column.id, compact: true }}
|
|
776
845
|
className={styles.selectFullWidth}
|
|
777
846
|
multi={false}
|
|
778
|
-
onChange={(
|
|
779
|
-
handleNewFieldChange(
|
|
847
|
+
onChange={(val) =>
|
|
848
|
+
handleNewFieldChange(val, column.id, groupId)
|
|
780
849
|
}
|
|
781
850
|
inputValue={value || ''}
|
|
782
851
|
options={multiOptions}
|
|
@@ -807,7 +876,7 @@ const GenericEditableTable = ({
|
|
|
807
876
|
}
|
|
808
877
|
};
|
|
809
878
|
|
|
810
|
-
//
|
|
879
|
+
// Inline form for adding a new row
|
|
811
880
|
const renderNewRowForm = (groupId = null) => {
|
|
812
881
|
const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
|
|
813
882
|
return (
|
|
@@ -837,7 +906,7 @@ const GenericEditableTable = ({
|
|
|
837
906
|
);
|
|
838
907
|
};
|
|
839
908
|
|
|
840
|
-
//
|
|
909
|
+
// Header cell with optional copy icon
|
|
841
910
|
const renderHeaderCell = (column, group = null) => (
|
|
842
911
|
<th
|
|
843
912
|
key={`${column.id}${group ? `-${group.id}` : ''}`}
|
|
@@ -866,11 +935,51 @@ const GenericEditableTable = ({
|
|
|
866
935
|
</th>
|
|
867
936
|
);
|
|
868
937
|
|
|
938
|
+
// Dataset selector at the top
|
|
939
|
+
const renderDatasetSelector = () =>
|
|
940
|
+
schedulingConfig.addDataset && (
|
|
941
|
+
<div className={styles.datasetSelector}>
|
|
942
|
+
<label htmlFor="dataset-select">Select Dataset:</label>
|
|
943
|
+
<select
|
|
944
|
+
id="dataset-select"
|
|
945
|
+
value={activeDatasetIndex}
|
|
946
|
+
onChange={(e) =>
|
|
947
|
+
setActiveDatasetIndex(Number(e.target.value))
|
|
948
|
+
}
|
|
949
|
+
>
|
|
950
|
+
{dataSets.map((ds, index) => (
|
|
951
|
+
<option key={index} value={index}>
|
|
952
|
+
{new Date(ds.created_at).toLocaleString()}
|
|
953
|
+
</option>
|
|
954
|
+
))}
|
|
955
|
+
</select>
|
|
956
|
+
<button onClick={handleAddNewDataset}>New Dataset</button>
|
|
957
|
+
</div>
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
// Create a new dataset by cloning the current active one
|
|
961
|
+
const handleAddNewDataset = () => {
|
|
962
|
+
const currentActiveData = localData;
|
|
963
|
+
const newDataset = {
|
|
964
|
+
created_at: new Date().toISOString(),
|
|
965
|
+
data: currentActiveData.map((row) => ({ ...row })), // shallow clone
|
|
966
|
+
};
|
|
967
|
+
const updatedDataSets = [...dataSets, newDataset];
|
|
968
|
+
setDataSets(updatedDataSets);
|
|
969
|
+
// Always pick the newly created dataset
|
|
970
|
+
setActiveDatasetIndex(updatedDataSets.length - 1);
|
|
971
|
+
debouncedUpdate(updatedDataSets); // Save immediately
|
|
972
|
+
toast.success('New dataset added');
|
|
973
|
+
};
|
|
974
|
+
|
|
869
975
|
return (
|
|
870
976
|
<div className={styles.gridtxt}>
|
|
977
|
+
{renderDatasetSelector()}
|
|
978
|
+
|
|
871
979
|
<div className={styles.gridtxt__header}>
|
|
872
980
|
<span>{title || 'Estimation Schedule'}</span>
|
|
873
981
|
</div>
|
|
982
|
+
|
|
874
983
|
{hasCategory ? (
|
|
875
984
|
groupedCategories.length > 0 ? (
|
|
876
985
|
<table className={styles.schedulingTable}>
|
|
@@ -888,6 +997,7 @@ const GenericEditableTable = ({
|
|
|
888
997
|
)}
|
|
889
998
|
<th style={{ width: '5%' }}></th>
|
|
890
999
|
</tr>
|
|
1000
|
+
|
|
891
1001
|
{group.entries.map((entry, idx) => (
|
|
892
1002
|
<tr
|
|
893
1003
|
key={entry.id}
|
|
@@ -965,16 +1075,23 @@ const GenericEditableTable = ({
|
|
|
965
1075
|
</td>
|
|
966
1076
|
</tr>
|
|
967
1077
|
))}
|
|
968
|
-
|
|
1078
|
+
|
|
969
1079
|
{renderNewRowForm(group.id)}
|
|
970
|
-
|
|
971
|
-
{
|
|
1080
|
+
|
|
1081
|
+
{columns.findIndex(
|
|
1082
|
+
(col) => col.type === 'total'
|
|
1083
|
+
) !== -1 && (
|
|
972
1084
|
<tr className={styles.subtotalRow}>
|
|
973
1085
|
{columns.map((col, index) => {
|
|
1086
|
+
const totalColIndex =
|
|
1087
|
+
columns.findIndex(
|
|
1088
|
+
(c) =>
|
|
1089
|
+
c.type === 'total'
|
|
1090
|
+
);
|
|
974
1091
|
if (
|
|
975
1092
|
index ===
|
|
976
|
-
|
|
977
|
-
|
|
1093
|
+
totalColIndex - 1 &&
|
|
1094
|
+
index > 0
|
|
978
1095
|
) {
|
|
979
1096
|
return (
|
|
980
1097
|
<td key={col.id}>
|
|
@@ -982,9 +1099,7 @@ const GenericEditableTable = ({
|
|
|
982
1099
|
</td>
|
|
983
1100
|
);
|
|
984
1101
|
}
|
|
985
|
-
if (
|
|
986
|
-
index === totalColumnIndex
|
|
987
|
-
) {
|
|
1102
|
+
if (index === totalColIndex) {
|
|
988
1103
|
const groupTotal =
|
|
989
1104
|
group.entries.reduce(
|
|
990
1105
|
(sum, entry) =>
|
|
@@ -992,7 +1107,7 @@ const GenericEditableTable = ({
|
|
|
992
1107
|
calculateTotal(
|
|
993
1108
|
entry,
|
|
994
1109
|
columns[
|
|
995
|
-
|
|
1110
|
+
totalColIndex
|
|
996
1111
|
].keys
|
|
997
1112
|
),
|
|
998
1113
|
0
|
|
@@ -1096,19 +1211,23 @@ const GenericEditableTable = ({
|
|
|
1096
1211
|
</td>
|
|
1097
1212
|
</tr>
|
|
1098
1213
|
))}
|
|
1099
|
-
|
|
1214
|
+
|
|
1100
1215
|
{renderNewRowForm()}
|
|
1101
|
-
|
|
1102
|
-
{
|
|
1216
|
+
|
|
1217
|
+
{columns.findIndex((col) => col.type === 'total') !==
|
|
1218
|
+
-1 && (
|
|
1103
1219
|
<tr className={styles.subtotalRow}>
|
|
1104
1220
|
{columns.map((col, index) => {
|
|
1221
|
+
const totalColIndex = columns.findIndex(
|
|
1222
|
+
(c) => c.type === 'total'
|
|
1223
|
+
);
|
|
1105
1224
|
if (
|
|
1106
|
-
index ===
|
|
1107
|
-
|
|
1225
|
+
index === totalColIndex - 1 &&
|
|
1226
|
+
index > 0
|
|
1108
1227
|
) {
|
|
1109
1228
|
return <td key={col.id}>Subtotal</td>;
|
|
1110
1229
|
}
|
|
1111
|
-
if (index ===
|
|
1230
|
+
if (index === totalColIndex) {
|
|
1112
1231
|
const overallTotal = sortBySortOrder(
|
|
1113
1232
|
localData
|
|
1114
1233
|
).reduce(
|
|
@@ -1116,8 +1235,7 @@ const GenericEditableTable = ({
|
|
|
1116
1235
|
sum +
|
|
1117
1236
|
calculateTotal(
|
|
1118
1237
|
entry,
|
|
1119
|
-
columns[
|
|
1120
|
-
.keys
|
|
1238
|
+
columns[totalColIndex].keys
|
|
1121
1239
|
),
|
|
1122
1240
|
0
|
|
1123
1241
|
);
|
|
@@ -358,3 +358,51 @@
|
|
|
358
358
|
border-top: 1px dashed rgba(var(--primary-rgb), 0.3);
|
|
359
359
|
border-bottom: 1px dashed rgba(var(--primary-rgb), 0.3);
|
|
360
360
|
}
|
|
361
|
+
|
|
362
|
+
.datasetSelector {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 1rem;
|
|
366
|
+
margin-bottom: 1rem;
|
|
367
|
+
padding: 0.5rem 1rem;
|
|
368
|
+
background: rgba(var(--primary-rgb), 0.05);
|
|
369
|
+
border: 1px solid rgba(var(--primary-rgb), 0.15);
|
|
370
|
+
border-radius: var(--br);
|
|
371
|
+
|
|
372
|
+
label {
|
|
373
|
+
font-size: 0.9rem;
|
|
374
|
+
font-weight: bold;
|
|
375
|
+
color: var(--paragraph-color);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
select {
|
|
379
|
+
padding: 0.4rem 0.75rem;
|
|
380
|
+
font-size: 0.9rem;
|
|
381
|
+
border: 1px solid rgba(var(--primary-rgb), 0.1);
|
|
382
|
+
border-radius: var(--br);
|
|
383
|
+
background: #fff;
|
|
384
|
+
outline: none;
|
|
385
|
+
box-shadow: none;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* New Dataset button styling to match .btn_compact */
|
|
389
|
+
button {
|
|
390
|
+
display: inline-block;
|
|
391
|
+
padding: 0.4rem 0.75rem;
|
|
392
|
+
font-size: 1rem;
|
|
393
|
+
color: var(--tertiary-color);
|
|
394
|
+
text-decoration: none;
|
|
395
|
+
background: var(--primary-color);
|
|
396
|
+
border: 1px solid rgba(var(--primary-color--rgb), 1.1);
|
|
397
|
+
border-radius: var(--br);
|
|
398
|
+
outline: none;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
|
|
401
|
+
|
|
402
|
+
&:hover {
|
|
403
|
+
color: var(--primary-color);
|
|
404
|
+
background: var(--highlight-color);
|
|
405
|
+
border: 1px solid rgba(var(--highlight-rgb), 1.05);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|