Skip to content

Commit 74b4fad

Browse files
authored
Merge pull request #4141 from Northeastern-Electric-Racing/#4107-maintenance-bom-inline-editing
#4107 BOM inline editing
2 parents 7563def + 626f17b commit 74b4fad

13 files changed

Lines changed: 307 additions & 55 deletions

File tree

src/backend/src/controllers/projects.controllers.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ export default class ProjectsController {
220220
quantity,
221221
unitName,
222222
price,
223-
subtotal,
224223
linkUrl,
225224
notes
226225
} = req.body;
@@ -237,7 +236,6 @@ export default class ProjectsController {
237236
manufacturerPartNumber,
238237
quantity,
239238
price,
240-
subtotal,
241239
notes,
242240
assemblyId,
243241
pdmFileName,
@@ -397,7 +395,6 @@ export default class ProjectsController {
397395
quantity,
398396
unitName,
399397
price,
400-
subtotal,
401398
linkUrl,
402399
notes
403400
} = req.body;
@@ -413,7 +410,6 @@ export default class ProjectsController {
413410
manufacturerPartNumber,
414411
quantity,
415412
price,
416-
subtotal,
417413
notes,
418414
unitName,
419415
assemblyId,

src/backend/src/prisma/seed.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3537,7 +3537,6 @@ const performSeed: () => Promise<void> = async () => {
35373537
'abcdef',
35383538
new Decimal(20),
35393539
30,
3540-
600,
35413540
'Here are some notes',
35423541
assembly1.assemblyId,
35433542
undefined,
@@ -3560,7 +3559,6 @@ const performSeed: () => Promise<void> = async () => {
35603559
'bacfed',
35613560
new Decimal(10),
35623561
7,
3563-
70,
35643562
'Here are some more notes',
35653563
undefined,
35663564
undefined,
@@ -3583,7 +3581,6 @@ const performSeed: () => Promise<void> = async () => {
35833581
'lalsd',
35843582
new Decimal(5),
35853583
10,
3586-
50,
35873584
undefined,
35883585
undefined,
35893586
undefined

src/backend/src/services/boms.services.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export default class BillOfMaterialsService {
5656
* @param manufacturerPartNumber the manufacturer part number for the material (optional)
5757
* @param quantity the quantity of material as a number (optional)
5858
* @param price the price of the material in whole cents (optional)
59-
* @param subtotal the subtotal of the price for the material in whole cents (optional)
6059
* @param notes any notes about the material as a string (optional)
6160
* @param assemblyId the id of the Assembly for the material (optional)
6261
* @param pdmFileName the name of the pdm file for the material (optional)
@@ -75,7 +74,6 @@ export default class BillOfMaterialsService {
7574
manufacturerPartNumber?: string,
7675
quantity?: Decimal,
7776
price?: number,
78-
subtotal?: number,
7977
notes?: string,
8078
assemblyId?: string,
8179
pdmFileName?: string,
@@ -119,6 +117,9 @@ export default class BillOfMaterialsService {
119117

120118
if (!perms) throw new AccessDeniedException('create materials');
121119

120+
const computedSubtotal =
121+
price !== undefined && quantity !== undefined ? Math.round(price * Number(quantity)) : undefined;
122+
122123
const createdMaterial = await prisma.material.create({
123124
data: {
124125
userCreatedId: creator.userId,
@@ -132,7 +133,7 @@ export default class BillOfMaterialsService {
132133
quantity,
133134
unitId: unit ? unit.id : null,
134135
price,
135-
subtotal,
136+
subtotal: computedSubtotal,
136137
linkUrl,
137138
notes,
138139
dateCreated: new Date(),
@@ -611,7 +612,6 @@ export default class BillOfMaterialsService {
611612
* @param manufacturerPartNumber the manufacturerPartNumber of the edited material (optional)
612613
* @param quantity the quantity of the edited material (optional)
613614
* @param price the price of the edited material (optional)
614-
* @param subtotal the subtotal of the edited material (optional)
615615
* @param notes the notes of the edited material (optional)
616616
* @param unitName the unit name of the edited material (optional)
617617
* @param assemblyId the assembly id of the edited material (optional)
@@ -631,7 +631,6 @@ export default class BillOfMaterialsService {
631631
manufacturerPartNumber?: string,
632632
quantity?: Decimal,
633633
price?: number,
634-
subtotal?: number,
635634
notes?: string,
636635
unitName?: string,
637636
assemblyId?: string,
@@ -670,6 +669,12 @@ export default class BillOfMaterialsService {
670669
manufacturer = await BillOfMaterialsService.getSingleManufacturerWithQueryArgs(manufacturerName, organization);
671670
}
672671

672+
// recalculate subtotal on edits
673+
const finalPrice = price ?? material.price ?? undefined;
674+
const finalQuantity = quantity ?? material.quantity ?? undefined;
675+
const computedSubtotal =
676+
finalPrice !== undefined && finalQuantity !== undefined ? Math.round(finalPrice * Number(finalQuantity)) : undefined;
677+
673678
const updatedMaterial = await prisma.material.update({
674679
where: { materialId },
675680
data: {
@@ -681,12 +686,12 @@ export default class BillOfMaterialsService {
681686
quantity,
682687
unitId: unit ? unit.id : null,
683688
price,
684-
subtotal,
689+
subtotal: computedSubtotal,
685690
linkUrl,
686691
notes,
687692
wbsElementId: project.wbsElementId,
688693
assemblyId,
689-
pdmFileName
694+
pdmFileName: pdmFileName !== undefined ? pdmFileName || null : undefined
690695
},
691696
...getMaterialQueryArgs(organization.organizationId)
692697
});

src/backend/src/utils/validation.utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ export const materialValidators = [
284284
decimalMinZero(body('quantity')).optional(),
285285
nonEmptyString(body('unitName')).optional(),
286286
intMinZero(body('price')).optional(), // in cents
287-
intMinZero(body('subtotal')).optional(), // in cents
288287
body('linkUrl').optional().isString(),
289288
body('notes').isString().optional()
290289
];

src/backend/tests/unmocked/project.test.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ describe('Material Tests', () => {
4040
manufacturer.name,
4141
'lalsd',
4242
new Decimal(5),
43-
10,
44-
50
43+
10
4544
);
4645

4746
expect(material.name).toEqual('100k Resistor');
@@ -99,7 +98,6 @@ describe('Material Tests', () => {
9998
'CAP-100UF',
10099
new Decimal(10),
101100
50,
102-
500,
103101
'Test notes'
104102
);
105103

@@ -114,8 +112,7 @@ describe('Material Tests', () => {
114112
manufacturer.name,
115113
'CAP-220UF',
116114
new Decimal(5),
117-
75,
118-
375
115+
75
119116
);
120117

121118
const newMaterialIds = await BillOfMaterials.copyMaterialsToProject(
@@ -185,8 +182,7 @@ describe('Material Tests', () => {
185182
manufacturer.name,
186183
'lalsd',
187184
new Decimal(5),
188-
10,
189-
50
185+
10
190186
);
191187

192188
const newMaterial = await BillOfMaterials.editMaterial(
@@ -200,8 +196,7 @@ describe('Material Tests', () => {
200196
manufacturer.name,
201197
'lalsd',
202198
new Decimal(5),
203-
10,
204-
50
199+
10
205200
);
206201

207202
expect(newMaterial.name).toEqual('100k Resistor Updated');

src/backend/tests/unmocked/reimbursement-requests.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,8 +1170,7 @@ describe('Reimbursement Requests', () => {
11701170
manufacturerId: manufacturer.id,
11711171
linkUrl: 'https://example.com',
11721172
quantity: 1,
1173-
price: 100,
1174-
subtotal: 100
1173+
price: 100
11751174
}
11761175
});
11771176
});
@@ -1276,8 +1275,7 @@ describe('Reimbursement Requests', () => {
12761275
manufacturerId: material.manufacturerId,
12771276
linkUrl: 'https://example.com',
12781277
quantity: 2,
1279-
price: 200,
1280-
subtotal: 400
1278+
price: 200
12811279
}
12821280
});
12831281

src/frontend/src/hooks/bom.hooks.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,27 @@ export const useCreateMaterialType = () => {
313313
);
314314
};
315315

316+
/**
317+
* Custom React hook to edit a material's status inline.
318+
* @param wbsNum The wbs element the material belongs to
319+
* @returns mutation function to edit a material's status
320+
*/
321+
export const useEditMaterialById = (wbsNum: WbsNumber) => {
322+
const queryClient = useQueryClient();
323+
return useMutation<Material, Error, { materialId: string; payload: MaterialDataSubmission }>(
324+
['materials', 'edit'],
325+
async ({ materialId, payload }) => {
326+
const data = await editMaterial(materialId, payload);
327+
return data;
328+
},
329+
{
330+
onSuccess: () => {
331+
queryClient.invalidateQueries(['materials', wbsPipe(wbsNum)]);
332+
}
333+
}
334+
);
335+
};
336+
316337
export const useGetAssembliesForWbsElement = (wbsNum: WbsNumber) => {
317338
return useQuery<Assembly[], Error>(['assemblies', wbsPipe(wbsNum)], async () => {
318339
const { data } = await getAssembliesForWbsElement(wbsNum);

src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTable.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,21 @@ interface BOMTableProps {
1313
columns: GridColumns<BomRow>;
1414
materials: Material[];
1515
assemblies: Assembly[];
16+
processRowUpdate: (newRow: BomRow, oldRow: BomRow) => Promise<BomRow>;
17+
onProcessRowUpdateError: (error: unknown) => void;
18+
editPerms: boolean;
1619
}
1720

18-
const BOMTable: React.FC<BOMTableProps> = ({ setHideColumn, assignMaterial, columns, materials, assemblies }) => {
21+
const BOMTable: React.FC<BOMTableProps> = ({
22+
setHideColumn,
23+
assignMaterial,
24+
columns,
25+
materials,
26+
assemblies,
27+
processRowUpdate,
28+
onProcessRowUpdateError,
29+
editPerms
30+
}) => {
1931
const [openRows, setOpenRows] = useState<String[]>([]);
2032
const [draggedMaterial, setDraggedMaterial] = useState<Material | null>(null);
2133

@@ -58,8 +70,9 @@ const BOMTable: React.FC<BOMTableProps> = ({ setHideColumn, assignMaterial, colu
5870
assembly.materials.reduce(addMaterialCosts, 0)
5971
)} ${arrowSymbol(assembly.assemblyId)}`,
6072
pdmFileName: '',
61-
quantity: '',
62-
price: '',
73+
quantity: undefined,
74+
price: undefined,
75+
unitName: undefined,
6376
subtotal: '',
6477
link: '',
6578
notes: '',
@@ -139,13 +152,19 @@ const BOMTable: React.FC<BOMTableProps> = ({ setHideColumn, assignMaterial, colu
139152
rows={rows.concat(materialsWithAssemblies.filter(isAssemblyOpen))}
140153
getRowClassName={(params) => {
141154
const stripe = params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd';
142-
const isAssemblyRow = String(params.row.id).startsWith('assembly-');
155+
const isAssemblyRow = params.row.id.startsWith('assembly-');
143156
return `super-app-theme--${stripe}${isAssemblyRow ? ' super-app-theme--assembly' : ''}`;
144157
}}
145158
rowsPerPageOptions={[100]}
146159
sx={bomTableStyles.datagrid}
147160
disableSelectionOnClick
148161
autoHeight={false}
162+
experimentalFeatures={{ newEditingApi: true }}
163+
processRowUpdate={
164+
processRowUpdate as unknown as (newRow: GridValidRowModel, oldRow: GridValidRowModel) => Promise<GridValidRowModel>
165+
}
166+
onProcessRowUpdateError={onProcessRowUpdateError}
167+
isCellEditable={(params) => editPerms && params.row.id.startsWith('assembly')}
149168
onRowClick={openAssembly}
150169
componentsProps={{
151170
row: {

src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableCustomCells.tsx

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { useState } from 'react';
12
import { Box } from '@mui/system';
23
import { GridRenderCellParams } from '@mui/x-data-grid';
34
import { MaterialStatus } from 'shared';
4-
import { Typography } from '@mui/material';
5+
import { Menu, MenuItem, Typography } from '@mui/material';
56
import { displayEnum } from '../../../../utils/pipes';
67

78
const getStatusColor = (status: MaterialStatus) => {
@@ -33,6 +34,76 @@ const bomStatusChipStyle = (status: MaterialStatus) => ({
3334
textAlign: 'center'
3435
});
3536

37+
interface StatusDropdownCellProps {
38+
status: MaterialStatus;
39+
disabled?: boolean;
40+
onStatusChange: (newStatus: MaterialStatus) => void;
41+
}
42+
43+
export const StatusDropdownCell: React.FC<StatusDropdownCellProps> = ({ status, disabled, onStatusChange }) => {
44+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
45+
46+
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
47+
event.stopPropagation();
48+
if (!disabled) setAnchorEl(event.currentTarget);
49+
};
50+
51+
const handleClose = () => setAnchorEl(null);
52+
53+
const handleSelect = (newStatus: MaterialStatus) => {
54+
onStatusChange(newStatus);
55+
handleClose();
56+
};
57+
58+
return (
59+
<>
60+
<Box
61+
sx={{
62+
...bomStatusChipStyle(status),
63+
cursor: disabled ? 'default' : 'pointer',
64+
gap: '2px'
65+
}}
66+
onClick={handleClick}
67+
>
68+
<Typography fontSize={{ xs: '11px', sm: '14px' }} color="black">
69+
{displayEnum(status)}
70+
</Typography>
71+
</Box>
72+
<Menu
73+
anchorEl={anchorEl}
74+
open={Boolean(anchorEl)}
75+
onClose={handleClose}
76+
slotProps={{
77+
paper: {
78+
sx: {
79+
padding: 0,
80+
background: 'transparent',
81+
borderRadius: '6px',
82+
overflow: 'hidden'
83+
}
84+
},
85+
list: { disablePadding: true }
86+
}}
87+
>
88+
{Object.values(MaterialStatus)
89+
.filter((s) => s !== status)
90+
.map((s) => {
91+
const chipStyle = bomStatusChipStyle(s);
92+
return (
93+
<MenuItem key={s} onClick={() => handleSelect(s)} sx={{ padding: 0 }}>
94+
<Box sx={{ ...chipStyle, borderRadius: 0, minWidth: '130px', width: '100%' }}>
95+
<Typography fontSize="14px" color="black">
96+
{displayEnum(s)}
97+
</Typography>
98+
</Box>
99+
</MenuItem>
100+
);
101+
})}
102+
</Menu>
103+
</>
104+
);
105+
};
106+
36107
export const renderStatusBOM = (params: GridRenderCellParams) => {
37108
if (!params.value) return;
38109
const status = params.value as MaterialStatus;

0 commit comments

Comments
 (0)