Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Autocomplete, Button, Card, Chip, IconButton, Link, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, formatHobbsTime, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
import ActionButton from "../../../common/ActionButton";
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
import { useMutation, useQuery } from "@apollo/client/react";
Expand Down Expand Up @@ -100,19 +100,6 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps)
/>;
}

function formatSeconds(allSeconds: number){
const hours = Math.floor(allSeconds / 3600);
if (hours > 0){
return (allSeconds / 3600).toFixed(2) + " hrs"
}
const minutes = Math.floor((allSeconds % 3600) / 60);
const seconds = allSeconds % 60;
if (minutes > 0){
return (minutes + seconds / 50).toFixed(2) + " min"
} else {
return Math.floor(allSeconds) + "sec"
}
}

function activeUserDisplay() {
if (!currentAccessController) {
Expand Down Expand Up @@ -165,7 +152,7 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps)
<Stack alignItems={"center"} justifyContent={"space-between"}>
{
!allowEdit
? formatSeconds(props.instance.hobbsTime)
? formatHobbsTime(props.instance.hobbsTime)
: hobbsTimeSetField()
}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Autocomplete, Button, Card, Chip, IconButton, Link, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, formatHobbsTime, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries";
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
import { useMutation, useQuery } from "@apollo/client/react";
import { useState } from "react";
Expand Down Expand Up @@ -99,20 +99,6 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) {
/>;
}

function formatSeconds(allSeconds: number){
const hours = Math.floor(allSeconds / 3600);
if (hours > 0){
return (allSeconds / 3600).toFixed(2) + " hrs"
}
const minutes = Math.floor((allSeconds % 3600) / 60);
const seconds = allSeconds % 60;
if (minutes > 0){
return (minutes + seconds / 50).toFixed(2) + " min"
} else {
return Math.floor(allSeconds) + "sec"
}
}

