diff --git a/client/src/AppRouter.tsx b/client/src/AppRouter.tsx
index 56c92a7c..0120dd84 100644
--- a/client/src/AppRouter.tsx
+++ b/client/src/AppRouter.tsx
@@ -50,6 +50,7 @@ import AdminHistoryPage from "./pages/site-settings/AdminHistoryPage";
import ThemeManagementPage from "./pages/site-settings/ThemeManagementPage";
import NewThemePage from "./pages/site-settings/NewThemePage";
import ManageThemePage from "./pages/site-settings/ManageThemePage";
+import EquipmentUserInfo from "./pages/makerspace_page/equipment_pages/EquipmentUserInfo";
function AppRoot() {
return (
@@ -136,7 +137,8 @@ export const routes = [
{ path: "/makerspace/:makerspaceID", element: },
{ path: "/terms", element: },
{ path: "/help", element: },
-
+ { path: "/makerspace/:makerspaceID/equipmentUserInfo/:equipmentID", element: },
+
/* Routes that need to be protected by auth */
{
element: ,
diff --git a/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx b/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx
new file mode 100644
index 00000000..a9c01cdd
--- /dev/null
+++ b/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx
@@ -0,0 +1,248 @@
+import PersonIcon from '@mui/icons-material/Person';
+import { useParams } from "react-router-dom";
+import { GET_EQUIPMENT_BY_ID } from "../../../queries/equipmentQueries";
+import { Alert, Button, CardActionArea, LinearProgress, Typography } from "@mui/material";
+import { useQuery } from "@apollo/client/react";
+import { Equipment } from "./ManageEquipmentPage";
+import { Stack } from "@mui/system";
+import { useCurrentUser } from "../../../common/CurrentUserProvider";
+import { ModuleStatus, moduleStatusMapper, TrainingModule } from "../../../common/TrainingModuleUtils";
+import { FullMakerspace, GET_MAKERSPACE_BY_ID } from "../../../queries/makerspaceQueries";
+import { GET_ROOM } from "../../../queries/roomQueries";
+import RequestWrapper from "../../../common/RequestWrapper";
+import Room from "../../../types/Room";
+import { ReactNode } from "react";
+import { IS_USER_WELCOMED } from "../../../queries/userQueries";
+import ModuleStatusRow from "../../../common/ModuleStatusRow";
+import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
+import CloseIcon from "@mui/icons-material/Close";
+import CheckIcon from '@mui/icons-material/Check';
+import { Link } from "react-router-dom";
+
+export default function EquipmentUserInfo() {
+ const user = useCurrentUser();
+ const isVisitor = user.visitor;
+
+
+ const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
+ const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
+ variables: {
+ id: equipmentID,
+ },
+ });
+ const getMakerspaceResult = useQuery(GET_MAKERSPACE_BY_ID, {
+ variables: {
+ id: makerspaceID,
+ },
+ });
+
+ const getRoomResult = useQuery(GET_ROOM, {
+ variables: {
+ id: getEquipmentByIDResult.data?.equipment?.room?.id ?? -1,
+ }
+ });
+ const isWelcomedResult = useQuery(IS_USER_WELCOMED, {
+ variables: {
+ userID: user.id,
+ roomID: getEquipmentByIDResult?.data?.equipment?.room?.id ?? -1,
+ }
+ });
+
+ function unfinishedTrainingWarning(): ReactNode {
+ return
+
+
+ It looks like you haven't finished your trainings yet.
+ Scroll down to see what's left.
+
+
+
+ }
+ function noInPersonAccessCheck(): ReactNode {
+ return
+
+
+ Almost there! Before you unlock the machine, you must take an in person access check. Talk to a staff member to start this process.
+
+
+
+ }
+ function unwelcomedWarning(): ReactNode {
+ return
+ It looks like you haven't signed in today. Tap your card at the front desk to sign in to the space.
+
+ }
+ function allGood(): ReactNode {
+ return
+ Qualifications complete! Talk to staff if you're still having issues.
+
+ }
+
+
+
+ function renderPage(isWelcomed: boolean | undefined, equipment: Equipment | undefined, room: Room | undefined, makerspace: FullMakerspace | undefined) {
+
+ if (!equipment || !room || !makerspace) {
+ return Failed to load equipment requirements
+ }
+
+ const hasApprovedAccessCheck: boolean = user.accessChecks.some((ac) => Number(ac.equipmentID) === Number(equipment.id) && ac.approved)
+
+ const makerspaceStatuses: ModuleStatus[] = makerspace.trainingModules.map(moduleStatusMapper(user.passedModules, user.trainingHolds));
+ const roomStatuses: ModuleStatus[] = room.trainingModules.map(moduleStatusMapper(user.passedModules, user.trainingHolds));
+ const equipmentStatuses: ModuleStatus[] = equipment.trainingModules.map((obj) => moduleStatusMapper(user.passedModules, user.trainingHolds)(obj as TrainingModule));
+
+ const numMakerspaceTrainingsComplete: number = makerspaceStatuses.filter((module) => module.status === "Passed" || module.status === "Expiring Soon").length;
+ const numRoomTrainingsComplete: number = roomStatuses.filter((module) => module.status === "Passed" || module.status === "Expiring Soon").length;
+ const numEquipmentTrainingsComplete: number = equipmentStatuses.filter((module) => module.status === "Passed" || module.status === "Expiring Soon").length
+
+ const byExpiry = [...makerspaceStatuses, ...roomStatuses, ...equipmentStatuses]
+ .filter((module) => module.status === "Expiring Soon" || module.status === "Passed")
+ .sort((a, b) => new Date(a.expirationDate).getTime() - new Date(b.expirationDate).getTime());
+
+ const totalRequirements = makerspaceStatuses.length + roomStatuses.length + equipmentStatuses.length + (equipment.requiresInPerson ? 1 : 0);
+ const totalReqsComplete = numMakerspaceTrainingsComplete + numRoomTrainingsComplete + numEquipmentTrainingsComplete + ((hasApprovedAccessCheck && equipment.requiresInPerson) ? 1 : 0);
+
+ const percentComplete: number = Math.round(totalReqsComplete / totalRequirements * 100);
+
+
+ let warningMessage: () => ReactNode = unfinishedTrainingWarning;
+ const hasAllTrainings = (numMakerspaceTrainingsComplete + numRoomTrainingsComplete + numEquipmentTrainingsComplete) >= makerspaceStatuses.length + roomStatuses.length + equipmentStatuses.length;
+ if (!hasAllTrainings) {
+ warningMessage = unfinishedTrainingWarning;
+ } else if (!hasApprovedAccessCheck && equipment.requiresInPerson) {
+ warningMessage = noInPersonAccessCheck;
+ } else if (!isWelcomed && equipment.needsWelcome) {
+ warningMessage = unwelcomedWarning;
+ } else {
+ warningMessage = allGood;
+ }
+
+
+ return
+ {warningMessage()}
+ {equipment.name}
+ 0 && byExpiry[0].status === "Expiring Soon"
+ ? "warning"
+ : "success"
+ }
+ sx={{
+ width: "95%",
+ height: "16px"
+ }}
+ />
+
+ {
+ totalReqsComplete !== totalRequirements
+ ? `Training ${percentComplete}% Complete`
+ : byExpiry.length > 0 && byExpiry[0].status === "Expiring Soon"
+ ? "Expiring Soon!"
+ : "Trainings Complete!"
+ }
+
+
+
+ {
+ makerspace.trainingModules.length > 0
+ ?
+ Makerspace Requirements
+ {
+ makerspaceStatuses.map((moduleStatus) => )
+ }
+
+ : null
+ }
+ {
+ room.trainingModules.length > 0
+ ?
+ Area Requirements
+ {
+ roomStatuses.map((moduleStatus) => )
+ }
+
+ : null
+ }
+ {
+ (equipment.trainingModules.length > 0 || equipment.requiresInPerson)
+ ?
+ Equipment Requirements
+ {
+ equipmentStatuses.map((moduleStatus) => )
+ }
+ {
+ equipment.requiresInPerson
+ ? window.open(equipment.signOffUrl, "_blank noopener noreferrer") : undefined}
+ disableRipple={equipment.signOffUrl === ""}
+ >
+
+ {user.visitor ? (
+
+ ) : hasApprovedAccessCheck ? (
+
+ ) : (
+
+ )}
+
+ {
+ equipment.signOffUrl !== ""
+ ? Staff Sign-Off
+ : Staff Sign-Off
+ }
+ {
+ (equipment.requiresInPerson && !hasApprovedAccessCheck)
+ ? Complete all other requirments before attempting sign-off!
+ : null
+ }
+
+
+
+ : null
+ }
+
+ : null
+ }
+
+
+
+ }
+
+ function renderVisitor(equipment: Equipment | undefined) {
+ if (!equipment) {
+ return Failed to load equipment information
+ }
+ return
+ {equipment.name}
+ To view equipment prerequisites, please log in.
+ }
+ onClick={() => window.location.replace(import.meta.env.VITE_LOGIN_URL + "?redir=" + import.meta.env.VITE_ORIGIN + window.location.pathname)}
+ >
+ LOGIN
+
+
+
+ }
+
+ return
+ {(getEquipmentByIDResult?.data?.equipment as Equipment)?.name ?? "Equipment Checklist"}
+ {
+ isVisitor
+ ? renderVisitor(getEquipmentByIDResult?.data?.equipment as Equipment)
+ : renderPage(isWelcomedResult?.data?.isUserWelcomed ? isWelcomedResult?.data?.isUserWelcomed : false, getEquipmentByIDResult?.data?.equipment as Equipment, getRoomResult.data?.room as Room, getMakerspaceResult.data?.makerspaceByID as FullMakerspace)
+ }
+
+}
\ No newline at end of file
diff --git a/client/src/queries/userQueries.ts b/client/src/queries/userQueries.ts
index 376be5cd..b5de3a40 100644
--- a/client/src/queries/userQueries.ts
+++ b/client/src/queries/userQueries.ts
@@ -207,6 +207,11 @@ export const UPDATE_STUDENT_PROFILE = gql`
}
`;
+export const IS_USER_WELCOMED = gql`
+ query IsUserWelcomed($userID: ID!, $roomID: ID!) {
+ isUserWelcomed(userID: $userID, roomID: $roomID)
+ }
+`
export const GET_CURRENT_USER = gql`
query GetCurrentUser {
diff --git a/server/src/graphql/resolvers/usersResolver.ts b/server/src/graphql/resolvers/usersResolver.ts
index 4fbe34e0..013ce4ba 100644
--- a/server/src/graphql/resolvers/usersResolver.ts
+++ b/server/src/graphql/resolvers/usersResolver.ts
@@ -13,6 +13,7 @@ import { getActiveTrainingHoldsByUser } from "../../database/repositories/Traini
import { getMakerspaceByID } from "../../database/repositories/Makerspaces/MakerspaceRespository.js";
import { EntityNotFound } from "../../EntityNotFound.js";
import { UserRow } from "../../database/knex/tables.js";
+import { User } from "../../database/models/users/User.js";
const UsersResolvers = {
User: {
@@ -147,6 +148,23 @@ const UsersResolvers = {
return user;
},
+ /**
+ * Check if the target user has been welcomed today
+ * @param userID the user to check
+ * @param roomID the room to check
+ * @returns true if welcomed
+ */
+
+ isUserWelcomed: async (
+ _parent: any,
+ args: { userID: string, roomID: string },
+ { ifStaffOrSelf }: ApolloContext) =>
+ ifStaffOrSelf(Number(args.userID), async () => {
+ const rawUser = await UserRepo.getUserByID(Number(args.userID))
+ const fullUser = new User(rawUser)
+ return await fullUser.wasWelcomedToday(Number(args.roomID));
+ }),
+
/**
* Fetch the number of total users
* @returns String JSON of {count: number}
diff --git a/server/src/graphql/schemas/usersSchema.ts b/server/src/graphql/schemas/usersSchema.ts
index e10222c1..79f13b5f 100644
--- a/server/src/graphql/schemas/usersSchema.ts
+++ b/server/src/graphql/schemas/usersSchema.ts
@@ -92,6 +92,7 @@ export const UsersTypeDefs = gql`
usersLimit(searchText: String): [User]
user(id: ID!): User
currentUser: User
+ isUserWelcomed(userID: ID!, roomID: ID!): Boolean
numUsers: Count
userByUsernameorUID(value: String): User
}