diff --git a/apps/client/pages/issues/index.tsx b/apps/client/pages/issues/index.tsx index af5df548c..ee68af36e 100644 --- a/apps/client/pages/issues/index.tsx +++ b/apps/client/pages/issues/index.tsx @@ -1,7 +1,7 @@ import useTranslation from "next-translate/useTranslation"; import { useRouter } from "next/router"; import Loader from "react-spinners/ClipLoader"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { ContextMenu } from "@radix-ui/themes"; import { getCookie } from "cookies-next"; @@ -10,7 +10,7 @@ import Link from "next/link"; import { useQuery } from "react-query"; import { useUser } from "../../store/session"; import { Popover, PopoverContent, PopoverTrigger } from "@/shadcn/ui/popover"; -import { CheckIcon, PlusCircle } from "lucide-react"; +import { CheckIcon, Filter, PlusCircle, X } from "lucide-react"; import { Button } from "@/shadcn/ui/button"; import { Command, @@ -32,6 +32,28 @@ async function getUserTickets(token: any) { return res.json(); } +// Add this new component for the filter badge +const FilterBadge = ({ + text, + onRemove, +}: { + text: string; + onRemove: () => void; +}) => ( +
+ {text} + +
+); + export default function Tickets() { const router = useRouter(); const { t } = useTranslation("peppermint"); @@ -47,7 +69,10 @@ 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 handlePriorityToggle = (priority: string) => { setSelectedPriorities((prev) => @@ -57,14 +82,65 @@ 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]); + return (
{status === "loading" && ( @@ -76,60 +152,221 @@ export default function Tickets() { {status === "success" && (
-
-
- All Tickets +
+
- - - - - 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) && ( + + )} +