diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx index 79eb2d2a..38a971db 100644 --- a/src/components/High/Layout.tsx +++ b/src/components/High/Layout.tsx @@ -33,8 +33,7 @@ export default function Layout({user, children, className, navDisabled = false, focusMode={focusMode} onFocusLayerMouseEnter={onFocusLayerMouseEnter} className="-md:hidden" - userType={user.type} - userId={user.id} + user={user} />
["admin", "developer", "agent"].includes(x.type)) + .filter((x) => checkAccess(x, ["admin", "developer", "agent"])) .map((u) => ({ value: u.id, label: `${u.name} - ${u.email}`, })), ]} - disabled={user.type === "agent"} + disabled={checkAccess(user, ["agent"])} value={ assignedTo ? { diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index 2b8b95fe..df55e6d6 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -1,160 +1,215 @@ -import {User} from "@/interfaces/user"; -import {Dialog, Transition} from "@headlessui/react"; +import { User } from "@/interfaces/user"; +import { Dialog, Transition } from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; import Image from "next/image"; import Link from "next/link"; -import {useRouter} from "next/router"; -import {Fragment} from "react"; -import {BsXLg} from "react-icons/bs"; +import { useRouter } from "next/router"; +import { Fragment } from "react"; +import { BsXLg } from "react-icons/bs"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; interface Props { - isOpen: boolean; - onClose: () => void; - path: string; - user: User; - disableNavigation?: boolean; + isOpen: boolean; + onClose: () => void; + path: string; + user: User; + disableNavigation?: boolean; } -export default function MobileMenu({isOpen, onClose, path, user, disableNavigation}: Props) { - const router = useRouter(); +export default function MobileMenu({ + isOpen, + onClose, + path, + user, + disableNavigation, +}: Props) { + const router = useRouter(); - const logout = async () => { - axios.post("/api/logout").finally(() => { - setTimeout(() => router.reload(), 500); - }); - }; + const logout = async () => { + axios.post("/api/logout").finally(() => { + setTimeout(() => router.reload(), 500); + }); + }; - return ( - - - -
- + return ( + + + +
+ -
-
- - - - - EnCoach logo - -
- -
-
-
- - Dashboard - - {(user.type === "student" || user.type === "teacher" || user.type === "developer") && ( - <> - - Exams - - - Exercises - - - )} - - Stats - - - Record - - {["admin", "developer", "agent", "corporate"].includes(user.type) && ( - - Payment Record - - )} - {["admin", "developer", "corporate", "teacher"].includes(user.type) && ( - - Settings - - )} - {["admin", "developer", "agent"].includes(user.type) && ( - - Tickets - - )} - - Profile - +
+
+ + + + + EnCoach logo + +
+ +
+
+
+ + Dashboard + + {checkAccess(user, ["student", "teacher", "developer"]) && ( + <> + + Exams + + + Exercises + + + )} + + Stats + + + Record + + {checkAccess(user, [ + "admin", + "developer", + "agent", + "corporate", + "mastercorporate", + ]) && ( + + Payment Record + + )} + {checkAccess(user, [ + "admin", + "developer", + "corporate", + "teacher", + "mastercorporate", + ]) && ( + + Settings + + )} + {checkAccess(user, ["admin", "developer", "agent"]) && ( + + Tickets + + )} + + Profile + - - Logout - -
-
-
-
-
-
-
- ); + + Logout + +
+ +
+
+ + + + ); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d13c0a5b..91d49c60 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,212 +1,382 @@ import clsx from "clsx"; -import {IconType} from "react-icons"; -import {MdSpaceDashboard} from "react-icons/md"; +import { IconType } from "react-icons"; +import { MdSpaceDashboard } from "react-icons/md"; import { - BsFileEarmarkText, - BsClockHistory, - BsPencil, - BsGraphUp, - BsChevronBarRight, - BsChevronBarLeft, - BsShieldFill, - BsCloudFill, - BsCurrencyDollar, - BsClipboardData, + BsFileEarmarkText, + BsClockHistory, + BsPencil, + BsGraphUp, + BsChevronBarRight, + BsChevronBarLeft, + BsShieldFill, + BsCloudFill, + BsCurrencyDollar, + BsClipboardData, + BsFileLock, } from "react-icons/bs"; -import {RiLogoutBoxFill} from "react-icons/ri"; -import {SlPencil} from "react-icons/sl"; -import {FaAward} from "react-icons/fa"; +import { RiLogoutBoxFill } from "react-icons/ri"; +import { SlPencil } from "react-icons/sl"; +import { FaAward } from "react-icons/fa"; import Link from "next/link"; -import {useRouter} from "next/router"; +import { useRouter } from "next/router"; import axios from "axios"; import FocusLayer from "@/components/FocusLayer"; -import {preventNavigation} from "@/utils/navigation.disabled"; -import {useEffect, useState} from "react"; +import { preventNavigation } from "@/utils/navigation.disabled"; +import { useEffect, useState } from "react"; import usePreferencesStore from "@/stores/preferencesStore"; -import {Type} from "@/interfaces/user"; +import { User } from "@/interfaces/user"; import useTicketsListener from "@/hooks/useTicketsListener"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; interface Props { - path: string; - navDisabled?: boolean; - focusMode?: boolean; - onFocusLayerMouseEnter?: () => void; - className?: string; - userType?: Type; - userId?: string; + path: string; + navDisabled?: boolean; + focusMode?: boolean; + onFocusLayerMouseEnter?: () => void; + className?: string; + user: User; } interface NavProps { - Icon: IconType; - label: string; - path: string; - keyPath: string; - disabled?: boolean; - isMinimized?: boolean; - badge?: number; + Icon: IconType; + label: string; + path: string; + keyPath: string; + disabled?: boolean; + isMinimized?: boolean; + badge?: number; } -const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, badge}: NavProps) => { - return ( - - - {!isMinimized && {label}} - {!!badge && badge > 0 && ( -
- {badge} -
- )} - - ); +const Nav = ({ + Icon, + label, + path, + keyPath, + disabled = false, + isMinimized = false, + badge, +}: NavProps) => { + return ( + + + {!isMinimized && {label}} + {!!badge && badge > 0 && ( +
+ {badge} +
+ )} + + ); }; -export default function Sidebar({path, navDisabled = false, focusMode = false, userType, onFocusLayerMouseEnter, className, userId}: Props) { - const router = useRouter(); +export default function Sidebar({ + path, + navDisabled = false, + focusMode = false, + user, + onFocusLayerMouseEnter, + className, +}: Props) { + const router = useRouter(); - const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]); + const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [ + state.isSidebarMinimized, + state.toggleSidebarMinimized, + ]); - const {totalAssignedTickets} = useTicketsListener(userId); + const { totalAssignedTickets } = useTicketsListener(user.id); - const logout = async () => { - axios.post("/api/logout").finally(() => { - setTimeout(() => router.reload(), 500); - }); - }; + const logout = async () => { + axios.post("/api/logout").finally(() => { + setTimeout(() => router.reload(), 500); + }); + }; - const disableNavigation = preventNavigation(navDisabled, focusMode); + const disableNavigation = preventNavigation(navDisabled, focusMode); - return ( -
-
-
-
-
+ return ( +
+
+
+
+
-
-
- {isMinimized ? : } - {!isMinimized && Minimize} -
-
{} : logout} - className={clsx( - "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", - isMinimized ? "w-fit" : "w-full min-w-[250px] px-8", - )}> - - {!isMinimized && Log Out} -
-
- {focusMode && } -
- ); +
+
+ {isMinimized ? ( + + ) : ( + + )} + {!isMinimized && ( + Minimize + )} +
+
{} : logout} + className={clsx( + "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", + isMinimized ? "w-fit" : "w-full min-w-[250px] px-8" + )} + > + + {!isMinimized && ( + Log Out + )} +
+
+ {focusMode && ( + + )} +
+ ); } diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 08d97df0..49dad039 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -1,15 +1,27 @@ import useStats from "@/hooks/useStats"; -import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User} from "@/interfaces/user"; -import {groupBySession, averageScore} from "@/utils/stats"; -import {RadioGroup} from "@headlessui/react"; +import { + CorporateInformation, + CorporateUser, + EMPLOYMENT_STATUS, + User, + Type, +} from "@/interfaces/user"; +import { groupBySession, averageScore } from "@/utils/stats"; +import { RadioGroup } from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; import moment from "moment"; -import {Divider} from "primereact/divider"; -import {useEffect, useState} from "react"; +import { Divider } from "primereact/divider"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {BsFileEarmarkText, BsPencil, BsPerson, BsPersonAdd, BsStar} from "react-icons/bs"; -import {toast} from "react-toastify"; +import { + BsFileEarmarkText, + BsPencil, + BsPerson, + BsPersonAdd, + BsStar, +} from "react-icons/bs"; +import { toast } from "react-toastify"; import Button from "./Low/Button"; import Checkbox from "./Low/Checkbox"; import CountrySelect from "./Low/CountrySelect"; @@ -17,638 +29,818 @@ import Input from "./Low/Input"; import ProfileSummary from "./ProfileSummary"; import Select from "react-select"; import useUsers from "@/hooks/useUsers"; -import {USER_TYPE_LABELS} from "@/resources/user"; -import {CURRENCIES} from "@/resources/paypal"; +import { USER_TYPE_LABELS } from "@/resources/user"; +import { CURRENCIES } from "@/resources/paypal"; import useCodes from "@/hooks/useCodes"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import { PERMISSIONS } from "@/constants/userPermissions"; +import { PermissionType } from "@/interfaces/permissions"; const expirationDateColor = (date: Date) => { - const momentDate = moment(date); - const today = moment(new Date()); + const momentDate = moment(date); + const today = moment(new Date()); - if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light"; - if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light"; - if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light"; + if (today.add(1, "days").isAfter(momentDate)) + return "!bg-mti-red-ultralight border-mti-red-light"; + if (today.add(3, "days").isAfter(momentDate)) + return "!bg-mti-rose-ultralight border-mti-rose-light"; + if (today.add(7, "days").isAfter(momentDate)) + return "!bg-mti-orange-ultralight border-mti-orange-light"; }; interface Props { - user: User; - loggedInUser: User; - onClose: (reload?: boolean) => void; - onViewStudents?: () => void; - onViewTeachers?: () => void; - onViewCorporate?: () => void; - disabled?: boolean; - disabledFields?: { - countryManager?: boolean; - }; + user: User; + loggedInUser: User; + onClose: (reload?: boolean) => void; + onViewStudents?: () => void; + onViewTeachers?: () => void; + onViewCorporate?: () => void; + disabled?: boolean; + disabledFields?: { + countryManager?: boolean; + }; } const USER_STATUS_OPTIONS = [ - { - value: "active", - label: "Active", - }, - { - value: "disabled", - label: "Disabled", - }, - { - value: "paymentDue", - label: "Payment Due", - }, + { + value: "active", + label: "Active", + }, + { + value: "disabled", + label: "Disabled", + }, + { + value: "paymentDue", + label: "Payment Due", + }, ]; const USER_TYPE_OPTIONS = Object.keys(USER_TYPE_LABELS).map((type) => ({ - value: type, - label: USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS], + value: type, + label: USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS], })); -const CURRENCIES_OPTIONS = CURRENCIES.map(({label, currency}) => ({ - value: currency, - label, +const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({ + value: currency, + label, })); -const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, onViewCorporate, disabled = false, disabledFields = {}}: Props) => { - const [expiryDate, setExpiryDate] = useState(user.subscriptionExpirationDate); - const [type, setType] = useState(user.type); - const [status, setStatus] = useState(user.status); - const [referralAgentLabel, setReferralAgentLabel] = useState(); - const [position, setPosition] = useState(user.type === "corporate" ? user.demographicInformation?.position : undefined); - const [passport_id, setPassportID] = useState(user.type === "student" ? user.demographicInformation?.passport_id : undefined); +const UserCard = ({ + user, + loggedInUser, + onClose, + onViewStudents, + onViewTeachers, + onViewCorporate, + disabled = false, + disabledFields = {}, +}: Props) => { + const [expiryDate, setExpiryDate] = useState( + user.subscriptionExpirationDate + ); + const [type, setType] = useState(user.type); + const [status, setStatus] = useState(user.status); + const [referralAgentLabel, setReferralAgentLabel] = useState(); + const [position, setPosition] = useState( + user.type === "corporate" + ? user.demographicInformation?.position + : undefined + ); + const [passport_id, setPassportID] = useState( + user.type === "student" + ? user.demographicInformation?.passport_id + : undefined + ); - const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined); - const [companyName, setCompanyName] = useState( - user.type === "corporate" - ? user.corporateInformation?.companyInformation.name - : user.type === "agent" - ? user.agentInformation?.companyName - : undefined, - ); - const [arabName, setArabName] = useState(user.type === "agent" ? user.agentInformation?.companyArabName : undefined); - const [commercialRegistration, setCommercialRegistration] = useState( - user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, - ); - const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined); - const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined); - const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR"); - const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined); - const [commissionValue, setCommission] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.commission : undefined); - const {stats} = useStats(user.id); - const {users} = useUsers(); - const {codes} = useCodes(user.id); + const [referralAgent, setReferralAgent] = useState( + user.type === "corporate" + ? user.corporateInformation?.referralAgent + : undefined + ); + const [companyName, setCompanyName] = useState( + user.type === "corporate" + ? user.corporateInformation?.companyInformation.name + : user.type === "agent" + ? user.agentInformation?.companyName + : undefined + ); + const [arabName, setArabName] = useState( + user.type === "agent" ? user.agentInformation?.companyArabName : undefined + ); + const [commercialRegistration, setCommercialRegistration] = useState( + user.type === "agent" + ? user.agentInformation?.commercialRegistration + : undefined + ); + const [userAmount, setUserAmount] = useState( + user.type === "corporate" + ? user.corporateInformation?.companyInformation.userAmount + : undefined + ); + const [paymentValue, setPaymentValue] = useState( + user.type === "corporate" + ? user.corporateInformation?.payment?.value + : undefined + ); + const [paymentCurrency, setPaymentCurrency] = useState( + user.type === "corporate" + ? user.corporateInformation?.payment?.currency + : "EUR" + ); + const [monthlyDuration, setMonthlyDuration] = useState( + user.type === "corporate" + ? user.corporateInformation?.monthlyDuration + : undefined + ); + const [commissionValue, setCommission] = useState( + user.type === "corporate" + ? user.corporateInformation?.payment?.commission + : undefined + ); + const { stats } = useStats(user.id); + const { users } = useUsers(); + const { codes } = useCodes(user.id); - useEffect(() => { - if (users && users.length > 0) { - if (!referralAgent) { - setReferralAgentLabel("No manager"); - return; - } + useEffect(() => { + if (users && users.length > 0) { + if (!referralAgent) { + setReferralAgentLabel("No manager"); + return; + } - const agent = users.find((x) => x.id === referralAgent); - setReferralAgentLabel(`${agent?.name} - ${agent?.email}`); - } - }, [users, referralAgent]); + const agent = users.find((x) => x.id === referralAgent); + setReferralAgentLabel(`${agent?.name} - ${agent?.email}`); + } + }, [users, referralAgent]); - const updateUser = () => { - if (user.type === "corporate" && (!paymentValue || paymentValue < 0)) - return toast.error("Please set a price for the user's package before updating!"); - if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) return; + const updateUser = () => { + if (user.type === "corporate" && (!paymentValue || paymentValue < 0)) + return toast.error( + "Please set a price for the user's package before updating!" + ); + if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) + return; - axios - .post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, { - ...user, - subscriptionExpirationDate: expiryDate, - type, - status, - agentInformation: - type === "agent" - ? { - companyName, - commercialRegistration, - arabName, - } - : undefined, - corporateInformation: - type === "corporate" - ? { - referralAgent, - monthlyDuration, - companyInformation: { - name: companyName, - userAmount, - }, - payment: { - value: paymentValue, - currency: paymentCurrency, - ...(referralAgent === "" ? {} : {commission: commissionValue}), - }, - } - : undefined, - }) - .then(() => { - toast.success("User updated successfully!"); - onClose(true); - }) - .catch(() => { - toast.error("Something went wrong!", {toastId: "update-error"}); - }); - }; + axios + .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { + ...user, + subscriptionExpirationDate: expiryDate, + type, + status, + agentInformation: + type === "agent" + ? { + companyName, + commercialRegistration, + arabName, + } + : undefined, + corporateInformation: + type === "corporate" + ? { + referralAgent, + monthlyDuration, + companyInformation: { + name: companyName, + userAmount, + }, + payment: { + value: paymentValue, + currency: paymentCurrency, + ...(referralAgent === "" + ? {} + : { commission: commissionValue }), + }, + } + : undefined, + }) + .then(() => { + toast.success("User updated successfully!"); + onClose(true); + }) + .catch(() => { + toast.error("Something went wrong!", { toastId: "update-error" }); + }); + }; - const generalProfileItems = [ - { - icon: , - value: Object.keys(groupBySession(stats)).length, - label: "Exams", - }, - { - icon: , - value: stats.length, - label: "Modules", - }, - { - icon: , - value: `${stats.length > 0 ? averageScore(stats) : 0}%`, - label: "Average Score", - }, - ]; + const generalProfileItems = [ + { + icon: ( + + ), + value: Object.keys(groupBySession(stats)).length, + label: "Exams", + }, + { + icon: , + value: stats.length, + label: "Modules", + }, + { + icon: , + value: `${stats.length > 0 ? averageScore(stats) : 0}%`, + label: "Average Score", + }, + ]; - const corporateProfileItems = - user.type === "corporate" - ? [ - { - icon: , - value: codes.length, - label: "Users Used", - }, - { - icon: , - value: user.corporateInformation.companyInformation.userAmount, - label: "Number of Users", - }, - ] - : []; + const corporateProfileItems = + user.type === "corporate" + ? [ + { + icon: ( + + ), + value: codes.length, + label: "Users Used", + }, + { + icon: ( + + ), + value: user.corporateInformation.companyInformation.userAmount, + label: "Number of Users", + }, + ] + : []; - return ( - <> - + const updateUserPermission = PERMISSIONS.updateUser[user.type] as { + list: Type[]; + perm: PermissionType; + }; + return ( + <> + - {user.type === "agent" && ( - <> -
- - - -
- - - )} - {user.type === "corporate" && ( - <> -
- - setUserAmount(e ? parseInt(e) : undefined)} - placeholder="Enter number of users" - defaultValue={userAmount} - disabled={disabled} - /> - setMonthlyDuration(e ? parseInt(e) : undefined)} - placeholder="Enter monthly duration" - defaultValue={monthlyDuration} - disabled={disabled} - /> -
- -
- setPaymentValue(e ? parseInt(e) : undefined)} - type="number" - defaultValue={paymentValue || 0} - className="col-span-3" - disabled={disabled} - /> - u.type === "agent") - .map((x) => ({ - value: x.id, - label: `${x.name} - ${x.email}`, - })), - ]} - defaultValue={{ - value: referralAgent, - label: referralAgentLabel, - }} - menuPortalTarget={document?.body} - onChange={(value) => setReferralAgent(value?.value)} - styles={{ - menuPortal: (base) => ({...base, zIndex: 9999}), - control: (styles) => ({ - ...styles, - paddingLeft: "4px", - border: "none", - outline: "none", - ":focus": { - outline: "none", - }, - }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - // editing country manager should only be available for dev/admin - isDisabled={!["developer", "admin"].includes(loggedInUser.type) || disabledFields.countryManager} - /> - )} -
-
- {referralAgent !== "" && loggedInUser.type !== "corporate" ? ( - <> - - setCommission(e ? parseInt(e) : undefined)} - type="number" - defaultValue={commissionValue || 0} - className="col-span-3" - disabled={disabled || loggedInUser.type === "agent"} - /> - - ) : ( -
- )} -
-
- - - )} -
-
- null} - placeholder="Enter your name" - defaultValue={user.name} - disabled - /> - null} - placeholder="Enter email address" - defaultValue={user.email} - disabled - /> -
+ {user.type === "agent" && ( + <> +
+ + + +
+ + + )} + {user.type === "corporate" && ( + <> +
+ + setUserAmount(e ? parseInt(e) : undefined)} + placeholder="Enter number of users" + defaultValue={userAmount} + disabled={disabled} + /> + setMonthlyDuration(e ? parseInt(e) : undefined)} + placeholder="Enter monthly duration" + defaultValue={monthlyDuration} + disabled={disabled} + /> +
+ +
+ setPaymentValue(e ? parseInt(e) : undefined)} + type="number" + defaultValue={paymentValue || 0} + className="col-span-3" + disabled={disabled} + /> + u.type === "agent") + .map((x) => ({ + value: x.id, + label: `${x.name} - ${x.email}`, + })), + ]} + defaultValue={{ + value: referralAgent, + label: referralAgentLabel, + }} + menuPortalTarget={document?.body} + onChange={(value) => setReferralAgent(value?.value)} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused + ? "#D5D9F0" + : state.isSelected + ? "#7872BF" + : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + // editing country manager should only be available for dev/admin + isDisabled={ + checkAccess( + loggedInUser, + getTypesOfUser(["developer", "admin"]) + ) || disabledFields.countryManager + } + /> + )} +
+
+ {referralAgent !== "" && loggedInUser.type !== "corporate" ? ( + <> + + setCommission(e ? parseInt(e) : undefined)} + type="number" + defaultValue={commissionValue || 0} + className="col-span-3" + disabled={disabled || loggedInUser.type === "agent"} + /> + + ) : ( +
+ )} +
+
+ + + )} +
+
+ null} + placeholder="Enter your name" + defaultValue={user.name} + disabled + /> + null} + placeholder="Enter email address" + defaultValue={user.email} + disabled + /> +
-
-
- - -
- null} - placeholder="Enter phone number" - defaultValue={user.demographicInformation?.phone} - disabled - /> -
+
+
+ + +
+ null} + placeholder="Enter phone number" + defaultValue={user.demographicInformation?.phone} + disabled + /> +
- {user.type === "student" && ( - null} - placeholder="Enter National ID or Passport number" - value={user.type === "student" ? user.demographicInformation?.passport_id : undefined} - disabled - required - /> - )} + {user.type === "student" && ( + null} + placeholder="Enter National ID or Passport number" + value={ + user.type === "student" + ? user.demographicInformation?.passport_id + : undefined + } + disabled + required + /> + )} -
- {user.type !== "corporate" && ( -
- - - {EMPLOYMENT_STATUS.map(({status, label}) => ( - - {({checked}) => ( - - {label} - - )} - - ))} - -
- )} - {user.type === "corporate" && ( - - )} -
-
- - - - {({checked}) => ( - - Male - - )} - - - {({checked}) => ( - - Female - - )} - - - {({checked}) => ( - - Other - - )} - - -
-
-
- - setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)} - disabled={disabled}> - Enabled - -
- {!expiryDate && ( -
- {!expiryDate && "Unlimited"} - {expiryDate && moment(expiryDate).format("DD/MM/YYYY")} -
- )} - {expiryDate && ( - - moment(date).isAfter(new Date()) && - (loggedInUser.subscriptionExpirationDate - ? moment(date).isBefore(moment(loggedInUser.subscriptionExpirationDate)) - : true) - } - dateFormat="dd/MM/yyyy" - selected={moment(expiryDate).toDate()} - onChange={(date) => setExpiryDate(date)} - disabled={disabled} - /> - )} -
-
-
- {(loggedInUser.type === "developer" || loggedInUser.type === "admin") && ( - <> - -
-
- - o.value === type)} - onChange={(value) => setType(value?.value as typeof user.type)} - styles={{ - control: (styles) => ({ - ...styles, - paddingLeft: "4px", - border: "none", - outline: "none", - ":focus": { - outline: "none", - }, - }), - menuPortal: (base) => ({...base, zIndex: 9999}), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - isDisabled={disabled} - /> -
-
- - )} -
+
+ {user.type !== "corporate" && user.type !== "mastercorporate" && ( +
+ + + {EMPLOYMENT_STATUS.map(({ status, label }) => ( + + {({ checked }) => ( + + {label} + + )} + + ))} + +
+ )} + {user.type === "corporate" && ( + + )} +
+
+ + + + {({ checked }) => ( + + Male + + )} + + + {({ checked }) => ( + + Female + + )} + + + {({ checked }) => ( + + Other + + )} + + +
+
+
+ + + setExpiryDate( + checked + ? user.subscriptionExpirationDate || new Date() + : null + ) + } + disabled={disabled} + > + Enabled + +
+ {!expiryDate && ( +
+ {!expiryDate && "Unlimited"} + {expiryDate && moment(expiryDate).format("DD/MM/YYYY")} +
+ )} + {expiryDate && ( + + moment(date).isAfter(new Date()) && + (loggedInUser.subscriptionExpirationDate + ? moment(date).isBefore( + moment(loggedInUser.subscriptionExpirationDate) + ) + : true) + } + dateFormat="dd/MM/yyyy" + selected={moment(expiryDate).toDate()} + onChange={(date) => setExpiryDate(date)} + disabled={disabled} + /> + )} +
+
+
+ {checkAccess(loggedInUser, ["developer", "admin"]) && ( + <> + +
+
+ + o.value === type)} + onChange={(value) => + setType(value?.value as typeof user.type) + } + styles={{ + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused + ? "#D5D9F0" + : state.isSelected + ? "#7872BF" + : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + isDisabled={disabled} + /> +
+
+ + )} +
-
-
- {onViewCorporate && ["student", "teacher"].includes(user.type) && ( - - )} - {onViewStudents && ["corporate", "teacher"].includes(user.type) && ( - - )} - {onViewTeachers && ["student", "corporate"].includes(user.type) && ( - - )} -
-
- - -
-
- - ); +
+
+ {onViewCorporate && ["student", "teacher"].includes(user.type) && ( + + )} + {onViewStudents && ["corporate", "teacher"].includes(user.type) && ( + + )} + {onViewTeachers && ["student", "corporate"].includes(user.type) && ( + + )} +
+
+ + +
+
+ + ); }; export default UserCard; diff --git a/src/constants/userPermissions.ts b/src/constants/userPermissions.ts index 734e9267..af3e7543 100644 --- a/src/constants/userPermissions.ts +++ b/src/constants/userPermissions.ts @@ -1,39 +1,91 @@ -import {Type} from "@/interfaces/user"; +import { Type } from "@/interfaces/user"; export const PERMISSIONS = { - generateCode: { - student: ["corporate", "developer", "admin"], - teacher: ["corporate", "developer", "admin"], - corporate: ["admin", "developer"], - admin: ["developer", "admin"], - agent: ["developer", "admin"], - developer: ["developer"], - }, - deleteUser: { - student: ["corporate", "developer", "admin"], - teacher: ["corporate", "developer", "admin"], - corporate: ["admin", "developer"], - admin: ["developer", "admin"], - agent: ["developer", "admin"], - developer: ["developer"], - }, - updateUser: { - student: ["developer", "admin"], - teacher: ["developer", "admin"], - corporate: ["admin", "developer"], - admin: ["developer", "admin"], - agent: ["developer", "admin"], - developer: ["developer"], - }, - updateExpiryDate: { - student: ["developer", "admin"], - teacher: ["developer", "admin"], - corporate: ["admin", "developer"], - admin: ["developer", "admin"], - agent: ["developer", "admin"], - developer: ["developer"], - }, - examManagement: { - delete: ["developer", "admin"], - }, + generateCode: { + student: ["corporate", "developer", "admin", "mastercorporate"], + teacher: ["corporate", "developer", "admin", "mastercorporate"], + corporate: ["admin", "developer"], + mastercorporate: ["admin", "developer"], + + admin: ["developer", "admin"], + agent: ["developer", "admin"], + developer: ["developer"], + }, + deleteUser: { + student: { + perm: "deleteStudent", + list: ["corporate", "developer", "admin", "mastercorporate"], + }, + teacher: { + perm: "deleteTeacher", + list: ["corporate", "developer", "admin", "mastercorporate"], + }, + corporate: { + perm: "deleteCorporate", + list: ["admin", "developer"], + }, + mastercorporate: { + perm: undefined, + list: ["admin", "developer"], + }, + + admin: { + perm: "deleteAdmin", + list: ["developer", "admin"], + }, + agent: { + perm: "deleteCountryManager", + list: ["developer", "admin"], + }, + developer: { + perm: undefined, + list: ["developer"], + }, + }, + updateUser: { + student: { + perm: "editStudent", + list: ["developer", "admin"], + }, + teacher: { + perm: "editTeacher", + list: ["developer", "admin"], + }, + + corporate: { + perm: "editCorporate", + list: ["admin", "developer"], + }, + mastercorporate: { + perm: undefined, + list: ["admin", "developer"], + }, + + admin: { + perm: "editAdmin", + list: ["developer", "admin"], + }, + + agent: { + perm: "editCountryManager", + list: ["developer", "admin"], + }, + developer: { + perm: undefined, + list: ["developer"], + }, + }, + updateExpiryDate: { + student: ["developer", "admin"], + teacher: ["developer", "admin"], + corporate: ["admin", "developer"], + mastercorporate: ["admin", "developer"], + + admin: ["developer", "admin"], + agent: ["developer", "admin"], + developer: ["developer"], + }, + examManagement: { + delete: ["developer", "admin"], + }, }; diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx index 94d5e5ab..06feae46 100644 --- a/src/dashboards/Corporate.tsx +++ b/src/dashboards/Corporate.tsx @@ -35,6 +35,7 @@ import GroupList from "@/pages/(admin)/Lists/GroupList"; import useFilterStore from "@/stores/listFilterStore"; import { useRouter } from "next/router"; import useCodes from "@/hooks/useCodes"; +import { getUserCorporate } from "@/utils/groups"; interface Props { user: CorporateUser; @@ -44,6 +45,8 @@ export default function CorporateDashboard({ user }: Props) { const [page, setPage] = useState(""); const [selectedUser, setSelectedUser] = useState(); const [showModal, setShowModal] = useState(false); + const [corporateUserToShow, setCorporateUserToShow] = + useState(); const { stats } = useStats(); const { users, reload } = useUsers(); @@ -57,6 +60,11 @@ export default function CorporateDashboard({ user }: Props) { setShowModal(!!selectedUser && page === ""); }, [selectedUser, page]); + useEffect(() => { + // in this case it fetches the master corporate account + getUserCorporate(user.id).then(setCorporateUserToShow); + }, [user]); + const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id); @@ -200,6 +208,15 @@ export default function CorporateDashboard({ user }: Props) { const DefaultDashboard = () => ( <> + {corporateUserToShow && ( +
+ Linked to:{" "} + + {corporateUserToShow?.corporateInformation?.companyInformation + .name || corporateUserToShow.name} + +
+ )}
setPage("students")} diff --git a/src/dashboards/MasterCorporate.tsx b/src/dashboards/MasterCorporate.tsx new file mode 100644 index 00000000..95736597 --- /dev/null +++ b/src/dashboards/MasterCorporate.tsx @@ -0,0 +1,424 @@ +/* eslint-disable @next/next/no-img-element */ +import Modal from "@/components/Modal"; +import useStats from "@/hooks/useStats"; +import useUsers from "@/hooks/useUsers"; +import { Group, MasterCorporateUser, Stat, User } from "@/interfaces/user"; +import UserList from "@/pages/(admin)/Lists/UserList"; +import { dateSorter } from "@/utils"; +import moment from "moment"; +import { useEffect, useState } from "react"; +import { + BsArrowLeft, + BsClipboard2Data, + BsClock, + BsPaperclip, + BsPersonFill, + BsPencilSquare, + BsPersonCheck, + BsPeople, + BsBank, +} from "react-icons/bs"; +import UserCard from "@/components/UserCard"; +import useGroups from "@/hooks/useGroups"; + +import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; +import { MODULE_ARRAY } from "@/utils/moduleUtils"; +import { Module } from "@/interfaces"; +import { groupByExam } from "@/utils/stats"; +import IconCard from "./IconCard"; +import GroupList from "@/pages/(admin)/Lists/GroupList"; +import useFilterStore from "@/stores/listFilterStore"; +import { useRouter } from "next/router"; +import useCodes from "@/hooks/useCodes"; + +interface Props { + user: MasterCorporateUser; +} + +export default function MasterCorporateDashboard({ user }: Props) { + const [page, setPage] = useState(""); + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); + + const { stats } = useStats(); + const { users, reload } = useUsers(); + const { codes } = useCodes(user.id); + const { groups } = useGroups(user.id, user.type); + + const masterCorporateUserGroups = [ + ...new Set( + groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants) + ), + ]; + const corporateUserGroups = [ + ...new Set(groups.flatMap((g) => g.participants)), + ]; + + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); + const router = useRouter(); + + useEffect(() => { + setShowModal(!!selectedUser && page === ""); + }, [selectedUser, page]); + + const studentFilter = (user: User) => + user.type === "student" && corporateUserGroups.includes(user.id); + const teacherFilter = (user: User) => + user.type === "teacher" && corporateUserGroups.includes(user.id); + + const getStatsByStudent = (user: User) => + stats.filter((s) => s.user === user.id); + + const UserDisplay = (displayUser: User) => ( +
setSelectedUser(displayUser)} + className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" + > + {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + const StudentsList = () => { + const filter = (x: User) => + x.type === "student" && + (!!selectedUser + ? corporateUserGroups.includes(x.id) || false + : corporateUserGroups.includes(x.id)); + + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Students ({total})

