From 5dd78d645b17b1e5cff95b8379dd2b326346f3e3 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Sat, 9 Nov 2024 01:02:56 +0000 Subject: [PATCH] fix: create first issue tbn --- apps/client/pages/issues/closed.tsx | 610 ++++++++++++++++++-- apps/client/pages/issues/index.tsx | 7 +- apps/client/pages/issues/open.tsx | 612 ++++++++++++++++++--- apps/client/pages/portal/index.tsx | 2 +- apps/client/pages/portal/issues/closed.tsx | 5 +- apps/client/pages/portal/issues/index.tsx | 8 +- apps/client/pages/portal/issues/open.tsx | 5 +- 7 files changed, 1120 insertions(+), 129 deletions(-) diff --git a/apps/client/pages/issues/closed.tsx b/apps/client/pages/issues/closed.tsx index a5036fd74..f9033c8e2 100644 --- a/apps/client/pages/issues/closed.tsx +++ b/apps/client/pages/issues/closed.tsx @@ -1,16 +1,25 @@ import useTranslation from "next-translate/useTranslation"; import { useRouter } from "next/router"; import Loader from "react-spinners/ClipLoader"; +import { useState, useMemo, useEffect } from "react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, + ContextMenuSub, + ContextMenuSubTrigger, + ContextMenuSubContent, +} from "@/shadcn/ui/context-menu"; import { getCookie } from "cookies-next"; import moment from "moment"; import Link from "next/link"; import { useQuery } from "react-query"; import { useUser } from "../../store/session"; -import { useState } from "react"; -import { ContextMenu } from "@radix-ui/themes"; import { Popover, PopoverContent, PopoverTrigger } from "@/shadcn/ui/popover"; -import { CheckIcon, PlusCircle } from "lucide-react"; +import { CheckIcon, Filter, X } from "lucide-react"; import { Button } from "@/shadcn/ui/button"; import { Command, @@ -22,10 +31,7 @@ import { CommandSeparator, } from "@/shadcn/ui/command"; import { cn } from "@/shadcn/lib/utils"; - -function classNames(...classes: any) { - return classes.filter(Boolean).join(" "); -} +import { toast } from "@/shadcn/hooks/use-toast"; async function getUserTickets(token: any) { const res = await fetch(`/api/v1/tickets/completed`, { @@ -36,16 +42,37 @@ async function getUserTickets(token: any) { return res.json(); } +const FilterBadge = ({ + text, + onRemove, +}: { + text: string; + onRemove: () => void; +}) => ( +
+ {text} + +
+); + export default function Tickets() { const router = useRouter(); const { t } = useTranslation("peppermint"); const token = getCookie("session"); - const { data, status, error } = useQuery( + const { data, status, error, refetch } = useQuery( "allusertickets", () => getUserTickets(token), { - refetchInterval: 1000, + refetchInterval: 5000, } ); @@ -55,7 +82,11 @@ export default function Tickets() { const low = "bg-blue-100 text-blue-800"; const normal = "bg-green-100 text-green-800"; + const [filterSelected, setFilterSelected] = useState(); const [selectedPriorities, setSelectedPriorities] = useState([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); + const [selectedAssignees, setSelectedAssignees] = useState([]); + const [users, setUsers] = useState([]); const handlePriorityToggle = (priority: string) => { setSelectedPriorities((prev) => @@ -65,14 +96,174 @@ export default function Tickets() { ); }; + const handleStatusToggle = (status: string) => { + setSelectedStatuses((prev) => + prev.includes(status) + ? prev.filter((s) => s !== status) + : [...prev, status] + ); + }; + + const handleAssigneeToggle = (assignee: string) => { + setSelectedAssignees((prev) => + prev.includes(assignee) + ? prev.filter((a) => a !== assignee) + : [...prev, assignee] + ); + }; + const filteredTickets = data - ? data.tickets.filter((ticket) => - selectedPriorities.length > 0 - ? selectedPriorities.includes(ticket.priority) - : true - ) + ? data.tickets.filter((ticket) => { + const priorityMatch = + selectedPriorities.length === 0 || + selectedPriorities.includes(ticket.priority); + const statusMatch = + selectedStatuses.length === 0 || + selectedStatuses.includes(ticket.isComplete ? "closed" : "open"); + const assigneeMatch = + selectedAssignees.length === 0 || + selectedAssignees.includes(ticket.assignedTo?.name || "Unassigned"); + + return priorityMatch && statusMatch && assigneeMatch; + }) : []; + type FilterType = "priority" | "status" | "assignee" | null; + const [activeFilter, setActiveFilter] = useState(null); + const [filterSearch, setFilterSearch] = useState(""); + + const filteredPriorities = useMemo(() => { + const priorities = ["low", "medium", "high"]; + return priorities.filter((priority) => + priority.toLowerCase().includes(filterSearch.toLowerCase()) + ); + }, [filterSearch]); + + const filteredStatuses = useMemo(() => { + const statuses = ["open", "closed"]; + return statuses.filter((status) => + status.toLowerCase().includes(filterSearch.toLowerCase()) + ); + }, [filterSearch]); + + const filteredAssignees = useMemo(() => { + const assignees = data?.tickets + .map((t) => t.assignedTo?.name || "Unassigned") + .filter((name, index, self) => self.indexOf(name) === index); + return assignees?.filter((assignee) => + assignee.toLowerCase().includes(filterSearch.toLowerCase()) + ); + }, [data?.tickets, filterSearch]); + + async function fetchUsers() { + await fetch(`/api/v1/users/all`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }) + .then((res) => res.json()) + .then((res) => { + if (res) { + setUsers(res.users); + } + }); + } + + async function updateTicketStatus(e: any, ticket: any) { + await fetch(`/api/v1/ticket/status/update`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: ticket.id, status: !ticket.isComplete }), + }) + .then((res) => res.json()) + .then(() => { + toast({ + title: ticket.isComplete ? "Issue re-opened" : "Issue closed", + description: "The status of the issue has been updated.", + duration: 3000, + }); + refetch(); + }); + } + + // Add these new functions + async function updateTicketAssignee(ticketId: string, user: any) { + try { + const response = await fetch(`/api/v1/ticket/transfer`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + user: user ? user.id : undefined, + id: ticketId, + }), + }); + + if (!response.ok) throw new Error("Failed to update assignee"); + + toast({ + title: "Assignee updated", + description: `Transferred issue successfully`, + duration: 3000, + }); + refetch(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to update assignee", + variant: "destructive", + duration: 3000, + }); + } + } + + async function updateTicketPriority(ticket: any, priority: string) { + try { + const response = await fetch(`/api/v1/ticket/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + id: ticket.id, + detail: ticket.detail, + note: ticket.note, + title: ticket.title, + priority: priority, + status: ticket.status, + }), + }).then((res) => res.json()); + + if (!response.success) throw new Error("Failed to update priority"); + + toast({ + title: "Priority updated", + description: `Ticket priority set to ${priority}`, + duration: 3000, + }); + refetch(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to update priority", + variant: "destructive", + duration: 3000, + }); + } + } + + useEffect(() => { + fetchUsers(); + }, []); + return (
{status === "loading" && ( @@ -84,63 +275,221 @@ export default function Tickets() { {status === "success" && (
-
-
- - You have {filteredTickets.length} closed ticket - {filteredTickets.length > 1 ? "'s" : ""} - +
+
- - - - - No results found. - - {["low", "medium", "high"].map((priority) => ( + + {!activeFilter ? ( + + + + No results found. + + setActiveFilter("priority")} + > + Priority + + setActiveFilter("status")} + > + Status + + setActiveFilter("assignee")} + > + Assigned To + + + + + ) : activeFilter === "priority" ? ( + + + + No priorities found. + + {filteredPriorities.map((priority) => ( + handlePriorityToggle(priority)} + > +
+ +
+ {priority} +
+ ))} +
+ + handlePriorityToggle(priority)} + onSelect={() => { + setActiveFilter(null); + setFilterSearch(""); + }} + className="justify-center text-center" > -
+ + + + ) : activeFilter === "status" ? ( + + + + No statuses found. + + {filteredStatuses.map((status) => ( + handleStatusToggle(status)} > - -
- {priority} +
+ +
+ {status} +
+ ))} +
+ + + { + setActiveFilter(null); + setFilterSearch(""); + }} + className="justify-center text-center" + > + Back to filters - ))} - - <> +
+
+
+ ) : activeFilter === "assignee" ? ( + + + + No assignees found. + + {filteredAssignees?.map((name) => ( + handleAssigneeToggle(name)} + > +
+ +
+ {name} +
+ ))} +
setSelectedPriorities([])} + onSelect={() => { + setActiveFilter(null); + setFilterSearch(""); + }} className="justify-center text-center" > - Clear filters + Back to filters - -
-
+ + + ) : null}
+ + {/* Display selected filters */} +
+ {selectedPriorities.map((priority) => ( + handlePriorityToggle(priority)} + /> + ))} + + {selectedStatuses.map((status) => ( + handleStatusToggle(status)} + /> + ))} + + {selectedAssignees.map((assignee) => ( + handleAssigneeToggle(assignee)} + /> + ))} + + {/* Clear all filters button - only show if there are filters */} + {(selectedPriorities.length > 0 || + selectedStatuses.length > 0 || + selectedAssignees.length > 0) && ( + + )} +
@@ -160,9 +509,9 @@ export default function Tickets() { } return ( - - - + + +
@@ -230,12 +579,147 @@ export default function Tickets() {
-
- - {/* Context menu items can be added here */} - -
- + + + + updateTicketStatus(e, ticket)} + > + {ticket.isComplete ? "Re-open Issue" : "Close Issue"} + + + + + Assign To + + + + + + updateTicketAssignee(ticket.id, undefined) + } + > +
+ +
+ Unassigned +
+ {users?.map((user) => ( + + updateTicketAssignee(ticket.id, user) + } + > +
+ +
+ {user.name} +
+ ))} +
+
+
+
+
+ + + + Change Priority + + + + + + {filteredPriorities.map((priority) => ( + + updateTicketPriority(ticket, priority) + } + > +
+ +
+ + {priority} + +
+ ))} +
+
+
+
+
+ + + + { + e.preventDefault(); + toast({ + title: "Link copied to clipboard", + description: + "You can now share the link with others.", + duration: 3000, + }); + navigator.clipboard.writeText( + `${window.location.origin}/issue/${ticket.id}` + ); + }} + > + 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 + +
+ ); }) ) : ( @@ -243,6 +727,10 @@ export default function Tickets() {
diff --git a/apps/client/pages/issues/index.tsx b/apps/client/pages/issues/index.tsx index a4872773a..177bc0751 100644 --- a/apps/client/pages/issues/index.tsx +++ b/apps/client/pages/issues/index.tsx @@ -42,7 +42,6 @@ async function getUserTickets(token: any) { return res.json(); } -// Add this new component for the filter badge const FilterBadge = ({ text, onRemove, @@ -728,6 +727,10 @@ export default function Tickets() {
diff --git a/apps/client/pages/issues/open.tsx b/apps/client/pages/issues/open.tsx index afe72aeab..3d84bd0a3 100644 --- a/apps/client/pages/issues/open.tsx +++ b/apps/client/pages/issues/open.tsx @@ -1,16 +1,25 @@ import useTranslation from "next-translate/useTranslation"; import { useRouter } from "next/router"; import Loader from "react-spinners/ClipLoader"; +import { useState, useMemo, useEffect } from "react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, + ContextMenuSub, + ContextMenuSubTrigger, + ContextMenuSubContent, +} from "@/shadcn/ui/context-menu"; import { getCookie } from "cookies-next"; import moment from "moment"; import Link from "next/link"; import { useQuery } from "react-query"; import { useUser } from "../../store/session"; -import { useState } from "react"; -import { ContextMenu } from "@radix-ui/themes"; import { Popover, PopoverContent, PopoverTrigger } from "@/shadcn/ui/popover"; -import { CheckIcon, PlusCircle } from "lucide-react"; +import { CheckIcon, Filter, X } from "lucide-react"; import { Button } from "@/shadcn/ui/button"; import { Command, @@ -22,13 +31,10 @@ import { CommandSeparator, } from "@/shadcn/ui/command"; import { cn } from "@/shadcn/lib/utils"; - -function classNames(...classes: any) { - return classes.filter(Boolean).join(" "); -} +import { toast } from "@/shadcn/hooks/use-toast"; async function getUserTickets(token: any) { - const res = await fetch(`/api/v1/tickets/user/open`, { + const res = await fetch(`/api/v1/tickets/open`, { headers: { Authorization: `Bearer ${token}`, }, @@ -36,16 +42,37 @@ async function getUserTickets(token: any) { return res.json(); } +const FilterBadge = ({ + text, + onRemove, +}: { + text: string; + onRemove: () => void; +}) => ( +
+ {text} + +
+); + export default function Tickets() { const router = useRouter(); const { t } = useTranslation("peppermint"); const token = getCookie("session"); - const { data, status, error } = useQuery( + const { data, status, error, refetch } = useQuery( "allusertickets", () => getUserTickets(token), { - refetchInterval: 1000, + refetchInterval: 5000, } ); @@ -55,7 +82,11 @@ export default function Tickets() { const low = "bg-blue-100 text-blue-800"; const normal = "bg-green-100 text-green-800"; + const [filterSelected, setFilterSelected] = useState(); const [selectedPriorities, setSelectedPriorities] = useState([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); + const [selectedAssignees, setSelectedAssignees] = useState([]); + const [users, setUsers] = useState([]); const handlePriorityToggle = (priority: string) => { setSelectedPriorities((prev) => @@ -65,14 +96,174 @@ export default function Tickets() { ); }; + const handleStatusToggle = (status: string) => { + setSelectedStatuses((prev) => + prev.includes(status) + ? prev.filter((s) => s !== status) + : [...prev, status] + ); + }; + + const handleAssigneeToggle = (assignee: string) => { + setSelectedAssignees((prev) => + prev.includes(assignee) + ? prev.filter((a) => a !== assignee) + : [...prev, assignee] + ); + }; + const filteredTickets = data - ? data.tickets.filter((ticket) => - selectedPriorities.length > 0 - ? selectedPriorities.includes(ticket.priority) - : true - ) + ? data.tickets.filter((ticket) => { + const priorityMatch = + selectedPriorities.length === 0 || + selectedPriorities.includes(ticket.priority); + const statusMatch = + selectedStatuses.length === 0 || + selectedStatuses.includes(ticket.isComplete ? "closed" : "open"); + const assigneeMatch = + selectedAssignees.length === 0 || + selectedAssignees.includes(ticket.assignedTo?.name || "Unassigned"); + + return priorityMatch && statusMatch && assigneeMatch; + }) : []; + type FilterType = "priority" | "status" | "assignee" | null; + const [activeFilter, setActiveFilter] = useState(null); + const [filterSearch, setFilterSearch] = useState(""); + + const filteredPriorities = useMemo(() => { + const priorities = ["low", "medium", "high"]; + return priorities.filter((priority) => + priority.toLowerCase().includes(filterSearch.toLowerCase()) + ); + }, [filterSearch]); + + const filteredStatuses = useMemo(() => { + const statuses = ["open", "closed"]; + return statuses.filter((status) => + status.toLowerCase().includes(filterSearch.toLowerCase()) + ); + }, [filterSearch]); + + const filteredAssignees = useMemo(() => { + const assignees = data?.tickets + .map((t) => t.assignedTo?.name || "Unassigned") + .filter((name, index, self) => self.indexOf(name) === index); + return assignees?.filter((assignee) => + assignee.toLowerCase().includes(filterSearch.toLowerCase()) + ); + }, [data?.tickets, filterSearch]); + + async function fetchUsers() { + await fetch(`/api/v1/users/all`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }) + .then((res) => res.json()) + .then((res) => { + if (res) { + setUsers(res.users); + } + }); + } + + async function updateTicketStatus(e: any, ticket: any) { + await fetch(`/api/v1/ticket/status/update`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: ticket.id, status: !ticket.isComplete }), + }) + .then((res) => res.json()) + .then(() => { + toast({ + title: ticket.isComplete ? "Issue re-opened" : "Issue closed", + description: "The status of the issue has been updated.", + duration: 3000, + }); + refetch(); + }); + } + + // Add these new functions + async function updateTicketAssignee(ticketId: string, user: any) { + try { + const response = await fetch(`/api/v1/ticket/transfer`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + user: user ? user.id : undefined, + id: ticketId, + }), + }); + + if (!response.ok) throw new Error("Failed to update assignee"); + + toast({ + title: "Assignee updated", + description: `Transferred issue successfully`, + duration: 3000, + }); + refetch(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to update assignee", + variant: "destructive", + duration: 3000, + }); + } + } + + async function updateTicketPriority(ticket: any, priority: string) { + try { + const response = await fetch(`/api/v1/ticket/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + id: ticket.id, + detail: ticket.detail, + note: ticket.note, + title: ticket.title, + priority: priority, + status: ticket.status, + }), + }).then((res) => res.json()); + + if (!response.success) throw new Error("Failed to update priority"); + + toast({ + title: "Priority updated", + description: `Ticket priority set to ${priority}`, + duration: 3000, + }); + refetch(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to update priority", + variant: "destructive", + duration: 3000, + }); + } + } + + useEffect(() => { + fetchUsers(); + }, []); + return (
{status === "loading" && ( @@ -84,63 +275,221 @@ export default function Tickets() { {status === "success" && (
-
-
- - You have {filteredTickets.length} open ticket - {filteredTickets.length > 1 ? "'s" : ""} - +
+
- - - - - No results found. - - {["low", "medium", "high"].map((priority) => ( + + {!activeFilter ? ( + + + + No results found. + + setActiveFilter("priority")} + > + Priority + + setActiveFilter("status")} + > + Status + + setActiveFilter("assignee")} + > + Assigned To + + + + + ) : activeFilter === "priority" ? ( + + + + No priorities found. + + {filteredPriorities.map((priority) => ( + handlePriorityToggle(priority)} + > +
+ +
+ {priority} +
+ ))} +
+ + handlePriorityToggle(priority)} + onSelect={() => { + setActiveFilter(null); + setFilterSearch(""); + }} + className="justify-center text-center" > -
+ + + + ) : activeFilter === "status" ? ( + + + + No statuses found. + + {filteredStatuses.map((status) => ( + handleStatusToggle(status)} > - -
- {priority} +
+ +
+ {status} +
+ ))} +
+ + + { + setActiveFilter(null); + setFilterSearch(""); + }} + className="justify-center text-center" + > + Back to filters - ))} - - <> +
+
+
+ ) : activeFilter === "assignee" ? ( + + + + No assignees found. + + {filteredAssignees?.map((name) => ( + handleAssigneeToggle(name)} + > +
+ +
+ {name} +
+ ))} +
setSelectedPriorities([])} + onSelect={() => { + setActiveFilter(null); + setFilterSearch(""); + }} className="justify-center text-center" > - Clear filters + Back to filters - -
-
+ + + ) : null}
+ + {/* Display selected filters */} +
+ {selectedPriorities.map((priority) => ( + handlePriorityToggle(priority)} + /> + ))} + + {selectedStatuses.map((status) => ( + handleStatusToggle(status)} + /> + ))} + + {selectedAssignees.map((assignee) => ( + handleAssigneeToggle(assignee)} + /> + ))} + + {/* Clear all filters button - only show if there are filters */} + {(selectedPriorities.length > 0 || + selectedStatuses.length > 0 || + selectedAssignees.length > 0) && ( + + )} +
@@ -160,9 +509,9 @@ export default function Tickets() { } return ( - - - + + +
@@ -230,12 +579,147 @@ export default function Tickets() {
-
- - {/* Context menu items can be added here */} - -
- + + + + updateTicketStatus(e, ticket)} + > + {ticket.isComplete ? "Re-open Issue" : "Close Issue"} + + + + + Assign To + + + + + + updateTicketAssignee(ticket.id, undefined) + } + > +
+ +
+ Unassigned +
+ {users?.map((user) => ( + + updateTicketAssignee(ticket.id, user) + } + > +
+ +
+ {user.name} +
+ ))} +
+
+
+
+
+ + + + Change Priority + + + + + + {filteredPriorities.map((priority) => ( + + updateTicketPriority(ticket, priority) + } + > +
+ +
+ + {priority} + +
+ ))} +
+
+
+
+
+ + + + { + e.preventDefault(); + toast({ + title: "Link copied to clipboard", + description: + "You can now share the link with others.", + duration: 3000, + }); + navigator.clipboard.writeText( + `${window.location.origin}/issue/${ticket.id}` + ); + }} + > + 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 + +
+ ); }) ) : ( @@ -243,6 +727,10 @@ export default function Tickets() {
diff --git a/apps/client/pages/portal/index.tsx b/apps/client/pages/portal/index.tsx index 2ee50564a..4cedf72cd 100644 --- a/apps/client/pages/portal/index.tsx +++ b/apps/client/pages/portal/index.tsx @@ -71,7 +71,7 @@ export default function Home() { /> - Create your first ticket + Create your first issue diff --git a/apps/client/pages/portal/issues/closed.tsx b/apps/client/pages/portal/issues/closed.tsx index c33071b3a..46f530c33 100644 --- a/apps/client/pages/portal/issues/closed.tsx +++ b/apps/client/pages/portal/issues/closed.tsx @@ -104,6 +104,9 @@ export default function Tickets() {
diff --git a/apps/client/pages/portal/issues/index.tsx b/apps/client/pages/portal/issues/index.tsx index 48edcc15b..fbcb91495 100644 --- a/apps/client/pages/portal/issues/index.tsx +++ b/apps/client/pages/portal/issues/index.tsx @@ -5,6 +5,7 @@ import { getCookie } from "cookies-next"; import moment from "moment"; import Link from "next/link"; import { useQuery } from "react-query"; +import { useRouter } from "next/router"; async function getUserTickets(token: any) { const res = await fetch(`/api/v1/tickets/user/external`, { @@ -18,6 +19,8 @@ async function getUserTickets(token: any) { export default function Tickets() { const { t } = useTranslation("peppermint"); + const router = useRouter(); + const token = getCookie("session"); const { data, status, error } = useQuery("allusertickets", () => getUserTickets(token) @@ -124,6 +127,9 @@ export default function Tickets() {
diff --git a/apps/client/pages/portal/issues/open.tsx b/apps/client/pages/portal/issues/open.tsx index ace097df2..22c3df570 100644 --- a/apps/client/pages/portal/issues/open.tsx +++ b/apps/client/pages/portal/issues/open.tsx @@ -104,6 +104,9 @@ export default function Tickets() {