Skip to content

Commit 224b45a

Browse files
committed
#4107 inline status dropdown
1 parent a8c2a4f commit 224b45a

3 files changed

Lines changed: 142 additions & 7 deletions

File tree

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 useEditMaterialStatus = (wbsNum: WbsNumber) => {
322+
const queryClient = useQueryClient();
323+
return useMutation<Material, Error, { materialId: string; payload: MaterialDataSubmission }>(
324+
['materials', 'edit', 'status'],
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/BOMTableCustomCells.tsx

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
/*
2+
* This file is part of NER's FinishLine and licensed under GNU AGPLv3.
3+
* See the LICENSE file in the repository root folder for details.
4+
*/
5+
6+
import { useState } from 'react';
17
import { Box } from '@mui/system';
28
import { GridRenderCellParams } from '@mui/x-data-grid';
39
import { MaterialStatus } from 'shared';
4-
import { Typography } from '@mui/material';
10+
import { Menu, MenuItem, Typography } from '@mui/material';
511
import { displayEnum } from '../../../../utils/pipes';
612

713
const getStatusColor = (status: MaterialStatus) => {
@@ -16,8 +22,6 @@ const getStatusColor = (status: MaterialStatus) => {
1622
return '#1b537a';
1723
case MaterialStatus.ReadyToOrder:
1824
return '#D34B27';
19-
default:
20-
return 'grey';
2125
}
2226
};
2327

@@ -33,6 +37,76 @@ const bomStatusChipStyle = (status: MaterialStatus) => ({
3337
textAlign: 'center'
3438
});
3539

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

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ import MoveToInboxIcon from '@mui/icons-material/MoveToInbox';
88
import { useCurrentUser } from '../../../../hooks/users.hooks';
99
import BOMTable from './BOMTable';
1010
import { useToast } from '../../../../hooks/toasts.hooks';
11-
import { useAssignMaterialToAssembly, useDeleteAssembly, useDeleteMaterial } from '../../../../hooks/bom.hooks';
11+
import {
12+
useAssignMaterialToAssembly,
13+
useDeleteAssembly,
14+
useDeleteMaterial,
15+
useEditMaterialStatus
16+
} from '../../../../hooks/bom.hooks';
1217
import LoadingIndicator from '../../../../components/LoadingIndicator';
1318
import EditMaterialModal from './MaterialForm/EditMaterialModal';
1419
import { Button, Link, Typography } from '@mui/material';
1520
import { bomBaseColDef } from '../../../../utils/bom.utils';
1621
import NERModal from '../../../../components/NERModal';
17-
import { renderStatusBOM } from './BOMTableCustomCells';
22+
import { StatusDropdownCell } from './BOMTableCustomCells';
1823
import LinkIcon from '@mui/icons-material/Link';
1924
import NotesIcon from '@mui/icons-material/Notes';
2025
import { routes } from '../../../../utils/routes';
@@ -42,6 +47,7 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
4247
const [modalShow, setModalShow] = useState(false);
4348
const { mutateAsync: deleteMaterialMutateAsync, isLoading: deleteMaterialIsLoading } = useDeleteMaterial(project.wbsNum);
4449
const { mutateAsync: deleteAssemblyMutateAsync, isLoading: deleteAssemblyIsLoading } = useDeleteAssembly(project.wbsNum);
50+
const { mutateAsync: editMaterialStatus } = useEditMaterialStatus(project.wbsNum);
4551
const { mutateAsync: assignMaterialToAssembly } = useAssignMaterialToAssembly();
4652

4753
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
@@ -275,10 +281,44 @@ const BOMTableWrapper: React.FC<BOMTableWrapperProps> = ({
275281
},
276282
{
277283
...bomBaseColDef,
278-
flex: 1.2,
284+
flex: 1.4,
279285
field: 'status',
280286
headerName: 'Status',
281-
renderCell: renderStatusBOM,
287+
renderCell: (params) => {
288+
if (!params.value || String(params.row.id).startsWith('assembly')) return null;
289+
const material = materials.find((m) => m.materialId === params.row.materialId);
290+
if (!material) return null;
291+
return (
292+
<StatusDropdownCell
293+
status={params.value}
294+
disabled={!editPerms}
295+
onStatusChange={async (newStatus) => {
296+
try {
297+
await editMaterialStatus({
298+
materialId: material.materialId,
299+
payload: {
300+
name: material.name,
301+
status: newStatus,
302+
materialTypeName: material.materialTypeName,
303+
manufacturerName: material.manufacturerName,
304+
manufacturerPartNumber: material.manufacturerPartNumber,
305+
pdmFileName: material.pdmFileName,
306+
price: material.price,
307+
quantity: material.quantity,
308+
unitName: material.unitName,
309+
linkUrl: material.linkUrl,
310+
notes: material.notes,
311+
assemblyId: material.assemblyId
312+
}
313+
});
314+
toast.success('Status updated successfully');
315+
} catch (e: unknown) {
316+
if (e instanceof Error) toast.error(e.message, 6000);
317+
}
318+
}}
319+
/>
320+
);
321+
},
282322
sortable: false,
283323
filterable: false,
284324
hide: hideColumn[1]

0 commit comments

Comments
 (0)