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 : (
+
+ {column.render("Header")}
+ {/* Render the columns filter UI */}
+
+ {column.canFilter ? column.render("Filter") : null}
+
+ |
+ )
+ )}
+
+ ))}
+
+
+ {page.map((row: any, i: any) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell: any) => (
+
+ {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 : (
+
+ {column.render("Header")}
+ {/* Render the columns filter UI */}
+
+ {column.canFilter ? column.render("Filter") : null}
+
+ |
+ )
+ )}
+
+ ))}
+
+
+ {page.map((row: any, i: any) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell: any) => (
+
+ {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" && (
+
+ )}
>