@@ -2,6 +2,7 @@ import { Box } from '@mui/system';
22import { GridActionsCellItem , GridColumns , GridRowParams } from '@mui/x-data-grid' ;
33import { useEffect , useState } from 'react' ;
44import { Assembly , Material , Project , isLeadership } from 'shared' ;
5+ import Decimal from 'decimal.js' ;
56import DeleteIcon from '@mui/icons-material/Delete' ;
67import EditIcon from '@mui/icons-material/Edit' ;
78import MoveToInboxIcon from '@mui/icons-material/MoveToInbox' ;
@@ -12,12 +13,15 @@ import {
1213 useAssignMaterialToAssembly ,
1314 useDeleteAssembly ,
1415 useDeleteMaterial ,
15- useEditMaterialStatus
16+ useEditMaterialStatus ,
17+ useGetAllManufacturers ,
18+ useGetAllMaterialTypes
1619} from '../../../../hooks/bom.hooks' ;
1720import LoadingIndicator from '../../../../components/LoadingIndicator' ;
1821import EditMaterialModal from './MaterialForm/EditMaterialModal' ;
1922import { Button , Link , Typography } from '@mui/material' ;
20- import { bomBaseColDef } from '../../../../utils/bom.utils' ;
23+ import { BomRow , bomBaseColDef } from '../../../../utils/bom.utils' ;
24+ import { centsToDollar } from '../../../../utils/pipes' ;
2125import NERModal from '../../../../components/NERModal' ;
2226import { StatusDropdownCell } from './BOMTableCustomCells' ;
2327import LinkIcon from '@mui/icons-material/Link' ;
@@ -49,6 +53,8 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
4953 const { mutateAsync : deleteAssemblyMutateAsync , isLoading : deleteAssemblyIsLoading } = useDeleteAssembly ( project . wbsNum ) ;
5054 const { mutateAsync : editMaterialStatus } = useEditMaterialStatus ( project . wbsNum ) ;
5155 const { mutateAsync : assignMaterialToAssembly } = useAssignMaterialToAssembly ( ) ;
56+ const { data : materialTypes } = useGetAllMaterialTypes ( ) ;
57+ const { data : manufacturers } = useGetAllManufacturers ( ) ;
5258
5359 const [ windowWidth , setWindowWidth ] = useState ( window . innerWidth ) ;
5460
@@ -120,6 +126,85 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
120126 project . teams . some ( ( team ) => team . leads . map ( ( lead ) => lead . userId ) . includes ( user . userId ) ) ||
121127 project . teams . some ( ( team ) => team . members . map ( ( member ) => member . userId ) . includes ( user . userId ) ) ;
122128
129+ const processRowUpdate = async ( newRow : BomRow , oldRow : BomRow ) : Promise < BomRow > => {
130+ // assemblies are not editable
131+ if ( String ( newRow . id ) . startsWith ( 'assembly' ) ) return newRow ;
132+
133+ const material = materials . find ( ( m ) => m . materialId === newRow . materialId ) ;
134+ if ( ! material ) return newRow ;
135+
136+ // MUI writes the edited number directly to the field, so we detect changes via typeof
137+ const newQuantity = typeof newRow . quantity === 'number' ? ( newRow . quantity as number ) : null ;
138+ const newPriceDollars = typeof newRow . price === 'number' ? ( newRow . price as number ) : null ;
139+
140+ if (
141+ newRow . name === oldRow . name &&
142+ newRow . type === oldRow . type &&
143+ newRow . manufacturer === oldRow . manufacturer &&
144+ newRow . manufacturerPN === oldRow . manufacturerPN &&
145+ newRow . pdmFileName === oldRow . pdmFileName &&
146+ newQuantity === null &&
147+ newPriceDollars === null
148+ )
149+ return newRow ;
150+
151+ if ( newRow . name !== undefined && ! newRow . name . trim ( ) ) {
152+ toast . error ( 'Name cannot be empty' ) ;
153+ return oldRow ;
154+ }
155+ if ( newQuantity !== null && ( isNaN ( newQuantity ) || newQuantity <= 0 ) ) {
156+ toast . error ( 'Quantity must be a positive number' ) ;
157+ return oldRow ;
158+ }
159+ if ( newPriceDollars !== null && ( isNaN ( newPriceDollars ) || newPriceDollars < 0 ) ) {
160+ toast . error ( 'Price must be a non-negative number' ) ;
161+ return oldRow ;
162+ }
163+
164+ const changedFields : string [ ] = [ ] ;
165+ if ( newRow . name !== oldRow . name ) changedFields . push ( 'Name' ) ;
166+ if ( newRow . type !== oldRow . type ) changedFields . push ( 'Type' ) ;
167+ if ( newRow . manufacturer !== oldRow . manufacturer ) changedFields . push ( 'Manufacturer' ) ;
168+ if ( newRow . manufacturerPN !== oldRow . manufacturerPN ) changedFields . push ( 'Manufacturer PN' ) ;
169+ if ( newRow . pdmFileName !== oldRow . pdmFileName ) changedFields . push ( 'PDM File Name' ) ;
170+ if ( newQuantity !== null ) changedFields . push ( 'Quantity' ) ;
171+ if ( newPriceDollars !== null ) changedFields . push ( 'Price' ) ;
172+
173+ const priceInCents = newPriceDollars !== null ? Math . round ( newPriceDollars * 100 ) : material . price ;
174+ const quantityValue = newQuantity !== null ? newQuantity : Number ( material . quantity ) ;
175+
176+ try {
177+ await editMaterialStatus ( {
178+ materialId : material . materialId ,
179+ payload : {
180+ name : newRow . name ,
181+ status : material . status ,
182+ materialTypeName : newRow . type ,
183+ manufacturerName : newRow . manufacturer || undefined ,
184+ manufacturerPartNumber : newRow . manufacturerPN || undefined ,
185+ pdmFileName : newRow . pdmFileName ,
186+ price : priceInCents ,
187+ quantity : new Decimal ( quantityValue ) ,
188+ unitName : material . unitName ,
189+ linkUrl : material . linkUrl ,
190+ notes : material . notes ,
191+ assemblyId : material . assemblyId
192+ }
193+ } ) ;
194+ toast . success ( `Material ${ changedFields . join ( ', ' ) } updated successfully` ) ;
195+ return {
196+ ...newRow ,
197+ quantity : material . unitName ? `${ quantityValue } ${ material . unitName } ` : `${ quantityValue } ` ,
198+ quantityRaw : quantityValue ,
199+ price : priceInCents !== undefined ? `$${ centsToDollar ( priceInCents ) } ` : newRow . price ,
200+ priceRaw : priceInCents !== undefined ? priceInCents / 100 : newRow . priceRaw
201+ } ;
202+ } catch ( e : unknown ) {
203+ if ( e instanceof Error ) toast . error ( e . message , 6000 ) ;
204+ return oldRow ;
205+ }
206+ } ;
207+
123208 const selectedMaterial = materials . find ( ( material ) => material . materialId === selectedMaterialId ) ;
124209
125210 const getActions = ( params : GridRowParams ) => {
@@ -327,7 +412,9 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
327412 ...bomBaseColDef ,
328413 field : 'type' ,
329414 headerName : 'Type' ,
330- type : 'string' ,
415+ editable : editPerms ,
416+ type : 'singleSelect' ,
417+ valueOptions : materialTypes ?. map ( ( mt ) => mt . name ) ?? [ ] ,
331418 sortable : false ,
332419 filterable : false ,
333420 hide : hideColumn [ 2 ]
@@ -337,7 +424,7 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
337424 flex : 1.5 ,
338425 field : 'name' ,
339426 headerName : 'Name' ,
340- type : 'string' ,
427+ editable : editPerms ,
341428 sortable : false ,
342429 filterable : false ,
343430 hide : hideColumn [ 3 ]
@@ -347,7 +434,9 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
347434 flex : 1.2 ,
348435 field : 'manufacturer' ,
349436 headerName : 'Manufacturer' ,
350- type : 'string' ,
437+ editable : editPerms ,
438+ type : 'singleSelect' ,
439+ valueOptions : [ '' , ...( manufacturers ?. map ( ( m ) => m . name ) ?? [ ] ) ] ,
351440 sortable : false ,
352441 filterable : false ,
353442 hide : hideColumn [ 4 ]
@@ -357,7 +446,7 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
357446 flex : 1.5 ,
358447 field : 'manufacturerPN' ,
359448 headerName : 'Manufacterer PN' ,
360- type : 'string' ,
449+ editable : editPerms ,
361450 sortable : false ,
362451 filterable : false ,
363452 colSpan : ( { row } ) => {
@@ -373,7 +462,7 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
373462 flex : 1.3 ,
374463 field : 'pdmFileName' ,
375464 headerName : 'PDM File Name' ,
376- type : 'string' ,
465+ editable : editPerms ,
377466 sortable : false ,
378467 filterable : false ,
379468 hide : hideColumn [ 6 ]
@@ -383,6 +472,9 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
383472 field : 'quantity' ,
384473 headerName : 'Quantity' ,
385474 type : 'number' ,
475+ editable : editPerms ,
476+ valueGetter : ( params ) => params . row . quantityRaw ,
477+ renderCell : ( params ) => params . row . quantity ,
386478 sortable : false ,
387479 filterable : false ,
388480 hide : hideColumn [ 7 ]
@@ -392,6 +484,9 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
392484 field : 'price' ,
393485 headerName : 'Price per Unit' ,
394486 type : 'number' ,
487+ editable : editPerms ,
488+ valueGetter : ( params ) => params . row . priceRaw ,
489+ renderCell : ( params ) => params . row . price ,
395490 sortable : false ,
396491 filterable : false ,
397492 hide : hideColumn [ 8 ]
@@ -400,7 +495,6 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
400495 ...bomBaseColDef ,
401496 field : 'subtotal' ,
402497 headerName : 'Subtotal' ,
403- type : 'number' ,
404498 sortable : false ,
405499 filterable : false ,
406500 hide : hideColumn [ 9 ]
@@ -449,6 +543,11 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
449543 columns = { columns }
450544 assemblies = { assemblies }
451545 materials = { materials }
546+ processRowUpdate = { processRowUpdate }
547+ onProcessRowUpdateError = { ( error ) => {
548+ if ( error instanceof Error ) toast . error ( error . message , 6000 ) ;
549+ } }
550+ editPerms = { editPerms }
452551 />
453552 </ Box >
454553 ) ;
0 commit comments