import { PERMISSIONS } from "@/constants/userPermissions"; import { Type, User } from "@/interfaces/user"; import { createColumnHelper } from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; import { capitalize } from "lodash"; import moment from "moment"; import { useMemo, useState } from "react"; import { BsCheck, BsCheckCircle, BsFillExclamationOctagonFill, 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 { USER_TYPE_LABELS } from "@/resources/user"; import useFilterStore from "@/stores/listFilterStore"; import { useRouter } from "next/router"; import { mapBy } from "@/utils"; import { exportListToExcel } from "@/utils/users"; import useEntitiesUsers from "@/hooks/useEntitiesUsers"; import { WithLabeledEntities } from "@/interfaces/entity"; import Table from "@/components/High/Table"; import useEntities from "@/hooks/useEntities"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; const columnHelper = createColumnHelper>(); const searchFields = [["name"], ["email"], ["entities", ""]]; export default function UserList({ user, filters = [], type, renderHeader, }: { user: User; filters?: ((user: User) => boolean)[]; type?: Type; renderHeader?: (total: number) => JSX.Element; }) { const [showDemographicInformation, setShowDemographicInformation] = useState(false); const [selectedUser, setSelectedUser] = useState(); const { users, isLoading, reload } = useEntitiesUsers(type); const { entities } = useEntities(); const isAdmin = useMemo( () => ["admin", "developer"].includes(user?.type), [user?.type] ); const entitiesViewStudents = useAllowedEntities( user, entities, "view_students" ); const entitiesEditStudents = useAllowedEntities( user, entities, "edit_students" ); const entitiesDeleteStudents = useAllowedEntities( user, entities, "delete_students" ); const entitiesViewTeachers = useAllowedEntities( user, entities, "view_teachers" ); const entitiesEditTeachers = useAllowedEntities( user, entities, "edit_teachers" ); const entitiesDeleteTeachers = useAllowedEntities( user, entities, "delete_teachers" ); const entitiesViewCorporates = useAllowedEntities( user, entities, "view_corporates" ); const entitiesEditCorporates = useAllowedEntities( user, entities, "edit_corporates" ); const entitiesDeleteCorporates = useAllowedEntities( user, entities, "delete_corporates" ); const entitiesViewMasterCorporates = useAllowedEntities( user, entities, "view_mastercorporates" ); const entitiesEditMasterCorporates = useAllowedEntities( user, entities, "edit_mastercorporates" ); const entitiesDeleteMasterCorporates = useAllowedEntities( user, entities, "delete_mastercorporates" ); const entitiesDownloadUsers = useAllowedEntities( user, entities, "download_user_list" ); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); const expirationDateColor = (date: Date) => { const momentDate = moment(date); const today = moment(new Date()); if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through"; if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light"; if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light"; if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light"; }; const allowedUsers = useMemo( () => users.filter((u) => { if (isAdmin) return true; if (u.id === user?.id) return false; switch (u.type) { case "student": return mapBy(u.entities || [], "id").some((id) => mapBy(entitiesViewStudents, "id").includes(id) ); case "teacher": return mapBy(u.entities || [], "id").some((id) => mapBy(entitiesViewTeachers, "id").includes(id) ); case "corporate": return mapBy(u.entities || [], "id").some((id) => mapBy(entitiesViewCorporates, "id").includes(id) ); case "mastercorporate": return mapBy(u.entities || [], "id").some((id) => mapBy(entitiesViewMasterCorporates, "id").includes(id) ); default: return false; } }), [ entitiesViewCorporates, entitiesViewMasterCorporates, entitiesViewStudents, entitiesViewTeachers, isAdmin, user?.id, users, ] ); const displayUsers = useMemo( () => filters.length > 0 ? filters.reduce((d, f) => d.filter(f), allowedUsers) : allowedUsers, [filters, allowedUsers] ); const deleteAccount = (user: User) => { if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return; axios .delete<{ ok: boolean }>(`/api/user?id=${user.id}`) .then(() => { toast.success("User deleted successfully!"); reload(); }) .catch(() => { 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}`, { ...user, isVerified: true, }) .then(() => { toast.success("User verified successfully!"); reload(); }) .catch(() => { 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 }'s account? This change is usually related to their payment state.` ) ) return; axios .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { ...user, status: user.status === "disabled" ? "active" : "disabled", }) .then(() => { toast.success( `User ${ user.status === "disabled" ? "enabled" : "disabled" } successfully!` ); reload(); }) .catch(() => { toast.error("Something went wrong!", { toastId: "update-error" }); }); }; const getEditPermission = (type: Type) => { if (type === "student") return entitiesEditStudents; if (type === "teacher") return entitiesEditTeachers; if (type === "corporate") return entitiesEditCorporates; if (type === "mastercorporate") return entitiesEditMasterCorporates; return []; }; const getDeletePermission = (type: Type) => { if (type === "student") return entitiesDeleteStudents; if (type === "teacher") return entitiesDeleteTeachers; if (type === "corporate") return entitiesDeleteCorporates; if (type === "mastercorporate") return entitiesDeleteMasterCorporates; return []; }; const canEditUser = (u: User) => isAdmin || u.entities.some((e) => mapBy(getEditPermission(u.type), "id").includes(e.id) ); const canDeleteUser = (u: User) => isAdmin || u.entities.some((e) => mapBy(getDeletePermission(u.type), "id").includes(e.id) ); const actionColumn = ({ row }: { row: { original: User } }) => { const canEdit = canEditUser(row.original); const canDelete = canDeleteUser(row.original); return (
{!row.original.isVerified && canEdit && (
verifyAccount(row.original)} >
)} {canEdit && (
toggleDisableAccount(row.original)} > {row.original.status === "disabled" ? ( ) : ( )}
)} {canDelete && (
deleteAccount(row.original)} >
)}
); }; const demographicColumns = [ columnHelper.accessor("name", { header: "Name", cell: ({ row, getValue }) => (
canEditUser(row.original) ? setSelectedUser(row.original) : null } > {getValue()}
), }), columnHelper.accessor("demographicInformation.country", { header: "Country", 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 })` : "N/A", }), columnHelper.accessor("demographicInformation.phone", { header: "Phone", cell: (info) => info.getValue() || "N/A", enableSorting: true, }), columnHelper.accessor( (x) => x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment, { id: "employment", header: "Employment", cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A", enableSorting: true, } ), columnHelper.accessor("lastLogin", { header: "Last Login", cell: (info) => !!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A", }), columnHelper.accessor("demographicInformation.gender", { header: "Gender", cell: (info) => capitalize(info.getValue()) || "N/A", enableSorting: true, }), { header: ( setShowDemographicInformation((prev) => !prev)} > Switch ), id: "actions", cell: actionColumn, sortable: false, }, ]; const defaultColumns = [ columnHelper.accessor("name", { header: "Name", cell: ({ row, getValue }) => (
canEditUser(row.original) ? setSelectedUser(row.original) : null } > {getValue()}
), }), columnHelper.accessor("email", { header: "E-mail", cell: ({ row, getValue }) => (
canEditUser(row.original) ? setSelectedUser(row.original) : null } > {getValue()}
), }), columnHelper.accessor("type", { header: "Type", cell: (info) => USER_TYPE_LABELS[info.getValue()], }), columnHelper.accessor("studentID", { header: "Student ID", cell: (info) => info.getValue() || "N/A", }), columnHelper.accessor("entities", { header: "Entities", cell: ({ getValue }) => mapBy(getValue(), "label").join(", "), }), columnHelper.accessor("subscriptionExpirationDate", { header: "Expiration", cell: (info) => ( {!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")} ), }), columnHelper.accessor("isVerified", { header: "Verified", cell: (info) => (
), }), { header: ( setShowDemographicInformation((prev) => !prev)} > Switch ), id: "actions", cell: actionColumn, sortable: false, }, ]; const downloadExcel = async (rows: WithLabeledEntities[]) => { if (entitiesDownloadUsers.length === 0) return toast.error("You are not allowed to download the user list."); const allowedRows = rows; const csv = await exportListToExcel(allowedRows); const element = document.createElement("a"); const file = new Blob([csv], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); element.href = URL.createObjectURL(file); element.download = "users.xlsx"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }; const viewStudentFilter = (x: User) => x.type === "student"; const viewTeacherFilter = (x: User) => x.type === "teacher"; const belongsToAdminFilter = (x: User) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], "id").includes(id) ); const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x); const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x); const renderUserCard = (selectedUser: User) => { const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin); const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin); return (
0 ? () => { appendUserFilters({ id: "view-students", filter: viewStudentFilter, }); appendUserFilters({ id: "belongs-to-admin", filter: belongsToAdminFilter, }); router.push("/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, }); router.push("/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: belongsToAdminFilter, }); router.push("/users"); } : undefined } onClose={(shouldReload) => { setSelectedUser(undefined); if (shouldReload) reload(); }} user={selectedUser} />
); }; return ( <> {renderHeader && renderHeader(displayUsers.length)}
setSelectedUser(undefined)} > {selectedUser && renderUserCard(selectedUser)} > data={displayUsers} columns={ (!showDemographicInformation ? defaultColumns : demographicColumns) as any } searchFields={searchFields} onDownload={ entitiesDownloadUsers.length > 0 ? downloadExcel : undefined } isLoading={isLoading} />
); }