@visns-studio/visns-components 5.13.0 → 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.13.0",
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": [
@@ -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) =>
@@ -311,48 +375,58 @@ const GenericEditableTable = ({
311
375
  if (column.type === 'dropdown' && column.options) {
312
376
  const selectedValue = entry[column.id];
313
377
  if (!selectedValue) return undefined;
314
-
315
- // Debug logging
316
- console.log('GenericEditableTable dropdown debug:', {
317
- columnId: column.id,
318
- selectedValue,
319
- columnOptions: column.options
320
- });
321
-
322
- const selectedOption = column.options.find(option => option.id === selectedValue);
323
- console.log('Found selected option:', selectedOption);
324
-
378
+
379
+ const selectedOption = column.options.find(
380
+ (option) => option.id === selectedValue
381
+ );
382
+
325
383
  return selectedOption?.colour || undefined;
326
384
  }
327
-
385
+
328
386
  // For total columns, look for a status column with color information
329
387
  if (column.type === 'total') {
330
388
  // Find the status column (typically named 'status' and has dropdown type with options)
331
- const statusColumn = columns.find(col =>
332
- col.type === 'dropdown' &&
333
- col.options &&
334
- col.options.some(option => option.colour) &&
335
- (col.id === 'status' || col.id.toLowerCase().includes('status'))
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'))
336
396
  );
337
-
397
+
338
398
  if (statusColumn) {
339
399
  const statusValue = entry[statusColumn.id];
340
400
  if (statusValue) {
341
- const statusOption = statusColumn.options.find(option => option.id === statusValue);
342
- console.log('Total column using status color:', {
343
- totalColumnId: column.id,
344
- statusColumnId: statusColumn.id,
345
- statusValue,
346
- statusOption
347
- });
401
+ const statusOption = statusColumn.options.find(
402
+ (option) => option.id === statusValue
403
+ );
348
404
  return statusOption?.colour || undefined;
349
405
  }
350
406
  }
351
407
  }
352
-
408
+
353
409
  return undefined;
354
410
  };
355
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
+ };
356
430
 
357
431
  // Calculate total for columns of type 'total'
358
432
  const calculateTotal = (entry, keys) => {
@@ -1111,6 +1185,126 @@ const GenericEditableTable = ({
1111
1185
  }
1112
1186
  };
1113
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
+
1114
1308
  // Inline form for adding a new row
1115
1309
  const renderNewRowForm = (groupId = null) => {
1116
1310
  const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
@@ -1292,7 +1486,11 @@ const GenericEditableTable = ({
1292
1486
  >
1293
1487
  {renderRowCheckbox(entry)}
1294
1488
  {columns.map((column) => {
1295
- const cellBgColor = getCellBackground(entry, column);
1489
+ const cellBgColor =
1490
+ getCellBackground(
1491
+ entry,
1492
+ column
1493
+ );
1296
1494
  return (
1297
1495
  <td
1298
1496
  key={column.id}
@@ -1302,8 +1500,11 @@ const GenericEditableTable = ({
1302
1500
  'currency'
1303
1501
  ? 'right'
1304
1502
  : 'left',
1305
- backgroundColor: cellBgColor,
1306
- color: cellBgColor ? '#000' : 'inherit',
1503
+ backgroundColor:
1504
+ cellBgColor,
1505
+ color: cellBgColor
1506
+ ? '#000'
1507
+ : 'inherit',
1307
1508
  }}
1308
1509
  >
1309
1510
  {renderScheduledTableField(
@@ -1446,6 +1647,9 @@ const GenericEditableTable = ({
1446
1647
  )}
1447
1648
  </React.Fragment>
1448
1649
  ))}
1650
+
1651
+ {/* Render Builder Margin row */}
1652
+ {renderMarginRow()}
1449
1653
  </tbody>
1450
1654
  </table>
1451
1655
  ) : (
@@ -1474,18 +1678,26 @@ const GenericEditableTable = ({
1474
1678
  >
1475
1679
  {renderRowCheckbox(entry)}
1476
1680
  {columns.map((column) => {
1477
- const cellBgColor = getCellBackground(entry, column);
1681
+ const cellBgColor = getCellBackground(
1682
+ entry,
1683
+ column
1684
+ );
1478
1685
  return (
1479
1686
  <td
1480
1687
  key={column.id}
1481
1688
  style={{
1482
- width: column.width || 'auto',
1689
+ width:
1690
+ column.width || 'auto',
1483
1691
  textAlign:
1484
- column.type === 'currency'
1692
+ column.type ===
1693
+ 'currency'
1485
1694
  ? 'right'
1486
1695
  : 'left',
1487
- backgroundColor: cellBgColor,
1488
- color: cellBgColor ? '#000' : 'inherit',
1696
+ backgroundColor:
1697
+ cellBgColor,
1698
+ color: cellBgColor
1699
+ ? '#000'
1700
+ : 'inherit',
1489
1701
  }}
1490
1702
  >
1491
1703
  {renderScheduledTableField(
@@ -1600,6 +1812,9 @@ const GenericEditableTable = ({
1600
1812
  <td></td>
1601
1813
  </tr>
1602
1814
  )}
1815
+
1816
+ {/* Render Builder Margin row */}
1817
+ {renderMarginRow()}
1603
1818
  </tbody>
1604
1819
  </table>
1605
1820
  )}