Skip to content

Commit 2283283

Browse files
authored
Merge pull request #3990 from Northeastern-Electric-Racing/#3884-Create-Copy-BOM-Modal-With-Car/Project-Selection
#3884 Added Functional Modal to Copy BOM
2 parents 9549767 + 87cebbe commit 2283283

5 files changed

Lines changed: 249 additions & 4 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 { useCopyMaterialsToProject } from '../../../../../hooks/bom.hooks';
6+
import React from 'react';
7+
import ErrorPage from '../../../../ErrorPage';
8+
import LoadingIndicator from '../../../../../components/LoadingIndicator';
9+
10+
export interface CopyBOMModalProps {
11+
open: boolean;
12+
onHide: () => void;
13+
destinationWbsNum: WbsNumber;
14+
}
15+
16+
const CopyBOMModal: React.FC<CopyBOMModalProps> = ({ open, onHide, destinationWbsNum }) => {
17+
const { data: cars, isLoading: isLoadingCars, isError: carsIsError, error: carsError } = useGetAllCars();
18+
const { data: projects, isLoading: isLoadingProjects, isError: projectsIsError, error: projectsError } = useAllProjects();
19+
const { mutateAsync: copyMaterials } = useCopyMaterialsToProject();
20+
21+
if (isLoadingCars || !cars || isLoadingProjects || !projects) return <LoadingIndicator />;
22+
if (carsIsError) return <ErrorPage message={carsError?.message} />;
23+
if (projectsIsError) return <ErrorPage message={projectsError?.message} />;
24+
25+
const handleCopy = async (materialIds: string[]) => {
26+
await copyMaterials({
27+
materialIds,
28+
destinationWbsNum: wbsPipe(destinationWbsNum)
29+
});
30+
onHide();
31+
};
32+
33+
return <CopyBOMView open={open} onHide={onHide} cars={cars} projects={projects} onCopy={handleCopy} />;
34+
};
35+
36+
export default CopyBOMModal;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
import { Typography } from '@mui/material';
3+
import { DataGrid, GridColDef } from '@mui/x-data-grid';
4+
import { useState } from 'react';
5+
import { ProjectPreview } from 'shared';
6+
import LoadingIndicator from '../../../../../components/LoadingIndicator';
7+
import { useGetAssembliesForWbsElement, useGetMaterialsForWbsElement } from '../../../../../hooks/bom.hooks';
8+
import ErrorPage from '../../../../ErrorPage';
9+
10+
interface CopyBOMProjectSectionProps {
11+
selectedProject: ProjectPreview;
12+
onSelectionChange: (materialIds: string[]) => void;
13+
}
14+
15+
const columns: GridColDef[] = [
16+
{ field: 'materialName', headerName: 'Material Name', flex: 1 },
17+
{ field: 'manufacturer', headerName: 'Manufacturer', flex: 1 },
18+
{ field: 'materialType', headerName: 'Material Type', flex: 1 },
19+
{ field: 'assembly', headerName: 'Assembly Name', flex: 1 }
20+
];
21+
22+
const CopyBOMProjectSection: React.FC<CopyBOMProjectSectionProps> = ({ selectedProject, onSelectionChange }) => {
23+
const [selectedMaterialIds, setSelectedMaterialIds] = useState<string[]>([]);
24+
const {
25+
data: materials,
26+
isLoading: isLoadingMaterials,
27+
isError: isErrorMaterials,
28+
error: materialsError
29+
} = useGetMaterialsForWbsElement(selectedProject.wbsNum);
30+
31+
const {
32+
data: assemblies,
33+
isLoading: isLoadingAssemblies,
34+
isError: isErrorAssemblies,
35+
error: assembliesError
36+
} = useGetAssembliesForWbsElement(selectedProject.wbsNum);
37+
38+
React.useEffect(() => {
39+
if (materials) {
40+
const allIds = materials.map((m) => m.materialId);
41+
setSelectedMaterialIds(allIds);
42+
onSelectionChange(allIds);
43+
}
44+
}, [materials, onSelectionChange]);
45+
46+
if (isLoadingMaterials || isLoadingAssemblies || !materials || !assemblies) return <LoadingIndicator />;
47+
if (isErrorMaterials) return <ErrorPage message={materialsError?.message} />;
48+
if (isErrorAssemblies) return <ErrorPage message={assembliesError?.message} />;
49+
50+
const rows = materials.map((m) => ({
51+
id: m.materialId,
52+
materialName: m.name,
53+
manufacturer: m.manufacturer?.name ?? '-',
54+
materialType: m.materialType.name,
55+
assembly: assemblies.find((a) => a.assemblyId === m.assemblyId)?.name ?? '-'
56+
}));
57+
58+
return (
59+
<>
60+
<Typography sx={{ mb: 1 }} variant="body2">
61+
{selectedMaterialIds.length} material{selectedMaterialIds.length !== 1 ? 's' : ''} selected
62+
</Typography>
63+
<DataGrid
64+
rows={rows}
65+
columns={columns}
66+
checkboxSelection
67+
autoHeight
68+
selectionModel={selectedMaterialIds}
69+
onSelectionModelChange={(newModel) => {
70+
const ids = newModel as string[];
71+
setSelectedMaterialIds(ids);
72+
onSelectionChange(ids);
73+
}}
74+
rowsPerPageOptions={[100]}
75+
hideFooterPagination
76+
sx={{
77+
'& .MuiDataGrid-columnHeaders': { backgroundColor: '#ef4345', color: 'white' },
78+
'& .MuiDataGrid-columnHeaders .MuiCheckbox-root': { color: 'white' }
79+
}}
80+
/>
81+
</>
82+
);
83+
};
84+
85+
export default CopyBOMProjectSection;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { useRef, useState } from 'react';
2+
import { Box, Grid } from '@mui/material';
3+
import { Car, ProjectPreview, wbsPipe } from 'shared';
4+
import NERModal from '../../../../../components/NERModal';
5+
import NERAutocomplete from '../../../../../components/NERAutocomplete';
6+
import CopyBOMProjectSection from './CopyBOMProjectSection';
7+
8+
interface CopyBOMViewProps {
9+
open: boolean;
10+
onHide: () => void;
11+
cars: Car[];
12+
projects: ProjectPreview[];
13+
onCopy: (materialIds: string[]) => Promise<void>;
14+
}
15+
16+
const CopyBOMView: React.FC<CopyBOMViewProps> = ({ open, onHide, cars, projects, onCopy }) => {
17+
const [selectedCar, setSelectedCar] = useState<Car | null>(null);
18+
const [selectedProject, setSelectedProject] = useState<ProjectPreview | null>(null);
19+
const [hasSelection, setHasSelection] = useState(false);
20+
const selectedMaterialIdsRef = useRef<string[]>([]);
21+
22+
const carOptions = cars.map((car) => ({
23+
label: `${car.wbsNum.carNumber} - ${car.name}`,
24+
id: car.id
25+
}));
26+
27+
const filteredProjects = selectedCar
28+
? projects.filter((p) => p.wbsNum.carNumber === selectedCar.wbsNum.carNumber)
29+
: projects;
30+
31+
const projectOptions = filteredProjects.map((p) => ({
32+
label: `${wbsPipe(p.wbsNum)} - ${p.name}`,
33+
id: wbsPipe(p.wbsNum)
34+
}));
35+
36+
const handleSubmit = async () => {
37+
await onCopy(selectedMaterialIdsRef.current);
38+
};
39+
40+
return (
41+
<NERModal
42+
open={open}
43+
onHide={onHide}
44+
title="Copy Existing BOM"
45+
submitText="Copy BOM"
46+
cancelText="Cancel"
47+
onSubmit={handleSubmit}
48+
disabled={!hasSelection}
49+
showCloseButton
50+
paperProps={{ minWidth: '700px' }}
51+
>
52+
<Grid container spacing={2}>
53+
<Grid item xs={6}>
54+
<NERAutocomplete
55+
id="car-autocomplete"
56+
options={carOptions}
57+
onChange={(_event, newValue) => {
58+
const car = newValue ? (cars.find((c) => c.id === newValue.id) ?? null) : null;
59+
setSelectedCar(car);
60+
setSelectedProject(null);
61+
}}
62+
value={
63+
selectedCar ? { label: `${selectedCar.wbsNum.carNumber} - ${selectedCar.name}`, id: selectedCar.id } : null
64+
}
65+
placeholder="Select Car"
66+
size="medium"
67+
/>
68+
</Grid>
69+
<Grid item xs={6}>
70+
<NERAutocomplete
71+
id="project-autocomplete"
72+
options={projectOptions}
73+
onChange={(_event, newValue) => {
74+
const project = newValue ? (filteredProjects.find((p) => wbsPipe(p.wbsNum) === newValue.id) ?? null) : null;
75+
setSelectedProject(project);
76+
}}
77+
value={
78+
selectedProject
79+
? {
80+
label: `${wbsPipe(selectedProject.wbsNum)} - ${selectedProject.name}`,
81+
id: wbsPipe(selectedProject.wbsNum)
82+
}
83+
: null
84+
}
85+
placeholder="Select Project"
86+
size="medium"
87+
disabled={!selectedCar}
88+
/>
89+
</Grid>
90+
91+
<Grid item xs={12}>
92+
{selectedProject ? (
93+
<CopyBOMProjectSection
94+
selectedProject={selectedProject}
95+
onSelectionChange={(ids) => {
96+
selectedMaterialIdsRef.current = ids;
97+
setHasSelection(ids.length > 0);
98+
}}
99+
/>
100+
) : (
101+
<Box
102+
sx={{
103+
height: '300px',
104+
border: '1px dashed',
105+
borderColor: 'grey.400',
106+
borderRadius: '4px',
107+
display: 'flex',
108+
alignItems: 'center',
109+
justifyContent: 'center',
110+
color: 'grey.500'
111+
}}
112+
>
113+
Select a project to view its materials
114+
</Box>
115+
)}
116+
</Grid>
117+
</Grid>
118+
</NERModal>
119+
);
120+
};
121+
122+
export default CopyBOMView;

