Skip to content

Commit c108d62

Browse files
authored
Merge pull request #1444 from rit-construct-makerspace/main
Hobbs Time Tickets and User Info Updates
2 parents 5c5772b + 8cc977b commit c108d62

17 files changed

Lines changed: 365 additions & 106 deletions

client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Autocomplete, Button, Card, Chip, IconButton, Link, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
2-
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
2+
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, formatHobbsTime, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
33
import ActionButton from "../../../common/ActionButton";
44
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
55
import { useMutation, useQuery } from "@apollo/client/react";
@@ -100,19 +100,6 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps)
100100
/>;
101101
}
102102

103-
function formatSeconds(allSeconds: number){
104-
const hours = Math.floor(allSeconds / 3600);
105-
if (hours > 0){
106-
return (allSeconds / 3600).toFixed(2) + " hrs"
107-
}
108-
const minutes = Math.floor((allSeconds % 3600) / 60);
109-
const seconds = allSeconds % 60;
110-
if (minutes > 0){
111-
return (minutes + seconds / 50).toFixed(2) + " min"
112-
} else {
113-
return Math.floor(allSeconds) + "sec"
114-
}
115-
}
116103

117104
function activeUserDisplay() {
118105
if (!currentAccessController) {
@@ -165,7 +152,7 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps)
165152
<Stack alignItems={"center"} justifyContent={"space-between"}>
166153
{
167154
!allowEdit
168-
? formatSeconds(props.instance.hobbsTime)
155+
? formatHobbsTime(props.instance.hobbsTime)
169156
: hobbsTimeSetField()
170157
}
171158
</Stack>

client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Autocomplete, Button, Card, Chip, IconButton, Link, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
2-
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
2+
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, formatHobbsTime, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
33
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
44
import { useMutation, useQuery } from "@apollo/client/react";
55
import { useState } from "react";
@@ -99,20 +99,6 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) {
9999
/>;
100100
}
101101

