Merged develop into feature/ExamGenRework

This commit is contained in:
carlos.mesquita
2024-12-11 15:34:09 +00:00
53 changed files with 2084 additions and 6520 deletions

View File

@@ -1,18 +1,18 @@
import ProgressBar from "@/components/Low/ProgressBar";
import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces";
import {Assignment} from "@/interfaces/results";
import {calculateBandScore} from "@/utils/score";
import { Module } from "@/interfaces";
import { Assignment } from "@/interfaces/results";
import { calculateBandScore } from "@/utils/score";
import clsx from "clsx";
import moment from "moment";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
import {usePDFDownload} from "@/hooks/usePDFDownload";
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
import {uniqBy} from "lodash";
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
import {useAssignmentRelease} from "@/hooks/useAssignmentRelease";
import {getUserName} from "@/utils/users";
import {User} from "@/interfaces/user";
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
import { usePDFDownload } from "@/hooks/usePDFDownload";
import { useAssignmentArchive } from "@/hooks/useAssignmentArchive";
import { uniqBy } from "lodash";
import { useAssignmentUnarchive } from "@/hooks/useAssignmentUnarchive";
import { useAssignmentRelease } from "@/hooks/useAssignmentRelease";
import { getUserName } from "@/utils/users";
import { User } from "@/interfaces/user";
import { EntityWithRoles } from "@/interfaces/entity";
interface Props {
@@ -71,7 +71,7 @@ export default function AssignmentCard({
// in order to be downloadable, the assignment has to be released
// the component should have the allowDownload prop
// and the assignment should not have the level module
return uniqModules.every(({module}) => module !== "level");
return uniqModules.every(({ module }) => module !== "level");
}
return false;
@@ -82,7 +82,7 @@ export default function AssignmentCard({
// in order to be downloadable, the assignment has to be released
// the component should have the allowExcelDownload prop
// and the assignment should have the level module
return uniqModules.some(({module}) => module === "level");
return uniqModules.some(({ module }) => module === "level");
}
return false;
@@ -121,7 +121,7 @@ export default function AssignmentCard({
{entityObj && <span>Entity: {entityObj.label}</span>}
</div>
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
{uniqModules.map(({module}) => (
{uniqModules.map(({ module }) => (
<div
key={module}
className={clsx(

View File

@@ -159,7 +159,7 @@ export default function AssignmentView({ isOpen, users, assignment, onClose }: P
<span className="font-medium">{formatTimestamp(stats[0].date.toString())}</span>
{timeSpent && (
<>
<span className="md:hidden 2xl:flex"> </span>
<span className="md:hidden 2xl:flex">⢠</span>
<span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span>
</>
)}

View File

@@ -6,72 +6,68 @@ import { BsArrowRepeat } from "react-icons/bs";
import { toast } from "react-toastify";
interface Props {
invite: Invite;
users: User[];
reload: () => void;
invite: Invite;
users: User[];
reload: () => void;
}
export default function InviteCard({ invite, users, reload }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const inviter = users.find((u) => u.id === invite.from);
const name = !inviter
? null
: inviter.type === "corporate"
? inviter.corporateInformation?.companyInformation?.name || inviter.name
: inviter.name;
const inviter = users.find((u) => u.id === invite.from);
const name = !inviter ? null : inviter.name;
const decide = (decision: "accept" | "decline") => {
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
const decide = (decision: "accept" | "decline") => {
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
setIsLoading(true);
axios
.get(`/api/invites/${decision}/${invite.id}`)
.then(() => {
toast.success(
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
{ toastId: "success" },
);
reload();
})
.catch((e) => {
toast.success(`Something went wrong, please try again later!`, {
toastId: "error",
});
reload();
})
.finally(() => setIsLoading(false));
};
setIsLoading(true);
axios
.get(`/api/invites/${decision}/${invite.id}`)
.then(() => {
toast.success(
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
{ toastId: "success" },
);
reload();
})
.catch((e) => {
toast.success(`Something went wrong, please try again later!`, {
toastId: "error",
});
reload();
})
.finally(() => setIsLoading(false));
};
return (
<div className="border-mti-gray-anti-flash flex min-w-[200px] flex-col gap-6 rounded-xl border p-4 text-black">
<span>Invited by {name}</span>
<div className="flex items-center gap-2">
<button
onClick={() => decide("accept")}
disabled={isLoading}
className="bg-mti-green-ultralight hover:bg-mti-green-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
>
{!isLoading && "Accept"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
<button
onClick={() => decide("decline")}
disabled={isLoading}
className="bg-mti-red-ultralight hover:bg-mti-red-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
>
{!isLoading && "Decline"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
</div>
</div>
);
return (
<div className="border-mti-gray-anti-flash flex min-w-[200px] flex-col gap-6 rounded-xl border p-4 text-black">
<span>Invited by {name}</span>
<div className="flex items-center gap-2">
<button
onClick={() => decide("accept")}
disabled={isLoading}
className="bg-mti-green-ultralight hover:bg-mti-green-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
>
{!isLoading && "Accept"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
<button
onClick={() => decide("decline")}
disabled={isLoading}
className="bg-mti-red-ultralight hover:bg-mti-red-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
>
{!isLoading && "Decline"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
</div>
</div>
);
}

View File

@@ -1,25 +1,25 @@
import {User} from "@/interfaces/user";
import { User } from "@/interfaces/user";
import Link from "next/link";
import FocusLayer from "@/components/FocusLayer";
import {preventNavigation} from "@/utils/navigation.disabled";
import {useRouter} from "next/router";
import {BsList, BsQuestionCircle, BsQuestionCircleFill} from "react-icons/bs";
import { preventNavigation } from "@/utils/navigation.disabled";
import { useRouter } from "next/router";
import { BsList, BsQuestionCircle, BsQuestionCircleFill } from "react-icons/bs";
import clsx from "clsx";
import moment from "moment";
import MobileMenu from "./MobileMenu";
import {useEffect, useState} from "react";
import {Type} from "@/interfaces/user";
import {USER_TYPE_LABELS} from "@/resources/user";
import { useEffect, useState } from "react";
import { Type } from "@/interfaces/user";
import { USER_TYPE_LABELS } from "@/resources/user";
import useGroups from "@/hooks/useGroups";
import {isUserFromCorporate} from "@/utils/groups";
import { isUserFromCorporate } from "@/utils/groups";
import Button from "./Low/Button";
import Modal from "./Modal";
import Input from "./Low/Input";
import TicketSubmission from "./High/TicketSubmission";
import {Module} from "@/interfaces";
import { Module } from "@/interfaces";
import Badge from "./Low/Badge";
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
import { BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
interface Props {
user: User;
navDisabled?: boolean;
@@ -29,7 +29,7 @@ interface Props {
}
/* eslint-disable @next/next/no-img-element */
export default function Navbar({user, path, navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) {
export default function Navbar({ user, path, navDisabled = false, focusMode = false, onFocusLayerMouseEnter }: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [disablePaymentPage, setDisablePaymentPage] = useState(true);
const [isTicketOpen, setIsTicketOpen] = useState(false);
@@ -109,9 +109,8 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
badges.map((badge) => (
<div
key={badge.module}
className={`${
badge.achieved ? `bg-ielts-${badge.module}` : "bg-mti-gray-anti-flash"
} flex h-8 w-8 items-center justify-center rounded-full`}>
className={`${badge.achieved ? `bg-ielts-${badge.module}` : "bg-mti-gray-anti-flash"
} flex h-8 w-8 items-center justify-center rounded-full`}>
{badge.icon()}
</div>
))}
@@ -145,9 +144,6 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
<Link href={disableNavigation ? "" : "/profile"} className="-md:hidden flex items-center justify-end gap-6">
<img src={user.profilePicture} alt={user.name} className="h-10 w-10 rounded-full object-cover" />
<span className="-md:hidden text-right">
{(user.type === "corporate" || user.type === "mastercorporate") && !!user.corporateInformation?.companyInformation?.name
? `${user.corporateInformation?.companyInformation.name} |`
: ""}{" "}
{user.name} | {USER_TYPE_LABELS[user.type]}
{user.type === "corporate" &&
!!user.demographicInformation?.position &&

View File

@@ -1,15 +1,15 @@
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type, Stat, Gender} from "@/interfaces/user";
import {groupBySession, averageScore} from "@/utils/stats";
import {RadioGroup} from "@headlessui/react";
import { CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type, Stat, Gender } 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,12 +17,12 @@ 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";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import { PERMISSIONS } from "@/constants/userPermissions";
import { PermissionType } from "@/interfaces/permissions";
import usePermissions from "@/hooks/usePermissions";
const expirationDateColor = (date: Date) => {
@@ -68,7 +68,7 @@ const USER_TYPE_OPTIONS = Object.keys(USER_TYPE_LABELS).map((type) => ({
label: USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS],
}));
const CURRENCIES_OPTIONS = CURRENCIES.map(({label, currency}) => ({
const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({
value: currency,
label,
}));
@@ -100,9 +100,7 @@ const UserCard = ({
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.referralAgent : undefined,
);
const [companyName, setCompanyName] = useState(
user.type === "corporate" || user.type === "mastercorporate"
? user.corporateInformation?.companyInformation.name
: user.type === "agent"
user.type === "agent"
? user.agentInformation?.companyName
: undefined,
);
@@ -110,25 +108,19 @@ const UserCard = ({
const [commercialRegistration, setCommercialRegistration] = useState(
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
);
const [userAmount, setUserAmount] = useState(
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.companyInformation.userAmount : undefined,
);
const [paymentValue, setPaymentValue] = useState(
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.value : undefined,
);
const [paymentCurrency, setPaymentCurrency] = useState(
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.currency : "EUR",
);
const [monthlyDuration, setMonthlyDuration] = useState(
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.monthlyDuration : undefined,
);
const [commissionValue, setCommission] = useState(
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.commission : undefined,
);
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id);
const {users} = useUsers();
const {codes} = useCodes(user.id);
const {permissions} = usePermissions(loggedInUser.id);
const { data: stats } = useFilterRecordsByUser<Stat[]>(user.id);
const { users } = useUsers();
const { codes } = useCodes(user.id);
const { permissions } = usePermissions(loggedInUser.id);
useEffect(() => {
if (users && users.length > 0) {
@@ -153,7 +145,7 @@ const UserCard = ({
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}`, {
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
...user,
subscriptionExpirationDate: expiryDate,
studentID,
@@ -167,26 +159,21 @@ const UserCard = ({
agentInformation:
type === "agent"
? {
companyName,
commercialRegistration,
arabName,
}
companyName,
commercialRegistration,
arabName,
}
: undefined,
corporateInformation:
type === "corporate" || type === "mastercorporate"
? {
referralAgent,
monthlyDuration,
companyInformation: {
name: companyName,
userAmount,
},
payment: {
value: paymentValue,
currency: paymentCurrency,
...(referralAgent === "" ? {} : {commission: commissionValue}),
},
}
referralAgent,
payment: {
value: paymentValue,
currency: paymentCurrency,
...(referralAgent === "" ? {} : { commission: commissionValue }),
},
}
: undefined,
})
.then(() => {
@@ -194,7 +181,7 @@ const UserCard = ({
onClose(true);
})
.catch(() => {
toast.error("Something went wrong!", {toastId: "update-error"});
toast.error("Something went wrong!", { toastId: "update-error" });
});
};
@@ -216,31 +203,16 @@ const UserCard = ({
},
];
const corporateProfileItems =
user.type === "corporate" || user.type === "mastercorporate"
? [
{
icon: <BsPerson className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: codes.length,
label: "Users Used",
},
{
icon: <BsPersonAdd className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: user.corporateInformation?.companyInformation?.userAmount,
label: "Number of Users",
},
]
: [];
const updateUserPermission = PERMISSIONS.updateUser[user.type] as {
list: Type[];
perm: PermissionType;
};
return (
<>
<ProfileSummary
user={user}
items={user.type === "corporate" || user.type === "mastercorporate" ? corporateProfileItems : generalProfileItems}
items={user.type === "corporate" || user.type === "mastercorporate" ? [] : generalProfileItems}
/>
{user.type === "agent" && (
@@ -283,48 +255,6 @@ const UserCard = ({
{(user.type === "corporate" || user.type === "mastercorporate") && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
<Input
label="Corporate Name"
type="text"
name="companyName"
onChange={setCompanyName}
placeholder="Enter corporate name"
defaultValue={companyName}
disabled={
disabled ||
checkAccess(
loggedInUser,
getTypesOfUser(
user.type === "mastercorporate" ? ["developer", "admin"] : ["developer", "admin", "mastercorporate"],
),
)
}
/>
<Input
label="Number of Users"
type="number"
name="userAmount"
max={maxUserAmount}
onChange={(e) => setUserAmount(e ? parseInt(e) : undefined)}
placeholder="Enter number of users"
defaultValue={userAmount}
disabled={
disabled ||
checkAccess(
loggedInUser,
getTypesOfUser(["developer", "admin", ...((user.type === "corporate" ? ["mastercorporate"] : []) as Type[])]),
)
}
/>
<Input
label="Monthly Duration"
type="number"
name="monthlyDuration"
onChange={(e) => setMonthlyDuration(e ? parseInt(e) : undefined)}
placeholder="Enter monthly duration"
defaultValue={monthlyDuration}
disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))}
/>
<div className="flex flex-col gap-3 w-full lg:col-span-3">
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
<div className="w-full grid grid-cols-6 gap-2">
@@ -346,7 +276,7 @@ const UserCard = ({
onChange={(value) => setPaymentCurrency(value?.value)}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -375,10 +305,10 @@ const UserCard = ({
className={clsx(
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
(checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"])) || disabledFields.countryManager) &&
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
)}
options={[
{value: "", label: "No referral"},
{ value: "", label: "No referral" },
...users
.filter((u) => u.type === "agent")
.map((x) => ({
@@ -393,7 +323,7 @@ const UserCard = ({
menuPortalTarget={document?.body}
onChange={(value) => setReferralAgent(value?.value)}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -505,9 +435,9 @@ const UserCard = ({
value={user.demographicInformation?.employment}
className="grid grid-cols-2 items-center gap-4 place-items-center"
disabled={disabled}>
{EMPLOYMENT_STATUS.map(({status, label}) => (
{EMPLOYMENT_STATUS.map(({ status, label }) => (
<RadioGroup.Option value={status} key={status}>
{({checked}) => (
{({ checked }) => (
<span
className={clsx(
"px-6 py-4 w-40 md:w-48 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
@@ -545,7 +475,7 @@ const UserCard = ({
className="flex flex-row gap-4 justify-between"
disabled={disabled}>
<RadioGroup.Option value="male">
{({checked}) => (
{({ checked }) => (
<span
className={clsx(
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
@@ -559,7 +489,7 @@ const UserCard = ({
)}
</RadioGroup.Option>
<RadioGroup.Option value="female">
{({checked}) => (
{({ checked }) => (
<span
className={clsx(
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
@@ -573,7 +503,7 @@ const UserCard = ({
)}
</RadioGroup.Option>
<RadioGroup.Option value="other">
{({checked}) => (
{({ checked }) => (
<span
className={clsx(
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
@@ -643,92 +573,92 @@ const UserCard = ({
permissions,
user.type === "teacher" ? "editTeacher" : user.type === "student" ? "editStudent" : undefined,
) && (
<>
<Divider className="w-full !m-0" />
<div className="flex flex-col md:flex-row gap-8 w-full">
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Status</label>
<Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={USER_STATUS_OPTIONS.filter((x) => {
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
return x.value !== "paymentDue";
})}
menuPortalTarget={document?.body}
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
onChange={(value) => setStatus(value?.value as typeof user.status)}
styles={{
control: (styles) => ({
...styles,
paddingLeft: "4px",
border: "none",
outline: "none",
":focus": {
<>
<Divider className="w-full !m-0" />
<div className="flex flex-col md:flex-row gap-8 w-full">
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Status</label>
<Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={USER_STATUS_OPTIONS.filter((x) => {
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
return x.value !== "paymentDue";
})}
menuPortalTarget={document?.body}
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
onChange={(value) => setStatus(value?.value as typeof user.status)}
styles={{
control: (styles) => ({
...styles,
paddingLeft: "4px",
border: "none",
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}
/>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Type</label>
<Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={USER_TYPE_OPTIONS.filter((x) => {
if (x.value === "student")
return checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
"editStudent",
);
":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}
/>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Type</label>
<Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={USER_TYPE_OPTIONS.filter((x) => {
if (x.value === "student")
return checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
"editStudent",
);
if (x.value === "teacher")
return checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
"editTeacher",
);
if (x.value === "teacher")
return checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
"editTeacher",
);
if (x.value === "corporate")
return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate");
if (x.value === "corporate")
return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate");
return checkAccess(loggedInUser, ["developer", "admin"]);
})}
menuPortalTarget={document?.body}
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
onChange={(value) => setType(value?.value as typeof user.type)}
styles={{
control: (styles) => ({
...styles,
paddingLeft: "4px",
border: "none",
outline: "none",
":focus": {
return checkAccess(loggedInUser, ["developer", "admin"]);
})}
menuPortalTarget={document?.body}
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
onChange={(value) => setType(value?.value as typeof user.type)}
styles={{
control: (styles) => ({
...styles,
paddingLeft: "4px",
border: "none",
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}
/>
":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}
/>
</div>
</div>
</div>
</>
)}
</>
)}
</section>
<div className="flex gap-4 justify-between mt-4 w-full">

View File

@@ -1,647 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {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,
BsBriefcaseFill,
BsGlobeCentralSouthAsia,
BsPerson,
BsPersonFill,
BsPencilSquare,
BsBank,
BsCurrencyDollar,
BsLayoutWtf,
BsLayoutSidebar,
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import IconCard from "./IconCard";
import useFilterStore from "@/stores/listFilterStore";
import {useRouter} from "next/router";
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
import CorporateStudentsLevels from "./CorporateStudentsLevels";
interface Props {
user: User;
}
const studentHash = {
type: "student",
size: 25,
orderBy: "registrationDate",
};
const teacherHash = {
type: "teacher",
size: 25,
orderBy: "registrationDate",
};
const corporateHash = {
type: "corporate",
size: 25,
orderBy: "registrationDate",
};
const agentsHash = {
type: "agent",
size: 25,
orderBy: "registrationDate",
};
export default function AdminDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash);
const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash);
const {users: corporates, total: totalCorporate, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(corporateHash);
const {users: agents, total: totalAgents, reload: reloadAgents, isLoading: isAgentsLoading} = useUsers(agentsHash);
const {groups} = useGroups({});
const {pending, done} = usePaymentStatusUsers();
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
useEffect(() => {
setShowModal(!!selectedUser && router.asPath === "/#");
}, [selectedUser, router.asPath]);
const inactiveCountryManagerFilter = (x: User) => x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate);
const UserDisplay = (displayUser: User) => (
<div
onClick={() => 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">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>
{displayUser.type === "corporate"
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
: displayUser.name}
</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const StudentsList = () => {
const filter = (x: User) =>
!!selectedUser
? groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id)
: true;
return (
<UserList
user={user}
type="student"
filters={[filter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
};
const TeachersList = () => {
const filter = (x: User) =>
!!selectedUser
? groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id) || false
: true;
return (
<UserList
user={user}
type="teacher"
filters={[filter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
</div>
)}
/>
);
};
const AgentsList = () => {
return (
<UserList
user={user}
type="agent"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Country Managers ({total})</h2>
</div>
)}
/>
);
};
const CorporateList = () => (
<UserList
user={user}
type="corporate"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
</div>
)}
/>
);
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => list.includes(x.id);
return (
<UserList
user={user}
type="corporate"
filters={[filter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
{paid ? "Payment Done" : "Pending Payment"} ({total})
</h2>
</div>
)}
/>
);
};
const InactiveCountryManagerList = () => {
return (
<UserList
user={user}
type="agent"
filters={[inactiveCountryManagerFilter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Inactive Country Managers ({total})</h2>
</div>
)}
/>
);
};
const InactiveStudentsList = () => {
const filter = (x: User) => x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate);
return (
<UserList
user={user}
type="student"
filters={[filter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Inactive Students ({total})</h2>
</div>
)}
/>
);
};
const InactiveCorporateList = () => {
const filter = (x: User) => x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate);
return (
<UserList
user={user}
filters={[filter]}
type="corporate"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Inactive Corporate ({total})</h2>
</div>
)}
/>
);
};
const CorporateStudentsLevelsHelper = () => {
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Corporate Students Levels</h2>
</div>
<CorporateStudentsLevels />
</>
);
};
const DefaultDashboard = () => (
<>
<section className="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 place-items-center items-center justify-between">
<IconCard
Icon={BsPersonFill}
isLoading={isStudentsLoading}
label="Students"
value={totalStudents}
onClick={() => router.push("/#students")}
color="purple"
/>
<IconCard
Icon={BsPencilSquare}
isLoading={isTeachersLoading}
label="Teachers"
value={totalTeachers}
onClick={() => router.push("/#teachers")}
color="purple"
/>
<IconCard
Icon={BsBank}
isLoading={isCorporatesLoading}
label="Corporate"
value={totalCorporate}
onClick={() => router.push("/#corporate")}
color="purple"
/>
<IconCard
Icon={BsBriefcaseFill}
isLoading={isAgentsLoading}
label="Country Managers"
value={totalAgents}
onClick={() => router.push("/#agents")}
color="purple"
/>
<IconCard
Icon={BsGlobeCentralSouthAsia}
isLoading={isAgentsLoading}
label="Countries"
value={[...new Set(agents.filter((x) => x.demographicInformation).map((x) => x.demographicInformation?.country))].length}
color="purple"
/>
<IconCard
onClick={() => router.push("/#inactiveStudents")}
Icon={BsPersonFill}
isLoading={isStudentsLoading}
label="Inactive Students"
value={
students.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
.length
}
color="rose"
/>
<IconCard
onClick={() => router.push("/#inactiveCountryManagers")}
Icon={BsBriefcaseFill}
isLoading={isAgentsLoading}
label="Inactive Country Managers"
value={agents.filter(inactiveCountryManagerFilter).length}
color="rose"
/>
<IconCard
onClick={() => router.push("/#inactiveCorporate")}
Icon={BsBank}
isLoading={isCorporatesLoading}
label="Inactive Corporate"
value={
corporates.filter(
(x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)),
).length
}
color="rose"
/>
<IconCard
onClick={() => router.push("/#paymentdone")}
Icon={BsCurrencyDollar}
label="Payment Done"
value={done.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/#paymentpending")}
Icon={BsCurrencyDollar}
label="Pending Payment"
value={pending.length}
color="rose"
/>
<IconCard
onClick={() => router.push("https://cms.encoach.com/admin")}
Icon={BsLayoutSidebar}
label="Content Management System (CMS)"
color="green"
/>
<IconCard
onClick={() => router.push("/#corporatestudentslevels")}
Icon={BsPersonFill}
isLoading={isStudentsLoading}
label="Corporate Students Levels"
color="purple"
/>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest teachers</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{teachers
.sort((a, b) => {
return dateSorter(a, b, "desc", "registrationDate");
})
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{corporates
.sort((a, b) => {
return dateSorter(a, b, "desc", "registrationDate");
})
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Unpaid Corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{corporates
.filter((x) => x.status === "paymentDue")
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Students expiring in 1 month</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.filter(
(x) =>
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Teachers expiring in 1 month</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{teachers
.filter(
(x) =>
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Country Manager expiring in 1 month</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{agents
.filter(
(x) =>
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Corporate expiring in 1 month</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{corporates
.filter(
(x) =>
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Expired Students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.filter((x) => x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Expired Teachers</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{teachers
.filter((x) => x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Expired Country Manager</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{agents
.filter((x) => x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Expired Corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{corporates
.filter((x) => x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
</section>
</>
);
return (
<>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<>
{selectedUser && (
<div className="w-full flex flex-col gap-8">
<UserCard
loggedInUser={user}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload && selectedUser!.type === "student") reloadStudents();
if (shouldReload && selectedUser!.type === "teacher") reloadTeachers();
if (shouldReload && selectedUser!.type === "corporate") reloadCorporates();
if (shouldReload && selectedUser!.type === "agent") reloadAgents();
}}
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("/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("/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("/users");
}
: undefined
}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
{router.asPath === "/#students" && <StudentsList />}
{router.asPath === "/#teachers" && <TeachersList />}
{router.asPath === "/#corporate" && <CorporateList />}
{router.asPath === "/#agents" && <AgentsList />}
{router.asPath === "/#inactiveStudents" && <InactiveStudentsList />}
{router.asPath === "/#inactiveCorporate" && <InactiveCorporateList />}
{router.asPath === "/#inactiveCountryManagers" && <InactiveCountryManagerList />}
{router.asPath === "/#paymentdone" && <CorporatePaidStatusList paid={true} />}
{router.asPath === "/#paymentpending" && <CorporatePaidStatusList paid={false} />}
{router.asPath === "/#corporatestudentslevels" && <CorporateStudentsLevelsHelper />}
{router.asPath === "/" && <DefaultDashboard />}
</>
);
}

View File

@@ -1,249 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {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, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import IconCard from "./IconCard";
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
interface Props {
user: User;
}
export default function AgentDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users, reload} = useUsers();
const {pending, done} = usePaymentStatusUsers();
useEffect(() => {
setShowModal(!!selectedUser && page === "");
}, [selectedUser, page]);
const corporateFilter = (user: User) => user.type === "corporate";
const referredCorporateFilter = (x: User) =>
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
const inactiveReferredCorporateFilter = (x: User) =>
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = ({displayUser, allowClick = true}: {displayUser: User; allowClick?: boolean}) => (
<div
onClick={() => allowClick && 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">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>
{displayUser.type === "corporate"
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
: displayUser.name}
</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const ReferredCorporateList = () => {
return (
<UserList
user={user}
filters={[referredCorporateFilter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Referred Corporate ({total})</h2>
</div>
)}
/>
);
};
const InactiveReferredCorporateList = () => {
return (
<UserList
user={user}
filters={[inactiveReferredCorporateFilter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({total})</h2>
</div>
)}
/>
);
};
const CorporateList = () => {
const filter = (x: User) => x.type === "corporate";
return (
<UserList
user={user}
filters={[filter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
</div>
)}
/>
);
};
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
return (
<UserList
user={user}
filters={[filter]}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
{paid ? "Payment Done" : "Pending Payment"} ({total})
</h2>
</div>
)}
/>
);
};
const DefaultDashboard = () => (
<>
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:gap-4 text-center">
<IconCard
onClick={() => setPage("referredCorporate")}
Icon={BsBank}
label="Referred Corporate"
value={users.filter(referredCorporateFilter).length}
color="purple"
/>
<IconCard
onClick={() => setPage("inactiveReferredCorporate")}
Icon={BsBank}
label="Inactive Referred Corporate"
value={users.filter(inactiveReferredCorporateFilter).length}
color="rose"
/>
<IconCard
onClick={() => setPage("corporate")}
Icon={BsBank}
label="Corporate"
value={users.filter(corporateFilter).length}
color="purple"
/>
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
<IconCard
onClick={() => setPage("paymentpending")}
Icon={BsCurrencyDollar}
label="Pending Payment"
value={pending.length}
color="rose"
/>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest Referred Corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(referredCorporateFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} displayUser={x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(corporateFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} displayUser={x} allowClick={false} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Referenced corporate expiring in 1 month</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(
(x) =>
referredCorporateFilter(x) &&
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} displayUser={x} />
))}
</div>
</div>
</section>
</>
);
return (
<>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<>
{selectedUser && (
<div className="w-full flex flex-col gap-8">
<UserCard
loggedInUser={user}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
}
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
{page === "referredCorporate" && <ReferredCorporateList />}
{page === "corporate" && <CorporateList />}
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
{page === "" && <DefaultDashboard />}
</>
);
}

View File

@@ -1,575 +0,0 @@
import Input from "@/components/Low/Input";
import Modal from "@/components/Modal";
import { Module } from "@/interfaces";
import clsx from "clsx";
import { useEffect, useMemo, useState } from "react";
import { BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
import { generate } from "random-words";
import { capitalize } from "lodash";
import useUsers from "@/hooks/useUsers";
import { Group, User } from "@/interfaces/user";
import ProgressBar from "@/components/Low/ProgressBar";
import { calculateAverageLevel } from "@/utils/score";
import Button from "@/components/Low/Button";
import ReactDatePicker from "react-datepicker";
import moment from "moment";
import axios from "axios";
import { getExam } from "@/utils/exams";
import { toast } from "react-toastify";
import { Assignment } from "@/interfaces/results";
import Checkbox from "@/components/Low/Checkbox";
import { InstructorGender, Variant } from "@/interfaces/exam";
import Select from "@/components/Low/Select";
import useExams from "@/hooks/useExams";
import { useListSearch } from "@/hooks/useListSearch";
interface Props {
isCreating: boolean;
users: User[];
user: User;
groups: Group[];
assignment?: Assignment;
cancelCreation: () => void;
}
const SIZE = 12;
export default function AssignmentCreator({ isCreating, assignment, user, groups, users, cancelCreation }: Props) {
const [studentsPage, setStudentsPage] = useState(0);
const [teachersPage, setTeachersPage] = useState(0);
const [selectedModules, setSelectedModules] = useState<Module[]>(assignment?.exams.map((e) => e.module) || []);
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
const [teachers, setTeachers] = useState<string[]>(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]);
const [name, setName] = useState(
assignment?.name ||
generate({
minLength: 6,
maxLength: 8,
min: 2,
max: 3,
join: " ",
formatter: capitalize,
}),
);
const [isLoading, setIsLoading] = useState(false);
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "hour").toDate());
const [endDate, setEndDate] = useState<Date | null>(
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
);
const [variant, setVariant] = useState<Variant>("full");
const [instructorGender, setInstructorGender] = useState<InstructorGender>(assignment?.instructorGender || "varied");
// creates a new exam for each assignee or just one exam for all assignees
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
const [released, setReleased] = useState<boolean>(assignment?.released || false);
const [autoStart, setAutostart] = useState<boolean>(assignment?.autoStart || false);
const [useRandomExams, setUseRandomExams] = useState(true);
const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]);
const { exams } = useExams();
const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]);
const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]);
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch, text: studentText } = useListSearch([["name"], ["email"]], userStudents);
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch, text: teacherText } = useListSearch([["name"], ["email"]], userTeachers);
useEffect(() => setStudentsPage(0), [studentText]);
const studentRows = useMemo(
() =>
filteredStudentsRows.slice(
studentsPage * SIZE,
(studentsPage + 1) * SIZE > filteredStudentsRows.length ? filteredStudentsRows.length : (studentsPage + 1) * SIZE,
),
[filteredStudentsRows, studentsPage],
);
useEffect(() => setTeachersPage(0), [teacherText]);
const teacherRows = useMemo(
() =>
filteredTeachersRows.slice(
teachersPage * SIZE,
(teachersPage + 1) * SIZE > filteredTeachersRows.length ? filteredTeachersRows.length : (teachersPage + 1) * SIZE,
),
[filteredTeachersRows, teachersPage],
);
useEffect(() => {
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
}, [selectedModules]);
const toggleModule = (module: Module) => {
const modules = selectedModules.filter((x) => x !== module);
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
};
const toggleAssignee = (user: User) => {
setAssignees((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
};
const toggleTeacher = (user: User) => {
setTeachers((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
};
const createAssignment = () => {
setIsLoading(true);
(assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, {
assignees,
name,
startDate,
examIDs: !useRandomExams ? examIDs : undefined,
endDate,
selectedModules,
generateMultiple,
teachers,
variant,
instructorGender,
released,
autoStart,
})
.then(() => {
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
cancelCreation();
})
.catch((e) => {
console.log(e);
toast.error("Something went wrong, please try again later!");
})
.finally(() => setIsLoading(false));
};
const deleteAssignment = () => {
if (assignment) {
setIsLoading(true);
if (!confirm(`Are you sure you want to delete the "${assignment.name}" assignment?`)) return;
axios
.delete(`api/assignments/${assignment.id}`)
.then(() => {
toast.success(`The assignment "${name}" has been deleted successfully!`);
cancelCreation();
})
.catch((e) => {
console.log(e);
toast.error("Something went wrong, please try again later!");
})
.finally(() => setIsLoading(false));
}
};
const startAssignment = () => {
if (assignment) {
setIsLoading(true);
axios
.post(`/api/assignments/${assignment.id}/start`)
.then(() => {
toast.success(`The assignment "${name}" has been started successfully!`);
cancelCreation();
})
.catch((e) => {
console.log(e);
toast.error("Something went wrong, please try again later!");
})
.finally(() => setIsLoading(false));
}
};
return (
<Modal isOpen={isCreating} onClose={cancelCreation} title="New Assignment">
<div className="w-full flex flex-col gap-4">
<section className="w-full grid -md:grid-cols-1 md:grid-cols-3 place-items-center -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
<div
onClick={!selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
className={clsx(
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
selectedModules.includes("reading") ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
<BsBook className="text-white w-7 h-7" />
</div>
<span className="ml-8 font-semibold">Reading</span>
{!selectedModules.includes("reading") && !selectedModules.includes("level") && (
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
{selectedModules.includes("reading") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
</div>
<div
onClick={!selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
className={clsx(
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
selectedModules.includes("listening") ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
<BsHeadphones className="text-white w-7 h-7" />
</div>
<span className="ml-8 font-semibold">Listening</span>
{!selectedModules.includes("listening") && !selectedModules.includes("level") && (
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
{selectedModules.includes("listening") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
</div>
<div
onClick={
(!selectedModules.includes("level") && selectedModules.length === 0) || selectedModules.includes("level")
? () => toggleModule("level")
: undefined
}
className={clsx(
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
selectedModules.includes("level") ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
<BsClipboard className="text-white w-7 h-7" />
</div>
<span className="ml-8 font-semibold">Level</span>
{!selectedModules.includes("level") && selectedModules.length === 0 && (
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
)}
{!selectedModules.includes("level") && selectedModules.length > 0 && <BsXCircle className="text-mti-red-light w-8 h-8" />}
{selectedModules.includes("level") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
</div>
<div
onClick={!selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
className={clsx(
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
selectedModules.includes("writing") ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
<BsPen className="text-white w-7 h-7" />
</div>
<span className="ml-8 font-semibold">Writing</span>
{!selectedModules.includes("writing") && !selectedModules.includes("level") && (
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
{selectedModules.includes("writing") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
</div>
<div
onClick={!selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
className={clsx(
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
selectedModules.includes("speaking") ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
<BsMegaphone className="text-white w-7 h-7" />
</div>
<span className="ml-8 font-semibold">Speaking</span>
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && (
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
{selectedModules.includes("speaking") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
</div>
</section>
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">Limit Start Date *</label>
<ReactDatePicker
className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip z-10",
"transition duration-300 ease-in-out",
)}
popperClassName="!z-20"
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
dateFormat="dd/MM/yyyy HH:mm"
selected={startDate}
showTimeSelect
onChange={(date) => setStartDate(date)}
/>
</div>
<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">End Date *</label>
<ReactDatePicker
className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip z-10",
"transition duration-300 ease-in-out",
)}
popperClassName="!z-20"
filterTime={(date) => moment(date).isAfter(startDate)}
dateFormat="dd/MM/yyyy HH:mm"
selected={endDate}
showTimeSelect
onChange={(date) => setEndDate(date)}
/>
</div>
</div>
{selectedModules.includes("speaking") && (
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor&apos;s Gender</label>
<Select
value={{
value: instructorGender,
label: capitalize(instructorGender),
}}
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
disabled={!selectedModules.includes("speaking") || !!assignment}
options={[
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
{ value: "varied", label: "Varied" },
]}
/>
</div>
)}
{selectedModules.length > 0 && (
<div className="flex flex-col gap-3 w-full">
<Checkbox isChecked={useRandomExams} onChange={setUseRandomExams}>
Random Exams
</Checkbox>
{!useRandomExams && (
<div className="grid md:grid-cols-2 w-full gap-4">
{selectedModules.map((module) => (
<div key={module} className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">{capitalize(module)} Exam</label>
<Select
value={{
value: examIDs.find((e) => e.module === module)?.id || null,
label: examIDs.find((e) => e.module === module)?.id || "",
}}
onChange={(value) =>
value
? setExamIDs((prev) => [...prev.filter((x) => x.module !== module), { id: value.value!, module }])
: setExamIDs((prev) => prev.filter((x) => x.module !== module))
}
options={exams
.filter((x) => !x.isDiagnostic && x.module === module)
.map((x) => ({ value: x.id, label: x.id }))}
/>
</div>
))}
</div>
)}
</div>
)}
<section className="w-full flex flex-col gap-4">
<span className="font-semibold">Assignees ({assignees.length} selected)</span>
<div className="grid grid-cols-5 gap-4">
{groups.map((g) => (
<button
key={g.id}
onClick={() => {
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
if (groupStudentIds.every((u) => assignees.includes(u))) {
setAssignees((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
} else {
setAssignees((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
}
}}
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
"!bg-mti-purple-light !text-white",
)}>
{g.name}
</button>
))}
</div>
{renderStudentSearch()}
<div className="flex flex-wrap -md:justify-center gap-4">
{studentRows.map((user) => (
<div
onClick={() => toggleAssignee(user)}
className={clsx(
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
"transition ease-in-out duration-300",
assignees.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
)}
key={user.id}>
<span className="flex flex-col gap-0 justify-center">
<span className="font-semibold">{user.name}</span>
<span className="text-sm opacity-80">{user.email}</span>
</span>
<ProgressBar
color="purple"
textClassName="!text-mti-black/80"
label={`Level ${calculateAverageLevel(user.levels)}`}
percentage={(calculateAverageLevel(user.levels) / 9) * 100}
className="h-6"
/>
<span className="text-mti-black/80 text-sm whitespace-pre-wrap mt-2">
Groups:{" "}
{groups
.filter((g) => g.participants.includes(user.id))
.map((g) => g.name)
.join(", ")}
</span>
</div>
))}
</div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={studentsPage === 0} onClick={() => setStudentsPage((prev) => prev - 1)}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{studentsPage * SIZE + 1} -{" "}
{(studentsPage + 1) * SIZE > filteredStudentsRows.length ? filteredStudentsRows.length : (studentsPage + 1) * SIZE} /{" "}
{filteredStudentsRows.length}
</span>
<Button
className="w-[200px]"
disabled={(studentsPage + 1) * SIZE >= filteredStudentsRows.length}
onClick={() => setStudentsPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
</section>
{user.type !== "teacher" && (
<section className="w-full flex flex-col gap-3">
<span className="font-semibold">Teachers ({teachers.length} selected)</span>
<div className="grid grid-cols-5 gap-4">
{groups.map((g) => (
<button
key={g.id}
onClick={() => {
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
if (groupStudentIds.every((u) => teachers.includes(u))) {
setTeachers((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
} else {
setTeachers((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
}
}}
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
"!bg-mti-purple-light !text-white",
)}>
{g.name}
</button>
))}
</div>
{renderTeacherSearch()}
<div className="flex flex-wrap -md:justify-center gap-4">
{teacherRows.map((user) => (
<div
onClick={() => toggleTeacher(user)}
className={clsx(
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
"transition ease-in-out duration-300",
teachers.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
)}
key={user.id}>
<span className="flex flex-col gap-0 justify-center">
<span className="font-semibold">{user.name}</span>
<span className="text-sm opacity-80">{user.email}</span>
</span>
<span className="text-mti-black/80 text-sm whitespace-pre-wrap mt-2">
Groups:{" "}
{groups
.filter((g) => g.participants.includes(user.id))
.map((g) => g.name)
.join(", ")}
</span>
</div>
))}
</div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={teachersPage === 0} onClick={() => setTeachersPage((prev) => prev - 1)}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{teachersPage * SIZE + 1} -{" "}
{(teachersPage + 1) * SIZE > filteredTeachersRows.length
? filteredTeachersRows.length
: (teachersPage + 1) * SIZE}{" "}
/ {filteredTeachersRows.length}
</span>
<Button
className="w-[200px]"
disabled={(teachersPage + 1) * SIZE >= filteredTeachersRows.length}
onClick={() => setTeachersPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
</section>
)}
<div className="flex gap-4 w-full items-end">
<Checkbox isChecked={variant === "full"} onChange={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
Full length exams
</Checkbox>
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple((d) => !d)}>
Generate different exams
</Checkbox>
<Checkbox isChecked={released} onChange={() => setReleased((d) => !d)}>
Auto release results
</Checkbox>
<Checkbox isChecked={autoStart} onChange={() => setAutostart((d) => !d)}>
Auto start exam
</Checkbox>
</div>
<div className="flex gap-4 w-full justify-end">
<Button className="w-full max-w-[200px]" variant="outline" onClick={cancelCreation} disabled={isLoading} isLoading={isLoading}>
Cancel
</Button>
{assignment && (
<>
<Button
className="w-full max-w-[200px]"
color="green"
variant="outline"
onClick={startAssignment}
disabled={isLoading || moment().isAfter(startDate)}
isLoading={isLoading}>
Start
</Button>
<Button
className="w-full max-w-[200px]"
color="red"
variant="outline"
onClick={deleteAssignment}
disabled={isLoading}
isLoading={isLoading}>
Delete
</Button>
</>
)}
<Button
disabled={
selectedModules.length === 0 ||
!name ||
!startDate ||
!endDate ||
assignees.length === 0 ||
(!useRandomExams && examIDs.length < selectedModules.length)
}
className="w-full max-w-[200px]"
onClick={createAssignment}
isLoading={isLoading}>
{assignment ? "Update" : "Create"}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -1,45 +0,0 @@
import useUsers, {userHashStudent, userHashTeacher} from "@/hooks/useUsers";
import {CorporateUser, User} from "@/interfaces/user";
import {useRouter} from "next/router";
import {useMemo} from "react";
import {BsArrowLeft} from "react-icons/bs";
import MasterStatistical from "../MasterCorporate/MasterStatistical";
interface Props {
user: CorporateUser;
}
const MasterStatisticalPage = ({user}: Props) => {
const {users: students} = useUsers(userHashStudent);
const {users: teachers} = useUsers(userHashTeacher);
// this workaround will allow us toreuse the master statistical due to master corporate restraints
// while still being able to use the corporate user
const groupedByNameCorporateIds = useMemo(
() => ({
[user.corporateInformation?.companyInformation?.name || user.name]: [user.id],
}),
[user],
);
const teachersAndStudents = useMemo(() => [...students, ...teachers], [students, teachers]);
const router = useRouter();
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Master Statistical</h2>
</div>
<MasterStatistical users={teachersAndStudents} corporateUsers={groupedByNameCorporateIds} displaySelection={false} />
</>
);
};
export default MasterStatisticalPage;

View File

@@ -1,154 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers, {userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers";
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter} from "@/utils";
import moment from "moment";
import {useEffect, useMemo, useState} from "react";
import {
BsArrowLeft,
BsClipboard2Data,
BsClipboard2DataFill,
BsClock,
BsGlobeCentralSouthAsia,
BsPaperclip,
BsPerson,
BsPersonAdd,
BsPersonFill,
BsPersonFillGear,
BsPersonGear,
BsPencilSquare,
BsPersonBadge,
BsPersonCheck,
BsPeople,
BsArrowRepeat,
BsPlus,
BsEnvelopePaper,
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import {averageLevelCalculator, 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";
import {getUserCorporate} from "@/utils/groups";
import useAssignments from "@/hooks/useAssignments";
import {Assignment} from "@/interfaces/results";
import AssignmentView from "../AssignmentView";
import AssignmentCreator from "../AssignmentCreator";
import clsx from "clsx";
import AssignmentCard from "../AssignmentCard";
import {createColumnHelper} from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox";
import List from "@/components/List";
import {getUserCompanyName} from "@/resources/user";
import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
import useUserBalance from "@/hooks/useUserBalance";
import AssignmentsPage from "../views/AssignmentsPage";
type StudentPerformanceItem = User & {corporateName: string; group: string};
const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const columnHelper = createColumnHelper<StudentPerformanceItem>();
const columns = [
columnHelper.accessor("name", {
header: "Student Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("demographicInformation.passport_id", {
header: "ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("group", {
header: "Group",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("corporateName", {
header: "Corporate",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("levels.reading", {
header: "Reading",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.level", {
header: "Level",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
users,
stats.filter((x) => x.user === info.row.original.id),
).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
}),
];
return (
<div className="flex flex-col gap-4 w-full h-full">
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
Show Utilization
</Checkbox>
<List<StudentPerformanceItem>
data={items.sort(
(a, b) =>
averageLevelCalculator(
users,
stats.filter((x) => x.user === b.id),
) -
averageLevelCalculator(
users,
stats.filter((x) => x.user === a.id),
),
)}
columns={columns}
/>
</div>
);
};
export default StudentPerformanceList;

View File

@@ -1,49 +0,0 @@
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useGroups from "@/hooks/useGroups";
import useUsers, {userHashStudent} from "@/hooks/useUsers";
import {Stat, User} from "@/interfaces/user";
import {getUserCompanyName} from "@/resources/user";
import clsx from "clsx";
import {useRouter} from "next/router";
import {BsArrowLeft, BsArrowRepeat} from "react-icons/bs";
import StudentPerformanceList from "./StudentPerformanceList";
interface Props {
user: User;
}
const StudentPerformancePage = ({user}: Props) => {
const {groups} = useGroups({admin: user.id});
const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(userHashStudent);
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const router = useRouter();
const performanceStudents = students.map((u) => ({
...u,
group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A",
corporateName: getUserCompanyName(user, [], groups),
}));
return (
<>
<div className="w-full flex justify-between items-center">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<div
onClick={reloadStudents}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isStudentsLoading && "animate-spin")} />
</div>
</div>
<StudentPerformanceList items={performanceStudents} stats={stats} users={students} />
</>
);
};
export default StudentPerformancePage;

View File

@@ -1,371 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter, mapBy} from "@/utils";
import moment from "moment";
import {useEffect, useMemo, useState} from "react";
import {
BsArrowLeft,
BsClipboard2Data,
BsClock,
BsPaperclip,
BsPersonFill,
BsPersonFillGear,
BsPencilSquare,
BsPersonCheck,
BsPeople,
BsEnvelopePaper,
BsDatabase,
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
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 useAssignments from "@/hooks/useAssignments";
import useUserBalance from "@/hooks/useUserBalance";
import AssignmentsPage from "../views/AssignmentsPage";
import StudentPerformancePage from "./StudentPerformancePage";
import MasterStatisticalPage from "./MasterStatisticalPage";
import {getEntitiesUsers} from "@/utils/users.be";
interface Props {
user: CorporateUser;
linkedCorporate?: CorporateUser | MasterCorporateUser;
}
const studentHash = {
type: "student",
orderBy: "registrationDate",
size: 25,
};
const teacherHash = {
type: "teacher",
orderBy: "registrationDate",
size: 25,
};
export default function CorporateDashboard({user, linkedCorporate}: Props) {
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {groups} = useGroups({admin: user.id});
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const {balance} = useUserBalance();
const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash);
const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash);
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
useEffect(() => {
setShowModal(!!selectedUser && router.asPath === "/#");
}, [selectedUser, router.asPath]);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
onClick={() => 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">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const GroupsList = () => {
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
</div>
<GroupList user={user} />
</>
);
};
const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: students.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);
};
if (router.asPath === "/#students")
return (
<UserList
user={user}
type="student"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
if (router.asPath === "/#teachers")
return (
<UserList
user={user}
type="teacher"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
</div>
)}
/>
);
if (router.asPath === "/#groups") return <GroupsList />;
if (router.asPath === "/#studentsPerformance") return <StudentPerformancePage user={user} />;
if (router.asPath === "/#assignments")
return (
<AssignmentsPage
assignments={assignments}
user={user}
groups={assignmentsGroups}
reloadAssignments={reloadAssignments}
isLoading={isAssignmentsLoading}
onBack={() => router.push("/")}
/>
);
if (router.asPath === "/#statistical") return <MasterStatisticalPage user={user} />;
return (
<>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<>
{selectedUser && (
<div className="w-full flex flex-col gap-8">
<UserCard
loggedInUser={user}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload && selectedUser!.type === "student") reloadStudents();
if (shouldReload && selectedUser!.type === "teacher") reloadTeachers();
}}
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("/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("/users");
}
: undefined
}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
<>
{!!linkedCorporate && (
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
Linked to: <b>{linkedCorporate?.corporateInformation?.companyInformation.name || linkedCorporate.name}</b>
</div>
)}
<section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center">
<IconCard
onClick={() => router.push("/#students")}
isLoading={isStudentsLoading}
Icon={BsPersonFill}
label="Students"
value={totalStudents}
color="purple"
/>
<IconCard
onClick={() => router.push("/#teachers")}
isLoading={isTeachersLoading}
Icon={BsPencilSquare}
label="Teachers"
value={totalTeachers}
color="purple"
/>
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
isLoading={isStudentsLoading}
label="Average Level"
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
color="purple"
/>
<IconCard onClick={() => router.push("/#groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${balance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
Icon={BsPersonFillGear}
isLoading={isStudentsLoading}
label="Student Performance"
value={totalStudents}
color="purple"
onClick={() => router.push("/#studentsPerformance")}
/>
<IconCard Icon={BsDatabase} label="Master Statistical" color="purple" onClick={() => router.push("/#statistical")} />
<button
disabled={isAssignmentsLoading}
onClick={() => router.push("/#assignments")}
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
</span>
</span>
</button>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest teachers</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{teachers
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
</section>
</>
</>
);
}

View File

@@ -1,96 +0,0 @@
import React, {useMemo} from "react";
import useUsers, { userHashStudent, userHashTeacher, userHashCorporate } from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import {User} from "@/interfaces/user";
import Select from "@/components/Low/Select";
import ProgressBar from "@/components/Low/ProgressBar";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {capitalize} from "lodash";
import {getLevelLabel} from "@/utils/score";
const Card = ({user}: {user: User}) => {
return (
<div className="border-mti-gray-platinum flex flex-col h-fit w-full cursor-pointer gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
<div className="flex flex-col gap-3">
<h3 className="text-xl font-semibold">{user.name}</h3>
</div>
<div className="flex w-full gap-3 flex-wrap">
{MODULE_ARRAY.map((module) => {
const desiredLevel = user.desiredLevels[module] || 9;
const level = user.levels[module] || 0;
return (
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]" key={module}>
<div className="flex items-center gap-2 md:gap-3">
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
{module === "reading" && <BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />}
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
</div>
<div className="flex w-full flex-col">
<span className="text-sm font-bold md:font-extrabold w-full">{capitalize(module)}</span>
<div className="text-mti-gray-dim text-sm font-normal">
{module === "level" && <span>English Level: {getLevelLabel(level).join(" / ")}</span>}
{module !== "level" && (
<div className="flex flex-col">
<span>Level {level} / Level 9</span>
<span>Desired Level: {desiredLevel}</span>
</div>
)}
</div>
</div>
</div>
<div className="md:pl-14">
<ProgressBar
color={module}
label=""
mark={Math.round((desiredLevel * 100) / 9)}
markLabel={`Desired Level: ${desiredLevel}`}
percentage={Math.round((level * 100) / 9)}
className="h-2 w-full"
/>
</div>
</div>
);
})}
</div>
</div>
);
};
const CorporateStudentsLevels = () => {
const [corporateId, setCorporateId] = React.useState<string>("");
const {users: students} = useUsers(userHashStudent);
const {users: corporates} = useUsers(userHashCorporate);
const corporate = useMemo(() => corporates.find((u) => u.id === corporateId) || corporates[0], [corporates, corporateId]);
return (
<>
<Select
options={corporates.map((x: User) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
}))}
value={corporate ? {value: corporate.id, label: corporate.name} : null}
onChange={(value) => setCorporateId(value?.value!)}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}
/>
{students.map((u) => (
<Card user={u} key={u.id} />
))}
</>
);
};
export default CorporateStudentsLevels;

View File

@@ -1,413 +0,0 @@
import React, {useEffect, useMemo, useState} from "react";
import {CorporateUser, StudentUser, User} from "@/interfaces/user";
import {BsFileExcel, BsBank, BsPersonFill} from "react-icons/bs";
import IconCard from "../IconCard";
import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates";
import ReactDatePicker from "react-datepicker";
import moment from "moment";
import {AssignmentWithCorporateId} from "@/interfaces/results";
import {flexRender, createColumnHelper, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox";
import {useListSearch} from "@/hooks/useListSearch";
import axios from "axios";
import {toast} from "react-toastify";
import Button from "@/components/Low/Button";
import {getUserName} from "@/utils/users";
interface GroupedCorporateUsers {
// list of user Ids
[key: string]: string[];
}
interface Props {
corporateUsers: GroupedCorporateUsers;
users: User[];
displaySelection?: boolean;
}
interface TableData {
user: User | undefined;
email: string;
correct: number;
corporate: string;
submitted: boolean;
date: moment.Moment;
assignment: string;
corporateId: string;
}
interface UserCount {
userCount: number;
maxUserCount: number;
}
const searchFilters = [["email"], ["user"], ["userId"], ["exams"], ["assignment"]];
const SIZE = 16;
const MasterStatistical = (props: Props) => {
const {users, corporateUsers, displaySelection = true} = props;
const [page, setPage] = useState(0);
// const corporateRelevantUsers = React.useMemo(
// () => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[],
// [corporateUsers]
// );
const corporates = React.useMemo(() => Object.values(corporateUsers).flat(), [corporateUsers]);
const [selectedCorporates, setSelectedCorporates] = React.useState<string[]>(corporates);
const [startDate, setStartDate] = React.useState<Date | null>(moment("01/01/2023").toDate());
const [endDate, setEndDate] = React.useState<Date | null>(moment().endOf("year").toDate());
const {assignments, isLoading} = useAssignmentsCorporates({
corporates: selectedCorporates,
startDate,
endDate,
});
const [downloading, setDownloading] = React.useState<boolean>(false);
const tableResults = React.useMemo(
() =>
assignments.reduce((accmA: TableData[], a: AssignmentWithCorporateId) => {
const userResults = a.assignees.map((assignee) => {
const userData = users.find((u) => u.id === assignee);
const userStats = a.results.find((r) => r.user === assignee)?.stats || [];
const corporate = getUserName(users.find((u) => u.id === a.assigner));
const commonData = {
user: userData,
email: userData?.email || "N/A",
userId: assignee,
corporateId: a.corporateId,
exams: a.exams.map((x) => x.id).join(", "),
corporate,
assignment: a.name,
};
if (userStats.length === 0) {
return {
...commonData,
correct: 0,
submitted: false,
date: null,
};
}
return {
...commonData,
correct: userStats.reduce((n, e) => n + e.score.correct, 0),
submitted: true,
date: moment.max(userStats.map((e) => moment(e.date))),
};
}) as TableData[];
return [...accmA, ...userResults];
}, []),
[assignments, users],
);
const getCorporateScores = (corporateId: string): UserCount => {
const corporateAssignmentsUsers = assignments.filter((a) => a.corporateId === corporateId).reduce((acc, a) => acc + a.assignees.length, 0);
const corporateResults = tableResults.filter((r) => r.corporateId === corporateId).length;
return {
maxUserCount: corporateAssignmentsUsers,
userCount: corporateResults,
};
};
const getCorporatesScoresHash = (data: string[]) =>
data.reduce(
(accm, id) => ({
...accm,
[id]: getCorporateScores(id),
}),
{},
) as Record<string, UserCount>;
const getConsolidateScore = (data: Record<string, UserCount>) =>
Object.values(data).reduce(
(acc: UserCount, {userCount, maxUserCount}: UserCount) => ({
userCount: acc.userCount + userCount,
maxUserCount: acc.maxUserCount + maxUserCount,
}),
{userCount: 0, maxUserCount: 0},
);
const corporateScores = getCorporatesScoresHash(corporates);
const consolidateScore = getConsolidateScore(corporateScores);
const getConsolidateScoreStr = (data: UserCount) => `${data.userCount}/${data.maxUserCount}`;
const columnHelper = createColumnHelper<TableData>();
const defaultColumns = [
columnHelper.accessor("user", {
header: "User",
id: "user",
cell: (info) => {
return <span>{info.getValue()?.name || "N/A"}</span>;
},
}),
columnHelper.accessor("email", {
header: "Email",
id: "email",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("user", {
header: "Student ID",
id: "studentID",
cell: (info) => {
return <span>{(info.getValue() as StudentUser)?.studentID || "N/A"}</span>;
},
}),
...(displaySelection
? [
columnHelper.accessor("corporate", {
header: "Corporate",
id: "corporate",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
]
: []),
columnHelper.accessor("assignment", {
header: "Assignment",
id: "assignment",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("submitted", {
header: "Submitted",
id: "submitted",
cell: (info) => {
return (
<Checkbox isChecked={info.getValue()} disabled onChange={() => {}}>
<span></span>
</Checkbox>
);
},
}),
columnHelper.accessor("correct", {
header: "Score",
id: "correct",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("date", {
header: "Date",
id: "date",
cell: (info) => {
const date = info.getValue();
if (date) {
return <span>{!!date ? date.format("DD/MM/YYYY") : "N/A"}</span>;
}
return <span>{""}</span>;
},
}),
];
const {rows: filteredRows, renderSearch, text: searchText} = useListSearch(searchFilters, tableResults);
useEffect(() => setPage(0), [searchText]);
const rows = useMemo(
() => filteredRows.slice(page * SIZE, (page + 1) * SIZE > filteredRows.length ? filteredRows.length : (page + 1) * SIZE),
[filteredRows, page],
);
const table = useReactTable({
data: rows,
columns: defaultColumns,
getCoreRowModel: getCoreRowModel(),
});
const areAllSelected = selectedCorporates.length === corporates.length;
const getStudentsConsolidateScore = () => {
if (tableResults.length === 0) {
return {highest: null, lowest: null};
}
// Find the student with the highest and lowest score
return tableResults.reduce(
(acc, curr) => {
if (curr.correct > acc.highest.correct) {
acc.highest = curr;
}
if (curr.correct < acc.lowest.correct) {
acc.lowest = curr;
}
return acc;
},
{highest: tableResults[0], lowest: tableResults[0]},
);
};
const triggerDownload = async () => {
try {
setDownloading(true);
const res = await axios.post("/api/assignments/statistical/excel", {
ids: selectedCorporates,
...(startDate ? {startDate: startDate.toISOString()} : {}),
...(endDate ? {endDate: endDate.toISOString()} : {}),
searchText,
});
toast.success("Report ready!");
const link = document.createElement("a");
link.href = res.data;
// download should have worked but there are some CORS issues
// https://firebase.google.com/docs/storage/web/download-files#cors_configuration
// link.download="report.pdf";
link.target = "_blank";
link.rel = "noreferrer";
link.click();
setDownloading(false);
} catch (err) {
toast.error("Failed to display the report!");
console.error(err);
setDownloading(false);
}
};
const consolidateResults = getStudentsConsolidateScore();
return (
<>
{displaySelection && (
<div className="flex flex-wrap gap-2 items-center text-center">
<IconCard
Icon={BsBank}
label="Consolidate"
isLoading={isLoading}
value={getConsolidateScoreStr(consolidateScore)}
color="purple"
onClick={() => {
if (areAllSelected) {
setSelectedCorporates([]);
return;
}
setSelectedCorporates(corporates);
}}
isSelected={areAllSelected}
/>
{Object.keys(corporateUsers).map((corporateName) => {
const group = corporateUsers[corporateName];
const isSelected = group.every((id) => selectedCorporates.includes(id));
const valueHash = getCorporatesScoresHash(group);
const value = getConsolidateScoreStr(getConsolidateScore(valueHash));
return (
<IconCard
key={corporateName}
Icon={BsBank}
isLoading={isLoading}
label={corporateName}
value={value}
color="purple"
onClick={() => {
if (isSelected) {
setSelectedCorporates((prev) => prev.filter((x) => !group.includes(x)));
return;
}
setSelectedCorporates((prev) => [...new Set([...prev, ...group])]);
}}
isSelected={isSelected}
/>
);
})}
</div>
)}
<div className="flex gap-3 w-full">
<div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">Date</label>
<ReactDatePicker
dateFormat="dd/MM/yyyy"
className="px-4 py-6 w-52 text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
selected={startDate}
startDate={startDate}
endDate={endDate}
selectsRange
showMonthDropdown
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);
}}
/>
</div>
{renderSearch()}
<div className="flex flex-col gap-3 justify-end">
<Button className="w-[200px] h-[70px]" variant="outline" isLoading={downloading} onClick={triggerDownload}>
Download
</Button>
</div>
</div>
<div className="w-full h-full flex flex-col gap-4">
<div className="w-full flex gap-2 justify-between">
<Button className="w-full max-w-[200px]" disabled={page === 0} onClick={() => setPage((prev) => prev - 1)}>
Previous Page
</Button>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{page * SIZE + 1} - {(page + 1) * SIZE > filteredRows.length ? filteredRows.length : (page + 1) * SIZE} /{" "}
{filteredRows.length}
</span>
<Button className="w-[200px]" disabled={(page + 1) * SIZE >= filteredRows.length} onClick={() => setPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
<table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="p-4 text-left" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex flex-wrap gap-2 items-center text-center">
{consolidateResults.highest && (
<IconCard onClick={() => {}} Icon={BsPersonFill} label={`Highest result: ${consolidateResults.highest.user}`} color="purple" />
)}
{consolidateResults.lowest && (
<IconCard onClick={() => {}} Icon={BsPersonFill} label={`Lowest result: ${consolidateResults.lowest.user}`} color="purple" />
)}
</div>
</>
);
};
export default MasterStatistical;

View File

@@ -1,43 +0,0 @@
import useUsers from "@/hooks/useUsers";
import {CorporateUser, User} from "@/interfaces/user";
import {groupBy} from "lodash";
import {useRouter} from "next/router";
import {useMemo} from "react";
import {BsArrowLeft} from "react-icons/bs";
import MasterStatistical from "./MasterStatistical";
interface Props {
groupedByNameCorporates: Record<string, CorporateUser[]>;
}
const MasterStatisticalPage = ({ groupedByNameCorporates }: Props) => {
const {users} = useUsers();
const router = useRouter();
const groupedByNameCorporateIds = useMemo(
() =>
Object.keys(groupedByNameCorporates).reduce((accm, x) => {
const corporateUserIds = (groupedByNameCorporates[x] as CorporateUser[]).map((y) => y.id);
return {...accm, [x]: corporateUserIds};
}, {}),
[groupedByNameCorporates],
);
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Master Statistical</h2>
</div>
<MasterStatistical users={users} corporateUsers={groupedByNameCorporateIds} />
</>
);
};
export default MasterStatisticalPage;

View File

@@ -1,252 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import {CorporateUser, Group, Stat, User} from "@/interfaces/user";
import {useState} from "react";
import {BsFilter} from "react-icons/bs";
import {averageLevelCalculator, calculateBandScore} from "@/utils/score";
import {groupByExam} from "@/utils/stats";
import {createColumnHelper} from "@tanstack/react-table";
import List from "@/components/List";
import {getUserCompanyName} from "@/resources/user";
import Checkbox from "@/components/Low/Checkbox";
import {uniqBy} from "lodash";
import Select from "@/components/Low/Select";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
type StudentPerformanceItem = User & {
corporate?: CorporateUser;
group?: Group;
};
const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const [availableCorporates] = useState(
uniqBy(
items.map((x) => x.corporate),
"id",
),
);
const [availableGroups] = useState(
uniqBy(
items.map((x) => x.group),
"id",
),
);
const [selectedCorporate, setSelectedCorporate] = useState<CorporateUser | null | undefined>(null);
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(null);
const columnHelper = createColumnHelper<StudentPerformanceItem>();
const columns = [
columnHelper.accessor("name", {
header: "Student Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("demographicInformation.passport_id", {
header: "ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("group", {
header: "Group",
cell: (info) => info.getValue()?.name || "N/A",
}),
columnHelper.accessor("corporate", {
header: "Corporate",
cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"),
}),
columnHelper.accessor("levels.reading", {
header: "Reading",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.level", {
header: "Level",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "level" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "level" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
users,
stats.filter((x) => x.user === info.row.original.id),
).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
}),
];
const filterUsers = (data: StudentPerformanceItem[]) => {
const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id;
const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id;
const filters: ((item: StudentPerformanceItem) => boolean)[] = [];
if (selectedCorporate !== null) filters.push(filterByCorporate);
if (selectedGroup !== null) filters.push(filterByGroup);
return filters.reduce((d, f) => d.filter(f), data);
};
return (
<div className="flex flex-col gap-4 w-full h-full">
<div className="w-full flex gap-4 justify-between items-center">
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
Show Utilization
</Checkbox>
<Popover>
<PopoverTrigger>
<div className="flex items-center justify-center p-2 hover:bg-neutral-300/50 rounded-full transition ease-in-out duration-300">
<BsFilter size={20} />
</div>
</PopoverTrigger>
<PopoverContent className="w-96">
<div className="flex flex-col gap-4">
<span className="font-bold text-lg">Filters</span>
<Select
options={availableCorporates.map((x) => ({
value: x?.id || "N/A",
label: x?.corporateInformation?.companyInformation?.name || x?.name || "N/A",
}))}
isClearable
value={
selectedCorporate === null
? null
: {
value: selectedCorporate?.id || "N/A",
label:
selectedCorporate?.corporateInformation?.companyInformation?.name ||
selectedCorporate?.name ||
"N/A",
}
}
placeholder="Select a Corporate..."
onChange={(value) =>
!value
? setSelectedCorporate(null)
: setSelectedCorporate(
value.value === "N/A" ? undefined : availableCorporates.find((x) => x?.id === value.value),
)
}
/>
<Select
options={availableGroups.map((x) => ({
value: x?.id || "N/A",
label: x?.name || "N/A",
}))}
isClearable
value={
selectedGroup === null
? null
: {
value: selectedGroup?.id || "N/A",
label: selectedGroup?.name || "N/A",
}
}
placeholder="Select a Group..."
onChange={(value) =>
!value
? setSelectedGroup(null)
: setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value))
}
/>
</div>
</PopoverContent>
</Popover>
</div>
<List<StudentPerformanceItem>
data={filterUsers(
items.sort(
(a, b) =>
averageLevelCalculator(
users,
stats.filter((x) => x.user === b.id),
) -
averageLevelCalculator(
users,
stats.filter((x) => x.user === a.id),
),
),
)}
columns={columns}
/>
</div>
);
};
export default StudentPerformanceList;

View File

@@ -1,46 +0,0 @@
import useAssignments from "@/hooks/useAssignments";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useGroups from "@/hooks/useGroups";
import useUsers, {userHashCorporate, userHashStudent} from "@/hooks/useUsers";
import {Stat, User} from "@/interfaces/user";
import clsx from "clsx";
import {useRouter} from "next/router";
import {BsArrowLeft, BsArrowRepeat} from "react-icons/bs";
import StudentPerformanceList from "./StudentPerformanceList";
interface Props {
user: User;
}
const StudentPerformancePage = ({user}: Props) => {
const {users: students} = useUsers(userHashStudent);
const {users: corporates} = useUsers(userHashCorporate);
const {groups} = useGroups({admin: user.id, userType: user.type});
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {reload: reloadAssignments, isLoading: isAssignmentsLoading} = useAssignments({corporate: user.id});
const router = useRouter();
return (
<>
<div className="w-full flex justify-between items-center">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<div
onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
</div>
</div>
<StudentPerformanceList items={students} stats={stats} users={corporates} groups={groups} />
</>
);
};
export default StudentPerformancePage;

View File

@@ -1,448 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {CorporateUser, MasterCorporateUser, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter} from "@/utils";
import moment from "moment";
import {useEffect, useState, useMemo} from "react";
import {
BsArrowLeft,
BsClipboard2Data,
BsClock,
BsPaperclip,
BsPersonFill,
BsPencilSquare,
BsPersonCheck,
BsPeople,
BsBank,
BsEnvelopePaper,
BsArrowRepeat,
BsPersonFillGear,
BsDatabase,
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import {averageLevelCalculator, calculateAverageLevel} from "@/utils/score";
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 useAssignments from "@/hooks/useAssignments";
import {Assignment} from "@/interfaces/results";
import clsx from "clsx";
import {getCorporateUser} from "@/resources/user";
import {groupBy, uniqBy} from "lodash";
import MasterStatistical from "./MasterStatistical";
import {activeAssignmentFilter} from "@/utils/assignments";
import useUserBalance from "@/hooks/useUserBalance";
import AssignmentsPage from "../views/AssignmentsPage";
import StudentPerformanceList from "./StudentPerformanceList";
import StudentPerformancePage from "./StudentPerformancePage";
import MasterStatisticalPage from "./MasterStatisticalPage";
interface Props {
user: MasterCorporateUser;
}
const studentHash = {
type: "student",
size: 25,
orderBy: "registrationDate",
};
const teacherHash = {
type: "teacher",
size: 25,
orderBy: "registrationDate",
};
const corporateHash = {
type: "corporate",
size: 25,
orderBy: "registrationDate",
};
export default function MasterCorporateDashboard({user}: Props) {
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]);
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash);
const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash);
const {users: corporates, total: totalCorporate, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(corporateHash);
const {groups} = useGroups({admin: user.id, userType: user.type});
const {balance} = useUserBalance();
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
useEffect(() => {
setShowModal(!!selectedUser && router.asPath === "/");
}, [selectedUser, router.asPath]);
useEffect(() => {
setCorporateAssignments(
assignments.filter(activeAssignmentFilter).map((a) => {
const assigner = [...teachers, ...corporates].find((x) => x.id === a.assigner);
return {
...a,
corporate: assigner ? getCorporateUser(assigner, [...teachers, ...corporates], groups) : undefined,
};
}),
);
}, [assignments, groups, teachers, corporates]);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
onClick={() => 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">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const {users} = useUsers();
const groupedByNameCorporates = useMemo(
() =>
groupBy(
users.filter((x) => x.type === "corporate"),
(x: CorporateUser) => x.corporateInformation?.companyInformation?.name || "N/A",
) as Record<string, CorporateUser[]>,
[users],
);
const groupedByNameCorporatesKeys = Object.keys(groupedByNameCorporates);
const GroupsList = () => {
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Groups ({groups.length})</h2>
</div>
<GroupList user={user} />
</>
);
};
if (router.asPath === "/#studentsPerformance") return <StudentPerformancePage user={user} />;
if (router.asPath === "/#statistical") return <MasterStatisticalPage groupedByNameCorporates={groupedByNameCorporates} />;
if (router.asPath === "/#groups") return <GroupsList />;
if (router.asPath === "/#students")
return (
<UserList
user={user}
type="student"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
if (router.asPath === "/#assignments")
return (
<AssignmentsPage
assignments={assignments}
corporateAssignments={corporateAssignments}
groups={assignmentsGroups}
user={user}
reloadAssignments={reloadAssignments}
isLoading={isAssignmentsLoading}
onBack={() => router.push("/")}
/>
);
if (router.asPath === "/#corporate")
return (
<UserList
user={user}
type="corporate"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
</div>
)}
/>
);
if (router.asPath === "/#students")
return (
<UserList
user={user}
type="student"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
if (router.asPath === "/#teachers")
return (
<UserList
user={user}
type="teacher"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
</div>
)}
/>
);
return (
<>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<>
{selectedUser && (
<div className="w-full flex flex-col gap-8">
<UserCard
maxUserAmount={
user.type === "mastercorporate"
? (user.corporateInformation?.companyInformation?.userAmount || 0) - balance
: undefined
}
loggedInUser={user}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload && selectedUser!.type === "student") reloadStudents();
if (shouldReload && selectedUser!.type === "teacher") reloadTeachers();
if (shouldReload && selectedUser!.type === "corporate") reloadCorporates();
}}
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("/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("/users");
}
: undefined
}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
<>
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
<IconCard
onClick={() => router.push("/#students")}
Icon={BsPersonFill}
isLoading={isStudentsLoading}
label="Students"
value={totalStudents}
color="purple"
/>
<IconCard
onClick={() => router.push("/#teachers")}
Icon={BsPencilSquare}
isLoading={isTeachersLoading}
label="Teachers"
value={totalTeachers}
color="purple"
/>
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
value={averageLevelCalculator(
students,
stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)),
).toFixed(1)}
color="purple"
/>
<IconCard onClick={() => router.push("/#groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${balance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
Icon={BsBank}
label="Corporate Accounts"
value={totalCorporate}
isLoading={isCorporatesLoading}
color="purple"
onClick={() => router.push("/#corporate")}
/>
<IconCard
Icon={BsBank}
label="Corporate"
value={groupedByNameCorporatesKeys.length}
isLoading={isCorporatesLoading}
color="purple"
/>
<IconCard
Icon={BsPersonFillGear}
isLoading={isStudentsLoading}
label="Student Performance"
value={totalStudents}
color="purple"
onClick={() => router.push("/#studentsPerformance")}
/>
<IconCard
Icon={BsDatabase}
label="Master Statistical"
// value={masterCorporateUserGroups.length}
color="purple"
onClick={() => router.push("/#statistical")}
/>
<button
disabled={isAssignmentsLoading}
onClick={() => router.push("/#assignments")}
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
</span>
</span>
</button>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest teachers</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{teachers
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
</section>
</>
</>
);
}

View File

@@ -1,253 +0,0 @@
import Button from "@/components/Low/Button";
import ProgressBar from "@/components/Low/ProgressBar";
import InviteCard from "@/components/Medium/InviteCard";
import ProfileSummary from "@/components/ProfileSummary";
import useAssignments from "@/hooks/useAssignments";
import useGradingSystem from "@/hooks/useGrading";
import useInvites from "@/hooks/useInvites";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers, { userHashStudent, userHashTeacher, userHashCorporate } from "@/hooks/useUsers";
import { Invite } from "@/interfaces/invite";
import { Assignment } from "@/interfaces/results";
import { CorporateUser, MasterCorporateUser, Stat, User } from "@/interfaces/user";
import useExamStore from "@/stores/exam";
import { getExamById } from "@/utils/exams";
import { getUserCorporate } from "@/utils/groups";
import { countExamModules, countFullExams, MODULE_ARRAY, sortByModule, sortByModuleName } from "@/utils/moduleUtils";
import { getGradingLabel, getLevelLabel, getLevelScore } from "@/utils/score";
import { averageScore, groupBySession } from "@/utils/stats";
import { CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OrderResponseBody } from "@paypal/paypal-js";
import { PayPalButtons } from "@paypal/react-paypal-js";
import axios from "axios";
import clsx from "clsx";
import { capitalize } from "lodash";
import moment from "moment";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar } from "react-icons/bs";
import { toast } from "react-toastify";
import { activeAssignmentFilter } from "@/utils/assignments";
import ModuleBadge from "@/components/ModuleBadge";
import useSessions from "@/hooks/useSessions";
interface Props {
user: User;
linkedCorporate?: CorporateUser | MasterCorporateUser;
}
export default function StudentDashboard({ user, linkedCorporate }: Props) {
const { sessions } = useSessions(user.id);
const { data: stats } = useFilterRecordsByUser<Stat[]>(user.id, !user?.id);
const { assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments } = useAssignments({ assignees: user?.id });
const { invites, isLoading: isInvitesLoading, reload: reloadInvites } = useInvites({ to: user.id });
const { users: teachers } = useUsers(userHashTeacher);
const { users: corporates } = useUsers(userHashCorporate);
const users = useMemo(() => [...teachers, ...corporates], [teachers, corporates]);
const router = useRouter();
const dispatch = useExamStore((state) => state.dispatch);
const startAssignment = (assignment: Assignment) => {
const examPromises = assignment.exams.filter((e) => e.assignee === user.id).map((e) => getExamById(e.module, e.id));
Promise.all(examPromises).then((exams) => {
if (exams.every((x) => !!x)) {
dispatch({
type: "INIT_EXAM", payload: {
exams: exams.map((x) => x!).sort(sortByModule),
modules: exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
assignment
}
})
router.push("/exam");
}
});
};
const studentAssignments = assignments.filter(activeAssignmentFilter);
return (
<>
{linkedCorporate && (
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
Linked to: <b>{linkedCorporate?.corporateInformation?.companyInformation.name || linkedCorporate.name}</b>
</div>
)}
<ProfileSummary
user={user}
items={[
{
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: countFullExams(stats),
label: "Exams",
tooltip: "Number of all conducted completed exams",
},
{
icon: <BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: countExamModules(stats),
label: "Modules",
tooltip: "Number of all exam modules performed including Level Test",
},
{
icon: <BsStar className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />,
value: `${stats.length > 0 ? averageScore(stats) : 0}%`,
label: "Average Score",
tooltip: "Average success rate for questions responded",
},
]}
/>
{/* Bio */}
<section className="flex flex-col gap-1 md:gap-3">
<span className="text-lg font-bold">Bio</span>
<span className="text-mti-gray-taupe">
{user.bio || "Your bio will appear here, you can change it by clicking on your name in the top right corner."}
</span>
</section>
{/* Assignments */}
<section className="flex flex-col gap-1 md:gap-3">
<div className="flex items-center gap-4">
<div
onClick={reloadAssignments}
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
<span className="text-mti-black text-lg font-bold">Assignments</span>
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
</div>
</div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."}
{studentAssignments
.sort((a, b) => moment(a.startDate).diff(b.startDate))
.map((assignment) => (
<div
className={clsx(
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
assignment.results.map((r) => r.user).includes(user.id) && "border-mti-green-light",
)}
key={assignment.id}>
<div className="flex flex-col gap-1">
<h3 className="text-mti-black/90 text-xl font-semibold">{assignment.name}</h3>
<span className="flex justify-between gap-1 text-lg">
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
<span>-</span>
<span>{moment(assignment.endDate).format("DD/MM/YY, HH:mm")}</span>
</span>
</div>
<div className="flex w-full items-center justify-between">
<div className="-md:mt-2 grid w-fit min-w-[140px] grid-cols-2 grid-rows-2 place-items-center justify-between gap-4">
{assignment.exams
.filter((e) => e.assignee === user.id)
.map((e) => e.module)
.sort(sortByModuleName)
.map((module) => (
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
))}
</div>
{!assignment.results.map((r) => r.user).includes(user.id) && (
<>
<div
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
data-tip="Your screen size is too small to perform an assignment">
<Button className="h-full w-full !rounded-xl" variant="outline">
Start
</Button>
</div>
<div
data-tip="You have already started this assignment!"
className={clsx(
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
sessions.filter((x) => x.assignment?.id === assignment.id).length > 0 && "tooltip",
)}>
<Button
className={clsx("w-full h-full !rounded-xl")}
onClick={() => startAssignment(assignment)}
variant="outline"
disabled={sessions.filter((x) => x.assignment?.id === assignment.id).length > 0}>
Start
</Button>
</div>
</>
)}
{assignment.results.map((r) => r.user).includes(user.id) && (
<Button
onClick={() => router.push("/record")}
color="green"
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
variant="outline">
Submitted
</Button>
)}
</div>
</div>
))}
</span>
</section>
{/* Invites */}
{invites.length > 0 && (
<section className="flex flex-col gap-1 md:gap-3">
<div className="flex items-center gap-4">
<div
onClick={reloadInvites}
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
<span className="text-mti-black text-lg font-bold">Invites</span>
<BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
</div>
</div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{invites.map((invite) => (
<InviteCard key={invite.id} invite={invite} users={users} reload={reloadInvites} />
))}
</span>
</section>
)}
{/* Score History */}
<section className="flex flex-col gap-3">
<span className="text-lg font-bold">Score History</span>
<div className="-md:grid-rows-4 grid gap-6 md:grid-cols-2">
{MODULE_ARRAY.map((module) => {
const desiredLevel = user.desiredLevels[module] || 9;
const level = user.levels[module] || 0;
return (
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4" key={module}>
<div className="flex items-center gap-2 md:gap-3">
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
{module === "reading" && <BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />}
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
</div>
<div className="flex w-full justify-between">
<span className="text-sm font-bold md:font-extrabold">{capitalize(module)}</span>
<span className="text-mti-gray-dim text-sm font-normal">
{module !== "level" && `Level ${level} / Level 9 (Desired Level: ${desiredLevel})`}
</span>
</div>
</div>
<div className="md:pl-14">
<ProgressBar
color={module}
label=""
mark={module === "level" ? undefined : Math.round((desiredLevel * 100) / 9)}
markLabel={`Desired Level: ${desiredLevel}`}
percentage={module === "level" ? level : Math.round((level * 100) / 9)}
className="h-2 w-full"
/>
</div>
</div>
);
})}
</div>
</section>
</>
);
}

View File

@@ -1,326 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers, {userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers";
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter} from "@/utils";
import moment from "moment";
import {useEffect, useMemo, useState} from "react";
import {
BsArrowLeft,
BsArrowRepeat,
BsClipboard2Data,
BsClipboard2DataFill,
BsClipboard2Heart,
BsClipboard2X,
BsClipboardPulse,
BsClock,
BsEnvelopePaper,
BsGlobeCentralSouthAsia,
BsPaperclip,
BsPeople,
BsPerson,
BsPersonAdd,
BsPersonFill,
BsPersonFillGear,
BsPersonGear,
BsPlus,
BsRepeat,
BsRepeat1,
} 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 useAssignments from "@/hooks/useAssignments";
import {Assignment} from "@/interfaces/results";
import AssignmentCard from "./AssignmentCard";
import Button from "@/components/Low/Button";
import clsx from "clsx";
import ProgressBar from "@/components/Low/ProgressBar";
import AssignmentCreator from "./AssignmentCreator";
import AssignmentView from "./AssignmentView";
import {getUserCorporate} from "@/utils/groups";
import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
import AssignmentsPage from "./views/AssignmentsPage";
import {useRouter} from "next/router";
import useFilterStore from "@/stores/listFilterStore";
interface Props {
user: User;
linkedCorporate?: CorporateUser | MasterCorporateUser;
}
const studentHash = {
type: "student",
orderBy: "registrationDate",
size: 25,
};
export default function TeacherDashboard({user, linkedCorporate}: Props) {
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {groups} = useGroups({adminAdmins: user.id});
const {permissions} = usePermissions(user.id);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash);
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
useEffect(() => {
setShowModal(!!selectedUser && router.asPath === "/#");
}, [selectedUser, router.asPath]);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
onClick={() => 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">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const GroupsList = () => {
const filter = (x: Group) => x.admin === user.id;
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
</div>
<GroupList user={user} />
</>
);
};
const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: students.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);
};
if (router.asPath === "/#students")
return (
<UserList
user={user}
type="student"
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => router.push("/")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
if (router.asPath === "/#assignments")
return (
<AssignmentsPage
assignments={assignments}
groups={assignmentsGroups}
user={user}
reloadAssignments={reloadAssignments}
isLoading={isAssignmentsLoading}
onBack={() => router.push("/")}
/>
);
if (router.asPath === "/#groups") return <GroupsList />;
return (
<>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<>
{selectedUser && (
<div className="w-full flex flex-col gap-8">
<UserCard
loggedInUser={user}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload && selectedUser!.type === "student") reloadStudents();
}}
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("/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("/users");
}
: undefined
}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
<>
{linkedCorporate && (
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
Linked to: <b>{linkedCorporate?.corporateInformation?.companyInformation.name || linkedCorporate.name}</b>
</div>
)}
<section
className={clsx(
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
!!linkedCorporate && "mt-12 xl:mt-6",
)}>
<IconCard
onClick={() => router.push("/#students")}
isLoading={isStudentsLoading}
Icon={BsPersonFill}
label="Students"
value={totalStudents}
color="purple"
/>
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
isLoading={isStudentsLoading}
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
color="purple"
/>
{checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
<IconCard
Icon={BsPeople}
label="Groups"
value={groups.filter((x) => x.admin === user.id).length}
color="purple"
onClick={() => router.push("/#groups")}
/>
)}
<div
onClick={() => router.push("/#assignments")}
className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
</span>
</div>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{students
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
</section>
</>
</>
);
}

View File

@@ -1,184 +0,0 @@
import useUsers from "@/hooks/useUsers";
import {Assignment} from "@/interfaces/results";
import {CorporateUser, Group, User} from "@/interfaces/user";
import {getUserCompanyName} from "@/resources/user";
import {
activeAssignmentFilter,
archivedAssignmentFilter,
futureAssignmentFilter,
pastAssignmentFilter,
startHasExpiredAssignmentFilter,
} from "@/utils/assignments";
import clsx from "clsx";
import {groupBy} from "lodash";
import {useState} from "react";
import {BsArrowLeft, BsArrowRepeat, BsPlus} from "react-icons/bs";
import AssignmentCard from "../AssignmentCard";
import AssignmentCreator from "../AssignmentCreator";
import AssignmentView from "../AssignmentView";
interface Props {
assignments: Assignment[];
corporateAssignments?: ({corporate?: CorporateUser} & Assignment)[];
groups: Group[];
isLoading: boolean;
user: User;
onBack: () => void;
reloadAssignments: () => void;
}
export default function AssignmentsPage({assignments, corporateAssignments, user, groups, isLoading, onBack, reloadAssignments}: Props) {
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const {users} = useUsers();
const displayAssignmentView = !!selectedAssignment && !isCreatingAssignment;
const assignmentsPastExpiredStart = assignments.filter(startHasExpiredAssignmentFilter);
return (
<>
{displayAssignmentView && (
<AssignmentView
isOpen={displayAssignmentView}
users={users}
onClose={() => {
setSelectedAssignment(undefined);
setIsCreatingAssignment(false);
reloadAssignments();
}}
assignment={selectedAssignment}
/>
)}
{/** I'll be using this is creating assingment as a workaround for a key to trigger a new rendering */}
{isCreatingAssignment && (
<AssignmentCreator
assignment={selectedAssignment}
groups={groups}
users={[...users, user]}
user={user}
isCreating={isCreatingAssignment}
cancelCreation={() => {
setIsCreatingAssignment(false);
setSelectedAssignment(undefined);
reloadAssignments();
}}
/>
)}
<div className="w-full flex justify-between items-center">
<div
onClick={onBack}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<div
onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-lg font-bold">Active Assignments Status</span>
<div className="flex items-center gap-4">
<span>
<b>Total:</b> {assignments.filter(activeAssignmentFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/
{assignments.filter(activeAssignmentFilter).reduce((acc, curr) => curr.exams.length + acc, 0)}
</span>
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => (
<div key={x}>
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span>
<span>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)}
</span>
</div>
))}
</div>
</div>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeAssignmentFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(activeAssignmentFilter).map((a) => (
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureAssignmentFilter).length})</h2>
<div className="flex flex-wrap gap-2">
<div
onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
<BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span>
</div>
{assignments.filter(futureAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
}}
key={a.id}
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastAssignmentFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(pastAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowArchive
allowExcelDownload
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Assignments start expired ({assignmentsPastExpiredStart.length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(startHasExpiredAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowArchive
allowExcelDownload
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(archivedAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowUnarchive
allowExcelDownload
/>
))}
</div>
</section>
</>
);
}

View File

@@ -1,31 +1,32 @@
import { RolePermission } from "@/resources/entityPermissions";
export interface Entity {
id: string;
label: string;
id: string;
label: string;
licenses: number
}
export interface Role {
id: string;
entityID: string;
permissions: RolePermission[];
label: string;
isDefault?: boolean
id: string;
entityID: string;
permissions: RolePermission[];
label: string;
isDefault?: boolean
}
export interface EntityWithRoles extends Entity {
roles: Role[];
roles: Role[];
};
export type WithLabeledEntities<T> = T extends { entities: { id: string; role: string }[] }
? Omit<T, "entities"> & { entities: { id: string; label?: string; role: string, roleLabel?: string }[] }
: T;
? Omit<T, "entities"> & { entities: { id: string; label?: string; role: string, roleLabel?: string }[] }
: T;
export type WithEntity<T> = T extends { entity?: string }
? Omit<T, "entity"> & { entity: Entity }
: T;
? Omit<T, "entity"> & { entity: Entity }
: T;
export type WithEntities<T> = T extends { entities: { id: string; role: string }[] }
? Omit<T, "entities"> & { entities: { entity?: Entity; role?: Role }[] }
: T;
? Omit<T, "entities"> & { entities: { entity?: Entity; role?: Role }[] }
: T;

View File

@@ -69,8 +69,6 @@ export interface DeveloperUser extends BasicUser {
}
export interface CorporateInformation {
companyInformation: CompanyInformation;
monthlyDuration: number;
payment?: {
value: number;
currency: string;
@@ -85,11 +83,6 @@ export interface AgentInformation {
companyArabName?: string;
}
export interface CompanyInformation {
name: string;
userAmount: number;
}
export interface DemographicInformation {
country: string;
phone: string;

View File

@@ -28,7 +28,7 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
return (
<>
{(creatorUser?.type === "corporate" ? creatorUser?.corporateInformation?.companyInformation?.name : creatorUser?.name || "N/A") || "N/A"}{" "}
{(creatorUser?.name || "N/A") || "N/A"}{" "}
{creatorUser && `(${USER_TYPE_LABELS[creatorUser?.type]})`}
</>
);
@@ -213,10 +213,7 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
value={
filteredCorporate
? {
label: `${filteredCorporate?.type === "corporate"
? filteredCorporate.corporateInformation?.companyInformation?.name || filteredCorporate.name
: filteredCorporate.name
} (${USER_TYPE_LABELS[filteredCorporate?.type]})`,
label: `${filteredCorporate.name} (${USER_TYPE_LABELS[filteredCorporate?.type]})`,
value: filteredCorporate.id,
}
: null
@@ -224,8 +221,7 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
options={users
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
.map((x) => ({
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${USER_TYPE_LABELS[x.type]
})`,
label: `${x.name} (${USER_TYPE_LABELS[x.type]})`,
value: x.id,
user: x,
}))}

View File

@@ -29,419 +29,417 @@ const columnHelper = createColumnHelper<WithLabeledEntities<User>>();
const searchFields = [["name"], ["email"], ["entities", ""]];
export default function UserList({
user,
filters = [],
type,
renderHeader,
user,
filters = [],
type,
renderHeader,
}: {
user: User;
filters?: ((user: User) => boolean)[];
type?: Type;
renderHeader?: (total: number) => JSX.Element;
user: User;
filters?: ((user: User) => boolean)[];
type?: Type;
renderHeader?: (total: number) => JSX.Element;
}) {
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
const [selectedUser, setSelectedUser] = useState<User>();
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
const [selectedUser, setSelectedUser] = useState<User>();
const { users, reload } = useEntitiesUsers(type)
const { entities } = useEntities()
const { users, reload } = useEntitiesUsers(type)
const { entities } = useEntities()
const { balance } = useUserBalance();
const { balance } = useUserBalance();
const isAdmin = useMemo(() => ["admin", "developer"].includes(user?.type), [user?.type])
const isAdmin = useMemo(() => ["admin", "developer"].includes(user?.type), [user?.type])
const entitiesViewStudents = useAllowedEntities(user, entities, "view_students")
const entitiesEditStudents = useAllowedEntities(user, entities, "edit_students")
const entitiesDeleteStudents = useAllowedEntities(user, entities, "delete_students")
const entitiesViewStudents = useAllowedEntities(user, entities, "view_students")
const entitiesEditStudents = useAllowedEntities(user, entities, "edit_students")
const entitiesDeleteStudents = useAllowedEntities(user, entities, "delete_students")
const entitiesViewTeachers = useAllowedEntities(user, entities, "view_teachers")
const entitiesEditTeachers = useAllowedEntities(user, entities, "edit_teachers")
const entitiesDeleteTeachers = useAllowedEntities(user, entities, "delete_teachers")
const entitiesViewTeachers = useAllowedEntities(user, entities, "view_teachers")
const entitiesEditTeachers = useAllowedEntities(user, entities, "edit_teachers")
const entitiesDeleteTeachers = useAllowedEntities(user, entities, "delete_teachers")
const entitiesViewCorporates = useAllowedEntities(user, entities, "view_corporates")
const entitiesEditCorporates = useAllowedEntities(user, entities, "edit_corporates")
const entitiesDeleteCorporates = useAllowedEntities(user, entities, "delete_corporates")
const entitiesViewCorporates = useAllowedEntities(user, entities, "view_corporates")
const entitiesEditCorporates = useAllowedEntities(user, entities, "edit_corporates")
const entitiesDeleteCorporates = useAllowedEntities(user, entities, "delete_corporates")
const entitiesViewMasterCorporates = useAllowedEntities(user, entities, "view_mastercorporates")
const entitiesEditMasterCorporates = useAllowedEntities(user, entities, "edit_mastercorporates")
const entitiesDeleteMasterCorporates = useAllowedEntities(user, entities, "delete_mastercorporates")
const entitiesViewMasterCorporates = useAllowedEntities(user, entities, "view_mastercorporates")
const entitiesEditMasterCorporates = useAllowedEntities(user, entities, "edit_mastercorporates")
const entitiesDeleteMasterCorporates = useAllowedEntities(user, entities, "delete_mastercorporates")
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";
};
const allowedUsers = useMemo(() => users.filter((u) => {
const allowedUsers = useMemo(() => users.filter((u) => {
if (isAdmin) return true
if (u.id === user?.id) return false
switch (u.type) {
case "student": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewStudents, 'id').includes(id))
case "teacher": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewTeachers, 'id').includes(id))
case 'corporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewCorporates, 'id').includes(id))
case 'mastercorporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewMasterCorporates, 'id').includes(id))
default: return false
}
})
, [entitiesViewCorporates, entitiesViewMasterCorporates, entitiesViewStudents, entitiesViewTeachers, isAdmin, user?.id, users])
switch (u.type) {
case "student": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewStudents, 'id').includes(id))
case "teacher": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewTeachers, 'id').includes(id))
case 'corporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewCorporates, 'id').includes(id))
case 'mastercorporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewMasterCorporates, 'id').includes(id))
default: return false
}
})
, [entitiesViewCorporates, entitiesViewMasterCorporates, entitiesViewStudents, entitiesViewTeachers, isAdmin, user?.id, users])
const displayUsers = useMemo(() =>
filters.length > 0 ? filters.reduce((d, f) => d.filter(f), allowedUsers) : allowedUsers,
[filters, allowedUsers])
const displayUsers = useMemo(() =>
filters.length > 0 ? filters.reduce((d, f) => d.filter(f), allowedUsers) : allowedUsers,
[filters, allowedUsers])
const deleteAccount = (user: User) => {
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
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 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 getEditPermission = (type: Type) => {
if (type === "student") return entitiesEditStudents
if (type === "teacher") return entitiesEditTeachers
if (type === "corporate") return entitiesEditCorporates
if (type === "mastercorporate") return entitiesEditMasterCorporates
const getEditPermission = (type: Type) => {
if (type === "student") return entitiesEditStudents
if (type === "teacher") return entitiesEditTeachers
if (type === "corporate") return entitiesEditCorporates
if (type === "mastercorporate") return entitiesEditMasterCorporates
return []
}
return []
}
const getDeletePermission = (type: Type) => {
if (type === "student") return entitiesDeleteStudents
if (type === "teacher") return entitiesDeleteTeachers
if (type === "corporate") return entitiesDeleteCorporates
if (type === "mastercorporate") return entitiesDeleteMasterCorporates
const getDeletePermission = (type: Type) => {
if (type === "student") return entitiesDeleteStudents
if (type === "teacher") return entitiesDeleteTeachers
if (type === "corporate") return entitiesDeleteCorporates
if (type === "mastercorporate") return entitiesDeleteMasterCorporates
return []
}
return []
}
const canEditUser = (u: User) =>
isAdmin || u.entities.some(e => mapBy(getEditPermission(u.type), 'id').includes(e.id))
const canEditUser = (u: User) =>
isAdmin || u.entities.some(e => mapBy(getEditPermission(u.type), 'id').includes(e.id))
const canDeleteUser = (u: User) =>
isAdmin || u.entities.some(e => mapBy(getDeletePermission(u.type), 'id').includes(e.id))
const canDeleteUser = (u: User) =>
isAdmin || u.entities.some(e => mapBy(getDeletePermission(u.type), 'id').includes(e.id))
const actionColumn = ({ row }: { row: { original: User } }) => {
const actionColumn = ({ row }: { row: { original: User } }) => {
const canEdit = canEditUser(row.original)
const canDelete = canDeleteUser(row.original)
return (
<div className="flex gap-4">
{!row.original.isVerified && canEdit && (
<div data-tip="Verify User" className="cursor-pointer tooltip" onClick={() => verifyAccount(row.original)}>
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
{canEdit && (
<div
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
className="cursor-pointer tooltip"
onClick={() => toggleDisableAccount(row.original)}>
{row.original.status === "disabled" ? (
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
) : (
<BsFillExclamationOctagonFill className="hover:text-mti-purple-light transition ease-in-out duration-300" />
)}
</div>
)}
{canDelete && (
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteAccount(row.original)}>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
</div>
);
};
return (
<div className="flex gap-4">
{!row.original.isVerified && canEdit && (
<div data-tip="Verify User" className="cursor-pointer tooltip" onClick={() => verifyAccount(row.original)}>
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
{canEdit && (
<div
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
className="cursor-pointer tooltip"
onClick={() => toggleDisableAccount(row.original)}>
{row.original.status === "disabled" ? (
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
) : (
<BsFillExclamationOctagonFill className="hover:text-mti-purple-light transition ease-in-out duration-300" />
)}
</div>
)}
{canDelete && (
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteAccount(row.original)}>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
</div>
);
};
const demographicColumns = [
columnHelper.accessor("name", {
header: "Name",
cell: ({ row, getValue }) => (
<div
className={clsx(
canEditUser(row.original) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() =>
canEditUser(row.original) ? setSelectedUser(row.original) : null
}>
{getValue()}
</div>
),
}),
columnHelper.accessor("demographicInformation.country", {
header: "Country",
cell: (info) =>
info.getValue()
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${countries[info.getValue() as unknown as keyof TCountries]?.name
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
: "N/A",
}),
columnHelper.accessor("demographicInformation.phone", {
header: "Phone",
cell: (info) => info.getValue() || "N/A",
enableSorting: true,
}),
columnHelper.accessor(
(x) =>
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
{
id: "employment",
header: "Employment",
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
enableSorting: true,
},
),
columnHelper.accessor("lastLogin", {
header: "Last Login",
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
}),
columnHelper.accessor("demographicInformation.gender", {
header: "Gender",
cell: (info) => capitalize(info.getValue()) || "N/A",
enableSorting: true,
}),
{
header: (
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
Switch
</span>
),
id: "actions",
cell: actionColumn,
sortable: false
},
];
const demographicColumns = [
columnHelper.accessor("name", {
header: "Name",
cell: ({ row, getValue }) => (
<div
className={clsx(
canEditUser(row.original) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() =>
canEditUser(row.original) ? setSelectedUser(row.original) : null
}>
{getValue()}
</div>
),
}),
columnHelper.accessor("demographicInformation.country", {
header: "Country",
cell: (info) =>
info.getValue()
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${countries[info.getValue() as unknown as keyof TCountries]?.name
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
: "N/A",
}),
columnHelper.accessor("demographicInformation.phone", {
header: "Phone",
cell: (info) => info.getValue() || "N/A",
enableSorting: true,
}),
columnHelper.accessor(
(x) =>
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
{
id: "employment",
header: "Employment",
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
enableSorting: true,
},
),
columnHelper.accessor("lastLogin", {
header: "Last Login",
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
}),
columnHelper.accessor("demographicInformation.gender", {
header: "Gender",
cell: (info) => capitalize(info.getValue()) || "N/A",
enableSorting: true,
}),
{
header: (
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
Switch
</span>
),
id: "actions",
cell: actionColumn,
sortable: false
},
];
const defaultColumns = [
columnHelper.accessor("name", {
header: "Name",
cell: ({ row, getValue }) => (
<div
className={clsx(
canEditUser(row.original) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() =>
canEditUser(row.original) ? setSelectedUser(row.original) : null
}>
{getValue()}
</div>
),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: ({ row, getValue }) => (
<div
className={clsx(
canEditUser(row.original) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() => (canEditUser(row.original) ? setSelectedUser(row.original) : null)}>
{getValue()}
</div>
),
}),
columnHelper.accessor("type", {
header: "Type",
cell: (info) => USER_TYPE_LABELS[info.getValue()],
}),
columnHelper.accessor("studentID", {
header: "Student ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("entities", {
header: "Entities",
cell: ({ getValue }) => mapBy(getValue(), 'label').join(', '),
}),
columnHelper.accessor("subscriptionExpirationDate", {
header: "Expiration",
cell: (info) => (
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
</span>
),
}),
columnHelper.accessor("isVerified", {
header: "Verified",
cell: (info) => (
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
<div
className={clsx(
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
"transition duration-300 ease-in-out",
info.getValue() && "!bg-mti-purple-light ",
)}>
<BsCheck color="white" className="w-full h-full" />
</div>
</div>
),
}),
{
header: (
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
Switch
</span>
),
id: "actions",
cell: actionColumn,
sortable: false
},
];
const defaultColumns = [
columnHelper.accessor("name", {
header: "Name",
cell: ({ row, getValue }) => (
<div
className={clsx(
canEditUser(row.original) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() =>
canEditUser(row.original) ? setSelectedUser(row.original) : null
}>
{getValue()}
</div>
),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: ({ row, getValue }) => (
<div
className={clsx(
canEditUser(row.original) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() => (canEditUser(row.original) ? setSelectedUser(row.original) : null)}>
{getValue()}
</div>
),
}),
columnHelper.accessor("type", {
header: "Type",
cell: (info) => USER_TYPE_LABELS[info.getValue()],
}),
columnHelper.accessor("studentID", {
header: "Student ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("entities", {
header: "Entities",
cell: ({ getValue }) => mapBy(getValue(), 'label').join(', '),
}),
columnHelper.accessor("subscriptionExpirationDate", {
header: "Expiration",
cell: (info) => (
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
</span>
),
}),
columnHelper.accessor("isVerified", {
header: "Verified",
cell: (info) => (
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
<div
className={clsx(
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
"transition duration-300 ease-in-out",
info.getValue() && "!bg-mti-purple-light ",
)}>
<BsCheck color="white" className="w-full h-full" />
</div>
</div>
),
}),
{
header: (
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
Switch
</span>
),
id: "actions",
cell: actionColumn,
sortable: false
},
];
const downloadExcel = (rows: WithLabeledEntities<User>[]) => {
const csv = exportListToExcel(rows);
const downloadExcel = (rows: WithLabeledEntities<User>[]) => {
const csv = exportListToExcel(rows);
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) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], 'id').includes(id));
const viewStudentFilter = (x: User) => x.type === "student";
const viewTeacherFilter = (x: User) => x.type === "teacher";
const belongsToAdminFilter = (x: User) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], 'id').includes(id));
const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x);
const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x);
const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x);
const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x);
const renderUserCard = (selectedUser: User) => {
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin);
return (
<div className="w-full flex flex-col gap-8">
<UserCard
maxUserAmount={
user.type === "mastercorporate" ? (user.corporateInformation?.companyInformation?.userAmount || 0) - balance : undefined
}
loggedInUser={user}
onViewStudents={
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 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 (
<div className="w-full flex flex-col gap-8">
<UserCard
maxUserAmount={0}
loggedInUser={user}
onViewStudents={
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
? () => {
appendUserFilters({
id: "view-students",
filter: viewStudentFilter,
});
appendUserFilters({
id: "belongs-to-admin",
filter: belongsToAdminFilter,
});
router.push("/users");
}
: undefined
}
onViewTeachers={
(selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0
? () => {
appendUserFilters({
id: "view-teachers",
filter: viewTeacherFilter,
});
appendUserFilters({
id: "belongs-to-admin",
filter: belongsToAdminFilter,
});
router.push("/users");
}
: undefined
}
onViewTeachers={
(selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0
? () => {
appendUserFilters({
id: "view-teachers",
filter: viewTeacherFilter,
});
appendUserFilters({
id: "belongs-to-admin",
filter: belongsToAdminFilter,
});
router.push("/users");
}
: undefined
}
onViewCorporate={
selectedUser.type === "teacher" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-corporate",
filter: (x: User) => x.type === "corporate",
});
appendUserFilters({
id: "belongs-to-admin",
filter: belongsToAdminFilter
});
router.push("/users");
}
: undefined
}
onViewCorporate={
selectedUser.type === "teacher" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-corporate",
filter: (x: User) => x.type === "corporate",
});
appendUserFilters({
id: "belongs-to-admin",
filter: belongsToAdminFilter
});
router.push("/users");
}
: undefined
}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload) reload();
}}
user={selectedUser}
/>
</div>
);
};
router.push("/users");
}
: undefined
}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload) reload();
}}
user={selectedUser}
/>
</div>
);
};
return (
<>
{renderHeader && renderHeader(displayUsers.length)}
<div className="w-full">
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
{selectedUser && renderUserCard(selectedUser)}
</Modal>
<Table<WithLabeledEntities<User>>
data={displayUsers}
columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any}
searchFields={searchFields}
onDownload={downloadExcel}
/>
</div>
</>
);
return (
<>
{renderHeader && renderHeader(displayUsers.length)}
<div className="w-full">
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
{selectedUser && renderUserCard(selectedUser)}
</Modal>
<Table<WithLabeledEntities<User>>
data={displayUsers}
columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any}
searchFields={searchFields}
onDownload={downloadExcel}
/>
</div>
</>
);
}

View File

@@ -13,271 +13,267 @@ import moment from "moment";
import useAcceptedTerms from "@/hooks/useAcceptedTerms";
interface Props {
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
mutateUser: KeyedMutator<User>;
sendEmailVerification: typeof sendEmailVerification;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
mutateUser: KeyedMutator<User>;
sendEmailVerification: typeof sendEmailVerification;
}
const availableDurations = {
"1_month": { label: "1 Month", number: 1 },
"3_months": { label: "3 Months", number: 3 },
"6_months": { label: "6 Months", number: 6 },
"12_months": { label: "12 Months", number: 12 },
"1_month": { label: "1 Month", number: 1 },
"3_months": { label: "3 Months", number: 3 },
"6_months": { label: "6 Months", number: 6 },
"12_months": { label: "12 Months", number: 12 },
};
export default function RegisterCorporate({
isLoading,
setIsLoading,
mutateUser,
sendEmailVerification,
isLoading,
setIsLoading,
mutateUser,
sendEmailVerification,
}: Props) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [referralAgent, setReferralAgent] = useState<string | undefined>();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [referralAgent, setReferralAgent] = useState<string | undefined>();
const [companyName, setCompanyName] = useState("");
const [companyUsers, setCompanyUsers] = useState(0);
const [subscriptionDuration, setSubscriptionDuration] = useState(1);
const {acceptedTerms, renderCheckbox} = useAcceptedTerms();
const [companyName, setCompanyName] = useState("");
const [companyUsers, setCompanyUsers] = useState(0);
const [subscriptionDuration, setSubscriptionDuration] = useState(1);
const { acceptedTerms, renderCheckbox } = useAcceptedTerms();
const { users } = useUsers();
const { users } = useUsers();
const onSuccess = () =>
toast.success(
"An e-mail has been sent, please make sure to check your spam folder!",
);
const onSuccess = () =>
toast.success(
"An e-mail has been sent, please make sure to check your spam folder!",
);
const onError = (e: Error) => {
console.error(e);
toast.error("Something went wrong, please logout and re-login.", {
toastId: "send-verify-error",
});
};
const onError = (e: Error) => {
console.error(e);
toast.error("Something went wrong, please logout and re-login.", {
toastId: "send-verify-error",
});
};
const register = (e: any) => {
e.preventDefault();
const register = (e: any) => {
e.preventDefault();
if (confirmPassword !== password) {
toast.error("Your passwords do not match!", {
toastId: "password-not-match",
});
return;
}
if (confirmPassword !== password) {
toast.error("Your passwords do not match!", {
toastId: "password-not-match",
});
return;
}
setIsLoading(true);
axios
.post("/api/register", {
name,
email,
password,
type: "corporate",
profilePicture: "/defaultAvatar.png",
subscriptionExpirationDate: moment().subtract(1, "days").toISOString(),
corporateInformation: {
companyInformation: {
name: companyName,
userAmount: companyUsers,
},
monthlyDuration: subscriptionDuration,
referralAgent,
},
})
.then((response) => {
mutateUser(response.data.user).then(() =>
sendEmailVerification(setIsLoading, onSuccess, onError),
);
})
.catch((error) => {
console.log(error.response.data);
setIsLoading(true);
axios
.post("/api/register", {
name,
email,
password,
type: "corporate",
profilePicture: "/defaultAvatar.png",
subscriptionExpirationDate: moment().subtract(1, "days").toISOString(),
corporateInformation: {
monthlyDuration: subscriptionDuration,
referralAgent,
},
})
.then((response) => {
mutateUser(response.data.user).then(() =>
sendEmailVerification(setIsLoading, onSuccess, onError),
);
})
.catch((error) => {
console.log(error.response.data);
if (error.response.status === 401) {
toast.error("There is already a user with that e-mail!");
return;
}
if (error.response.status === 401) {
toast.error("There is already a user with that e-mail!");
return;
}
if (error.response.status === 400) {
toast.error("The provided code is invalid!");
return;
}
if (error.response.status === 400) {
toast.error("The provided code is invalid!");
return;
}
toast.error("There was something wrong, please try again!");
})
.finally(() => setIsLoading(false));
};
toast.error("There was something wrong, please try again!");
})
.finally(() => setIsLoading(false));
};
return (
<form
className="flex w-full flex-col items-center gap-4"
onSubmit={register}
>
<div className="flex w-full gap-4">
<Input
type="text"
name="name"
onChange={(e) => setName(e)}
placeholder="Enter your name"
defaultValue={name}
required
/>
<Input
type="email"
name="email"
onChange={(e) => setEmail(e.toLowerCase())}
placeholder="Enter email address"
defaultValue={email}
required
/>
</div>
return (
<form
className="flex w-full flex-col items-center gap-4"
onSubmit={register}
>
<div className="flex w-full gap-4">
<Input
type="text"
name="name"
onChange={(e) => setName(e)}
placeholder="Enter your name"
defaultValue={name}
required
/>
<Input
type="email"
name="email"
onChange={(e) => setEmail(e.toLowerCase())}
placeholder="Enter email address"
defaultValue={email}
required
/>
</div>
<div className="flex w-full gap-4">
<Input
type="password"
name="password"
onChange={(e) => setPassword(e)}
placeholder="Enter your password"
defaultValue={password}
required
/>
<Input
type="password"
name="confirmPassword"
onChange={(e) => setConfirmPassword(e)}
placeholder="Confirm your password"
defaultValue={confirmPassword}
required
/>
</div>
<div className="flex w-full gap-4">
<Input
type="password"
name="password"
onChange={(e) => setPassword(e)}
placeholder="Enter your password"
defaultValue={password}
required
/>
<Input
type="password"
name="confirmPassword"
onChange={(e) => setConfirmPassword(e)}
placeholder="Confirm your password"
defaultValue={confirmPassword}
required
/>
</div>
<Divider className="!my-2 w-full" />
<Divider className="!my-2 w-full" />
<div className="flex w-full gap-4">
<Input
type="text"
name="companyName"
onChange={(e) => setCompanyName(e)}
placeholder="Corporate name"
label="Corporate name"
defaultValue={companyName}
required
/>
<Input
type="number"
name="companyUsers"
onChange={(e) => setCompanyUsers(parseInt(e))}
label="Number of users"
defaultValue={companyUsers}
required
/>
</div>
<div className="flex w-full gap-4">
<Input
type="text"
name="companyName"
onChange={(e) => setCompanyName(e)}
placeholder="Corporate name"
label="Corporate name"
defaultValue={companyName}
required
/>
<Input
type="number"
name="companyUsers"
onChange={(e) => setCompanyUsers(parseInt(e))}
label="Number of users"
defaultValue={companyUsers}
required
/>
</div>
<div className="flex w-full gap-4">
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Referral *
</label>
<Select
className="placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none disabled:cursor-not-allowed"
options={[
{ value: "", label: "No referral" },
...users
.filter((u) => u.type === "agent")
.map((x) => ({ value: x.id, label: `${x.name} - ${x.email}` })),
]}
defaultValue={{ value: "", label: "No referral" }}
onChange={(value) => setReferralAgent(value?.value)}
styles={{
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,
}),
}}
/>
</div>
<div className="flex w-full gap-4">
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Referral *
</label>
<Select
className="placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none disabled:cursor-not-allowed"
options={[
{ value: "", label: "No referral" },
...users
.filter((u) => u.type === "agent")
.map((x) => ({ value: x.id, label: `${x.name} - ${x.email}` })),
]}
defaultValue={{ value: "", label: "No referral" }}
onChange={(value) => setReferralAgent(value?.value)}
styles={{
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,
}),
}}
/>
</div>
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Subscription Duration *
</label>
<Select
className="placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none disabled:cursor-not-allowed"
options={Object.keys(availableDurations).map((value) => ({
value,
label:
availableDurations[value as keyof typeof availableDurations]
.label,
}))}
defaultValue={{
value: "1_month",
label: availableDurations["1_month"].label,
}}
onChange={(value) =>
setSubscriptionDuration(
value
? availableDurations[
value.value as keyof typeof availableDurations
].number
: 1,
)
}
styles={{
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,
}),
}}
/>
</div>
</div>
<div className="flex w-full flex-col items-start gap-4">
{renderCheckbox()}
</div>
<Button
className="w-full lg:mt-8"
color="purple"
disabled={
isLoading ||
!email ||
!name ||
!password ||
!confirmPassword ||
password !== confirmPassword ||
!companyName ||
companyUsers <= 0
}
>
Create account
</Button>
</form>
);
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Subscription Duration *
</label>
<Select
className="placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none disabled:cursor-not-allowed"
options={Object.keys(availableDurations).map((value) => ({
value,
label:
availableDurations[value as keyof typeof availableDurations]
.label,
}))}
defaultValue={{
value: "1_month",
label: availableDurations["1_month"].label,
}}
onChange={(value) =>
setSubscriptionDuration(
value
? availableDurations[
value.value as keyof typeof availableDurations
].number
: 1,
)
}
styles={{
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,
}),
}}
/>
</div>
</div>
<div className="flex w-full flex-col items-start gap-4">
{renderCheckbox()}
</div>
<Button
className="w-full lg:mt-8"
color="purple"
disabled={
isLoading ||
!email ||
!name ||
!password ||
!confirmPassword ||
password !== confirmPassword ||
!companyName ||
companyUsers <= 0
}
>
Create account
</Button>
</form>
);
}

