Skip to content

Commit 51c6ab1

Browse files
authored
Merge pull request #4116 from Northeastern-Electric-Racing/feature/bom-improvement-pt2
BOM Usability feature merge
2 parents 2ff07f5 + 4e9a4a9 commit 51c6ab1

18 files changed

Lines changed: 689 additions & 18 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Material" ADD COLUMN "isCopied" BOOLEAN NOT NULL DEFAULT false;

src/backend/src/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,7 @@ model Material {
10201020
linkUrl String
10211021
notes String?
10221022
reimbursementProducts Reimbursement_Product[]
1023+
isCopied Boolean @default(false)
10231024
10241025
@@index([assemblyId])
10251026
@@index([materialTypeId])

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ export default class BillOfMaterialsService {
204204
dateCreated: new Date(),
205205
userCreatedId: user.userId,
206206
wbsElementId: destinationProject.wbsElementId,
207-
assemblyId: null
207+
assemblyId: null,
208+
isCopied: true
208209
},
209210
...getMaterialQueryArgs(organization.organizationId)
210211
});

src/backend/src/transformers/material.transformer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export const materialTransformer = (material: Prisma.MaterialGetPayload<Material
4848
.filter((p) => p.reimbursementRequest && !p.reimbursementRequest.dateDeleted)
4949
.map((p) => [p.reimbursementRequest!.reimbursementRequestId, p.reimbursementRequest!])
5050
).values()
51-
)
51+
),
52+
isCopied: material.isCopied
5253
};
5354
};
5455

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('Material Tests', () => {
5252
expect(material.manufacturerName).toEqual('Digikey');
5353
expect(material.manufacturerPartNumber).toEqual('lalsd');
5454
expect(material.quantity?.toString()).toEqual('5');
55+
expect(material.isCopied).toBe(false);
5556
});
5657
});
5758

@@ -148,6 +149,9 @@ describe('Material Tests', () => {
148149
expect(copiedMat1.notes).toBe('Test notes');
149150

150151
expect(copiedMat2.status).toBe('NOT_READY_TO_ORDER');
152+
153+
expect(copiedMat1.isCopied).toBe(true);
154+
expect(copiedMat2.isCopied).toBe(true);
151155
});
152156