102-
function formatSeconds(allSeconds: number){
103-
const hours = Math.floor(allSeconds / 3600);
104-
if (hours > 0){
105-
return (allSeconds / 3600).toFixed(2) + " hrs"
106-
}
107-
const minutes = Math.floor((allSeconds % 3600) / 60);
108-
const seconds = allSeconds % 60;
109-
if (minutes > 0){
110-
return (minutes + seconds / 50).toFixed(2) + " min"
111-
} else {
112-
return Math.floor(allSeconds) + "sec"
113-
}
114-
}
115-
116102
function activeUserDisplay() {
117103
if (!currentAccessController) {
118104
return <Typography>No User</Typography>;
@@ -162,7 +148,7 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) {
162148
<Stack alignItems={"center"} direction={"row"} width={"200px"}>
163149
{
164150
!allowEdit
165-
? formatSeconds(props.instance.hobbsTime)
151+
? formatHobbsTime(props.instance.hobbsTime)
166152
: hobbsTimeSetField()
167153
}
168154
</Stack>

client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import PersonIcon from '@mui/icons-material/Person';
22
import { useParams } from "react-router-dom";
33
import { GET_EQUIPMENT_BY_ID } from "../../../queries/equipmentQueries";
4-
import { Alert, Button, CardActionArea, LinearProgress, Typography } from "@mui/material";
4+
import { Alert, Button, CardActionArea, LinearProgress, Link, Typography } from "@mui/material";
55
import { useQuery } from "@apollo/client/react";
66
import { Equipment } from "./ManageEquipmentPage";
77
import { Stack } from "@mui/system";
@@ -11,20 +11,24 @@ import { FullMakerspace, GET_MAKERSPACE_BY_ID } from "../../../queries/makerspac
1111
import { GET_ROOM } from "../../../queries/roomQueries";
1212
import RequestWrapper from "../../../common/RequestWrapper";
1313
import Room from "../../../types/Room";
14-
import { ReactNode } from "react";
14+
import { ReactNode, useState } from "react";
1515
import { IS_USER_WELCOMED } from "../../../queries/userQueries";
1616
import ModuleStatusRow from "../../../common/ModuleStatusRow";
1717
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
1818
import CloseIcon from "@mui/icons-material/Close";
1919
import CheckIcon from '@mui/icons-material/Check';
20-
import { Link } from "react-router-dom";
20+
import WarningIcon from '@mui/icons-material/Warning';
21+
import NewTicketModal from '../maintenance_pages/NewTicketModal';
22+
import { isStaffFor } from '../../../common/PrivilegeUtils';
23+
2124

2225
export default function EquipmentUserInfo() {
2326
const user = useCurrentUser();
2427
const isVisitor = user.visitor;
2528

2629

2730
const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
31+
const isStaffForHere = isStaffFor(user, Number(makerspaceID));
2832
const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
2933
variables: {
3034
id: equipmentID,
@@ -48,6 +52,10 @@ export default function EquipmentUserInfo() {
4852
}
4953
});
5054

55+
const getMakerspace = useQuery(GET_MAKERSPACE_BY_ID, { variables: { id: makerspaceID } });
56+
const [newTicketModal, setNewTicketModal] = useState(false);
57+
58+
5159
function unfinishedTrainingWarning(): ReactNode {
5260
return <Alert severity="warning" title="Unfinished Training">
5361
<Stack>
@@ -209,12 +217,24 @@ export default function EquipmentUserInfo() {
209217
: null
210218
}
211219
</Stack>
212-
<Button
213-
color="info"
214-
variant="contained"
215-
onClick={() => window.open(equipment.sopUrl, "_blank")}
220+
<Stack direction="row" spacing="10px">
221+
<Button
222+
color="info"
223+
variant="contained"
224+
onClick={() => window.open(equipment.sopUrl, "_blank")}
225+
>
226+
Equipment Information
227+
</Button>
216228

217-
>Equipment Information</Button>
229+
{isStaffForHere ? <Button
230+
color="primary"
231+
variant="contained"
232+
onClick={() => setNewTicketModal(true)}
233+
startIcon={<WarningIcon />}
234+
>
235+
Report Issue
236+
</Button> : undefined}
237+
</Stack>
218238
</Stack>
219239
}
220240

@@ -234,7 +254,6 @@ export default function EquipmentUserInfo() {
234254
LOGIN
235255
</Button>
236256
</Stack>
237-
238257
}
239258

240259
return <RequestWrapper loading={getEquipmentByIDResult.loading || getMakerspaceResult.loading || getRoomResult.loading} error={getEquipmentByIDResult.error || getMakerspaceResult.error || getRoomResult.error} minHeight={322}>
@@ -244,5 +263,7 @@ export default function EquipmentUserInfo() {
244263
? renderVisitor(getEquipmentByIDResult?.data?.equipment as Equipment)
245264
: renderPage(isWelcomedResult?.data?.isUserWelcomed ? isWelcomedResult?.data?.isUserWelcomed : false, getEquipmentByIDResult?.data?.equipment as Equipment, getRoomResult.data?.room as Room, getMakerspaceResult.data?.makerspaceByID as FullMakerspace)
246265
}
266+
{getMakerspace?.data?.makerspaceByID ? <NewTicketModal open={newTicketModal} onClose={() => setNewTicketModal(false)} makerspace={getMakerspace?.data?.makerspaceByID} /> : undefined}
267+
247268
</RequestWrapper>
248269
}

client/src/pages/makerspace_page/equipment_pages/ManageEquipmentPage.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { useMutation, useQuery } from "@apollo/client/react";
2-
import { Button, Stack, TextField, Typography } from "@mui/material";
2+
import { Button, Link, Stack, TextField, Typography } from "@mui/material";
33
import { GET_EQUIPMENT_BY_ID } from "../../../queries/equipmentQueries";
44
import { useNavigate, useParams } from "react-router-dom";
55
import RequestWrapper2 from "../../../common/RequestWrapper2";
66
import EquipmentInformation from "./EquipmentInformation";
77
import PrettyModal from "../../../common/PrettyModal";
88
import InstanceGrid from "./InstanceGrid";
99
import { useIsMobile } from "../../../common/IsMobileProvider";
10-
import { useState } from "react";
10+
import { useRef, useState } from "react";
1111
import AddIcon from '@mui/icons-material/Add';
1212
import { CREATE_EQUIPMENT_INSTANCE } from "../../../queries/equipmentInstanceQueries";
1313
import HistoryIcon from '@mui/icons-material/History';
@@ -18,6 +18,8 @@ import MaintenanceTicketGrid from "../maintenance_pages/MaintenanceTicketGrid";
1818
import WarningIcon from '@mui/icons-material/Warning';
1919
import NewTicketModal from "../maintenance_pages/NewTicketModal";
2020
import { useMakeTheme } from "../../../common/MakeThemeProvider";
21+
import QrCodeIcon from '@mui/icons-material/QrCode';
22+
import { QRCode } from "react-qr-code";
2123

2224
export interface Equipment {
2325
id: number;
@@ -51,13 +53,57 @@ export interface Equipment {
5153

5254
}
5355

56+
57+
58+
59+
5460
export default function ManageEquipmentPage() {
5561
const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
5662
const isMobile = useIsMobile();
5763
const navigate = useNavigate();
5864
const user = useCurrentUser();
5965
const makeTheme = useMakeTheme();
6066

67+
const qrRef = useRef(null);
68+
69+
function copyQRCode() {
70+
try {
71+
const svg = qrRef.current;
72+
if (svg === null) { return; }
73+
const svgData = new XMLSerializer().serializeToString(svg);
74+
const canvas = document.createElement("canvas");
75+
const ctx = canvas.getContext("2d");
76+
const img = new Image();
77+
console.log("qrRef", qrRef.current)
78+
79+
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
80+
const url = URL.createObjectURL(svgBlob);
81+
82+
img.onload = () => {
83+
// Set canvas dimensions to match SVG
84+
canvas.width = img.width;
85+
canvas.height = img.height;
86+
if (ctx === null) { return; }
87+
ctx.drawImage(img, 0, 0);
88+
89+
// Copy to clipboard as a PNG
90+
canvas.toBlob((blob) => {
91+
if (blob === null) { return; }
92+
const item = new ClipboardItem({ "image/png": blob });
93+
navigator.clipboard.write([item]);
94+
});
95+
96+
URL.revokeObjectURL(url);
97+
};
98+
99+
img.src = url;
100+
} catch (err) {
101+
console.error("Failed to copy qr code", err)
102+
}
103+
}
104+
105+
106+
61107
const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
62108
variables: {
63109
id: equipmentID,
@@ -70,6 +116,7 @@ export default function ManageEquipmentPage() {
70116

71117
const [newInstanceName, setNewInstanceName] = useState("");
72118
const [newInstanceModal, setNewInstanceModal] = useState(false);
119+
const [showQrCode, setShowQrCode] = useState<boolean>(false);
73120

74121
const [editEquipmentModal, setEditEquipmentModal] = useState(false);
75122

@@ -104,6 +151,19 @@ export default function ManageEquipmentPage() {
104151
>
105152
<Typography variant={isMobile ? "h5" : "h3"}>{`Manage ${equipment.name}`}</Typography>
106153
<Stack direction={"row"} spacing={1}>
154+
155+
<Button
156+
color="info"
157+
variant="contained"
158+
startIcon={<QrCodeIcon />}
159+
onClick={() => setShowQrCode(true)}
160+
sx={{
161+
justifySelf: "flex-end"
162+
}}
163+
>
164+
User QR Code
165+
</Button>
166+
107167
<Button
108168
color="secondary"
109169
variant="contained"
@@ -174,7 +234,28 @@ export default function ManageEquipmentPage() {
174234
</Button>
175235
</Stack>
176236
<MaintenanceTicketGrid equipmentID={equipment.id} />
237+
238+
239+
{/* Equipment User Info QR Code Modal */}
240+
<PrettyModal open={showQrCode} onClose={() => setShowQrCode(false)} elevation={7}>
241+
<Stack alignItems={"center"} spacing="10px">
242+
<Link
243+
href={`${import.meta.env.VITE_ORIGIN}/app/makerspace/${makerspaceID}/equipmentUserInfo/${equipmentID}`}
244+
target={"_blank"}
245+
>User Info Page</Link>
246+
<QRCode ref={qrRef} value={`${import.meta.env.VITE_ORIGIN}/app/makerspace/${makerspaceID}/equipmentUserInfo/${equipmentID}`} />
247+
<Button variant="contained" color="info" onClick={copyQRCode} >Copy</Button>
248+
</Stack>
249+
</PrettyModal>
250+
251+
252+
177253
</Stack>
254+
255+
256+
257+
258+
178259
{/* Equipment Information Modal */}
179260
<PrettyModal open={editEquipmentModal} onClose={() => setEditEquipmentModal(false)} width={"90%"} elevation={8}>
180261
<EquipmentInformation equipment={equipment} />

client/src/pages/makerspace_page/maintenance_pages/MaintenancePage.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,16 @@ export default function MaintenancePage() {
6060
// const containsOperator = getGridStringOperators().filter((operator) => operator.value === "contains");
6161

6262
const columns: GridColDef[] = [
63-
{ field: "id", headerName: "ID", width: 10, filterable: false, resizable: false, hideable: false },
63+
{ field: "id", headerName: "ID", width: 25, filterable: false, resizable: false, hideable: false },
6464
{ field: "equipment", headerName: "Equipment", width: 350, sortable: false, filterable: false, resizable: false },
6565
{ field: "instance", headerName: "Instance", width: 300, sortable: false, filterable: false, resizable: false },
6666
{ field: "type", headerName: "Type", width: 140, sortable: false, filterable: false, resizable: false },
6767
{ field: "status", headerName: "Status", width: 140, filterable: false, resizable: false },
68-
{ field: "severity", headerName: "Severity", width: 140, filterable: false, resizable: false },
69-
{ field: "creator", headerName: "Creator", width: 140, sortable: false, filterable: false, resizable: false },
70-
{ field: "assigned", headerName: "Assigned", width: 140, sortable: false, filterable: false, resizable: false },
71-
{ field: "dateCreated", headerName: "Created", width: 180, filterable: false, resizable: false },
68+
{ field: "severity", headerName: "Severity", width: 120, filterable: false, resizable: false },
69+
{ field: "creator", headerName: "Creator", width: 120, sortable: false, filterable: false, resizable: false },
70+
{ field: "assigned", headerName: "Assigned", width: 120, sortable: false, filterable: false, resizable: false },
71+
{ field: "timeUnit", headerName: "Time Unit", width: 140, sortable: false, filterable: false, resizable: false },
72+
{ field: "dateCreated", headerName: "Create Date", width: 180, filterable: false, resizable: false },
7273
{
7374
field: "manage", headerName: "Manage", width: 140, filterable: false, sortable: false, renderCell: (params: GridRenderCellParams<any, MaintenanceTicket>) => (
7475
<MaintenanceTicketButtonCell ticket={params.value} />
@@ -96,6 +97,7 @@ export default function MaintenancePage() {
9697
severity: ticket.severity,
9798
creator: ticket.type === MaintenanceTicketType.REPORTED ? ticket.creator?.ritUsername ?? "" : "SERVER",
9899
assigned: ticket.assigned?.ritUsername ?? "UNASSIGNED",
100+
timeUnit: ticket.timeUnit,
99101
dateCreated: formatter.format(new Date(Number(ticket.dateCreated))),
100102
manage: ticket
101103
}

client/src/pages/makerspace_page/maintenance_pages/MaintenanceTicketModal.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Stack } from "@mui/system";
22
import PrettyModal from "../../../common/PrettyModal";
3-
import { ASSIGN_MAINTENANCE_TICKET, DELETE_MAINTENACE_TICKET, MaintenanceTicket, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketType, MODIFY_MAINTENANCE_TICKET_STATUS, UPDATE_MAINTENACE_TICKET } from "../../../queries/maintenanceTicketQueries";
3+
import { ASSIGN_MAINTENANCE_TICKET, DELETE_MAINTENACE_TICKET, MaintenanceTicket, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketTimeUnit, MaintenanceTicketType, MODIFY_MAINTENANCE_TICKET_STATUS, UPDATE_MAINTENACE_TICKET } from "../../../queries/maintenanceTicketQueries";
44
import { Autocomplete, Button, Chip, IconButton, TextField, Typography } from "@mui/material";
55
import { useState } from "react";
66
import CloseIcon from '@mui/icons-material/Close';
@@ -17,6 +17,7 @@ import { useParams } from "react-router-dom";
1717
import { GET_VALID_STAFF } from "../../../queries/makerspaceQueries";
1818
import { CurrentUser, useCurrentUser } from "../../../common/CurrentUserProvider";
1919
import { isManager } from "../../../common/PrivilegeUtils";
20+
import { formatHobbsTime } from "../../../queries/equipmentInstanceQueries";
2021

2122
interface TicketModalProps {
2223
open: boolean,
@@ -194,6 +195,19 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
194195
}
195196
}
196197

198+
function createTimeLabel(){
199+
const after = `Create After: ${props.ticket.intervalHours} hours`
200+
const difference = props.ticket.instance.hobbsTime - props.ticket.hobbsTimeAtCreate
201+
if (difference < 0){
202+
return `${after} (${formatHobbsTime(Math.abs(difference))} to go)`
203+
} else {
204+
return `${after} (${formatHobbsTime(difference)} ago)`
205+
}
206+
}
207+
208+
const shouldShowHobbsAtOpen= props.ticket.timeUnit == MaintenanceTicketTimeUnit.USAGE || props.ticket.status != MaintenanceTicketStatus.UPCOMING;
209+
const shouldShowHobbsAtClose= props.ticket.status == MaintenanceTicketStatus.CLOSED;
210+
197211
return (
198212
<PrettyModal open={props.open} onClose={handleClose} width={"600px"}>
199213
<Stack spacing={2}>
@@ -214,7 +228,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
214228
</Stack>
215229
<Stack direction={"row"} justifyContent={"space-between"}>
216230
<Typography variant="subtitle1">{`Ticket #${props.ticket.id}`}</Typography>
217-
<Typography variant="subtitle1">{`Created: ${formatter.format(Number(props.ticket.dateCreated))}`}</Typography>
231+
{ props.ticket.timeUnit=== MaintenanceTicketTimeUnit.CALENDAR ?
232+
<Typography variant="subtitle1">{`Create Date: ${formatter.format(Number(props.ticket.dateCreated))}`}</Typography>
233+
: <Typography variant="subtitle1">{createTimeLabel()}</Typography>
234+
}
218235
</Stack>
219236
<Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
220237
<Typography variant="body1">{`Reported by: ${props.ticket.type === MaintenanceTicketType.REPORTED ? props.ticket.creator?.ritUsername ?? "" : "SERVER"}`}</Typography>
@@ -317,6 +334,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
317334
</Stack>
318335
}
319336
</Stack>
337+
<Stack justifyContent={"space-between"} direction={"row"}>
338+
{shouldShowHobbsAtOpen ? <Typography variant="body1">Usage at Open: {formatHobbsTime(props.ticket.hobbsTimeAtCreate)}</Typography> : undefined}
339+
{shouldShowHobbsAtClose ? <Typography variant="body1">Usage at Close: {props.ticket.hobbsTimeAtClose ? formatHobbsTime(props.ticket.hobbsTimeAtClose) : "N/A"}</Typography> : undefined}
340+
</Stack>
320341
{
321342
editing
322343
? <TextField

0 commit comments

Comments
 (0)