diff --git a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx
index 445484dd..71644729 100644
--- a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx
+++ b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx
@@ -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";
@@ -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) {
@@ -165,7 +152,7 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps)
{
!allowEdit
- ? formatSeconds(props.instance.hobbsTime)
+ ? formatHobbsTime(props.instance.hobbsTime)
: hobbsTimeSetField()
}
diff --git a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx
index 83c298dd..9256b127 100644
--- a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx
+++ b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx
@@ -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";
@@ -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 No User;
@@ -162,7 +148,7 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) {
{
!allowEdit
- ? formatSeconds(props.instance.hobbsTime)
+ ? formatHobbsTime(props.instance.hobbsTime)
: hobbsTimeSetField()
}
diff --git a/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx b/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx
index a9c01cdd..099dca8a 100644
--- a/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx
+++ b/client/src/pages/makerspace_page/equipment_pages/EquipmentUserInfo.tsx
@@ -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";
@@ -11,13 +11,16 @@ 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();
@@ -25,6 +28,7 @@ export default function EquipmentUserInfo() {
const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
+ const isStaffForHere = isStaffFor(user, Number(makerspaceID));
const getEquipmentByIDResult = useQuery(GET_EQUIPMENT_BY_ID, {
variables: {
id: equipmentID,
@@ -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
@@ -209,12 +217,24 @@ export default function EquipmentUserInfo() {
: null
}
-
+ {isStaffForHere ? setNewTicketModal(true)}
+ startIcon={}
+ >
+ Report Issue
+ : undefined}
+
}
@@ -234,7 +254,6 @@ export default function EquipmentUserInfo() {
LOGIN
-
}
return
@@ -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 ? setNewTicketModal(false)} makerspace={getMakerspace?.data?.makerspaceByID} /> : undefined}
+
}
\ No newline at end of file
diff --git a/client/src/pages/makerspace_page/equipment_pages/ManageEquipmentPage.tsx b/client/src/pages/makerspace_page/equipment_pages/ManageEquipmentPage.tsx
index 78cd495b..c0969a46 100644
--- a/client/src/pages/makerspace_page/equipment_pages/ManageEquipmentPage.tsx
+++ b/client/src/pages/makerspace_page/equipment_pages/ManageEquipmentPage.tsx
@@ -1,5 +1,5 @@
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";
@@ -7,7 +7,7 @@ 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';
@@ -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;
@@ -51,6 +53,10 @@ export interface Equipment {
}
+
+
+
+
export default function ManageEquipmentPage() {
const { makerspaceID, equipmentID } = useParams<{ makerspaceID: string, equipmentID: string }>();
const isMobile = useIsMobile();
@@ -58,6 +64,46 @@ export default function ManageEquipmentPage() {
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,
@@ -70,6 +116,7 @@ export default function ManageEquipmentPage() {
const [newInstanceName, setNewInstanceName] = useState("");
const [newInstanceModal, setNewInstanceModal] = useState(false);
+ const [showQrCode, setShowQrCode] = useState(false);
const [editEquipmentModal, setEditEquipmentModal] = useState(false);
@@ -104,6 +151,19 @@ export default function ManageEquipmentPage() {
>
{`Manage ${equipment.name}`}
+
+ }
+ onClick={() => setShowQrCode(true)}
+ sx={{
+ justifySelf: "flex-end"
+ }}
+ >
+ User QR Code
+
+
+
+
+ {/* Equipment User Info QR Code Modal */}
+ setShowQrCode(false)} elevation={7}>
+
+ User Info Page
+
+ Copy
+
+
+
+
+
+
+
+
+
+
{/* Equipment Information Modal */}
setEditEquipmentModal(false)} width={"90%"} elevation={8}>
diff --git a/client/src/pages/makerspace_page/maintenance_pages/MaintenancePage.tsx b/client/src/pages/makerspace_page/maintenance_pages/MaintenancePage.tsx
index 45cc5d1a..4f511371 100644
--- a/client/src/pages/makerspace_page/maintenance_pages/MaintenancePage.tsx
+++ b/client/src/pages/makerspace_page/maintenance_pages/MaintenancePage.tsx
@@ -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) => (
@@ -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
}
diff --git a/client/src/pages/makerspace_page/maintenance_pages/MaintenanceTicketModal.tsx b/client/src/pages/makerspace_page/maintenance_pages/MaintenanceTicketModal.tsx
index 83871475..b9312ff4 100644
--- a/client/src/pages/makerspace_page/maintenance_pages/MaintenanceTicketModal.tsx
+++ b/client/src/pages/makerspace_page/maintenance_pages/MaintenanceTicketModal.tsx
@@ -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';
@@ -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,
@@ -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 (
@@ -214,7 +228,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
{`Ticket #${props.ticket.id}`}
- {`Created: ${formatter.format(Number(props.ticket.dateCreated))}`}
+ { props.ticket.timeUnit=== MaintenanceTicketTimeUnit.CALENDAR ?
+ {`Create Date: ${formatter.format(Number(props.ticket.dateCreated))}`}
+ : {createTimeLabel()}
+ }
{`Reported by: ${props.ticket.type === MaintenanceTicketType.REPORTED ? props.ticket.creator?.ritUsername ?? "" : "SERVER"}`}
@@ -317,6 +334,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
}
+
+ {shouldShowHobbsAtOpen ? Usage at Open: {formatHobbsTime(props.ticket.hobbsTimeAtCreate)} : undefined}
+ {shouldShowHobbsAtClose ? Usage at Close: {props.ticket.hobbsTimeAtClose ? formatHobbsTime(props.ticket.hobbsTimeAtClose) : "N/A"} : undefined}
+
{
editing
? ();
const equipmentInstancesResult = useQuery(GET_EQUIPMENT_INSTANCES, { variables: { equipmentID: equipment?.id ?? -1 } });
- const [createTicket] = useMutation(CREATE_INTERVAL_MAINTENANCE_TICKET, { refetchQueries: ["PaginatedMaintenanceTickets", "MaintenanceTickets"] });
+ const [createTicket] = useMutation(CREATE_INTERVAL_MAINTENANCE_TICKET, { refetchQueries: [{query: PAGINATED_MAINTENANCE_TICKETS}, {query: GET_MAINTENANCE_TICKETS}] });
const EMPTY_ARRAY: EquipmentInstance[] = [];
const instances: EquipmentInstance[] = equipmentInstancesResult.data?.equipmentInstances ?? EMPTY_ARRAY;
@@ -50,7 +50,8 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
const makerspace_equipments = makerspace_equipments_2?.flat(1);
const [startDate, setStartDate] = useState(new Date());
- const [scale, setScale] = useState("days");
+ const [timeUnit, setTimeUnit] = useState(""); // USAGE | CALENDAR
+ const [scale, setScale] = useState("days"); // hours | days | weeks
const [interval, setInterval] = useState("1");
function handleChangeScale(_event: React.MouseEvent, value: string) {
@@ -58,6 +59,11 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
setScale(value);
}
}
+ function handleChangeTimeUnit(_event: React.MouseEvent, value: string) {
+ if (value !== null && value !== timeUnit) {
+ setTimeUnit(value);
+ }
+ }
function handleClose() {
setEquipment(undefined);
@@ -67,17 +73,20 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
setImageUrl(undefined);
setStartDate(new Date());
setScale("days");
+ setTimeUnit("CALENDAR")
setInterval("1");
props.onClose();
}
async function handleCreateTicket() {
- if (!(equipment && reportedInstance && severity && !Number.isNaN(Number(interval)))) {
- toast.error("A required field is empty!");
+ if (!(equipment && reportedInstance && severity && !Number.isNaN(Number(interval)) && (timeUnit == "USAGE" || timeUnit == "CALENDAR") ) ) {
+ toast.error("A required field is empty or invalid!");
return;
}
try {
+ const intervalHours = scale === "days" ? Math.floor(Number(interval) * 24) : (scale == "weeks") ? Math.floor(Number(interval) * 168) : Math.floor(Number(interval))
+
await createTicket({
variables: {
severity: severity,
@@ -85,7 +94,8 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
description: description,
startDate: startDate.toISOString(),
imageUrl: imageUrl,
- intervalHours: scale === "days" ? Number(interval) * 24 : Number(interval) * 168
+ intervalHours: intervalHours,
+ timeUnit: timeUnit,
}
})
} catch (e) {
@@ -127,6 +137,7 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
{
equipment
? (
@@ -159,21 +170,40 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
disabled
/>
}
- (
-
- )
- }
- options={[MaintenanceTicketSeverity.HIGH, MaintenanceTicketSeverity.MEDIUM, MaintenanceTicketSeverity.LOW]}
- value={severity}
- onChange={(event, newValue) => setSeverity(newValue ?? undefined)}
- />
+
+ (
+
+ )
+ }
+ options={[MaintenanceTicketSeverity.HIGH, MaintenanceTicketSeverity.MEDIUM, MaintenanceTicketSeverity.LOW]}
+ value={severity}
+ onChange={(event, newValue) => setSeverity(newValue ?? undefined)}
+ sx={{flexGrow: 1}}
+ />
+
+
+
+ Usage
+
+
+ Calendar
+
+
+
+
+
+
+
+
+ Hours
+
Days
diff --git a/client/src/pages/makerspace_page/maintenance_pages/NewTicketModal.tsx b/client/src/pages/makerspace_page/maintenance_pages/NewTicketModal.tsx
index 2fb54285..4e5431c2 100644
--- a/client/src/pages/makerspace_page/maintenance_pages/NewTicketModal.tsx
+++ b/client/src/pages/makerspace_page/maintenance_pages/NewTicketModal.tsx
@@ -13,6 +13,7 @@ import { toast } from "react-toastify";
import FileUploadButton from "../../../common/FileUploadButton";
import styled from "styled-components";
import { makeCDNLink } from "../../../common/ImageFinder";
+import { useIsMobile } from "../../../common/IsMobileProvider";
const StyledImg = styled.img`
padding: 5px
@@ -27,6 +28,7 @@ interface NewTicketModalProps {
export default function NewTicketModal(props: NewTicketModalProps) {
const user = useCurrentUser();
+ const isMobile = useIsMobile();
const [equipment, setEquipment] = useState(props.equipment);
const [instance, setInstance] = useState();
@@ -93,7 +95,7 @@ export default function NewTicketModal(props: NewTicketModalProps) {
}
return (
-
+
Create a Maintenance Ticket
@@ -186,7 +188,7 @@ export default function NewTicketModal(props: NewTicketModalProps) {
?
: null
}
-
+
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"
+ }
+}
+
+
export const GET_EQUIPMENT_INSTANCES = gql`
query EquipmentInstances($equipmentID: ID!) {
diff --git a/client/src/queries/maintenanceTicketQueries.ts b/client/src/queries/maintenanceTicketQueries.ts
index 4f4cf875..3088c871 100644
--- a/client/src/queries/maintenanceTicketQueries.ts
+++ b/client/src/queries/maintenanceTicketQueries.ts
@@ -2,6 +2,11 @@ import { gql } from "@apollo/client";
import { CurrentUser } from "../common/CurrentUserProvider";
import { EquipmentInstance } from "./equipmentInstanceQueries";
+export enum MaintenanceTicketTimeUnit {
+ USAGE = "USAGE",
+ CALENDAR = "CALENDAR"
+}
+
export enum MaintenanceTicketType {
AUTOMATIC = "AUTOMATIC",
REPORTED = "REPORTED"
@@ -32,7 +37,10 @@ export interface MaintenanceTicket {
creator: CurrentUser | null,
instance: EquipmentInstance,
assigned: CurrentUser | null,
- intervalHours: number | null
+ intervalHours: number | null,
+ timeUnit: MaintenanceTicketTimeUnit,
+ hobbsTimeAtCreate: number,
+ hobbsTimeAtClose: number | null
}
export const PAGINATED_MAINTENANCE_TICKETS = gql`
@@ -47,6 +55,9 @@ export const PAGINATED_MAINTENANCE_TICKETS = gql`
dateCreated
dateClosed
intervalHours
+ timeUnit
+ hobbsTimeAtCreate
+ hobbsTimeAtClose
creator {
id
ritUsername
@@ -58,6 +69,7 @@ export const PAGINATED_MAINTENANCE_TICKETS = gql`
id
name
}
+ hobbsTime
}
assigned {
id
@@ -80,6 +92,9 @@ export const GET_MAINTENANCE_TICKET = gql`
imageUrl
dateCreated
dateClosed
+ timeUnit
+ hobbsTimeAtCreate
+ hobbsTimeAtClose
creator {
id
ritUsername
@@ -91,6 +106,7 @@ export const GET_MAINTENANCE_TICKET = gql`
id
name
}
+ hobbsTime
}
}
}
@@ -133,6 +149,9 @@ export const GET_MAINTENANCE_TICKETS = gql`
dateCreated
dateClosed
intervalHours
+ timeUnit
+ hobbsTimeAtCreate
+ hobbsTimeAtClose
creator {
id
ritUsername
@@ -150,6 +169,7 @@ export const GET_MAINTENANCE_TICKETS = gql`
id
name
}
+ hobbsTime
}
}
}
@@ -180,7 +200,8 @@ export const CREATE_INTERVAL_MAINTENANCE_TICKET = gql`
$description: String!,
$startDate: String!,
$intervalHours: Int!,
- $imageUrl: String
+ $imageUrl: String,
+ $timeUnit: String!,
) {
createIntervalMaintenanceTicket(
severity: $severity,
@@ -188,7 +209,8 @@ export const CREATE_INTERVAL_MAINTENANCE_TICKET = gql`
description: $description,
startDate: $startDate,
intervalHours: $intervalHours,
- imageUrl: $imageUrl
+ imageUrl: $imageUrl,
+ timeUnit: $timeUnit,
) {
id
}
diff --git a/server/src/database/knex/migrations/20260721160735_hobbs_time_tickets.ts b/server/src/database/knex/migrations/20260721160735_hobbs_time_tickets.ts
new file mode 100644
index 00000000..4dfc5008
--- /dev/null
+++ b/server/src/database/knex/migrations/20260721160735_hobbs_time_tickets.ts
@@ -0,0 +1,27 @@
+import type { Knex } from "knex";
+
+
+export async function up(knex: Knex): Promise {
+ knex.schema.hasTable("MaintenanceTickets").then(function (exists) {
+ if (!exists) return;
+
+ return knex.schema.alterTable("MaintenanceTickets", function (t) {
+ t.enum("timeUnit", ["USAGE", "CALENDAR"]).notNullable().defaultTo("CALENDAR")
+ t.integer("hobbsTimeAtCreate").notNullable().defaultTo(0);
+ t.integer("hobbsTimeAtClose").nullable();
+ });
+ });
+}
+
+
+export async function down(knex: Knex): Promise {
+ knex.schema.hasTable("MaintenanceTickets").then(function (exists) {
+ if (!exists) return;
+
+ return knex.schema.alterTable("MaintenanceTickets", function (t) {
+ t.dropColumns("timeUnit", "hobbsTimeAtCreate", "hobbsTimeAtClose");
+ });
+ });
+
+}
+
diff --git a/server/src/database/knex/tables.ts b/server/src/database/knex/tables.ts
index b83ccb44..5029e0b3 100644
--- a/server/src/database/knex/tables.ts
+++ b/server/src/database/knex/tables.ts
@@ -626,6 +626,11 @@ export interface ReservationRow {
end: string; // ^
}
+export enum MaintenanceTicketTimeUnit {
+ USAGE = "USAGE",
+ CALENDAR = "CALENDAR"
+}
+
export enum MaintenanceTicketType {
AUTOMATIC = "AUTOMATIC",
REPORTED = "REPORTED"
@@ -640,7 +645,7 @@ export enum MaintenanceTicketSeverity {
export enum MaintenanceTicketStatus {
UPCOMING = "UPCOMING",
TODO = "TODO",
- IN_PROGESS = "IN_PROGRESS",
+ IN_PROGRESS = "IN_PROGRESS",
CLOSED = "CLOSED"
}
@@ -657,6 +662,9 @@ export interface MaintenanceTicketRow {
dateClosed: string | null; // ^
intervalHours: number | null;
assignedID: number | null;
+ timeUnit: MaintenanceTicketTimeUnit;
+ hobbsTimeAtCreate: number;
+ hobbsTimeAtClose: number | null;
}
export interface TempCardRow {
diff --git a/server/src/database/repositories/Equipment/MaintenanceTicketRepository.ts b/server/src/database/repositories/Equipment/MaintenanceTicketRepository.ts
index fe423259..6dd8f755 100644
--- a/server/src/database/repositories/Equipment/MaintenanceTicketRepository.ts
+++ b/server/src/database/repositories/Equipment/MaintenanceTicketRepository.ts
@@ -1,7 +1,8 @@
import { GraphQLError } from "graphql";
import { knex } from "../../knex/index.js";
-import { MaintenanceTicketRow, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketType } from "../../knex/tables.js";
+import { MaintenanceTicketRow, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketTimeUnit, MaintenanceTicketType } from "../../knex/tables.js";
import { addHours, endOfDay } from "date-fns"
+import { getInstanceByID } from "./EquipmentInstancesRepository.js";
export async function createMaintenanceTicket(
type: MaintenanceTicketType,
@@ -31,6 +32,8 @@ export async function createIntervalMaintenanceTicket(
instanceID: number,
description: string,
startDate: string,
+ hobbsTimeAtCreate: number,
+ timeUnit: MaintenanceTicketTimeUnit,
intervalHours: number,
imageUrl?: string
): Promise {
@@ -42,7 +45,9 @@ export async function createIntervalMaintenanceTicket(
description,
dateCreated: startDate,
intervalHours,
- imageUrl
+ timeUnit: timeUnit,
+ imageUrl,
+ hobbsTimeAtCreate: hobbsTimeAtCreate
}).returning("*");
if (result.length > 0) {
@@ -63,20 +68,28 @@ export async function modifyMaintenanceTicketStatus(id: number, status: Maintena
return 0; // don't update the status to the same thing
}
- if (status === MaintenanceTicketStatus.CLOSED) {
-
- if (ticket.type === MaintenanceTicketType.AUTOMATIC) {
-
- const newTargetDate = addHours(new Date(), ticket.intervalHours ?? 0);
- await createIntervalMaintenanceTicket(
- ticket.severity,
- ticket.instanceID,
- ticket.description,
- newTargetDate.toISOString(), ticket.intervalHours ?? 0
- )
- }
+ const inst = await getInstanceByID(ticket.instanceID)
+ if (!inst) {
+ throw new GraphQLError("Could not find instance for ticket to update hours")
+ }
- return await knex("MaintenanceTickets").update({ status: status, dateClosed: knex.fn.now() }).where({ id: id });
+ if (status === MaintenanceTicketStatus.CLOSED && ticket.type === MaintenanceTicketType.AUTOMATIC) {
+
+ const newTargetDate = addHours(new Date(), ticket.intervalHours ?? 0);
+ const newTargetHobbs = inst.hobbsTime + (ticket.intervalHours ?? 0) * 60 * 60;
+ await createIntervalMaintenanceTicket(
+ ticket.severity,
+ ticket.instanceID,
+ ticket.description,
+ newTargetDate.toISOString(),
+ newTargetHobbs,
+ ticket.timeUnit,
+ ticket.intervalHours ?? 0
+ )
+
+ return await knex("MaintenanceTickets").update({ status: status, hobbsTimeAtClose: inst.hobbsTime, dateClosed: knex.fn.now() }).where({ id: id });
+ } else if (status === MaintenanceTicketStatus.CLOSED) {
+ return await knex("MaintenanceTickets").update({ status: status, hobbsTimeAtClose: inst.hobbsTime }).where({ id: id });
} else {
return await knex("MaintenanceTickets").update({ status: status }).where({ id: id });
}
@@ -189,12 +202,28 @@ export async function assignMaintenanceTicket(id: number, assignedID: number | n
return result[0];
}
-export async function advanceIntervalTickets(): Promise {
- return await knex("MaintenanceTickets").update({ status: MaintenanceTicketStatus.TODO })
- .where("status", "=", MaintenanceTicketStatus.UPCOMING).andWhere("dateCreated", "<=", endOfDay(new Date()).toISOString()) // TODO: normalize this to timezone
- .returning("*");
+export async function advanceCalendarIntervalTickets(): Promise {
+
+ return await knex("MaintenanceTickets as mt")
+ .update({ status: MaintenanceTicketStatus.TODO, hobbsTimeAtCreate: knex.raw('e."hobbsTime"') })
+ .updateFrom("EquipmentInstances as e").where("e.id", "=", knex.raw('mt."instanceID"'))
+ .andWhere("mt.status", "=", MaintenanceTicketStatus.UPCOMING)
+ .andWhere("mt.timeUnit", "=", MaintenanceTicketTimeUnit.CALENDAR)
+ .andWhere("mt.dateCreated", "<=", endOfDay(new Date()).toISOString()) // TODO: normalize this to timezone
+ .returning("mt.*");
+}
+export async function advanceUsageIntervalTickets(): Promise {
+ return await knex("MaintenanceTickets as m")
+ .update({ status: MaintenanceTicketStatus.TODO, dateCreated: knex.fn.now(), hobbsTimeAtCreate: knex.raw('e."hobbsTime"') })
+ .updateFrom("EquipmentInstances as e").where("e.id", "=", knex.raw('m."instanceID"'))
+ .andWhere("m.status", "=", MaintenanceTicketStatus.UPCOMING)
+ .andWhere("m.timeUnit", "=", MaintenanceTicketTimeUnit.USAGE)
+ .andWhere("m.hobbsTimeAtCreate", "<=", knex.raw('e."hobbsTime"'))
+ .returning("m.*");
}
+
+
export async function deleteMaintenanceTicket(id: number): Promise {
return await knex("MaintenanceTickets").delete().where("id", "=", id);
}
\ No newline at end of file
diff --git a/server/src/graphql/resolvers/maintenanceTicketResolver.ts b/server/src/graphql/resolvers/maintenanceTicketResolver.ts
index 04ae8262..33d8556f 100644
--- a/server/src/graphql/resolvers/maintenanceTicketResolver.ts
+++ b/server/src/graphql/resolvers/maintenanceTicketResolver.ts
@@ -1,5 +1,5 @@
import { ApolloContext } from "../../context.js"
-import { MaintenanceTicketRow, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketType } from "../../database/knex/tables.js"
+import { MaintenanceTicketRow, MaintenanceTicketSeverity, MaintenanceTicketStatus, MaintenanceTicketTimeUnit, MaintenanceTicketType } from "../../database/knex/tables.js"
import * as MaintenanceTicketRepo from "../../database/repositories/Equipment/MaintenanceTicketRepository.js"
import * as InstanceRepo from "../../database/repositories/Equipment/EquipmentInstancesRepository.js"
import * as UserRepo from "../../database/repositories/Users/UserRepository.js"
@@ -7,6 +7,7 @@ import * as AuditLogRepo from "../../database/repositories/AuditLogs/AuditLogRep
import * as EquipmentRepo from "../../database/repositories/Equipment/EquipmentRepository.js";
import { notifyNewMaintenanceTicket } from "../../integrations/slack/slack.js"
import { Equipment } from "../../database/models/equipment/Equipment.js"
+import { GraphQLError } from "graphql"
const MaintenanceTicketResolver = {
MaintenanceTicket: {
@@ -112,19 +113,30 @@ const MaintenanceTicketResolver = {
description: string,
startDate: string,
intervalHours: number,
- imageUrl?: string
+ imageUrl?: string,
+ timeUnit: MaintenanceTicketTimeUnit,
},
{ isManager }: ApolloContext // should perhaps be ifManagerFor
- ) => isManager(async (_user) => (
+ ) => isManager(async (_user) => {
+ const inst = await InstanceRepo.getInstanceByID(args.instanceID);
+ if (inst == undefined){
+ throw new GraphQLError("Could not find instance for maintenance ticket");
+ }
+ let hobbsTimeAtCreate = inst.hobbsTime;
+ if (args.timeUnit == MaintenanceTicketTimeUnit.USAGE){
+ hobbsTimeAtCreate += args.intervalHours * 60 * 60;
+ }
await MaintenanceTicketRepo.createIntervalMaintenanceTicket(
args.severity,
args.instanceID,
args.description,
- args.startDate,
- args.intervalHours,
+ args.startDate,
+ hobbsTimeAtCreate,
+ args.timeUnit,
+ args.intervalHours,
args.imageUrl
)
- )),
+ }),
modifyMaintenanceTicketStatus: async (
_parent: any,
diff --git a/server/src/graphql/schemas/maintenanceTicketSchema.ts b/server/src/graphql/schemas/maintenanceTicketSchema.ts
index 16f54dc7..e5a50b77 100644
--- a/server/src/graphql/schemas/maintenanceTicketSchema.ts
+++ b/server/src/graphql/schemas/maintenanceTicketSchema.ts
@@ -2,6 +2,11 @@ import { gql } from "graphql-tag";
export const MaintenanceTicketTypeDefs = gql`
+ enum MaintenanceTicketTimeUnit {
+ USAGE
+ CALENDAR
+ }
+
enum MaintenanceTicketType {
AUTOMATIC
REPORTED
@@ -36,6 +41,9 @@ export const MaintenanceTicketTypeDefs = gql`
instance: EquipmentInstance!
creator: User
assigned: User
+ timeUnit: MaintenanceTicketTimeUnit
+ hobbsTimeAtCreate: Int!
+ hobbsTimeAtClose: Int
}
input Pagination {
@@ -75,7 +83,8 @@ export const MaintenanceTicketTypeDefs = gql`
description: String!,
startDate: String!,
intervalHours: Int!,
- imageUrl: String
+ imageUrl: String,
+ timeUnit: String!,
): MaintenanceTicket
modifyMaintenanceTicketStatus(id: Int!, status: MaintenanceTicketStatus!): Int
diff --git a/server/src/periodicActions.ts b/server/src/periodicActions.ts
index 9ea901f1..ba9a260c 100644
--- a/server/src/periodicActions.ts
+++ b/server/src/periodicActions.ts
@@ -5,7 +5,7 @@ import { notifyNewMaintenanceTicket } from "./integrations/slack/slack.js";
import { CoreActions } from "./database/models/api/ACSFormats.js";
import { ACSOrchestrator } from "./database/models/api/ACSOrchestrator.js";
import { createUnassocaitedAuditLog } from "./database/repositories/AuditLogs/AuditLogRepository.js";
-import { advanceIntervalTickets } from "./database/repositories/Equipment/MaintenanceTicketRepository.js";
+import { advanceCalendarIntervalTickets, advanceUsageIntervalTickets } from "./database/repositories/Equipment/MaintenanceTicketRepository.js";
/**
* If you use the CDN to store user-uploads, make sure you update this query so it knows that those images are in use
@@ -67,9 +67,12 @@ export async function deletePastSpecialHours(): Promise {
export async function advanceTimeTickets(): Promise {
try {
- const result = await advanceIntervalTickets();
+ const calendarResult = await advanceCalendarIntervalTickets();
+ const usageResult = await advanceUsageIntervalTickets();
+
+ const result = [...calendarResult, ...usageResult]
if (result.length > 0) {
- createUnassocaitedAuditLog(`Advanced ${result.length} time-based maintenance tickets`, "server");
+ createUnassocaitedAuditLog(`Advanced ${calendarResult.length} calendar-time and ${usageResult.length} usage-time maintenance tickets`, "server");
}
result.forEach((ticket) => {
diff --git a/server/src/server.ts b/server/src/server.ts
index dd776a21..d2e3b9df 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -623,7 +623,7 @@ async function startServer() {
// Delete past special hours
await deletePastSpecialHours();
- // Advance any time-based maintennace tickets from UPCOMING -> TODO
+ // Advance any time-based maintenance tickets from UPCOMING -> TODO
await advanceTimeTickets();
// Get a new root certificate for readers