Skip to content

Commit c96c97d

Browse files
authored
Merge pull request #1440 from rit-construct-makerspace/cowsed/EquipmentUserInfo
Info page for QR codes at machines
2 parents 65937c6 + 0607845 commit c96c97d

5 files changed

Lines changed: 275 additions & 1 deletion

File tree

client/src/AppRouter.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import AdminHistoryPage from "./pages/site-settings/AdminHistoryPage";
5050
import ThemeManagementPage from "./pages/site-settings/ThemeManagementPage";
5151
import NewThemePage from "./pages/site-settings/NewThemePage";
5252
import ManageThemePage from "./pages/site-settings/ManageThemePage";
53+
import EquipmentUserInfo from "./pages/makerspace_page/equipment_pages/EquipmentUserInfo";
5354

5455
function AppRoot() {
5556
return (
@@ -136,7 +137,8 @@ export const routes = [
136137
{ path: "/makerspace/:makerspaceID", element: <MakerspacePage /> },
137138
{ path: "/terms", element: <TermsPage /> },
138139
{ path: "/help", element: <HelpPage /> },
139-
140+
{ path: "/makerspace/:makerspaceID/equipmentUserInfo/:equipmentID", element: <EquipmentUserInfo /> },
141+
140142
/* Routes that need to be protected by auth */
141143
{
142144
element: <AuthedRoute />,
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import PersonIcon from '@mui/icons-material/Person';
2+
import { useParams } from "react-router-dom";
3+
import { GET_EQUIPMENT_BY_ID } from "../../../queries/equipmentQueries";
4+
import { Alert, Button, CardActionArea, LinearProgress, Typography } from "@mui/material";
5+
import { useQuery } from "@apollo/client/react";
6+
import { Equipment } from "./ManageEquipmentPage";
7+
import { Stack } from "@mui/system";
8+
import { useCurrentUser } from "../../../common/CurrentUserProvider";
9+
import { ModuleStatus, moduleStatusMapper, TrainingModule } from "../../../common/TrainingModuleUtils";
10+
import { FullMakerspace, GET_MAKERSPACE_BY_ID } from "../../../queries/makerspaceQueries";
11+
import { GET_ROOM } from "../../../queries/roomQueries";
12+
import RequestWrapper from "../../../common/RequestWrapper";
13+
import Room from "../../../types/Room";
14+
import { ReactNode } from "react";
15+
import { IS_USER_WELCOMED } from "../../../queries/userQueries";
16+
import ModuleStatusRow from "../../../common/ModuleStatusRow";
17+
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
18+
import CloseIcon from "@mui/icons-material/Close";
19+
import CheckIcon from '@mui/icons-material/Check';
20+
import { Link } from "react-router-dom";
21+
22+
export default function EquipmentUserInfo() {
23+
const user = useCurrentUser();
24+
const isVisitor = user.visitor;
25+
26+
27+
const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
28+
const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
29+
variables: {
30+
id: equipmentID,
31+
},
32+
});
33+
const getMakerspaceResult = useQuery(GET_MAKERSPACE_BY_ID, {
34+
variables: {
35+
id: makerspaceID,
36+
},
37+
});
38+
39+
const getRoomResult = useQuery(GET_ROOM, {
40+
variables: {
41+
id: getEquipmentByIDResult.data?.equipment?.room?.id ?? -1,
42+
}
43+
});
44+
const isWelcomedResult = useQuery(IS_USER_WELCOMED, {
45+
variables: {
46+
userID: user.id,
47+
roomID: getEquipmentByIDResult?.data?.equipment?.room?.id ?? -1,
48+
}
49+
});
50+
51+
function unfinishedTrainingWarning(): ReactNode {
52+
return <Alert severity="warning" title="Unfinished Training">
53+
<Stack>
54+
<Typography variant="body1">
55+
It looks like you haven't finished your trainings yet.
56+
Scroll down to see what's left.
57+
</Typography>
58+
</Stack>
59+
</Alert>
60+
}
61+
function noInPersonAccessCheck(): ReactNode {
62+
return <Alert severity="warning" title="No access check">
63+
<Stack>
64+
<Typography variant="body1">
65+
Almost there! Before you unlock the machine, you must take an in person access check. Talk to a staff member to start this process.
66+
</Typography>
67+
</Stack>
68+
</Alert>
69+
}
70+
function unwelcomedWarning(): ReactNode {
71+
return <Alert severity="warning" title="Not yet signed">
72+
It looks like you haven't signed in today. Tap your card at the front desk to sign in to the space.
73+
</Alert>
74+
}
75+
function allGood(): ReactNode {
76+
return <Alert severity="success">
77+
Qualifications complete! Talk to staff if you're still having issues.
78+
</Alert>
79+
}
80+
81+
82+
83+
function renderPage(isWelcomed: boolean | undefined, equipment: Equipment | undefined, room: Room | undefined, makerspace: FullMakerspace | undefined) {
84+
85+
if (!equipment || !room || !makerspace) {
86+
return <Alert severity="error">Failed to load equipment requirements</Alert>
87+
}
88+
89+
const hasApprovedAccessCheck: boolean = user.accessChecks.some((ac) => Number(ac.equipmentID) === Number(equipment.id) && ac.approved)
90+
91+
const makerspaceStatuses: ModuleStatus[] = makerspace.trainingModules.map(moduleStatusMapper(user.passedModules, user.trainingHolds));
92+
const roomStatuses: ModuleStatus[] = room.trainingModules.map(moduleStatusMapper(user.passedModules, user.trainingHolds));
93+
const equipmentStatuses: ModuleStatus[] = equipment.trainingModules.map((obj) => moduleStatusMapper(user.passedModules, user.trainingHolds)(obj as TrainingModule));
94+
95+
const numMakerspaceTrainingsComplete: number = makerspaceStatuses.filter((module) => module.status === "Passed" || module.status === "Expiring Soon").length;
96+
const numRoomTrainingsComplete: number = roomStatuses.filter((module) => module.status === "Passed" || module.status === "Expiring Soon").length;
97+
const numEquipmentTrainingsComplete: number = equipmentStatuses.filter((module) => module.status === "Passed" || module.status === "Expiring Soon").length
98+
99+
const byExpiry = [...makerspaceStatuses, ...roomStatuses, ...equipmentStatuses]
100+
.filter((module) => module.status === "Expiring Soon" || module.status === "Passed")
101+
.sort((a, b) => new Date(a.expirationDate).getTime() - new Date(b.expirationDate).getTime());
102+
103+
const totalRequirements = makerspaceStatuses.length + roomStatuses.length + equipmentStatuses.length + (equipment.requiresInPerson ? 1 : 0);
104+
const totalReqsComplete = numMakerspaceTrainingsComplete + numRoomTrainingsComplete + numEquipmentTrainingsComplete + ((hasApprovedAccessCheck && equipment.requiresInPerson) ? 1 : 0);
105+
106+
const percentComplete: number = Math.round(totalReqsComplete / totalRequirements * 100);
107+
108+
109+
let warningMessage: () => ReactNode = unfinishedTrainingWarning;
110+
const hasAllTrainings = (numMakerspaceTrainingsComplete + numRoomTrainingsComplete + numEquipmentTrainingsComplete) >= makerspaceStatuses.length + roomStatuses.length + equipmentStatuses.length;
111+
if (!hasAllTrainings) {
112+
warningMessage = unfinishedTrainingWarning;
113+
} else if (!hasApprovedAccessCheck && equipment.requiresInPerson) {
114+
warningMessage = noInPersonAccessCheck;
115+
} else if (!isWelcomed && equipment.needsWelcome) {
116+
warningMessage = unwelcomedWarning;
117+
} else {
118+
warningMessage = allGood;
119+
}
120+
121+
122+
return <Stack padding={"20px 20px 15px"} spacing="10px" justifyContent={"center"} alignItems={"center"} display={"flex"}>
123+
{warningMessage()}
124+
<Typography variant="h1" fontSize="1.5em" fontWeight={"400"} >{equipment.name}</Typography>
125+
<LinearProgress
126+
variant="determinate"
127+
value={percentComplete}
128+
color={
129+
totalReqsComplete !== totalRequirements
130+
? "primary"
131+
: byExpiry.length > 0 && byExpiry[0].status === "Expiring Soon"
132+
? "warning"
133+
: "success"
134+
}
135+
sx={{
136+
width: "95%",
137+
height: "16px"
138+
}}
139+
/>
140+
<Typography variant="subtitle1" fontWeight={"bold"} display={"item"}>
141+
{
142+
totalReqsComplete !== totalRequirements
143+
? `Training ${percentComplete}% Complete`
144+
: byExpiry.length > 0 && byExpiry[0].status === "Expiring Soon"
145+
? "Expiring Soon!"
146+
: "Trainings Complete!"
147+
}
148+
</Typography>
149+
150+
<Stack spacing={2}>
151+
{
152+
makerspace.trainingModules.length > 0
153+
? <Stack>
154+
<Typography variant="h6">Makerspace Requirements</Typography>
155+
{
156+
makerspaceStatuses.map((moduleStatus) => <ModuleStatusRow ms={moduleStatus} />)
157+
}
158+
</Stack>
159+
: null
160+
}
161+
{
162+
room.trainingModules.length > 0
163+
? <Stack>
164+
<Typography variant="h6">Area Requirements</Typography>
165+
{
166+
roomStatuses.map((moduleStatus) => <ModuleStatusRow ms={moduleStatus} />)
167+
}
168+
</Stack>
169+
: null
170+
}
171+
{
172+
(equipment.trainingModules.length > 0 || equipment.requiresInPerson)
173+
? <Stack>
174+
<Typography variant="h6">Equipment Requirements</Typography>
175+
{
176+
equipmentStatuses.map((moduleStatus) => <ModuleStatusRow ms={moduleStatus} />)
177+
}
178+
{
179+
equipment.requiresInPerson
180+
? <CardActionArea
181+
onClick={equipment.signOffUrl ? () => window.open(equipment.signOffUrl, "_blank noopener noreferrer") : undefined}
182+
disableRipple={equipment.signOffUrl === ""}
183+
>
184+
<Stack direction={"row"} spacing={1} alignItems="center" padding="7px">
185+
{user.visitor ? (
186+
<RadioButtonUncheckedIcon color="secondary" />
187+
) : hasApprovedAccessCheck ? (
188+
<CheckIcon color="success" />
189+
) : (
190+
<CloseIcon color="error" />
191+
)}
192+
<Stack direction={"column"} width={"100%"}>
193+
{
194+
equipment.signOffUrl !== ""
195+
? <Link variant="body2">Staff Sign-Off</Link>
196+
: <Typography variant="body2">Staff Sign-Off</Typography>
197+
}
198+
{
199+
(equipment.requiresInPerson && !hasApprovedAccessCheck)
200+
? <Typography variant="body2">Complete all other requirments before attempting sign-off!</Typography>
201+
: null
202+
}
203+
</Stack>
204+
</Stack>
205+
</CardActionArea>
206+
: null
207+
}
208+
</Stack>
209+
: null
210+
}
211+
</Stack>
212+
<Button
213+
color="info"
214+
variant="contained"
215+
onClick={() => window.open(equipment.sopUrl, "_blank")}
216+
217+
>Equipment Information</Button>
218+
</Stack>
219+
}
220+
221+
function renderVisitor(equipment: Equipment | undefined) {
222+
if (!equipment) {
223+
return <Alert severity="error">Failed to load equipment information</Alert>
224+
}
225+
return <Stack padding="10px" spacing="10px">
226+
<Typography variant="h1" fontSize="1.5em" fontWeight={"400"} >{equipment.name}</Typography>
227+
To view equipment prerequisites, please log in.
228+
<Button
229+
variant="contained"
230+
color="secondary"
231+
endIcon={<PersonIcon />}
232+
onClick={() => window.location.replace(import.meta.env.VITE_LOGIN_URL + "?redir=" + import.meta.env.VITE_ORIGIN + window.location.pathname)}
233+
>
234+
LOGIN
235+
</Button>
236+
</Stack>
237+
238+
}
239+
240+
return <RequestWrapper loading={getEquipmentByIDResult.loading || getMakerspaceResult.loading || getRoomResult.loading} error={getEquipmentByIDResult.error || getMakerspaceResult.error || getRoomResult.error} minHeight={322}>
241+
<title>{(getEquipmentByIDResult?.data?.equipment as Equipment)?.name ?? "Equipment Checklist"}</title>
242+
{
243+
isVisitor
244+
? renderVisitor(getEquipmentByIDResult?.data?.equipment as Equipment)
245+
: renderPage(isWelcomedResult?.data?.isUserWelcomed ? isWelcomedResult?.data?.isUserWelcomed : false, getEquipmentByIDResult?.data?.equipment as Equipment, getRoomResult.data?.room as Room, getMakerspaceResult.data?.makerspaceByID as FullMakerspace)
246+
}
247+
</RequestWrapper>
248+
}

client/src/queries/userQueries.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ export const UPDATE_STUDENT_PROFILE = gql`
207207
}
208208
`;
209209

210+
export const IS_USER_WELCOMED = gql`
211+
query IsUserWelcomed($userID: ID!, $roomID: ID!) {
212+
isUserWelcomed(userID: $userID, roomID: $roomID)
213+
}
214+
`
210215

211216
export const GET_CURRENT_USER = gql`
212217
query GetCurrentUser {

server/src/graphql/resolvers/usersResolver.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getActiveTrainingHoldsByUser } from "../../database/repositories/Traini
1313
import { getMakerspaceByID } from "../../database/repositories/Makerspaces/MakerspaceRespository.js";
1414
import { EntityNotFound } from "../../EntityNotFound.js";
1515
import { UserRow } from "../../database/knex/tables.js";
16+
import { User } from "../../database/models/users/User.js";
1617

1718
const UsersResolvers = {
1819
User: {
@@ -147,6 +148,23 @@ const UsersResolvers = {
147148
return user;
148149
},
149150

151+
/**
152+
* Check if the target user has been welcomed today
153+
* @param userID the user to check
154+
* @param roomID the room to check
155+
* @returns true if welcomed
156+
*/
157+
158+
isUserWelcomed: async (
159+
_parent: any,
160+
args: { userID: string, roomID: string },
161+
{ ifStaffOrSelf }: ApolloContext) =>
162+
ifStaffOrSelf(Number(args.userID), async () => {
163+
const rawUser = await UserRepo.getUserByID(Number(args.userID))
164+
const fullUser = new User(rawUser)
165+
return await fullUser.wasWelcomedToday(Number(args.roomID));
166+
}),
167+
150168
/**
151169
* Fetch the number of total users
152170
* @returns String JSON of {count: number}

server/src/graphql/schemas/usersSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const UsersTypeDefs = gql`
9292
usersLimit(searchText: String): [User]
9393
user(id: ID!): User
9494
currentUser: User
95+
isUserWelcomed(userID: ID!, roomID: ID!): Boolean
9596
numUsers: Count
9697
userByUsernameorUID(value: String): User
9798
}

0 commit comments

Comments
 (0)