@visns-studio/visns-components 5.12.16 → 5.13.1
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/README.md
CHANGED
|
@@ -36,6 +36,7 @@ VISNS Components is a React-based UI component library that provides a set of re
|
|
|
36
36
|
- **Dropzone** - File upload with drag and drop support
|
|
37
37
|
- **AsyncSelect** - Asynchronous data loading for select components
|
|
38
38
|
- **Creatable Select** - Select components that allow creating new options
|
|
39
|
+
- **GenericEditableTable** - Interactive table component with inline editing, color-coded columns, and multiple data types
|
|
39
40
|
|
|
40
41
|
### Navigation
|
|
41
42
|
|
|
@@ -328,9 +329,10 @@ The DataGrid component utilizes a modular column renderer system with 28 special
|
|
|
328
329
|
- `currency` - Formatted monetary values
|
|
329
330
|
- `date` - Date formatting with validation
|
|
330
331
|
- `datetime` - Date and time formatting
|
|
332
|
+
- `html5_date` - HTML5 native date picker input
|
|
331
333
|
- `arrayCount` - Count of array elements
|
|
332
334
|
- `colour` - Color picker display
|
|
333
|
-
- `dropdown` - Interactive dropdown selections
|
|
335
|
+
- `dropdown` - Interactive dropdown selections with optional background color support
|
|
334
336
|
- `image` - Image display from file URLs
|
|
335
337
|
- `file` - File download links with icons
|
|
336
338
|
- `icons` - Multiple icon display
|
|
@@ -355,6 +357,79 @@ The DataGrid component utilizes a modular column renderer system with 28 special
|
|
|
355
357
|
|
|
356
358
|
All column renderers are exported from `@visns-studio/visns-components` and can be used individually or as part of the DataGrid component.
|
|
357
359
|
|
|
360
|
+
#### Dropdown Column Color Support
|
|
361
|
+
|
|
362
|
+
The dropdown column type now supports automatic background color application based on the selected option's `colour` property. This feature works in both DataGrid and GenericEditableTable components.
|
|
363
|
+
|
|
364
|
+
**Configuration Example:**
|
|
365
|
+
```jsx
|
|
366
|
+
{
|
|
367
|
+
id: 'status',
|
|
368
|
+
label: 'Status',
|
|
369
|
+
type: 'dropdown',
|
|
370
|
+
options: [
|
|
371
|
+
{ id: 'Pending', label: 'Pending', colour: '#FFE5B4' },
|
|
372
|
+
{ id: 'Approved', label: 'Approved', colour: '#B4E5D1' },
|
|
373
|
+
{ id: 'Rejected', label: 'Rejected', colour: '#FFCDD2' },
|
|
374
|
+
{ id: 'Disputed', label: 'Disputed', colour: '#E1F5FE' }
|
|
375
|
+
],
|
|
376
|
+
width: '10%'
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Features:**
|
|
381
|
+
- **Automatic Color Application**: Cells automatically display the background color of the selected option
|
|
382
|
+
- **Total Column Integration**: Total columns inherit the same background color as the status column in the same row
|
|
383
|
+
- **Consistent Styling**: Black text color is automatically applied for optimal contrast
|
|
384
|
+
- **Flexible Configuration**: Works with both static column options and dynamic dropdown data
|
|
385
|
+
- **Cross-Component Support**: Available in both DataGrid and GenericEditableTable components
|
|
386
|
+
|
|
387
|
+
**Example Usage in GenericEditableTable:**
|
|
388
|
+
```jsx
|
|
389
|
+
<GenericEditableTable
|
|
390
|
+
schedulingConfig={{
|
|
391
|
+
columns: [
|
|
392
|
+
{
|
|
393
|
+
id: 'status',
|
|
394
|
+
label: 'Status',
|
|
395
|
+
type: 'dropdown',
|
|
396
|
+
options: [
|
|
397
|
+
{ id: 'Pending', label: 'Pending', colour: '#FFE5B4' },
|
|
398
|
+
{ id: 'Approved', label: 'Approved', colour: '#B4E5D1' }
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: 'total',
|
|
403
|
+
label: 'Total',
|
|
404
|
+
type: 'total',
|
|
405
|
+
keys: ['amount', 'tax'] // Will inherit status color
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
}}
|
|
409
|
+
/>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### HTML5 Date Type
|
|
413
|
+
|
|
414
|
+
The `html5_date` column type provides native HTML5 date picker functionality in GenericEditableTable components.
|
|
415
|
+
|
|
416
|
+
**Features:**
|
|
417
|
+
- **Native Date Picker**: Uses browser's built-in date picker for consistent UX
|
|
418
|
+
- **Cross-browser Support**: Works across all modern browsers
|
|
419
|
+
- **Consistent Styling**: Matches existing date field styling
|
|
420
|
+
- **Read-only Support**: Respects column read-only configuration
|
|
421
|
+
- **Change Integration**: Fully integrated with existing field change handlers
|
|
422
|
+
|
|
423
|
+
**Configuration Example:**
|
|
424
|
+
```jsx
|
|
425
|
+
{
|
|
426
|
+
id: 'start_date',
|
|
427
|
+
label: 'Start Date',
|
|
428
|
+
type: 'html5_date',
|
|
429
|
+
width: '15%'
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
358
433
|
#### Intelligent Relationship Sorting
|
|
359
434
|
|
|
360
435
|
The DataGrid component includes intelligent sorting capabilities that automatically detect and enable sorting for relationship and JSON fields. This feature works seamlessly with the backend `HasRelationshipSorting` trait.
|
|
@@ -629,6 +704,12 @@ You can add summary calculations to grouped data:
|
|
|
629
704
|
|
|
630
705
|
The DataGrid component now supports powerful bulk editing capabilities through group-based operations, allowing users to efficiently manage multiple items simultaneously.
|
|
631
706
|
|
|
707
|
+
**New in v5.13.0:**
|
|
708
|
+
- **Dropdown Column Color Support**: Automatic background color application for dropdown columns based on option colors
|
|
709
|
+
- **Total Column Color Integration**: Total columns now inherit background colors from associated status columns
|
|
710
|
+
- **HTML5 Date Type**: Added native HTML5 date picker support for GenericEditableTable components
|
|
711
|
+
- **Enhanced GenericEditableTable**: Improved column type support and consistent styling across components
|
|
712
|
+
|
|
632
713
|
#### How Group Bulk Edit Works
|
|
633
714
|
|
|
634
715
|
When data is grouped using the `groupBy` or `defaultGroupBy` properties, each group header can display action icons for common operations. The new bulk edit functionality adds an "edit" icon that opens a form modal for updating all items in the group simultaneously.
|
package/package.json
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
88
88
|
},
|
|
89
89
|
"name": "@visns-studio/visns-components",
|
|
90
|
-
"version": "5.
|
|
90
|
+
"version": "5.13.1",
|
|
91
91
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
92
92
|
"main": "src/index.js",
|
|
93
93
|
"files": [
|
|
@@ -354,8 +354,14 @@ export const renderDropdownColumn = ({
|
|
|
354
354
|
return (
|
|
355
355
|
<select
|
|
356
356
|
name={column.id}
|
|
357
|
-
value={data[column.id]?.id || ''}
|
|
358
|
-
style={{
|
|
357
|
+
value={data[column.id]?.id || data[column.id] || ''}
|
|
358
|
+
style={{
|
|
359
|
+
padding: '7px',
|
|
360
|
+
width: '100%',
|
|
361
|
+
border: 'none',
|
|
362
|
+
backgroundColor: 'transparent',
|
|
363
|
+
outline: 'none',
|
|
364
|
+
}}
|
|
359
365
|
onChange={(e) => {
|
|
360
366
|
e.preventDefault();
|
|
361
367
|
|
|
@@ -384,10 +390,55 @@ export const renderDropdownColumn = ({
|
|
|
384
390
|
{option.label}
|
|
385
391
|
</option>
|
|
386
392
|
))
|
|
393
|
+
: column.options && column.options.length > 0
|
|
394
|
+
? column.options.map((option, index) => (
|
|
395
|
+
<option value={option.id} key={index}>
|
|
396
|
+
{option.label}
|
|
397
|
+
</option>
|
|
398
|
+
))
|
|
387
399
|
: null}
|
|
388
400
|
</select>
|
|
389
401
|
);
|
|
390
402
|
},
|
|
403
|
+
cellProps: ({ data }) => {
|
|
404
|
+
// Find the selected option to get its color
|
|
405
|
+
const selectedValue = data[column.id]?.id || data[column.id] || '';
|
|
406
|
+
let selectedOption = null;
|
|
407
|
+
|
|
408
|
+
// Debug logging
|
|
409
|
+
console.log('Dropdown cellProps debug:', {
|
|
410
|
+
columnId: column.id,
|
|
411
|
+
selectedValue,
|
|
412
|
+
dataValue: data[column.id],
|
|
413
|
+
dropdownData: dropdownData[column.id],
|
|
414
|
+
columnOptions: column.options
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (selectedValue && dropdownData[column.id]) {
|
|
418
|
+
selectedOption = dropdownData[column.id].find(option => option.id === selectedValue);
|
|
419
|
+
console.log('Found option in dropdownData:', selectedOption);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// If no option found in dropdownData, check if column has options array
|
|
423
|
+
if (!selectedOption && selectedValue && column.options) {
|
|
424
|
+
selectedOption = column.options.find(option => option.id === selectedValue);
|
|
425
|
+
console.log('Found option in column.options:', selectedOption);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Return cell props with background color
|
|
429
|
+
if (selectedOption?.colour) {
|
|
430
|
+
console.log('Applying background color:', selectedOption.colour);
|
|
431
|
+
return {
|
|
432
|
+
style: {
|
|
433
|
+
backgroundColor: selectedOption.colour,
|
|
434
|
+
color: '#000',
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log('No color found, returning empty props');
|
|
440
|
+
return {};
|
|
441
|
+
},
|
|
391
442
|
};
|
|
392
443
|
};
|
|
393
444
|
|
|
@@ -214,6 +214,7 @@ const GenericEditableTable = ({
|
|
|
214
214
|
const { columns, rows, form, title } = schedulingConfig;
|
|
215
215
|
const hasCategory = rows && rows.categoryKey; // Only group if defined
|
|
216
216
|
const hasColourColumn = columns.some((col) => col.id === 'colours');
|
|
217
|
+
const hasMarginRow = rows && rows.marginRow;
|
|
217
218
|
|
|
218
219
|
// dataSets: each item => { created_at: string, data: array of rows }
|
|
219
220
|
const [dataSets, setDataSets] = useState([]);
|
|
@@ -228,6 +229,59 @@ const GenericEditableTable = ({
|
|
|
228
229
|
// Row selection state for copy functionality
|
|
229
230
|
const [selectedRows, setSelectedRows] = useState(new Set());
|
|
230
231
|
|
|
232
|
+
// Margin percentage state for Builder Margin row
|
|
233
|
+
const [marginPercentage, setMarginPercentage] = useState(0);
|
|
234
|
+
|
|
235
|
+
// Handle margin percentage change
|
|
236
|
+
const handleMarginPercentageChange = (percentage) => {
|
|
237
|
+
setMarginPercentage(percentage);
|
|
238
|
+
|
|
239
|
+
// Update or create margin row in localData
|
|
240
|
+
setLocalData((prevData) => {
|
|
241
|
+
const updatedData = [...prevData];
|
|
242
|
+
const marginRowIndex = updatedData.findIndex(
|
|
243
|
+
(entry) => entry.description === rows.marginRow
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const totalProjectCost = calculateTotalProjectCost(updatedData);
|
|
247
|
+
const marginAmount = calculateMarginAmount(
|
|
248
|
+
totalProjectCost,
|
|
249
|
+
percentage
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const marginRowData = {
|
|
253
|
+
id: 'margin-row',
|
|
254
|
+
description: rows.marginRow,
|
|
255
|
+
category: rows.marginRow,
|
|
256
|
+
sort_order: 9999, // Ensure it's always at the bottom
|
|
257
|
+
margin_percentage: percentage,
|
|
258
|
+
[columns.find((col) => col.type === 'total')?.id]: marginAmount,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (marginRowIndex !== -1) {
|
|
262
|
+
// Update existing margin row
|
|
263
|
+
updatedData[marginRowIndex] = {
|
|
264
|
+
...updatedData[marginRowIndex],
|
|
265
|
+
...marginRowData,
|
|
266
|
+
};
|
|
267
|
+
} else {
|
|
268
|
+
// Add new margin row
|
|
269
|
+
updatedData.push(marginRowData);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Update the active dataset
|
|
273
|
+
const updatedDataSets = [...dataSets];
|
|
274
|
+
updatedDataSets[activeDatasetIndex] = {
|
|
275
|
+
...updatedDataSets[activeDatasetIndex],
|
|
276
|
+
data: updatedData,
|
|
277
|
+
};
|
|
278
|
+
setDataSets(updatedDataSets);
|
|
279
|
+
debouncedUpdate(updatedDataSets);
|
|
280
|
+
|
|
281
|
+
return updatedData;
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
231
285
|
// On mount or when 'data' changes, read from data[schedulingConfig.rows.key] and handle both new & old formats
|
|
232
286
|
useEffect(() => {
|
|
233
287
|
const raw = data && data[schedulingConfig.rows.key];
|
|
@@ -287,12 +341,22 @@ const GenericEditableTable = ({
|
|
|
287
341
|
sort_order: entry.sort_order ?? index + 1,
|
|
288
342
|
}));
|
|
289
343
|
setLocalData(sortBySortOrder(enrichedData));
|
|
344
|
+
|
|
345
|
+
// Initialize margin percentage from existing margin row
|
|
346
|
+
if (hasMarginRow) {
|
|
347
|
+
const marginRow = enrichedData.find(
|
|
348
|
+
(entry) => entry.description === rows.marginRow
|
|
349
|
+
);
|
|
350
|
+
if (marginRow && marginRow.margin_percentage) {
|
|
351
|
+
setMarginPercentage(marginRow.margin_percentage);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
290
354
|
} else {
|
|
291
355
|
setLocalData([]);
|
|
292
356
|
}
|
|
293
357
|
// Clear selected rows when dataset changes
|
|
294
358
|
setSelectedRows(new Set());
|
|
295
|
-
}, [dataSets, activeDatasetIndex]);
|
|
359
|
+
}, [dataSets, activeDatasetIndex, hasMarginRow, rows.marginRow]);
|
|
296
360
|
|
|
297
361
|
// Helper to sort by sort_order
|
|
298
362
|
const sortBySortOrder = (dataArray) =>
|
|
@@ -305,6 +369,65 @@ const GenericEditableTable = ({
|
|
|
305
369
|
return coloursValue ? `${coloursValue}40` : undefined; // Add 40% opacity
|
|
306
370
|
};
|
|
307
371
|
|
|
372
|
+
// Helper function to get background color for dropdown and total cells
|
|
373
|
+
const getCellBackground = (entry, column) => {
|
|
374
|
+
// For dropdown columns, get color from selected option
|
|
375
|
+
if (column.type === 'dropdown' && column.options) {
|
|
376
|
+
const selectedValue = entry[column.id];
|
|
377
|
+
if (!selectedValue) return undefined;
|
|
378
|
+
|
|
379
|
+
const selectedOption = column.options.find(
|
|
380
|
+
(option) => option.id === selectedValue
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return selectedOption?.colour || undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// For total columns, look for a status column with color information
|
|
387
|
+
if (column.type === 'total') {
|
|
388
|
+
// Find the status column (typically named 'status' and has dropdown type with options)
|
|
389
|
+
const statusColumn = columns.find(
|
|
390
|
+
(col) =>
|
|
391
|
+
col.type === 'dropdown' &&
|
|
392
|
+
col.options &&
|
|
393
|
+
col.options.some((option) => option.colour) &&
|
|
394
|
+
(col.id === 'status' ||
|
|
395
|
+
col.id.toLowerCase().includes('status'))
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (statusColumn) {
|
|
399
|
+
const statusValue = entry[statusColumn.id];
|
|
400
|
+
if (statusValue) {
|
|
401
|
+
const statusOption = statusColumn.options.find(
|
|
402
|
+
(option) => option.id === statusValue
|
|
403
|
+
);
|
|
404
|
+
return statusOption?.colour || undefined;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return undefined;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Calculate total project cost (sum of all entries excluding margin row)
|
|
413
|
+
const calculateTotalProjectCost = (dataEntries) => {
|
|
414
|
+
const totalColIndex = columns.findIndex((c) => c.type === 'total');
|
|
415
|
+
if (totalColIndex === -1) return 0;
|
|
416
|
+
|
|
417
|
+
const totalColumn = columns[totalColIndex];
|
|
418
|
+
return dataEntries
|
|
419
|
+
.filter((entry) => entry.description !== rows.marginRow) // Exclude margin row
|
|
420
|
+
.reduce(
|
|
421
|
+
(sum, entry) => sum + calculateTotal(entry, totalColumn.keys),
|
|
422
|
+
0
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Calculate margin amount based on percentage
|
|
427
|
+
const calculateMarginAmount = (totalProjectCost, percentage) => {
|
|
428
|
+
return totalProjectCost * (percentage / 100);
|
|
429
|
+
};
|
|
430
|
+
|
|
308
431
|
// Calculate total for columns of type 'total'
|
|
309
432
|
const calculateTotal = (entry, keys) => {
|
|
310
433
|
let total = 0;
|
|
@@ -813,6 +936,22 @@ const GenericEditableTable = ({
|
|
|
813
936
|
readOnly={column.readOnly || false}
|
|
814
937
|
/>
|
|
815
938
|
);
|
|
939
|
+
case 'html5_date':
|
|
940
|
+
return (
|
|
941
|
+
<input
|
|
942
|
+
type="date"
|
|
943
|
+
value={entry[column.id] || ''}
|
|
944
|
+
onChange={(e) =>
|
|
945
|
+
handleFieldChange(
|
|
946
|
+
e.target.value,
|
|
947
|
+
column.id,
|
|
948
|
+
keyCounter
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
style={{ textAlign: 'right' }}
|
|
952
|
+
readOnly={column.readOnly || false}
|
|
953
|
+
/>
|
|
954
|
+
);
|
|
816
955
|
case 'dropdown':
|
|
817
956
|
return (
|
|
818
957
|
<select
|
|
@@ -951,6 +1090,21 @@ const GenericEditableTable = ({
|
|
|
951
1090
|
style={{ textAlign: 'right' }}
|
|
952
1091
|
/>
|
|
953
1092
|
);
|
|
1093
|
+
case 'html5_date':
|
|
1094
|
+
return (
|
|
1095
|
+
<input
|
|
1096
|
+
type="date"
|
|
1097
|
+
value={value || ''}
|
|
1098
|
+
onChange={(e) =>
|
|
1099
|
+
handleNewFieldChange(
|
|
1100
|
+
e.target.value,
|
|
1101
|
+
column.id,
|
|
1102
|
+
groupId
|
|
1103
|
+
)
|
|
1104
|
+
}
|
|
1105
|
+
style={{ textAlign: 'right' }}
|
|
1106
|
+
/>
|
|
1107
|
+
);
|
|
954
1108
|
case 'dropdown':
|
|
955
1109
|
return (
|
|
956
1110
|
<select
|
|
@@ -1031,6 +1185,126 @@ const GenericEditableTable = ({
|
|
|
1031
1185
|
}
|
|
1032
1186
|
};
|
|
1033
1187
|
|
|
1188
|
+
// Render Builder Margin row - should appear after all categories
|
|
1189
|
+
const renderMarginRow = () => {
|
|
1190
|
+
if (!hasMarginRow) return null;
|
|
1191
|
+
|
|
1192
|
+
const totalProjectCost = calculateTotalProjectCost(localData);
|
|
1193
|
+
const marginAmount = calculateMarginAmount(
|
|
1194
|
+
totalProjectCost,
|
|
1195
|
+
marginPercentage
|
|
1196
|
+
);
|
|
1197
|
+
const totalWithMargin = totalProjectCost + marginAmount;
|
|
1198
|
+
|
|
1199
|
+
return (
|
|
1200
|
+
<>
|
|
1201
|
+
{/* Margin row category header */}
|
|
1202
|
+
<tr className={styles.categoryRow}>
|
|
1203
|
+
<td
|
|
1204
|
+
colSpan={columns.length + 2}
|
|
1205
|
+
style={{
|
|
1206
|
+
backgroundColor: '#e9ecef',
|
|
1207
|
+
fontWeight: 'bold',
|
|
1208
|
+
padding: '8px 12px',
|
|
1209
|
+
}}
|
|
1210
|
+
>
|
|
1211
|
+
{rows.marginRow}
|
|
1212
|
+
</td>
|
|
1213
|
+
</tr>
|
|
1214
|
+
|
|
1215
|
+
{/* Margin row column headers - only show Description, Margin %, and Total */}
|
|
1216
|
+
<tr>
|
|
1217
|
+
{/* Description column - larger width */}
|
|
1218
|
+
<th
|
|
1219
|
+
style={{
|
|
1220
|
+
color: 'white',
|
|
1221
|
+
fontWeight: 'bold',
|
|
1222
|
+
padding: '8px',
|
|
1223
|
+
textAlign: 'left',
|
|
1224
|
+
width: '60%',
|
|
1225
|
+
}}
|
|
1226
|
+
colSpan={6}
|
|
1227
|
+
>
|
|
1228
|
+
Description
|
|
1229
|
+
</th>
|
|
1230
|
+
{/* Margin % column - smaller width */}
|
|
1231
|
+
<th
|
|
1232
|
+
style={{
|
|
1233
|
+
color: 'white',
|
|
1234
|
+
fontWeight: 'bold',
|
|
1235
|
+
padding: '8px',
|
|
1236
|
+
textAlign: 'left',
|
|
1237
|
+
width: '20%',
|
|
1238
|
+
}}
|
|
1239
|
+
colSpan={3}
|
|
1240
|
+
>
|
|
1241
|
+
Margin %
|
|
1242
|
+
</th>
|
|
1243
|
+
{/* Total column */}
|
|
1244
|
+
<th
|
|
1245
|
+
style={{
|
|
1246
|
+
color: 'white',
|
|
1247
|
+
fontWeight: 'bold',
|
|
1248
|
+
padding: '8px',
|
|
1249
|
+
textAlign: 'right',
|
|
1250
|
+
width: '20%',
|
|
1251
|
+
}}
|
|
1252
|
+
>
|
|
1253
|
+
Total
|
|
1254
|
+
</th>
|
|
1255
|
+
<th></th>
|
|
1256
|
+
</tr>
|
|
1257
|
+
|
|
1258
|
+
{/* Margin row content */}
|
|
1259
|
+
<tr style={{ backgroundColor: '#f8f9fa' }}>
|
|
1260
|
+
{/* Description */}
|
|
1261
|
+
<td
|
|
1262
|
+
style={{ fontWeight: 'bold', padding: '8px' }}
|
|
1263
|
+
colSpan={6}
|
|
1264
|
+
>
|
|
1265
|
+
{rows.marginRow}
|
|
1266
|
+
</td>
|
|
1267
|
+
{/* Margin % input */}
|
|
1268
|
+
<td
|
|
1269
|
+
style={{ textAlign: 'center', padding: '8px' }}
|
|
1270
|
+
colSpan={3}
|
|
1271
|
+
>
|
|
1272
|
+
<input
|
|
1273
|
+
type="number"
|
|
1274
|
+
value={marginPercentage}
|
|
1275
|
+
onChange={(e) =>
|
|
1276
|
+
handleMarginPercentageChange(
|
|
1277
|
+
parseFloat(e.target.value) || 0
|
|
1278
|
+
)
|
|
1279
|
+
}
|
|
1280
|
+
placeholder="% margin"
|
|
1281
|
+
style={{
|
|
1282
|
+
width: '80px',
|
|
1283
|
+
padding: '6px',
|
|
1284
|
+
border: '1px solid #ccc',
|
|
1285
|
+
borderRadius: '3px',
|
|
1286
|
+
}}
|
|
1287
|
+
/>
|
|
1288
|
+
</td>
|
|
1289
|
+
{/* Total (subtotal of all categories + margin) */}
|
|
1290
|
+
<td
|
|
1291
|
+
style={{
|
|
1292
|
+
textAlign: 'right',
|
|
1293
|
+
fontWeight: 'bold',
|
|
1294
|
+
padding: '8px',
|
|
1295
|
+
}}
|
|
1296
|
+
>
|
|
1297
|
+
{totalWithMargin.toLocaleString('en-AU', {
|
|
1298
|
+
style: 'currency',
|
|
1299
|
+
currency: 'AUD',
|
|
1300
|
+
})}
|
|
1301
|
+
</td>
|
|
1302
|
+
<td></td>
|
|
1303
|
+
</tr>
|
|
1304
|
+
</>
|
|
1305
|
+
);
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1034
1308
|
// Inline form for adding a new row
|
|
1035
1309
|
const renderNewRowForm = (groupId = null) => {
|
|
1036
1310
|
const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
|
|
@@ -1211,24 +1485,36 @@ const GenericEditableTable = ({
|
|
|
1211
1485
|
}}
|
|
1212
1486
|
>
|
|
1213
1487
|
{renderRowCheckbox(entry)}
|
|
1214
|
-
{columns.map((column) =>
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
style={{
|
|
1218
|
-
textAlign:
|
|
1219
|
-
column.type ===
|
|
1220
|
-
'currency'
|
|
1221
|
-
? 'right'
|
|
1222
|
-
: 'left',
|
|
1223
|
-
}}
|
|
1224
|
-
>
|
|
1225
|
-
{renderScheduledTableField(
|
|
1488
|
+
{columns.map((column) => {
|
|
1489
|
+
const cellBgColor =
|
|
1490
|
+
getCellBackground(
|
|
1226
1491
|
entry,
|
|
1227
|
-
column
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1492
|
+
column
|
|
1493
|
+
);
|
|
1494
|
+
return (
|
|
1495
|
+
<td
|
|
1496
|
+
key={column.id}
|
|
1497
|
+
style={{
|
|
1498
|
+
textAlign:
|
|
1499
|
+
column.type ===
|
|
1500
|
+
'currency'
|
|
1501
|
+
? 'right'
|
|
1502
|
+
: 'left',
|
|
1503
|
+
backgroundColor:
|
|
1504
|
+
cellBgColor,
|
|
1505
|
+
color: cellBgColor
|
|
1506
|
+
? '#000'
|
|
1507
|
+
: 'inherit',
|
|
1508
|
+
}}
|
|
1509
|
+
>
|
|
1510
|
+
{renderScheduledTableField(
|
|
1511
|
+
entry,
|
|
1512
|
+
column,
|
|
1513
|
+
entry.keyCounter
|
|
1514
|
+
)}
|
|
1515
|
+
</td>
|
|
1516
|
+
);
|
|
1517
|
+
})}
|
|
1232
1518
|
<td>
|
|
1233
1519
|
<div
|
|
1234
1520
|
className={styles.tdactions}
|
|
@@ -1361,6 +1647,9 @@ const GenericEditableTable = ({
|
|
|
1361
1647
|
)}
|
|
1362
1648
|
</React.Fragment>
|
|
1363
1649
|
))}
|
|
1650
|
+
|
|
1651
|
+
{/* Render Builder Margin row */}
|
|
1652
|
+
{renderMarginRow()}
|
|
1364
1653
|
</tbody>
|
|
1365
1654
|
</table>
|
|
1366
1655
|
) : (
|
|
@@ -1388,36 +1677,39 @@ const GenericEditableTable = ({
|
|
|
1388
1677
|
}}
|
|
1389
1678
|
>
|
|
1390
1679
|
{renderRowCheckbox(entry)}
|
|
1391
|
-
{columns.map((column) =>
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1680
|
+
{columns.map((column) => {
|
|
1681
|
+
const cellBgColor = getCellBackground(
|
|
1682
|
+
entry,
|
|
1683
|
+
column
|
|
1684
|
+
);
|
|
1685
|
+
return (
|
|
1686
|
+
<td
|
|
1687
|
+
key={column.id}
|
|
1688
|
+
style={{
|
|
1689
|
+
width:
|
|
1690
|
+
column.width || 'auto',
|
|
1691
|
+
textAlign:
|
|
1692
|
+
column.type ===
|
|
1693
|
+
'currency'
|
|
1694
|
+
? 'right'
|
|
1695
|
+
: 'left',
|
|
1696
|
+
backgroundColor:
|
|
1697
|
+
cellBgColor,
|
|
1698
|
+
color: cellBgColor
|
|
1699
|
+
? '#000'
|
|
1700
|
+
: 'inherit',
|
|
1701
|
+
}}
|
|
1702
|
+
>
|
|
1703
|
+
{renderScheduledTableField(
|
|
1704
|
+
entry,
|
|
1705
|
+
column,
|
|
1706
|
+
idx
|
|
1707
|
+
)}
|
|
1708
|
+
</td>
|
|
1709
|
+
);
|
|
1710
|
+
})}
|
|
1409
1711
|
<td>
|
|
1410
1712
|
<div className={styles.tdactions}>
|
|
1411
|
-
{!hasColourColumn && (
|
|
1412
|
-
<ColourPicker
|
|
1413
|
-
value={
|
|
1414
|
-
entry.row_colour || ''
|
|
1415
|
-
}
|
|
1416
|
-
columnId={'row_colour'}
|
|
1417
|
-
keyCounter={idx}
|
|
1418
|
-
onChange={handleFieldChange}
|
|
1419
|
-
/>
|
|
1420
|
-
)}
|
|
1421
1713
|
{idx > 0 && (
|
|
1422
1714
|
<Tooltip text="Move up">
|
|
1423
1715
|
<ArrowUp
|
|
@@ -1520,6 +1812,9 @@ const GenericEditableTable = ({
|
|
|
1520
1812
|
<td></td>
|
|
1521
1813
|
</tr>
|
|
1522
1814
|
)}
|
|
1815
|
+
|
|
1816
|
+
{/* Render Builder Margin row */}
|
|
1817
|
+
{renderMarginRow()}
|
|
1523
1818
|
</tbody>
|
|
1524
1819
|
</table>
|
|
1525
1820
|
)}
|