@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.12.16",
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={{ padding: '7px' }}
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
- <td
1216
- key={column.id}
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
- entry.keyCounter
1229
- )}
1230
- </td>
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
- <td
1393
- key={column.id}
1394
- style={{
1395
- width: column.width || 'auto',
1396
- textAlign:
1397
- column.type === 'currency'
1398
- ? 'right'
1399
- : 'left',
1400
- }}
1401
- >
1402
- {renderScheduledTableField(
1403
- entry,
1404
- column,
1405
- idx
1406
- )}
1407
- </td>
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
  )}