From 03015760374212c5dfb5a2522aee612e6ea71a0d Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Fri, 15 Nov 2024 19:59:34 +0000 Subject: [PATCH 1/7] patch: issue deletion --- apps/api/src/controllers/ticket.ts | 105 +++++++++++++++++- apps/api/src/main.ts | 40 ++++--- .../client/components/TicketDetails/index.tsx | 19 ++-- apps/client/pages/issues/closed.tsx | 77 +++++++------ apps/client/pages/issues/index.tsx | 81 ++++++++------ apps/client/pages/issues/open.tsx | 81 ++++++++------ apps/client/pages/submit.tsx | 2 +- 7 files changed, 280 insertions(+), 125 deletions(-) diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index 240548f80..f6ce5dfc1 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -25,7 +25,6 @@ const validateEmail = (email: string) => { }; export function ticketRoutes(fastify: FastifyInstance) { - // Create a new ticket - public endpoint, no preHandler needed fastify.post( "/api/v1/ticket/create", { @@ -133,6 +132,110 @@ export function ticketRoutes(fastify: FastifyInstance) { } ); + fastify.post( + "/api/v1/ticket/public/create", + async (request: FastifyRequest, reply: FastifyReply) => { + const { + name, + company, + detail, + title, + priority, + email, + engineer, + type, + createdBy, + }: any = request.body; + + const ticket: any = await prisma.ticket.create({ + data: { + name, + title, + detail: JSON.stringify(detail), + priority: priority ? priority : "low", + email, + type: type ? type.toLowerCase() : "support", + createdBy: createdBy + ? { + id: createdBy.id, + name: createdBy.name, + role: createdBy.role, + email: createdBy.email, + } + : undefined, + client: + company !== undefined + ? { + connect: { id: company.id || company }, + } + : undefined, + fromImap: false, + assignedTo: + engineer && engineer.name !== "Unassigned" + ? { + connect: { id: engineer.id }, + } + : undefined, + isComplete: Boolean(false), + }, + }); + + if (!email && !validateEmail(email)) { + await sendTicketCreate(ticket); + } + + if (engineer && engineer.name !== "Unassigned") { + const assgined = await prisma.user.findUnique({ + where: { + id: ticket.userId, + }, + }); + + await sendAssignedEmail(assgined!.email); + + await assignedNotification(engineer.id, ticket); + } + + const webhook = await prisma.webhooks.findMany({ + where: { + type: "ticket_created", + }, + }); + + for (let i = 0; i < webhook.length; i++) { + if (webhook[i].active === true) { + const message = { + event: "ticket_created", + id: ticket.id, + title: ticket.title, + priority: ticket.priority, + email: ticket.email, + name: ticket.name, + type: ticket.type, + createdBy: ticket.createdBy, + assignedTo: ticket.assignedTo, + client: ticket.client, + }; + + await sendWebhookNotification(webhook[i], message); + } + } + + const hog = track(); + + hog.capture({ + event: "ticket_created", + distinctId: ticket.id, + }); + + reply.status(200).send({ + message: "Ticket created correctly", + success: true, + id: ticket.id, + }); + } + ); + // Get a ticket by id - requires auth fastify.get( "/api/v1/ticket/:id", diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index b057ccc2b..22e14fc0b 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -36,22 +36,26 @@ server.register(multer.contentParser); registerRoutes(server); -server.get("/", { - schema: { - tags: ['health'], // This groups the endpoint under a category - description: 'Health check endpoint', - response: { - 200: { - type: 'object', - properties: { - healthy: { type: 'boolean' } - } - } - } +server.get( + "/", + { + schema: { + tags: ["health"], // This groups the endpoint under a category + description: "Health check endpoint", + response: { + 200: { + type: "object", + properties: { + healthy: { type: "boolean" }, + }, + }, + }, + }, + }, + async function (request, response) { + response.send({ healthy: true }); } -}, async function (request, response) { - response.send({ healthy: true }); -}); +); // JWT authentication hook server.addHook("preHandler", async function (request: any, reply: any) { @@ -59,6 +63,12 @@ server.addHook("preHandler", async function (request: any, reply: any) { if (request.url === "/api/v1/auth/login" && request.method === "POST") { return true; } + if ( + request.url === "/api/v1/ticket/public/create" && + request.method === "POST" + ) { + return true; + } const bearer = request.headers.authorization!.split(" ")[1]; checkToken(bearer); } catch (err) { diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index 615931e27..3a7a46497 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -1319,17 +1319,14 @@ export default function Ticket() { - { - e.preventDefault(); - if (confirm("Are you sure you want to delete this ticket?")) { - deleteIssue(data.ticket.id); - } - }} - > - Delete Ticket - + {user.isAdmin && ( + deleteIssue(data.ticket.id)} + > + Delete Ticket + + )} )} diff --git a/apps/client/pages/issues/closed.tsx b/apps/client/pages/issues/closed.tsx index 38f410f43..0ffb4b4e2 100644 --- a/apps/client/pages/issues/closed.tsx +++ b/apps/client/pages/issues/closed.tsx @@ -84,29 +84,38 @@ export default function Tickets() { const [filterSelected, setFilterSelected] = useState(); const [selectedPriorities, setSelectedPriorities] = useState(() => { - const saved = localStorage.getItem('closed_selectedPriorities'); + const saved = localStorage.getItem("closed_selectedPriorities"); return saved ? JSON.parse(saved) : []; }); const [selectedStatuses, setSelectedStatuses] = useState(() => { - const saved = localStorage.getItem('closed_selectedStatuses'); + const saved = localStorage.getItem("closed_selectedStatuses"); return saved ? JSON.parse(saved) : []; }); const [selectedAssignees, setSelectedAssignees] = useState(() => { - const saved = localStorage.getItem('closed_selectedAssignees'); + const saved = localStorage.getItem("closed_selectedAssignees"); return saved ? JSON.parse(saved) : []; }); const [users, setUsers] = useState([]); useEffect(() => { - localStorage.setItem('closed_selectedPriorities', JSON.stringify(selectedPriorities)); + localStorage.setItem( + "closed_selectedPriorities", + JSON.stringify(selectedPriorities) + ); }, [selectedPriorities]); useEffect(() => { - localStorage.setItem('closed_selectedStatuses', JSON.stringify(selectedStatuses)); + localStorage.setItem( + "closed_selectedStatuses", + JSON.stringify(selectedStatuses) + ); }, [selectedStatuses]); useEffect(() => { - localStorage.setItem('closed_selectedAssignees', JSON.stringify(selectedAssignees)); + localStorage.setItem( + "closed_selectedAssignees", + JSON.stringify(selectedAssignees) + ); }, [selectedAssignees]); const handlePriorityToggle = (priority: string) => { @@ -715,30 +724,36 @@ export default function Tickets() { Share Link - - - { - e.preventDefault(); - if ( - confirm( - "Are you sure you want to delete this ticket?" - ) - ) { - fetch(`/api/v1/ticket/delete`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ id: ticket.id }), - }); - } - }} - > - Delete Ticket - + {user.isAdmin && ( + <> + + + { + e.preventDefault(); + if ( + confirm( + "Are you sure you want to delete this ticket?" + ) + ) { + fetch(`/api/v1/ticket/delete`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: ticket.id }), + }).then(() => { + refetch(); + }); + } + }} + > + Delete Ticket + + + )} ); @@ -749,7 +764,7 @@ export default function Tickets() { type="button" className="relative block w-[400px] rounded-lg border-2 border-dashed border-gray-300 p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" onClick={() => { - const event = new KeyboardEvent('keydown', { key: 'c' }); + const event = new KeyboardEvent("keydown", { key: "c" }); document.dispatchEvent(event); }} > diff --git a/apps/client/pages/issues/index.tsx b/apps/client/pages/issues/index.tsx index a487ba404..e84b4a3b2 100644 --- a/apps/client/pages/issues/index.tsx +++ b/apps/client/pages/issues/index.tsx @@ -83,25 +83,34 @@ export default function Tickets() { const normal = "bg-green-100 text-green-800"; const [selectedPriorities, setSelectedPriorities] = useState(() => { - const saved = localStorage.getItem('all_selectedPriorities'); + const saved = localStorage.getItem("all_selectedPriorities"); return saved ? JSON.parse(saved) : []; }); - + const [selectedStatuses, setSelectedStatuses] = useState(() => { - const saved = localStorage.getItem('all_selectedStatuses'); + const saved = localStorage.getItem("all_selectedStatuses"); return saved ? JSON.parse(saved) : []; }); - + const [selectedAssignees, setSelectedAssignees] = useState(() => { - const saved = localStorage.getItem('all_selectedAssignees'); + const saved = localStorage.getItem("all_selectedAssignees"); return saved ? JSON.parse(saved) : []; }); // Update local storage when filters change useEffect(() => { - localStorage.setItem('all_selectedPriorities', JSON.stringify(selectedPriorities)); - localStorage.setItem('all_selectedStatuses', JSON.stringify(selectedStatuses)); - localStorage.setItem('all_selectedAssignees', JSON.stringify(selectedAssignees)); + localStorage.setItem( + "all_selectedPriorities", + JSON.stringify(selectedPriorities) + ); + localStorage.setItem( + "all_selectedStatuses", + JSON.stringify(selectedStatuses) + ); + localStorage.setItem( + "all_selectedAssignees", + JSON.stringify(selectedAssignees) + ); }, [selectedPriorities, selectedStatuses, selectedAssignees]); const [users, setUsers] = useState([]); @@ -712,32 +721,36 @@ export default function Tickets() { Share Link - - - { - e.preventDefault(); - if ( - confirm( - "Are you sure you want to delete this ticket?" - ) - ) { - fetch(`/api/v1/ticket/delete`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ id: ticket.id }), - }).then(() => { - refetch(); - }); - } - }} - > - Delete Ticket - + {user.isAdmin && ( + <> + + + { + e.preventDefault(); + if ( + confirm( + "Are you sure you want to delete this ticket?" + ) + ) { + fetch(`/api/v1/ticket/delete`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: ticket.id }), + }).then(() => { + refetch(); + }); + } + }} + > + Delete Ticket + + + )} ); diff --git a/apps/client/pages/issues/open.tsx b/apps/client/pages/issues/open.tsx index 6604e0ab2..260776d25 100644 --- a/apps/client/pages/issues/open.tsx +++ b/apps/client/pages/issues/open.tsx @@ -84,38 +84,47 @@ export default function Tickets() { const [filterSelected, setFilterSelected] = useState(); const [selectedPriorities, setSelectedPriorities] = useState(() => { - const saved = localStorage.getItem('open_selectedPriorities'); + const saved = localStorage.getItem("open_selectedPriorities"); return saved ? JSON.parse(saved) : []; }); const [selectedStatuses, setSelectedStatuses] = useState(() => { - const saved = localStorage.getItem('open_selectedStatuses'); + const saved = localStorage.getItem("open_selectedStatuses"); return saved ? JSON.parse(saved) : []; }); const [selectedAssignees, setSelectedAssignees] = useState(() => { - const saved = localStorage.getItem('open_selectedAssignees'); + const saved = localStorage.getItem("open_selectedAssignees"); return saved ? JSON.parse(saved) : []; }); const [users, setUsers] = useState([]); useEffect(() => { - localStorage.setItem('open_selectedPriorities', JSON.stringify(selectedPriorities)); + localStorage.setItem( + "open_selectedPriorities", + JSON.stringify(selectedPriorities) + ); }, [selectedPriorities]); useEffect(() => { - localStorage.setItem('open_selectedStatuses', JSON.stringify(selectedStatuses)); + localStorage.setItem( + "open_selectedStatuses", + JSON.stringify(selectedStatuses) + ); }, [selectedStatuses]); useEffect(() => { - localStorage.setItem('open_selectedAssignees', JSON.stringify(selectedAssignees)); + localStorage.setItem( + "open_selectedAssignees", + JSON.stringify(selectedAssignees) + ); }, [selectedAssignees]); const clearAllFilters = () => { setSelectedPriorities([]); setSelectedStatuses([]); setSelectedAssignees([]); - localStorage.removeItem('open_selectedPriorities'); - localStorage.removeItem('open_selectedStatuses'); - localStorage.removeItem('open_selectedAssignees'); + localStorage.removeItem("open_selectedPriorities"); + localStorage.removeItem("open_selectedStatuses"); + localStorage.removeItem("open_selectedAssignees"); }; const handlePriorityToggle = (priority: string) => { @@ -722,28 +731,36 @@ export default function Tickets() { - { - e.preventDefault(); - if ( - confirm( - "Are you sure you want to delete this ticket?" - ) - ) { - fetch(`/api/v1/ticket/delete`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ id: ticket.id }), - }); - } - }} - > - Delete Ticket - + {user.isAdmin && ( + <> + + + { + e.preventDefault(); + if ( + confirm( + "Are you sure you want to delete this ticket?" + ) + ) { + fetch(`/api/v1/ticket/delete`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: ticket.id }), + }).then(() => { + refetch(); + }); + } + }} + > + Delete Ticket + + + )} ); @@ -754,7 +771,7 @@ export default function Tickets() { type="button" className="relative block w-[400px] rounded-lg border-2 border-dashed border-gray-300 p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" onClick={() => { - const event = new KeyboardEvent('keydown', { key: 'c' }); + const event = new KeyboardEvent("keydown", { key: "c" }); document.dispatchEvent(event); }} > diff --git a/apps/client/pages/submit.tsx b/apps/client/pages/submit.tsx index 2be97bfed..6cbbe0400 100644 --- a/apps/client/pages/submit.tsx +++ b/apps/client/pages/submit.tsx @@ -50,7 +50,7 @@ export default function ClientTicketNew() { async function submitTicket() { setIsLoading(true); - await fetch(`/api/v1/ticket/create`, { + await fetch(`/api/v1/ticket/public/create`, { method: "POST", headers: { "content-type": "application/json", From 8b08acc0256b3f46ac13676408d2b5ab6b283ac9 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Fri, 15 Nov 2024 23:42:09 +0000 Subject: [PATCH 2/7] feat: update client --- apps/api/src/controllers/ticket.ts | 34 +++- apps/client/components/Combo/index.tsx | 147 +++++++++++++++--- .../client/components/TicketDetails/index.tsx | 98 +++++++++++- 3 files changed, 255 insertions(+), 24 deletions(-) diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index f6ce5dfc1..811af8a21 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -480,7 +480,8 @@ export function ticketRoutes(fastify: FastifyInstance) { preHandler: requirePermission(["issue::update"]), }, async (request: FastifyRequest, reply: FastifyReply) => { - const { id, note, detail, title, priority, status }: any = request.body; + const { id, note, detail, title, priority, status, client }: any = + request.body; await prisma.ticket.update({ where: { id: id }, @@ -538,6 +539,37 @@ export function ticketRoutes(fastify: FastifyInstance) { } ); + // Transfer an Issue to another client + fastify.post( + "/api/v1/ticket/transfer/client", + { + preHandler: requirePermission(["issue::transfer"]), + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const { client, id }: any = request.body; + + if (client) { + await prisma.ticket.update({ + where: { id: id }, + data: { + clientId: client, + }, + }); + } else { + await prisma.ticket.update({ + where: { id: id }, + data: { + clientId: null, + }, + }); + } + + reply.send({ + success: true, + }); + } + ); + // Link a ticket to another ticket // fastify.post( diff --git a/apps/client/components/Combo/index.tsx b/apps/client/components/Combo/index.tsx index 738ac27e1..ceb0103bf 100644 --- a/apps/client/components/Combo/index.tsx +++ b/apps/client/components/Combo/index.tsx @@ -1,13 +1,5 @@ +import { Coffee, LucideIcon } from "lucide-react"; import * as React from "react"; -import { - ArrowUpCircle, - CheckCircle2, - Circle, - HelpCircle, - LucideIcon, - SignalLowIcon, - XCircle, -} from "lucide-react"; import { cn } from "@/shadcn/lib/utils"; import { Button } from "@/shadcn/ui/button"; @@ -34,6 +26,7 @@ export function UserCombo({ hideInitial, showIcon, disabled, + placeholder, }) { const [open, setOpen] = React.useState(false); const [selectedStatus, setSelectedStatus] = React.useState(null); @@ -83,10 +76,29 @@ export function UserCombo({ - {/* */} + No results found. + { + setSelectedStatus(null); + update(null); + setOpen(false); + }} + > + {/* */} + Unassign + {value.map((val) => ( - {/* */} {val.name} ))} @@ -212,3 +216,110 @@ export function IconCombo({ ); } + +export function ClientCombo({ + value, + update, + defaultName, + hideInitial, + showIcon, + disabled, +}) { + const [open, setOpen] = React.useState(false); + const [selectedStatus, setSelectedStatus] = React.useState(null); + + return ( +
+ + + + + + + + + No results found. + + { + setSelectedStatus(null); + update(null); + setOpen(false); + }} + > + {/* */} + Unassign + + {value.map((val) => ( + { + const user = value.find((k) => k.name === selected); + setSelectedStatus(user); + update(user); + setOpen(false); + }} + > + {/* */} + {val.name} + + ))} + + + + + +
+ ); +} diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index 3a7a46497..a37fc36e6 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -59,7 +59,7 @@ import { Unlock, } from "lucide-react"; import { useUser } from "../../store/session"; -import { IconCombo, UserCombo } from "../Combo"; +import { ClientCombo, IconCombo, UserCombo } from "../Combo"; const ticketStatusMap = [ { id: 1, value: "needs_support", name: "Needs Support", icon: LifeBuoy }, @@ -135,6 +135,7 @@ export default function Ticket() { const [labelEdit, setLabelEdit] = useState(false); const [users, setUsers] = useState(); + const [clients, setClients] = useState(); const [n, setN] = useState(); const [note, setNote] = useState(); @@ -148,6 +149,7 @@ export default function Ticket() { const [publicComment, setPublicComment] = useState(false); const [timeReason, setTimeReason] = useState(""); const [file, setFile] = useState(null); + const [assignedClient, setAssignedClient] = useState(); const history = useRouter(); @@ -259,7 +261,7 @@ export default function Ticket() { refetch(); } - async function deleteIssue(locked) { + async function deleteIssue() { await fetch(`/api/v1/ticket/delete`, { method: "POST", headers: { @@ -386,6 +388,31 @@ export default function Ticket() { } } + async function fetchClients() { + const res = await fetch(`/api/v1/clients/all`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to fetch clients", + }); + return; + } + + console.log(res); + + if (res.clients) { + setClients(res.clients); + } + } + async function transferTicket() { if (data && data.ticket && data.ticket.locked) return; if (n === undefined) return; @@ -397,7 +424,7 @@ export default function Ticket() { Authorization: `Bearer ${token}`, }, body: JSON.stringify({ - user: n.id, + user: n ? n.id : undefined, id, }), }).then((res) => res.json()); @@ -415,6 +442,35 @@ export default function Ticket() { refetch(); } + async function transferClient() { + if (data && data.ticket && data.ticket.locked) return; + if (assignedClient === undefined) return; + + const res = await fetch(`/api/v1/ticket/transfer/client`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + client: assignedClient ? assignedClient.id : undefined, + id, + }), + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to transfer client", + }); + return; + } + + setAssignedEdit(false); + refetch(); + } + const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { setFile(e.target.files[0]); @@ -461,12 +517,17 @@ export default function Ticket() { useEffect(() => { fetchUsers(); + fetchClients(); }, []); useEffect(() => { transferTicket(); }, [n]); + useEffect(() => { + transferClient(); + }, [assignedClient]); + const [debouncedValue] = useDebounce(issue, 500); const [debounceTitle] = useDebounce(title, 500); @@ -785,6 +846,9 @@ export default function Ticket() { : "" } disabled={data.ticket.locked} + placeholder="Assign User..." + hideInitial={false} + showIcon={true} /> )} @@ -796,15 +860,19 @@ export default function Ticket() { data.ticket.priority ? data.ticket.priority : "" } disabled={data.ticket.locked} + hideInitial={false} /> - @@ -1087,6 +1155,9 @@ export default function Ticket() { : "" } disabled={data.ticket.locked} + showIcon={true} + placeholder="Change User..." + hideInitial={false} /> )} @@ -1097,6 +1168,7 @@ export default function Ticket() { data.ticket.priority ? data.ticket.priority : "" } disabled={data.ticket.locked} + hideInitial={false} /> + {clients && ( + + )} + {/*
@@ -1322,7 +1410,7 @@ export default function Ticket() { {user.isAdmin && ( deleteIssue(data.ticket.id)} + onClick={(e) => deleteIssue()} > Delete Ticket From 60c31373277c8d4b4279421ccc4a54029e0f5ad8 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Fri, 15 Nov 2024 23:46:31 +0000 Subject: [PATCH 3/7] chore: admin can delete any comment (#413) --- .../client/components/TicketDetails/index.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index a37fc36e6..a15f30ac7 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -1009,15 +1009,16 @@ export default function Ticket() { {moment(comment.createdAt).format("LLL")} - {comment.user && - comment.userId === user.id && ( - { - deleteComment(comment.id); - }} - /> - )} + {(user.isAdmin || + (comment.user && + comment.userId === user.id)) && ( + { + deleteComment(comment.id); + }} + /> + )}
{comment.text} From 7f910d17fdec17dc0a14d3660b33733aae724928 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Sat, 16 Nov 2024 01:47:09 +0000 Subject: [PATCH 4/7] chore: add on hold status (#415) * feat: follow an issue (#414) * patch: issue deletion * feat: update client * feat: follow an issue * feat: notifications when following * feat: see who is subscribed to this issue * patch: on hold * patch: migratiom * patch: fix notififaction --- apps/api/src/controllers/ticket.ts | 237 +++++++++++++++++- .../src/lib/notifications/issue/assigned.ts | 40 ++- .../src/lib/notifications/issue/comment.ts | 37 ++- .../src/lib/notifications/issue/priority.ts | 33 +++ .../api/src/lib/notifications/issue/status.ts | 80 ++++++ .../20241115235800_following/migration.sql | 2 + .../20241116014522_hold/migration.sql | 2 + apps/api/src/prisma/schema.prisma | 2 + apps/client/@/shadcn/ui/switch.tsx | 27 ++ .../client/components/TicketDetails/index.tsx | 152 ++++++++--- apps/client/package.json | 1 + apps/client/pages/notifications.tsx | 6 +- yarn.lock | 3 +- 13 files changed, 562 insertions(+), 60 deletions(-) create mode 100644 apps/api/src/lib/notifications/issue/priority.ts create mode 100644 apps/api/src/prisma/migrations/20241115235800_following/migration.sql create mode 100644 apps/api/src/prisma/migrations/20241116014522_hold/migration.sql create mode 100644 apps/client/@/shadcn/ui/switch.tsx diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index 811af8a21..375b8fa12 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -11,6 +11,11 @@ import { sendTicketCreate } from "../lib/nodemailer/ticket/create"; import { sendTicketStatus } from "../lib/nodemailer/ticket/status"; import { assignedNotification } from "../lib/notifications/issue/assigned"; import { commentNotification } from "../lib/notifications/issue/comment"; +import { priorityNotification } from "../lib/notifications/issue/priority"; +import { + activeStatusNotification, + statusUpdateNotification, +} from "../lib/notifications/issue/status"; import { sendWebhookNotification } from "../lib/notifications/webhook"; import { requirePermission } from "../lib/roles"; import { checkSession } from "../lib/session"; @@ -43,6 +48,8 @@ export function ticketRoutes(fastify: FastifyInstance) { createdBy, }: any = request.body; + const user = await checkSession(request); + const ticket: any = await prisma.ticket.create({ data: { name, @@ -89,7 +96,109 @@ export function ticketRoutes(fastify: FastifyInstance) { await sendAssignedEmail(assgined!.email); - await assignedNotification(engineer.id, ticket); + await assignedNotification(engineer, ticket, user); + } + + const webhook = await prisma.webhooks.findMany({ + where: { + type: "ticket_created", + }, + }); + + for (let i = 0; i < webhook.length; i++) { + if (webhook[i].active === true) { + const message = { + event: "ticket_created", + id: ticket.id, + title: ticket.title, + priority: ticket.priority, + email: ticket.email, + name: ticket.name, + type: ticket.type, + createdBy: ticket.createdBy, + assignedTo: ticket.assignedTo, + client: ticket.client, + }; + + await sendWebhookNotification(webhook[i], message); + } + } + + const hog = track(); + + hog.capture({ + event: "ticket_created", + distinctId: ticket.id, + }); + + reply.status(200).send({ + message: "Ticket created correctly", + success: true, + id: ticket.id, + }); + } + ); + + fastify.post( + "/api/v1/ticket/public/create", + async (request: FastifyRequest, reply: FastifyReply) => { + const { + name, + company, + detail, + title, + priority, + email, + engineer, + type, + createdBy, + }: any = request.body; + + const ticket: any = await prisma.ticket.create({ + data: { + name, + title, + detail: JSON.stringify(detail), + priority: priority ? priority : "low", + email, + type: type ? type.toLowerCase() : "support", + createdBy: createdBy + ? { + id: createdBy.id, + name: createdBy.name, + role: createdBy.role, + email: createdBy.email, + } + : undefined, + client: + company !== undefined + ? { + connect: { id: company.id || company }, + } + : undefined, + fromImap: false, + assignedTo: + engineer && engineer.name !== "Unassigned" + ? { + connect: { id: engineer.id }, + } + : undefined, + isComplete: Boolean(false), + }, + }); + + if (!email && !validateEmail(email)) { + await sendTicketCreate(ticket); + } + + if (engineer && engineer.name !== "Unassigned") { + const assgined = await prisma.user.findUnique({ + where: { + id: ticket.userId, + }, + }); + + await sendAssignedEmail(assgined!.email); } const webhook = await prisma.webhooks.findMany({ @@ -193,7 +302,9 @@ export function ticketRoutes(fastify: FastifyInstance) { await sendAssignedEmail(assgined!.email); - await assignedNotification(engineer.id, ticket); + const user = await checkSession(request); + + await assignedNotification(engineer, ticket, user); } const webhook = await prisma.webhooks.findMany({ @@ -483,6 +594,12 @@ export function ticketRoutes(fastify: FastifyInstance) { const { id, note, detail, title, priority, status, client }: any = request.body; + const user = await checkSession(request); + + const issue = await prisma.ticket.findUnique({ + where: { id: id }, + }); + await prisma.ticket.update({ where: { id: id }, data: { @@ -494,6 +611,14 @@ export function ticketRoutes(fastify: FastifyInstance) { }, }); + if (priority && issue!.priority !== priority) { + await priorityNotification(issue, user, issue!.priority, priority); + } + + if (status && issue!.status !== status) { + await statusUpdateNotification(issue, user, status); + } + reply.send({ success: true, }); @@ -509,6 +634,8 @@ export function ticketRoutes(fastify: FastifyInstance) { async (request: FastifyRequest, reply: FastifyReply) => { const { user, id }: any = request.body; + const assigner = await checkSession(request); + if (user) { const assigned = await prisma.user.update({ where: { id: user }, @@ -523,7 +650,12 @@ export function ticketRoutes(fastify: FastifyInstance) { const { email } = assigned; + const ticket = await prisma.ticket.findUnique({ + where: { id: id }, + }); + await sendAssignedEmail(email); + await assignedNotification(assigned, ticket, assigner); } else { await prisma.ticket.update({ where: { id: id }, @@ -647,7 +779,7 @@ export function ticketRoutes(fastify: FastifyInstance) { sendComment(text, title, ticket!.id, email!); } - await commentNotification(user!.id, ticket, user!.name); + await commentNotification(ticket, user); const hog = track(); @@ -691,6 +823,8 @@ export function ticketRoutes(fastify: FastifyInstance) { async (request: FastifyRequest, reply: FastifyReply) => { const { status, id }: any = request.body; + const user = await checkSession(request); + const ticket: any = await prisma.ticket.update({ where: { id: id }, data: { @@ -698,6 +832,10 @@ export function ticketRoutes(fastify: FastifyInstance) { }, }); + await activeStatusNotification(ticket, user, status); + + await sendTicketStatus(ticket); + const webhook = await prisma.webhooks.findMany({ where: { type: "ticket_status_changed", @@ -738,8 +876,6 @@ export function ticketRoutes(fastify: FastifyInstance) { } } - sendTicketStatus(ticket); - reply.send({ success: true, }); @@ -975,4 +1111,95 @@ export function ticketRoutes(fastify: FastifyInstance) { }); } ); + + // Subscribe to a ticket + fastify.get( + "/api/v1/ticket/subscribe/:id", + { + preHandler: requirePermission(["issue::read"]), + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const { id }: any = request.params; + + const user = await checkSession(request); + + if (id) { + const ticket = await prisma.ticket.findUnique({ + where: { id: id }, + }); + + const following = ticket?.following as string[]; + + if (following.includes(user!.id)) { + reply.send({ + success: false, + message: "You are already following this issue", + }); + } + + if (ticket) { + await prisma.ticket.update({ + where: { id: id }, + data: { + following: [...following, user!.id], + }, + }); + } else { + reply.status(400).send({ + success: false, + message: "No ticket ID provided", + }); + } + + reply.send({ + success: true, + }); + } + } + ); + + // Unsubscribe from a ticket + fastify.get( + "/api/v1/ticket/unsubscribe/:id", + { + preHandler: requirePermission(["issue::read"]), + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const { id }: any = request.params; + const user = await checkSession(request); + + if (id) { + const ticket = await prisma.ticket.findUnique({ + where: { id: id }, + }); + + const following = ticket?.following as string[]; + + if (!following.includes(user!.id)) { + return reply.send({ + success: false, + message: "You are not following this issue", + }); + } + + if (ticket) { + await prisma.ticket.update({ + where: { id: id }, + data: { + following: following.filter((userId) => userId !== user!.id), + }, + }); + } else { + return reply.status(400).send({ + success: false, + message: "No ticket ID provided", + }); + } + + reply.send({ + success: true, + }); + } + } + ); } diff --git a/apps/api/src/lib/notifications/issue/assigned.ts b/apps/api/src/lib/notifications/issue/assigned.ts index 7f51c3662..f9947a910 100644 --- a/apps/api/src/lib/notifications/issue/assigned.ts +++ b/apps/api/src/lib/notifications/issue/assigned.ts @@ -1,18 +1,40 @@ import { prisma } from "../../../prisma"; +/** + * Creates assignment notifications for all ticket followers. + * + * @param {object} ticket - The ticket object + * @param {object} assignee - The user object being assigned + * @param {object} assigner - The user object doing the assigning + * @returns {Promise} + */ export async function assignedNotification( - userId: string, - ticket: any + assignee: any, + ticket: any, + assigner: any ) { try { - return await prisma.notifications.create({ - data: { - text: `Assigned Ticket #${ticket.Number}`, - userId, - ticketId: ticket.id, - }, + const text = `Ticket #${ticket.Number} was assigned to ${assignee.name} by ${assigner.name}`; + + // Get all followers of the ticket, ensuring the creator is not already a follower + const followers = [ + ...(ticket.following || []), + ...(ticket.following?.includes(ticket.createdBy.id) + ? [] + : [ticket.createdBy.id]), + ]; + + // Create notifications for all followers (except the assigner) + await prisma.notifications.createMany({ + data: followers + .filter((userId: string) => userId !== assigner.id) + .map((userId: string) => ({ + text, + userId, + ticketId: ticket.id, + })), }); } catch (error) { - console.error("Error creating notification:", error); + console.error("Error creating assignment notifications:", error); } } diff --git a/apps/api/src/lib/notifications/issue/comment.ts b/apps/api/src/lib/notifications/issue/comment.ts index 4266f9654..62ccc583f 100644 --- a/apps/api/src/lib/notifications/issue/comment.ts +++ b/apps/api/src/lib/notifications/issue/comment.ts @@ -1,24 +1,35 @@ import { prisma } from "../../../prisma"; /** - * Creates a new comment notification. - * - * @param {string} userId - The ID of the user to notify. + * Creates comment notifications for all ticket followers. + * * @param {object} ticket - The ticket object related to the comment. - * @param {string} comment - The content of the comment. + * @param {object} commenter - The user object who commented. * @returns {Promise} */ -export async function commentNotification(userId: string, ticket: any, user: string) { +export async function commentNotification(issue: any, commenter: any) { try { - const text = `New comment on #${ticket.Number} by ${user}`; - return await prisma.notifications.create({ - data: { - text, - userId, - ticketId: ticket.id, - }, + const text = `New comment on #${issue.Number} by ${commenter.name}`; + + // Get all followers of the ticket, ensuring the creator is not already a follower + const followers = [ + ...(issue.following || []), + ...(issue.following?.includes(issue.createdBy.id) + ? [] + : [issue.createdBy.id]), + ]; + + // Create notifications for all followers (except the commenter) + await prisma.notifications.createMany({ + data: followers + .filter((userId: string) => userId !== commenter.id) + .map((userId: string) => ({ + text, + userId, + ticketId: issue.id, + })), }); } catch (error) { - console.error("Error creating comment notification:", error); + console.error("Error creating comment notifications:", error); } } diff --git a/apps/api/src/lib/notifications/issue/priority.ts b/apps/api/src/lib/notifications/issue/priority.ts new file mode 100644 index 000000000..3034e45e4 --- /dev/null +++ b/apps/api/src/lib/notifications/issue/priority.ts @@ -0,0 +1,33 @@ +import { prisma } from "../../../prisma"; + +export async function priorityNotification( + issue: any, + updatedBy: any, + oldPriority: string, + newPriority: string +) { + try { + const text = `Priority changed on #${issue.Number} from ${oldPriority} to ${newPriority} by ${updatedBy.name}`; + + // Get all followers of the ticket, ensuring the creator is not already a follower + const followers = [ + ...(issue.following || []), + ...(issue.following?.includes(issue.createdBy.id) + ? [] + : [issue.createdBy.id]), + ]; + + // Create notifications for all followers (except the person who updated) + await prisma.notifications.createMany({ + data: followers + .filter((userId: string) => userId !== updatedBy.id) + .map((userId: string) => ({ + text, + userId, + ticketId: issue.id, + })), + }); + } catch (error) { + console.error("Error creating priority change notifications:", error); + } +} diff --git a/apps/api/src/lib/notifications/issue/status.ts b/apps/api/src/lib/notifications/issue/status.ts index 8b1378917..169f601d1 100644 --- a/apps/api/src/lib/notifications/issue/status.ts +++ b/apps/api/src/lib/notifications/issue/status.ts @@ -1 +1,81 @@ +import { prisma } from "../../../prisma"; +/** + * Creates status change notifications for all ticket followers. + * + * @param {object} ticket - The ticket object + * @param {object} updater - The username of the person who updated the status + * @param {string} newStatus - The new status of the ticket + * @returns {Promise} + */ +export async function activeStatusNotification( + ticket: any, + updater: any, + newStatus: string +) { + try { + const text = `#${ticket.Number} status changed to ${ + newStatus ? "Closed" : "Open" + } by ${updater.name}`; + + // Get all followers of the ticket, ensuring the creator is not already a follower + const followers = [ + ...(ticket.following || []), + ...(ticket.following?.includes(ticket.createdBy.id) + ? [] + : [ticket.createdBy.id]), + ]; + + // Create notifications for all followers (except the updater) + await prisma.notifications.createMany({ + data: followers + .filter((userId: string) => userId !== updater.id) + .map((userId: string) => ({ + text, + userId, + ticketId: ticket.id, + })), + }); + } catch (error) { + console.error("Error creating status change notifications:", error); + } +} + +/** + * Creates status change notifications for all ticket followers. + * + * @param {object} ticket - The ticket object + * @param {object} updater - The username of the person who updated the status + * @param {string} newStatus - The new status of the ticket + * @returns {Promise} + */ +export async function statusUpdateNotification( + ticket: any, + updater: any, + newStatus: string +) { + try { + const text = `#${ticket.Number} status changed to ${newStatus} by ${updater.name}`; + + // Get all followers of the ticket, ensuring the creator is not already a follower + const followers = [ + ...(ticket.following || []), + ...(ticket.following?.includes(ticket.createdBy.id) + ? [] + : [ticket.createdBy.id]), + ]; + + // Create notifications for all followers (except the updater) + await prisma.notifications.createMany({ + data: followers + .filter((userId: string) => userId !== updater.id) + .map((userId: string) => ({ + text, + userId, + ticketId: ticket.id, + })), + }); + } catch (error) { + console.error("Error creating status update notifications:", error); + } +} diff --git a/apps/api/src/prisma/migrations/20241115235800_following/migration.sql b/apps/api/src/prisma/migrations/20241115235800_following/migration.sql new file mode 100644 index 000000000..328e244a8 --- /dev/null +++ b/apps/api/src/prisma/migrations/20241115235800_following/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Ticket" ADD COLUMN "following" JSONB; diff --git a/apps/api/src/prisma/migrations/20241116014522_hold/migration.sql b/apps/api/src/prisma/migrations/20241116014522_hold/migration.sql new file mode 100644 index 000000000..720977a52 --- /dev/null +++ b/apps/api/src/prisma/migrations/20241116014522_hold/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "TicketStatus" ADD VALUE 'hold'; diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index 50a49029a..040ed6460 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -138,6 +138,7 @@ model Ticket { hidden Boolean @default(false) createdBy Json? locked Boolean @default(false) + following Json? TicketFile TicketFile[] Comment Comment[] @@ -416,6 +417,7 @@ enum Hook { } enum TicketStatus { + hold needs_support in_progress in_review diff --git a/apps/client/@/shadcn/ui/switch.tsx b/apps/client/@/shadcn/ui/switch.tsx new file mode 100644 index 000000000..d6a54c9c4 --- /dev/null +++ b/apps/client/@/shadcn/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/shadcn/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index a15f30ac7..feacaf4ad 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -17,9 +17,7 @@ import { } from "@/shadcn/ui/context-menu"; import { BlockNoteEditor, PartialBlock } from "@blocknote/core"; import { BlockNoteView } from "@blocknote/mantine"; -import { Switch } from "@headlessui/react"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; -import { Text, Tooltip } from "@radix-ui/themes"; import { getCookie } from "cookies-next"; import moment from "moment"; import useTranslation from "next-translate/useTranslation"; @@ -33,6 +31,7 @@ import { toast } from "@/shadcn/hooks/use-toast"; import { hasAccess } from "@/shadcn/lib/hasAccess"; import { cn } from "@/shadcn/lib/utils"; import { Avatar, AvatarFallback, AvatarImage } from "@/shadcn/ui/avatar"; +import { Button } from "@/shadcn/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -41,6 +40,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/shadcn/ui/dropdown-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shadcn/ui/popover"; +import { Switch } from "@/shadcn/ui/switch"; import { CheckIcon, CircleCheck, @@ -52,6 +53,7 @@ import { Loader, LoaderCircle, Lock, + PanelTopClose, SignalHigh, SignalLow, SignalMedium, @@ -62,6 +64,7 @@ import { useUser } from "../../store/session"; import { ClientCombo, IconCombo, UserCombo } from "../Combo"; const ticketStatusMap = [ + { id: 0, value: "hold", name: "Hold", icon: CircleDotDashed }, { id: 1, value: "needs_support", name: "Needs Support", icon: LifeBuoy }, { id: 2, value: "in_progress", name: "In Progress", icon: CircleDotDashed }, { id: 3, value: "in_review", name: "In Review", icon: Loader }, @@ -413,6 +416,39 @@ export default function Ticket() { } } + async function subscribe() { + if (data && data.ticket && data.ticket.locked) return; + + const isFollowing = data.ticket.following?.includes(user.id); + const action = isFollowing ? "unsubscribe" : "subscribe"; + + const res = await fetch(`/api/v1/ticket/${action}/${id}`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || `Failed to ${action} to issue`, + }); + return; + } + + toast({ + title: isFollowing ? "Unsubscribed" : "Subscribed", + description: isFollowing + ? "You will no longer receive updates" + : "You will now receive updates", + duration: 3000, + }); + + refetch(); + } + async function transferTicket() { if (data && data.ticket && data.ticket.locked) return; if (n === undefined) return; @@ -907,13 +943,79 @@ export default function Ticket() { className="border-t mt-4" >
-
+
Activity + +
+ + + {data.ticket.following.length > 0 && ( +
+ + + + + +
+ Followers + {data.ticket.following.map( + (follower: any) => { + const userMatch = users.find( + (user) => + user.id === follower && + user.id !== + data.ticket.assignedTo.id + ); + console.log(userMatch); + return userMatch ? ( +
+ {userMatch.name} +
+ ) : null; + } + )} + + {data.ticket.following.filter( + (follower: any) => + follower !== data.ticket.assignedTo.id + ).length === 0 && ( + + This issue has no followers + + )} +
+
+
+
+ )} +
@@ -1049,33 +1151,15 @@ export default function Ticket() { />
- +
- - Enable notifications - - - - - Public Reply - + onCheckedChange={setPublicComment} + /> + Public Reply
- +
{data.ticket.isComplete ? ( @@ -1161,7 +1245,6 @@ export default function Ticket() { hideInitial={false} /> )} - - + {clients && ( + + )} {clients && ( res.json()); await fetchUserProfile(); } + + return (
diff --git a/yarn.lock b/yarn.lock index fd2b14e78..d44ceb6dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3881,7 +3881,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-switch@npm:^1.0.3": +"@radix-ui/react-switch@npm:^1.0.3, @radix-ui/react-switch@npm:^1.1.1": version: 1.1.1 resolution: "@radix-ui/react-switch@npm:1.1.1" dependencies: @@ -7550,6 +7550,7 @@ __metadata: "@radix-ui/react-select": "npm:^2.0.0" "@radix-ui/react-separator": "npm:^1.1.0" "@radix-ui/react-slot": "npm:^1.1.0" + "@radix-ui/react-switch": "npm:^1.1.1" "@radix-ui/react-toast": "npm:^1.2.2" "@radix-ui/react-tooltip": "npm:^1.1.3" "@radix-ui/themes": "npm:^2.0.1" From 67fb83c7063b161a12991995bede55fa27301169 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Sat, 16 Nov 2024 01:54:40 +0000 Subject: [PATCH 5/7] patch: remove dupe code --- apps/api/src/controllers/ticket.ts | 102 ----------------------------- 1 file changed, 102 deletions(-) diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index 375b8fa12..27ec2e573 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -139,108 +139,6 @@ export function ticketRoutes(fastify: FastifyInstance) { } ); - fastify.post( - "/api/v1/ticket/public/create", - async (request: FastifyRequest, reply: FastifyReply) => { - const { - name, - company, - detail, - title, - priority, - email, - engineer, - type, - createdBy, - }: any = request.body; - - const ticket: any = await prisma.ticket.create({ - data: { - name, - title, - detail: JSON.stringify(detail), - priority: priority ? priority : "low", - email, - type: type ? type.toLowerCase() : "support", - createdBy: createdBy - ? { - id: createdBy.id, - name: createdBy.name, - role: createdBy.role, - email: createdBy.email, - } - : undefined, - client: - company !== undefined - ? { - connect: { id: company.id || company }, - } - : undefined, - fromImap: false, - assignedTo: - engineer && engineer.name !== "Unassigned" - ? { - connect: { id: engineer.id }, - } - : undefined, - isComplete: Boolean(false), - }, - }); - - if (!email && !validateEmail(email)) { - await sendTicketCreate(ticket); - } - - if (engineer && engineer.name !== "Unassigned") { - const assgined = await prisma.user.findUnique({ - where: { - id: ticket.userId, - }, - }); - - await sendAssignedEmail(assgined!.email); - } - - const webhook = await prisma.webhooks.findMany({ - where: { - type: "ticket_created", - }, - }); - - for (let i = 0; i < webhook.length; i++) { - if (webhook[i].active === true) { - const message = { - event: "ticket_created", - id: ticket.id, - title: ticket.title, - priority: ticket.priority, - email: ticket.email, - name: ticket.name, - type: ticket.type, - createdBy: ticket.createdBy, - assignedTo: ticket.assignedTo, - client: ticket.client, - }; - - await sendWebhookNotification(webhook[i], message); - } - } - - const hog = track(); - - hog.capture({ - event: "ticket_created", - distinctId: ticket.id, - }); - - reply.status(200).send({ - message: "Ticket created correctly", - success: true, - id: ticket.id, - }); - } - ); - fastify.post( "/api/v1/ticket/public/create", async (request: FastifyRequest, reply: FastifyReply) => { From 1630d8f6a01c30a07a778815166ede0107cb375d Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Sat, 16 Nov 2024 11:50:51 +0000 Subject: [PATCH 6/7] patch: fix null check --- apps/client/components/TicketDetails/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index feacaf4ad..0a892f835 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -976,7 +976,7 @@ export default function Ticket() { )} - {data.ticket.following.length > 0 && ( + {data.ticket.following && data.ticket.following.length > 0 && (
From 8a50dfb5fce037ff5cfef2b1d0022f2d8fed014b Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Sat, 16 Nov 2024 12:10:14 +0000 Subject: [PATCH 7/7] patch: remove code --- .../client/components/TicketDetails/index.tsx | 92 ++++++++----------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index 0a892f835..79bf9ce31 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -976,45 +976,46 @@ export default function Ticket() { )} - {data.ticket.following && data.ticket.following.length > 0 && ( -
- - - - - -
- Followers - {data.ticket.following.map( - (follower: any) => { - const userMatch = users.find( - (user) => - user.id === follower && - user.id !== - data.ticket.assignedTo.id - ); - console.log(userMatch); - return userMatch ? ( -
- {userMatch.name} -
- ) : null; - } - )} + {data.ticket.following && + data.ticket.following.length > 0 && ( +
+ + + + + +
+ Followers + {data.ticket.following.map( + (follower: any) => { + const userMatch = users.find( + (user) => + user.id === follower && + user.id !== + data.ticket.assignedTo.id + ); + console.log(userMatch); + return userMatch ? ( +
+ {userMatch.name} +
+ ) : null; + } + )} - {data.ticket.following.filter( - (follower: any) => - follower !== data.ticket.assignedTo.id - ).length === 0 && ( - - This issue has no followers - - )} -
-
-
-
- )} + {data.ticket.following.filter( + (follower: any) => + follower !== data.ticket.assignedTo.id + ).length === 0 && ( + + This issue has no followers + + )} +
+
+
+
+ )}
@@ -1276,21 +1277,6 @@ export default function Ticket() { /> )} - {clients && ( - - )} - {/*