function activeUserDisplay() {
if (!currentAccessController) {
return <Typography>No User</Typography>;
Expand Down Expand Up @@ -162,7 +148,7 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) {
<Stack alignItems={"center"} direction={"row"} width={"200px"}>
{
!allowEdit
? formatSeconds(props.instance.hobbsTime)
? formatHobbsTime(props.instance.hobbsTime)
: hobbsTimeSetField()
}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 { Alert, Button, CardActionArea, LinearProgress, Link, Typography } from "@mui/material";
import { useQuery } from "@apollo/client/react";
import { Equipment } from "./ManageEquipmentPage";
import { Stack } from "@mui/system";
Expand All @@ -11,20 +11,24 @@ import { FullMakerspace, GET_MAKERSPACE_BY_ID } from "../../../queries/makerspac
import { GET_ROOM } from "../../../queries/roomQueries";
import RequestWrapper from "../../../common/RequestWrapper";
import Room from "../../../types/Room";
import { ReactNode } from "react";
import { ReactNode, useState } 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";
import WarningIcon from '@mui/icons-material/Warning';
import NewTicketModal from '../maintenance_pages/NewTicketModal';
import { isStaffFor } from '../../../common/PrivilegeUtils';


export default function EquipmentUserInfo() {
const user = useCurrentUser();
const isVisitor = user.visitor;


const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
const isStaffForHere = isStaffFor(user, Number(makerspaceID));
const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
variables: {
id: equipmentID,
Expand All @@ -48,6 +52,10 @@ export default function EquipmentUserInfo() {
}
});

const getMakerspace = useQuery(GET_MAKERSPACE_BY_ID, { variables: { id: makerspaceID } });
const [newTicketModal, setNewTicketModal] = useState(false);


function unfinishedTrainingWarning(): ReactNode {
return <Alert severity="warning" title="Unfinished Training">
<Stack>
Expand Down Expand Up @@ -209,12 +217,24 @@ export default function EquipmentUserInfo() {
: null
}
</Stack>
<Button
color="info"
variant="contained"
onClick={() => window.open(equipment.sopUrl, "_blank")}
<Stack direction="row" spacing="10px">
<Button
color="info"
variant="contained"
onClick={() => window.open(equipment.sopUrl, "_blank")}
>
Equipment Information
</Button>

>Equipment Information</Button>
{isStaffForHere ? <Button
color="primary"
variant="contained"
onClick={() => setNewTicketModal(true)}
startIcon={<WarningIcon />}
>
Report Issue
</Button> : undefined}
</Stack>
</Stack>
}

Expand All @@ -234,7 +254,6 @@ export default function EquipmentUserInfo() {
LOGIN
</Button>
</Stack>

}

return <RequestWrapper loading={getEquipmentByIDResult.loading || getMakerspaceResult.loading || getRoomResult.loading} error={getEquipmentByIDResult.error || getMakerspaceResult.error || getRoomResult.error} minHeight={322}>
Expand All @@ -244,5 +263,7 @@ export default function EquipmentUserInfo() {
? 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)
}
{getMakerspace?.data?.makerspaceByID ? <NewTicketModal open={newTicketModal} onClose={() => setNewTicketModal(false)} makerspace={getMakerspace?.data?.makerspaceByID} /> : undefined}

</RequestWrapper>
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useMutation, useQuery } from "@apollo/client/react";
import { Button, Stack, TextField, Typography } from "@mui/material";
import { Button, Link, Stack, TextField, Typography } from "@mui/material";
import { GET_EQUIPMENT_BY_ID } from "../../../queries/equipmentQueries";
import { useNavigate, useParams } from "react-router-dom";
import RequestWrapper2 from "../../../common/RequestWrapper2";
import EquipmentInformation from "./EquipmentInformation";
import PrettyModal from "../../../common/PrettyModal";
import InstanceGrid from "./InstanceGrid";
import { useIsMobile } from "../../../common/IsMobileProvider";
import { useState } from "react";
import { useRef, useState } from "react";
import AddIcon from '@mui/icons-material/Add';
import { CREATE_EQUIPMENT_INSTANCE } from "../../../queries/equipmentInstanceQueries";
import HistoryIcon from '@mui/icons-material/History';
Expand All @@ -18,6 +18,8 @@ import MaintenanceTicketGrid from "../maintenance_pages/MaintenanceTicketGrid";
import WarningIcon from '@mui/icons-material/Warning';
import NewTicketModal from "../maintenance_pages/NewTicketModal";
import { useMakeTheme } from "../../../common/MakeThemeProvider";
import QrCodeIcon from '@mui/icons-material/QrCode';
import { QRCode } from "react-qr-code";

