From 67c2c8fdccdba0a83c89cbe69b17da67b9d452d6 Mon Sep 17 00:00:00 2001 From: jakeaturner Date: Sat, 27 Jan 2024 16:05:13 -0800 Subject: [PATCH] fix(SupportCenter): improve ticket view layout --- client/src/components/kb/DefaultLayout.tsx | 16 ++- .../components/support/AssignTicketModal.tsx | 3 + .../components/support/CreateTicketFlow.tsx | 57 ++++---- client/src/components/support/Navbar.tsx | 33 +++-- .../src/components/support/StaffDashboard.tsx | 38 ++++- .../src/components/support/TicketDetails.tsx | 47 +++++++ client/src/components/support/TicketFeed.tsx | 77 ++++++++++ .../components/support/TicketMessaging.tsx | 83 +++++++---- .../src/screens/conductor/support/Ticket.tsx | 70 +++------- .../src/screens/conductor/support/index.tsx | 2 +- client/src/types/index.ts | 2 + client/src/types/support.ts | 12 +- server/api.js | 17 +-- server/api/mail.js | 20 ++- server/api/support.ts | 132 +++++++++++++----- server/api/validators/support.ts | 6 +- server/conductor-errors.ts | 1 + server/models/supporticket.ts | 46 +++++- server/models/supporticketmessage.ts | 85 ++++++----- server/models/user.ts | 8 ++ 20 files changed, 552 insertions(+), 203 deletions(-) create mode 100644 client/src/components/support/TicketDetails.tsx create mode 100644 client/src/components/support/TicketFeed.tsx diff --git a/client/src/components/kb/DefaultLayout.tsx b/client/src/components/kb/DefaultLayout.tsx index 931b23db..a2406469 100644 --- a/client/src/components/kb/DefaultLayout.tsx +++ b/client/src/components/kb/DefaultLayout.tsx @@ -1,5 +1,17 @@ -const DefaultLayout = ({ children }: { children: JSX.Element[] | JSX.Element }) => { - return
{children}
; +const DefaultLayout = ({ + children, + altBackground, +}: { + children: JSX.Element[] | JSX.Element; + altBackground?: boolean; +}) => { + return ( +
+ {children} +
+ ); }; export default DefaultLayout; diff --git a/client/src/components/support/AssignTicketModal.tsx b/client/src/components/support/AssignTicketModal.tsx index 591fb72b..b9e6cbdc 100644 --- a/client/src/components/support/AssignTicketModal.tsx +++ b/client/src/components/support/AssignTicketModal.tsx @@ -88,6 +88,9 @@ const AssignTicketModal: React.FC = ({ Assign Ticket to User(s)
e.preventDefault()}> +

+ Assigned users will be notified of new messages and updates on this ticket. +

({ diff --git a/client/src/components/support/CreateTicketFlow.tsx b/client/src/components/support/CreateTicketFlow.tsx index e018366c..eabcfca3 100644 --- a/client/src/components/support/CreateTicketFlow.tsx +++ b/client/src/components/support/CreateTicketFlow.tsx @@ -228,68 +228,71 @@ const CreateTicketFlow: React.FC = ({ isLoggedIn }) => { )}

Request Info

-
( ({ - key: app.id, - value: app.id, - text: app.name, - }))} + id="selectCategory" + options={SupportTicketCategoryOptions} {...field} onChange={(e, { value }) => { field.onChange(value); }} fluid selection - multiple search - placeholder="Select the application(s) related to your ticket" + placeholder="Select the category of your ticket" /> )} />
+
+ +
( ({ + key: app.id, + value: app.id, + text: app.name, + }))} {...field} onChange={(e, { value }) => { field.onChange(value); }} fluid selection + multiple search - placeholder="Select the category of your ticket" + placeholder="Select the applications and/or libraries related to your ticket" /> )} /> @@ -328,7 +331,7 @@ const CreateTicketFlow: React.FC = ({ isLoggedIn }) => { control={control} name="capturedURL" label="URL (if applicable)" - placeholder="Enter the URL of the page you're having trouble with" + placeholder="Enter the URL of the page this ticket is related to - this may help us resolve your issue faster" type="url" />
diff --git a/client/src/components/support/Navbar.tsx b/client/src/components/support/Navbar.tsx index 65531972..5b6739cc 100644 --- a/client/src/components/support/Navbar.tsx +++ b/client/src/components/support/Navbar.tsx @@ -114,17 +114,28 @@ const SupportCenterNavbar: React.FC<{}> = () => { Dashboard ) : ( - <> - // + <> + + + )} diff --git a/client/src/components/support/StaffDashboard.tsx b/client/src/components/support/StaffDashboard.tsx index a6ef919c..7000b66f 100644 --- a/client/src/components/support/StaffDashboard.tsx +++ b/client/src/components/support/StaffDashboard.tsx @@ -10,6 +10,7 @@ import { PaginationWithItemsSelect } from "../util/PaginationWithItemsSelect"; import { useTypedSelector } from "../../state/hooks"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import LoadingSpinner from "../LoadingSpinner"; +const AssignTicketModal = lazy(() => import("./AssignTicketModal")); const SupportCenterSettingsModal = lazy( () => import("./SupportCenterSettingsModal") ); @@ -26,6 +27,8 @@ const StaffDashboard = () => { const [metricOpen, setMetricOpen] = useState(0); const [metricAvgMins, setMetricAvgMins] = useState(0); const [metricWeek, setMetricWeek] = useState(0); + const [showAssignModal, setShowAssignModal] = useState(false); + const [selectedTicketId, setSelectedTicketId] = useState(""); const queryClient = useQueryClient(); @@ -94,6 +97,17 @@ const StaffDashboard = () => { window.open(`/support/ticket/${uuid}`, "_blank"); } + function openAssignModal(ticketId: string) { + setSelectedTicketId(ticketId); + setShowAssignModal(true); + } + + function onCloseAssignModal() { + setShowAssignModal(false); + setSelectedTicketId(""); + queryClient.invalidateQueries(["openTickets"]); + } + const DashboardMetric = ({ metric, title, @@ -124,7 +138,10 @@ const StaffDashboard = () => { )}
- + { />
-

Open Tickets

+

Open/In Progress Tickets

{ View + {ticket.status === "open" && ( + + )} ))} @@ -202,6 +229,13 @@ const StaffDashboard = () => { open={showSettingsModal} onClose={() => setShowSettingsModal(false)} /> + {selectedTicketId && ( + + )}
); }; diff --git a/client/src/components/support/TicketDetails.tsx b/client/src/components/support/TicketDetails.tsx new file mode 100644 index 00000000..714af1b4 --- /dev/null +++ b/client/src/components/support/TicketDetails.tsx @@ -0,0 +1,47 @@ +import { Label } from "semantic-ui-react"; +import { SupportTicket } from "../../types"; +import { format, parseISO } from "date-fns"; + +interface TicketDetailsProps { + ticket: SupportTicket; +} + +const TicketDetails: React.FC = ({ ticket }) => { + return ( +
+

+ Requester:{" "} + {ticket.user && ( + <> + + `${ticket.user.firstName} ${ticket.user.lastName} ($ + {ticket.user.email})` + + + + )} + {ticket.guest && + `${ticket.guest.firstName} ${ticket.guest.lastName} (${ticket.guest.email})`} +

+

+ Subject: {ticket?.title} +

+

+ Date Opened:{" "} + {format(parseISO(ticket.timeOpened), "MM/dd/yyyy hh:mm aa")} +

+ {ticket.status === "closed" && ( +

+ Date Closed:{" "} + {format(parseISO(ticket.timeClosed ?? ""), "MM/dd/yyyy hh:mm aa")} +

+ )} +

+ Description:{" "} + {ticket?.description} +

+
+ ); +}; + +export default TicketDetails; diff --git a/client/src/components/support/TicketFeed.tsx b/client/src/components/support/TicketFeed.tsx new file mode 100644 index 00000000..3186b0c2 --- /dev/null +++ b/client/src/components/support/TicketFeed.tsx @@ -0,0 +1,77 @@ +import { useState, useEffect } from "react"; +import { + Button, + Comment, + Feed, + FeedContent, + FeedDate, + FeedEvent, + FeedLabel, + FeedLike, + FeedMeta, + FeedSummary, + FeedUser, + Form, + Header, + Icon, + TextArea, +} from "semantic-ui-react"; +import { + SupportTicket, + SupportTicketFeedEntry, + SupportTicketMessage, +} from "../../types"; +import { format, parseISO } from "date-fns"; +import useGlobalError from "../error/ErrorHooks"; +import axios from "axios"; +import { useForm } from "react-hook-form"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +interface TicketFeedProps { + ticket: SupportTicket; +} + +const TicketFeed: React.FC = ({ ticket }) => { + const { handleGlobalError } = useGlobalError(); + + const getEntryTimestamp = (entry: SupportTicketFeedEntry) => { + return format(parseISO(entry.date), "MM/dd/yyyy hh:mm aa"); + } + + const TicketFeedEntry = ({ entry }: { entry: SupportTicketFeedEntry }) => { + return ( +
+
+ +
+
+
+

{entry.action}

+

{entry.blame} - {getEntryTimestamp(entry)}

+
+
+
+ ); + }; + + return ( +
+
+

Ticket Feed

+
+ {ticket.feed?.length === 0 && ( +

+ No history yet... +

+ )} + + {ticket.feed?.map((f) => ( + + ))} + +
+
+
+ ); +}; +export default TicketFeed; diff --git a/client/src/components/support/TicketMessaging.tsx b/client/src/components/support/TicketMessaging.tsx index daef99ab..b7d26cca 100644 --- a/client/src/components/support/TicketMessaging.tsx +++ b/client/src/components/support/TicketMessaging.tsx @@ -11,14 +11,17 @@ import { SupportTicket, SupportTicketMessage } from "../../types"; import { format, parseISO } from "date-fns"; import useGlobalError from "../error/ErrorHooks"; import axios from "axios"; -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import userEvent from "@testing-library/user-event"; +import { useTypedSelector } from "../../state/hooks"; interface TicketMessagingProps { id: string; } const TicketMessaging: React.FC = ({ id }) => { + const user = useTypedSelector((state) => state.user); const { handleGlobalError } = useGlobalError(); const queryClient = useQueryClient(); const { control, getValues, setValue, watch, trigger, reset } = @@ -33,13 +36,14 @@ const TicketMessaging: React.FC = ({ id }) => { queryFn: () => getMessages(), keepPreviousData: true, staleTime: 1000 * 60, // 1 minutes + refetchInterval: 1000 * 60, // 1 minutes refetchOnWindowFocus: true, }); async function getMessages(): Promise { try { if (!id) throw new Error("Invalid ticket ID"); - const res = await axios.get(`/support/ticket/${id}/msg/staff`); + const res = await axios.get(`/support/ticket/${id}/msg`); if (res.data.err) { throw new Error(res.data.errMsg); } @@ -62,11 +66,13 @@ const TicketMessaging: React.FC = ({ id }) => { async function sendMessage() { try { if (!id) throw new Error("Invalid ticket ID"); - if (!(await trigger())) return; + if (!getValues("message")) return; + console.log(getValues("message").length); - const res = await axios.post(`/support/ticket/${id}/msg/staff`, { + const res = await axios.post(`/support/ticket/${id}/msg`, { ...getValues(), }); + if (res.data.err) { throw new Error(res.data.errMsg); } @@ -75,7 +81,6 @@ const TicketMessaging: React.FC = ({ id }) => { } reset(); // clear form - getMessages(); } catch (err) { handleGlobalError(err); } @@ -88,8 +93,13 @@ const TicketMessaging: React.FC = ({ id }) => { }, }); + const getSenderIsSelf = (msg: SupportTicketMessage): boolean => { + if (msg.senderIsStaff && user && user.isSuperAdmin) return true; + return false; + }; + const TicketComment = (msg: SupportTicketMessage) => { - const senderIsSelf = true; + const senderIsSelf = getSenderIsSelf(msg); return (
= ({ id }) => { senderIsSelf ? "text-right mr-2" : "text-left ml-2" }`} > - {msg.sender} at{" "} - {format(parseISO(msg.timeSent), "MM/dd/yyyy hh:mm aa")} + {msg.sender + ? `${msg.sender.firstName} ${msg.sender.lastName}` + : msg.senderEmail}{" "} + at {format(parseISO(msg.timeSent), "MM/dd/yyyy hh:mm aa")}

); }; return ( -
+
-

Ticket Chat

-
+

Ticket Chat

+
{messages?.length === 0 && (

No messages yet... @@ -124,30 +136,53 @@ const TicketMessaging: React.FC = ({ id }) => { {messages?.map((msg) => ( ))} -

-

Send Message:

- -