Added the ability to sort by every column

This commit is contained in:
Tiago Ribeiro
2023-10-19 09:40:52 +01:00
parent ffe534edd9
commit 171f328278
2 changed files with 191 additions and 47 deletions

View File

@@ -2,15 +2,15 @@ import Button from "@/components/Low/Button";
import {PERMISSIONS} from "@/constants/userPermissions"; import {PERMISSIONS} from "@/constants/userPermissions";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {Type, User} from "@/interfaces/user"; import {Type, User, userTypes} from "@/interfaces/user";
import {Popover, Transition} from "@headlessui/react"; import {Popover, Transition} from "@headlessui/react";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {capitalize} from "lodash"; import {capitalize, reverse} from "lodash";
import moment from "moment"; import moment from "moment";
import {Fragment, useState} from "react"; import {Fragment, useEffect, useState} from "react";
import {BsCheck, BsCheckCircle, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs"; import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {countries, TCountries} from "countries-list"; import {countries, TCountries} from "countries-list";
import countryCodes from "country-codes-list"; import countryCodes from "country-codes-list";
@@ -19,10 +19,22 @@ const columnHelper = createColumnHelper<User>();
export default function UserList({user}: {user: User}) { export default function UserList({user}: {user: User}) {
const [showDemographicInformation, setShowDemographicInformation] = useState(false); const [showDemographicInformation, setShowDemographicInformation] = useState(false);
const [sorter, setSorter] = useState<string>();
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {groups} = useGroups(user ? user.id : undefined); const {groups} = useGroups(user ? user.id : undefined);
useEffect(() => {
if (user && users) {
const filterUsers =
user.type === "admin" || user.type === "student" ? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id)) : users;
setDisplayUsers([...filterUsers.sort(sortFunction)]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, users, sorter, groups]);
const deleteAccount = (user: User) => { const deleteAccount = (user: User) => {
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return; if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
@@ -84,6 +96,13 @@ export default function UserList({user}: {user: User}) {
}); });
}; };
const SorterArrow = ({name}: {name: string}) => {
if (sorter === name) return <BsArrowUp />;
if (sorter === reverseString(name)) return <BsArrowDown />;
return <BsArrowDownUp />;
};
const actionColumn = ({row}: {row: {original: User}}) => { const actionColumn = ({row}: {row: {original: User}}) => {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
@@ -161,12 +180,21 @@ export default function UserList({user}: {user: User}) {
const demographicColumns = [ const demographicColumns = [
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: "Name", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
<span>Name</span>
<SorterArrow name="name" />
</button>
) as any,
cell: (info) => info.getValue(), cell: (info) => info.getValue(),
enableSorting: true,
}), }),
columnHelper.accessor("demographicInformation.country", { columnHelper.accessor("demographicInformation.country", {
header: "Country", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "country"))}>
<span>Country</span>
<SorterArrow name="country" />
</button>
) as any,
cell: (info) => cell: (info) =>
info.getValue() info.getValue()
? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${ ? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${
@@ -175,17 +203,32 @@ export default function UserList({user}: {user: User}) {
: "Not available", : "Not available",
}), }),
columnHelper.accessor("demographicInformation.phone", { columnHelper.accessor("demographicInformation.phone", {
header: "Phone", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}>
<span>Phone</span>
<SorterArrow name="phone" />
</button>
) as any,
cell: (info) => info.getValue() || "Not available", cell: (info) => info.getValue() || "Not available",
enableSorting: true, enableSorting: true,
}), }),
columnHelper.accessor("demographicInformation.employment", { columnHelper.accessor("demographicInformation.employment", {
header: "Employment", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
<span>Employment</span>
<SorterArrow name="employment" />
</button>
) as any,
cell: (info) => capitalize(info.getValue()) || "Not available", cell: (info) => capitalize(info.getValue()) || "Not available",
enableSorting: true, enableSorting: true,
}), }),
columnHelper.accessor("demographicInformation.gender", { columnHelper.accessor("demographicInformation.gender", {
header: "Gender", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
<span>Gender</span>
<SorterArrow name="gender" />
</button>
) as any,
cell: (info) => capitalize(info.getValue()) || "Not available", cell: (info) => capitalize(info.getValue()) || "Not available",
enableSorting: true, enableSorting: true,
}), }),
@@ -202,24 +245,48 @@ export default function UserList({user}: {user: User}) {
const defaultColumns = [ const defaultColumns = [
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: "Name", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
<span>Name</span>
<SorterArrow name="name" />
</button>
) as any,
cell: (info) => info.getValue(), cell: (info) => info.getValue(),
enableSorting: true,
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
header: "E-mail", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "email"))}>
<span>E-mail</span>
<SorterArrow name="email" />
</button>
) as any,
cell: (info) => info.getValue(), cell: (info) => info.getValue(),
}), }),
columnHelper.accessor("type", { columnHelper.accessor("type", {
header: "Type", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "type"))}>
<span>Type</span>
<SorterArrow name="type" />
</button>
) as any,
cell: (info) => capitalize(info.getValue()), cell: (info) => capitalize(info.getValue()),
}), }),
columnHelper.accessor("subscriptionExpirationDate", { columnHelper.accessor("subscriptionExpirationDate", {
header: "Expiry Date", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
<span>Expiry Date</span>
<SorterArrow name="expiryDate" />
</button>
) as any,
cell: (info) => (!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")), cell: (info) => (!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")),
}), }),
columnHelper.accessor("isVerified", { columnHelper.accessor("isVerified", {
header: "Verification", header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
<span>Verification</span>
<SorterArrow name="verification" />
</button>
) as any,
cell: (info) => ( cell: (info) => (
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center"> <div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
<div <div
@@ -244,39 +311,116 @@ export default function UserList({user}: {user: User}) {
}, },
]; ];
const reverseString = (str: string) => reverse(str.split("")).join("");
const selectSorter = (previous: string | undefined, name: string) => {
if (!previous) return name;
if (previous === name) return reverseString(name);
return undefined;
};
const sortFunction = (a: User, b: User) => {
if (sorter === "name" || sorter === reverseString("name"))
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
if (sorter === "email" || sorter === reverseString("email"))
return sorter === "email" ? a.email.localeCompare(b.email) : b.email.localeCompare(a.email);
if (sorter === "type" || sorter === reverseString("type"))
return sorter === "type"
? userTypes.findIndex((t) => a.type === t) - userTypes.findIndex((t) => b.type === t)
: userTypes.findIndex((t) => b.type === t) - userTypes.findIndex((t) => a.type === t);
if (sorter === "verification" || sorter === reverseString("verification"))
return sorter === "verification"
? a.isVerified.toString().localeCompare(b.isVerified.toString())
: b.isVerified.toString().localeCompare(a.isVerified.toString());
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate) return sorter === "expiryDate" ? -1 : 1;
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return sorter === "expiryDate" ? 1 : -1;
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return 0;
if (moment(a.subscriptionExpirationDate).isAfter(b.subscriptionExpirationDate)) return sorter === "expiryDate" ? -1 : 1;
if (moment(b.subscriptionExpirationDate).isAfter(a.subscriptionExpirationDate)) return sorter === "expiryDate" ? 1 : -1;
return 0;
}
if (sorter === "country" || sorter === reverseString("country")) {
if (!a.demographicInformation?.country && b.demographicInformation?.country) return sorter === "country" ? -1 : 1;
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;
if (!a.demographicInformation?.country && !b.demographicInformation?.country) return 0;
return sorter === "country"
? a.demographicInformation!.country.localeCompare(b.demographicInformation!.country)
: b.demographicInformation!.country.localeCompare(a.demographicInformation!.country);
}
if (sorter === "phone" || sorter === reverseString("phone")) {
if (!a.demographicInformation?.phone && b.demographicInformation?.phone) return sorter === "phone" ? -1 : 1;
if (a.demographicInformation?.phone && !b.demographicInformation?.phone) return sorter === "phone" ? 1 : -1;
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone) return 0;
return sorter === "phone"
? a.demographicInformation!.phone.localeCompare(b.demographicInformation!.phone)
: b.demographicInformation!.phone.localeCompare(a.demographicInformation!.phone);
}
if (sorter === "employment" || sorter === reverseString("employment")) {
if (!a.demographicInformation?.employment && b.demographicInformation?.employment) return sorter === "employment" ? -1 : 1;
if (a.demographicInformation?.employment && !b.demographicInformation?.employment) return sorter === "employment" ? 1 : -1;
if (!a.demographicInformation?.employment && !b.demographicInformation?.employment) return 0;
return sorter === "employment"
? a.demographicInformation!.employment.localeCompare(b.demographicInformation!.employment)
: b.demographicInformation!.employment.localeCompare(a.demographicInformation!.employment);
}
if (sorter === "gender" || sorter === reverseString("gender")) {
if (!a.demographicInformation?.gender && b.demographicInformation?.gender) return sorter === "employment" ? -1 : 1;
if (a.demographicInformation?.gender && !b.demographicInformation?.gender) return sorter === "employment" ? 1 : -1;
if (!a.demographicInformation?.gender && !b.demographicInformation?.gender) return 0;
return sorter === "gender"
? a.demographicInformation!.gender.localeCompare(b.demographicInformation!.gender)
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
}
return a.id.localeCompare(b.id);
};
const table = useReactTable({ const table = useReactTable({
data: data: displayUsers,
user && (user.type === "admin" || user.type === "student")
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
: users,
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any, columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
return ( return (
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full"> <div className="w-full">
<thead> <table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
{table.getHeaderGroups().map((headerGroup) => ( <thead>
<tr key={headerGroup.id}> {table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => ( <tr key={headerGroup.id}>
<th className="py-4 px-4 text-left" key={header.id}> {headerGroup.headers.map((header) => (
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} <th className="py-4 px-4 text-left" key={header.id}>
</th> {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
))} </th>
</tr> ))}
))} </tr>
</thead> ))}
<tbody className="px-2"> </thead>
{table.getRowModel().rows.map((row) => ( <tbody className="px-2">
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}> {table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => ( <tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
<td className="px-4 py-2 items-center w-fit" key={cell.id}> {row.getVisibleCells().map((cell) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())} <td className="px-4 py-2 items-center w-fit" key={cell.id}>
</td> {flexRender(cell.column.columnDef.cell, cell.getContext())}
))} </td>
</tr> ))}
))} </tr>
</tbody> ))}
</table> </tbody>
</table>
</div>
); );
} }

View File

@@ -46,15 +46,15 @@ export default function Lists({user}: {user: User}) {
</Tab> </Tab>
</Tab.List> </Tab.List>
<Tab.Panels className="mt-2"> <Tab.Panels className="mt-2">
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow"> <Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
<UserList user={user} /> <UserList user={user} />
</Tab.Panel> </Tab.Panel>
{user?.type === "developer" && ( {user?.type === "developer" && (
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow"> <Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
<ExamList user={user} /> <ExamList user={user} />
</Tab.Panel> </Tab.Panel>
)} )}
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow"> <Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
<GroupList user={user} /> <GroupList user={user} />
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>