Skip to content

Commit c2e96fa

Browse files
committed
Calculate hobbs create time server side, update modals to show correct time information without being too misleading, advance tickets and set parameters accordingly
1 parent f38a1da commit c2e96fa

8 files changed

Lines changed: 73 additions & 43 deletions

File tree

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: 19 additions & 4 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';
@@ -195,6 +195,18 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
195195
}
196196
}
197197

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;
198210

199211
return (
200212
<PrettyModal open={props.open} onClose={handleClose} width={"600px"}>
@@ -216,7 +228,10 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
216228
</Stack>
217229
<Stack direction={"row"} justifyContent={"space-between"}>
218230
<Typography variant="subtitle1">{`Ticket #${props.ticket.id}`}</Typography>
219-
<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+
}
220235
</Stack>
221236
<Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
222237
<Typography variant="body1">{`Reported by: ${props.ticket.type === MaintenanceTicketType.REPORTED ? props.ticket.creator?.ritUsername ?? "" : "SERVER"}`}</Typography>
@@ -320,8 +335,8 @@ export default function MaintenanceTicketModal(props: TicketModalProps) {
320335
}
321336
</Stack>
322337
<Stack justifyContent={"space-between"} direction={"row"}>
323-
<Typography variant="body1">Usage at Open: {formatHobbsTime(props.ticket.hobbsTimeAtCreate)}</Typography>
324-
<Typography variant="body1">Usage at Close: {props.ticket.hobbsTimeAtClose ? formatHobbsTime(props.ticket.hobbsTimeAtClose) : "N/A"}</Typography>
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}
325340
</Stack>
326341
{
327342
editing

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { FullMakerspace } from "../../../queries/makerspaceQueries";
77
import { useMemo, useState } from "react";
88
import { EquipmentInstance, GET_EQUIPMENT_INSTANCES } from "../../../queries/equipmentInstanceQueries";
99
import { useMutation, useQuery } from "@apollo/client/react";
10-
import { CREATE_INTERVAL_MAINTENANCE_TICKET, MaintenanceTicketSeverity } from "../../../queries/maintenanceTicketQueries";
10+
import { CREATE_INTERVAL_MAINTENANCE_TICKET, GET_MAINTENANCE_TICKETS, MaintenanceTicketSeverity, PAGINATED_MAINTENANCE_TICKETS } from "../../../queries/maintenanceTicketQueries";
1111
import { toast } from "react-toastify";
1212
import FileUploadButton from "../../../common/FileUploadButton";
1313
import styled from "styled-components";
@@ -32,7 +32,7 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
3232
const [imageUrl, setImageUrl] = useState<string>();
3333

3434
const equipmentInstancesResult = useQuery(GET_EQUIPMENT_INSTANCES, { variables: { equipmentID: equipment?.id ?? -1 } });
35-
const [createTicket] = useMutation(CREATE_INTERVAL_MAINTENANCE_TICKET, { refetchQueries: ["PaginatedMaintenanceTickets", "MaintenanceTickets"] });
35+
const [createTicket] = useMutation(CREATE_INTERVAL_MAINTENANCE_TICKET, { refetchQueries: [{query: PAGINATED_MAINTENANCE_TICKETS}, {query: GET_MAINTENANCE_TICKETS}] });
3636

3737
const EMPTY_ARRAY: EquipmentInstance[] = [];
3838
const instances: EquipmentInstance[] = equipmentInstancesResult.data?.equipmentInstances ?? EMPTY_ARRAY;
@@ -50,7 +50,7 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
5050
const makerspace_equipments = makerspace_equipments_2?.flat(1);
5151

5252
const [startDate, setStartDate] = useState(new Date());
53-
const [timeUnit, setTimeUnit] = useState("USAGE"); // USAGE | CALENDAR
53+
const [timeUnit, setTimeUnit] = useState(""); // USAGE | CALENDAR
5454
const [scale, setScale] = useState("days"); // hours | days | weeks
5555
const [interval, setInterval] = useState("1");
5656

@@ -73,19 +73,20 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
7373
setImageUrl(undefined);
7474
setStartDate(new Date());
7575
setScale("days");
76-
setTimeUnit("calendar")
76+
setTimeUnit("CALENDAR")
7777
setInterval("1");
7878

7979
props.onClose();
8080
}
8181

