Skip to content

Commit 8a5383e

Browse files
committed
#3884 Create Copy BOM Modal with Car/Project Selection
1 parent f4ef024 commit 8a5383e

5 files changed

Lines changed: 249 additions & 4 deletions

File tree

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

2729
const user = useCurrentUser();
@@ -57,6 +59,7 @@ const BOMTab = ({ project }: { project: Project }) => {
5759
assemblies={assemblies}
5860
/>
5961
<CreateAssemblyModal open={showAddAssembly} onHide={() => setShowAddAssembly(false)} wbsElement={project} />
62+
<CopyBOMModal open={showCopyBOM} onHide={() => setShowCopyBOM(false)} destinationWbsNum={project.wbsNum} />
6063
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
6164
<BOMTableWrapper
6265
project={project}
@@ -93,11 +96,9 @@ const BOMTab = ({ project }: { project: Project }) => {
9396
>
9497
Show All Columns
9598
</NERButton>
96-
{/*
97-
<NERButton variant="contained" onClick={() => {}} disabled={isGuest(user.role)}>
99+
<NERButton variant="contained" onClick={() => setShowCopyBOM(true)} disabled={isGuest(user.role)}>
98100
Copy Existing BOM
99101
</NERButton>
100-
*/}
101102
</Box>
102103
<Box display="flex" gap="20px" alignItems="center">
103104
<Box sx={{ backgroundColor: theme.palette.background.paper, padding: '8px 14px 8px 14px', borderRadius: '6px' }}>

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

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

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

0 commit comments

Comments
 (0)