From 9177a6b2acf9e4af76ae7e8a6f5441dd353ba4d6 Mon Sep 17 00:00:00 2001 From: Carlos Mesquita Date: Mon, 9 Sep 2024 01:22:13 +0100 Subject: [PATCH] Pagination on UserList --- src/hooks/useUsers.tsx | 4 +- src/pages/(admin)/Lists/UserList.tsx | 170 +++++++++++++-------------- src/pages/api/users/list.ts | 4 +- src/utils/users.be.ts | 41 ++++--- 4 files changed, 111 insertions(+), 108 deletions(-) diff --git a/src/hooks/useUsers.tsx b/src/hooks/useUsers.tsx index 7b860637..fcd5916b 100644 --- a/src/hooks/useUsers.tsx +++ b/src/hooks/useUsers.tsx @@ -9,7 +9,7 @@ export const userHashStudent = {type: "student"} as {type: Type}; export const userHashTeacher = {type: "teacher"} as {type: Type}; export const userHashCorporate = {type: "corporate"} as {type: Type}; -export default function useUsers(props?: {type?: string; page?: number; size?: number; orderBy?: string; direction?: "asc" | "desc"}) { +export default function useUsers(props?: {type?: string; page?: number; size?: number; orderBy?: string; direction?: "asc" | "desc", searchTerm?: string | undefined}) { const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [isLoading, setIsLoading] = useState(false); @@ -35,7 +35,7 @@ export default function useUsers(props?: {type?: string; page?: number; size?: n .finally(() => setIsLoading(false)); }; // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(getData, [props?.page, props?.size, props?.type, props?.orderBy, props?.direction]); + useEffect(getData, [props?.page, props?.size, props?.type, props?.orderBy, props?.direction, props?.searchTerm]); return {users, total, isLoading, isError, reload: getData}; } diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 38f48b47..bcb3dceb 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -1,37 +1,38 @@ import Button from "@/components/Low/Button"; -import {PERMISSIONS} from "@/constants/userPermissions"; +import { PERMISSIONS } from "@/constants/userPermissions"; import useGroups from "@/hooks/useGroups"; import useUsers from "@/hooks/useUsers"; -import {Type, User, userTypes, CorporateUser, Group} from "@/interfaces/user"; -import {Popover, Transition} from "@headlessui/react"; -import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import { Type, User, userTypes, CorporateUser, Group } from "@/interfaces/user"; +import { Popover, Transition } from "@headlessui/react"; +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import {capitalize, reverse} from "lodash"; +import { capitalize, reverse } from "lodash"; import moment from "moment"; -import {Fragment, useEffect, useState, useMemo} from "react"; -import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs"; -import {toast} from "react-toastify"; -import {countries, TCountries} from "countries-list"; +import { Fragment, useEffect, useState, useMemo } from "react"; +import { BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash } from "react-icons/bs"; +import { toast } from "react-toastify"; +import { countries, TCountries } from "countries-list"; import countryCodes from "country-codes-list"; import Modal from "@/components/Modal"; import UserCard from "@/components/UserCard"; -import {getUserCompanyName, isAgentUser, USER_TYPE_LABELS} from "@/resources/user"; +import { getUserCompanyName, isAgentUser, USER_TYPE_LABELS } from "@/resources/user"; import useFilterStore from "@/stores/listFilterStore"; -import {useRouter} from "next/router"; -import {isCorporateUser} from "@/resources/user"; -import {useListSearch} from "@/hooks/useListSearch"; -import {getUserCorporate} from "@/utils/groups"; -import {asyncSorter} from "@/utils"; -import {exportListToExcel, UserListRow} from "@/utils/users"; -import {checkAccess} from "@/utils/permissions"; -import {PermissionType} from "@/interfaces/permissions"; +import { useRouter } from "next/router"; +import { isCorporateUser } from "@/resources/user"; +import { useListSearch } from "@/hooks/useListSearch"; +import { getUserCorporate } from "@/utils/groups"; +import { asyncSorter } from "@/utils"; +import { exportListToExcel, UserListRow } from "@/utils/users"; +import { checkAccess } from "@/utils/permissions"; +import { PermissionType } from "@/interfaces/permissions"; import usePermissions from "@/hooks/usePermissions"; import useUserBalance from "@/hooks/useUserBalance"; +import Input from "@/components/Low/Input"; const columnHelper = createColumnHelper(); const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; -const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => { +const CompanyNameCell = ({ users, user, groups }: { user: User; users: User[]; groups: Group[] }) => { const [companyName, setCompanyName] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -59,20 +60,13 @@ export default function UserList({ const [displayUsers, setDisplayUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(); const [page, setPage] = useState(0); + const [searchTerm, setSearchTerm] = useState(undefined); - const userHash = useMemo( - () => ({ - type, - size: 16, - page, - }), - [type, page], - ); + const { users, total, isLoading, reload } = useUsers({type: type, size: 16, page: page, searchTerm: searchTerm}); - const {users, total, isLoading, reload} = useUsers(userHash); - const {permissions} = usePermissions(user?.id || ""); - const {balance} = useUserBalance(); - const {groups} = useGroups({ + const { permissions } = usePermissions(user?.id || ""); + const { balance } = useUserBalance(); + const { groups } = useGroups({ admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined, userType: user?.type, }); @@ -106,20 +100,20 @@ export default function UserList({ if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return; axios - .delete<{ok: boolean}>(`/api/user?id=${user.id}`) + .delete<{ ok: boolean }>(`/api/user?id=${user.id}`) .then(() => { toast.success("User deleted successfully!"); reload(); }) .catch(() => { - toast.error("Something went wrong!", {toastId: "delete-error"}); + toast.error("Something went wrong!", { toastId: "delete-error" }); }) .finally(reload); }; const verifyAccount = (user: User) => { axios - .post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, { + .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { ...user, isVerified: true, }) @@ -128,22 +122,21 @@ export default function UserList({ reload(); }) .catch(() => { - toast.error("Something went wrong!", {toastId: "update-error"}); + toast.error("Something went wrong!", { toastId: "update-error" }); }); }; const toggleDisableAccount = (user: User) => { if ( !confirm( - `Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${ - user.name + `Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${user.name }'s account? This change is usually related to their payment state.`, ) ) return; axios - .post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, { + .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { ...user, status: user.status === "disabled" ? "active" : "disabled", }) @@ -152,18 +145,18 @@ export default function UserList({ reload(); }) .catch(() => { - toast.error("Something went wrong!", {toastId: "update-error"}); + toast.error("Something went wrong!", { toastId: "update-error" }); }); }; - const SorterArrow = ({name}: {name: string}) => { + const SorterArrow = ({ name }: { name: string }) => { if (sorter === name) return ; if (sorter === reverseString(name)) return ; return ; }; - const actionColumn = ({row}: {row: {original: User}}) => { + const actionColumn = ({ row }: { row: { original: User } }) => { const updateUserPermission = PERMISSIONS.updateUser[row.original.type] as { list: Type[]; perm: PermissionType; @@ -208,11 +201,11 @@ export default function UserList({ ) as any, - cell: ({row, getValue}) => ( + cell: ({ row, getValue }) => (
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null @@ -230,9 +223,8 @@ export default function UserList({ ) as any, cell: (info) => info.getValue() - ? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${ - countries[info.getValue() as unknown as keyof TCountries]?.name - } (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})` + ? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${countries[info.getValue() as unknown as keyof TCountries]?.name + } (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})` : "N/A", }), columnHelper.accessor("demographicInformation.phone", { @@ -298,11 +290,11 @@ export default function UserList({ ) as any, - cell: ({row, getValue}) => ( + cell: ({ row, getValue }) => (
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null @@ -318,11 +310,11 @@ export default function UserList({ ) as any, - cell: ({row, getValue}) => ( + cell: ({ row, getValue }) => (
(PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}> {getValue()} @@ -505,19 +497,17 @@ export default function UserList({ return a.id.localeCompare(b.id); }; - const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); - const table = useReactTable({ - data: filteredRows, + data: displayUsers, columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any, getCoreRowModel: getCoreRowModel(), }); const downloadExcel = () => { - const csv = exportListToExcel(filteredRows, users, groups); + const csv = exportListToExcel(displayUsers, users, groups); const element = document.createElement("a"); - const file = new Blob([csv], {type: "text/csv"}); + const file = new Blob([csv], { type: "text/csv" }); element.href = URL.createObjectURL(file); element.download = "users.csv"; document.body.appendChild(element); @@ -551,53 +541,53 @@ export default function UserList({ onViewStudents={ (selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0 ? () => { - appendUserFilters({ - id: "view-students", - filter: viewStudentFilter, - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: belongsToAdminFilter, - }); + appendUserFilters({ + id: "view-students", + filter: viewStudentFilter, + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: belongsToAdminFilter, + }); - router.push("/list/users"); - } + router.push("/list/users"); + } : undefined } onViewTeachers={ (selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0 ? () => { - appendUserFilters({ - id: "view-teachers", - filter: viewTeacherFilter, - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: belongsToAdminFilter, - }); + appendUserFilters({ + id: "view-teachers", + filter: viewTeacherFilter, + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: belongsToAdminFilter, + }); - router.push("/list/users"); - } + router.push("/list/users"); + } : undefined } onViewCorporate={ selectedUser.type === "teacher" || selectedUser.type === "student" ? () => { - appendUserFilters({ - id: "view-corporate", - filter: (x: User) => x.type === "corporate", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter((g) => g.participants.includes(selectedUser.id)) - .flatMap((g) => [g.admin, ...g.participants]) - .includes(x.id), - }); + appendUserFilters({ + id: "view-corporate", + filter: (x: User) => x.type === "corporate", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter((g) => g.participants.includes(selectedUser.id)) + .flatMap((g) => [g.admin, ...g.participants]) + .includes(x.id), + }); - router.push("/list/users"); - } + router.push("/list/users"); + } : undefined } onClose={(shouldReload) => { @@ -619,7 +609,7 @@ export default function UserList({
- {renderSearch()} + diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index 41d509ca..f9806c9a 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -19,7 +19,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { page, orderBy, direction = "desc", - } = req.query as {size?: string; type?: Type; page?: string; orderBy?: string; direction?: "asc" | "desc"}; + searchTerm + } = req.query as {size?: string; type?: Type; page?: string; orderBy?: string; direction?: "asc" | "desc"; searchTerm?: string | undefined}; const {users, total} = await getLinkedUsers( req.session.user?.id, @@ -29,6 +30,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { size !== undefined ? parseInt(size) : undefined, orderBy, direction, + searchTerm ); res.status(200).json({users, total}); diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index a04bfd20..57de4924 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -1,7 +1,7 @@ -import {CorporateUser, Group, Type, User} from "@/interfaces/user"; -import {getGroupsForUser, getParticipantGroups, getUserGroups, getUsersGroups} from "./groups.be"; -import {last, uniq, uniqBy} from "lodash"; -import {getUserCodes} from "./codes.be"; +import { CorporateUser, Group, Type, User } from "@/interfaces/user"; +import { getGroupsForUser, getParticipantGroups, getUserGroups, getUsersGroups } from "./groups.be"; +import { last, uniq, uniqBy } from "lodash"; +import { getUserCodes } from "./codes.be"; import moment from "moment"; import client from "@/lib/mongodb"; @@ -12,7 +12,7 @@ export async function getUsers() { } export async function getUser(id: string): Promise { - const user = await db.collection("users").findOne({id}); + const user = await db.collection("users").findOne({ id }); return !!user ? user : undefined; } @@ -21,7 +21,7 @@ export async function getSpecificUsers(ids: string[]) { return await db .collection("users") - .find({id: {$in: ids}}) + .find({ id: { $in: ids } }) .toArray(); } @@ -33,21 +33,32 @@ export async function getLinkedUsers( size?: number, sort?: string, direction?: "asc" | "desc", + searchTerm?: string | undefined, ) { - const filters = { - ...(!!type ? {type} : {}), - }; + const filters: any = {}; + + if (type) { + filters.type = type; + } + + if (searchTerm) { + filters.$or = [ + { name: { $regex: searchTerm, $options: 'i' } }, + { email: { $regex: searchTerm, $options: 'i' } }, + { company: { $regex: searchTerm, $options: 'i' } }, + ]; + } if (!userID || userType === "admin" || userType === "developer") { const users = await db .collection("users") .find(filters) - .sort(sort ? {[sort]: direction === "desc" ? -1 : 1} : {}) + .sort(sort ? { [sort]: direction === "desc" ? -1 : 1 } : {}) .skip(page && size ? page * size : 0) .limit(size || 0) .toArray(); const total = await db.collection("users").countDocuments(filters); - return {users, total}; + return { users, total }; } const adminGroups = await getUserGroups(userID); @@ -61,17 +72,17 @@ export async function getLinkedUsers( ]); // тип [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] { - if (participants.length === 0) return {users: [], total: 0}; + if (participants.length === 0) return { users: [], total: 0 }; const users = await db .collection("users") - .find({...filters, id: {$in: participants}}) + .find({ ...filters, id: { $in: participants } }) .skip(page && size ? page * size : 0) .limit(size || 0) .toArray(); - const total = await db.collection("users").countDocuments({...filters, id: {$in: participants}}); + const total = await db.collection("users").countDocuments({ ...filters, id: { $in: participants } }); - return {users, total}; + return { users, total }; } export async function getUserBalance(user: User) {