8282
async function handleCreateTicket() {
83-
if (!(equipment && reportedInstance && severity && !Number.isNaN(Number(interval)))) {
84-
toast.error("A required field is empty!");
83+
if (!(equipment && reportedInstance && severity && !Number.isNaN(Number(interval)) && (timeUnit == "USAGE" || timeUnit == "CALENDAR") ) ) {
84+
toast.error("A required field is empty or invalid!");
8585
return;
8686
}
8787
try {
88-
const intervalHours = scale === "days" ? Number(interval) * 24 : (scale == "weeks" ? Number(interval) * 168 : interval)
88+
const intervalHours = scale === "days" ? Math.floor(Number(interval) * 24) : (scale == "weeks") ? Math.floor(Number(interval) * 168) : Math.floor(Number(interval))
89+
8990
await createTicket({
9091
variables: {
9192
severity: severity,
@@ -95,8 +96,6 @@ export default function NewIntervalTicketModal(props: NewTicketModalProps) {
9596
imageUrl: imageUrl,
9697
intervalHours: intervalHours,
9798
timeUnit: timeUnit,
98-
hobbsTimeAtCreate: instance?.hobbsTime
99-
10099
}
101100
})
102101
} catch (e) {

client/src/queries/maintenanceTicketQueries.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const PAGINATED_MAINTENANCE_TICKETS = gql`
6969
id
7070
name
7171
}
72+
hobbsTime
7273
}
7374
assigned {
7475
id
@@ -105,6 +106,7 @@ export const GET_MAINTENANCE_TICKET = gql`
105106
id
106107
name
107108
}
109+
hobbsTime
108110
}
109111
}
110112
}
@@ -167,6 +169,7 @@ export const GET_MAINTENANCE_TICKETS = gql`
167169
id
168170
name
169171
}
172+
hobbsTime
170173
}
171174
}
172175
}
@@ -199,7 +202,6 @@ export const CREATE_INTERVAL_MAINTENANCE_TICKET = gql`
199202
$intervalHours: Int!,
200203
$imageUrl: String,
201204
$timeUnit: String!,
202-
$hobbsTimeAtCreate: Int!,
203205
) {
204206
createIntervalMaintenanceTicket(
205207
severity: $severity,
@@ -209,7 +211,6 @@ export const CREATE_INTERVAL_MAINTENANCE_TICKET = gql`
209211
intervalHours: $intervalHours,
210212
imageUrl: $imageUrl,
211213
timeUnit: $timeUnit,
212-
hobbsTimeAtCreate: $hobbsTimeAtCreate,
213214
) {
214215
id
215216
}

server/src/database/repositories/Equipment/MaintenanceTicketRepository.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,28 @@ export async function modifyMaintenanceTicketStatus(id: number, status: Maintena
6868
return 0; // don't update the status to the same thing
6969
}
7070

71+
const inst = await getInstanceByID(ticket.instanceID)
72+
if (!inst) {
73+
throw new GraphQLError("Could not find instance for ticket to update hours")
74+
}
75+
7176
if (status === MaintenanceTicketStatus.CLOSED && ticket.type === MaintenanceTicketType.AUTOMATIC) {
72-
const inst = await getInstanceByID(ticket.instanceID)
73-
if (!inst) {
74-
throw new GraphQLError("Could not find instance for ticket to update hours")
75-
}
7677

7778
const newTargetDate = addHours(new Date(), ticket.intervalHours ?? 0);
7879
const newTargetHobbs = inst.hobbsTime + (ticket.intervalHours ?? 0) * 60 * 60;
7980
await createIntervalMaintenanceTicket(
8081
ticket.severity,
8182
ticket.instanceID,
82-
ticket.description,
83+
ticket.description,
8384
newTargetDate.toISOString(),
8485
newTargetHobbs,
8586
ticket.timeUnit,
8687
ticket.intervalHours ?? 0
8788
)
8889

89-
return await knex("MaintenanceTickets").update({ status: status, dateClosed: knex.fn.now() }).where({ id: id });
90+
return await knex("MaintenanceTickets").update({ status: status, hobbsTimeAtClose: inst.hobbsTime, dateClosed: knex.fn.now() }).where({ id: id });
91+
} else if (status === MaintenanceTicketStatus.CLOSED) {
92+
return await knex("MaintenanceTickets").update({ status: status, hobbsTimeAtClose: inst.hobbsTime }).where({ id: id });
9093
} else {
9194
return await knex("MaintenanceTickets").update({ status: status }).where({ id: id });
9295
}
@@ -200,20 +203,23 @@ export async function assignMaintenanceTicket(id: number, assignedID: number | n
200203
}
201204

202205
export async function advanceCalendarIntervalTickets(): Promise<MaintenanceTicketRow[]> {
203-
return await knex("MaintenanceTickets").update({ status: MaintenanceTicketStatus.TODO })
204-
.where("status", "=", MaintenanceTicketStatus.UPCOMING)
205-
.andWhere("timeUnit", "=", MaintenanceTicketTimeUnit.CALENDAR)
206-
.andWhere("dateCreated", "<=", endOfDay(new Date()).toISOString()) // TODO: normalize this to timezone
207-
.returning("*");
206+
207+
return await knex("MaintenanceTickets as mt")
208+
.update({ status: MaintenanceTicketStatus.TODO, hobbsTimeAtCreate: knex.raw('e."hobbsTime"') })
209+
.updateFrom("EquipmentInstances as e").where("e.id", "=", knex.raw('mt."instanceID"'))
210+
.andWhere("mt.status", "=", MaintenanceTicketStatus.UPCOMING)
211+
.andWhere("mt.timeUnit", "=", MaintenanceTicketTimeUnit.CALENDAR)
212+
.andWhere("mt.dateCreated", "<=", endOfDay(new Date()).toISOString()) // TODO: normalize this to timezone
213+
.returning("mt.*");
208214
}
209215
export async function advanceUsageIntervalTickets(): Promise<MaintenanceTicketRow[]> {
210216
return await knex("MaintenanceTickets as m")
211-
.leftJoin("EquipmentInstances as e", "e.id", "=", "m.instanceID")
212-
.update({ status: MaintenanceTicketStatus.TODO, dateCreated: knex.fn.now() })
213-
.where("status", "=", MaintenanceTicketStatus.UPCOMING)
214-
.andWhere("timeUnit", "=", MaintenanceTicketTimeUnit.USAGE)
215-
.andWhere("m.hobbsTimeAtCreate", "<=", "e.hobbsTime")
216-
.returning("*");
217+
.update({ status: MaintenanceTicketStatus.TODO, dateCreated: knex.fn.now(), hobbsTimeAtCreate: knex.raw('e."hobbsTime"') })
218+
.updateFrom("EquipmentInstances as e").where("e.id", "=", knex.raw('m."instanceID"'))
219+
.andWhere("m.status", "=", MaintenanceTicketStatus.UPCOMING)
220+
.andWhere("m.timeUnit", "=", MaintenanceTicketTimeUnit.USAGE)
221+
.andWhere("m.hobbsTimeAtCreate", "<=", knex.raw('e."hobbsTime"'))
222+
.returning("m.*");
217223
}
218224

219225

server/src/graphql/resolvers/maintenanceTicketResolver.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as AuditLogRepo from "../../database/repositories/AuditLogs/AuditLogRep
77
import * as EquipmentRepo from "../../database/repositories/Equipment/EquipmentRepository.js";
88
import { notifyNewMaintenanceTicket } from "../../integrations/slack/slack.js"
99
import { Equipment } from "../../database/models/equipment/Equipment.js"
10+
import { GraphQLError } from "graphql"
1011

1112
const MaintenanceTicketResolver = {
1213
MaintenanceTicket: {
@@ -114,21 +115,28 @@ const MaintenanceTicketResolver = {
114115
intervalHours: number,
115116
imageUrl?: string,
116117
timeUnit: MaintenanceTicketTimeUnit,
117-
hobbsTimeAtCreate: number,
118118
},
119119
{ isManager }: ApolloContext // should perhaps be ifManagerFor
120-
) => isManager(async (_user) => (
120+
) => isManager(async (_user) => {
121+
const inst = await InstanceRepo.getInstanceByID(args.instanceID);
122+
if (inst == undefined){
123+
throw new GraphQLError("Could not find instance for maintenance ticket");
124+
}
125+
let hobbsTimeAtCreate = inst.hobbsTime;
126+
if (args.timeUnit == MaintenanceTicketTimeUnit.USAGE){
127+
hobbsTimeAtCreate += args.intervalHours * 60 * 60;
128+
}
121129
await MaintenanceTicketRepo.createIntervalMaintenanceTicket(
122130
args.severity,
123131
args.instanceID,
124132
args.description,
125133
args.startDate,
126-
args.hobbsTimeAtCreate,
134+
hobbsTimeAtCreate,
127135
args.timeUnit,
128136
args.intervalHours,
129137
args.imageUrl
130138
)
131-
)),
139+
}),
132140

133141
modifyMaintenanceTicketStatus: async (
134142
_parent: any,

server/src/graphql/schemas/maintenanceTicketSchema.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ export const MaintenanceTicketTypeDefs = gql`
8585
intervalHours: Int!,
8686
imageUrl: String,
8787
timeUnit: String!,
88-
hobbsTimeAtCreate: Int!
8988
): MaintenanceTicket
9089
9190
modifyMaintenanceTicketStatus(id: Int!, status: MaintenanceTicketStatus!): Int

server/src/periodicActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function advanceTimeTickets(): Promise<void> {
7272

7373
const result = [...calendarResult, ...usageResult]
7474
if (result.length > 0) {
75-
createUnassocaitedAuditLog(`Advanced ${result.length} time-based maintenance tickets`, "server");
75+
createUnassocaitedAuditLog(`Advanced ${calendarResult.length} calendar-time and ${usageResult.length} usage-time maintenance tickets`, "server");
7676
}
7777

7878
result.forEach((ticket) => {

0 commit comments

Comments
 (0)