+
+ )} + /> + ); + }; + + const TeachersList = () => { + const filter = (x: User) => + x.type === "teacher" && + (!!selectedUser + ? corporateUserGroups.includes(x.id) || false + : corporateUserGroups.includes(x.id)); + + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Teachers ({total})

+
+ )} + /> + ); + }; + + const corporateUserFilter = (x: User) => + x.type === "corporate" && + (!!selectedUser + ? masterCorporateUserGroups.includes(x.id) || false + : masterCorporateUserGroups.includes(x.id)); + + const CorporateList = () => { + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Corporates ({total})

+
+ )} + /> + ); + }; + + const GroupsList = () => { + return ( + <> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

+ Groups ({groups.length}) +

+
+ + + + ); + }; + + const averageLevelCalculator = (studentStats: Stat[]) => { + const formattedStats = studentStats + .map((s) => ({ + focus: users.find((u) => u.id === s.user)?.focus, + score: s.score, + module: s.module, + })) + .filter((f) => !!f.focus); + const bandScores = formattedStats.map((s) => ({ + module: s.module, + level: calculateBandScore( + s.score.correct, + s.score.total, + s.module, + s.focus! + ), + })); + + const levels: { [key in Module]: number } = { + reading: 0, + listening: 0, + writing: 0, + speaking: 0, + level: 0, + }; + bandScores.forEach((b) => (levels[b.module] += b.level)); + + return calculateAverageLevel(levels); + }; + + const DefaultDashboard = () => ( + <> +
+ setPage("students")} + Icon={BsPersonFill} + label="Students" + value={users.filter(studentFilter).length} + color="purple" + /> + setPage("teachers")} + Icon={BsPencilSquare} + label="Teachers" + value={users.filter(teacherFilter).length} + color="purple" + /> + + groups.flatMap((g) => g.participants).includes(s.user) + ).length + } + color="purple" + /> + + groups.flatMap((g) => g.participants).includes(s.user) + ) + ).toFixed(1)} + color="purple" + /> + setPage("groups")} + Icon={BsPeople} + label="Groups" + value={groups.length} + color="purple" + /> + + + setPage("corporate")} + /> +
+ +
+
+ Latest students +
+ {users + .filter(studentFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {users + .filter(teacherFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {users + .filter(studentFilter) + .sort( + (a, b) => + calculateAverageLevel(b.levels) - + calculateAverageLevel(a.levels) + ) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {users + .filter(studentFilter) + .sort( + (a, b) => + Object.keys(groupByExam(getStatsByStudent(b))).length - + Object.keys(groupByExam(getStatsByStudent(a))).length + ) + .map((x) => ( + + ))} +
+
+
+ + ); + + return ( + <> + setSelectedUser(undefined)}> + <> + {selectedUser && ( +
+ { + setSelectedUser(undefined); + if (shouldReload) reload(); + }} + onViewStudents={ + selectedUser.type === "corporate" || + selectedUser.type === "teacher" + ? () => { + appendUserFilters({ + id: "view-students", + filter: (x: User) => x.type === "student", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter( + (g) => + g.admin === selectedUser.id || + g.participants.includes(selectedUser.id) + ) + .flatMap((g) => g.participants) + .includes(x.id), + }); + + router.push("/list/users"); + } + : undefined + } + onViewTeachers={ + selectedUser.type === "corporate" || + selectedUser.type === "student" + ? () => { + appendUserFilters({ + id: "view-teachers", + filter: (x: User) => x.type === "teacher", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter( + (g) => + g.admin === selectedUser.id || + g.participants.includes(selectedUser.id) + ) + .flatMap((g) => g.participants) + .includes(x.id), + }); + + router.push("/list/users"); + } + : undefined + } + user={selectedUser} + /> +
+ )} + +
+ {page === "students" && } + {page === "teachers" && } + {page === "groups" && } + {page === "corporate" && } + {page === "" && } + + ); +} diff --git a/src/hooks/useGroups.tsx b/src/hooks/useGroups.tsx index 8d3bbc9d..a4d39dc7 100644 --- a/src/hooks/useGroups.tsx +++ b/src/hooks/useGroups.tsx @@ -2,16 +2,23 @@ import {Group, User} from "@/interfaces/user"; import axios from "axios"; import {useEffect, useState} from "react"; -export default function useGroups(admin?: string) { +export default function useGroups(admin?: string, userType?: string) { const [groups, setGroups] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); + const isMasterType = userType?.startsWith('master'); + const getData = () => { setIsLoading(true); + + const url = admin ? `/api/groups?admin=${admin}` : "/api/groups"; axios - .get("/api/groups") + .get(url) .then((response) => { + if(isMasterType) { + return setGroups(response.data); + } const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || ""); const filteredGroups = admin ? response.data.filter(filter) : response.data; @@ -20,7 +27,7 @@ export default function useGroups(admin?: string) { .finally(() => setIsLoading(false)); }; - useEffect(getData, [admin]); + useEffect(getData, [admin, isMasterType]); return {groups, isLoading, isError, reload: getData}; } diff --git a/src/interfaces/permissions.ts b/src/interfaces/permissions.ts new file mode 100644 index 00000000..2e448af5 --- /dev/null +++ b/src/interfaces/permissions.ts @@ -0,0 +1,49 @@ +export const markets = ["au", "br", "de"] as const; + +export const permissions = [ + // generate codes are basicly invites + "createCodeStudent", + "createCodeTeacher", + "createCodeCorporate", + "createCodeCountryManager", + "createCodeAdmin", + // exams + "createReadingExam", + "createListeningExam", + "createWritingExam", + "createSpeakingExam", + "createLevelExam", + // view pages + "viewExams", + "viewExercises", + "viewRecords", + "viewStats", + "viewTickets", + "viewPaymentRecords", + // view data + "viewStudent", + "viewTeacher", + "viewCorporate", + "viewCountryManager", + "viewAdmin", + // edit data + "editStudent", + "editTeacher", + "editCorporate", + "editCountryManager", + "editAdmin", + // delete data + "deleteStudent", + "deleteTeacher", + "deleteCorporate", + "deleteCountryManager", + "deleteAdmin", +] as const; + +export type PermissionType = (typeof permissions)[keyof typeof permissions]; + +export interface Permission { + id: string; + type: PermissionType; + users: string[]; +} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 85b5d9fa..ddda1ccb 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,152 +1,189 @@ -import {Module} from "."; -import {InstructorGender} from "./exam"; +import { Module } from "."; +import { InstructorGender } from "./exam"; +import { PermissionType } from "./permissions"; -export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser; +export type User = + | StudentUser + | TeacherUser + | CorporateUser + | AgentUser + | AdminUser + | DeveloperUser + | MasterCorporateUser; export type UserStatus = "active" | "disabled" | "paymentDue"; export interface BasicUser { - email: string; - name: string; - profilePicture: string; - id: string; - isFirstLogin: boolean; - focus: "academic" | "general"; - levels: {[key in Module]: number}; - desiredLevels: {[key in Module]: number}; - type: Type; - bio: string; - isVerified: boolean; - subscriptionExpirationDate?: null | Date; - registrationDate?: Date; - status: UserStatus; + email: string; + name: string; + profilePicture: string; + id: string; + isFirstLogin: boolean; + focus: "academic" | "general"; + levels: { [key in Module]: number }; + desiredLevels: { [key in Module]: number }; + type: Type; + bio: string; + isVerified: boolean; + subscriptionExpirationDate?: null | Date; + registrationDate?: Date; + status: UserStatus; + permissions: PermissionType[], } export interface StudentUser extends BasicUser { - type: "student"; - preferredGender?: InstructorGender; - demographicInformation?: DemographicInformation; - preferredTopics?: string[]; + type: "student"; + preferredGender?: InstructorGender; + demographicInformation?: DemographicInformation; + preferredTopics?: string[]; } export interface TeacherUser extends BasicUser { - type: "teacher"; - demographicInformation?: DemographicInformation; + type: "teacher"; + demographicInformation?: DemographicInformation; } export interface CorporateUser extends BasicUser { - type: "corporate"; - corporateInformation: CorporateInformation; - demographicInformation?: DemographicCorporateInformation; + type: "corporate"; + corporateInformation: CorporateInformation; + demographicInformation?: DemographicCorporateInformation; +} + +export interface MasterCorporateUser extends BasicUser { + type: "mastercorporate"; + corporateInformation: CorporateInformation; + demographicInformation?: DemographicCorporateInformation; } export interface AgentUser extends BasicUser { - type: "agent"; - agentInformation: AgentInformation; - demographicInformation?: DemographicInformation; + type: "agent"; + agentInformation: AgentInformation; + demographicInformation?: DemographicInformation; } export interface AdminUser extends BasicUser { - type: "admin"; - demographicInformation?: DemographicInformation; + type: "admin"; + demographicInformation?: DemographicInformation; } export interface DeveloperUser extends BasicUser { - type: "developer"; - preferredGender?: InstructorGender; - demographicInformation?: DemographicInformation; - preferredTopics?: string[]; + type: "developer"; + preferredGender?: InstructorGender; + demographicInformation?: DemographicInformation; + preferredTopics?: string[]; } export interface CorporateInformation { - companyInformation: CompanyInformation; - monthlyDuration: number; - payment?: { - value: number; - currency: string; - commission: number; - }; - referralAgent?: string; + companyInformation: CompanyInformation; + monthlyDuration: number; + payment?: { + value: number; + currency: string; + commission: number; + }; + referralAgent?: string; } export interface AgentInformation { - companyName: string; - commercialRegistration: string; - companyArabName?: string; + companyName: string; + commercialRegistration: string; + companyArabName?: string; } export interface CompanyInformation { - name: string; - userAmount: number; + name: string; + userAmount: number; } export interface DemographicInformation { - country: string; - phone: string; - gender: Gender; - employment: EmploymentStatus; - passport_id?: string; - timezone?: string; + country: string; + phone: string; + gender: Gender; + employment: EmploymentStatus; + passport_id?: string; + timezone?: string; } export interface DemographicCorporateInformation { - country: string; - phone: string; - gender: Gender; - position: string; - timezone?: string; + country: string; + phone: string; + gender: Gender; + position: string; + timezone?: string; } export type Gender = "male" | "female" | "other"; -export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other"; -export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [ - {status: "student", label: "Student"}, - {status: "employed", label: "Employed"}, - {status: "unemployed", label: "Unemployed"}, - {status: "self-employed", label: "Self-employed"}, - {status: "retired", label: "Retired"}, - {status: "other", label: "Other"}, -]; +export type EmploymentStatus = + | "employed" + | "student" + | "self-employed" + | "unemployed" + | "retired" + | "other"; +export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] = + [ + { status: "student", label: "Student" }, + { status: "employed", label: "Employed" }, + { status: "unemployed", label: "Unemployed" }, + { status: "self-employed", label: "Self-employed" }, + { status: "retired", label: "Retired" }, + { status: "other", label: "Other" }, + ]; export interface Stat { - id: string; - user: string; - exam: string; - exercise: string; - session: string; - date: number; - module: Module; - solutions: any[]; - type: string; - timeSpent?: number; - inactivity?: number; - assignment?: string; - score: { - correct: number; - total: number; - missing: number; - }; - isDisabled?: boolean; + id: string; + user: string; + exam: string; + exercise: string; + session: string; + date: number; + module: Module; + solutions: any[]; + type: string; + timeSpent?: number; + inactivity?: number; + assignment?: string; + score: { + correct: number; + total: number; + missing: number; + }; + isDisabled?: boolean; } export interface Group { - admin: string; - name: string; - participants: string[]; - id: string; - disableEditing?: boolean; + admin: string; + name: string; + participants: string[]; + id: string; + disableEditing?: boolean; } export interface Code { - code: string; - creator: string; - expiryDate: Date; - type: Type; - creationDate?: string; - userId?: string; - email?: string; - name?: string; - passport_id?: string; + code: string; + creator: string; + expiryDate: Date; + type: Type; + creationDate?: string; + userId?: string; + email?: string; + name?: string; + passport_id?: string; } -export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent"; -export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent"]; +export type Type = + | "student" + | "teacher" + | "corporate" + | "admin" + | "developer" + | "agent" + | "mastercorporate"; +export const userTypes: Type[] = [ + "student", + "teacher", + "corporate", + "admin", + "developer", + "agent", + "mastercorporate", +]; diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index 8c6a4c87..7a0d5f52 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -1,248 +1,366 @@ import Button from "@/components/Low/Button"; import Checkbox from "@/components/Low/Checkbox"; -import {PERMISSIONS} from "@/constants/userPermissions"; +import { PERMISSIONS } from "@/constants/userPermissions"; import useUsers from "@/hooks/useUsers"; -import {Type, User} from "@/interfaces/user"; -import {USER_TYPE_LABELS} from "@/resources/user"; +import { Type, User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; import axios from "axios"; import clsx from "clsx"; -import {capitalize, uniqBy} from "lodash"; +import { capitalize, uniqBy } from "lodash"; import moment from "moment"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {toast} from "react-toastify"; +import { toast } from "react-toastify"; import ShortUniqueId from "short-unique-id"; -import {useFilePicker} from "use-file-picker"; +import { useFilePicker } from "use-file-picker"; import readXlsxFile from "read-excel-file"; import Modal from "@/components/Modal"; -import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs"; +import { BsFileEarmarkEaselFill, BsQuestionCircleFill } from "react-icons/bs"; +import { checkAccess } from "@/utils/permissions"; +import { PermissionType } from "@/interfaces/permissions"; +const EMAIL_REGEX = new RegExp( + /^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/ +); -const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); - -const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = { - student: [], - teacher: [], - agent: [], - corporate: ["student", "teacher"], - admin: ["student", "teacher", "agent", "corporate", "admin"], - developer: ["student", "teacher", "agent", "corporate", "admin", "developer"], +const USER_TYPE_PERMISSIONS: { + [key in Type]: { perm: PermissionType | undefined; list: Type[] }; +} = { + student: { + perm: "createCodeStudent", + list: [], + }, + teacher: { + perm: "createCodeTeacher", + list: [], + }, + agent: { + perm: "createCodeCountryManager", + list: [], + }, + corporate: { + perm: "createCodeCorporate", + list: ["student", "teacher"], + }, + mastercorporate: { + perm: undefined, + list: ["student", "teacher", "corporate"], + }, + admin: { + perm: "createCodeAdmin", + list: [ + "student", + "teacher", + "agent", + "corporate", + "admin", + "mastercorporate", + ], + }, + developer: { + perm: undefined, + list: [ + "student", + "teacher", + "agent", + "corporate", + "admin", + "developer", + "mastercorporate", + ], + }, }; -export default function BatchCodeGenerator({user}: {user: User}) { - const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]); - const [isLoading, setIsLoading] = useState(false); - const [expiryDate, setExpiryDate] = useState( - user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null, - ); - const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); - const [type, setType] = useState("student"); - const [showHelp, setShowHelp] = useState(false); +export default function BatchCodeGenerator({ user }: { user: User }) { + const [infos, setInfos] = useState< + { email: string; name: string; passport_id: string }[] + >([]); + const [isLoading, setIsLoading] = useState(false); + const [expiryDate, setExpiryDate] = useState( + user?.subscriptionExpirationDate + ? moment(user.subscriptionExpirationDate).toDate() + : null + ); + const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); + const [type, setType] = useState("student"); + const [showHelp, setShowHelp] = useState(false); - const {users} = useUsers(); + const { users } = useUsers(); - const {openFilePicker, filesContent, clear} = useFilePicker({ - accept: ".xlsx", - multiple: false, - readAs: "ArrayBuffer", - }); + const { openFilePicker, filesContent, clear } = useFilePicker({ + accept: ".xlsx", + multiple: false, + readAs: "ArrayBuffer", + }); - useEffect(() => { - if (!isExpiryDateEnabled) setExpiryDate(null); - }, [isExpiryDateEnabled]); + useEffect(() => { + if (!isExpiryDateEnabled) setExpiryDate(null); + }, [isExpiryDateEnabled]); - useEffect(() => { - if (filesContent.length > 0) { - const file = filesContent[0]; - readXlsxFile(file.content).then((rows) => { - try { - const information = uniqBy( - rows - .map((row) => { - const [firstName, lastName, country, passport_id, email, ...phone] = row as string[]; - return EMAIL_REGEX.test(email.toString().trim()) - ? { - email: email.toString().trim().toLowerCase(), - name: `${firstName ?? ""} ${lastName ?? ""}`.trim(), - passport_id: passport_id?.toString().trim() || undefined, - } - : undefined; - }) - .filter((x) => !!x) as typeof infos, - (x) => x.email, - ); + useEffect(() => { + if (filesContent.length > 0) { + const file = filesContent[0]; + readXlsxFile(file.content).then((rows) => { + try { + const information = uniqBy( + rows + .map((row) => { + const [ + firstName, + lastName, + country, + passport_id, + email, + ...phone + ] = row as string[]; + return EMAIL_REGEX.test(email.toString().trim()) + ? { + email: email.toString().trim().toLowerCase(), + name: `${firstName ?? ""} ${lastName ?? ""}`.trim(), + passport_id: passport_id?.toString().trim() || undefined, + } + : undefined; + }) + .filter((x) => !!x) as typeof infos, + (x) => x.email + ); - if (information.length === 0) { - toast.error( - "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!", - ); - return clear(); - } + if (information.length === 0) { + toast.error( + "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!" + ); + return clear(); + } - setInfos(information); - } catch { - toast.error( - "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!", - ); - return clear(); - } - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filesContent]); + setInfos(information); + } catch { + toast.error( + "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!" + ); + return clear(); + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filesContent]); - const generateAndInvite = async () => { - const newUsers = infos.filter((x) => !users.map((u) => u.email).includes(x.email)); - const existingUsers = infos - .filter((x) => users.map((u) => u.email).includes(x.email)) - .map((i) => users.find((u) => u.email === i.email)) - .filter((x) => !!x && x.type === "student") as User[]; + const generateAndInvite = async () => { + const newUsers = infos.filter( + (x) => !users.map((u) => u.email).includes(x.email) + ); + const existingUsers = infos + .filter((x) => users.map((u) => u.email).includes(x.email)) + .map((i) => users.find((u) => u.email === i.email)) + .filter((x) => !!x && x.type === "student") as User[]; - const newUsersSentence = newUsers.length > 0 ? `generate ${newUsers.length} code(s)` : undefined; - const existingUsersSentence = existingUsers.length > 0 ? `invite ${existingUsers.length} registered student(s)` : undefined; - if ( - !confirm( - `You are about to ${[newUsersSentence, existingUsersSentence].filter((x) => !!x).join(" and ")}, are you sure you want to continue?`, - ) - ) - return; + const newUsersSentence = + newUsers.length > 0 ? `generate ${newUsers.length} code(s)` : undefined; + const existingUsersSentence = + existingUsers.length > 0 + ? `invite ${existingUsers.length} registered student(s)` + : undefined; + if ( + !confirm( + `You are about to ${[newUsersSentence, existingUsersSentence] + .filter((x) => !!x) + .join(" and ")}, are you sure you want to continue?` + ) + ) + return; - setIsLoading(true); - Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, {to: u.id, from: user.id}))) - .then(() => toast.success(`Successfully invited ${existingUsers.length} registered student(s)!`)) - .finally(() => { - if (newUsers.length === 0) setIsLoading(false); - }); + setIsLoading(true); + Promise.all( + existingUsers.map( + async (u) => + await axios.post(`/api/invites`, { to: u.id, from: user.id }) + ) + ) + .then(() => + toast.success( + `Successfully invited ${existingUsers.length} registered student(s)!` + ) + ) + .finally(() => { + if (newUsers.length === 0) setIsLoading(false); + }); - if (newUsers.length > 0) generateCode(type, newUsers); - setInfos([]); - }; + if (newUsers.length > 0) generateCode(type, newUsers); + setInfos([]); + }; - const generateCode = (type: Type, informations: typeof infos) => { - const uid = new ShortUniqueId(); - const codes = informations.map(() => uid.randomUUID(6)); + const generateCode = (type: Type, informations: typeof infos) => { + const uid = new ShortUniqueId(); + const codes = informations.map(() => uid.randomUUID(6)); - setIsLoading(true); - axios - .post<{ok: boolean; valid?: number; reason?: string}>("/api/code", { - type, - codes, - infos: informations, - expiryDate, - }) - .then(({data, status}) => { - if (data.ok) { - toast.success( - `Successfully generated${data.valid ? ` ${data.valid}/${informations.length}` : ""} ${capitalize( - type, - )} codes and they have been notified by e-mail!`, - {toastId: "success"}, - ); - return; - } + setIsLoading(true); + axios + .post<{ ok: boolean; valid?: number; reason?: string }>("/api/code", { + type, + codes, + infos: informations, + expiryDate, + }) + .then(({ data, status }) => { + if (data.ok) { + toast.success( + `Successfully generated${ + data.valid ? ` ${data.valid}/${informations.length}` : "" + } ${capitalize(type)} codes and they have been notified by e-mail!`, + { toastId: "success" } + ); + return; + } - if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); - } - }) - .catch(({response: {status, data}}) => { - if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); - return; - } + if (status === 403) { + toast.error(data.reason, { toastId: "forbidden" }); + } + }) + .catch(({ response: { status, data } }) => { + if (status === 403) { + toast.error(data.reason, { toastId: "forbidden" }); + return; + } - toast.error(`Something went wrong, please try again later!`, { - toastId: "error", - }); - }) - .finally(() => { - setIsLoading(false); - return clear(); - }); - }; + toast.error(`Something went wrong, please try again later!`, { + toastId: "error", + }); + }) + .finally(() => { + setIsLoading(false); + return clear(); + }); + }; - return ( - <> - setShowHelp(false)} title="Excel File Format"> -
- Please upload an Excel file with the following format: - - - - - - - - - - - -
First NameLast NameCountryPassport/National IDE-mailPhone Number
- - Notes: -
    -
  • - All incorrect e-mails will be ignored;
  • -
  • - All already registered e-mails will be ignored;
  • -
  • - You may have a header row with the format above, however, it is not necessary;
  • -
  • - All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.
  • -
-
-
-
-
-
- -
setShowHelp(true)}> - -
-
- - {user && (user.type === "developer" || user.type === "admin" || user.type === "corporate") && ( - <> -
- - - Enabled - -
- {isExpiryDateEnabled && ( - - moment(date).isAfter(new Date()) && - (user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true) - } - dateFormat="dd/MM/yyyy" - selected={expiryDate} - onChange={(date) => setExpiryDate(date)} - /> - )} - - )} - - {user && ( - - )} - -
- - ); + return ( + <> + setShowHelp(false)} + title="Excel File Format" + > +
+ Please upload an Excel file with the following format: + + + + + + + + + + + +
+ First Name + + Last Name + Country + Passport/National ID + E-mail + Phone Number +
+ + Notes: +
    +
  • - All incorrect e-mails will be ignored;
  • +
  • - All already registered e-mails will be ignored;
  • +
  • + - You may have a header row with the format above, however, it + is not necessary; +
  • +
  • + - All of the e-mails in the file will receive an e-mail to join + EnCoach with the role selected below. +
  • +
+
+
+
+
+
+ +
setShowHelp(true)} + > + +
+
+ + {user && + checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && ( + <> +
+ + + Enabled + +
+ {isExpiryDateEnabled && ( + + moment(date).isAfter(new Date()) && + (user.subscriptionExpirationDate + ? moment(date).isBefore(user.subscriptionExpirationDate) + : true) + } + dateFormat="dd/MM/yyyy" + selected={expiryDate} + onChange={(date) => setExpiryDate(date)} + /> + )} + + )} + + {user && ( + + )} + +
+ + ); } diff --git a/src/pages/(admin)/CodeGenerator.tsx b/src/pages/(admin)/CodeGenerator.tsx index 50774f53..30c5007a 100644 --- a/src/pages/(admin)/CodeGenerator.tsx +++ b/src/pages/(admin)/CodeGenerator.tsx @@ -1,125 +1,197 @@ import Button from "@/components/Low/Button"; import Checkbox from "@/components/Low/Checkbox"; -import {PERMISSIONS} from "@/constants/userPermissions"; -import {Type, User} from "@/interfaces/user"; -import {USER_TYPE_LABELS} from "@/resources/user"; +import { PERMISSIONS } from "@/constants/userPermissions"; +import { Type, User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; +import { capitalize } from "lodash"; import moment from "moment"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {toast} from "react-toastify"; +import { toast } from "react-toastify"; import ShortUniqueId from "short-unique-id"; +import { checkAccess } from "@/utils/permissions"; +import { PermissionType } from "@/interfaces/permissions"; -const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = { - student: [], - teacher: [], - agent: [], - corporate: ["student", "teacher"], - admin: ["student", "teacher", "agent", "corporate", "admin"], - developer: ["student", "teacher", "agent", "corporate", "admin", "developer"], +const USER_TYPE_PERMISSIONS: { + [key in Type]: { perm: PermissionType | undefined; list: Type[] }; +} = { + student: { + perm: "createCodeStudent", + list: [], + }, + teacher: { + perm: "createCodeTeacher", + list: [], + }, + agent: { + perm: "createCodeCountryManager", + list: [], + }, + corporate: { + perm: "createCodeCorporate", + list: ["student", "teacher"], + }, + mastercorporate: { + perm: undefined, + list: ["student", "teacher", "corporate"], + }, + admin: { + perm: "createCodeAdmin", + list: [ + "student", + "teacher", + "agent", + "corporate", + "admin", + "mastercorporate", + ], + }, + developer: { + perm: undefined, + list: [ + "student", + "teacher", + "agent", + "corporate", + "admin", + "developer", + "mastercorporate", + ], + }, }; -export default function CodeGenerator({user}: {user: User}) { - const [generatedCode, setGeneratedCode] = useState(); - const [expiryDate, setExpiryDate] = useState( - user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null, - ); - const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); - const [type, setType] = useState("student"); +export default function CodeGenerator({ user }: { user: User }) { + const [generatedCode, setGeneratedCode] = useState(); + const [expiryDate, setExpiryDate] = useState( + user?.subscriptionExpirationDate + ? moment(user.subscriptionExpirationDate).toDate() + : null + ); + const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); + const [type, setType] = useState("student"); - useEffect(() => { - if (!isExpiryDateEnabled) setExpiryDate(null); - }, [isExpiryDateEnabled]); + useEffect(() => { + if (!isExpiryDateEnabled) setExpiryDate(null); + }, [isExpiryDateEnabled]); - const generateCode = (type: Type) => { - const uid = new ShortUniqueId(); - const code = uid.randomUUID(6); + const generateCode = (type: Type) => { + const uid = new ShortUniqueId(); + const code = uid.randomUUID(6); - axios - .post("/api/code", {type, codes: [code], expiryDate}) - .then(({data, status}) => { - if (data.ok) { - toast.success(`Successfully generated a ${capitalize(type)} code!`, {toastId: "success"}); - setGeneratedCode(code); - return; - } + axios + .post("/api/code", { type, codes: [code], expiryDate }) + .then(({ data, status }) => { + if (data.ok) { + toast.success(`Successfully generated a ${capitalize(type)} code!`, { + toastId: "success", + }); + setGeneratedCode(code); + return; + } - if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); - } - }) - .catch(({response: {status, data}}) => { - if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); - return; - } + if (status === 403) { + toast.error(data.reason, { toastId: "forbidden" }); + } + }) + .catch(({ response: { status, data } }) => { + if (status === 403) { + toast.error(data.reason, { toastId: "forbidden" }); + return; + } - toast.error(`Something went wrong, please try again later!`, {toastId: "error"}); - }); - }; + toast.error(`Something went wrong, please try again later!`, { + toastId: "error", + }); + }); + }; - return ( -
- - {user && ( - - )} - {user && (user.type === "developer" || user.type === "admin" || user.type === "corporate") && ( - <> -
- - - Enabled - -
- {isExpiryDateEnabled && ( - - moment(date).isAfter(new Date()) && - (user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true) - } - dateFormat="dd/MM/yyyy" - selected={expiryDate} - onChange={(date) => setExpiryDate(date)} - /> - )} - - )} - - -
{ - if (generatedCode) navigator.clipboard.writeText(generatedCode); - }}> - {generatedCode} -
- {generatedCode && Give this code to the user to complete their registration} -
- ); + return ( +
+ + {user && ( + + )} + {user && + checkAccess(user, ["developer", "admin", "corporate"]) && ( + <> +
+ + + Enabled + +
+ {isExpiryDateEnabled && ( + + moment(date).isAfter(new Date()) && + (user.subscriptionExpirationDate + ? moment(date).isBefore(user.subscriptionExpirationDate) + : true) + } + dateFormat="dd/MM/yyyy" + selected={expiryDate} + onChange={(date) => setExpiryDate(date)} + /> + )} + + )} + + +
{ + if (generatedCode) navigator.clipboard.writeText(generatedCode); + }} + > + {generatedCode} +
+ {generatedCode && ( + + Give this code to the user to complete their registration + + )} +
+ ); } diff --git a/src/pages/(admin)/Lists/GroupList.tsx b/src/pages/(admin)/Lists/GroupList.tsx index 5e28543d..ae36a5cd 100644 --- a/src/pages/(admin)/Lists/GroupList.tsx +++ b/src/pages/(admin)/Lists/GroupList.tsx @@ -86,7 +86,7 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => { const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); const filteredUsers = emailUsers.filter( (x) => - ((user.type === "developer" || user.type === "admin" || user.type === "corporate") && + ((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") && (x?.type === "student" || x?.type === "teacher")) || (user.type === "teacher" && x?.type === "student"), ); @@ -189,7 +189,7 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => { ); }; -const filterTypes = ["corporate", "teacher"]; +const filterTypes = ["corporate", "teacher", "mastercorporate"]; export default function GroupList({user}: {user: User}) { const [isCreating, setIsCreating] = useState(false); @@ -197,10 +197,10 @@ export default function GroupList({user}: {user: User}) { const [filterByUser, setFilterByUser] = useState(false); const {users} = useUsers(); - const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined); + const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined, user?.type); useEffect(() => { - if (user && (user.type === "corporate" || user.type === "teacher")) { + if (user && (['corporate', 'teacher', 'mastercorporate'].includes(user.type))) { setFilterByUser(true); } }, [user]); diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 23b941a5..430bd542 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -1,650 +1,966 @@ 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} 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 } 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 { 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"; const columnHelper = createColumnHelper(); -const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; +const searchFields = [ + ["name"], + ["email"], + ["corporateInformation", "companyInformation", "name"], +]; -const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => { - const [companyName, setCompanyName] = useState(""); - const [isLoading, setIsLoading] = useState(false); +const CompanyNameCell = ({ + users, + user, + groups, +}: { + user: User; + users: User[]; + groups: Group[]; +}) => { + const [companyName, setCompanyName] = useState(""); + const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - const name = getUserCompanyName(user, users, groups); - setCompanyName(name); - }, [user, users, groups]); + useEffect(() => { + const name = getUserCompanyName(user, users, groups); + setCompanyName(name); + }, [user, users, groups]); - return isLoading ? Loading... : <>{companyName}; + return isLoading ? ( + Loading... + ) : ( + <>{companyName} + ); }; export default function UserList({ - user, - filters = [], - renderHeader, + user, + filters = [], + renderHeader, }: { - user: User; - filters?: ((user: User) => boolean)[]; - renderHeader?: (total: number) => JSX.Element; + user: User; + filters?: ((user: User) => boolean)[]; + renderHeader?: (total: number) => JSX.Element; }) { - const [showDemographicInformation, setShowDemographicInformation] = useState(false); - const [sorter, setSorter] = useState(); - const [displayUsers, setDisplayUsers] = useState([]); - const [selectedUser, setSelectedUser] = useState(); + const [showDemographicInformation, setShowDemographicInformation] = + useState(false); + const [sorter, setSorter] = useState(); + const [displayUsers, setDisplayUsers] = useState([]); + const [selectedUser, setSelectedUser] = useState(); - const {users, reload} = useUsers(); - const {groups} = useGroups(user && (user?.type === "corporate" || user?.type === "teacher") ? user.id : undefined); + const { users, reload } = useUsers(); + const { groups } = useGroups( + user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) + ? user.id + : undefined + ); - const appendUserFilters = useFilterStore((state) => state.appendUserFilter); - const router = useRouter(); + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); + const router = useRouter(); - const expirationDateColor = (date: Date) => { - const momentDate = moment(date); - const today = moment(new Date()); + 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"; - }; + 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"; + }; - useEffect(() => { - (async () => { - if (user && users) { - const filterUsers = - user.type === "corporate" || user.type === "teacher" - ? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id)) - : users; + useEffect(() => { + (async () => { + if (user && users) { + const filterUsers = + user.type === "corporate" || user.type === "teacher" + ? users.filter((u) => + groups.flatMap((g) => g.participants).includes(u.id) + ) + : users; - const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers); - const sortedUsers = await asyncSorter(filteredUsers, sortFunction); + const filteredUsers = filters.reduce( + (d, f) => d.filter(f), + filterUsers + ); + const sortedUsers = await asyncSorter( + filteredUsers, + sortFunction + ); - setDisplayUsers([...sortedUsers]); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user, users, sorter, groups]); + setDisplayUsers([...sortedUsers]); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user, users, sorter, groups]); - const deleteAccount = (user: User) => { - if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return; + 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); - }; + 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 updateAccountType = (user: User, type: Type) => { - if (!confirm(`Are you sure you want to update ${user.name}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`)) return; + const updateAccountType = (user: User, type: Type) => { + if ( + !confirm( + `Are you sure you want to update ${ + user.name + }'s account from ${capitalize(user.type)} to ${capitalize(type)}?` + ) + ) + return; - axios - .post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, { - ...user, - type, - }) - .then(() => { - toast.success("User type updated successfully!"); - reload(); - }) - .catch(() => { - toast.error("Something went wrong!", {toastId: "update-error"}); - }); - }; + axios + .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { + ...user, + type, + }) + .then(() => { + toast.success("User type updated successfully!"); + reload(); + }) + .catch(() => { + toast.error("Something went wrong!", { toastId: "update-error" }); + }); + }; - 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 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; + 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"}); - }); - }; + 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 SorterArrow = ({name}: {name: string}) => { - if (sorter === name) return ; - if (sorter === reverseString(name)) return ; + const SorterArrow = ({ name }: { name: string }) => { + if (sorter === name) return ; + if (sorter === reverseString(name)) return ; - return ; - }; + return ; + }; - const actionColumn = ({row}: {row: {original: User}}) => { - return ( -
- {PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && ( - - -
- -
-
- - -
- - - - -
-
-
-
- )} - {!row.original.isVerified && PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && ( -
verifyAccount(row.original)}> - -
- )} - {PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && ( -
toggleDisableAccount(row.original)}> - {row.original.status === "disabled" ? ( - - ) : ( - - )} -
- )} - {PERMISSIONS.deleteUser[row.original.type]?.includes(user.type) && ( -
deleteAccount(row.original)}> - -
- )} -
- ); - }; + const actionColumn = ({ row }: { row: { original: User } }) => { + const updateUserPermission = PERMISSIONS.updateUser[row.original.type] as { + list: Type[]; + perm: PermissionType; + }; + const deleteUserPermission = PERMISSIONS.deleteUser[row.original.type] as { + list: Type[]; + perm: PermissionType; + }; + return ( +
+ {checkAccess( + user, + updateUserPermission.list, + updateUserPermission.perm + ) && ( + + +
+ +
+
+ + +
+ + + + +
+
+
+
+ )} + {!row.original.isVerified && + checkAccess( + user, + updateUserPermission.list, + updateUserPermission.perm + ) && ( +
verifyAccount(row.original)} + > + +
+ )} + {checkAccess( + user, + updateUserPermission.list, + updateUserPermission.perm + ) && ( +
toggleDisableAccount(row.original)} + > + {row.original.status === "disabled" ? ( + + ) : ( + + )} +
+ )} + {checkAccess( + user, + deleteUserPermission.list, + deleteUserPermission.perm + ) && ( +
deleteAccount(row.original)} + > + +
+ )} +
+ ); + }; - const demographicColumns = [ - columnHelper.accessor("name", { - header: ( - - ) as any, - cell: ({row, getValue}) => ( -
(PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}> - {getValue()} -
- ), - }), - columnHelper.accessor("demographicInformation.country", { - header: ( - - ) 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})` - : "Not available", - }), - columnHelper.accessor("demographicInformation.phone", { - header: ( - - ) as any, - cell: (info) => info.getValue() || "Not available", - enableSorting: true, - }), - columnHelper.accessor((x) => (x.type === "corporate" ? x.demographicInformation?.position : x.demographicInformation?.employment), { - id: "employment", - header: ( - - ) as any, - cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "Not available", - enableSorting: true, - }), - columnHelper.accessor("demographicInformation.gender", { - header: ( - - ) as any, - cell: (info) => capitalize(info.getValue()) || "Not available", - enableSorting: true, - }), - { - header: ( - setShowDemographicInformation((prev) => !prev)}> - Switch - - ), - id: "actions", - cell: actionColumn, - }, - ]; + const demographicColumns = [ + columnHelper.accessor("name", { + header: ( + + ) as any, + cell: ({ row, getValue }) => ( +
+ PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) + ? setSelectedUser(row.original) + : null + } + > + {getValue()} +
+ ), + }), + columnHelper.accessor("demographicInformation.country", { + header: ( + + ) 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 + })` + : "Not available", + }), + columnHelper.accessor("demographicInformation.phone", { + header: ( + + ) as any, + cell: (info) => info.getValue() || "Not available", + enableSorting: true, + }), + columnHelper.accessor( + (x) => + x.type === "corporate" || x.type === "mastercorporate" + ? x.demographicInformation?.position + : x.demographicInformation?.employment, + { + id: "employment", + header: ( + + ) as any, + cell: (info) => + (info.row.original.type === "corporate" + ? info.getValue() + : capitalize(info.getValue())) || "Not available", + enableSorting: true, + } + ), + columnHelper.accessor("demographicInformation.gender", { + header: ( + + ) as any, + cell: (info) => capitalize(info.getValue()) || "Not available", + enableSorting: true, + }), + { + header: ( + setShowDemographicInformation((prev) => !prev)} + > + Switch + + ), + id: "actions", + cell: actionColumn, + }, + ]; - const defaultColumns = [ - columnHelper.accessor("name", { - header: ( - - ) as any, - cell: ({row, getValue}) => ( -
(PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}> - {row.original.type === "corporate" ? row.original.corporateInformation?.companyInformation?.name || getValue() : getValue()} -
- ), - }), - columnHelper.accessor("email", { - header: ( - - ) as any, - cell: ({row, getValue}) => ( -
(PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}> - {getValue()} -
- ), - }), - columnHelper.accessor("type", { - header: ( - - ) as any, - cell: (info) => USER_TYPE_LABELS[info.getValue()], - }), - columnHelper.accessor("corporateInformation.companyInformation.name", { - header: ( - - ) as any, - cell: (info) => , - }), - columnHelper.accessor("subscriptionExpirationDate", { - header: ( - - ) as any, - cell: (info) => ( - - {!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")} - - ), - }), - columnHelper.accessor("isVerified", { - header: ( - - ) as any, - cell: (info) => ( -
-
- -
-
- ), - }), - { - header: ( - setShowDemographicInformation((prev) => !prev)}> - Switch - - ), - id: "actions", - cell: actionColumn, - }, - ]; + const defaultColumns = [ + columnHelper.accessor("name", { + header: ( + + ) as any, + cell: ({ row, getValue }) => ( +
+ PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) + ? setSelectedUser(row.original) + : null + } + > + {row.original.type === "corporate" + ? row.original.corporateInformation?.companyInformation?.name || + getValue() + : getValue()} +
+ ), + }), + columnHelper.accessor("email", { + header: ( + + ) as any, + cell: ({ row, getValue }) => ( +
+ PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) + ? setSelectedUser(row.original) + : null + } + > + {getValue()} +
+ ), + }), + columnHelper.accessor("type", { + header: ( + + ) as any, + cell: (info) => USER_TYPE_LABELS[info.getValue()], + }), + columnHelper.accessor("corporateInformation.companyInformation.name", { + header: ( + + ) as any, + cell: (info) => ( + + ), + }), + columnHelper.accessor("subscriptionExpirationDate", { + header: ( + + ) as any, + cell: (info) => ( + + {!info.getValue() + ? "No expiry date" + : moment(info.getValue()).format("DD/MM/YYYY")} + + ), + }), + columnHelper.accessor("isVerified", { + header: ( + + ) as any, + cell: (info) => ( +
+
+ +
+
+ ), + }), + { + header: ( + setShowDemographicInformation((prev) => !prev)} + > + Switch + + ), + id: "actions", + cell: actionColumn, + }, + ]; - const reverseString = (str: string) => reverse(str.split("")).join(""); + 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); + const selectSorter = (previous: string | undefined, name: string) => { + if (!previous) return name; + if (previous === name) return reverseString(name); - return undefined; - }; + return undefined; + }; - const sortFunction = async (a: User, b: User) => { - if (sorter === "name" || sorter === reverseString("name")) - return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); + const sortFunction = async (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 === "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 === "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 === "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 === "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; + 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); - } + 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; + 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); - } + return sorter === "phone" + ? a.demographicInformation!.phone.localeCompare( + b.demographicInformation!.phone + ) + : b.demographicInformation!.phone.localeCompare( + a.demographicInformation!.phone + ); + } - if (sorter === "employment" || sorter === reverseString("employment")) { - const aSortingItem = a.type === "corporate" ? a.demographicInformation?.position : a.demographicInformation?.employment; - const bSortingItem = b.type === "corporate" ? b.demographicInformation?.position : b.demographicInformation?.employment; + if (sorter === "employment" || sorter === reverseString("employment")) { + const aSortingItem = + a.type === "corporate" || a.type === "mastercorporate" + ? a.demographicInformation?.position + : a.demographicInformation?.employment; + const bSortingItem = + b.type === "corporate" || b.type === "mastercorporate" + ? b.demographicInformation?.position + : b.demographicInformation?.employment; - if (!aSortingItem && bSortingItem) return sorter === "employment" ? -1 : 1; - if (aSortingItem && !bSortingItem) return sorter === "employment" ? 1 : -1; - if (!aSortingItem && !bSortingItem) return 0; + if (!aSortingItem && bSortingItem) + return sorter === "employment" ? -1 : 1; + if (aSortingItem && !bSortingItem) + return sorter === "employment" ? 1 : -1; + if (!aSortingItem && !bSortingItem) return 0; - return sorter === "employment" ? aSortingItem!.localeCompare(bSortingItem!) : bSortingItem!.localeCompare(aSortingItem!); - } + return sorter === "employment" + ? aSortingItem!.localeCompare(bSortingItem!) + : bSortingItem!.localeCompare(aSortingItem!); + } - 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; + 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 sorter === "gender" + ? a.demographicInformation!.gender.localeCompare( + b.demographicInformation!.gender + ) + : b.demographicInformation!.gender.localeCompare( + a.demographicInformation!.gender + ); + } - if (sorter === "companyName" || sorter === reverseString("companyName")) { - const aCorporateName = getUserCompanyName(a, users, groups); - const bCorporateName = getUserCompanyName(b, users, groups); - if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1; - if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1; - if (!aCorporateName && !bCorporateName) return 0; + if (sorter === "companyName" || sorter === reverseString("companyName")) { + const aCorporateName = getUserCompanyName(a, users, groups); + const bCorporateName = getUserCompanyName(b, users, groups); + if (!aCorporateName && bCorporateName) + return sorter === "companyName" ? -1 : 1; + if (aCorporateName && !bCorporateName) + return sorter === "companyName" ? 1 : -1; + if (!aCorporateName && !bCorporateName) return 0; - return sorter === "companyName" ? aCorporateName.localeCompare(bCorporateName) : bCorporateName.localeCompare(aCorporateName); - } + return sorter === "companyName" + ? aCorporateName.localeCompare(bCorporateName) + : bCorporateName.localeCompare(aCorporateName); + } - return a.id.localeCompare(b.id); - }; + return a.id.localeCompare(b.id); + }; - const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); + const { rows: filteredRows, renderSearch } = useListSearch( + searchFields, + displayUsers + ); - const table = useReactTable({ - data: filteredRows, - columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any, - getCoreRowModel: getCoreRowModel(), - }); + const table = useReactTable({ + data: filteredRows, + columns: (!showDemographicInformation + ? defaultColumns + : demographicColumns) as any, + getCoreRowModel: getCoreRowModel(), + }); - const downloadExcel = () => { - const csv = exportListToExcel(filteredRows, users, groups); + const downloadExcel = () => { + const csv = exportListToExcel(filteredRows, users, groups); - const element = document.createElement("a"); - const file = new Blob([csv], {type: "text/csv"}); - element.href = URL.createObjectURL(file); - element.download = "users.csv"; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - }; + const element = document.createElement("a"); + const file = new Blob([csv], { type: "text/csv" }); + element.href = URL.createObjectURL(file); + element.download = "users.csv"; + 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) => { - if (!selectedUser) return false; - return groups - .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) - .flatMap((g) => g.participants) - .includes(x.id); - }; + const viewStudentFilter = (x: User) => x.type === "student"; + const viewTeacherFilter = (x: User) => x.type === "teacher"; + const belongsToAdminFilter = (x: User) => { + if (!selectedUser) return false; + return groups + .filter( + (g) => + g.admin === selectedUser.id || + g.participants.includes(selectedUser.id) + ) + .flatMap((g) => g.participants) + .includes(x.id); + }; - const viewStudentFilterBelongsToAdmin = (x: User) => x.type === "student" && belongsToAdminFilter(x); - const viewTeacherFilterBelongsToAdmin = (x: User) => x.type === "teacher" && belongsToAdminFilter(x); + const viewStudentFilterBelongsToAdmin = (x: User) => + x.type === "student" && belongsToAdminFilter(x); + const viewTeacherFilterBelongsToAdmin = (x: User) => + x.type === "teacher" && 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, - }); + 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("/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, - }); + 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, + }); - 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), - }); + 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), + }); - router.push("/list/users"); - } - : undefined - } - onClose={(shouldReload) => { - setSelectedUser(undefined); - if (shouldReload) reload(); - }} - user={selectedUser} - /> -
- ); - }; + router.push("/list/users"); + } + : undefined + } + onClose={(shouldReload) => { + setSelectedUser(undefined); + if (shouldReload) reload(); + }} + user={selectedUser} + /> +
+ ); + }; - return ( - <> - {renderHeader && renderHeader(displayUsers.length)} -
- setSelectedUser(undefined)}> - {selectedUser && renderUserCard(selectedUser)} - -
-
- {renderSearch()} - -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
-
- - ); + return ( + <> + {renderHeader && renderHeader(displayUsers.length)} +
+ setSelectedUser(undefined)} + > + {selectedUser && renderUserCard(selectedUser)} + +
+
+ {renderSearch()} + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+
+ + ); } diff --git a/src/pages/api/groups/index.ts b/src/pages/api/groups/index.ts index db32967b..d42bbae1 100644 --- a/src/pages/api/groups/index.ts +++ b/src/pages/api/groups/index.ts @@ -30,29 +30,74 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") await post(req, res); } +const getGroupsForUser = async (admin: string, participant: string) => { + try { + const queryConstraints = [ + ...(admin ? [where("admin", "==", admin)] : []), + ...(participant + ? [where("participants", "array-contains", participant)] + : []), + ]; + const snapshot = await getDocs( + queryConstraints.length > 0 + ? query(collection(db, "groups"), ...queryConstraints) + : collection(db, "groups") + ); + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; + + return groups; + } catch (e) { + console.error(e); + return []; + } +}; async function get(req: NextApiRequest, res: NextApiResponse) { const { admin, participant } = req.query as { admin: string; participant: string; }; - const queryConstraints = [ - ...(admin ? [where("admin", "==", admin)] : []), - ...(participant - ? [where("participants", "array-contains", participant)] - : []), - ]; - const snapshot = await getDocs( - queryConstraints.length > 0 - ? query(collection(db, "groups"), ...queryConstraints) - : collection(db, "groups"), - ); - const groups = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as Group[]; + if (req.session?.user?.type === "mastercorporate") { + try { + const masterCorporateGroups = await getGroupsForUser(admin, participant); + const corporatesFromMaster = masterCorporateGroups + .filter((g) => g.name === "Corporate") + .flatMap((g) => g.participants); - res.status(200).json(groups); + if (corporatesFromMaster.length === 0) { + res.status(200).json([]); + return; + } + Promise.all( + corporatesFromMaster.map((c) => getGroupsForUser(c, participant)) + ) + .then((groups) => { + res.status(200).json([...masterCorporateGroups, ...groups.flat()]); + return; + }) + .catch((e) => { + console.error(e); + res.status(500).json({ ok: false }); + return; + }); + } catch (e) { + console.error(e); + res.status(500).json({ ok: false }); + return; + } + return; + } + + try { + const groups = await getGroupsForUser(admin, participant); + res.status(200).json(groups); + } catch (e) { + console.error(e); + res.status(500).json({ ok: false }); + } } async function post(req: NextApiRequest, res: NextApiResponse) { @@ -60,8 +105,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { await Promise.all( body.participants.map( - async (p) => await updateExpiryDateOnGroup(p, body.admin), - ), + async (p) => await updateExpiryDateOnGroup(p, body.admin) + ) ); await setDoc(doc(db, "groups", v4()), { diff --git a/src/pages/api/permissions/[id].ts b/src/pages/api/permissions/[id].ts new file mode 100644 index 00000000..7c379719 --- /dev/null +++ b/src/pages/api/permissions/[id].ts @@ -0,0 +1,30 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { getFirestore, doc, setDoc } from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "PATCH") return patch(req, res); +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + const { id } = req.query as { id: string }; + const { users } = req.body; + try { + await setDoc(doc(db, "permissions", id), { users }, { merge: true }); + return res.status(200).json({ ok: true }); + } catch (err) { + console.error(err); + return res.status(500).json({ ok: false }); + } +} diff --git a/src/pages/api/permissions/bootstrap.ts b/src/pages/api/permissions/bootstrap.ts new file mode 100644 index 00000000..cd36b52a --- /dev/null +++ b/src/pages/api/permissions/bootstrap.ts @@ -0,0 +1,43 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + collection, + getDocs, + query, + where, + doc, + setDoc, + addDoc, + getDoc, + deleteDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Permission } from "@/interfaces/permissions"; +import { bootstrap } from "@/utils/permissions.be"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + console.log("Boostrap"); + try { + await bootstrap(); + return res.status(200).json({ ok: true }); + } catch (err) { + console.error("Failed to update permissions", err); + return res.status(500).json({ ok: false }); + } +} diff --git a/src/pages/api/permissions/index.ts b/src/pages/api/permissions/index.ts new file mode 100644 index 00000000..3f9fbfac --- /dev/null +++ b/src/pages/api/permissions/index.ts @@ -0,0 +1,36 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + collection, + getDocs, + query, + where, + doc, + setDoc, + addDoc, + getDoc, + deleteDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Permission } from "@/interfaces/permissions"; +import { getPermissions, getPermissionDocs } from "@/utils/permissions.be"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + const docs = await getPermissionDocs(); + res.status(200).json(docs); +} diff --git a/src/pages/api/register.ts b/src/pages/api/register.ts index 773bfbd6..15a445d2 100644 --- a/src/pages/api/register.ts +++ b/src/pages/api/register.ts @@ -143,9 +143,18 @@ async function registerCorporate(req: NextApiRequest, res: NextApiResponse) { disableEditing: true, }; + const defaultCorporateGroup: Group = { + admin: userId, + id: v4(), + name: "Corporate", + participants: [], + disableEditing: true, + }; + await setDoc(doc(db, "users", userId), user); await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup); await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup); + await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup); req.session.user = {...user, id: userId}; await req.session.save(); diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts index 921c0222..dcb2bf0d 100644 --- a/src/pages/api/user.ts +++ b/src/pages/api/user.ts @@ -1,11 +1,22 @@ -import {PERMISSIONS} from "@/constants/userPermissions"; -import {app, adminApp} from "@/firebase"; -import {Group, User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import {collection, deleteDoc, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore"; -import {getAuth} from "firebase-admin/auth"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {NextApiRequest, NextApiResponse} from "next"; +import { PERMISSIONS } from "@/constants/userPermissions"; +import { app, adminApp } from "@/firebase"; +import { Group, User } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { + collection, + deleteDoc, + doc, + getDoc, + getDocs, + getFirestore, + query, + setDoc, + where, +} from "firebase/firestore"; +import { getAuth } from "firebase-admin/auth"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { NextApiRequest, NextApiResponse } from "next"; +import { getPermissions, getPermissionDocs } from "@/utils/permissions.be"; const db = getFirestore(app); const auth = getAuth(adminApp); @@ -13,89 +24,132 @@ const auth = getAuth(adminApp); export default withIronSessionApiRoute(user, sessionOptions); async function user(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "GET") return get(req, res); - if (req.method === "DELETE") return del(req, res); + if (req.method === "GET") return get(req, res); + if (req.method === "DELETE") return del(req, res); - res.status(404).json(undefined); + res.status(404).json(undefined); } async function del(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ok: false}); - return; - } + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } - const {id} = req.query as {id: string}; + const { id } = req.query as { id: string }; - const docUser = await getDoc(doc(db, "users", req.session.user.id)); - if (!docUser.exists()) { - res.status(401).json({ok: false}); - return; - } + const docUser = await getDoc(doc(db, "users", req.session.user.id)); + if (!docUser.exists()) { + res.status(401).json({ ok: false }); + return; + } - const user = docUser.data() as User; + const user = docUser.data() as User; - const docTargetUser = await getDoc(doc(db, "users", id)); - if (!docTargetUser.exists()) { - res.status(404).json({ok: false}); - return; - } + const docTargetUser = await getDoc(doc(db, "users", id)); + if (!docTargetUser.exists()) { + res.status(404).json({ ok: false }); + return; + } - const targetUser = {...docTargetUser.data(), id: docTargetUser.id} as User; + const targetUser = { ...docTargetUser.data(), id: docTargetUser.id } as User; - if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) { - res.json({ok: true}); + if ( + user.type === "corporate" && + (targetUser.type === "student" || targetUser.type === "teacher") + ) { + res.json({ ok: true }); - const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id))); - await Promise.all([ - ...userParticipantGroup.docs - .filter((x) => (x.data() as Group).admin === user.id) - .map(async (x) => await setDoc(x.ref, {participants: x.data().participants.filter((y: string) => y !== id)}, {merge: true})), - ]); + const userParticipantGroup = await getDocs( + query( + collection(db, "groups"), + where("participants", "array-contains", id) + ) + ); + await Promise.all([ + ...userParticipantGroup.docs + .filter((x) => (x.data() as Group).admin === user.id) + .map( + async (x) => + await setDoc( + x.ref, + { + participants: x + .data() + .participants.filter((y: string) => y !== id), + }, + { merge: true } + ) + ), + ]); - return; - } + return; + } - const permission = PERMISSIONS.deleteUser[targetUser.type]; - if (!permission.includes(user.type)) { - res.status(403).json({ok: false}); - return; - } + const permission = PERMISSIONS.deleteUser[targetUser.type]; + if (!permission.list.includes(user.type)) { + res.status(403).json({ ok: false }); + return; + } - res.json({ok: true}); + res.json({ ok: true }); - await auth.deleteUser(id); - await deleteDoc(doc(db, "users", id)); - const userCodeDocs = await getDocs(query(collection(db, "codes"), where("userId", "==", id))); - const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id))); - const userGroupAdminDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); - const userStatsDocs = await getDocs(query(collection(db, "stats"), where("user", "==", id))); + await auth.deleteUser(id); + await deleteDoc(doc(db, "users", id)); + const userCodeDocs = await getDocs( + query(collection(db, "codes"), where("userId", "==", id)) + ); + const userParticipantGroup = await getDocs( + query(collection(db, "groups"), where("participants", "array-contains", id)) + ); + const userGroupAdminDocs = await getDocs( + query(collection(db, "groups"), where("admin", "==", id)) + ); + const userStatsDocs = await getDocs( + query(collection(db, "stats"), where("user", "==", id)) + ); - await Promise.all([ - ...userCodeDocs.docs.map(async (x) => await deleteDoc(x.ref)), - ...userGroupAdminDocs.docs.map(async (x) => await deleteDoc(x.ref)), - ...userStatsDocs.docs.map(async (x) => await deleteDoc(x.ref)), - ...userParticipantGroup.docs.map( - async (x) => await setDoc(x.ref, {participants: x.data().participants.filter((y: string) => y !== id)}, {merge: true}), - ), - ]); + await Promise.all([ + ...userCodeDocs.docs.map(async (x) => await deleteDoc(x.ref)), + ...userGroupAdminDocs.docs.map(async (x) => await deleteDoc(x.ref)), + ...userStatsDocs.docs.map(async (x) => await deleteDoc(x.ref)), + ...userParticipantGroup.docs.map( + async (x) => + await setDoc( + x.ref, + { + participants: x.data().participants.filter((y: string) => y !== id), + }, + { merge: true } + ) + ), + ]); } async function get(req: NextApiRequest, res: NextApiResponse) { - if (req.session.user) { - const docUser = await getDoc(doc(db, "users", req.session.user.id)); - if (!docUser.exists()) { - res.status(401).json(undefined); - return; - } + if (req.session.user) { + const docUser = await getDoc(doc(db, "users", req.session.user.id)); + if (!docUser.exists()) { + res.status(401).json(undefined); + return; + } - const user = docUser.data() as User; + const user = docUser.data() as User; + + const permissionDocs = await getPermissionDocs(); - req.session.user = {...user, id: req.session.user.id}; - await req.session.save(); + const userWithPermissions = { + ...user, + permissions: getPermissions(req.session.user.id, permissionDocs), + }; + req.session.user = { + ...userWithPermissions, + id: req.session.user.id, + }; + await req.session.save(); - res.json({...user, id: req.session.user.id}); - } else { - res.status(401).json(undefined); - } + res.json({ ...userWithPermissions, id: req.session.user.id }); + } else { + res.status(401).json(undefined); + } } diff --git a/src/pages/generation.tsx b/src/pages/generation.tsx index b52f6ef6..c2a0de83 100644 --- a/src/pages/generation.tsx +++ b/src/pages/generation.tsx @@ -1,19 +1,19 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; import useUser from "@/hooks/useUser"; -import {toast, ToastContainer} from "react-toastify"; +import { toast, ToastContainer } from "react-toastify"; import Layout from "@/components/High/Layout"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import {useState} from "react"; -import {Module} from "@/interfaces"; -import {RadioGroup, Tab} from "@headlessui/react"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { useState } from "react"; +import { Module } from "@/interfaces"; +import { RadioGroup, Tab } from "@headlessui/react"; import clsx from "clsx"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; -import {capitalize} from "lodash"; +import { MODULE_ARRAY } from "@/utils/moduleUtils"; +import { capitalize } from "lodash"; import Button from "@/components/Low/Button"; -import {Exercise, ReadingPart} from "@/interfaces/exam"; +import { Exercise, ReadingPart } from "@/interfaces/exam"; import Input from "@/components/Low/Input"; import axios from "axios"; import ReadingGeneration from "./(generation)/ReadingGeneration"; @@ -21,101 +21,109 @@ import ListeningGeneration from "./(generation)/ListeningGeneration"; import WritingGeneration from "./(generation)/WritingGeneration"; import LevelGeneration from "./(generation)/LevelGeneration"; import SpeakingGeneration from "./(generation)/SpeakingGeneration"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; -export const getServerSideProps = withIronSessionSsr(({req, res}) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(({ req, res }) => { + const user = req.session.user; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - } - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if (shouldRedirectHome(user) || user.type !== "developer") { - return { - redirect: { - destination: "/", - permanent: false, - } - }; - } + if ( + shouldRedirectHome(user) || + checkAccess(user, getTypesOfUser(["developer"])) + ) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - return { - props: {user: req.session.user}, - }; + return { + props: { user: req.session.user }, + }; }, sessionOptions); export default function Generation() { - const [module, setModule] = useState("reading"); + const [module, setModule] = useState("reading"); - const {user} = useUser({redirectTo: "/login"}); + const { user } = useUser({ redirectTo: "/login" }); - return ( - <> - - Exam Generation | EnCoach - - - - - - {user && ( - -

Exam Generation

-
- - - {[...MODULE_ARRAY].map((x) => ( - - {({checked}) => ( - - {capitalize(x)} - - )} - - ))} - -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } -
- )} - - ); + return ( + <> + + Exam Generation | EnCoach + + + + + + {user && ( + +

Exam Generation

+
+ + + {[...MODULE_ARRAY].map((x) => ( + + {({ checked }) => ( + + {capitalize(x)} + + )} + + ))} + +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } +
+ )} + + ); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index bd3365ea..9a824ded 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,196 +1,244 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; import Navbar from "@/components/Navbar"; -import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone} from "react-icons/bs"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {useEffect, useState} from "react"; +import { + BsFileEarmarkText, + BsPencil, + BsStar, + BsBook, + BsHeadphones, + BsPen, + BsMegaphone, +} from "react-icons/bs"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { useEffect, useState } from "react"; import useStats from "@/hooks/useStats"; -import {averageScore, groupBySession, totalExams} from "@/utils/stats"; +import { averageScore, groupBySession, totalExams } from "@/utils/stats"; import useUser from "@/hooks/useUser"; -import Sidebar from "@/components/Sidebar"; import Diagnostic from "@/components/Diagnostic"; -import {ToastContainer} from "react-toastify"; -import {capitalize} from "lodash"; -import {Module} from "@/interfaces"; +import { ToastContainer } from "react-toastify"; +import { capitalize } from "lodash"; +import { Module } from "@/interfaces"; import ProgressBar from "@/components/Low/ProgressBar"; import Layout from "@/components/High/Layout"; -import {calculateAverageLevel} from "@/utils/score"; +import { calculateAverageLevel } from "@/utils/score"; import axios from "axios"; import DemographicInformationInput from "@/components/DemographicInformationInput"; import moment from "moment"; import Link from "next/link"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import { MODULE_ARRAY } from "@/utils/moduleUtils"; import ProfileSummary from "@/components/ProfileSummary"; import StudentDashboard from "@/dashboards/Student"; import AdminDashboard from "@/dashboards/Admin"; import CorporateDashboard from "@/dashboards/Corporate"; import TeacherDashboard from "@/dashboards/Teacher"; import AgentDashboard from "@/dashboards/Agent"; +import MasterCorporateDashboard from "@/dashboards/MasterCorporate"; import PaymentDue from "./(status)/PaymentDue"; -import {useRouter} from "next/router"; -import {PayPalScriptProvider} from "@paypal/react-paypal-js"; -import {CorporateUser, Type, userTypes} from "@/interfaces/user"; +import { useRouter } from "next/router"; +import { PayPalScriptProvider } from "@paypal/react-paypal-js"; +import { + CorporateUser, + MasterCorporateUser, + Type, + userTypes, +} from "@/interfaces/user"; import Select from "react-select"; -import {USER_TYPE_LABELS} from "@/resources/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; -export const getServerSideProps = withIronSessionSsr(({req, res}) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(({ req, res }) => { + const user = req.session.user; - const envVariables: {[key: string]: string} = {}; - Object.keys(process.env) - .filter((x) => x.startsWith("NEXT_PUBLIC")) - .forEach((x: string) => { - envVariables[x] = process.env[x]!; - }); + const envVariables: { [key: string]: string } = {}; + Object.keys(process.env) + .filter((x) => x.startsWith("NEXT_PUBLIC")) + .forEach((x: string) => { + envVariables[x] = process.env[x]!; + }); - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - return { - props: {user: req.session.user, envVariables}, - }; + return { + props: { user: req.session.user, envVariables }, + }; }, sessionOptions); interface Props { - user: any; - envVariables: {[key: string]: string}; + user: any; + envVariables: { [key: string]: string }; } export default function Home(props: Props) { - const {envVariables} = props; - const [showDiagnostics, setShowDiagnostics] = useState(false); - const [showDemographicInput, setShowDemographicInput] = useState(false); - const [selectedScreen, setSelectedScreen] = useState("admin"); + const { envVariables } = props; + const [showDiagnostics, setShowDiagnostics] = useState(false); + const [showDemographicInput, setShowDemographicInput] = useState(false); + const [selectedScreen, setSelectedScreen] = useState("admin"); - const {user, mutateUser} = useUser({redirectTo: "/login"}); - const router = useRouter(); + const { user, mutateUser } = useUser({ redirectTo: "/login" }); + const router = useRouter(); - useEffect(() => { - if (user) { - setShowDemographicInput( - !user.demographicInformation || - !user.demographicInformation.country || - !user.demographicInformation.gender || - !user.demographicInformation.phone, - ); - setShowDiagnostics(user.isFirstLogin && user.type === "student"); - } - }, [user]); + useEffect(() => { + if (user) { + setShowDemographicInput( + !user.demographicInformation || + !user.demographicInformation.country || + !user.demographicInformation.gender || + !user.demographicInformation.phone + ); + setShowDiagnostics(user.isFirstLogin && user.type === "student"); + } + }, [user]); - const checkIfUserExpired = () => { - const expirationDate = user!.subscriptionExpirationDate; + const checkIfUserExpired = () => { + const expirationDate = user!.subscriptionExpirationDate; - if (expirationDate === null || expirationDate === undefined) return false; - if (moment(expirationDate).isAfter(moment(new Date()))) return false; + if (expirationDate === null || expirationDate === undefined) return false; + if (moment(expirationDate).isAfter(moment(new Date()))) return false; - return true; - }; + return true; + }; - if (user && (user.status === "paymentDue" || user.status === "disabled" || checkIfUserExpired())) { - return ( - <> - - EnCoach - - - - - {user.status === "disabled" && ( - -
- Your account has been disabled! - Please contact an administrator if you believe this to be a mistake. -
-
- )} - {(user.status === "paymentDue" || checkIfUserExpired()) && } - - ); - } + if ( + user && + (user.status === "paymentDue" || + user.status === "disabled" || + checkIfUserExpired()) + ) { + return ( + <> + + EnCoach + + + + + {user.status === "disabled" && ( + +
+ + Your account has been disabled! + + + Please contact an administrator if you believe this to be a + mistake. + +
+
+ )} + {(user.status === "paymentDue" || checkIfUserExpired()) && ( + + )} + + ); + } - if (user && showDemographicInput) { - return ( - <> - - EnCoach - - - - - - - - - ); - } + if (user && showDemographicInput) { + return ( + <> + + EnCoach + + + + + + + + + ); + } - if (user && showDiagnostics) { - return ( - <> - - EnCoach - - - - - - setShowDiagnostics(false)} /> - - - ); - } + if (user && showDiagnostics) { + return ( + <> + + EnCoach + + + + + + setShowDiagnostics(false)} /> + + + ); + } - return ( - <> - - EnCoach - - - - - - {user && ( - - {user.type === "student" && } - {user.type === "teacher" && } - {user.type === "corporate" && } - {user.type === "agent" && } - {user.type === "admin" && } - {user.type === "developer" && ( - <> - ({ + value: u, + label: USER_TYPE_LABELS[u], + }))} + value={{ + value: selectedScreen, + label: USER_TYPE_LABELS[selectedScreen], + }} + onChange={(value) => + value + ? setSelectedScreen(value.value) + : setSelectedScreen("admin") + } + /> - {selectedScreen === "student" && } - {selectedScreen === "teacher" && } - {selectedScreen === "corporate" && } - {selectedScreen === "agent" && } - {selectedScreen === "admin" && } - - )} - - )} - - ); + {selectedScreen === "student" && } + {selectedScreen === "teacher" && } + {selectedScreen === "corporate" && ( + + )} + {selectedScreen === "mastercorporate" && ( + + )} + {selectedScreen === "agent" && } + {selectedScreen === "admin" && } + + )} + + )} + + ); } diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 51801211..aa5ba11c 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -1,20 +1,28 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; import useUser from "@/hooks/useUser"; -import {toast, ToastContainer} from "react-toastify"; +import { toast, ToastContainer } from "react-toastify"; import Layout from "@/components/High/Layout"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; import usePayments from "@/hooks/usePayments"; import usePaypalPayments from "@/hooks/usePaypalPayments"; -import {Payment, PaypalPayment} from "@/interfaces/paypal"; -import {CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable} from "@tanstack/react-table"; -import {CURRENCIES} from "@/resources/paypal"; -import {BsTrash} from "react-icons/bs"; +import { Payment, PaypalPayment } from "@/interfaces/paypal"; +import { + CellContext, + createColumnHelper, + flexRender, + getCoreRowModel, + HeaderGroup, + Table, + useReactTable, +} from "@tanstack/react-table"; +import { CURRENCIES } from "@/resources/paypal"; +import { BsTrash } from "react-icons/bs"; import axios from "axios"; -import {useEffect, useState, useMemo} from "react"; -import {AgentUser, CorporateUser, User} from "@/interfaces/user"; +import { useEffect, useState, useMemo } from "react"; +import { AgentUser, CorporateUser, User } from "@/interfaces/user"; import UserCard from "@/components/UserCard"; import Modal from "@/components/Modal"; import clsx from "clsx"; @@ -26,1192 +34,1448 @@ import Input from "@/components/Low/Input"; import ReactDatePicker from "react-datepicker"; import moment from "moment"; import PaymentAssetManager from "@/components/PaymentAssetManager"; -import {toFixedNumber} from "@/utils/number"; -import {CSVLink} from "react-csv"; -import {Tab} from "@headlessui/react"; -import {useListSearch} from "@/hooks/useListSearch"; -export const getServerSideProps = withIronSessionSsr(({req, res}) => { - const user = req.session.user; +import { toFixedNumber } from "@/utils/number"; +import { CSVLink } from "react-csv"; +import { Tab } from "@headlessui/react"; +import { useListSearch } from "@/hooks/useListSearch"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } +export const getServerSideProps = withIronSessionSsr(({ req, res }) => { + const user = req.session.user; - if (shouldRedirectHome(user) || !["admin", "developer", "agent", "corporate"].includes(user.type)) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - return { - props: {user: req.session.user}, - }; + if ( + shouldRedirectHome(user) || + checkAccess( + user, + getTypesOfUser([ + "admin", + "developer", + "agent", + "corporate", + "mastercorporate", + ]) + ) + ) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + return { + props: { user: req.session.user }, + }; }, sessionOptions); const columnHelper = createColumnHelper(); const paypalColumnHelper = createColumnHelper(); -const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () => void; reload: () => void; showComission: boolean}) => { - const [corporate, setCorporate] = useState(); - const [date, setDate] = useState(new Date()); +const PaymentCreator = ({ + onClose, + reload, + showComission = false, +}: { + onClose: () => void; + reload: () => void; + showComission: boolean; +}) => { + const [corporate, setCorporate] = useState(); + const [date, setDate] = useState(new Date()); - const {users} = useUsers(); + const { users } = useUsers(); - const price = corporate?.corporateInformation?.payment?.value || 0; - const commission = corporate?.corporateInformation?.payment?.commission || 0; - const currency = corporate?.corporateInformation?.payment?.currency || "EUR"; + const price = corporate?.corporateInformation?.payment?.value || 0; + const commission = corporate?.corporateInformation?.payment?.commission || 0; + const currency = corporate?.corporateInformation?.payment?.currency || "EUR"; - const referralAgent = useMemo(() => { - if (corporate?.corporateInformation?.referralAgent) { - return users.find((u) => u.id === corporate.corporateInformation.referralAgent); - } + const referralAgent = useMemo(() => { + if (corporate?.corporateInformation?.referralAgent) { + return users.find( + (u) => u.id === corporate.corporateInformation.referralAgent + ); + } - return undefined; - }, [corporate?.corporateInformation?.referralAgent, users]); + return undefined; + }, [corporate?.corporateInformation?.referralAgent, users]); - const submit = () => { - axios - .post(`/api/payments`, { - corporate: corporate?.id, - agent: referralAgent?.id, - agentCommission: commission, - agentValue: toFixedNumber((commission! / 100) * price!, 2), - currency, - value: price, - isPaid: false, - date: date.toISOString(), - }) - .then(() => { - toast.success("New payment has been created successfully!"); - reload(); - onClose(); - }) - .catch(() => { - toast.error("Something went wrong, please try again later!"); - }); - }; + const submit = () => { + axios + .post(`/api/payments`, { + corporate: corporate?.id, + agent: referralAgent?.id, + agentCommission: commission, + agentValue: toFixedNumber((commission! / 100) * price!, 2), + currency, + value: price, + isPaid: false, + date: date.toISOString(), + }) + .then(() => { + toast.success("New payment has been created successfully!"); + reload(); + onClose(); + }) + .catch(() => { + toast.error("Something went wrong, please try again later!"); + }); + }; - return ( -
-

New Payment

-
-
- - u.type === "corporate") as CorporateUser[] + ).map((user) => ({ + value: user.id, + meta: user, + label: `${ + user.corporateInformation?.companyInformation?.name || user.name + } - ${user.email}`, + }))} + defaultValue={{ value: "undefined", label: "Select an account" }} + onChange={(value) => + setCorporate((value as any)?.meta ?? undefined) + } + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused + ? "#D5D9F0" + : state.isSelected + ? "#7872BF" + : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
-
- -
- {}} type="number" value={price} defaultValue={0} className="col-span-3" disabled /> - {}} type="number" defaultValue={0} value={commission} disabled /> -
-
- - c.currency === currency)?.label}`} - onChange={() => null} - type="text" - defaultValue={0} - disabled - /> -
-
- )} +
+ +
+ {}} + type="number" + value={price} + defaultValue={0} + className="col-span-3" + disabled + /> + {}} + type="number" + defaultValue={0} + value={commission} + disabled + /> +
+
+ + c.currency === currency)?.label + }`} + onChange={() => null} + type="text" + defaultValue={0} + disabled + /> +
+
+ )} -
-
- - setDate(date ?? new Date())} - /> -
+
+
+ + setDate(date ?? new Date())} + /> +
-
- - null} - type="text" - defaultValue={"No country manager"} - disabled - /> -
-
-
- - -
-
-
- ); +
+ + null} + type="text" + defaultValue={"No country manager"} + disabled + /> +
+
+
+ + +
+
+
+ ); }; const IS_PAID_OPTIONS = [ - { - value: null, - label: "All", - }, - { - value: false, - label: "Unpaid", - }, - { - value: true, - label: "Paid", - }, + { + value: null, + label: "All", + }, + { + value: false, + label: "Unpaid", + }, + { + value: true, + label: "Paid", + }, ]; const IS_FILE_SUBMITTED_OPTIONS = [ - { - value: null, - label: "All", - }, - { - value: false, - label: "Submitted", - }, - { - value: true, - label: "Not Submitted", - }, + { + value: null, + label: "All", + }, + { + value: false, + label: "Submitted", + }, + { + value: true, + label: "Not Submitted", + }, ]; -const CSV_PAYMENTS_WHITELISTED_KEYS = ["corporateId", "corporate", "date", "amount", "agent", "agentCommission", "agentValue", "isPaid"]; +const CSV_PAYMENTS_WHITELISTED_KEYS = [ + "corporateId", + "corporate", + "date", + "amount", + "agent", + "agentCommission", + "agentValue", + "isPaid", +]; -const CSV_PAYPAL_WHITELISTED_KEYS = ["orderId", "status", "name", "email", "value", "createdAt", "subscriptionExpirationDate"]; +const CSV_PAYPAL_WHITELISTED_KEYS = [ + "orderId", + "status", + "name", + "email", + "value", + "createdAt", + "subscriptionExpirationDate", +]; interface SimpleCSVColumn { - key: string; - label: string; - index: number; + key: string; + label: string; + index: number; } interface PaypalPaymentWithUserData extends PaypalPayment { - name: string; - email: string; + name: string; + email: string; } const paypalFilterRows = [["email"], ["name"]]; export default function PaymentRecord() { - const [selectedCorporateUser, setSelectedCorporateUser] = useState(); - const [selectedAgentUser, setSelectedAgentUser] = useState(); - const [isCreatingPayment, setIsCreatingPayment] = useState(false); - const [filters, setFilters] = useState<{filter: (p: Payment) => boolean; id: string}[]>([]); - const [displayPayments, setDisplayPayments] = useState([]); + const [selectedCorporateUser, setSelectedCorporateUser] = useState(); + const [selectedAgentUser, setSelectedAgentUser] = useState(); + const [isCreatingPayment, setIsCreatingPayment] = useState(false); + const [filters, setFilters] = useState< + { filter: (p: Payment) => boolean; id: string }[] + >([]); + const [displayPayments, setDisplayPayments] = useState([]); - const [corporate, setCorporate] = useState(); - const [agent, setAgent] = useState(); + const [corporate, setCorporate] = useState(); + const [agent, setAgent] = useState(); - const {user} = useUser({redirectTo: "/login"}); - const {users, reload: reloadUsers} = useUsers(); - const {payments: originalPayments, reload: reloadPayment} = usePayments(); - const {payments: paypalPayments, reload: reloadPaypalPayment} = usePaypalPayments(); - const [startDate, setStartDate] = useState(moment("01/01/2023").toDate()); - const [endDate, setEndDate] = useState(moment().endOf("day").toDate()); - const [paid, setPaid] = useState(IS_PAID_OPTIONS[0].value); - const [commissionTransfer, setCommissionTransfer] = useState(IS_FILE_SUBMITTED_OPTIONS[0].value); - const [corporateTransfer, setCorporateTransfer] = useState(IS_FILE_SUBMITTED_OPTIONS[0].value); - const reload = () => { - reloadUsers(); - reloadPayment(); - }; + const { user } = useUser({ redirectTo: "/login" }); + const { users, reload: reloadUsers } = useUsers(); + const { payments: originalPayments, reload: reloadPayment } = usePayments(); + const { payments: paypalPayments, reload: reloadPaypalPayment } = + usePaypalPayments(); + const [startDate, setStartDate] = useState( + moment("01/01/2023").toDate() + ); + const [endDate, setEndDate] = useState( + moment().endOf("day").toDate() + ); + const [paid, setPaid] = useState(IS_PAID_OPTIONS[0].value); + const [commissionTransfer, setCommissionTransfer] = useState( + IS_FILE_SUBMITTED_OPTIONS[0].value + ); + const [corporateTransfer, setCorporateTransfer] = useState( + IS_FILE_SUBMITTED_OPTIONS[0].value + ); + const reload = () => { + reloadUsers(); + reloadPayment(); + }; - const payments = useMemo(() => { - return originalPayments.filter((p: Payment) => { - const date = moment(p.date); - return date.isAfter(startDate) && date.isBefore(endDate); - }); - }, [originalPayments, startDate, endDate]); + const payments = useMemo(() => { + return originalPayments.filter((p: Payment) => { + const date = moment(p.date); + return date.isAfter(startDate) && date.isBefore(endDate); + }); + }, [originalPayments, startDate, endDate]); - const [selectedIndex, setSelectedIndex] = useState(0); + const [selectedIndex, setSelectedIndex] = useState(0); - useEffect(() => { - setDisplayPayments( - filters - .map((f) => f.filter) - .reduce((d, f) => d.filter(f), payments) - .sort((a, b) => moment(b.date).diff(moment(a.date))), - ); - }, [payments, filters]); + useEffect(() => { + setDisplayPayments( + filters + .map((f) => f.filter) + .reduce((d, f) => d.filter(f), payments) + .sort((a, b) => moment(b.date).diff(moment(a.date))) + ); + }, [payments, filters]); - useEffect(() => { - if (user && user.type === "agent") { - setAgent(user); - } - }, [user]); + useEffect(() => { + if (user && user.type === "agent") { + setAgent(user); + } + }, [user]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "agent-filter"), - ...(!agent - ? [] - : [ - { - id: "agent-filter", - filter: (p: Payment) => p.agent === agent.id, - }, - ]), - ]); - }, [agent]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "agent-filter"), + ...(!agent + ? [] + : [ + { + id: "agent-filter", + filter: (p: Payment) => p.agent === agent.id, + }, + ]), + ]); + }, [agent]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "corporate-filter"), - ...(!corporate - ? [] - : [ - { - id: "corporate-filter", - filter: (p: Payment) => p.corporate === corporate.id, - }, - ]), - ]); - }, [corporate]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "corporate-filter"), + ...(!corporate + ? [] + : [ + { + id: "corporate-filter", + filter: (p: Payment) => p.corporate === corporate.id, + }, + ]), + ]); + }, [corporate]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "paid"), - ...(typeof paid !== "boolean" ? [] : [{id: "paid", filter: (p: Payment) => p.isPaid === paid}]), - ]); - }, [paid]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "paid"), + ...(typeof paid !== "boolean" + ? [] + : [{ id: "paid", filter: (p: Payment) => p.isPaid === paid }]), + ]); + }, [paid]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "commissionTransfer"), - ...(typeof commissionTransfer !== "boolean" - ? [] - : [ - { - id: "commissionTransfer", - filter: (p: Payment) => !p.commissionTransfer === commissionTransfer, - }, - ]), - ]); - }, [commissionTransfer]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "commissionTransfer"), + ...(typeof commissionTransfer !== "boolean" + ? [] + : [ + { + id: "commissionTransfer", + filter: (p: Payment) => + !p.commissionTransfer === commissionTransfer, + }, + ]), + ]); + }, [commissionTransfer]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "corporateTransfer"), - ...(typeof corporateTransfer !== "boolean" - ? [] - : [ - { - id: "corporateTransfer", - filter: (p: Payment) => !p.corporateTransfer === corporateTransfer, - }, - ]), - ]); - }, [corporateTransfer]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "corporateTransfer"), + ...(typeof corporateTransfer !== "boolean" + ? [] + : [ + { + id: "corporateTransfer", + filter: (p: Payment) => + !p.corporateTransfer === corporateTransfer, + }, + ]), + ]); + }, [corporateTransfer]); - useEffect(() => { - if (user && user.type === "corporate") return setCorporate(user); - if (user && user.type === "agent") return setAgent(user); - }, [user]); + useEffect(() => { + if (user && user.type === "corporate") return setCorporate(user); + if (user && user.type === "agent") return setAgent(user); + }, [user]); - const updatePayment = (payment: Payment, key: string, value: any) => { - axios - .patch(`api/payments/${payment.id}`, {...payment, [key]: value}) - .then(() => toast.success("Updated the payment")) - .finally(reload); - }; + const updatePayment = (payment: Payment, key: string, value: any) => { + axios + .patch(`api/payments/${payment.id}`, { ...payment, [key]: value }) + .then(() => toast.success("Updated the payment")) + .finally(reload); + }; - const deletePayment = (id: string) => { - if (!confirm(`Are you sure you want to delete this payment?`)) return; + const deletePayment = (id: string) => { + if (!confirm(`Are you sure you want to delete this payment?`)) return; - axios - .delete(`/api/payments/${id}`) - .then(() => toast.success(`Deleted the "${id}" payment`)) - .catch((reason) => { - if (reason.response.status === 404) { - toast.error("Exam not found!"); - return; - } + axios + .delete(`/api/payments/${id}`) + .then(() => toast.success(`Deleted the "${id}" payment`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Exam not found!"); + return; + } - if (reason.response.status === 403) { - toast.error("You do not have permission to delete an approved payment record!"); - return; - } + if (reason.response.status === 403) { + toast.error( + "You do not have permission to delete an approved payment record!" + ); + return; + } - toast.error("Something went wrong, please try again later."); - }) - .finally(reload); - }; + toast.error("Something went wrong, please try again later."); + }) + .finally(reload); + }; - const getFileAssetsColumns = () => { - if (user) { - const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer"; - switch (user.type) { - case "corporate": - return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - id: "corporateTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case "agent": - return [ - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - id: "commissionTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case "admin": - return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - id: "corporateTransfer", - cell: (info) => ( -
- -
- ), - }), - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - id: "commissionTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case "developer": - return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - id: "corporateTransfer", - cell: (info) => ( -
- -
- ), - }), - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - id: "commissionTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - default: - return []; - } - } + const getFileAssetsColumns = () => { + if (user) { + const containerClassName = + "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer"; + switch (user.type) { + case "corporate": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + id: "corporateTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "agent": + return [ + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + id: "commissionTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "admin": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + id: "corporateTransfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + id: "commissionTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "developer": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + id: "corporateTransfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + id: "commissionTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + default: + return []; + } + } - return []; - }; + return []; + }; - const columHelperValue = (key: string, info: any) => { - switch (key) { - case "agentCommission": { - const value = info.getValue(); - return {value: `${value}%`}; - } - case "agent": { - const user = users.find((x) => x.id === info.row.original.agent) as AgentUser; - return { - value: user?.name, - user, - }; - } - case "agentValue": - case "amount": { - const value = info.getValue(); - const numberValue = toFixedNumber(value, 2); - return {value: numberValue}; - } - case "date": { - const value = info.getValue(); - return {value: moment(value).format("DD/MM/YYYY")}; - } - case "corporate": { - const specificValue = info.row.original.corporate; - const user = users.find((x) => x.id === specificValue) as CorporateUser; - return { - user, - value: user?.corporateInformation.companyInformation.name || user?.name, - }; - } - case "currency": { - return { - value: info.row.original.currency, - }; - } - case "isPaid": - case "corporateId": - default: { - const value = info.getValue(); - return {value}; - } - } - }; + const columHelperValue = (key: string, info: any) => { + switch (key) { + case "agentCommission": { + const value = info.getValue(); + return { value: `${value}%` }; + } + case "agent": { + const user = users.find( + (x) => x.id === info.row.original.agent + ) as AgentUser; + return { + value: user?.name, + user, + }; + } + case "agentValue": + case "amount": { + const value = info.getValue(); + const numberValue = toFixedNumber(value, 2); + return { value: numberValue }; + } + case "date": { + const value = info.getValue(); + return { value: moment(value).format("DD/MM/YYYY") }; + } + case "corporate": { + const specificValue = info.row.original.corporate; + const user = users.find((x) => x.id === specificValue) as CorporateUser; + return { + user, + value: + user?.corporateInformation.companyInformation.name || user?.name, + }; + } + case "currency": { + return { + value: info.row.original.currency, + }; + } + case "isPaid": + case "corporateId": + default: { + const value = info.getValue(); + return { value }; + } + } + }; - const hiddenToCorporateColumns = () => { - if (user && user.type !== "corporate") - return [ - columnHelper.accessor("agent", { - header: "Country Manager", - id: "agent", - cell: (info) => { - const {user, value} = columHelperValue(info.column.id, info); - return ( -
setSelectedAgentUser(user)}> - {value} -
- ); - }, - }), - columnHelper.accessor("agentCommission", { - header: "Commission", - id: "agentCommission", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return <>{value}; - }, - }), - columnHelper.accessor("agentValue", { - header: "Commission Value", - id: "agentValue", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - const finalValue = `${value} ${info.row.original.currency}`; - return {finalValue}; - }, - }), - ]; - return []; - }; + const hiddenToCorporateColumns = () => { + if (user && user.type !== "corporate") + return [ + columnHelper.accessor("agent", { + header: "Country Manager", + id: "agent", + cell: (info) => { + const { user, value } = columHelperValue(info.column.id, info); + return ( +
setSelectedAgentUser(user)} + > + {value} +
+ ); + }, + }), + columnHelper.accessor("agentCommission", { + header: "Commission", + id: "agentCommission", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return <>{value}; + }, + }), + columnHelper.accessor("agentValue", { + header: "Commission Value", + id: "agentValue", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + const finalValue = `${value} ${info.row.original.currency}`; + return {finalValue}; + }, + }), + ]; + return []; + }; - const defaultColumns = [ - columnHelper.accessor("corporate", { - header: "Corporate ID", - id: "corporateId", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return value; - }, - }), - columnHelper.accessor("corporate", { - header: "Corporate", - id: "corporate", - cell: (info) => { - const {user, value} = columHelperValue(info.column.id, info); - return ( -
setSelectedCorporateUser(user)}> - {value} -
- ); - }, - }), - columnHelper.accessor("date", { - header: "Date", - id: "date", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {value}; - }, - }), - columnHelper.accessor("value", { - header: "Amount", - id: "amount", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label; - const finalValue = `${value} ${currency}`; - return {finalValue}; - }, - }), - ...hiddenToCorporateColumns(), - columnHelper.accessor("isPaid", { - header: "Paid", - id: "isPaid", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); + const defaultColumns = [ + columnHelper.accessor("corporate", { + header: "Corporate ID", + id: "corporateId", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return value; + }, + }), + columnHelper.accessor("corporate", { + header: "Corporate", + id: "corporate", + cell: (info) => { + const { user, value } = columHelperValue(info.column.id, info); + return ( +
setSelectedCorporateUser(user)} + > + {value} +
+ ); + }, + }), + columnHelper.accessor("date", { + header: "Date", + id: "date", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {value}; + }, + }), + columnHelper.accessor("value", { + header: "Amount", + id: "amount", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + const currency = CURRENCIES.find( + (x) => x.currency === info.row.original.currency + )?.label; + const finalValue = `${value} ${currency}`; + return {finalValue}; + }, + }), + ...hiddenToCorporateColumns(), + columnHelper.accessor("isPaid", { + header: "Paid", + id: "isPaid", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); - return ( - { - if (user?.type === agent || user?.type === "corporate" || value) return null; - if (!info.row.original.commissionTransfer || !info.row.original.corporateTransfer) - return alert("All files need to be uploaded to consider it paid!"); - if (!confirm(`Are you sure you want to consider this payment paid?`)) return null; + return ( + { + if (user?.type === agent || user?.type === "corporate" || value) + return null; + if ( + !info.row.original.commissionTransfer || + !info.row.original.corporateTransfer + ) + return alert( + "All files need to be uploaded to consider it paid!" + ); + if ( + !confirm(`Are you sure you want to consider this payment paid?`) + ) + return null; - return updatePayment(info.row.original, "isPaid", e); - }}> - - - ); - }, - }), - ...getFileAssetsColumns(), - { - header: "", - id: "actions", - cell: ({row}: {row: {original: Payment}}) => { - return ( -
- {user?.type !== "agent" && ( -
deletePayment(row.original.id)}> - -
- )} -
- ); - }, - }, - ]; + return updatePayment(info.row.original, "isPaid", e); + }} + > + +
+ ); + }, + }), + ...getFileAssetsColumns(), + { + header: "", + id: "actions", + cell: ({ row }: { row: { original: Payment } }) => { + return ( +
+ {user?.type !== "agent" && ( +
deletePayment(row.original.id)} + > + +
+ )} +
+ ); + }, + }, + ]; - const table = useReactTable({ - data: displayPayments, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }); + const table = useReactTable({ + data: displayPayments, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); - const updatedPaypalPayments = useMemo( - () => - paypalPayments.map((p) => { - const user = users.find((x) => x.id === p.userId) as User; - return {...p, name: user?.name, email: user?.email}; - }), - [paypalPayments, users], - ); + const updatedPaypalPayments = useMemo( + () => + paypalPayments.map((p) => { + const user = users.find((x) => x.id === p.userId) as User; + return { ...p, name: user?.name, email: user?.email }; + }), + [paypalPayments, users] + ); - const paypalColumns = [ - paypalColumnHelper.accessor("orderId", { - header: "Order ID", - id: "orderId", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("status", { - header: "Status", - id: "status", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("name", { - header: "User Name", - id: "name", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("email", { - header: "Email", - id: "email", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("value", { - header: "Amount", - id: "value", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - const finalValue = `${value} ${info.row.original.currency}`; - return {finalValue}; - }, - }), - paypalColumnHelper.accessor("createdAt", { - header: "Date", - id: "createdAt", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {moment(value).format("DD/MM/YYYY")}; - }, - }), - paypalColumnHelper.accessor("subscriptionExpirationDate", { - header: "Expiration Date", - id: "subscriptionExpirationDate", - cell: (info) => { - const {value} = columHelperValue(info.column.id, info); - return {moment(value).format("DD/MM/YYYY")}; - }, - }), - ]; + const paypalColumns = [ + paypalColumnHelper.accessor("orderId", { + header: "Order ID", + id: "orderId", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("status", { + header: "Status", + id: "status", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("name", { + header: "User Name", + id: "name", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("email", { + header: "Email", + id: "email", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("value", { + header: "Amount", + id: "value", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + const finalValue = `${value} ${info.row.original.currency}`; + return {finalValue}; + }, + }), + paypalColumnHelper.accessor("createdAt", { + header: "Date", + id: "createdAt", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {moment(value).format("DD/MM/YYYY")}; + }, + }), + paypalColumnHelper.accessor("subscriptionExpirationDate", { + header: "Expiration Date", + id: "subscriptionExpirationDate", + cell: (info) => { + const { value } = columHelperValue(info.column.id, info); + return {moment(value).format("DD/MM/YYYY")}; + }, + }), + ]; - const {rows: filteredRows, renderSearch} = useListSearch(paypalFilterRows, updatedPaypalPayments); + const { rows: filteredRows, renderSearch } = useListSearch( + paypalFilterRows, + updatedPaypalPayments + ); - const paypalTable = useReactTable({ - data: filteredRows.sort((a, b) => moment(b.createdAt).diff(moment(a.createdAt), "second")), - columns: paypalColumns, - getCoreRowModel: getCoreRowModel(), - }); + const paypalTable = useReactTable({ + data: filteredRows.sort((a, b) => + moment(b.createdAt).diff(moment(a.createdAt), "second") + ), + columns: paypalColumns, + getCoreRowModel: getCoreRowModel(), + }); - const getUserModal = () => { - if (user) { - if (selectedCorporateUser) { - return ( - setSelectedCorporateUser(undefined)}> - <> - {selectedCorporateUser && ( -
- { - setSelectedCorporateUser(undefined); - if (shouldReload) reload(); - }} - user={selectedCorporateUser} - disabled - disabledFields={{countryManager: true}} - /> -
- )} - -
- ); - } + const getUserModal = () => { + if (user) { + if (selectedCorporateUser) { + return ( + setSelectedCorporateUser(undefined)} + > + <> + {selectedCorporateUser && ( +
+ { + setSelectedCorporateUser(undefined); + if (shouldReload) reload(); + }} + user={selectedCorporateUser} + disabled + disabledFields={{ countryManager: true }} + /> +
+ )} + +
+ ); + } - if (selectedAgentUser) { - return ( - setSelectedAgentUser(undefined)}> - <> - {selectedAgentUser && ( -
- { - setSelectedAgentUser(undefined); - if (shouldReload) reload(); - }} - user={selectedAgentUser} - /> -
- )} - -
- ); - } - } + if (selectedAgentUser) { + return ( + setSelectedAgentUser(undefined)} + > + <> + {selectedAgentUser && ( +
+ { + setSelectedAgentUser(undefined); + if (shouldReload) reload(); + }} + user={selectedAgentUser} + /> +
+ )} + +
+ ); + } + } - return null; - }; + return null; + }; - const getCSVData = () => { - const tables = [table, paypalTable]; - const whitelists = [CSV_PAYMENTS_WHITELISTED_KEYS, CSV_PAYPAL_WHITELISTED_KEYS]; - const currentTable = tables[selectedIndex]; - const whitelist = whitelists[selectedIndex]; - const columns = (currentTable.getHeaderGroups() as any[]).reduce((accm: any[], group: any) => { - const whitelistedColumns = group.headers.filter((header: any) => whitelist.includes(header.id)); + const getCSVData = () => { + const tables = [table, paypalTable]; + const whitelists = [ + CSV_PAYMENTS_WHITELISTED_KEYS, + CSV_PAYPAL_WHITELISTED_KEYS, + ]; + const currentTable = tables[selectedIndex]; + const whitelist = whitelists[selectedIndex]; + const columns = (currentTable.getHeaderGroups() as any[]).reduce( + (accm: any[], group: any) => { + const whitelistedColumns = group.headers.filter((header: any) => + whitelist.includes(header.id) + ); - const data = whitelistedColumns.map((data: any) => ({ - key: data.column.columnDef.id, - label: data.column.columnDef.header, - })) as SimpleCSVColumn[]; + const data = whitelistedColumns.map((data: any) => ({ + key: data.column.columnDef.id, + label: data.column.columnDef.header, + })) as SimpleCSVColumn[]; - return [...accm, ...data]; - }, []); + return [...accm, ...data]; + }, + [] + ); - const {rows} = currentTable.getRowModel(); + const { rows } = currentTable.getRowModel(); - const finalColumns = [ - ...columns, - { - key: "currency", - label: "Currency", - }, - ]; + const finalColumns = [ + ...columns, + { + key: "currency", + label: "Currency", + }, + ]; - return { - columns: finalColumns, - rows: rows.map((row) => { - return finalColumns.reduce((accm, {key}) => { - const {value} = columHelperValue(key, { - row, - getValue: () => row.getValue(key), - }); - return { - ...accm, - [key]: value, - }; - }, {}); - }), - }; - }; + return { + columns: finalColumns, + rows: rows.map((row) => { + return finalColumns.reduce((accm, { key }) => { + const { value } = columHelperValue(key, { + row, + getValue: () => row.getValue(key), + }); + return { + ...accm, + [key]: value, + }; + }, {}); + }), + }; + }; - const {rows: csvRows, columns: csvColumns} = getCSVData(); + const { rows: csvRows, columns: csvColumns } = getCSVData(); - const renderTable = (table: Table) => ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
- ); + const renderTable = (table: Table) => ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); - return ( - <> - - Payment Record | EnCoach - - - - - - {user && ( - - {getUserModal()} - setIsCreatingPayment(false)}> - setIsCreatingPayment(false)} - reload={reload} - showComission={user.type === "developer" || user.type === "admin"} - /> - + return ( + <> + + Payment Record | EnCoach + + + + + + {user && ( + + {getUserModal()} + setIsCreatingPayment(false)} + > + setIsCreatingPayment(false)} + reload={reload} + showComission={checkAccess(user, ["developer", "admin"])} + /> + -
-

Payment Record

-
- {(user.type === "developer" || user.type === "admin" || user.type === "agent" || user.type === "corporate") && ( - - )} - {(user.type === "developer" || user.type === "admin") && ( - - )} -
-
- - - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - Payments - - {["admin", "developer"].includes(user.type) && ( - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - Paymob - - )} - - - -
-
- - u.type === "agent") as AgentUser[]).map((user) => ({ - value: user.id, - meta: user, - label: `${user.name} - ${user.email}`, - }))} - value={ - agent - ? { - value: agent?.id, - label: `${agent.name} - ${agent.email}`, - } - : undefined - } - onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)} - menuPortalTarget={document?.body} - styles={{ - menuPortal: (base) => ({...base, zIndex: 9999}), - control: (styles) => ({ - ...styles, - paddingLeft: "4px", - border: "none", - outline: "none", - ":focus": { - outline: "none", - }, - }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - /> -
- )} -
- - e.value === commissionTransfer)} - onChange={(value) => { - if (value) return setCommissionTransfer(value.value); - setCommissionTransfer(null); - }} - menuPortalTarget={document?.body} - styles={{ - menuPortal: (base) => ({...base, zIndex: 9999}), - control: (styles) => ({ - ...styles, - paddingLeft: "4px", - border: "none", - outline: "none", - ":focus": { - outline: "none", - }, - }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - /> -
- )} -
- - u.type === "corporate" + ) as CorporateUser[] + ).map((user) => ({ + value: user.id, + meta: user, + label: `${ + user.corporateInformation?.companyInformation?.name || + user.name + } - ${user.email}`, + }))} + defaultValue={ + user.type === "corporate" + ? { + value: user.id, + meta: user, + label: `${ + user.corporateInformation?.companyInformation + ?.name || user.name + } - ${user.email}`, + } + : undefined + } + isDisabled={user.type === "corporate"} + onChange={(value) => + setCorporate((value as any)?.meta ?? undefined) + } + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused + ? "#D5D9F0" + : state.isSelected + ? "#7872BF" + : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+ {user.type !== "corporate" && ( +
+ + e.value === paid)} + onChange={(value) => { + if (value) return setPaid(value.value); + setPaid(null); + }} + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused + ? "#D5D9F0" + : state.isSelected + ? "#7872BF" + : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+
+ + + moment(date).isSameOrBefore(moment(new Date())) + } + onChange={([initialDate, finalDate]: [Date, Date]) => { + setStartDate( + initialDate ?? moment("01/01/2023").toDate() + ); + if (finalDate) { + // basicly selecting a final day works as if I'm selecting the first + // minute of that day. this way it covers the whole day + setEndDate(moment(finalDate).endOf("day").toDate()); + return; + } + setEndDate(null); + }} + /> +
+ {user.type !== "corporate" && ( +
+ + e.value === corporateTransfer + )} + onChange={(value) => { + if (value) return setCorporateTransfer(value.value); + setCorporateTransfer(null); + }} + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused + ? "#D5D9F0" + : state.isSelected + ? "#7872BF" + : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+
+ {renderTable(table as Table)} +
+ + {renderSearch()} + {renderTable(paypalTable as Table)} + +
+
+
+ )} + + ); } diff --git a/src/pages/permissions/[id].tsx b/src/pages/permissions/[id].tsx new file mode 100644 index 00000000..093c946b --- /dev/null +++ b/src/pages/permissions/[id].tsx @@ -0,0 +1,190 @@ +/* eslint-disable @next/next/no-img-element */ +import Head from "next/head"; +import { useState } from "react"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { Permission, PermissionType } from "@/interfaces/permissions"; +import { getPermissionDoc } from "@/utils/permissions.be"; +import { User } from "@/interfaces/user"; +import Layout from "@/components/High/Layout"; +import { getUsers } from "@/utils/users.be"; +import { BsTrash } from "react-icons/bs"; +import Select from "@/components/Low/Select"; +import Button from "@/components/Low/Button"; +import axios from "axios"; +import { toast, ToastContainer } from "react-toastify"; + +interface BasicUser { + id: string; + name: string; +} + +interface PermissionWithBasicUsers { + id: string; + type: PermissionType; + users: BasicUser[]; +} + +export const getServerSideProps = withIronSessionSsr(async (context) => { + const { req, params } = context; + const user = req.session.user; + + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + if (!params?.id) { + return { + redirect: { + destination: "/permissions", + permanent: false, + }, + }; + } + + // Fetch data from external API + const permission: Permission = await getPermissionDoc(params.id as string); + + const allUserData: User[] = await getUsers(); + const users = allUserData.map((u) => ({ + id: u.id, + name: u.name, + })) as BasicUser[]; + + // const res = await fetch("api/permissions"); + // const permissions: Permission[] = await res.json(); + // Pass data to the page via props + const usersData: BasicUser[] = permission.users.reduce( + (acc: BasicUser[], userId) => { + const user = users.find((u) => u.id === userId) as BasicUser; + if (user) { + acc.push(user); + } + return acc; + }, + [] + ); + + return { + props: { + // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), + permission: { + ...permission, + id: params.id, + users: usersData, + }, + user: req.session.user, + users, + }, + }; +}, sessionOptions); + +interface Props { + permission: PermissionWithBasicUsers; + user: User; + users: BasicUser[]; +} + +export default function Page(props: Props) { + console.log("Props", props); + + const { permission, user, users } = props; + + const [selectedUsers, setSelectedUsers] = useState(() => + permission.users.map((u) => u.id) + ); + + const onChange = (value: any) => { + console.log("value", value); + setSelectedUsers((prev) => { + if (value?.value) { + return [...prev, value?.value]; + } + return prev; + }); + }; + const removeUser = (id: string) => { + setSelectedUsers((prev) => prev.filter((u) => u !== id)); + }; + + const update = async () => { + console.log("update", selectedUsers); + try { + await axios.patch(`/api/permissions/${permission.id}`, { + users: selectedUsers, + }); + toast.success("Permission updated"); + } catch (err) { + toast.error("Failed to update permission"); + } + }; + + return ( + <> + + EnCoach + + + + + + +

+ Permission: {permission.type as string} +

+
+ setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> - ) : ( - - setCorporateInformation((prev) => ({ - ...prev!, - companyInformation: { - ...prev!.companyInformation, - name: e, - }, - })) - } - placeholder="Enter your company's name" - defaultValue={corporateInformation?.companyInformation.name} - required - /> - )} + return ( + +
+

Edit Profile

+
+
+

Edit Profile

+
e.preventDefault()} + > + + {user.type !== "corporate" ? ( + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> + ) : ( + + setCorporateInformation((prev) => ({ + ...prev!, + companyInformation: { + ...prev!.companyInformation, + name: e, + }, + })) + } + placeholder="Enter your company's name" + defaultValue={corporateInformation?.companyInformation.name} + required + /> + )} - {user.type === "agent" && ( - setArabName(e)} - placeholder="Enter your arab name" - defaultValue={arabName} - required - /> - )} + {user.type === "agent" && ( + setArabName(e)} + placeholder="Enter your arab name" + defaultValue={arabName} + required + /> + )} - setEmail(e)} - placeholder="Enter email address" - defaultValue={email} - required - /> - - - setPassword(e)} - placeholder="Enter your password" - required - /> - setNewPassword(e)} - placeholder="Enter your new password (optional)" - /> - - {user.type === "agent" && ( -
- null} - placeholder="Enter your company's name" - defaultValue={companyName} - disabled - /> - null} - placeholder="Enter commercial registration" - defaultValue={commercialRegistration} - disabled - /> -
- )} + setEmail(e)} + placeholder="Enter email address" + defaultValue={email} + required + /> + + + setPassword(e)} + placeholder="Enter your password" + required + /> + setNewPassword(e)} + placeholder="Enter your new password (optional)" + /> + + {user.type === "agent" && ( +
+ null} + placeholder="Enter your company's name" + defaultValue={companyName} + disabled + /> + null} + placeholder="Enter commercial registration" + defaultValue={commercialRegistration} + disabled + /> +
+ )} - -
- - -
- setPhone(e)} - placeholder="Enter phone number" - defaultValue={phone} - required - /> -
+ +
+ + +
+ setPhone(e)} + placeholder="Enter phone number" + defaultValue={phone} + required + /> +
- {user.type === "student" ? ( - - setPassportID(e)} - placeholder="Enter National ID or Passport number" - value={passport_id} - required - /> - - - ) : ( - - )} + {user.type === "student" ? ( + + setPassportID(e)} + placeholder="Enter National ID or Passport number" + value={passport_id} + required + /> + + + ) : ( + + )} - + - {desiredLevels && ["developer", "student"].includes(user.type) && ( - <> -
- - >} - /> -
-
- -
- - -
-
- - )} + {desiredLevels && + ["developer", "student"].includes(user.type) && ( + <> +
+ + + > + } + /> +
+
+ +
+ + +
+
+ + )} - {preferredGender && ["developer", "student"].includes(user.type) && ( - <> - - -
- - + value + ? setPreferredGender( + value.value as InstructorGender + ) + : null + } + options={[ + { value: "male", label: "Male" }, + { value: "female", label: "Female" }, + { value: "varied", label: "Varied" }, + ]} + /> +
+
+ + +
+
- setIsPreferredTopicsOpen(false)} - selectTopics={setPreferredTopics} - initialTopics={preferredTopics || []} - /> + setIsPreferredTopicsOpen(false)} + selectTopics={setPreferredTopics} + initialTopics={preferredTopics || []} + /> - - - )} + + + )} - {user.type === "corporate" && ( - <> - - null} - label="Number of users" - defaultValue={user.corporateInformation.companyInformation.userAmount} - disabled - required - /> - null} - label="Pricing" - defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} - disabled - required - /> - - - - )} + {user.type === "corporate" && ( + <> + + null} + label="Number of users" + defaultValue={ + user.corporateInformation.companyInformation.userAmount + } + disabled + required + /> + null} + label="Pricing" + defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} + disabled + required + /> + + + + )} - {user.type === "corporate" && ( - <> - - - setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> - - - - )} + {user.type === "corporate" && ( + <> + + + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> + + + + )} - {user.type === "corporate" && user.corporateInformation.referralAgent && ( - <> - - - null} - defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name} - type="text" - label="Country Manager's Name" - placeholder="Not available" - required - disabled - /> - null} - defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email} - type="text" - label="Country Manager's E-mail" - placeholder="Not available" - required - disabled - /> - - -
- - x.id === user.corporateInformation.referralAgent)?.demographicInformation - ?.country - } - onChange={() => null} - disabled - /> -
+ {user.type === "corporate" && + user.corporateInformation.referralAgent && ( + <> + + + null} + defaultValue={ + users.find( + (x) => + x.id === user.corporateInformation.referralAgent + )?.name + } + type="text" + label="Country Manager's Name" + placeholder="Not available" + required + disabled + /> + null} + defaultValue={ + users.find( + (x) => + x.id === user.corporateInformation.referralAgent + )?.email + } + type="text" + label="Country Manager's E-mail" + placeholder="Not available" + required + disabled + /> + + +
+ + + x.id === user.corporateInformation.referralAgent + )?.demographicInformation?.country + } + onChange={() => null} + disabled + /> +
- null} - placeholder="Not available" - defaultValue={ - users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone - } - disabled - required - /> -
- - )} + null} + placeholder="Not available" + defaultValue={ + users.find( + (x) => + x.id === user.corporateInformation.referralAgent + )?.demographicInformation?.phone + } + disabled + required + /> +
+ + )} - {user.type !== "corporate" && ( - - + {user.type !== "corporate" && ( + + -
- - -
-
- )} - -
-
-
(profilePictureInput.current as any)?.click()}> -
-
- -
- {user.name} -
- - (profilePictureInput.current as any)?.click()} - className="cursor-pointer text-mti-purple-light text-sm"> - Change picture - -
{USER_TYPE_LABELS[user.type]}
-
- {user.type === "agent" && ( -
- {user.demographicInformation?.country.toLowerCase() -
- )} - {manualDownloadLink && ( - - - - )} -
-
-
- Bio -