153157
test('Fails when material does not exist', async () => {

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useMutation, useQuery, useQueryClient } from 'react-query';
2-
import { Assembly, Manufacturer, Material, MaterialType, Unit, WbsNumber, wbsPipe } from 'shared';
2+
import { Assembly, Manufacturer, Material, MaterialType, ProjectPreview, Unit, WbsNumber, wbsPipe } from 'shared';
33
import { useToast } from '../hooks/toasts.hooks';
44
import {
55
assignMaterialToAssembly,
@@ -326,3 +326,33 @@ export const useGetMaterialsForWbsElement = (wbsNum: WbsNumber) => {
326326
return data;
327327
});
328328
};
329+
330+
export const useGetMaterialsForCar = (carNumber: number | null, projects: ProjectPreview[]) => {
331+
const projectsInCar = projects.filter((p) => p.wbsNum.carNumber === carNumber);
332+
333+
return useQuery<Material[], Error>(
334+
['materials', 'car', carNumber ?? 'none'],
335+
async () => {
336+
const results = await Promise.all(
337+
projectsInCar.map(async (p) => {
338+
const { data } = await getMaterialsForWbsElement({
339+
carNumber: p.wbsNum.carNumber,
340+
projectNumber: p.wbsNum.projectNumber,
341+
workPackageNumber: 0
342+
});
343+
return data;
344+
})
345+
);
346+
347+
const flat = results.flat();
348+
const seen = new Set<string>();
349+
return flat.filter((material) => {
350+
const key = `${material.name.toLowerCase()}-${material.assemblyId ?? 'no-assembly'}`;
351+
if (seen.has(key)) return false;
352+
seen.add(key);
353+
return true;
354+
});
355+
},
356+
{ enabled: carNumber !== null && projectsInCar.length > 0 }
357+
);
358+
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ const BOMTable: React.FC<BOMTableProps> = ({ setHideColumn, assignMaterial, colu
6363
subtotal: '',
6464
link: '',
6565
notes: '',
66-
assemblyId: assembly.assemblyId
66+
assemblyId: assembly.assemblyId,
67+
isCopied: false
6768
});
6869

6970
assemblyMaterials.forEach((material, indx) => materialsWithAssemblies.push(materialToRow(material, indx)));

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { useToast } from '../../../../hooks/toasts.hooks';
1111
import { useAssignMaterialToAssembly, useDeleteAssembly, useDeleteMaterial } from '../../../../hooks/bom.hooks';
1212
import LoadingIndicator from '../../../../components/LoadingIndicator';
1313
import EditMaterialModal from './MaterialForm/EditMaterialModal';
14-
import { Button, Link, Typography } from '@mui/material';
14+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
15+
import { Button, Link, Tooltip, Typography } from '@mui/material';
1516
import { bomBaseColDef } from '../../../../utils/bom.utils';
1617
import NERModal from '../../../../components/NERModal';
1718
import { renderStatusBOM } from './BOMTableCustomCells';
@@ -300,7 +301,20 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
300301
type: 'string',
301302
sortable: false,
302303
filterable: false,
303-
hide: hideColumn[3]
304+
hide: hideColumn[3],
305+
renderCell: (params) => {
306+
const material = materials.find((m) => m.materialId === params.row.materialId);
307+
return (
308+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
309+
<Typography variant="body2">{params.value}</Typography>
310+
{material?.isCopied && (
311+
<Tooltip title="Copied from another BOM">
312+
<ContentCopyIcon sx={{ fontSize: 14, color: 'warning.main' }} />
313+
</Tooltip>
314+
)}
315+
</Box>
316+
);
317+
}
304318
},
305319
{
306320
...bomBaseColDef,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import NERModal from '../../../../../components/NERModal';
2+
import { useCopyMaterialsToProject } from '../../../../../hooks/bom.hooks';
3+
4+
export interface BOMCopyConfirmModalProps {
5+
open: boolean;
6+
onHide: () => void;
7+
onSuccess: () => void;
8+
materialIds: string[];
9+
sourceProjectName: string;
10+
currentProjectName: string;
11+
destinationWbsNum: string;
12+
}
13+
14+
const BOMCopyConfirmModal = ({
15+
open,
16+
onHide,
17+
onSuccess,
18+
materialIds,
19+
sourceProjectName,
20+
currentProjectName,
21+
destinationWbsNum
22+
}: BOMCopyConfirmModalProps) => {
23+
const { mutateAsync: copyMaterials } = useCopyMaterialsToProject();
24+
25+
const handleConfirm = async () => {
26+
await copyMaterials({ materialIds, destinationWbsNum });
27+
onSuccess();
28+
onHide();
29+
};
30+
31+
const message = `Are you sure you want to copy ${materialIds.length} materials from ${sourceProjectName} to ${currentProjectName}?`;
32+
return (
33+
<NERModal open={open} onHide={onHide} onSubmit={handleConfirm} title="Confirm Copy">
34+
<p>{message}</p>
35+
</NERModal>
36+
);
37+
};
38+
39+
export default BOMCopyConfirmModal;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { WbsNumber, wbsPipe } from 'shared';
2+
import CopyBOMView from './CopyBOMView';
3+
import { useGetAllCars } from '../../../../../hooks/cars.hooks';
4+
import { useAllProjects } from '../../../../../hooks/projects.hooks';
5+
import React, { useState } from 'react';
6+
import ErrorPage from '../../../../ErrorPage';
7+
import LoadingIndicator from '../../../../../components/LoadingIndicator';
8+
import BOMCopyConfirmModal from './BOMCopyConfirmModal';
9+
10+
export interface CopyBOMModalProps {
11+
open: boolean;
12+
onHide: () => void;
13+
destinationWbsNum: WbsNumber;
14+
currentProjectName: string;
15+
}
16+
17+
const CopyBOMModal: React.FC<CopyBOMModalProps> = ({ open, onHide, destinationWbsNum, currentProjectName }) => {
18+
const { data: cars, isLoading: isLoadingCars, isError: carsIsError, error: carsError } = useGetAllCars();
19+
const { data: projects, isLoading: isLoadingProjects, isError: projectsIsError, error: projectsError } = useAllProjects();
20+
const [confirmOpen, setConfirmOpen] = useState(false);
21+
const [confirmedMaterialIds, setConfirmedMaterialIds] = useState<string[]>([]);
22+
const [confirmedSourceProjectName, setConfirmedSourceProjectName] = useState('');
23+
24+
if (carsIsError) return <ErrorPage message={carsError?.message} />;
25+
if (projectsIsError) return <ErrorPage message={projectsError?.message} />;
26+
if (isLoadingCars || !cars || isLoadingProjects || !projects) return <LoadingIndicator />;
27+
28+
const destinationWbs = wbsPipe(destinationWbsNum);
29+
30+
return (
31+
<>
32+
<CopyBOMView
33+
open={open}
34+
onHide={onHide}
35+
cars={cars}
36+
projects={projects}
37+
onCopy={(materialIds, sourceProjectName) => {
38+
setConfirmedMaterialIds(materialIds);
39+
setConfirmedSourceProjectName(sourceProjectName);
40+
setConfirmOpen(true);
41+
}}
42+
/>
43+
<BOMCopyConfirmModal
44+
open={confirmOpen}
45+
onHide={() => setConfirmOpen(false)}
46+
onSuccess={() => {
47+
onHide();
48+
setConfirmOpen(false);
49+
}}
50+
materialIds={confirmedMaterialIds}
51+
sourceProjectName={confirmedSourceProjectName}
52+
currentProjectName={`${wbsPipe(destinationWbsNum)} - ${currentProjectName}`}
53+
destinationWbsNum={destinationWbs}
54+
/>
55+
</>
56+
);
57+
};
58+
59+
export default CopyBOMModal;

0 commit comments

Comments
 (0)