diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index a241b5f2e..0b9f11836 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -220,6 +220,41 @@ export function ticketRoutes(fastify: FastifyInstance) { } ); + // Get all tickets (admin) + fastify.get( + "/api/v1/tickets/all/admin", + async (request: FastifyRequest, reply: FastifyReply) => { + const bearer = request.headers.authorization!.split(" ")[1]; + const token = checkToken(bearer); + + if (token) { + const tickets = await prisma.ticket.findMany({ + orderBy: [ + { + createdAt: "desc", + }, + ], + include: { + client: { + select: { id: true, name: true, number: true }, + }, + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); + + reply.send({ + tickets: tickets, + sucess: true, + }); + } + } + ); + // Get all open tickets for a user fastify.get( "/api/v1/tickets/user/open", diff --git a/apps/client/components/TicketViews/admin.tsx b/apps/client/components/TicketViews/admin.tsx new file mode 100644 index 000000000..e59b3c734 --- /dev/null +++ b/apps/client/components/TicketViews/admin.tsx @@ -0,0 +1,377 @@ +import { getCookie } from "cookies-next"; +import moment from "moment"; +import { useRouter } from "next/router"; +import React, { useMemo } from "react"; +import { useQuery } from "react-query"; +import { + useFilters, + useGlobalFilter, + usePagination, + useTable, +} from "react-table"; +import TicketsMobileList from "../../components/TicketsMobileList"; + +const fetchALLTIckets = async () => { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/tickets/all/admin`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${getCookie("session")}`, + }, + } + ); + return res.json(); +}; + +function DefaultColumnFilter({ column: { filterValue, setFilter } }: any) { + return ( + { + setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely + }} + placeholder="Type to filter" + /> + ); +} +function Table({ columns, data }: any) { + const filterTypes = React.useMemo( + () => ({ + // Add a new fuzzyTextFilterFn filter type. + // fuzzyText: fuzzyTextFilterFn, + // Or, override the default text filter to use + // "startWith" + text: (rows: any, id: any, filterValue: any) => + rows.filter((row: any) => { + const rowValue = row.values[id]; + return rowValue !== undefined + ? String(rowValue) + .toLowerCase() + .startsWith(String(filterValue).toLowerCase()) + : true; + }), + }), + [] + ); + + const defaultColumn = React.useMemo( + () => ({ + // Let's set up our default Filter UI + Filter: DefaultColumnFilter, + }), + [] + ); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + //@ts-expect-error + page, + prepareRow, + //@ts-expect-error + canPreviousPage, + //@ts-expect-error + canNextPage, + //@ts-expect-error + pageCount, + //@ts-expect-error + gotoPage, + //@ts-expect-error + nextPage, + //@ts-expect-error + previousPage, + //@ts-expect-error + setPageSize, + //@ts-expect-error + state: { pageIndex, pageSize }, + } = useTable( + { + columns, + data, + //@ts-expect-error + defaultColumn, // Be sure to pass the defaultColumn option + filterTypes, + initialState: { + //@ts-expect-error + pageIndex: 0, + }, + }, + useFilters, // useFilters! + useGlobalFilter, + usePagination + ); + + return ( +
+
+
+ + + {headerGroups.map((headerGroup: any) => ( + header.id)} + > + {headerGroup.headers.map((column: any) => + column.hideHeader === false ? null : ( + + ) + )} + + ))} + + + {page.map((row: any, i: any) => { + prepareRow(row); + return ( + + {row.cells.map((cell: any) => ( + + ))} + + ); + })} + +
+ {column.render("Header")} + {/* Render the columns filter UI */} +
+ {column.canFilter ? column.render("Filter") : null} +
+
+ {cell.render("Cell")} +
+ + {data.length > 10 && ( + + )} +
+
+
+ ); +} + +export default function AdminTicketLayout() { + const { data, status, refetch } = useQuery( + "fetchallTickets", + fetchALLTIckets + ); + + const router = useRouter(); + + const high = "bg-red-100 text-red-800"; + const low = "bg-blue-100 text-blue-800"; + const normal = "bg-green-100 text-green-800"; + + const columns = useMemo( + () => [ + { + Header: "Type", + accessor: "type", + id: "type", + width: 50, + }, + { + Header: "Summary", + accessor: "title", + id: "summary", + Cell: ({ row, value }: any) => { + return ( + <> + {value} + + ); + }, + }, + { + Header: "Assignee", + accessor: "assignedTo.name", + id: "assignee", + Cell: ({ row, value }: any) => { + return ( + <> + {value ? value : "n/a"} + + ); + }, + }, + { + Header: "Client", + accessor: "client.name", + id: "client", + Cell: ({ row, value }: any) => { + return ( + <> + {value ? value : "n/a"} + + ); + }, + }, + { + Header: "Priority", + accessor: "priority", + id: "priority", + Cell: ({ row, value }) => { + let p = value; + let badge; + + if (p === "Low") { + badge = low; + } + if (p === "Normal") { + badge = normal; + } + if (p === "High") { + badge = high; + } + + return ( + <> + + {value} + + + ); + }, + }, + { + Header: "Status", + accessor: "status", + id: "status", + Cell: ({ row, value }) => { + let p = value; + let badge; + + return ( + <> + + {value === "needs_support" && Needs Support} + {value === "in_progress" && In Progress} + {value === "in_review" && In Review} + {value === "done" && Done} + + + ); + }, + }, + { + Header: "Created", + accessor: "createdAt", + id: "created", + Cell: ({ row, value }) => { + const now = moment(value).format("DD/MM/YYYY"); + return ( + <> + {now} + + ); + }, + }, + ], + [] + ); + + return ( + <> + {status === "success" && ( + <> + {data.tickets && data.tickets.length > 0 && ( + <> +
+ + + +
+ +
+ + )} + + {data.tickets.length === 0 && ( + <> +
+ + + + +

+ You currently don't have any assigned tickets. :) +

+
+ + )} + + )} + + ); +} diff --git a/apps/client/pages/admin/tickets.tsx b/apps/client/pages/admin/tickets.tsx new file mode 100644 index 000000000..16a06ac9f --- /dev/null +++ b/apps/client/pages/admin/tickets.tsx @@ -0,0 +1,410 @@ +import { getCookie } from "cookies-next"; +import moment from "moment"; +import { useRouter } from "next/router"; +import React, { useMemo } from "react"; +import { useQuery } from "react-query"; +import { + useFilters, + useGlobalFilter, + usePagination, + useTable, +} from "react-table"; +import TicketsMobileList from "../../components/TicketsMobileList"; + +const fetchALLTIckets = async () => { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/tickets/all/admin`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${getCookie("session")}`, + }, + } + ); + return res.json(); +}; + +function DefaultColumnFilter({ column: { filterValue, setFilter } }: any) { + return ( + { + setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely + }} + placeholder="Type to filter" + /> + ); +} +function Table({ columns, data }: any) { + const filterTypes = React.useMemo( + () => ({ + // Add a new fuzzyTextFilterFn filter type. + // fuzzyText: fuzzyTextFilterFn, + // Or, override the default text filter to use + // "startWith" + text: (rows: any, id: any, filterValue: any) => + rows.filter((row: any) => { + const rowValue = row.values[id]; + return rowValue !== undefined + ? String(rowValue) + .toLowerCase() + .startsWith(String(filterValue).toLowerCase()) + : true; + }), + }), + [] + ); + + const defaultColumn = React.useMemo( + () => ({ + // Let's set up our default Filter UI + Filter: DefaultColumnFilter, + }), + [] + ); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + //@ts-expect-error + page, + prepareRow, + //@ts-expect-error + canPreviousPage, + //@ts-expect-error + canNextPage, + //@ts-expect-error + pageCount, + //@ts-expect-error + gotoPage, + //@ts-expect-error + nextPage, + //@ts-expect-error + previousPage, + //@ts-expect-error + setPageSize, + //@ts-expect-error + state: { pageIndex, pageSize }, + } = useTable( + { + columns, + data, + //@ts-expect-error + defaultColumn, // Be sure to pass the defaultColumn option + filterTypes, + initialState: { + //@ts-expect-error + pageIndex: 0, + }, + }, + useFilters, // useFilters! + useGlobalFilter, + usePagination + ); + + return ( +
+
+
+
+ + {headerGroups.map((headerGroup: any) => ( + header.id)} + > + {headerGroup.headers.map((column: any) => + column.hideHeader === false ? null : ( + + ) + )} + + ))} + + + {page.map((row: any, i: any) => { + prepareRow(row); + return ( + + {row.cells.map((cell: any) => ( + + ))} + + ); + })} + +
+ {column.render("Header")} + {/* Render the columns filter UI */} +
+ {column.canFilter ? column.render("Filter") : null} +
+
+ {cell.render("Cell")} +
+ + {data.length > 10 && ( + + )} +
+ + + ); +} + +export default function Clients() { + const { data, status, refetch } = useQuery( + "fetchallTickets", + fetchALLTIckets + ); + + const router = useRouter(); + + const high = "bg-red-100 text-red-800"; + const low = "bg-blue-100 text-blue-800"; + const normal = "bg-green-100 text-green-800"; + + const columns = useMemo( + () => [ + { + Header: "Type", + accessor: "type", + id: "type", + width: 50, + }, + { + Header: "Summary", + accessor: "title", + id: "summary", + Cell: ({ row, value }: any) => { + return ( + <> + {value} + + ); + }, + }, + { + Header: "Assignee", + accessor: "assignedTo.name", + id: "assignee", + Cell: ({ row, value }: any) => { + return ( + <> + {value ? value : "n/a"} + + ); + }, + }, + { + Header: "Client", + accessor: "client.name", + id: "client", + Cell: ({ row, value }: any) => { + return ( + <> + {value ? value : "n/a"} + + ); + }, + }, + { + Header: "Priority", + accessor: "priority", + id: "priority", + Cell: ({ row, value }) => { + let p = value; + let badge; + + if (p === "Low") { + badge = low; + } + if (p === "Normal") { + badge = normal; + } + if (p === "High") { + badge = high; + } + + return ( + <> + + {value} + + + ); + }, + }, + { + Header: "Status", + accessor: "status", + id: "status", + Cell: ({ row, value }) => { + let p = value; + let badge; + + return ( + <> + + {value === "needs_support" && Needs Support} + {value === "in_progress" && In Progress} + {value === "in_review" && In Review} + {value === "done" && Done} + + + ); + }, + }, + { + Header: "Created", + accessor: "createdAt", + id: "created", + Cell: ({ row, value }) => { + const now = moment(value).format("DD/MM/YYYY"); + return ( + <> + {now} + + ); + }, + }, + ], + [] + ); + + return ( +
+
+
+
+

Tickets

+
+
+
+
+

+ A list of all your organisation's tickets, regardless of + status. +

+
+
+
+ {status === "loading" && ( +
+

Loading data ...

+
+ )} + + {status === "error" && ( +
+

+ {" "} + Error fetching data ...{" "} +

+
+ )} + {status === "success" && ( + <> + {data.tickets && data.tickets.length > 0 && ( + <> +
+ + + +
+ +
+ + )} + + {data.tickets.length === 0 && ( + <> +
+ + + + +

+ You currently don't have any assigned tickets. :) +

+
+ + )} + + )} + + + + + + ); +} diff --git a/apps/client/pages/admin/time-tracking.tsx b/apps/client/pages/admin/time-tracking.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/client/pages/tickets/index.tsx b/apps/client/pages/tickets/index.tsx index 05ad4d3c7..078872ed0 100644 --- a/apps/client/pages/tickets/index.tsx +++ b/apps/client/pages/tickets/index.tsx @@ -4,10 +4,12 @@ import { useRouter } from "next/router"; import { useState } from "react"; import Loader from "react-spinners/ClipLoader"; +import TicketsAdminLayout from "../../components/TicketViews/admin"; import AssignedTickets from "../../components/TicketViews/assigned"; import ClosedTickets from "../../components/TicketViews/closed"; import OpenTickets from "../../components/TicketViews/open"; import UnassignedTickets from "../../components/TicketViews/unassiged"; +import { useUser } from "../../store/session"; function classNames(...classes: any) { return classes.filter(Boolean).join(" "); @@ -19,6 +21,8 @@ export default function Tickets() { const [loading, setLoading] = useState(false); + const user = useUser(); + const tabs = [ { name: t("open"), @@ -68,7 +72,10 @@ export default function Tickets() { defaultValue={tabs[0].name} > {tabs.map((tab) => ( - + <> + + {user.isAdmin && } + ))} @@ -76,19 +83,20 @@ export default function Tickets() {
@@ -115,6 +136,9 @@ export default function Tickets() { )} {router.asPath === "/tickets?filter=closed" && } + {router.asPath === "/tickets?filter=admin" && ( + + )}