src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOMTab.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useState } from 'react';
77
import BOMTableWrapper from './BOM/BOMTableWrapper';
88
import CreateMaterialModal from './BOM/MaterialForm/CreateMaterialModal';
99
import CreateAssemblyModal from './BOM/AssemblyForm/CreateAssemblyModal';
10+
import CopyBOMModal from './BOM/CopyBOM/CopyBOMModal';
1011
import NERSuccessButton from '../../../components/NERSuccessButton';
1112
import { centsToDollar } from '../../../utils/pipes';
1213
import { useCurrentUser } from '../../../hooks/users.hooks';
@@ -28,6 +29,7 @@ const BOMTab = ({ project }: { project: Project }) => {
2829
const [hideColumn, setHideColumn] = useState<boolean[]>(initialHideColumn);
2930
const [showAddMaterial, setShowAddMaterial] = useState(false);
3031
const [showAddAssembly, setShowAddAssembly] = useState(false);
32+
const [showCopyBOM, setShowCopyBOM] = useState(false);
3133
const [showImportBOM, setShowImportBOM] = useState(false);
3234
const theme = useTheme();
3335

@@ -97,6 +99,7 @@ const BOMTab = ({ project }: { project: Project }) => {
9799
allUnits={units}
98100
assemblies={assemblies}
99101
/>
102+
<CopyBOMModal open={showCopyBOM} onHide={() => setShowCopyBOM(false)} destinationWbsNum={project.wbsNum} />
100103
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
101104
<BOMTableWrapper
102105
project={project}
@@ -141,11 +144,9 @@ const BOMTab = ({ project }: { project: Project }) => {
141144
>
142145
Show All Columns
143146
</NERButton>
144-
{/*
145-
<NERButton variant="contained" onClick={() => {}} disabled={isGuest(user.role)}>
147+
<NERButton variant="contained" onClick={() => setShowCopyBOM(true)} disabled={isGuest(user.role)}>
146148
Copy Existing BOM
147149
</NERButton>
148-
*/}
149150
</Box>
150151
<Box display="flex" gap="20px" alignItems="center">
151152
<Box sx={{ backgroundColor: theme.palette.background.paper, padding: '8px 14px 8px 14px', borderRadius: '6px' }}>

src/frontend/src/utils/teams.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type SubmitText =
4646
| 'Create Change Request'
4747
| 'Update'
4848
| 'Submit Vendor'
49-
| 'Accept';
49+
| 'Accept'
50+
| 'Copy BOM';
5051

5152
export type CancelText = 'Cancel' | 'Delete' | 'Exit' | 'No';

0 commit comments

Comments
 (0)