ENCOA-272
This commit is contained in:
@@ -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(
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}))}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' weaknesses and strengths</li>
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -372,16 +370,16 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
// update the stats entries with the pdf url to prevent duplication
|
||||
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);
|
||||
return;
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (admin) {
|
||||
return {
|
||||
...d,
|
||||
corporate: admin.corporateInformation?.companyInformation?.name,
|
||||
corporate: admin.name,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user