Applied the same fix for other pages

This commit is contained in:
Tiago Ribeiro
2024-03-13 09:21:16 +00:00
parent 49ee3c45e5
commit 74dd96d000
2 changed files with 261 additions and 341 deletions

View File

@@ -22,16 +22,16 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
redirect: { redirect: {
destination: "/login", destination: "/login",
permanent: false, permanent: false,
} },
}; };
} }
if (shouldRedirectHome(user) || user.type !== "developer") { if (shouldRedirectHome(user) || !["developer", "admin", "corporate", "agent"].includes(user.type)) {
return { return {
redirect: { redirect: {
destination: "/", destination: "/",
permanent: false, permanent: false,
} },
}; };
} }

View File

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