View File

@@ -3,15 +3,15 @@ import Layout from "@/components/High/Layout";
import useGroups from "@/hooks/useGroups";
import usePackages from "@/hooks/usePackages";
import useUsers from "@/hooks/useUsers";
import {User} from "@/interfaces/user";
import { User } from "@/interfaces/user";
import clsx from "clsx";
import {capitalize} from "lodash";
import {useEffect, useState} from "react";
import { capitalize } from "lodash";
import { useEffect, useState } from "react";
import useInvites from "@/hooks/useInvites";
import {BsArrowRepeat} from "react-icons/bs";
import { BsArrowRepeat } from "react-icons/bs";
import InviteCard from "@/components/Medium/InviteCard";
import {useRouter} from "next/router";
import {ToastContainer} from "react-toastify";
import { useRouter } from "next/router";
import { ToastContainer } from "react-toastify";
import useDiscounts from "@/hooks/useDiscounts";
import PaymobPayment from "@/components/PaymobPayment";
import moment from "moment";
@@ -22,17 +22,17 @@ interface Props {
reload: () => void;
}
export default function PaymentDue({user, hasExpired = false, reload}: Props) {
export default function PaymentDue({ user, hasExpired = false, reload }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [appliedDiscount, setAppliedDiscount] = useState(0);
const router = useRouter();
const {packages} = usePackages();
const {discounts} = useDiscounts();
const {users} = useUsers();
const {groups} = useGroups({});
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
const { packages } = usePackages();
const { discounts } = useDiscounts();
const { users } = useUsers();
const { groups } = useGroups({});
const { invites, isLoading: isInvitesLoading, reload: reloadInvites } = useInvites({ to: user?.id });
useEffect(() => {
const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`));
@@ -172,7 +172,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
<div className="mb-2 flex flex-col items-start">
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
<span className="text-xl font-semibold">
EnCoach - {user.corporateInformation?.monthlyDuration} Months
EnCoach - {12} Months
</span>
</div>
<div className="flex w-full flex-col items-start gap-2">
@@ -184,7 +184,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
setIsPaymentLoading={setIsLoading}
currency={user.corporateInformation.payment.currency}
price={user.corporateInformation.payment.value}
duration={user.corporateInformation.monthlyDuration}
duration={12}
duration_unit="months"
onSuccess={() => {
setIsLoading(false);
@@ -196,8 +196,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
<span>This includes:</span>
<ul className="flex flex-col items-start text-sm">
<li>
- Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers
to use EnCoach
- Allow a total of 0 students and teachers to use EnCoach
</li>
<li>- Train their abilities for the IELTS exam</li>
<li>- Gain insights into your students&apos; weaknesses and strengths</li>

View File

@@ -13,23 +13,23 @@ import { getStudentGroupsForUsersWithoutAdmin } from "@/utils/groups.be";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import { getUserName } from "@/utils/users";
interface GroupScoreSummaryHelper {
score: [number, number];
label: string;
sessions: string[];
score: [number, number];
label: string;
sessions: string[];
}
interface AssignmentData {
id: string;
assigner: string;
assignees: string[];
results: any;
exams: { module: Module }[];
startDate: string;
excel: {
path: string;
version: string;
};
name: string;
id: string;
assigner: string;
assignees: string[];
results: any;
exams: { module: Module }[];
startDate: string;
excel: {
path: string;
version: string;
};
name: string;
}
const db = client.db(process.env.MONGODB_DB);
@@ -37,427 +37,427 @@ const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
// if (req.method === "GET") return get(req, res);
if (req.method === "POST") return await post(req, res);
// if (req.method === "GET") return get(req, res);
if (req.method === "POST") return await post(req, res);
}
function logWorksheetData(worksheet: any) {
worksheet.eachRow((row: any, rowNumber: number) => {
console.log(`Row ${rowNumber}:`);
row.eachCell((cell: any, colNumber: number) => {
console.log(` Cell ${colNumber}: ${cell.value}`);
});
});
worksheet.eachRow((row: any, rowNumber: number) => {
console.log(`Row ${rowNumber}:`);
row.eachCell((cell: any, colNumber: number) => {
console.log(` Cell ${colNumber}: ${cell.value}`);
});
});
}
function commonExcel({
data,
userName,
users,
sectionName,
customTable,
customTableHeaders,
renderCustomTableData,
data,
userName,
users,
sectionName,
customTable,
customTableHeaders,
renderCustomTableData,
}: {
data: AssignmentData;
userName: string;
users: User[];
sectionName: string;
customTable: string[][];
customTableHeaders: string[];
renderCustomTableData: (data: any) => string[];
data: AssignmentData;
userName: string;
users: User[];
sectionName: string;
customTable: string[][];
customTableHeaders: string[];
renderCustomTableData: (data: any) => string[];
}) {
const allStats = data.results.flatMap((r: any) => r.stats);
const allStats = data.results.flatMap((r: any) => r.stats);
const uniqueExercises = [...new Set(allStats.map((s: any) => s.exercise))];
const uniqueExercises = [...new Set(allStats.map((s: any) => s.exercise))];
const assigneesData = data.assignees
.map((assignee: string) => {
const userStats = allStats.filter((s: any) => s.user === assignee);
const dates = userStats.map((s: any) => moment(s.date));
const user = users.find((u) => u.id === assignee);
return {
userId: assignee,
// added some default values in case the user is not found
// could it be possible to have an assigned user deleted from the database?
user: user || {
name: "Unknown",
email: "Unknown",
demographicInformation: { passportId: "Unknown", gender: "Unknown" },
},
...userStats.reduce(
(acc: any, curr: any) => {
return {
...acc,
correct: acc.correct + curr.score.correct,
missing: acc.missing + curr.score.missing,
total: acc.total + curr.score.total,
};
},
{ correct: 0, missing: 0, total: 0 }
),
firstDate: moment.min(...dates),
lastDate: moment.max(...dates),
stats: userStats,
};
})
.sort((a, b) => b.correct - a.correct);
const assigneesData = data.assignees
.map((assignee: string) => {
const userStats = allStats.filter((s: any) => s.user === assignee);
const dates = userStats.map((s: any) => moment(s.date));
const user = users.find((u) => u.id === assignee);
return {
userId: assignee,
// added some default values in case the user is not found
// could it be possible to have an assigned user deleted from the database?
user: user || {
name: "Unknown",
email: "Unknown",
demographicInformation: { passportId: "Unknown", gender: "Unknown" },
},
...userStats.reduce(
(acc: any, curr: any) => {
return {
...acc,
correct: acc.correct + curr.score.correct,
missing: acc.missing + curr.score.missing,
total: acc.total + curr.score.total,
};
},
{ correct: 0, missing: 0, total: 0 }
),
firstDate: moment.min(...dates),
lastDate: moment.max(...dates),
stats: userStats,
};
})
.sort((a, b) => b.correct - a.correct);
const results = assigneesData.map((r: any) => r.correct);
const highestScore = Math.max(...results);
const lowestScore = Math.min(...results);
const averageScore = results.reduce((a, b) => a + b, 0) / results.length;
const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate));
const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate));
const results = assigneesData.map((r: any) => r.correct);
const highestScore = Math.max(...results);
const lowestScore = Math.min(...results);
const averageScore = results.reduce((a, b) => a + b, 0) / results.length;
const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate));
const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate));
const firstSectionData = [
{
label: sectionName,
value: userName,
},
{
label: "Report Download date :",
value: moment().format("DD/MM/YYYY"),
},
{ label: "Test Information :", value: data.name },
{
label: "Date of Test :",
value: moment(data.startDate).format("DD/MM/YYYY"),
},
{ label: "Number of Candidates :", value: data.assignees.length },
{ label: "Highest score :", value: highestScore },
{ label: "Lowest score :", value: lowestScore },
{ label: "Average score :", value: averageScore },
{ label: "", value: "" },
{
label: "Date and time of First submission :",
value: firstDate.format("DD/MM/YYYY"),
},
{
label: "Date and time of Last submission :",
value: lastDate.format("DD/MM/YYYY"),
},
];
const firstSectionData = [
{
label: sectionName,
value: userName,
},
{
label: "Report Download date :",
value: moment().format("DD/MM/YYYY"),
},
{ label: "Test Information :", value: data.name },
{
label: "Date of Test :",
value: moment(data.startDate).format("DD/MM/YYYY"),
},
{ label: "Number of Candidates :", value: data.assignees.length },
{ label: "Highest score :", value: highestScore },
{ label: "Lowest score :", value: lowestScore },
{ label: "Average score :", value: averageScore },
{ label: "", value: "" },
{
label: "Date and time of First submission :",
value: firstDate.format("DD/MM/YYYY"),
},
{
label: "Date and time of Last submission :",
value: lastDate.format("DD/MM/YYYY"),
},
];
// Create a new workbook and add a worksheet
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Report Data");
// Create a new workbook and add a worksheet
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Report Data");
// Populate the worksheet with the data
firstSectionData.forEach(({ label, value }, index) => {
worksheet.getCell(`A${index + 1}`).value = label; // First column (labels)
worksheet.getCell(`B${index + 1}`).value = value; // Second column (values)
});
// Populate the worksheet with the data
firstSectionData.forEach(({ label, value }, index) => {
worksheet.getCell(`A${index + 1}`).value = label; // First column (labels)
worksheet.getCell(`B${index + 1}`).value = value; // Second column (values)
});
// added empty arrays to force row spacings
const customTableAndLine = [[], ...customTable, []];
customTableAndLine.forEach((row: string[], index) => {
worksheet.addRow(row);
});
// added empty arrays to force row spacings
const customTableAndLine = [[], ...customTable, []];
customTableAndLine.forEach((row: string[], index) => {
worksheet.addRow(row);
});
// Define the static part of the headers (before "Test Sections")
const staticHeaders = [
"Sr N",
"Candidate ID",
"First and Last Name",
"Passport/ID",
"Email ID",
"Gender",
...customTableHeaders,
];
// Define the static part of the headers (before "Test Sections")
const staticHeaders = [
"Sr N",
"Candidate ID",
"First and Last Name",
"Passport/ID",
"Email ID",
"Gender",
...customTableHeaders,
];
// Define additional headers after "Test Sections"
const additionalHeaders = ["Time Spent", "Date", "Score"];
// Define additional headers after "Test Sections"
const additionalHeaders = ["Time Spent", "Date", "Score"];
// Calculate the dynamic columns based on the testSectionsArray
const testSectionHeaders = uniqueExercises.map(
(section, index) => `Part ${index + 1}`
);
// Calculate the dynamic columns based on the testSectionsArray
const testSectionHeaders = uniqueExercises.map(
(section, index) => `Part ${index + 1}`
);
const tableColumnHeadersFirstPart = [
...staticHeaders,
...uniqueExercises.map((a) => "Test Sections"),
];
// Add the main header row, merging static columns and "Test Sections"
const tableColumnHeaders = [
...tableColumnHeadersFirstPart,
...additionalHeaders,
];
worksheet.addRow(tableColumnHeaders);
const tableColumnHeadersFirstPart = [
...staticHeaders,
...uniqueExercises.map((a) => "Test Sections"),
];
// Add the main header row, merging static columns and "Test Sections"
const tableColumnHeaders = [
...tableColumnHeadersFirstPart,
...additionalHeaders,
];
worksheet.addRow(tableColumnHeaders);
// 1 headers rows
const startIndexTable =
firstSectionData.length + customTableAndLine.length + 1;
// 1 headers rows
const startIndexTable =
firstSectionData.length + customTableAndLine.length + 1;
// // Merge "Test Sections" over dynamic number of columns
// const tableColumns = staticHeaders.length + numberOfTestSections;
// // Merge "Test Sections" over dynamic number of columns
// const tableColumns = staticHeaders.length + numberOfTestSections;
// K10:M12 = 10,11,12,13
// horizontally group Test Sections
// K10:M12 = 10,11,12,13
// horizontally group Test Sections
// if there are test section headers to even merge:
if (testSectionHeaders.length > 1) {
worksheet.mergeCells(
startIndexTable,
staticHeaders.length + 1,
startIndexTable,
tableColumnHeadersFirstPart.length
);
}
// if there are test section headers to even merge:
if (testSectionHeaders.length > 1) {
worksheet.mergeCells(
startIndexTable,
staticHeaders.length + 1,
startIndexTable,
tableColumnHeadersFirstPart.length
);
}
// Add the dynamic second and third header rows for test sections and sub-columns
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...testSectionHeaders,
"",
"",
"",
]);
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(() => "Grammar & Vocabulary"),
"",
"",
"",
]);
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(
(exercise) => allStats.find((s: any) => s.exercise === exercise).type
),
"",
"",
"",
]);
// Add the dynamic second and third header rows for test sections and sub-columns
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...testSectionHeaders,
"",
"",
"",
]);
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(() => "Grammar & Vocabulary"),
"",
"",
"",
]);
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(
(exercise) => allStats.find((s: any) => s.exercise === exercise).type
),
"",
"",
"",
]);
// vertically group based on the part, exercise and type
staticHeaders.forEach((header, index) => {
worksheet.mergeCells(
startIndexTable,
index + 1,
startIndexTable + 3,
index + 1
);
});
// vertically group based on the part, exercise and type
staticHeaders.forEach((header, index) => {
worksheet.mergeCells(
startIndexTable,
index + 1,
startIndexTable + 3,
index + 1
);
});
assigneesData.forEach((data, index) => {
worksheet.addRow([
index + 1,
data.userId,
data.user.name,
data.user.demographicInformation?.passportId,
data.user.email,
data.user.demographicInformation?.gender,
...renderCustomTableData(data),
...uniqueExercises.map((exercise) => {
const score = data.stats.find(
(s: any) => s.exercise === exercise && s.user === data.userId
).score;
return `${score.correct}/${score.total}`;
}),
`${Math.ceil(
data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) /
60
)} minutes`,
data.lastDate.format("DD/MM/YYYY HH:mm"),
data.correct,
]);
});
assigneesData.forEach((data, index) => {
worksheet.addRow([
index + 1,
data.userId,
data.user.name,
data.user.demographicInformation?.passportId,
data.user.email,
data.user.demographicInformation?.gender,
...renderCustomTableData(data),
...uniqueExercises.map((exercise) => {
const score = data.stats.find(
(s: any) => s.exercise === exercise && s.user === data.userId
).score;
return `${score.correct}/${score.total}`;
}),
`${Math.ceil(
data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) /
60
)} minutes`,
data.lastDate.format("DD/MM/YYYY HH:mm"),
data.correct,
]);
});
worksheet.addRow([""]);
worksheet.addRow([""]);
worksheet.addRow([""]);
worksheet.addRow([""]);
for (let i = 0; i < tableColumnHeaders.length; i++) {
worksheet.getColumn(i + 1).width = 30;
}
for (let i = 0; i < tableColumnHeaders.length; i++) {
worksheet.getColumn(i + 1).width = 30;
}
// Apply styles to the headers
[startIndexTable].forEach((rowNumber) => {
worksheet.getRow(rowNumber).eachCell((cell) => {
if (cell.value) {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFBFBFBF" }, // Grey color for headers
};
cell.font = { bold: true };
cell.alignment = { vertical: "middle", horizontal: "center" };
}
});
});
// Apply styles to the headers
[startIndexTable].forEach((rowNumber) => {
worksheet.getRow(rowNumber).eachCell((cell) => {
if (cell.value) {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFBFBFBF" }, // Grey color for headers
};
cell.font = { bold: true };
cell.alignment = { vertical: "middle", horizontal: "center" };
}
});
});
worksheet.addRow(["Printed by: Confidential Information"]);
worksheet.addRow(["info@encoach.com"]);
worksheet.addRow(["Printed by: Confidential Information"]);
worksheet.addRow(["info@encoach.com"]);
// Convert workbook to Buffer (Node.js) or Blob (Browser)
return workbook.xlsx.writeBuffer();
// Convert workbook to Buffer (Node.js) or Blob (Browser)
return workbook.xlsx.writeBuffer();
}
function corporateAssignment(
user: CorporateUser,
data: AssignmentData,
users: User[]
user: CorporateUser,
data: AssignmentData,
users: User[]
) {
return commonExcel({
data,
userName: user.corporateInformation?.companyInformation?.name || "",
users,
sectionName: "Corporate Name :",
customTable: [],
customTableHeaders: [],
renderCustomTableData: () => [],
});
return commonExcel({
data,
userName: user.name || "",
users,
sectionName: "Corporate Name :",
customTable: [],
customTableHeaders: [],
renderCustomTableData: () => [],
});
}
async function mastercorporateAssignment(
user: MasterCorporateUser,
data: AssignmentData,
users: User[]
user: MasterCorporateUser,
data: AssignmentData,
users: User[]
) {
const userGroups = await getStudentGroupsForUsersWithoutAdmin(
user.id,
data.assignees
);
const adminUsers = [...new Set(userGroups.map((g) => g.admin))];
const userGroups = await getStudentGroupsForUsersWithoutAdmin(
user.id,
data.assignees
);
const adminUsers = [...new Set(userGroups.map((g) => g.admin))];
const userGroupsParticipants = userGroups.flatMap((g) => g.participants);
const adminsData = await getSpecificUsers(adminUsers);
const companiesData = adminsData.map((user) => {
const name = getUserName(user);
const users = userGroupsParticipants.filter((p) =>
data.assignees.includes(p)
);
const userGroupsParticipants = userGroups.flatMap((g) => g.participants);
const adminsData = await getSpecificUsers(adminUsers);
const companiesData = adminsData.map((user) => {
const name = getUserName(user);
const users = userGroupsParticipants.filter((p) =>
data.assignees.includes(p)
);
const stats = data.results
.flatMap((r: any) => r.stats)
.filter((s: any) => users.includes(s.user));
const correct = stats.reduce(
(acc: number, s: any) => acc + s.score.correct,
0
);
const total = stats.reduce(
(acc: number, curr: any) => acc + curr.score.total,
0
);
const stats = data.results
.flatMap((r: any) => r.stats)
.filter((s: any) => users.includes(s.user));
const correct = stats.reduce(
(acc: number, s: any) => acc + s.score.correct,
0
);
const total = stats.reduce(
(acc: number, curr: any) => acc + curr.score.total,
0
);
return {
name,
correct,
total,
};
});
return {
name,
correct,
total,
};
});
const customTable = [
...companiesData,
{
name: "Total",
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
},
].map((c) => [c.name, `${c.correct}/${c.total}`]);
const customTable = [
...companiesData,
{
name: "Total",
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
},
].map((c) => [c.name, `${c.correct}/${c.total}`]);
const customTableHeaders = [
{ name: "Corporate", helper: (data: any) => data.user.corporateName },
];
return commonExcel({
data,
userName: user.corporateInformation?.companyInformation?.name || "",
users: users.map((u) => {
const userGroup = userGroups.find((g) => g.participants.includes(u.id));
const admin = adminsData.find((a) => a.id === userGroup?.admin);
return {
...u,
corporateName: getUserName(admin),
};
}),
sectionName: "Master Corporate Name :",
customTable: [["Corporate Summary"], ...customTable],
customTableHeaders: customTableHeaders.map((h) => h.name),
renderCustomTableData: (data) =>
customTableHeaders.map((h) => h.helper(data)),
});
const customTableHeaders = [
{ name: "Corporate", helper: (data: any) => data.user.corporateName },
];
return commonExcel({
data,
userName: user.name || "",
users: users.map((u) => {
const userGroup = userGroups.find((g) => g.participants.includes(u.id));
const admin = adminsData.find((a) => a.id === userGroup?.admin);
return {
...u,
corporateName: getUserName(admin),
};
}),
sectionName: "Master Corporate Name :",
customTable: [["Corporate Summary"], ...customTable],
customTableHeaders: customTableHeaders.map((h) => h.name),
renderCustomTableData: (data) =>
customTableHeaders.map((h) => h.helper(data)),
});
}
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export
if (req.session.user) {
const { id } = req.query as { id: string };
// verify if it's a logged user that is trying to export
if (req.session.user) {
const { id } = req.query as { id: string };
const assignment = await db.collection("assignments").findOne<AssignmentData>({ id: id });
if (!assignment) {
res.status(400).end();
return;
}
const assignment = await db.collection("assignments").findOne<AssignmentData>({ id: id });
if (!assignment) {
res.status(400).end();
return;
}
// if (
// data.excel &&
// data.excel.path &&
// data.excel.version === process.env.EXCEL_VERSION
// ) {
// // if it does, return the excel url
// const fileRef = ref(storage, data.excel.path);
// const url = await getDownloadURL(fileRef);
// res.status(200).end(url);
// return;
// }
// if (
// data.excel &&
// data.excel.path &&
// data.excel.version === process.env.EXCEL_VERSION
// ) {
// // if it does, return the excel url
// const fileRef = ref(storage, data.excel.path);
// const url = await getDownloadURL(fileRef);
// res.status(200).end(url);
// return;
// }
const objectIds = assignment.assignees.map(id => id);
const objectIds = assignment.assignees.map(id => id);
const users = await db.collection("users").find<User>({
id: { $in: objectIds }
}).toArray();
const users = await db.collection("users").find<User>({
id: { $in: objectIds }
}).toArray();
const user = await db.collection("users").findOne<User>({ id: assignment.assigner });
const user = await db.collection("users").findOne<User>({ id: assignment.assigner });
// we'll need the user in order to get the user data (name, email, focus, etc);
if (user && users) {
// generate the file ref for storage
const fileName = `${Date.now().toString()}.xlsx`;
const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName);
// we'll need the user in order to get the user data (name, email, focus, etc);
if (user && users) {
// generate the file ref for storage
const fileName = `${Date.now().toString()}.xlsx`;
const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName);
const getExcelFn = () => {
switch (user.type) {
case "teacher":
case "corporate":
return corporateAssignment(user as CorporateUser, assignment, users);
case "mastercorporate":
return mastercorporateAssignment(
user as MasterCorporateUser,
assignment,
users
);
default:
throw new Error("Invalid user type");
}
};
const buffer = await getExcelFn();
const getExcelFn = () => {
switch (user.type) {
case "teacher":
case "corporate":
return corporateAssignment(user as CorporateUser, assignment, users);
case "mastercorporate":
return mastercorporateAssignment(
user as MasterCorporateUser,
assignment,
users
);
default:
throw new Error("Invalid user type");
}
};
const buffer = await getExcelFn();
// upload the pdf to storage
await uploadBytes(fileRef, buffer, {
contentType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
// upload the pdf to storage
await uploadBytes(fileRef, buffer, {
contentType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
// update the stats entries with the pdf url to prevent duplication
await db.collection("assignments").updateOne(
{ id: assignment.id },
{
$set: {
excel: {
path: refName,
version: process.env.EXCEL_VERSION,
}
}
}
);
// update the stats entries with the pdf url to prevent duplication
await db.collection("assignments").updateOne(
{ id: assignment.id },
{
$set: {
excel: {
path: refName,
version: process.env.EXCEL_VERSION,
}
}
}
);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
}
return;
}
}
res.status(401).json({ message: "Unauthorized" });
res.status(401).json({ message: "Unauthorized" });
}

View File

@@ -310,19 +310,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (groups.length > 0) {
const admins = await db.collection("users")
.find<CorporateUser>({ id: { $in: groups.map(g => g.admin).map(id => id)} })
.find<CorporateUser>({ id: { $in: groups.map(g => g.admin).map(id => id) } })
.toArray();
const adminData = admins.find((a) => a.corporateInformation?.companyInformation?.name);
const adminData = admins.find((a) => a.name);
if (adminData) {
return adminData.corporateInformation.companyInformation.name;
return adminData.name;
}
}
}
if (assignerUser.type === "corporate" && assignerUser.corporateInformation?.companyInformation?.name) {
return assignerUser.corporateInformation.companyInformation.name;
}
return assignerUser.type
}
} catch (err) {
console.error(err);
@@ -373,14 +371,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
await db.collection("assignments").updateOne(
{ id: data.id },
{
$set: {
pdf: {
path: refName,
version: process.env.PDF_VERSION,
$set: {
pdf: {
path: refName,
version: process.env.PDF_VERSION,
}
}
}
}
);
);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);

View File

@@ -62,7 +62,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (req.session.user.type === "corporate") {
const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length;
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
const allowedCodes = 0;
if (totalCodes > allowedCodes) {
res.status(403).json({
@@ -127,7 +127,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
// upsert: true -> if it doesnt exist insert
await db.collection("codes").updateOne(
{ id: code },
{ $set: { id: code, ...codeInformation} },
{ $set: { id: code, ...codeInformation } },
{ upsert: true }
);
}

View File

@@ -1,13 +1,14 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {deleteEntity, getEntity, getEntityWithRoles} from "@/utils/entities.be";
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { deleteEntity, getEntity, getEntityWithRoles } from "@/utils/entities.be";
import client from "@/lib/mongodb";
import {Entity} from "@/interfaces/entity";
import { Entity } from "@/interfaces/entity";
import { doesEntityAllow } from "@/utils/permissions";
import { getUser } from "@/utils/users.be";
import { requestUser } from "@/utils/api";
import { isAdmin } from "@/utils/users";
const db = client.db(process.env.MONGODB_DB);
@@ -23,7 +24,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id, showRoles} = req.query as {id: string; showRoles: string};
const { id, showRoles } = req.query as { id: string; showRoles: string };
const entity = await (!!showRoles ? getEntityWithRoles : getEntity)(id);
res.status(200).json(entity);
@@ -39,23 +40,31 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
if (!entity) return res.status(404).json({ ok: false })
if (!doesEntityAllow(user, entity, "delete_entity") && !["admin", "developer"].includes(user.type))
return res.status(403).json({ok: false})
return res.status(403).json({ ok: false })
await deleteEntity(entity)
return res.status(200).json({ok: true});
return res.status(200).json({ ok: true });
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const { id } = req.query as { id: string };
if (!user.entities.map((x) => x.id).includes(id)) {
return res.status(403).json({ok: false});
if (!user.entities.map((x) => x.id).includes(id) && !isAdmin(user)) {
return res.status(403).json({ ok: false });
}
const entity = await db.collection<Entity>("entities").updateOne({id}, {$set: {label: req.body.label}});
if (req.body.label) {
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { label: req.body.label } });
return res.status(200).json({ ok: entity.acknowledged });
}
return res.status(200).json({ok: entity.acknowledged});
if (req.body.licenses) {
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { licenses: req.body.licenses } });
return res.status(200).json({ ok: entity.acknowledged });
}
return res.status(200).json({ ok: true });
}

View File

@@ -1,10 +1,10 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {addUsersToEntity, addUserToEntity, createEntity, getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
import {Entity} from "@/interfaces/entity";
import {v4} from "uuid";
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { addUsersToEntity, addUserToEntity, createEntity, getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
import { Entity } from "@/interfaces/entity";
import { v4 } from "uuid";
import { requestUser } from "@/utils/api";
export default withIronSessionApiRoute(handler, sessionOptions);
@@ -18,7 +18,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {showRoles} = req.query as {showRoles: string};
const { showRoles } = req.query as { showRoles: string };
const getFn = showRoles ? getEntitiesWithRoles : getEntities;
@@ -31,12 +31,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer"].includes(user.type)) {
return res.status(403).json({ok: false});
return res.status(403).json({ ok: false });
}
const entity: Entity = {
id: v4(),
label: req.body.label,
licenses: req.body.licenses
};
const members = req.body.members as string[] | undefined || []

View File

@@ -1,6 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import client from "@/lib/mongodb";
import type {NextApiRequest, NextApiResponse} from "next";
import type { NextApiRequest, NextApiResponse } from "next";
const db = client.db(process.env.MONGODB_DB);
@@ -9,8 +9,5 @@ type Data = {
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
// await db.collection("users").updateMany({}, {$set: {entities: []}});
await db.collection("invites").deleteMany({});
res.status(200).json({name: "John Doe"});
res.status(200).json({ name: "John Doe" });
}

View File

@@ -14,17 +14,17 @@ import { getEntityWithRoles } from "@/utils/entities.be";
import { findBy } from "@/utils";
const DEFAULT_DESIRED_LEVELS = {
reading: 9,
listening: 9,
writing: 9,
speaking: 9,
reading: 9,
listening: 9,
writing: 9,
speaking: 9,
};
const DEFAULT_LEVELS = {
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
};
const auth = getAuth(app);
@@ -33,99 +33,94 @@ const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
if (req.method === "POST") return post(req, res);
return res.status(404).json({ ok: false });
return res.status(404).json({ ok: false });
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const maker = req.session.user;
if (!maker) {
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
}
const maker = req.session.user;
if (!maker) {
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
}
const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as {
email: string;
password?: string;
passport_id: string;
type: string;
entity: string;
groupID?: string;
corporate?: string;
expiryDate: null | Date;
};
const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as {
email: string;
password?: string;
passport_id: string;
type: string;
entity: string;
groupID?: string;
corporate?: string;
expiryDate: null | Date;
};
// cleaning data
delete req.body.passport_id;
delete req.body.groupID;
delete req.body.expiryDate;
delete req.body.password;
delete req.body.corporate;
delete req.body.entity
// cleaning data
delete req.body.passport_id;
delete req.body.groupID;
delete req.body.expiryDate;
delete req.body.password;
delete req.body.corporate;
delete req.body.entity
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
const entityWithRole = await getEntityWithRoles(entity)
const defaultRole = findBy(entityWithRole?.roles || [], "isDefault", true)
const entityWithRole = await getEntityWithRoles(entity)
const defaultRole = findBy(entityWithRole?.roles || [], "isDefault", true)
const user = {
...req.body,
bio: "",
id: userId,
type: type,
focus: "academic",
status: "active",
desiredLevels: DEFAULT_DESIRED_LEVELS,
profilePicture: "/defaultAvatar.png",
levels: DEFAULT_LEVELS,
isFirstLogin: false,
isVerified: true,
registrationDate: new Date(),
entities: [{ id: entity, role: defaultRole?.id || "" }],
subscriptionExpirationDate: expiryDate || null,
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
? {
corporateInformation: {
companyInformation: {
name: maker.corporateInformation?.companyInformation?.name || "N/A",
userAmount: 0,
},
},
}
: {}),
};
const user = {
...req.body,
bio: "",
id: userId,
type: type,
focus: "academic",
status: "active",
desiredLevels: DEFAULT_DESIRED_LEVELS,
profilePicture: "/defaultAvatar.png",
levels: DEFAULT_LEVELS,
isFirstLogin: false,
isVerified: true,
registrationDate: new Date(),
entities: [{ id: entity, role: defaultRole?.id || "" }],
subscriptionExpirationDate: expiryDate || null,
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
? {
corporateInformation: {},
}
: {}),
};
const uid = new ShortUniqueId();
const code = uid.randomUUID(6);
const uid = new ShortUniqueId();
const code = uid.randomUUID(6);
await db.collection("users").insertOne(user);
await db.collection("codes").insertOne({
code,
creator: maker.id,
expiryDate,
type,
creationDate: new Date(),
userId,
email: email.toLowerCase(),
name: req.body.name,
...(!!passport_id ? { passport_id } : {}),
});
await db.collection("users").insertOne(user);
await db.collection("codes").insertOne({
code,
creator: maker.id,
expiryDate,
type,
creationDate: new Date(),
userId,
email: email.toLowerCase(),
name: req.body.name,
...(!!passport_id ? { passport_id } : {}),
});
if (!!groupID) {
const group = await getGroup(groupID);
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
}
if (!!groupID) {
const group = await getGroup(groupID);
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
}
console.log(`Returning - ${email}`);
return res.status(200).json({ ok: true });
})
.catch((error) => {
if (error.code.includes("email-already-in-use")) return res.status(403).json({ error, message: "E-mail is already in the platform." });
console.log(`Returning - ${email}`);
return res.status(200).json({ ok: true });
})
.catch((error) => {
if (error.code.includes("email-already-in-use")) return res.status(403).json({ error, message: "E-mail is already in the platform." });
console.log(`Failing - ${email}`);
console.log(error);
return res.status(401).json({ error });
});
console.log(`Failing - ${email}`);
console.log(error);
return res.status(401).json({ error });
});
}

View File

@@ -58,7 +58,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
if (admin) {
return {
...d,
corporate: admin.corporateInformation?.companyInformation?.name,
corporate: admin.name,
};
}

View File

@@ -1,7 +1,7 @@
import Layout from "@/components/High/Layout";
import Separator from "@/components/Low/Separator";
import AssignmentCard from "@/dashboards/AssignmentCard";
import AssignmentView from "@/dashboards/AssignmentView";
import AssignmentCard from "@/components/AssignmentCard";
import AssignmentView from "@/components/AssignmentView";
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination";

View File

@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout";
import UserDisplayList from "@/components/UserDisplayList";
import IconCard from "@/dashboards/IconCard";
import IconCard from "@/components/IconCard";
import { Module } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";
import { Assignment } from "@/interfaces/results";

View File

@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout";
import UserDisplayList from "@/components/UserDisplayList";
import IconCard from "@/dashboards/IconCard";
import IconCard from "@/components/IconCard";
import { Module } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";
import { Assignment } from "@/interfaces/results";
@@ -25,183 +25,183 @@ import Head from "next/head";
import { useRouter } from "next/router";
import { useMemo } from "react";
import {
BsClipboard2Data,
BsClock,
BsEnvelopePaper,
BsPaperclip,
BsPencilSquare,
BsPeople,
BsPeopleFill,
BsPersonFill,
BsPersonFillGear,
BsClipboard2Data,
BsClock,
BsEnvelopePaper,
BsPaperclip,
BsPencilSquare,
BsPeople,
BsPeopleFill,
BsPersonFill,
BsPersonFillGear,
} from "react-icons/bs";
import { ToastContainer } from "react-toastify";
interface Props {
user: User;
users: User[];
entities: EntityWithRoles[];
assignments: Assignment[];
stats: Stat[];
groups: Group[];
user: User;
users: User[];
entities: EntityWithRoles[];
assignments: Assignment[];
stats: Stat[];
groups: Group[];
}
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
if (!checkAccess(user, ["admin", "developer", "corporate"])) return redirect("/")
if (!checkAccess(user, ["admin", "developer", "corporate"])) return redirect("/")
const entityIDS = mapBy(user.entities, "id") || [];
const entities = await getEntitiesWithRoles(entityIDS);
const users = await filterAllowedUsers(user, entities)
const entityIDS = mapBy(user.entities, "id") || [];
const entities = await getEntitiesWithRoles(entityIDS);
const users = await filterAllowedUsers(user, entities)
const assignments = await getEntitiesAssignments(entityIDS);
const stats = await getStatsByUsers(users.map((u) => u.id));
const groups = await getGroupsByEntities(entityIDS);
const assignments = await getEntitiesAssignments(entityIDS);
const stats = await getStatsByUsers(users.map((u) => u.id));
const groups = await getGroupsByEntities(entityIDS);
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
}, sessionOptions);
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
const router = useRouter();
const router = useRouter();
const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: students.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 averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: students.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));
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);
};
return calculateAverageLevel(levels);
};
const UserDisplay = (displayUser: User) => (
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const UserDisplay = (displayUser: User) => (
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
<Layout user={user}>
<div className="w-full flex flex-col gap-4">
{entities.length > 0 && (
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
<b>{mapBy(entities, "label")?.join(", ")}</b>
</div>
)}
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
<IconCard
onClick={() => router.push("/users?type=student")}
Icon={BsPersonFill}
label="Students"
value={students.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/users?type=teacher")}
Icon={BsPencilSquare}
label="Teachers"
value={teachers.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/classrooms")}
Icon={BsPeople}
label="Classrooms"
value={groups.length}
color="purple"
/>
<IconCard Icon={BsPeopleFill}
onClick={() => router.push("/entities")}
label="Entities"
value={entities.length}
color="purple"
/>
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
<IconCard Icon={BsPersonFillGear}
onClick={() => router.push("/users/performance")}
label="Student Performance"
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
<Layout user={user}>
<div className="w-full flex flex-col gap-4">
{entities.length > 0 && (
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
<b>{mapBy(entities, "label")?.join(", ")}</b>
</div>
)}
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
<IconCard
onClick={() => router.push("/users?type=student")}
Icon={BsPersonFill}
label="Students"
value={students.length}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
Icon={BsEnvelopePaper}
className="col-span-2"
onClick={() => router.push("/assignments")}
label="Assignments"
value={assignments.filter((a) => !a.archived).length}
color="purple"
/>
</section>
</div>
<IconCard
onClick={() => router.push("/users?type=teacher")}
Icon={BsPencilSquare}
label="Teachers"
value={teachers.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/classrooms")}
Icon={BsPeople}
label="Classrooms"
value={groups.length}
color="purple"
/>
<IconCard Icon={BsPeopleFill}
onClick={() => router.push("/entities")}
label="Entities"
value={entities.length}
color="purple"
/>
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
<IconCard Icon={BsPersonFillGear}
onClick={() => router.push("/users/performance")}
label="Student Performance"
value={students.length}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
Icon={BsEnvelopePaper}
className="col-span-2"
onClick={() => router.push("/assignments")}
label="Assignments"
value={assignments.filter((a) => !a.archived).length}
color="purple"
/>
</section>
</div>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<UserDisplayList
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
title="Latest Students"
/>
<UserDisplayList
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
title="Latest Teachers"
/>
<UserDisplayList
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
title="Highest level students"
/>
<UserDisplayList
users={
students
.sort(
(a, b) =>
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
)
}
title="Highest exam count students"
/>
</section>
</Layout>
</>
);
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<UserDisplayList
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
title="Latest Students"
/>
<UserDisplayList
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
title="Latest Teachers"
/>
<UserDisplayList
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
title="Highest level students"
/>
<UserDisplayList
users={
students
.sort(
(a, b) =>
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
)
}
title="Highest exam count students"
/>
</section>
</Layout>
</>
);
}

View File

@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout";
import UserDisplayList from "@/components/UserDisplayList";
import IconCard from "@/dashboards/IconCard";
import IconCard from "@/components/IconCard";
import { Module } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";
import { Assignment } from "@/interfaces/results";

View File

@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout";
import UserDisplayList from "@/components/UserDisplayList";
import IconCard from "@/dashboards/IconCard";
import IconCard from "@/components/IconCard";
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
import { Module } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";

View File

@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout";
import UserDisplayList from "@/components/UserDisplayList";
import IconCard from "@/dashboards/IconCard";
import IconCard from "@/components/IconCard";
import { Module } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";
import { Assignment } from "@/interfaces/results";
@@ -28,139 +28,139 @@ import { useAllowedEntities } from "@/hooks/useEntityPermissions";
import { filterAllowedUsers } from "@/utils/users.be";
interface Props {
user: User;
users: User[];
entities: EntityWithRoles[];
assignments: Assignment[];
stats: Stat[];
groups: Group[];
user: User;
users: User[];
entities: EntityWithRoles[];
assignments: Assignment[];
stats: Stat[];
groups: Group[];
}
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
if (!checkAccess(user, ["admin", "developer", "teacher"]))
return redirect("/")
if (!checkAccess(user, ["admin", "developer", "teacher"]))
return redirect("/")
const entityIDS = mapBy(user.entities, "id") || [];
const entities = await getEntitiesWithRoles(entityIDS);
const users = await filterAllowedUsers(user, entities)
const entityIDS = mapBy(user.entities, "id") || [];
const entities = await getEntitiesWithRoles(entityIDS);
const users = await filterAllowedUsers(user, entities)
const assignments = await getEntitiesAssignments(entityIDS);
const stats = await getStatsByUsers(users.map((u) => u.id));
const groups = await getGroupsByEntities(entityIDS);
const assignments = await getEntitiesAssignments(entityIDS);
const stats = await getStatsByUsers(users.map((u) => u.id));
const groups = await getGroupsByEntities(entityIDS);
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
}, sessionOptions);
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
const router = useRouter();
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
const router = useRouter();
const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: students.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 averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: students.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));
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);
};
return calculateAverageLevel(levels);
};
const UserDisplay = (displayUser: User) => (
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const UserDisplay = (displayUser: User) => (
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
<Layout user={user}>
<div className="w-full flex flex-col gap-4">
{entities.length > 0 && (
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
<b>{mapBy(entities, "label")?.join(", ")}</b>
</div>
)}
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
<IconCard
Icon={BsPersonFill}
onClick={() => router.push("/users?type=student")}
label="Students"
value={students.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/classrooms")}
Icon={BsPeople}
label="Classrooms"
value={groups.length}
color="purple"
/>
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
<IconCard
Icon={BsEnvelopePaper}
onClick={() => router.push("/assignments")}
label="Assignments"
value={assignments.filter((a) => !a.archived).length}
color="purple"
/>
</section>
</div>
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
<Layout user={user}>
<div className="w-full flex flex-col gap-4">
{entities.length > 0 && (
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
<b>{mapBy(entities, "label")?.join(", ")}</b>
</div>
)}
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
<IconCard
Icon={BsPersonFill}
onClick={() => router.push("/users?type=student")}
label="Students"
value={students.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/classrooms")}
Icon={BsPeople}
label="Classrooms"
value={groups.length}
color="purple"
/>
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
<IconCard
Icon={BsEnvelopePaper}
onClick={() => router.push("/assignments")}
label="Assignments"
value={assignments.filter((a) => !a.archived).length}
color="purple"
/>
</section>
</div>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<UserDisplayList
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
title="Latest Students"
/>
<UserDisplayList
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
title="Highest level students"
/>
<UserDisplayList
users={
students
.sort(
(a, b) =>
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
)
}
title="Highest exam count students"
/>
</section>
</Layout>
</>
);
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<UserDisplayList
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
title="Latest Students"
/>
<UserDisplayList
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
title="Highest level students"
/>
<UserDisplayList
users={
students
.sort(
(a, b) =>
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
)
}
title="Highest exam count students"
/>
</section>
</Layout>
</>
);
}

View File

@@ -4,34 +4,35 @@ import Layout from "@/components/High/Layout";
import Select from "@/components/Low/Select";
import Tooltip from "@/components/Low/Tooltip";
import { useEntityPermission } from "@/hooks/useEntityPermissions";
import {useListSearch} from "@/hooks/useListSearch";
import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination";
import {Entity, EntityWithRoles, Role} from "@/interfaces/entity";
import {GroupWithUsers, User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import {USER_TYPE_LABELS} from "@/resources/user";
import { Entity, EntityWithRoles, Role } from "@/interfaces/entity";
import { GroupWithUsers, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { USER_TYPE_LABELS } from "@/resources/user";
import { findBy, mapBy, redirect, serialize } from "@/utils";
import {getEntityWithRoles} from "@/utils/entities.be";
import {convertToUsers, getGroup} from "@/utils/groups.be";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {checkAccess, doesEntityAllow, getTypesOfUser} from "@/utils/permissions";
import {getUserName, isAdmin} from "@/utils/users";
import {filterAllowedUsers, getEntitiesUsers, getEntityUsers, getLinkedUsers, getSpecificUsers, getUsers} from "@/utils/users.be";
import { getEntityWithRoles } from "@/utils/entities.be";
import { convertToUsers, getGroup } from "@/utils/groups.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { checkAccess, doesEntityAllow, getTypesOfUser } from "@/utils/permissions";
import { getUserName, isAdmin } from "@/utils/users";
import { filterAllowedUsers, getEntitiesUsers, getEntityUsers, getLinkedUsers, getSpecificUsers, getUsers } from "@/utils/users.be";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import axios from "axios";
import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next";
import { withIronSessionSsr } from "iron-session/next";
import moment from "moment";
import Head from "next/head";
import Link from "next/link";
import {useRouter} from "next/router";
import {Divider} from "primereact/divider";
import {useEffect, useMemo, useState} from "react";
import { useRouter } from "next/router";
import { Divider } from "primereact/divider";
import { useEffect, useMemo, useState } from "react";
import {
BsChevronLeft,
BsClockFill,
BsEnvelopeFill,
BsFillPersonVcardFill,
BsHash,
BsPerson,
BsPlus,
BsSquare,
@@ -40,15 +41,15 @@ import {
BsTrash,
BsX,
} from "react-icons/bs";
import {toast} from "react-toastify";
import { toast } from "react-toastify";
export const getServerSideProps = withIronSessionSsr(async ({req, params}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, params }) => {
const user = req.session.user as User;
if (!user) return redirect("/login")
if (shouldRedirectHome(user)) return redirect("/")
const {id} = params as {id: string};
const { id } = params as { id: string };
const entity = await getEntityWithRoles(id);
if (!entity) return redirect("/entities")
@@ -56,12 +57,12 @@ export const getServerSideProps = withIronSessionSsr(async ({req, params}) => {
if (!doesEntityAllow(user, entity, "view_entities")) return redirect(`/entities`)
const linkedUsers = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(user.entities, 'id'),
{$and: [{type: {$ne: "developer"}}, {type: {$ne: "admin"}}]}))
{ $and: [{ type: { $ne: "developer" } }, { type: { $ne: "admin" } }] }))
const entityUsers = await (isAdmin(user) ? getEntityUsers(id) : filterAllowedUsers(user, [entity]));
const usersWithRole = entityUsers.map((u) => {
const e = u.entities.find((e) => e.id === id);
return {...u, role: findBy(entity.roles, 'id', e?.role)};
return { ...u, role: findBy(entity.roles, 'id', e?.role) };
});
return {
@@ -74,7 +75,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, params}) => {
};
}, sessionOptions);
type UserWithRole = User & {role?: Role};
type UserWithRole = User & { role?: Role };
interface Props {
user: User;
@@ -83,7 +84,7 @@ interface Props {
linkedUsers: User[];
}
export default function Home({user, entity, users, linkedUsers}: Props) {
export default function Home({ user, entity, users, linkedUsers }: Props) {
const [isAdding, setIsAdding] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -110,7 +111,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
setIsLoading(true);
axios
.patch(`/api/entities/${entity.id}/users`, {add: false, members: selectedUsers})
.patch(`/api/entities/${entity.id}/users`, { add: false, members: selectedUsers })
.then(() => {
toast.success("The entity has been updated successfully!");
router.replace(router.asPath);
@@ -132,7 +133,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
const defaultRole = findBy(entity.roles, 'isDefault', true)!
axios
.patch(`/api/entities/${entity.id}/users`, {add: true, members: selectedUsers, role: defaultRole.id})
.patch(`/api/entities/${entity.id}/users`, { add: true, members: selectedUsers, role: defaultRole.id })
.then(() => {
toast.success("The entity has been updated successfully!");
router.replace(router.asPath);
@@ -153,7 +154,28 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
setIsLoading(true);
axios
.patch(`/api/entities/${entity.id}`, {label})
.patch(`/api/entities/${entity.id}`, { label })
.then(() => {
toast.success("The entity has been updated successfully!");
router.replace(router.asPath);
})
.catch((e) => {
console.error(e);
toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
const editLicenses = () => {
if (!isAdmin(user)) return;
const licenses = prompt("Update the number of licenses:", (entity.licenses || 0).toString());
if (!licenses) return;
if (!parseInt(licenses) || parseInt(licenses) <= 0) return toast.error("Write a valid number of licenses!")
setIsLoading(true);
axios
.patch(`/api/entities/${entity.id}`, { licenses })
.then(() => {
toast.success("The entity has been updated successfully!");
router.replace(router.asPath);
@@ -190,7 +212,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
setIsLoading(true);
axios
.post(`/api/roles/${role}/users`, {users: selectedUsers})
.post(`/api/roles/${role}/users`, { users: selectedUsers })
.then(() => {
toast.success("The role has been assigned successfully!");
router.replace(router.asPath);
@@ -274,7 +296,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
<BsChevronLeft />
</Link>
<h2 className="font-bold text-2xl">{entity.label}</h2>
<h2 className="font-bold text-2xl">{entity.label} {isAdmin(user) && `- ${entity.licenses || 0} licenses`}</h2>
</div>
</div>
<div className="flex items-center gap-2">
@@ -285,6 +307,15 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
<BsTag />
<span className="text-xs">Rename Entity</span>
</button>
{isAdmin(user) && (
<button
onClick={editLicenses}
disabled={isLoading || !isAdmin(user)}
className="flex items-center gap-1 px-2 py-2 border rounded-full hover:bg-neutral-100 disabled:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
<BsHash />
<span className="text-xs">Edit Licenses</span>
</button>
)}
<button
onClick={() => router.push(`/entities/${entity.id}/roles`)}
disabled={isLoading || !canViewRoles}
@@ -325,7 +356,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
{entity.roles.map((role) => (
<MenuItem key={role.id}>
<button onClick={() => assignUsersToRole(role.id)} className="p-4 hover:bg-neutral-100 w-32">
{ role.label }
{role.label}
</button>
</MenuItem>
))}

View File

@@ -3,32 +3,32 @@ import Layout from "@/components/High/Layout";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
import Tooltip from "@/components/Low/Tooltip";
import {useListSearch} from "@/hooks/useListSearch";
import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination";
import {Entity, EntityWithRoles} from "@/interfaces/entity";
import {User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import {USER_TYPE_LABELS} from "@/resources/user";
import {mapBy, redirect, serialize} from "@/utils";
import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {getUserName} from "@/utils/users";
import {getLinkedUsers, getUsers} from "@/utils/users.be";
import { Entity, EntityWithRoles } from "@/interfaces/entity";
import { User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { USER_TYPE_LABELS } from "@/resources/user";
import { mapBy, redirect, serialize } from "@/utils";
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getUserName } from "@/utils/users";
import { getLinkedUsers, getUsers } from "@/utils/users.be";
import axios from "axios";
import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next";
import { withIronSessionSsr } from "iron-session/next";
import moment from "moment";
import Head from "next/head";
import Link from "next/link";
import {useRouter} from "next/router";
import {Divider} from "primereact/divider";
import {useState} from "react";
import {BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill} from "react-icons/bs";
import {toast, ToastContainer} from "react-toastify";
import { useRouter } from "next/router";
import { Divider } from "primereact/divider";
import { useState } from "react";
import { BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill } from "react-icons/bs";
import { toast, ToastContainer } from "react-toastify";
import { requestUser } from "@/utils/api";
import { findAllowedEntities } from "@/utils/permissions";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
@@ -38,7 +38,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const users = await getUsers()
return {
props: serialize({user, users: users.filter((x) => x.id !== user.id)}),
props: serialize({ user, users: users.filter((x) => x.id !== user.id) }),
};
}, sessionOptions);
@@ -47,13 +47,14 @@ interface Props {
users: User[];
}
export default function Home({user, users}: Props) {
export default function Home({ user, users }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [label, setLabel] = useState("");
const [licenses, setLicenses] = useState(0);
const {rows, renderSearch} = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], users);
const {items, renderMinimal} = usePagination<User>(rows, 16);
const { rows, renderSearch } = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], users);
const { items, renderMinimal } = usePagination<User>(rows, 16);
const router = useRouter();
@@ -64,7 +65,7 @@ export default function Home({user, users}: Props) {
setIsLoading(true);
axios
.post<Entity>(`/api/entities`, {label, members: selectedUsers})
.post<Entity>(`/api/entities`, { label, licenses, members: selectedUsers })
.then((result) => {
toast.success("Your entity has been created successfully!");
router.replace(`/entities/${result.data.id}`);
@@ -104,7 +105,7 @@ export default function Home({user, users}: Props) {
<div className="flex items-center gap-4">
<button
onClick={createGroup}
disabled={!label.trim() || isLoading}
disabled={!label.trim() || licenses <= 0 || isLoading}
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
<BsCheck />
<span className="text-xs">Create Entity</span>
@@ -112,9 +113,16 @@ export default function Home({user, users}: Props) {
</div>
</div>
<Divider />
<div className="flex flex-col gap-4 w-full">
<span className="font-semibold text-xl">Entity Label:</span>
<Input name="name" onChange={setLabel} type="text" placeholder="Entity A" />
<div className="w-full grid grid-cols-2 gap-4">
<div className="flex flex-col gap-4 w-full">
<span className="font-semibold text-xl">Entity Label:</span>
<Input name="name" onChange={setLabel} type="text" placeholder="Entity A" />
</div>
<div className="flex flex-col gap-4 w-full">
<span className="font-semibold text-xl">Licenses:</span>
<Input name="licenses" min={0} onChange={(v) => setLicenses(parseInt(v))} type="number" placeholder="12" />
</div>
</div>
<Divider />
<div className="flex items-center justify-between mb-4">

View File

@@ -6,7 +6,7 @@ import { ToastContainer } from "react-toastify";
import Layout from "@/components/High/Layout";
import { GroupWithUsers, User } from "@/interfaces/user";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getUserName } from "@/utils/users";
import { getUserName, isAdmin } from "@/utils/users";
import { convertToUsers, getGroupsForUser } from "@/utils/groups.be";
import { countEntityUsers, getEntityUsers, getSpecificUsers, getUsers } from "@/utils/users.be";
import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions";
@@ -64,7 +64,7 @@ export default function Home({ user, entities }: Props) {
</span>
<span className="flex items-center gap-1">
<span className="bg-mti-purple text-white font-semibold px-2">Members</span>
<span className="bg-mti-purple-light/50 px-2">{count}</span>
<span className="bg-mti-purple-light/50 px-2">{count}{isAdmin(user) && ` / ${entity.licenses || 0}`}</span>
</span>
<span>
{users.map(getUserName).join(", ")}{' '}

View File

@@ -1,20 +1,20 @@
/* 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,15 +26,15 @@ 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";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
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";
import { requestUser } from "@/utils/api";
import { redirect } from "@/utils";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
@@ -43,18 +43,18 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
}
return {
props: {user},
props: { user },
};
}, sessionOptions);
const columnHelper = createColumnHelper<Payment>();
const paypalColumnHelper = createColumnHelper<PaypalPaymentWithUserData>();
const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () => void; reload: () => void; showComission: boolean}) => {
const PaymentCreator = ({ onClose, reload, showComission = false }: { onClose: () => void; reload: () => void; showComission: boolean }) => {
const [corporate, setCorporate] = useState<CorporateUser>();
const [date, setDate] = useState<Date>(new Date());
const {users} = useUsers();
const { users } = useUsers();
const price = corporate?.corporateInformation?.payment?.value || 0;
const commission = corporate?.corporateInformation?.payment?.commission || 0;
@@ -101,13 +101,13 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
value: user.id,
meta: user,
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`,
label: `${user.name} - ${user.email}`,
}))}
defaultValue={{value: "undefined", label: "Select an account"}}
defaultValue={{ value: "undefined", label: "Select an account" }}
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -129,10 +129,10 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Price *</label>
<div className="w-full grid grid-cols-5 gap-2">
<Input name="paymentValue" onChange={() => {}} type="number" value={price} defaultValue={0} className="col-span-3" disabled />
<Input name="paymentValue" onChange={() => { }} type="number" value={price} defaultValue={0} className="col-span-3" disabled />
<Select
className="px-4 col-span-2 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-mti-gray-platinum/40 text-mti-gray-dim cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
options={CURRENCIES.map(({label, currency}) => ({
options={CURRENCIES.map(({ label, currency }) => ({
value: currency,
label,
}))}
@@ -140,14 +140,14 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
value: currency || "EUR",
label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
}}
onChange={() => {}}
onChange={() => { }}
value={{
value: currency || "EUR",
label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
}}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -171,7 +171,7 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
<div className="flex gap-4 w-full">
<div className="flex flex-col w-full gap-3">
<label className="font-normal text-base text-mti-gray-dim">Commission *</label>
<Input name="commission" onChange={() => {}} type="number" defaultValue={0} value={commission} disabled />
<Input name="commission" onChange={() => { }} type="number" defaultValue={0} value={commission} disabled />
</div>
<div className="flex flex-col w-full gap-3">
<label className="font-normal text-base text-mti-gray-dim">Commission Value*</label>
@@ -277,16 +277,16 @@ export default function PaymentRecord() {
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
const [isCreatingPayment, setIsCreatingPayment] = useState(false);
const [filters, setFilters] = useState<{filter: (p: Payment) => boolean; id: string}[]>([]);
const [filters, setFilters] = useState<{ filter: (p: Payment) => boolean; id: string }[]>([]);
const [displayPayments, setDisplayPayments] = useState<Payment[]>([]);
const [corporate, setCorporate] = useState<User>();
const [agent, setAgent] = useState<User>();
const {user} = useUser({redirectTo: "/login"});
const {users, reload: reloadUsers} = useUsers();
const {payments: originalPayments, reload: reloadPayment} = usePayments();
const {payments: paypalPayments, reload: reloadPaypalPayment} = usePaypalPayments();
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<Date | null>(moment("01/01/2023").toDate());
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
@@ -331,11 +331,11 @@ export default function PaymentRecord() {
...(!agent
? []
: [
{
id: "agent-filter",
filter: (p: Payment) => p.agent === agent.id,
},
]),
{
id: "agent-filter",
filter: (p: Payment) => p.agent === agent.id,
},
]),
]);
}, [agent]);
@@ -345,18 +345,18 @@ export default function PaymentRecord() {
...(!corporate
? []
: [
{
id: "corporate-filter",
filter: (p: Payment) => p.corporate === corporate.id,
},
]),
{
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}]),
...(typeof paid !== "boolean" ? [] : [{ id: "paid", filter: (p: Payment) => p.isPaid === paid }]),
]);
}, [paid]);
@@ -366,11 +366,11 @@ export default function PaymentRecord() {
...(typeof commissionTransfer !== "boolean"
? []
: [
{
id: "commissionTransfer",
filter: (p: Payment) => !p.commissionTransfer === commissionTransfer,
},
]),
{
id: "commissionTransfer",
filter: (p: Payment) => !p.commissionTransfer === commissionTransfer,
},
]),
]);
}, [commissionTransfer]);
@@ -380,11 +380,11 @@ export default function PaymentRecord() {
...(typeof corporateTransfer !== "boolean"
? []
: [
{
id: "corporateTransfer",
filter: (p: Payment) => !p.corporateTransfer === corporateTransfer,
},
]),
{
id: "corporateTransfer",
filter: (p: Payment) => !p.corporateTransfer === corporateTransfer,
},
]),
]);
}, [corporateTransfer]);
@@ -395,7 +395,7 @@ export default function PaymentRecord() {
const updatePayment = (payment: Payment, key: string, value: any) => {
axios
.patch(`api/payments/${payment.id}`, {...payment, [key]: value})
.patch(`api/payments/${payment.id}`, { ...payment, [key]: value })
.then(() => toast.success("Updated the payment"))
.finally(reload);
};
@@ -540,7 +540,7 @@ export default function PaymentRecord() {
switch (key) {
case "agentCommission": {
const value = info.getValue();
return {value: `${value}%`};
return { value: `${value}%` };
}
case "agent": {
const user = users.find((x) => x.id === info.row.original.agent) as AgentUser;
@@ -553,18 +553,18 @@ export default function PaymentRecord() {
case "amount": {
const value = info.getValue();
const numberValue = toFixedNumber(value, 2);
return {value: numberValue};
return { value: numberValue };
}
case "date": {
const value = info.getValue();
return {value: moment(value).format("DD/MM/YYYY")};
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,
value: user?.name,
};
}
case "currency": {
@@ -576,7 +576,7 @@ export default function PaymentRecord() {
case "corporateId":
default: {
const value = info.getValue();
return {value};
return { value };
}
}
};
@@ -588,7 +588,7 @@ export default function PaymentRecord() {
header: "Country Manager",
id: "agent",
cell: (info) => {
const {user, value} = columHelperValue(info.column.id, info);
const { user, value } = columHelperValue(info.column.id, info);
return (
<div
className={clsx(
@@ -604,7 +604,7 @@ export default function PaymentRecord() {
header: "Commission",
id: "agentCommission",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <>{value}</>;
},
}),
@@ -612,7 +612,7 @@ export default function PaymentRecord() {
header: "Commission Value",
id: "agentValue",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
const finalValue = `${value} ${info.row.original.currency}`;
return <span>{finalValue}</span>;
},
@@ -626,7 +626,7 @@ export default function PaymentRecord() {
header: "Corporate ID",
id: "corporateId",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return value;
},
}),
@@ -634,7 +634,7 @@ export default function PaymentRecord() {
header: "Corporate",
id: "corporate",
cell: (info) => {
const {user, value} = columHelperValue(info.column.id, info);
const { user, value } = columHelperValue(info.column.id, info);
return (
<div
className={clsx(
@@ -650,7 +650,7 @@ export default function PaymentRecord() {
header: "Date",
id: "date",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{value}</span>;
},
}),
@@ -658,7 +658,7 @@ export default function PaymentRecord() {
header: "Amount",
id: "amount",
cell: (info) => {
const {value} = columHelperValue(info.column.id, 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 <span>{finalValue}</span>;
@@ -669,7 +669,7 @@ export default function PaymentRecord() {
header: "Paid",
id: "isPaid",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return (
<Checkbox
@@ -691,7 +691,7 @@ export default function PaymentRecord() {
{
header: "",
id: "actions",
cell: ({row}: {row: {original: Payment}}) => {
cell: ({ row }: { row: { original: Payment } }) => {
return (
<div className="flex gap-4">
{user?.type !== "agent" && (
@@ -720,7 +720,7 @@ export default function PaymentRecord() {
})
.map((p) => {
const user = users.find((x) => x.id === p.userId) as User;
return {...p, name: user?.name, email: user?.email};
return { ...p, name: user?.name, email: user?.email };
}),
[paypalPayments, users, startDatePaymob, endDatePaymob],
);
@@ -730,7 +730,7 @@ export default function PaymentRecord() {
header: "Order ID",
id: "orderId",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{value}</span>;
},
}),
@@ -738,7 +738,7 @@ export default function PaymentRecord() {
header: "Status",
id: "status",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{value}</span>;
},
}),
@@ -746,7 +746,7 @@ export default function PaymentRecord() {
header: "User Name",
id: "name",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{value}</span>;
},
}),
@@ -754,7 +754,7 @@ export default function PaymentRecord() {
header: "Email",
id: "email",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{value}</span>;
},
}),
@@ -762,7 +762,7 @@ export default function PaymentRecord() {
header: "Amount",
id: "value",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
const finalValue = `${value} ${info.row.original.currency}`;
return <span>{finalValue}</span>;
},
@@ -771,7 +771,7 @@ export default function PaymentRecord() {
header: "Date",
id: "createdAt",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{moment(value).format("DD/MM/YYYY")}</span>;
},
}),
@@ -779,13 +779,13 @@ export default function PaymentRecord() {
header: "Expiration Date",
id: "subscriptionExpirationDate",
cell: (info) => {
const {value} = columHelperValue(info.column.id, info);
const { value } = columHelperValue(info.column.id, info);
return <span>{moment(value).format("DD/MM/YYYY")}</span>;
},
}),
];
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")),
@@ -809,7 +809,7 @@ export default function PaymentRecord() {
}}
user={selectedCorporateUser}
disabled
disabledFields={{countryManager: true}}
disabledFields={{ countryManager: true }}
/>
</div>
)}
@@ -859,7 +859,7 @@ export default function PaymentRecord() {
return [...accm, ...data];
}, []);
const {rows} = currentTable.getRowModel();
const { rows } = currentTable.getRowModel();
const finalColumns = [
...columns,
@@ -872,8 +872,8 @@ export default function PaymentRecord() {
return {
columns: finalColumns,
rows: rows.map((row) => {
return finalColumns.reduce((accm, {key}) => {
const {value} = columHelperValue(key, {
return finalColumns.reduce((accm, { key }) => {
const { value } = columHelperValue(key, {
row,
getValue: () => row.getValue(key),
});
@@ -886,7 +886,7 @@ export default function PaymentRecord() {
};
};
const {rows: csvRows, columns: csvColumns} = getCSVData();
const { rows: csvRows, columns: csvColumns } = getCSVData();
const renderTable = (table: Table<any>) => (
<table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full">
@@ -958,7 +958,7 @@ export default function PaymentRecord() {
<Tab.Group selectedIndex={selectedIndex} onChange={setSelectedIndex}>
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
<Tab
className={({selected}) =>
className={({ selected }) =>
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",
@@ -970,7 +970,7 @@ export default function PaymentRecord() {
</Tab>
{checkAccess(user, ["developer", "admin"]) && (
<Tab
className={({selected}) =>
className={({ selected }) =>
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",
@@ -996,24 +996,22 @@ export default function PaymentRecord() {
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
value: user.id,
meta: user,
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`,
label: `${user.name} - ${user.email}`,
}))}
defaultValue={
user.type === "corporate"
? {
value: user.id,
meta: user,
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${
user.email
}`,
}
value: user.id,
meta: user,
label: `${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}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -1049,15 +1047,15 @@ export default function PaymentRecord() {
value={
agent
? {
value: agent?.id,
label: `${agent.name} - ${agent.email}`,
}
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}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -1092,7 +1090,7 @@ export default function PaymentRecord() {
}}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -1149,7 +1147,7 @@ export default function PaymentRecord() {
}}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -1183,7 +1181,7 @@ export default function PaymentRecord() {
}}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({...base, zIndex: 9999}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({
...styles,
paddingLeft: "4px",

View File

@@ -1,16 +1,16 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from "react";
import useUser from "@/hooks/useUser";
import {toast, ToastContainer} from "react-toastify";
import { toast, ToastContainer } from "react-toastify";
import Layout from "@/components/High/Layout";
import Input from "@/components/Low/Input";
import Button from "@/components/Low/Button";
import Link from "next/link";
import axios from "axios";
import {ErrorMessage} from "@/constants/errors";
import { ErrorMessage } from "@/constants/errors";
import clsx from "clsx";
import {
CorporateUser,
@@ -23,32 +23,32 @@ import {
Group,
} from "@/interfaces/user";
import CountrySelect from "@/components/Low/CountrySelect";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import moment from "moment";
import {BsCamera, BsQuestionCircleFill} from "react-icons/bs";
import {USER_TYPE_LABELS} from "@/resources/user";
import { BsCamera, BsQuestionCircleFill } from "react-icons/bs";
import { USER_TYPE_LABELS } from "@/resources/user";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
import {convertBase64, redirect} from "@/utils";
import {Divider} from "primereact/divider";
import { convertBase64, redirect } from "@/utils";
import { Divider } from "primereact/divider";
import GenderInput from "@/components/High/GenderInput";
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
import TimezoneSelect from "@/components/Low/TImezoneSelect";
import Modal from "@/components/Modal";
import {Module} from "@/interfaces";
import { Module } from "@/interfaces";
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
import Select from "@/components/Low/Select";
import {InstructorGender} from "@/interfaces/exam";
import {capitalize} from "lodash";
import { InstructorGender } from "@/interfaces/exam";
import { capitalize } from "lodash";
import TopicModal from "@/components/Medium/TopicModal";
import {v4} from "uuid";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {getParticipantGroups, getUserCorporate} from "@/utils/groups.be";
import {InferGetServerSidePropsType} from "next";
import {getUsers} from "@/utils/users.be";
import { v4 } from "uuid";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import { getParticipantGroups, getUserCorporate } from "@/utils/groups.be";
import { InferGetServerSidePropsType } from "next";
import { getUsers } from "@/utils/users.be";
import { requestUser } from "@/utils/api";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
@@ -72,9 +72,9 @@ interface Props {
linkedCorporate?: CorporateUser | MasterCorporateUser;
}
const DoubleColumnRow = ({children}: {children: ReactNode}) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>;
const DoubleColumnRow = ({ children }: { children: ReactNode }) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>;
function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props) {
function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props) {
const [bio, setBio] = useState(user.bio || "");
const [name, setName] = useState(user.name || "");
const [email, setEmail] = useState(user.email || "");
@@ -182,21 +182,21 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
passport_id,
timezone,
},
...(user.type === "corporate" ? {corporateInformation} : {}),
...(user.type === "corporate" ? { corporateInformation } : {}),
...(user.type === "agent"
? {
agentInformation: {
companyName,
commercialRegistration,
arabName,
},
}
agentInformation: {
companyName,
commercialRegistration,
arabName,
},
}
: {}),
})
.then((response) => {
if (response.status === 200) {
toast.success("Your profile has been updated!");
mutateUser((response.data as {user: User}).user);
mutateUser((response.data as { user: User }).user);
setIsLoading(false);
return;
}
@@ -247,7 +247,7 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
<form className="flex flex-col items-center gap-6 w-full" onSubmit={(e) => e.preventDefault()}>
<DoubleColumnRow>
{user.type !== "corporate" && user.type !== "mastercorporate" ? (
{user.type !== "corporate" && user.type !== "mastercorporate" && (
<Input
label={user.type === "agent" ? "English name" : "Name"}
type="text"
@@ -257,25 +257,6 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
defaultValue={name}
required
/>
) : (
<Input
label="Company name"
type="text"
name="name"
disabled={!!linkedCorporate}
onChange={(e) =>
setCorporateInformation((prev) => ({
...prev!,
companyInformation: {
...prev!.companyInformation,
name: e,
},
}))
}
placeholder="Enter your company's name"
defaultValue={corporateInformation?.companyInformation?.name}
required
/>
)}
{user.type === "agent" && (
@@ -381,7 +362,7 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
<ModuleLevelSelector
levels={desiredLevels}
setLevels={setDesiredLevels as Dispatch<SetStateAction<{[key in Module]: number}>>}
setLevels={setDesiredLevels as Dispatch<SetStateAction<{ [key in Module]: number }>>}
/>
</div>
<div className="flex flex-col gap-3 w-full">
@@ -425,9 +406,9 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
}}
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
options={[
{value: "male", label: "Male"},
{value: "female", label: "Female"},
{value: "varied", label: "Varied"},
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
{ value: "varied", label: "Varied" },
]}
/>
</div>
@@ -461,15 +442,6 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
{user.type === "corporate" && (
<>
<DoubleColumnRow>
<Input
type="number"
name="companyUsers"
onChange={() => null}
label="Number of users"
defaultValue={user.corporateInformation?.companyInformation.userAmount}
disabled
required
/>
<Input
type="text"
name="pricing"
@@ -642,8 +614,8 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
);
}
export default function Home(props: {linkedCorporate?: CorporateUser | MasterCorporateUser; groups: Group[]; users: User[]}) {
const {user, mutateUser} = useUser({redirectTo: "/login"});
export default function Home(props: { linkedCorporate?: CorporateUser | MasterCorporateUser; groups: Group[]; users: User[] }) {
const { user, mutateUser } = useUser({ redirectTo: "/login" });
return (
<>

View File

@@ -16,7 +16,7 @@ import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
import { useState } from "react";
import Modal from "@/components/Modal";
import IconCard from "@/dashboards/IconCard";
import IconCard from "@/components/IconCard";
import { BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill } from "react-icons/bs";
import UserCreator from "./(admin)/UserCreator";
import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";

View File

@@ -1,199 +0,0 @@
/* 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 { averageScore, groupBySession, totalExams } from "@/utils/stats";
import useUser from "@/hooks/useUser";
import Diagnostic from "@/components/Diagnostic";
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 axios from "axios";
import DemographicInformationInput from "@/components/DemographicInformationInput";
import moment from "moment";
import Link from "next/link";
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, MasterCorporateUser, Type, User, userTypes } from "@/interfaces/user";
import Select from "react-select";
import { USER_TYPE_LABELS } from "@/resources/user";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import { getUserCorporate } from "@/utils/groups.be";
import { getUsers } from "@/utils/users.be";
import { requestUser } from "@/utils/api";
import { redirect, serialize } from "@/utils";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
const linkedCorporate = (await getUserCorporate(user.id)) || null;
return {
props: serialize({ user, linkedCorporate }),
};
}, sessionOptions);
interface Props {
user: User;
linkedCorporate?: CorporateUser | MasterCorporateUser;
}
export default function Home({ user: propsUser, linkedCorporate }: Props) {
const [user, setUser] = useState(propsUser);
const [showDiagnostics, setShowDiagnostics] = useState(false);
const [showDemographicInput, setShowDemographicInput] = useState(false);
const [selectedScreen, setSelectedScreen] = useState<Type>("admin");
const { mutateUser } = useUser({ redirectTo: "/login" });
const router = useRouter();
useEffect(() => {
if (user) {
// setShowDemographicInput(!user.demographicInformation || !user.demographicInformation.country || !user.demographicInformation.phone);
setShowDiagnostics(user.isFirstLogin && user.type === "student");
}
}, [user]);
const checkIfUserExpired = () => {
const expirationDate = user!.subscriptionExpirationDate;
if (expirationDate === null || expirationDate === undefined) return false;
if (moment(expirationDate).isAfter(moment(new Date()))) return false;
return true;
};
if (user && (user.status === "paymentDue" || user.status === "disabled" || checkIfUserExpired())) {
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
{user.status === "disabled" && (
<Layout user={user} navDisabled>
<div className="flex flex-col items-center justify-center text-center w-full gap-4">
<span className="font-bold text-lg">Your account has been disabled!</span>
<span>Please contact an administrator if you believe this to be a mistake.</span>
</div>
</Layout>
)}
{(user.status === "paymentDue" || checkIfUserExpired()) && <PaymentDue hasExpired user={user} reload={router.reload} />}
</>
);
}
if (user && showDemographicInput) {
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Layout user={user} navDisabled>
<DemographicInformationInput
mutateUser={(user) => {
setUser(user);
mutateUser(user);
}}
user={user}
/>
</Layout>
</>
);
}
if (user && showDiagnostics) {
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Layout user={user} navDisabled>
<Diagnostic user={user} onFinish={() => setShowDiagnostics(false)} />
</Layout>
</>
);
}
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user}>
{checkAccess(user, ["student"]) && <StudentDashboard linkedCorporate={linkedCorporate} user={user} />}
{checkAccess(user, ["teacher"]) && <TeacherDashboard linkedCorporate={linkedCorporate} user={user} />}
{checkAccess(user, ["corporate"]) && <CorporateDashboard linkedCorporate={linkedCorporate} user={user as CorporateUser} />}
{checkAccess(user, ["mastercorporate"]) && <MasterCorporateDashboard user={user as MasterCorporateUser} />}
{checkAccess(user, ["agent"]) && <AgentDashboard user={user} />}
{checkAccess(user, ["admin"]) && <AdminDashboard user={user} />}
{checkAccess(user, ["developer"]) && (
<>
<Select
options={userTypes.map((u) => ({
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" && <StudentDashboard linkedCorporate={linkedCorporate} user={user} />}
{selectedScreen === "teacher" && <TeacherDashboard linkedCorporate={linkedCorporate} user={user} />}
{selectedScreen === "corporate" && (
<CorporateDashboard linkedCorporate={linkedCorporate} user={user as unknown as CorporateUser} />
)}
{selectedScreen === "mastercorporate" && <MasterCorporateDashboard user={user as unknown as MasterCorporateUser} />}
{selectedScreen === "agent" && <AgentDashboard user={user} />}
{selectedScreen === "admin" && <AdminDashboard user={user} />}
</>
)}
</Layout>
)}
</>
);
}

View File

@@ -1,6 +1,6 @@
import {Type, User, CorporateUser, AgentUser, Group} from "@/interfaces/user";
import { Type, User, CorporateUser, AgentUser, Group } from "@/interfaces/user";
export const USER_TYPE_LABELS: {[key in Type]: string} = {
export const USER_TYPE_LABELS: { [key in Type]: string } = {
student: "Student",
teacher: "Teacher",
corporate: "Corporate",
@@ -19,8 +19,8 @@ export function isAgentUser(user: User): user is AgentUser {
}
export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
if (isCorporateUser(user)) return user.corporateInformation?.companyInformation?.name || user.name;
if (isAgentUser(user)) return user.agentInformation?.companyName || user.name;
if (isCorporateUser(user)) return user.name;
const belongingGroups = groups.filter((x) => x.participants.includes(user?.id));
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
@@ -28,7 +28,7 @@ export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
if (belongingGroupsAdmins.length === 0) return "";
const admin = belongingGroupsAdmins[0] as CorporateUser;
return admin.corporateInformation?.companyInformation.name || admin.name;
return admin.name;
}
export function getCorporateUser(user: User, users: User[], groups: Group[]) {

View File

@@ -10,95 +10,95 @@ import { getSpecificUsers } from "./users.be";
const db = client.db(process.env.MONGODB_DB);
const addEntityToGroupPipeline = [
{
$lookup: {
from: "entities",
localField: "entity",
foreignField: "id",
as: "entity"
}
},
{
$addFields: {
entity: { $arrayElemAt: ["$entity", 0] }
}
},
{
$addFields: {
entity: {
$cond: {
if: { $isArray: "$entity" },
then: undefined,
else: "$entity"
}
}
}
}
{
$lookup: {
from: "entities",
localField: "entity",
foreignField: "id",
as: "entity"
}
},
{
$addFields: {
entity: { $arrayElemAt: ["$entity", 0] }
}
},
{
$addFields: {
entity: {
$cond: {
if: { $isArray: "$entity" },
then: undefined,
else: "$entity"
}
}
}
}
]
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
const corporate = await db.collection("users").findOne<User>({ id: corporateID });
const participant = await db.collection("users").findOne<User>({ id: participantID });
const corporate = await db.collection("users").findOne<User>({ id: corporateID });
const participant = await db.collection("users").findOne<User>({ id: participantID });
if (!corporate || !participant) return;
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
if (!corporate || !participant) return;
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate)
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: null } });
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate)
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: null } });
const corporateDate = moment(corporate.subscriptionExpirationDate);
const participantDate = moment(participant.subscriptionExpirationDate);
const corporateDate = moment(corporate.subscriptionExpirationDate);
const participantDate = moment(participant.subscriptionExpirationDate);
if (corporateDate.isAfter(participantDate))
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: corporateDate.toISOString() } });
if (corporateDate.isAfter(participantDate))
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: corporateDate.toISOString() } });
return;
return;
};
export const getUserCorporate = async (id: string) => {
const user = await getUser(id);
if (!user) return undefined;
const user = await getUser(id);
if (!user) return undefined;
if (["admin", "developer"].includes(user.type)) return undefined;
if (user.type === "mastercorporate") return user;
if (["admin", "developer"].includes(user.type)) return undefined;
if (user.type === "mastercorporate") return user;
const groups = await getParticipantGroups(id);
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
const corporates = admins
.filter((x) => (user.type === "corporate" ? x?.type === "mastercorporate" : x?.type === "corporate"))
.filter((x) => !!x) as User[];
const groups = await getParticipantGroups(id);
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
const corporates = admins
.filter((x) => (user.type === "corporate" ? x?.type === "mastercorporate" : x?.type === "corporate"))
.filter((x) => !!x) as User[];
if (corporates.length === 0) return undefined;
return corporates.shift() as CorporateUser | MasterCorporateUser;
if (corporates.length === 0) return undefined;
return corporates.shift() as CorporateUser | MasterCorporateUser;
};
export const getGroup = async (id: string) => {
return await db.collection("groups").findOne<Group>({ id });
return await db.collection("groups").findOne<Group>({ id });
};
export const getGroups = async (): Promise<WithEntity<Group>[]> => {
return await db.collection("groups")
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
return await db.collection("groups")
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
};
export const getParticipantGroups = async (id: string) => {
return await db.collection("groups").find<Group>({ participants: id }).toArray();
return await db.collection("groups").find<Group>({ participants: id }).toArray();
};
export const getParticipantsGroups = async (ids: string[]) => {
return await db.collection("groups").find<Group>({ participants: { $in: ids } }).toArray();
return await db.collection("groups").find<Group>({ participants: { $in: ids } }).toArray();
};
export const getUserGroups = async (id: string): Promise<Group[]> => {
return await db.collection("groups").find<Group>({ admin: id }).toArray();
return await db.collection("groups").find<Group>({ admin: id }).toArray();
};
export const getUserNamedGroup = async (id: string, name: string) => {
return await db.collection("groups").findOne<Group>({ admin: id, name });
return await db.collection("groups").findOne<Group>({ admin: id, name });
};
export const removeParticipantFromGroup = async (id: string, user: string) => {
return await db.collection("groups").updateOne({id}, {
return await db.collection("groups").updateOne({ id }, {
// @ts-expect-error
$pull: {
participants: user
@@ -107,69 +107,69 @@ export const removeParticipantFromGroup = async (id: string, user: string) => {
}
export const getUsersGroups = async (ids: string[]) => {
return await db
.collection("groups")
.find<Group>({ admin: { $in: ids } })
.toArray();
return await db
.collection("groups")
.find<Group>({ admin: { $in: ids } })
.toArray();
};
export const convertToUsers = (group: Group, users: User[]): GroupWithUsers =>
Object.assign(group, {
admin: users.find((u) => u.id === group.admin),
participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[],
});
Object.assign(group, {
admin: users.find((u) => u.id === group.admin),
participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[],
});
export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise<string[]> => {
const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher");
const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate");
const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher");
const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate");
return [...linkedTeachers.users.map((x) => x.id), ...linkedCorporates.users.map((x) => x.id)];
return [...linkedTeachers.users.map((x) => x.id), ...linkedCorporates.users.map((x) => x.id)];
};
export const getGroupsForEntities = async (ids: string[]) =>
await db
.collection("groups")
.find<Group>({ entity: { $in: ids } })
.toArray();
await db
.collection("groups")
.find<Group>({ entity: { $in: ids } })
.toArray();
export const getGroupsForUser = async (admin?: string, participant?: string) => {
if (admin && participant) return await db.collection("groups").find<Group>({ admin, participant }).toArray();
if (admin && participant) return await db.collection("groups").find<Group>({ admin, participant }).toArray();
if (admin) return await getUserGroups(admin);
if (participant) return await getParticipantGroups(participant);
if (admin) return await getUserGroups(admin);
if (participant) return await getParticipantGroups(participant);
return await getGroups();
return await getGroups();
};
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => {
return await db
.collection("groups")
.find<Group>({ ...(admin ? { admin: { $ne: admin } } : {}), ...(participants ? { participants } : {}) })
.toArray();
return await db
.collection("groups")
.find<Group>({ ...(admin ? { admin: { $ne: admin } } : {}), ...(participants ? { participants } : {}) })
.toArray();
};
export const getCorporateNameForStudent = async (studentID: string) => {
const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]);
if (groups.length === 0) return "";
const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]);
if (groups.length === 0) return "";
const adminUserIds = [...new Set(groups.map((g) => g.admin))];
const adminUsersData = (await getSpecificUsers(adminUserIds)).filter((x) => !!x) as User[];
const adminUserIds = [...new Set(groups.map((g) => g.admin))];
const adminUsersData = (await getSpecificUsers(adminUserIds)).filter((x) => !!x) as User[];
if (adminUsersData.length === 0) return "";
const admins = adminUsersData.filter((x) => x.type === "corporate");
if (adminUsersData.length === 0) return "";
const admins = adminUsersData.filter((x) => x.type === "corporate");
if (admins.length > 0) {
return (admins[0] as CorporateUser).corporateInformation.companyInformation.name;
}
if (admins.length > 0) {
return (admins[0] as CorporateUser).name;
}
return "";
return "";
};
export const getGroupsByEntity = async (id: string) => await db.collection("groups").find<Group>({ entity: id }).toArray();
export const getGroupsByEntities = async (ids: string[]): Promise<WithEntity<Group>[]> =>
await db.collection("groups")
.aggregate<WithEntity<Group>>([
{ $match: { entity: { $in: ids } } },
...addEntityToGroupPipeline
]).toArray()
await db.collection("groups")
.aggregate<WithEntity<Group>>([
{ $match: { entity: { $in: ids } } },
...addEntityToGroupPipeline
]).toArray()

View File

@@ -12,48 +12,48 @@ import { mapBy } from ".";
const db = client.db(process.env.MONGODB_DB);
export async function getUsers(filter?: object) {
return await db
.collection("users")
.find<User>(filter || {}, { projection: { _id: 0 } })
.toArray();
return await db
.collection("users")
.find<User>(filter || {}, { projection: { _id: 0 } })
.toArray();
}
export async function getUserWithEntity(id: string): Promise<WithEntities<User> | undefined> {
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
if (!user) return undefined;
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
if (!user) return undefined;
const entities = await Promise.all(
user.entities.map(async (e) => {
const entity = await getEntity(e.id);
const role = await getRole(e.role);
const entities = await Promise.all(
user.entities.map(async (e) => {
const entity = await getEntity(e.id);
const role = await getRole(e.role);
return { entity, role };
}),
);
return { entity, role };
}),
);
return { ...user, entities };
return { ...user, entities };
}
export async function getUser(id: string): Promise<User | undefined> {
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
return !!user ? user : undefined;
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
return !!user ? user : undefined;
}
export async function getSpecificUsers(ids: string[]) {
if (ids.length === 0) return [];
if (ids.length === 0) return [];
return await db
.collection("users")
.find<User>({ id: { $in: ids } }, { projection: { _id: 0 } })
.toArray();
return await db
.collection("users")
.find<User>({ id: { $in: ids } }, { projection: { _id: 0 } })
.toArray();
}
export async function getEntityUsers(id: string, limit?: number, filter?: object) {
return await db
.collection("users")
return await db
.collection("users")
.find<User>({ "entities.id": id, ...(filter || {}) })
.limit(limit || 0)
.toArray();
.limit(limit || 0)
.toArray();
}
export async function countEntityUsers(id: string, filter?: object) {
@@ -61,96 +61,96 @@ export async function countEntityUsers(id: string, filter?: object) {
}
export async function getEntitiesUsers(ids: string[], filter?: object, limit?: number) {
return await db
.collection("users")
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) })
.limit(limit || 0)
.toArray();
return await db
.collection("users")
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) })
.limit(limit || 0)
.toArray();
}
export async function countEntitiesUsers(ids: string[]) {
return await db.collection("users").countDocuments({ "entities.id": { $in: ids } });
return await db.collection("users").countDocuments({ "entities.id": { $in: ids } });
}
export async function getLinkedUsers(
userID?: string,
userType?: Type,
type?: Type,
page?: number,
size?: number,
sort?: string,
direction?: "asc" | "desc",
userID?: string,
userType?: Type,
type?: Type,
page?: number,
size?: number,
sort?: string,
direction?: "asc" | "desc",
) {
const filters = {
...(!!type ? { type } : {}),
};
const filters = {
...(!!type ? { type } : {}),
};
if (!userID || userType === "admin" || userType === "developer") {
const users = await db
.collection("users")
.find<User>(filters)
.sort(sort ? { [sort]: direction === "desc" ? -1 : 1 } : {})
.skip(page && size ? page * size : 0)
.limit(size || 0)
.toArray();
if (!userID || userType === "admin" || userType === "developer") {
const users = await db
.collection("users")
.find<User>(filters)
.sort(sort ? { [sort]: direction === "desc" ? -1 : 1 } : {})
.skip(page && size ? page * size : 0)
.limit(size || 0)
.toArray();
const total = await db.collection("users").countDocuments(filters);
return { users, total };
}
const total = await db.collection("users").countDocuments(filters);
return { users, total };
}
const adminGroups = await getUserGroups(userID);
const groups = await getUsersGroups(adminGroups.flatMap((x) => x.participants));
const belongingGroups = await getParticipantGroups(userID);
const adminGroups = await getUserGroups(userID);
const groups = await getUsersGroups(adminGroups.flatMap((x) => x.participants));
const belongingGroups = await getParticipantGroups(userID);
const participants = uniq([
...adminGroups.flatMap((x) => x.participants),
...(userType === "mastercorporate" ? groups.flat().flatMap((x) => x.participants) : []),
...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []),
]);
const participants = uniq([
...adminGroups.flatMap((x) => x.participants),
...(userType === "mastercorporate" ? groups.flat().flatMap((x) => x.participants) : []),
...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []),
]);
// [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] {
if (participants.length === 0) return { users: [], total: 0 };
// [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] {
if (participants.length === 0) return { users: [], total: 0 };
const users = await db
.collection("users")
.find<User>({ ...filters, id: { $in: participants } })
.skip(page && size ? page * size : 0)
.limit(size || 0)
.toArray();
const total = await db.collection("users").countDocuments({ ...filters, id: { $in: participants } });
const users = await db
.collection("users")
.find<User>({ ...filters, id: { $in: participants } })
.skip(page && size ? page * size : 0)
.limit(size || 0)
.toArray();
const total = await db.collection("users").countDocuments({ ...filters, id: { $in: participants } });
return { users, total };
return { users, total };
}
export async function getUserBalance(user: User) {
const codes = await getUserCodes(user.id);
if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length;
const codes = await getUserCodes(user.id);
if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length;
const groups = await getGroupsForUser(user.id);
const participants = uniq(groups.flatMap((x) => x.participants));
const groups = await getGroupsForUser(user.id);
const participants = uniq(groups.flatMap((x) => x.participants));
if (user.type === "corporate") return participants.length + codes.filter((x) => !participants.includes(x.userId || "")).length;
if (user.type === "corporate") return participants.length + codes.filter((x) => !participants.includes(x.userId || "")).length;
const participantUsers = await Promise.all(participants.map(getUser));
const corporateUsers = participantUsers.filter((x) => x?.type === "corporate") as CorporateUser[];
const participantUsers = await Promise.all(participants.map(getUser));
const corporateUsers = participantUsers.filter((x) => x?.type === "corporate") as CorporateUser[];
return (
corporateUsers.reduce((acc, curr) => acc + curr.corporateInformation?.companyInformation?.userAmount || 0, 0) +
corporateUsers.length +
codes.filter((x) => !participants.includes(x.userId || "") && !corporateUsers.map((u) => u.id).includes(x.userId || "")).length
);
return (
corporateUsers.reduce((acc, curr) => acc + 0, 0) +
corporateUsers.length +
codes.filter((x) => !participants.includes(x.userId || "") && !corporateUsers.map((u) => u.id).includes(x.userId || "")).length
);
}
export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students')
const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers')
const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates')
const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates')
const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers')
const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates')
const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates')
const students = await getEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), {type: "student"})
const teachers = await getEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), {type: "teacher"})
const corporates = await getEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), {type: "corporate"})
const masterCorporates = await getEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), {type: "mastercorporate"})
const students = await getEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" })
const teachers = await getEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), { type: "teacher" })
const corporates = await getEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), { type: "corporate" })
const masterCorporates = await getEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), { type: "mastercorporate" })
return [...students, ...teachers, ...corporates, ...masterCorporates]
return [...students, ...teachers, ...corporates, ...masterCorporates]
}

View File

@@ -8,44 +8,44 @@ import { findAllowedEntities } from "./permissions";
import { getEntitiesUsers } from "./users.be";
export interface UserListRow {
name: string;
email: string;
type: string;
entities: string;
expiryDate: string;
verified: string;
country: string;
phone: string;
employmentPosition: string;
gender: string;
name: string;
email: string;
type: string;
entities: string;
expiryDate: string;
verified: string;
country: string;
phone: string;
employmentPosition: string;
gender: string;
}
export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
const rows: UserListRow[] = rowUsers.map((user) => ({
name: user.name,
email: user.email,
type: USER_TYPE_LABELS[user.type],
entities: user.entities.map((e) => e.label).join(', '),
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
country: user.demographicInformation?.country || "N/A",
phone: user.demographicInformation?.phone || "N/A",
employmentPosition:
(user.type === "corporate" || user.type === "mastercorporate"
? user.demographicInformation?.position
: user.demographicInformation?.employment) || "N/A",
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
verified: user.isVerified?.toString() || "FALSE",
}));
const header = "Name,Email,Type,Entities,Expiry Date,Country,Phone,Employment/Department,Gender,Verification";
const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n");
const rows: UserListRow[] = rowUsers.map((user) => ({
name: user.name,
email: user.email,
type: USER_TYPE_LABELS[user.type],
entities: user.entities.map((e) => e.label).join(', '),
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
country: user.demographicInformation?.country || "N/A",
phone: user.demographicInformation?.phone || "N/A",
employmentPosition:
(user.type === "corporate" || user.type === "mastercorporate"
? user.demographicInformation?.position
: user.demographicInformation?.employment) || "N/A",
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
verified: user.isVerified?.toString() || "FALSE",
}));
const header = "Name,Email,Type,Entities,Expiry Date,Country,Phone,Employment/Department,Gender,Verification";
const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n");
return `${header}\n${rowsString}`;
return `${header}\n${rowsString}`;
};
export const getUserName = (user?: User) => {
if (!user) return "N/A";
if (user.type === "corporate" || user.type === "mastercorporate") return user.corporateInformation?.companyInformation?.name || user.name;
return user.name;
if (!user) return "N/A";
if (user.type === "corporate" || user.type === "mastercorporate") return user.name;
return user.name;
};
export const isAdmin = (user: User) => ["admin", "developer"].includes(user.type)