Skip to content

Commit 2d5f6d7

Browse files
authored
Quick Materials Deduction (#1307)
1 parent 22948d6 commit 2d5f6d7

14 files changed

Lines changed: 440 additions & 56 deletions

File tree

client/src/AppRouter.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import HoursDisplay from "./pages/signage/HoursDisplay";
2121
import TopNav from "./top_nav/TopNav";
2222
import QuizPage from "./pages/maker/take_quiz/QuizPage";
2323
import QuizResults from "./pages/maker/take_quiz/QuizResults";
24-
import InventoryPage from "./pages/lab_management/inventory/InventoryPage";
24+
import AdminInventoryPage from "./pages/lab_management/inventory/AdminInventoryPage";
2525
import AnnouncementsPage from "./pages/lab_management/announcements/AnnouncementsPage";
2626
import EditAnnouncement from "./pages/lab_management/announcements/EditAnnouncement";
2727
import NewAnnouncementPage from "./pages/lab_management/announcements/NewAnnouncementPage";
@@ -48,6 +48,8 @@ import ReservationRequestPage from "./pages/makerspace_page/reservation_pages/Re
4848
import ManageReservationsPage from "./pages/makerspace_page/reservation_pages/ManageReservationsPage";
4949
import MaintenancePage from "./pages/makerspace_page/maintenance_pages/MaintenancePage";
5050
import { Box } from "@mui/material";
51+
import InventoryPage from "./pages/makerspace_page/inventory_pages/InventoryPage";
52+
import QuickEditInventoryPage from "./pages/makerspace_page/inventory_pages/QuickEditInventoryPage";
5153

5254
function AppRoot() {
5355
return (
@@ -159,7 +161,6 @@ export const appRouter = createBrowserRouter(
159161
children: [
160162
{ path: "/makerspace/:makerspaceID/people", element: <UsersPage /> },
161163
{ path: "/makerspace/:makerspaceID/people/:userID", element: <UserPage /> },
162-
{ path: "/makerspace/:makerspaceID/inventory", element: <InventoryPage /> },
163164
{ path: "/makerspace/:makerspaceID/storefront/carts", element: <CartListPage /> },
164165
{ path: "/makerspace/:makerspaceID/storefront/carts/:cartID", element: <CartPage /> },
165166

@@ -174,6 +175,10 @@ export const appRouter = createBrowserRouter(
174175
{ path: "/makerspace/:makerspaceID/equipment/new", element: <NewEquipmentPage /> },
175176
{ path: "/makerspace/:makerspaceID/equipment/:equipmentID", element: <ManageEquipmentPage /> },
176177

178+
{ path: "/makerspace/:makerspaceID/inventory", element: <InventoryPage /> },
179+
{ path: "/makerspace/:makerspaceID/inventory/quick/item/:invID", element: <QuickEditInventoryPage fromTag={false} /> },
180+
{ path: "/makerspace/:makerspaceID/inventory/quick/tag/:invID", element: <QuickEditInventoryPage fromTag={true} /> },
181+
177182
{ path: "/makerspace/:makerspaceID/tools", element: <ToolItemPage /> },
178183
{ path: "/makerspace/:makerspaceID/tools/type/:typeid", element: <ToolItemPage /> },
179184
{ path: "/makerspace/:makerspaceID/tools/type/", element: <ToolItemPage /> },
@@ -211,6 +216,8 @@ export const appRouter = createBrowserRouter(
211216
{ path: "/admin/announcements/:id", element: <EditAnnouncement /> },
212217
{ path: "/admin/announcements/new", element: <NewAnnouncementPage /> },
213218

219+
{ path: "/admin/inventory", element: <AdminInventoryPage /> },
220+
214221
{ path: "/admin/newreader", element: <NewReaderPage /> },
215222
{ path: "/admin/settings", element: <SiteSettingsPage /> },
216223
],
@@ -220,8 +227,6 @@ export const appRouter = createBrowserRouter(
220227
{ path: "/maker/training/:id", element: <QuizPage /> },
221228
{ path: "/maker/training/:id/results/", element: <QuizResults /> },
222229
{ path: "/maker/training/:id/results/:submissionID", element: <QuizResults /> },
223-
224-
{ path: "/admin/inventory", element: <InventoryPage /> },
225230
],
226231
},
227232
/* END OF PROTECTED ROUTES */

client/src/pages/lab_management/inventory/InventoryPage.tsx renamed to client/src/pages/lab_management/inventory/AdminInventoryPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import LowInventory from "./common/LowInventory";
1616
import StaffBar from "../../makerspace_page/StaffBar";
1717

1818

19-
export default function InventoryPage() {
19+
export default function AdminInventoryPage() {
2020
const [searchText, setSearchText] = useState<string>("");
2121
const [modalItemId, setModalItemId] = useState<string>("");
2222
const [tagsModalOpen, setTagsModalOpen] = useState<boolean>(false);
@@ -38,7 +38,7 @@ export default function InventoryPage() {
3838

3939
setSearchText(queryString)
4040
}, [location.search]);
41-
41+
4242
return (
4343
<RequestWrapper loading={makerspacesWithItemsResult.loading} error={makerspacesWithItemsResult.error}>
4444
<AdminPage>
@@ -55,7 +55,7 @@ export default function InventoryPage() {
5555
placeholder="Search inventory"
5656
value={searchText}
5757
onChange={(e) => setSearchText(e.target.value)}
58-
onSubmit={() => setUrlParam("a", searchText)}
58+
onSubmit={() => setUrlParam("a", searchText)}
5959
onClear={() => setSearchText("")}
6060
/>
6161
<Button

client/src/pages/lab_management/inventory/common/InventoryForMakerspace.tsx

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,23 @@ export function InventoryForMakerspace(props: InventoryForMakerspaceProps) {
7878
];
7979

8080
return (
81-
<Box>
82-
<PageSectionHeader>{props.makerspace.name}</PageSectionHeader>
83-
<Box sx={{ width: "100%", overflowX: "scroll" }}>
84-
<DataGrid
85-
rows={matchingItems}
86-
columns={columns}
87-
rowHeight={70}
88-
initialState={{
89-
pagination: {
90-
paginationModel: {
91-
pageSize: 50,
92-
},
81+
<Box sx={{ width: "100%" }}>
82+
<DataGrid
83+
rows={matchingItems}
84+
columns={columns}
85+
rowHeight={70}
86+
initialState={{
87+
pagination: {
88+
paginationModel: {
89+
pageSize: 50,
9390
},
94-
}}
95-
pageSizeOptions={[50]}
96-
//checkboxSelection
97-
disableRowSelectionOnClick
98-
/>
99-
</Box>
91+
},
92+
}}
93+
pageSizeOptions={[50]}
94+
//checkboxSelection
95+
disableRowSelectionOnClick
96+
disableColumnResize
97+
/>
10098
</Box>
10199
)
102100
}
Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IconButton, Select, MenuItem, Chip } from "@mui/material";
1+
import { IconButton, Select, MenuItem, Chip, Autocomplete, TextField } from "@mui/material";
22
import { Stack } from "@mui/system";
33
import InventoryItem, { InventoryTag } from "../../../../types/InventoryItem";
44
import InventoryTagChip from "../InventoryTagChip";
@@ -15,37 +15,38 @@ interface TagsCellProps {
1515

1616
export function TagsCell(props: TagsCellProps) {
1717
const [addTag] = useMutation(ADD_TAG_TO_ITEM, {
18-
refetchQueries: [{ query: GET_INVENTORY_ITEMS }] });
18+
refetchQueries: [{ query: GET_INVENTORY_ITEMS }]
19+
});
1920

2021
const [removeTag] = useMutation(REMOVE_TAG_FROM_ITEM, {
21-
refetchQueries: [{ query: GET_INVENTORY_ITEMS }] });
22+
refetchQueries: [{ query: GET_INVENTORY_ITEMS }]
23+
});
2224

2325
const handleRemoveTagClick = (id: number) => {
24-
removeTag({variables: {itemID: props.item.id, tagID: id}});
26+
removeTag({ variables: { itemID: props.item.id, tagID: id } });
2527
}
2628

2729
const [showTagsDropdown, setShowTagsDropdown] = useState<boolean>(false);
2830

2931
const handleAddTagClick = (id: number) => {
30-
addTag({variables: {itemID: props.item.id, tagID: id}});
32+
addTag({ variables: { itemID: props.item.id, tagID: id } });
3133
setShowTagsDropdown(false);
3234
}
3335

34-
3536
return (
3637
<Stack direction={"row"} flexWrap={"wrap"}>
37-
{props.item.tags && props.item.tags.map((tag: InventoryTag) => (
38-
tag && <InventoryTagChip id={tag.id} label={tag.label} color={tag.color} removeTag={handleRemoveTagClick} />
39-
))}
40-
<div style={{ alignSelf: 'flex-end' }}>
41-
<IconButton onClick={() => setShowTagsDropdown(!showTagsDropdown)}>{showTagsDropdown ? <ExpandLessIcon /> : <AddCircleIcon />}</IconButton>
42-
{showTagsDropdown &&
43-
<Select defaultOpen={showTagsDropdown}>
44-
{props.allTags.map((tag: InventoryTag) => (
45-
<MenuItem onClick={() => handleAddTagClick(tag.id)}><Chip variant="outlined" color={tag.color as ("default" | "primary" | "secondary" | "warning" | "info" | "error" | "success")} label={tag.label} /></MenuItem>
46-
))}
47-
</Select>}
48-
</div>
49-
</Stack>
38+
{props.item.tags && props.item.tags.map((tag: InventoryTag) => (
39+
tag && <InventoryTagChip id={tag.id} label={tag.label} color={tag.color} removeTag={handleRemoveTagClick} />
40+
))}
41+
<div style={{ alignSelf: 'flex-end' }}>
42+
<IconButton onClick={() => setShowTagsDropdown(!showTagsDropdown)}>{showTagsDropdown ? <ExpandLessIcon /> : <AddCircleIcon />}</IconButton>
43+
{showTagsDropdown &&
44+
<Select defaultOpen={showTagsDropdown}>
45+
{props.allTags.map((tag: InventoryTag) => (
46+
<MenuItem onClick={() => handleAddTagClick(tag.id)}><Chip variant="outlined" color={tag.color as ("default" | "primary" | "secondary" | "warning" | "info" | "error" | "success")} label={tag.label} /></MenuItem>
47+
))}
48+
</Select>}
49+
</div>
50+
</Stack>
5051
)
5152
}

client/src/pages/makerspace_page/StaffBar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ export default function StaffBar() {
100100
icon={<SchoolIcon />}
101101
/>
102102
<NavLink
103-
primary={"Materials"}
104-
to={"/admin/inventory"}
103+
primary={"Inventory"}
104+
to={`/makerspace/${makerspaceID}/inventory`}
105105
icon={<InventoryIcon />}
106106
/>
107107
<NavLink
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useQuery } from "@apollo/client";
2+
import { useParams } from "react-router-dom";
3+
import { Button, Stack, Typography } from "@mui/material";
4+
import { GET_MAKERSPACE_WITH_ITEMS } from "../../../queries/makerspaceQueries";
5+
import InventoryItem from "../../../types/InventoryItem";
6+
import { InventoryForMakerspace } from "../../lab_management/inventory/common/InventoryForMakerspace";
7+
import { GET_INVENTORY_TAGS } from "../../../queries/inventoryQueries";
8+
import { useState } from "react";
9+
import InventoryTagsModal from "../../lab_management/inventory/InventoryTagsModal";
10+
import MaterialModal from "../../lab_management/inventory/MaterialModal";
11+
import QuickInventoryQRCodeModal from "./QuickInventoryQRCodeModal";
12+
import QrCodeIcon from '@mui/icons-material/QrCode';
13+
import SellIcon from '@mui/icons-material/Sell';
14+
15+
export default function InventoryPage() {
16+
const { makerspaceID } = useParams<{ makerspaceID: string }>();
17+
18+
const makerspaceInventoryResult = useQuery(GET_MAKERSPACE_WITH_ITEMS, {
19+
variables: { id: Number(makerspaceID) }
20+
});
21+
const inventoryTagsResult = useQuery(GET_INVENTORY_TAGS);
22+
23+
const name: string = makerspaceInventoryResult.data?.makerspaceByID.name ?? "Loading";
24+
const items: InventoryItem[] = makerspaceInventoryResult.data?.makerspaceByID.items ?? [];
25+
26+
const [modalItemID, setModalItemID] = useState("");
27+
const [tagsModalOpen, setTagsModalOpen] = useState(false);
28+
const [qrModal, setQrModal] = useState(false);
29+
30+
return (
31+
<Stack spacing={2} sx={{ padding: "15px" }}>
32+
<title>{`${name} Inventory`}</title>
33+
<Typography variant="h3">{name} Inventory</Typography>
34+
<Stack direction={"row"} spacing={2}>
35+
<Button variant="contained" color="primary" onClick={() => setQrModal(true)} startIcon={<QrCodeIcon />}>Create QR Codes</Button>
36+
<Button variant="contained" color="secondary" onClick={() => setTagsModalOpen(true)} startIcon={<SellIcon />}>Manage Tags</Button>
37+
</Stack>
38+
<InventoryForMakerspace
39+
searchText=""
40+
makerspace={makerspaceInventoryResult.data?.makerspaceByID ?? { name: "Loading", items: [] }}
41+
tags={inventoryTagsResult.data?.inventoryTags ?? []}
42+
setModalItemId={setModalItemID}
43+
/>
44+
45+
<QuickInventoryQRCodeModal open={qrModal} onClose={() => setQrModal(false)} />
46+
<MaterialModal itemId={modalItemID} onClose={() => setModalItemID("")} />
47+
<InventoryTagsModal tagModalOpen={tagsModalOpen} setTagModalOpen={setTagsModalOpen} />
48+
</Stack>
49+
);
50+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { useMutation, useQuery } from "@apollo/client";
2+
import { useParams } from "react-router-dom";
3+
import { GET_INVENTORY_ITEM, GET_INVENTORY_ITEMS_BY_TAG, SET_ITEM_AMOUNT } from "../../../queries/inventoryQueries";
4+
import { Autocomplete, Button, Stack, TextField, Typography } from "@mui/material";
5+
import InventoryItem from "../../../types/InventoryItem";
6+
import { useIsMobile } from "../../../common/IsMobileProvider";
7+
import { useState, useMemo, useEffect } from "react";
8+
import AddIcon from '@mui/icons-material/Add';
9+
import RemoveIcon from '@mui/icons-material/Remove';
10+
import { useDebounce } from "../../../common/useDebounce";
11+
12+
interface QuickEditInventoryPageProps {
13+
fromTag: boolean
14+
}
15+
16+
export default function QuickEditInventoryPage(props: QuickEditInventoryPageProps) {
17+
const { invID } = useParams<{ invID: string }>();
18+
19+
const isMobile = useIsMobile();
20+
21+
const invItemsResult = props.fromTag
22+
? useQuery(GET_INVENTORY_ITEMS_BY_TAG, { variables: { tagID: Number(invID) } })
23+
: useQuery(GET_INVENTORY_ITEM, { variables: { id: invID } })
24+
25+
const [setItemAmount] = useMutation(SET_ITEM_AMOUNT, { refetchQueries: ["GetInventoryItem", "InventoryItemsByTag"] });
26+
27+
const invItems: InventoryItem[] = useMemo(() => {
28+
if (invItemsResult.data === undefined) { return [] }
29+
if (props.fromTag) {
30+
return invItemsResult.data.inventoryItemsByTag
31+
} else {
32+
return [invItemsResult.data.InventoryItem]
33+
}
34+
}, [invItemsResult.data])
35+
36+
const [selectedItem, setSelectedItem] = useState<InventoryItem>();
37+
38+
const evaluatedItem = useMemo(() => {
39+
if (props.fromTag) {
40+
return selectedItem;
41+
} else {
42+
return invItems.length > 0 ? invItems[0] : undefined;
43+
}
44+
}, [invItems, selectedItem])
45+
46+
const [quantity, setQuantity] = useState(evaluatedItem?.count ?? -1);
47+
useEffect(() => setQuantity(evaluatedItem?.count ?? -1), [evaluatedItem])
48+
49+
const debouncedQuantity = useDebounce(quantity, 250);
50+
useEffect(() => console.log(debouncedQuantity), [debouncedQuantity])
51+
useEffect(() => {
52+
if (evaluatedItem !== undefined && debouncedQuantity !== -1 && debouncedQuantity !== evaluatedItem.count) {
53+
setItemAmount({ variables: { itemID: Number(evaluatedItem.id), count: debouncedQuantity } });
54+
}
55+
}, [debouncedQuantity])
56+
57+
return (
58+
<Stack spacing={4} justifyContent={"center"}>
59+
<title>Quick Edit Inventory</title>
60+
<Typography variant={isMobile ? "h5" : "h3"}>Quick Manage Item{invItems.length > 1 ? "s" : ""}</Typography>
61+
<Autocomplete
62+
key={invItems.length}
63+
renderInput={(params) => (
64+
<TextField
65+
{...params}
66+
label="Item"
67+
placeholder={evaluatedItem ? "" : "Select Item..."}
68+
/>
69+
)}
70+
options={invItems}
71+
disabled={!props.fromTag}
72+
value={evaluatedItem}
73+
onChange={(e, newValue) => setSelectedItem(newValue ?? undefined)}
74+
getOptionLabel={(option) => option.name}
75+
renderValue={(item) => item.name}
76+
isOptionEqualToValue={(option, value) => option.id === value.id}
77+
/>
78+
<Stack direction={"row"} width={"100%"} justifyContent={"space-around"} alignItems={"center"}>
79+
<Button
80+
color="error"
81+
disabled={evaluatedItem === undefined}
82+
variant="contained"
83+
size="large"
84+
onClick={() => setQuantity(quantity - 1)}
85+
>
86+
<RemoveIcon />
87+
</Button>
88+
<Typography
89+
variant="h6"
90+
fontWeight={"bold"}
91+
>
92+
{evaluatedItem === undefined ? "--" : quantity}
93+
</Typography>
94+
<Button
95+
color="success"
96+
disabled={evaluatedItem === undefined}
97+
variant="contained"
98+
size="large"
99+
onClick={() => setQuantity(quantity + 1)}
100+
>
101+
<AddIcon />
102+
</Button>
103+
</Stack>
104+
</Stack>
105+
);
106+
}

0 commit comments

Comments
 (0)