export interface Equipment {
id: number;
Expand Down Expand Up @@ -51,13 +53,57 @@ export interface Equipment {

}





export default function ManageEquipmentPage() {
const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
const isMobile = useIsMobile();
const navigate = useNavigate();
const user = useCurrentUser();
const makeTheme = useMakeTheme();

const qrRef = useRef(null);

function copyQRCode() {
try {
const svg = qrRef.current;
if (svg === null) { return; }
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
console.log("qrRef", qrRef.current)

const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);

img.onload = () => {
// Set canvas dimensions to match SVG
canvas.width = img.width;
canvas.height = img.height;
if (ctx === null) { return; }
ctx.drawImage(img, 0, 0);

// Copy to clipboard as a PNG
canvas.toBlob((blob) => {
if (blob === null) { return; }
const item = new ClipboardItem({ "image/png": blob });
navigator.clipboard.write([item]);
});

URL.revokeObjectURL(url);
};

img.src = url;
} catch (err) {
console.error("Failed to copy qr code", err)
}
}



const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
variables: {
id: equipmentID,
Expand All @@ -70,6 +116,7 @@ export default function ManageEquipmentPage() {

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

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

Expand Down Expand Up @@ -104,6 +151,19 @@ export default function ManageEquipmentPage() {
>
<Typography variant={isMobile ? "h5" : "h3"}>{`Manage ${equipment.name}`}</Typography>
<Stack direction={"row"} spacing={1}>

<Button
color="info"
variant="contained"
startIcon={<QrCodeIcon />}
onClick={() => setShowQrCode(true)}
sx={{
justifySelf: "flex-end"
}}
>
User QR Code
</Button>

<Button
color="secondary"
variant="contained"
Expand Down Expand Up @@ -174,7 +234,28 @@ export default function ManageEquipmentPage() {
</Button>
</Stack>
<MaintenanceTicketGrid equipmentID={equipment.id} />


{/* Equipment User Info QR Code Modal */}
<PrettyModal open={showQrCode} onClose={() => setShowQrCode(false)} elevation={7}>
<Stack alignItems={"center"} spacing="10px">
<Link
href={`${import.meta.env.VITE_ORIGIN}/app/makerspace/${makerspaceID}/equipmentUserInfo/${equipmentID}`}
target={"_blank"}
>User Info Page</Link>
<QRCode ref={qrRef} value={`${import.meta.env.VITE_ORIGIN}/app/makerspace/${makerspaceID}/equipmentUserInfo/${equipmentID}`} />
<Button variant="contained" color="info" onClick={copyQRCode} >Copy</Button>
</Stack>
</PrettyModal>



</Stack>





{/* Equipment Information Modal */}
<PrettyModal open={editEquipmentModal} onClose={() => setEditEquipmentModal(false)} width={"90%"} elevation={8}>
<EquipmentInformation equipment={equipment} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,16 @@ export default function MaintenancePage() {
// const containsOperator = getGridStringOperators().filter((operator) => operator.value === "contains");

const columns: GridColDef[] = [
{ field: "id", headerName: "ID", width: 10, filterable: false, resizable: false, hideable: false },
{ field: "id", headerName: "ID", width: 25, filterable: false, resizable: false, hideable: false },
{ field: "equipment", headerName: "Equipment", width: 350, sortable: false, filterable: false, resizable: false },
{ field: "instance", headerName: "Instance", width: 300, sortable: false, filterable: false, resizable: false },
{ field: "type", headerName: "Type", width: 140, sortable: false, filterable: false, resizable: false },
{ field: "status", headerName: "Status", width: 140, filterable: false, resizable: false },
{ field: "severity", headerName: "Severity", width: 140, filterable: false, resizable: false },
{ field: "creator", headerName: "Creator", width: 140, sortable: false, filterable: false, resizable: false },
{ field: "assigned", headerName: "Assigned", width: 140, sortable: false, filterable: false, resizable: false },
{ field: "dateCreated", headerName: "Created", width: 180, filterable: false, resizable: false },
{ field: "severity", headerName: "Severity", width: 120, filterable: false, resizable: false },
{ field: "creator", headerName: "Creator", width: 120, sortable: false, filterable: false, resizable: false },
{ field: "assigned", headerName: "Assigned", width: 120, sortable: false, filterable: false, resizable: false },
{ field: "timeUnit", headerName: "Time Unit", width: 140, sortable: false, filterable: false, resizable: false },
{ field: "dateCreated", headerName: "Create Date", width: 180, filterable: false, resizable: false },
{
field: "manage", headerName: "Manage", width: 140, filterable: false, sortable: false, renderCell: (params: GridRenderCellParams<any, MaintenanceTicket>) => (
<MaintenanceTicketButtonCell ticket={params.value} />
Expand Down Expand Up @@ -96,6 +97,7 @@ export default function MaintenancePage() {
severity: ticket.severity,
creator: ticket.type === MaintenanceTicketType.REPORTED ? ticket.creator?.ritUsername ?? "" : "SERVER",
assigned: ticket.assigned?.ritUsername ?? "UNASSIGNED",
timeUnit: ticket.timeUnit,
dateCreated: formatter.format(new Date(Number(ticket.dateCreated))),
manage: ticket
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Stack } from "@mui/system";
import PrettyModal from "../../../common/PrettyModal";
import { ASSIGN_MAINTENANCE_TICKET, DELETE_MAINTENACE_TICKET, MaintenanceTicket, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketType, MODIFY_MAINTENANCE_TICKET_STATUS, UPDATE_MAINTENACE_TICKET } from "../../../queries/maintenanceTicketQueries";
import { ASSIGN_MAINTENANCE_TICKET, DELETE_MAINTENACE_TICKET, MaintenanceTicket, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketTimeUnit, MaintenanceTicketType, MODIFY_MAINTENANCE_TICKET_STATUS, UPDATE_MAINTENACE_TICKET } from "../../../queries/maintenanceTicketQueries";
import { Autocomplete, Button, Chip, IconButton, TextField, Typography } from "@mui/material";
import { useState } from "react";
import CloseIcon from '@mui/icons-material/Close';
Expand All @@ -17,6 +17,7 @@ import { useParams } from "react-router-dom";
import { GET_VALID_STAFF } from "../../../queries/makerspaceQueries";
import { CurrentUser, useCurrentUser } from "../../../common/CurrentUserProvider";
import { isManager } from "../../../common/PrivilegeUtils";
import { formatHobbsTime } from "../../../queries/equipmentInstanceQueries";

interface TicketModalProps {
open: boolean,
Expand Down Expand Up @@ -194,6 +195,19 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
}
}

function createTimeLabel(){
const after = `Create After: ${props.ticket.intervalHours} hours`
const difference = props.ticket.instance.hobbsTime - props.ticket.hobbsTimeAtCreate
if (difference < 0){
return `${after} (${formatHobbsTime(Math.abs(difference))} to go)`
} else {
return `${after} (${formatHobbsTime(difference)} ago)`
}
}

const shouldShowHobbsAtOpen= props.ticket.timeUnit == MaintenanceTicketTimeUnit.USAGE || props.ticket.status != MaintenanceTicketStatus.UPCOMING;
const shouldShowHobbsAtClose= props.ticket.status == MaintenanceTicketStatus.CLOSED;

return (
<PrettyModal open={props.open} onClose={handleClose} width={"600px"}>
<Stack spacing={2}>
Expand All @@ -214,7 +228,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
</Stack>
<Stack direction={"row"} justifyContent={"space-between"}>
<Typography variant="subtitle1">{`Ticket #${props.ticket.id}`}</Typography>
<Typography variant="subtitle1">{`Created: ${formatter.format(Number(props.ticket.dateCreated))}`}</Typography>
{ props.ticket.timeUnit=== MaintenanceTicketTimeUnit.CALENDAR ?
<Typography variant="subtitle1">{`Create Date: ${formatter.format(Number(props.ticket.dateCreated))}`}</Typography>
: <Typography variant="subtitle1">{createTimeLabel()}</Typography>
}
</Stack>
<Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
<Typography variant="body1">{`Reported by: ${props.ticket.type === MaintenanceTicketType.REPORTED ? props.ticket.creator?.ritUsername ?? "" : "SERVER"}`}</Typography>
Expand Down Expand Up @@ -317,6 +334,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
</Stack>
}
</Stack>
<Stack justifyContent={"space-between"} direction={"row"}>
{shouldShowHobbsAtOpen ? <Typography variant="body1">Usage at Open: {formatHobbsTime(props.ticket.hobbsTimeAtCreate)}</Typography> : undefined}
{shouldShowHobbsAtClose ? <Typography variant="body1">Usage at Close: {props.ticket.hobbsTimeAtClose ? formatHobbsTime(props.ticket.hobbsTimeAtClose) : "N/A"}</Typography> : undefined}
</Stack>
{
editing
? <TextField
Expand Down
Loading
Loading