Created a new page for ticket handling as well as submission
This commit is contained in:
316
src/pages/tickets.tsx
Normal file
316
src/pages/tickets.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
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 { 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) || user.type !== "developer") {
|
||||
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 [typeFilter, setTypeFilter] = useState<TicketType>();
|
||||
const [statusFilter, setStatusFilter] = useState<TicketStatus>();
|
||||
|
||||
const { user } = useUser({ redirectTo: "/login" });
|
||||
const { users } = useUsers();
|
||||
const { tickets, reload } = useTickets();
|
||||
|
||||
useEffect(() => {
|
||||
const filters = [];
|
||||
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));
|
||||
}, [tickets, typeFilter, statusFilter, assigneeFilter]);
|
||||
|
||||
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", {
|
||||
header: "Date",
|
||||
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 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"].includes(x.type))
|
||||
.map((u) => ({
|
||||
value: u.id,
|
||||
label: `${u.name} - ${u.email}`,
|
||||
})),
|
||||
]}
|
||||
value={
|
||||
assigneeFilter
|
||||
? {
|
||||
value: assigneeFilter,
|
||||
label: `${users.find((u) => u.id === assigneeFilter)?.name} - ${users.find((u) => u.id === assigneeFilter)?.email}`,
|
||||
}
|
||||
: null
|
||||
}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user