Files
encoach_frontend/src/pages/tickets.tsx
2024-01-31 10:47:14 +00:00

357 lines
11 KiB
TypeScript

import Layout from "@/components/High/Layout";
import TicketDisplay from "@/components/High/TicketDisplay";
import Select from "@/components/Low/Select";
import Modal from "@/components/Modal";
import useTickets from "@/hooks/useTickets";
import useUser from "@/hooks/useUser";
import useUsers from "@/hooks/useUsers";
import {
Ticket,
TicketStatus,
TicketStatusLabel,
TicketType,
TicketTypeLabel,
} from "@/interfaces/ticket";
import { sessionOptions } from "@/lib/session";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import clsx from "clsx";
import { withIronSessionSsr } from "iron-session/next";
import moment from "moment";
import Head from "next/head";
import { useEffect, useState } from "react";
import { BsArrowDown, BsArrowUp } from "react-icons/bs";
import { ToastContainer } from "react-toastify";
const columnHelper = createColumnHelper<Ticket>();
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user;
if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return {
props: {
user: null,
},
};
}
if (
shouldRedirectHome(user) ||
["admin", "developer", "agent"].includes(user.type)
) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return {
props: {
user: null,
},
};
}
return {
props: { user: req.session.user },
};
}, sessionOptions);
const StatusClassNames: { [key in TicketStatus]: string } = {
submitted: "bg-mti-gray-dim",
"in-progress": "bg-mti-blue-dark",
completed: "bg-mti-green-dark",
};
const TypesClassNames: { [key in TicketType]: string } = {
feedback: "bg-mti-green-light",
bug: "bg-mti-red-dark",
help: "bg-mti-blue-light",
};
export default function Tickets() {
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
const [selectedTicket, setSelectedTicket] = useState<Ticket>();
const [assigneeFilter, setAssigneeFilter] = useState<string>();
const [dateSorting, setDateSorting] = useState<"asc" | "desc">("desc");
const [typeFilter, setTypeFilter] = useState<TicketType>();
const [statusFilter, setStatusFilter] = useState<TicketStatus>();
const { user } = useUser({ redirectTo: "/login" });
const { users } = useUsers();
const { tickets, reload } = useTickets();
const sortByDate = (a: Ticket, b: Ticket) => {
return moment((dateSorting === "desc" ? b : a).date).diff(
moment((dateSorting === "desc" ? a : b).date),
);
};
useEffect(() => {
const filters = [];
if (user?.type === "agent")
filters.push((x: Ticket) => x.assignedTo === user.id);
if (typeFilter) filters.push((x: Ticket) => x.type === typeFilter);
if (statusFilter) filters.push((x: Ticket) => x.status === statusFilter);
if (assigneeFilter)
filters.push((x: Ticket) => x.assignedTo === assigneeFilter);
setFilteredTickets(
[...filters.reduce((d, f) => d.filter(f), tickets)].sort(sortByDate),
);
}, [tickets, typeFilter, statusFilter, assigneeFilter, dateSorting, user]);
const columns = [
columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("type", {
header: "Type",
cell: (info) => (
<span
className={clsx(
"rounded-lg p-1 px-2 text-white",
TypesClassNames[info.getValue()],
)}
>
{TicketTypeLabel[info.getValue()]}
</span>
),
}),
columnHelper.accessor("reporter", {
header: "Reporter",
cell: (info) => info.getValue().email,
}),
columnHelper.accessor("reportedFrom", {
header: "Reported From",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("date", {
id: "date",
header: (
<button
className="flex items-center gap-2"
onClick={() =>
setDateSorting((prev) => (prev === "asc" ? "desc" : "asc"))
}
>
<span>Date</span>
{dateSorting === "desc" && <BsArrowDown />}
{dateSorting === "asc" && <BsArrowUp />}
</button>
) as any,
cell: (info) => moment(info.getValue()).format("DD/MM/YYYY - HH:mm"),
}),
columnHelper.accessor("subject", {
header: "Subject",
cell: (info) =>
info.getValue().substring(0, 12) +
(info.getValue().length > 12 ? "..." : ""),
}),
columnHelper.accessor("status", {
header: "Status",
cell: (info) => (
<span
className={clsx(
"rounded-lg p-1 px-2 text-white",
StatusClassNames[info.getValue()],
)}
>
{TicketStatusLabel[info.getValue()]}
</span>
),
}),
columnHelper.accessor("assignedTo", {
header: "Assignee",
cell: (info) => users.find((x) => x.id === info.getValue())?.name || "",
}),
];
const getAssigneeValue = () => {
if (user && user.type === "agent")
return { value: user.id, label: `${user.name} - ${user.email}` };
if (assigneeFilter) {
const assigneeUser = users.find((x) => x.id === assigneeFilter);
return assigneeUser
? {
value: assigneeFilter,
label: `${assigneeUser.name} - ${assigneeUser.email}`,
}
: null;
}
return null;
};
const table = useReactTable({
data: filteredTickets,
columns: columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<>
<Modal
isOpen={!!selectedTicket}
onClose={() => {
reload();
setSelectedTicket(undefined);
}}
>
{selectedTicket && (
<TicketDisplay
user={user!}
ticket={selectedTicket}
onClose={() => {
reload();
setSelectedTicket(undefined);
}}
/>
)}
</Modal>
<Head>
<title>Tickets Panel | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user} className="gap-6">
<h1 className="text-2xl font-semibold">Tickets</h1>
<div className="flex w-full items-center gap-4">
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Status
</label>
<Select
options={Object.keys(TicketStatusLabel).map((x) => ({
value: x,
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
}))}
value={
statusFilter
? {
value: statusFilter,
label: TicketStatusLabel[statusFilter],
}
: undefined
}
onChange={(value) =>
setStatusFilter((value?.value as TicketStatus) ?? undefined)
}
isClearable
placeholder="Status..."
/>
</div>
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Type
</label>
<Select
options={Object.keys(TicketTypeLabel).map((x) => ({
value: x,
label: TicketTypeLabel[x as keyof typeof TicketTypeLabel],
}))}
value={
typeFilter
? { value: typeFilter, label: TicketTypeLabel[typeFilter] }
: undefined
}
onChange={(value) =>
setTypeFilter((value?.value as TicketType) ?? undefined)
}
isClearable
placeholder="Type..."
/>
</div>
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Assignee
</label>
<Select
options={[
{ value: "me", label: "Assigned to me" },
...users
.filter((x) =>
["admin", "developer", "agent"].includes(x.type),
)
.map((u) => ({
value: u.id,
label: `${u.name} - ${u.email}`,
})),
]}
disabled={user.type === "agent"}
value={getAssigneeValue()}
onChange={(value) =>
value
? setAssigneeFilter(
value.value === "me" ? user.id : value.value,
)
: setAssigneeFilter(undefined)
}
placeholder="Assignee..."
isClearable
/>
</div>
</div>
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="px-4 py-4 text-left" key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr
className={clsx(
"even:bg-mti-purple-ultralight/40 hover:bg-mti-purple-ultralight cursor-pointer rounded-lg py-2 odd:bg-white",
"transition duration-300 ease-in-out",
)}
onClick={() => setSelectedTicket(row.original)}
key={row.id}
>
{row.getVisibleCells().map((cell) => (
<td className="w-fit items-center px-4 py-2" key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</Layout>
)}
</>
);
}