ENCOA-272
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import {calculateBandScore} from "@/utils/score";
|
import { calculateBandScore } from "@/utils/score";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
||||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||||
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
|
import { useAssignmentArchive } from "@/hooks/useAssignmentArchive";
|
||||||
import {uniqBy} from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
|
import { useAssignmentUnarchive } from "@/hooks/useAssignmentUnarchive";
|
||||||
import {useAssignmentRelease} from "@/hooks/useAssignmentRelease";
|
import { useAssignmentRelease } from "@/hooks/useAssignmentRelease";
|
||||||
import {getUserName} from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -71,7 +71,7 @@ export default function AssignmentCard({
|
|||||||
// in order to be downloadable, the assignment has to be released
|
// in order to be downloadable, the assignment has to be released
|
||||||
// the component should have the allowDownload prop
|
// the component should have the allowDownload prop
|
||||||
// and the assignment should not have the level module
|
// and the assignment should not have the level module
|
||||||
return uniqModules.every(({module}) => module !== "level");
|
return uniqModules.every(({ module }) => module !== "level");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -82,7 +82,7 @@ export default function AssignmentCard({
|
|||||||
// in order to be downloadable, the assignment has to be released
|
// in order to be downloadable, the assignment has to be released
|
||||||
// the component should have the allowExcelDownload prop
|
// the component should have the allowExcelDownload prop
|
||||||
// and the assignment should have the level module
|
// and the assignment should have the level module
|
||||||
return uniqModules.some(({module}) => module === "level");
|
return uniqModules.some(({ module }) => module === "level");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -121,7 +121,7 @@ export default function AssignmentCard({
|
|||||||
{entityObj && <span>Entity: {entityObj.label}</span>}
|
{entityObj && <span>Entity: {entityObj.label}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
|
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
|
||||||
{uniqModules.map(({module}) => (
|
{uniqModules.map(({ module }) => (
|
||||||
<div
|
<div
|
||||||
key={module}
|
key={module}
|
||||||
className={clsx(
|
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>
|
<span className="font-medium">{formatTimestamp(stats[0].date.toString())}</span>
|
||||||
{timeSpent && (
|
{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>
|
<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";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
invite: Invite;
|
invite: Invite;
|
||||||
users: User[];
|
users: User[];
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InviteCard({ invite, users, reload }: Props) {
|
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 inviter = users.find((u) => u.id === invite.from);
|
||||||
const name = !inviter
|
const name = !inviter ? null : inviter.name;
|
||||||
? null
|
|
||||||
: inviter.type === "corporate"
|
|
||||||
? inviter.corporateInformation?.companyInformation?.name || inviter.name
|
|
||||||
: inviter.name;
|
|
||||||
|
|
||||||
const decide = (decision: "accept" | "decline") => {
|
const decide = (decision: "accept" | "decline") => {
|
||||||
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
|
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.get(`/api/invites/${decision}/${invite.id}`)
|
.get(`/api/invites/${decision}/${invite.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(
|
||||||
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
|
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
|
||||||
{ toastId: "success" },
|
{ toastId: "success" },
|
||||||
);
|
);
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.success(`Something went wrong, please try again later!`, {
|
toast.success(`Something went wrong, please try again later!`, {
|
||||||
toastId: "error",
|
toastId: "error",
|
||||||
});
|
});
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-mti-gray-anti-flash flex min-w-[200px] flex-col gap-6 rounded-xl border p-4 text-black">
|
<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>
|
<span>Invited by {name}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => decide("accept")}
|
onClick={() => decide("accept")}
|
||||||
disabled={isLoading}
|
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"
|
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 && "Accept"}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => decide("decline")}
|
onClick={() => decide("decline")}
|
||||||
disabled={isLoading}
|
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"
|
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 && "Decline"}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import FocusLayer from "@/components/FocusLayer";
|
import FocusLayer from "@/components/FocusLayer";
|
||||||
import {preventNavigation} from "@/utils/navigation.disabled";
|
import { preventNavigation } from "@/utils/navigation.disabled";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {BsList, BsQuestionCircle, BsQuestionCircleFill} from "react-icons/bs";
|
import { BsList, BsQuestionCircle, BsQuestionCircleFill } from "react-icons/bs";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import MobileMenu from "./MobileMenu";
|
import MobileMenu from "./MobileMenu";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {Type} from "@/interfaces/user";
|
import { Type } from "@/interfaces/user";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import {isUserFromCorporate} from "@/utils/groups";
|
import { isUserFromCorporate } from "@/utils/groups";
|
||||||
import Button from "./Low/Button";
|
import Button from "./Low/Button";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import Input from "./Low/Input";
|
import Input from "./Low/Input";
|
||||||
import TicketSubmission from "./High/TicketSubmission";
|
import TicketSubmission from "./High/TicketSubmission";
|
||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import Badge from "./Low/Badge";
|
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 {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
navDisabled?: boolean;
|
navDisabled?: boolean;
|
||||||
@@ -29,7 +29,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable @next/next/no-img-element */
|
/* 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 [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [disablePaymentPage, setDisablePaymentPage] = useState(true);
|
const [disablePaymentPage, setDisablePaymentPage] = useState(true);
|
||||||
const [isTicketOpen, setIsTicketOpen] = useState(false);
|
const [isTicketOpen, setIsTicketOpen] = useState(false);
|
||||||
@@ -109,9 +109,8 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
|
|||||||
badges.map((badge) => (
|
badges.map((badge) => (
|
||||||
<div
|
<div
|
||||||
key={badge.module}
|
key={badge.module}
|
||||||
className={`${
|
className={`${badge.achieved ? `bg-ielts-${badge.module}` : "bg-mti-gray-anti-flash"
|
||||||
badge.achieved ? `bg-ielts-${badge.module}` : "bg-mti-gray-anti-flash"
|
} flex h-8 w-8 items-center justify-center rounded-full`}>
|
||||||
} flex h-8 w-8 items-center justify-center rounded-full`}>
|
|
||||||
{badge.icon()}
|
{badge.icon()}
|
||||||
</div>
|
</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">
|
<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" />
|
<img src={user.profilePicture} alt={user.name} className="h-10 w-10 rounded-full object-cover" />
|
||||||
<span className="-md:hidden text-right">
|
<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.name} | {USER_TYPE_LABELS[user.type]}
|
||||||
{user.type === "corporate" &&
|
{user.type === "corporate" &&
|
||||||
!!user.demographicInformation?.position &&
|
!!user.demographicInformation?.position &&
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type, Stat, Gender} from "@/interfaces/user";
|
import { CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type, Stat, Gender } from "@/interfaces/user";
|
||||||
import {groupBySession, averageScore} from "@/utils/stats";
|
import { groupBySession, averageScore } from "@/utils/stats";
|
||||||
import {RadioGroup} from "@headlessui/react";
|
import { RadioGroup } from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import {BsFileEarmarkText, BsPencil, BsPerson, BsPersonAdd, BsStar} from "react-icons/bs";
|
import { BsFileEarmarkText, BsPencil, BsPerson, BsPersonAdd, BsStar } from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import Button from "./Low/Button";
|
import Button from "./Low/Button";
|
||||||
import Checkbox from "./Low/Checkbox";
|
import Checkbox from "./Low/Checkbox";
|
||||||
import CountrySelect from "./Low/CountrySelect";
|
import CountrySelect from "./Low/CountrySelect";
|
||||||
@@ -17,12 +17,12 @@ import Input from "./Low/Input";
|
|||||||
import ProfileSummary from "./ProfileSummary";
|
import ProfileSummary from "./ProfileSummary";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import {CURRENCIES} from "@/resources/paypal";
|
import { CURRENCIES } from "@/resources/paypal";
|
||||||
import useCodes from "@/hooks/useCodes";
|
import useCodes from "@/hooks/useCodes";
|
||||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||||
import {PermissionType} from "@/interfaces/permissions";
|
import { PermissionType } from "@/interfaces/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const expirationDateColor = (date: Date) => {
|
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],
|
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,
|
value: currency,
|
||||||
label,
|
label,
|
||||||
}));
|
}));
|
||||||
@@ -100,9 +100,7 @@ const UserCard = ({
|
|||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.referralAgent : undefined,
|
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.referralAgent : undefined,
|
||||||
);
|
);
|
||||||
const [companyName, setCompanyName] = useState(
|
const [companyName, setCompanyName] = useState(
|
||||||
user.type === "corporate" || user.type === "mastercorporate"
|
user.type === "agent"
|
||||||
? user.corporateInformation?.companyInformation.name
|
|
||||||
: user.type === "agent"
|
|
||||||
? user.agentInformation?.companyName
|
? user.agentInformation?.companyName
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
@@ -110,25 +108,19 @@ const UserCard = ({
|
|||||||
const [commercialRegistration, setCommercialRegistration] = useState(
|
const [commercialRegistration, setCommercialRegistration] = useState(
|
||||||
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
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(
|
const [paymentValue, setPaymentValue] = useState(
|
||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.value : undefined,
|
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.value : undefined,
|
||||||
);
|
);
|
||||||
const [paymentCurrency, setPaymentCurrency] = useState(
|
const [paymentCurrency, setPaymentCurrency] = useState(
|
||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.currency : "EUR",
|
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(
|
const [commissionValue, setCommission] = useState(
|
||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.commission : undefined,
|
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.commission : undefined,
|
||||||
);
|
);
|
||||||
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id);
|
const { data: stats } = useFilterRecordsByUser<Stat[]>(user.id);
|
||||||
const {users} = useUsers();
|
const { users } = useUsers();
|
||||||
const {codes} = useCodes(user.id);
|
const { codes } = useCodes(user.id);
|
||||||
const {permissions} = usePermissions(loggedInUser.id);
|
const { permissions } = usePermissions(loggedInUser.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (users && users.length > 0) {
|
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;
|
if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
||||||
...user,
|
...user,
|
||||||
subscriptionExpirationDate: expiryDate,
|
subscriptionExpirationDate: expiryDate,
|
||||||
studentID,
|
studentID,
|
||||||
@@ -167,26 +159,21 @@ const UserCard = ({
|
|||||||
agentInformation:
|
agentInformation:
|
||||||
type === "agent"
|
type === "agent"
|
||||||
? {
|
? {
|
||||||
companyName,
|
companyName,
|
||||||
commercialRegistration,
|
commercialRegistration,
|
||||||
arabName,
|
arabName,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
corporateInformation:
|
corporateInformation:
|
||||||
type === "corporate" || type === "mastercorporate"
|
type === "corporate" || type === "mastercorporate"
|
||||||
? {
|
? {
|
||||||
referralAgent,
|
referralAgent,
|
||||||
monthlyDuration,
|
payment: {
|
||||||
companyInformation: {
|
value: paymentValue,
|
||||||
name: companyName,
|
currency: paymentCurrency,
|
||||||
userAmount,
|
...(referralAgent === "" ? {} : { commission: commissionValue }),
|
||||||
},
|
},
|
||||||
payment: {
|
}
|
||||||
value: paymentValue,
|
|
||||||
currency: paymentCurrency,
|
|
||||||
...(referralAgent === "" ? {} : {commission: commissionValue}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -194,7 +181,7 @@ const UserCard = ({
|
|||||||
onClose(true);
|
onClose(true);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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 {
|
const updateUserPermission = PERMISSIONS.updateUser[user.type] as {
|
||||||
list: Type[];
|
list: Type[];
|
||||||
perm: PermissionType;
|
perm: PermissionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProfileSummary
|
<ProfileSummary
|
||||||
user={user}
|
user={user}
|
||||||
items={user.type === "corporate" || user.type === "mastercorporate" ? corporateProfileItems : generalProfileItems}
|
items={user.type === "corporate" || user.type === "mastercorporate" ? [] : generalProfileItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{user.type === "agent" && (
|
{user.type === "agent" && (
|
||||||
@@ -283,48 +255,6 @@ const UserCard = ({
|
|||||||
{(user.type === "corporate" || user.type === "mastercorporate") && (
|
{(user.type === "corporate" || user.type === "mastercorporate") && (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
<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">
|
<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>
|
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
|
||||||
<div className="w-full grid grid-cols-6 gap-2">
|
<div className="w-full grid grid-cols-6 gap-2">
|
||||||
@@ -346,7 +276,7 @@ const UserCard = ({
|
|||||||
onChange={(value) => setPaymentCurrency(value?.value)}
|
onChange={(value) => setPaymentCurrency(value?.value)}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -375,10 +305,10 @@ const UserCard = ({
|
|||||||
className={clsx(
|
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",
|
"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) &&
|
(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={[
|
options={[
|
||||||
{value: "", label: "No referral"},
|
{ value: "", label: "No referral" },
|
||||||
...users
|
...users
|
||||||
.filter((u) => u.type === "agent")
|
.filter((u) => u.type === "agent")
|
||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
@@ -393,7 +323,7 @@ const UserCard = ({
|
|||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
onChange={(value) => setReferralAgent(value?.value)}
|
onChange={(value) => setReferralAgent(value?.value)}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -505,9 +435,9 @@ const UserCard = ({
|
|||||||
value={user.demographicInformation?.employment}
|
value={user.demographicInformation?.employment}
|
||||||
className="grid grid-cols-2 items-center gap-4 place-items-center"
|
className="grid grid-cols-2 items-center gap-4 place-items-center"
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
{EMPLOYMENT_STATUS.map(({status, label}) => (
|
{EMPLOYMENT_STATUS.map(({ status, label }) => (
|
||||||
<RadioGroup.Option value={status} key={status}>
|
<RadioGroup.Option value={status} key={status}>
|
||||||
{({checked}) => (
|
{({ checked }) => (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
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",
|
"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"
|
className="flex flex-row gap-4 justify-between"
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
<RadioGroup.Option value="male">
|
<RadioGroup.Option value="male">
|
||||||
{({checked}) => (
|
{({ checked }) => (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"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>
|
||||||
<RadioGroup.Option value="female">
|
<RadioGroup.Option value="female">
|
||||||
{({checked}) => (
|
{({ checked }) => (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"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>
|
||||||
<RadioGroup.Option value="other">
|
<RadioGroup.Option value="other">
|
||||||
{({checked}) => (
|
{({ checked }) => (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"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,
|
permissions,
|
||||||
user.type === "teacher" ? "editTeacher" : user.type === "student" ? "editStudent" : undefined,
|
user.type === "teacher" ? "editTeacher" : user.type === "student" ? "editStudent" : undefined,
|
||||||
) && (
|
) && (
|
||||||
<>
|
<>
|
||||||
<Divider className="w-full !m-0" />
|
<Divider className="w-full !m-0" />
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Status</label>
|
<label className="font-normal text-base text-mti-gray-dim">Status</label>
|
||||||
<Select
|
<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"
|
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) => {
|
options={USER_STATUS_OPTIONS.filter((x) => {
|
||||||
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
|
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
|
||||||
return x.value !== "paymentDue";
|
return x.value !== "paymentDue";
|
||||||
})}
|
})}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
||||||
onChange={(value) => setStatus(value?.value as typeof user.status)}
|
onChange={(value) => setStatus(value?.value as typeof user.status)}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
border: "none",
|
border: "none",
|
||||||
outline: "none",
|
|
||||||
":focus": {
|
|
||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
":focus": {
|
||||||
}),
|
outline: "none",
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
},
|
||||||
option: (styles, state) => ({
|
}),
|
||||||
...styles,
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
option: (styles, state) => ({
|
||||||
color: state.isFocused ? "black" : styles.color,
|
...styles,
|
||||||
}),
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
}}
|
color: state.isFocused ? "black" : styles.color,
|
||||||
isDisabled={disabled}
|
}),
|
||||||
/>
|
}}
|
||||||
</div>
|
isDisabled={disabled}
|
||||||
<div className="flex flex-col gap-3 w-full">
|
/>
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
</div>
|
||||||
<Select
|
<div className="flex flex-col gap-3 w-full">
|
||||||
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"
|
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||||
options={USER_TYPE_OPTIONS.filter((x) => {
|
<Select
|
||||||
if (x.value === "student")
|
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"
|
||||||
return checkAccess(
|
options={USER_TYPE_OPTIONS.filter((x) => {
|
||||||
loggedInUser,
|
if (x.value === "student")
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
return checkAccess(
|
||||||
permissions,
|
loggedInUser,
|
||||||
"editStudent",
|
["developer", "admin", "corporate", "mastercorporate"],
|
||||||
);
|
permissions,
|
||||||
|
"editStudent",
|
||||||
|
);
|
||||||
|
|
||||||
if (x.value === "teacher")
|
if (x.value === "teacher")
|
||||||
return checkAccess(
|
return checkAccess(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
["developer", "admin", "corporate", "mastercorporate"],
|
||||||
permissions,
|
permissions,
|
||||||
"editTeacher",
|
"editTeacher",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (x.value === "corporate")
|
if (x.value === "corporate")
|
||||||
return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate");
|
return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate");
|
||||||
|
|
||||||
return checkAccess(loggedInUser, ["developer", "admin"]);
|
return checkAccess(loggedInUser, ["developer", "admin"]);
|
||||||
})}
|
})}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
||||||
onChange={(value) => setType(value?.value as typeof user.type)}
|
onChange={(value) => setType(value?.value as typeof user.type)}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
border: "none",
|
border: "none",
|
||||||
outline: "none",
|
|
||||||
":focus": {
|
|
||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
":focus": {
|
||||||
}),
|
outline: "none",
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
},
|
||||||
option: (styles, state) => ({
|
}),
|
||||||
...styles,
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
option: (styles, state) => ({
|
||||||
color: state.isFocused ? "black" : styles.color,
|
...styles,
|
||||||
}),
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
}}
|
color: state.isFocused ? "black" : styles.color,
|
||||||
isDisabled={disabled}
|
}),
|
||||||
/>
|
}}
|
||||||
|
isDisabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="flex gap-4 justify-between mt-4 w-full">
|
<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 {
|
export interface CorporateInformation {
|
||||||
companyInformation: CompanyInformation;
|
|
||||||
monthlyDuration: number;
|
|
||||||
payment?: {
|
payment?: {
|
||||||
value: number;
|
value: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
@@ -85,11 +83,6 @@ export interface AgentInformation {
|
|||||||
companyArabName?: string;
|
companyArabName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyInformation {
|
|
||||||
name: string;
|
|
||||||
userAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DemographicInformation {
|
export interface DemographicInformation {
|
||||||
country: string;
|
country: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
|
|||||||
|
|
||||||
return (
|
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]})`}
|
{creatorUser && `(${USER_TYPE_LABELS[creatorUser?.type]})`}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -213,10 +213,7 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
|
|||||||
value={
|
value={
|
||||||
filteredCorporate
|
filteredCorporate
|
||||||
? {
|
? {
|
||||||
label: `${filteredCorporate?.type === "corporate"
|
label: `${filteredCorporate.name} (${USER_TYPE_LABELS[filteredCorporate?.type]})`,
|
||||||
? filteredCorporate.corporateInformation?.companyInformation?.name || filteredCorporate.name
|
|
||||||
: filteredCorporate.name
|
|
||||||
} (${USER_TYPE_LABELS[filteredCorporate?.type]})`,
|
|
||||||
value: filteredCorporate.id,
|
value: filteredCorporate.id,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -224,8 +221,7 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
|
|||||||
options={users
|
options={users
|
||||||
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
|
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
|
||||||
.map((x) => ({
|
.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,
|
value: x.id,
|
||||||
user: x,
|
user: x,
|
||||||
}))}
|
}))}
|
||||||
|
|||||||
@@ -29,419 +29,417 @@ const columnHelper = createColumnHelper<WithLabeledEntities<User>>();
|
|||||||
const searchFields = [["name"], ["email"], ["entities", ""]];
|
const searchFields = [["name"], ["email"], ["entities", ""]];
|
||||||
|
|
||||||
export default function UserList({
|
export default function UserList({
|
||||||
user,
|
user,
|
||||||
filters = [],
|
filters = [],
|
||||||
type,
|
type,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
filters?: ((user: User) => boolean)[];
|
filters?: ((user: User) => boolean)[];
|
||||||
type?: Type;
|
type?: Type;
|
||||||
renderHeader?: (total: number) => JSX.Element;
|
renderHeader?: (total: number) => JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
|
|
||||||
const { users, reload } = useEntitiesUsers(type)
|
const { users, reload } = useEntitiesUsers(type)
|
||||||
const { entities } = useEntities()
|
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 entitiesViewStudents = useAllowedEntities(user, entities, "view_students")
|
||||||
const entitiesEditStudents = useAllowedEntities(user, entities, "edit_students")
|
const entitiesEditStudents = useAllowedEntities(user, entities, "edit_students")
|
||||||
const entitiesDeleteStudents = useAllowedEntities(user, entities, "delete_students")
|
const entitiesDeleteStudents = useAllowedEntities(user, entities, "delete_students")
|
||||||
|
|
||||||
const entitiesViewTeachers = useAllowedEntities(user, entities, "view_teachers")
|
const entitiesViewTeachers = useAllowedEntities(user, entities, "view_teachers")
|
||||||
const entitiesEditTeachers = useAllowedEntities(user, entities, "edit_teachers")
|
const entitiesEditTeachers = useAllowedEntities(user, entities, "edit_teachers")
|
||||||
const entitiesDeleteTeachers = useAllowedEntities(user, entities, "delete_teachers")
|
const entitiesDeleteTeachers = useAllowedEntities(user, entities, "delete_teachers")
|
||||||
|
|
||||||
const entitiesViewCorporates = useAllowedEntities(user, entities, "view_corporates")
|
const entitiesViewCorporates = useAllowedEntities(user, entities, "view_corporates")
|
||||||
const entitiesEditCorporates = useAllowedEntities(user, entities, "edit_corporates")
|
const entitiesEditCorporates = useAllowedEntities(user, entities, "edit_corporates")
|
||||||
const entitiesDeleteCorporates = useAllowedEntities(user, entities, "delete_corporates")
|
const entitiesDeleteCorporates = useAllowedEntities(user, entities, "delete_corporates")
|
||||||
|
|
||||||
const entitiesViewMasterCorporates = useAllowedEntities(user, entities, "view_mastercorporates")
|
const entitiesViewMasterCorporates = useAllowedEntities(user, entities, "view_mastercorporates")
|
||||||
const entitiesEditMasterCorporates = useAllowedEntities(user, entities, "edit_mastercorporates")
|
const entitiesEditMasterCorporates = useAllowedEntities(user, entities, "edit_mastercorporates")
|
||||||
const entitiesDeleteMasterCorporates = useAllowedEntities(user, entities, "delete_mastercorporates")
|
const entitiesDeleteMasterCorporates = useAllowedEntities(user, entities, "delete_mastercorporates")
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const expirationDateColor = (date: Date) => {
|
const expirationDateColor = (date: Date) => {
|
||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through";
|
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(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
||||||
if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-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.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 (isAdmin) return true
|
||||||
if (u.id === user?.id) return false
|
if (u.id === user?.id) return false
|
||||||
|
|
||||||
switch (u.type) {
|
switch (u.type) {
|
||||||
case "student": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewStudents, 'id').includes(id))
|
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 "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 '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))
|
case 'mastercorporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewMasterCorporates, 'id').includes(id))
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
, [entitiesViewCorporates, entitiesViewMasterCorporates, entitiesViewStudents, entitiesViewTeachers, isAdmin, user?.id, users])
|
, [entitiesViewCorporates, entitiesViewMasterCorporates, entitiesViewStudents, entitiesViewTeachers, isAdmin, user?.id, users])
|
||||||
|
|
||||||
const displayUsers = useMemo(() =>
|
const displayUsers = useMemo(() =>
|
||||||
filters.length > 0 ? filters.reduce((d, f) => d.filter(f), allowedUsers) : allowedUsers,
|
filters.length > 0 ? filters.reduce((d, f) => d.filter(f), allowedUsers) : allowedUsers,
|
||||||
[filters, allowedUsers])
|
[filters, allowedUsers])
|
||||||
|
|
||||||
const deleteAccount = (user: User) => {
|
const deleteAccount = (user: User) => {
|
||||||
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User deleted successfully!");
|
toast.success("User deleted successfully!");
|
||||||
reload()
|
reload()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Something went wrong!", { toastId: "delete-error" });
|
toast.error("Something went wrong!", { toastId: "delete-error" });
|
||||||
})
|
})
|
||||||
.finally(reload);
|
.finally(reload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyAccount = (user: User) => {
|
const verifyAccount = (user: User) => {
|
||||||
axios
|
axios
|
||||||
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
||||||
...user,
|
...user,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User verified successfully!");
|
toast.success("User verified successfully!");
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Something went wrong!", { toastId: "update-error" });
|
toast.error("Something went wrong!", { toastId: "update-error" });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDisableAccount = (user: User) => {
|
const toggleDisableAccount = (user: User) => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${user.name
|
`Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${user.name
|
||||||
}'s account? This change is usually related to their payment state.`,
|
}'s account? This change is usually related to their payment state.`,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
||||||
...user,
|
...user,
|
||||||
status: user.status === "disabled" ? "active" : "disabled",
|
status: user.status === "disabled" ? "active" : "disabled",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`User ${user.status === "disabled" ? "enabled" : "disabled"} successfully!`);
|
toast.success(`User ${user.status === "disabled" ? "enabled" : "disabled"} successfully!`);
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Something went wrong!", { toastId: "update-error" });
|
toast.error("Something went wrong!", { toastId: "update-error" });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEditPermission = (type: Type) => {
|
const getEditPermission = (type: Type) => {
|
||||||
if (type === "student") return entitiesEditStudents
|
if (type === "student") return entitiesEditStudents
|
||||||
if (type === "teacher") return entitiesEditTeachers
|
if (type === "teacher") return entitiesEditTeachers
|
||||||
if (type === "corporate") return entitiesEditCorporates
|
if (type === "corporate") return entitiesEditCorporates
|
||||||
if (type === "mastercorporate") return entitiesEditMasterCorporates
|
if (type === "mastercorporate") return entitiesEditMasterCorporates
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDeletePermission = (type: Type) => {
|
const getDeletePermission = (type: Type) => {
|
||||||
if (type === "student") return entitiesDeleteStudents
|
if (type === "student") return entitiesDeleteStudents
|
||||||
if (type === "teacher") return entitiesDeleteTeachers
|
if (type === "teacher") return entitiesDeleteTeachers
|
||||||
if (type === "corporate") return entitiesDeleteCorporates
|
if (type === "corporate") return entitiesDeleteCorporates
|
||||||
if (type === "mastercorporate") return entitiesDeleteMasterCorporates
|
if (type === "mastercorporate") return entitiesDeleteMasterCorporates
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const canEditUser = (u: User) =>
|
const canEditUser = (u: User) =>
|
||||||
isAdmin || u.entities.some(e => mapBy(getEditPermission(u.type), 'id').includes(e.id))
|
isAdmin || u.entities.some(e => mapBy(getEditPermission(u.type), 'id').includes(e.id))
|
||||||
|
|
||||||
const canDeleteUser = (u: User) =>
|
const canDeleteUser = (u: User) =>
|
||||||
isAdmin || u.entities.some(e => mapBy(getDeletePermission(u.type), 'id').includes(e.id))
|
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 canEdit = canEditUser(row.original)
|
||||||
const canDelete = canDeleteUser(row.original)
|
const canDelete = canDeleteUser(row.original)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{!row.original.isVerified && canEdit && (
|
{!row.original.isVerified && canEdit && (
|
||||||
<div data-tip="Verify User" className="cursor-pointer tooltip" onClick={() => verifyAccount(row.original)}>
|
<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" />
|
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div
|
<div
|
||||||
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
|
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={() => toggleDisableAccount(row.original)}>
|
onClick={() => toggleDisableAccount(row.original)}>
|
||||||
{row.original.status === "disabled" ? (
|
{row.original.status === "disabled" ? (
|
||||||
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<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" />
|
<BsFillExclamationOctagonFill className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteAccount(row.original)}>
|
<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" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const demographicColumns = [
|
const demographicColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: "Name",
|
header: "Name",
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
canEditUser(row.original) &&
|
canEditUser(row.original) &&
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
canEditUser(row.original) ? setSelectedUser(row.original) : null
|
canEditUser(row.original) ? setSelectedUser(row.original) : null
|
||||||
}>
|
}>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.country", {
|
columnHelper.accessor("demographicInformation.country", {
|
||||||
header: "Country",
|
header: "Country",
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
info.getValue()
|
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())?.flag} ${countries[info.getValue() as unknown as keyof TCountries]?.name
|
||||||
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
|
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
|
||||||
: "N/A",
|
: "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
columnHelper.accessor("demographicInformation.phone", {
|
||||||
header: "Phone",
|
header: "Phone",
|
||||||
cell: (info) => info.getValue() || "N/A",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(
|
columnHelper.accessor(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
||||||
{
|
{
|
||||||
id: "employment",
|
id: "employment",
|
||||||
header: "Employment",
|
header: "Employment",
|
||||||
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
columnHelper.accessor("lastLogin", {
|
columnHelper.accessor("lastLogin", {
|
||||||
header: "Last Login",
|
header: "Last Login",
|
||||||
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.gender", {
|
columnHelper.accessor("demographicInformation.gender", {
|
||||||
header: "Gender",
|
header: "Gender",
|
||||||
cell: (info) => capitalize(info.getValue()) || "N/A",
|
cell: (info) => capitalize(info.getValue()) || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: actionColumn,
|
cell: actionColumn,
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: "Name",
|
header: "Name",
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
canEditUser(row.original) &&
|
canEditUser(row.original) &&
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
canEditUser(row.original) ? setSelectedUser(row.original) : null
|
canEditUser(row.original) ? setSelectedUser(row.original) : null
|
||||||
}>
|
}>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: "E-mail",
|
header: "E-mail",
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
canEditUser(row.original) &&
|
canEditUser(row.original) &&
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
"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)}>
|
onClick={() => (canEditUser(row.original) ? setSelectedUser(row.original) : null)}>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("type", {
|
columnHelper.accessor("type", {
|
||||||
header: "Type",
|
header: "Type",
|
||||||
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("studentID", {
|
columnHelper.accessor("studentID", {
|
||||||
header: "Student ID",
|
header: "Student ID",
|
||||||
cell: (info) => info.getValue() || "N/A",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("entities", {
|
columnHelper.accessor("entities", {
|
||||||
header: "Entities",
|
header: "Entities",
|
||||||
cell: ({ getValue }) => mapBy(getValue(), 'label').join(', '),
|
cell: ({ getValue }) => mapBy(getValue(), 'label').join(', '),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: "Expiration",
|
header: "Expiration",
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
||||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("isVerified", {
|
columnHelper.accessor("isVerified", {
|
||||||
header: "Verified",
|
header: "Verified",
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
|
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
info.getValue() && "!bg-mti-purple-light ",
|
info.getValue() && "!bg-mti-purple-light ",
|
||||||
)}>
|
)}>
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: actionColumn,
|
cell: actionColumn,
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const downloadExcel = (rows: WithLabeledEntities<User>[]) => {
|
const downloadExcel = (rows: WithLabeledEntities<User>[]) => {
|
||||||
const csv = exportListToExcel(rows);
|
const csv = exportListToExcel(rows);
|
||||||
|
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
const file = new Blob([csv], { type: "text/csv" });
|
const file = new Blob([csv], { type: "text/csv" });
|
||||||
element.href = URL.createObjectURL(file);
|
element.href = URL.createObjectURL(file);
|
||||||
element.download = "users.csv";
|
element.download = "users.csv";
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
element.click();
|
element.click();
|
||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewStudentFilter = (x: User) => x.type === "student";
|
const viewStudentFilter = (x: User) => x.type === "student";
|
||||||
const viewTeacherFilter = (x: User) => x.type === "teacher";
|
const viewTeacherFilter = (x: User) => x.type === "teacher";
|
||||||
const belongsToAdminFilter = (x: User) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], 'id').includes(id));
|
const belongsToAdminFilter = (x: User) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], 'id').includes(id));
|
||||||
|
|
||||||
const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x);
|
const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x);
|
||||||
const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x);
|
const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x);
|
||||||
|
|
||||||
const renderUserCard = (selectedUser: User) => {
|
const renderUserCard = (selectedUser: User) => {
|
||||||
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
||||||
const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin);
|
const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin);
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-8">
|
<div className="w-full flex flex-col gap-8">
|
||||||
<UserCard
|
<UserCard
|
||||||
maxUserAmount={
|
maxUserAmount={0}
|
||||||
user.type === "mastercorporate" ? (user.corporateInformation?.companyInformation?.userAmount || 0) - balance : undefined
|
loggedInUser={user}
|
||||||
}
|
onViewStudents={
|
||||||
loggedInUser={user}
|
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
|
||||||
onViewStudents={
|
? () => {
|
||||||
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
|
appendUserFilters({
|
||||||
? () => {
|
id: "view-students",
|
||||||
appendUserFilters({
|
filter: viewStudentFilter,
|
||||||
id: "view-students",
|
});
|
||||||
filter: viewStudentFilter,
|
appendUserFilters({
|
||||||
});
|
id: "belongs-to-admin",
|
||||||
appendUserFilters({
|
filter: belongsToAdminFilter,
|
||||||
id: "belongs-to-admin",
|
});
|
||||||
filter: belongsToAdminFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push("/users");
|
router.push("/users");
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
(selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0
|
(selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
filter: viewTeacherFilter,
|
filter: viewTeacherFilter,
|
||||||
});
|
});
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: belongsToAdminFilter,
|
filter: belongsToAdminFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push("/users");
|
router.push("/users");
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewCorporate={
|
onViewCorporate={
|
||||||
selectedUser.type === "teacher" || selectedUser.type === "student"
|
selectedUser.type === "teacher" || selectedUser.type === "student"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-corporate",
|
id: "view-corporate",
|
||||||
filter: (x: User) => x.type === "corporate",
|
filter: (x: User) => x.type === "corporate",
|
||||||
});
|
});
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: belongsToAdminFilter
|
filter: belongsToAdminFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push("/users");
|
router.push("/users");
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onClose={(shouldReload) => {
|
onClose={(shouldReload) => {
|
||||||
setSelectedUser(undefined);
|
setSelectedUser(undefined);
|
||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderHeader && renderHeader(displayUsers.length)}
|
{renderHeader && renderHeader(displayUsers.length)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
|
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
|
||||||
{selectedUser && renderUserCard(selectedUser)}
|
{selectedUser && renderUserCard(selectedUser)}
|
||||||
</Modal>
|
</Modal>
|
||||||
<Table<WithLabeledEntities<User>>
|
<Table<WithLabeledEntities<User>>
|
||||||
data={displayUsers}
|
data={displayUsers}
|
||||||
columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any}
|
columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any}
|
||||||
searchFields={searchFields}
|
searchFields={searchFields}
|
||||||
onDownload={downloadExcel}
|
onDownload={downloadExcel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,271 +13,267 @@ import moment from "moment";
|
|||||||
import useAcceptedTerms from "@/hooks/useAcceptedTerms";
|
import useAcceptedTerms from "@/hooks/useAcceptedTerms";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setIsLoading: (isLoading: boolean) => void;
|
setIsLoading: (isLoading: boolean) => void;
|
||||||
mutateUser: KeyedMutator<User>;
|
mutateUser: KeyedMutator<User>;
|
||||||
sendEmailVerification: typeof sendEmailVerification;
|
sendEmailVerification: typeof sendEmailVerification;
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableDurations = {
|
const availableDurations = {
|
||||||
"1_month": { label: "1 Month", number: 1 },
|
"1_month": { label: "1 Month", number: 1 },
|
||||||
"3_months": { label: "3 Months", number: 3 },
|
"3_months": { label: "3 Months", number: 3 },
|
||||||
"6_months": { label: "6 Months", number: 6 },
|
"6_months": { label: "6 Months", number: 6 },
|
||||||
"12_months": { label: "12 Months", number: 12 },
|
"12_months": { label: "12 Months", number: 12 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RegisterCorporate({
|
export default function RegisterCorporate({
|
||||||
isLoading,
|
isLoading,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
mutateUser,
|
mutateUser,
|
||||||
sendEmailVerification,
|
sendEmailVerification,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const [referralAgent, setReferralAgent] = useState<string | undefined>();
|
const [referralAgent, setReferralAgent] = useState<string | undefined>();
|
||||||
|
|
||||||
const [companyName, setCompanyName] = useState("");
|
const [companyName, setCompanyName] = useState("");
|
||||||
const [companyUsers, setCompanyUsers] = useState(0);
|
const [companyUsers, setCompanyUsers] = useState(0);
|
||||||
const [subscriptionDuration, setSubscriptionDuration] = useState(1);
|
const [subscriptionDuration, setSubscriptionDuration] = useState(1);
|
||||||
const {acceptedTerms, renderCheckbox} = useAcceptedTerms();
|
const { acceptedTerms, renderCheckbox } = useAcceptedTerms();
|
||||||
|
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
|
|
||||||
const onSuccess = () =>
|
const onSuccess = () =>
|
||||||
toast.success(
|
toast.success(
|
||||||
"An e-mail has been sent, please make sure to check your spam folder!",
|
"An e-mail has been sent, please make sure to check your spam folder!",
|
||||||
);
|
);
|
||||||
|
|
||||||
const onError = (e: Error) => {
|
const onError = (e: Error) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Something went wrong, please logout and re-login.", {
|
toast.error("Something went wrong, please logout and re-login.", {
|
||||||
toastId: "send-verify-error",
|
toastId: "send-verify-error",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = (e: any) => {
|
const register = (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (confirmPassword !== password) {
|
if (confirmPassword !== password) {
|
||||||
toast.error("Your passwords do not match!", {
|
toast.error("Your passwords do not match!", {
|
||||||
toastId: "password-not-match",
|
toastId: "password-not-match",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.post("/api/register", {
|
.post("/api/register", {
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
type: "corporate",
|
type: "corporate",
|
||||||
profilePicture: "/defaultAvatar.png",
|
profilePicture: "/defaultAvatar.png",
|
||||||
subscriptionExpirationDate: moment().subtract(1, "days").toISOString(),
|
subscriptionExpirationDate: moment().subtract(1, "days").toISOString(),
|
||||||
corporateInformation: {
|
corporateInformation: {
|
||||||
companyInformation: {
|
monthlyDuration: subscriptionDuration,
|
||||||
name: companyName,
|
referralAgent,
|
||||||
userAmount: companyUsers,
|
},
|
||||||
},
|
})
|
||||||
monthlyDuration: subscriptionDuration,
|
.then((response) => {
|
||||||
referralAgent,
|
mutateUser(response.data.user).then(() =>
|
||||||
},
|
sendEmailVerification(setIsLoading, onSuccess, onError),
|
||||||
})
|
);
|
||||||
.then((response) => {
|
})
|
||||||
mutateUser(response.data.user).then(() =>
|
.catch((error) => {
|
||||||
sendEmailVerification(setIsLoading, onSuccess, onError),
|
console.log(error.response.data);
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error.response.data);
|
|
||||||
|
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
toast.error("There is already a user with that e-mail!");
|
toast.error("There is already a user with that e-mail!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.response.status === 400) {
|
if (error.response.status === 400) {
|
||||||
toast.error("The provided code is invalid!");
|
toast.error("The provided code is invalid!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error("There was something wrong, please try again!");
|
toast.error("There was something wrong, please try again!");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="flex w-full flex-col items-center gap-4"
|
className="flex w-full flex-col items-center gap-4"
|
||||||
onSubmit={register}
|
onSubmit={register}
|
||||||
>
|
>
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
onChange={(e) => setName(e)}
|
onChange={(e) => setName(e)}
|
||||||
placeholder="Enter your name"
|
placeholder="Enter your name"
|
||||||
defaultValue={name}
|
defaultValue={name}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
onChange={(e) => setEmail(e.toLowerCase())}
|
onChange={(e) => setEmail(e.toLowerCase())}
|
||||||
placeholder="Enter email address"
|
placeholder="Enter email address"
|
||||||
defaultValue={email}
|
defaultValue={email}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
onChange={(e) => setPassword(e)}
|
onChange={(e) => setPassword(e)}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
defaultValue={password}
|
defaultValue={password}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
onChange={(e) => setConfirmPassword(e)}
|
onChange={(e) => setConfirmPassword(e)}
|
||||||
placeholder="Confirm your password"
|
placeholder="Confirm your password"
|
||||||
defaultValue={confirmPassword}
|
defaultValue={confirmPassword}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider className="!my-2 w-full" />
|
<Divider className="!my-2 w-full" />
|
||||||
|
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="companyName"
|
name="companyName"
|
||||||
onChange={(e) => setCompanyName(e)}
|
onChange={(e) => setCompanyName(e)}
|
||||||
placeholder="Corporate name"
|
placeholder="Corporate name"
|
||||||
label="Corporate name"
|
label="Corporate name"
|
||||||
defaultValue={companyName}
|
defaultValue={companyName}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
name="companyUsers"
|
name="companyUsers"
|
||||||
onChange={(e) => setCompanyUsers(parseInt(e))}
|
onChange={(e) => setCompanyUsers(parseInt(e))}
|
||||||
label="Number of users"
|
label="Number of users"
|
||||||
defaultValue={companyUsers}
|
defaultValue={companyUsers}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">
|
||||||
Referral *
|
Referral *
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<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"
|
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={[
|
options={[
|
||||||
{ value: "", label: "No referral" },
|
{ value: "", label: "No referral" },
|
||||||
...users
|
...users
|
||||||
.filter((u) => u.type === "agent")
|
.filter((u) => u.type === "agent")
|
||||||
.map((x) => ({ value: x.id, label: `${x.name} - ${x.email}` })),
|
.map((x) => ({ value: x.id, label: `${x.name} - ${x.email}` })),
|
||||||
]}
|
]}
|
||||||
defaultValue={{ value: "", label: "No referral" }}
|
defaultValue={{ value: "", label: "No referral" }}
|
||||||
onChange={(value) => setReferralAgent(value?.value)}
|
onChange={(value) => setReferralAgent(value?.value)}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
border: "none",
|
border: "none",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
":focus": {
|
":focus": {
|
||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused
|
||||||
? "#D5D9F0"
|
? "#D5D9F0"
|
||||||
: state.isSelected
|
: state.isSelected
|
||||||
? "#7872BF"
|
? "#7872BF"
|
||||||
: "white",
|
: "white",
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">
|
||||||
Subscription Duration *
|
Subscription Duration *
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<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"
|
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) => ({
|
options={Object.keys(availableDurations).map((value) => ({
|
||||||
value,
|
value,
|
||||||
label:
|
label:
|
||||||
availableDurations[value as keyof typeof availableDurations]
|
availableDurations[value as keyof typeof availableDurations]
|
||||||
.label,
|
.label,
|
||||||
}))}
|
}))}
|
||||||
defaultValue={{
|
defaultValue={{
|
||||||
value: "1_month",
|
value: "1_month",
|
||||||
label: availableDurations["1_month"].label,
|
label: availableDurations["1_month"].label,
|
||||||
}}
|
}}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setSubscriptionDuration(
|
setSubscriptionDuration(
|
||||||
value
|
value
|
||||||
? availableDurations[
|
? availableDurations[
|
||||||
value.value as keyof typeof availableDurations
|
value.value as keyof typeof availableDurations
|
||||||
].number
|
].number
|
||||||
: 1,
|
: 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
border: "none",
|
border: "none",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
":focus": {
|
":focus": {
|
||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused
|
||||||
? "#D5D9F0"
|
? "#D5D9F0"
|
||||||
: state.isSelected
|
: state.isSelected
|
||||||
? "#7872BF"
|
? "#7872BF"
|
||||||
: "white",
|
: "white",
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-start gap-4">
|
<div className="flex w-full flex-col items-start gap-4">
|
||||||
{renderCheckbox()}
|
{renderCheckbox()}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="w-full lg:mt-8"
|
className="w-full lg:mt-8"
|
||||||
color="purple"
|
color="purple"
|
||||||
disabled={
|
disabled={
|
||||||
isLoading ||
|
isLoading ||
|
||||||
!email ||
|
!email ||
|
||||||
!name ||
|
!name ||
|
||||||
!password ||
|
!password ||
|
||||||
!confirmPassword ||
|
!confirmPassword ||
|
||||||
password !== confirmPassword ||
|
password !== confirmPassword ||
|
||||||
!companyName ||
|
!companyName ||
|
||||||
companyUsers <= 0
|
companyUsers <= 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Create account
|
Create account
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import Layout from "@/components/High/Layout";
|
|||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import usePackages from "@/hooks/usePackages";
|
import usePackages from "@/hooks/usePackages";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize} from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useInvites from "@/hooks/useInvites";
|
import useInvites from "@/hooks/useInvites";
|
||||||
import {BsArrowRepeat} from "react-icons/bs";
|
import { BsArrowRepeat } from "react-icons/bs";
|
||||||
import InviteCard from "@/components/Medium/InviteCard";
|
import InviteCard from "@/components/Medium/InviteCard";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {ToastContainer} from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import useDiscounts from "@/hooks/useDiscounts";
|
import useDiscounts from "@/hooks/useDiscounts";
|
||||||
import PaymobPayment from "@/components/PaymobPayment";
|
import PaymobPayment from "@/components/PaymobPayment";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -22,17 +22,17 @@ interface Props {
|
|||||||
reload: () => void;
|
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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [appliedDiscount, setAppliedDiscount] = useState(0);
|
const [appliedDiscount, setAppliedDiscount] = useState(0);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {packages} = usePackages();
|
const { packages } = usePackages();
|
||||||
const {discounts} = useDiscounts();
|
const { discounts } = useDiscounts();
|
||||||
const {users} = useUsers();
|
const { users } = useUsers();
|
||||||
const {groups} = useGroups({});
|
const { groups } = useGroups({});
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
const { invites, isLoading: isInvitesLoading, reload: reloadInvites } = useInvites({ to: user?.id });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`));
|
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">
|
<div className="mb-2 flex flex-col items-start">
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
||||||
<span className="text-xl font-semibold">
|
<span className="text-xl font-semibold">
|
||||||
EnCoach - {user.corporateInformation?.monthlyDuration} Months
|
EnCoach - {12} Months
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-start gap-2">
|
<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}
|
setIsPaymentLoading={setIsLoading}
|
||||||
currency={user.corporateInformation.payment.currency}
|
currency={user.corporateInformation.payment.currency}
|
||||||
price={user.corporateInformation.payment.value}
|
price={user.corporateInformation.payment.value}
|
||||||
duration={user.corporateInformation.monthlyDuration}
|
duration={12}
|
||||||
duration_unit="months"
|
duration_unit="months"
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -196,8 +196,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
|
|||||||
<span>This includes:</span>
|
<span>This includes:</span>
|
||||||
<ul className="flex flex-col items-start text-sm">
|
<ul className="flex flex-col items-start text-sm">
|
||||||
<li>
|
<li>
|
||||||
- Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers
|
- Allow a total of 0 students and teachers to use EnCoach
|
||||||
to use EnCoach
|
|
||||||
</li>
|
</li>
|
||||||
<li>- Train their abilities for the IELTS exam</li>
|
<li>- Train their abilities for the IELTS exam</li>
|
||||||
<li>- Gain insights into your students' weaknesses and strengths</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 { getSpecificUsers, getUser } from "@/utils/users.be";
|
||||||
import { getUserName } from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
interface GroupScoreSummaryHelper {
|
interface GroupScoreSummaryHelper {
|
||||||
score: [number, number];
|
score: [number, number];
|
||||||
label: string;
|
label: string;
|
||||||
sessions: string[];
|
sessions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssignmentData {
|
interface AssignmentData {
|
||||||
id: string;
|
id: string;
|
||||||
assigner: string;
|
assigner: string;
|
||||||
assignees: string[];
|
assignees: string[];
|
||||||
results: any;
|
results: any;
|
||||||
exams: { module: Module }[];
|
exams: { module: Module }[];
|
||||||
startDate: string;
|
startDate: string;
|
||||||
excel: {
|
excel: {
|
||||||
path: string;
|
path: string;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
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);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// if (req.method === "GET") return get(req, res);
|
// if (req.method === "GET") return get(req, res);
|
||||||
if (req.method === "POST") return await post(req, res);
|
if (req.method === "POST") return await post(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
function logWorksheetData(worksheet: any) {
|
function logWorksheetData(worksheet: any) {
|
||||||
worksheet.eachRow((row: any, rowNumber: number) => {
|
worksheet.eachRow((row: any, rowNumber: number) => {
|
||||||
console.log(`Row ${rowNumber}:`);
|
console.log(`Row ${rowNumber}:`);
|
||||||
row.eachCell((cell: any, colNumber: number) => {
|
row.eachCell((cell: any, colNumber: number) => {
|
||||||
console.log(` Cell ${colNumber}: ${cell.value}`);
|
console.log(` Cell ${colNumber}: ${cell.value}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function commonExcel({
|
function commonExcel({
|
||||||
data,
|
data,
|
||||||
userName,
|
userName,
|
||||||
users,
|
users,
|
||||||
sectionName,
|
sectionName,
|
||||||
customTable,
|
customTable,
|
||||||
customTableHeaders,
|
customTableHeaders,
|
||||||
renderCustomTableData,
|
renderCustomTableData,
|
||||||
}: {
|
}: {
|
||||||
data: AssignmentData;
|
data: AssignmentData;
|
||||||
userName: string;
|
userName: string;
|
||||||
users: User[];
|
users: User[];
|
||||||
sectionName: string;
|
sectionName: string;
|
||||||
customTable: string[][];
|
customTable: string[][];
|
||||||
customTableHeaders: string[];
|
customTableHeaders: string[];
|
||||||
renderCustomTableData: (data: any) => 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
|
const assigneesData = data.assignees
|
||||||
.map((assignee: string) => {
|
.map((assignee: string) => {
|
||||||
const userStats = allStats.filter((s: any) => s.user === assignee);
|
const userStats = allStats.filter((s: any) => s.user === assignee);
|
||||||
const dates = userStats.map((s: any) => moment(s.date));
|
const dates = userStats.map((s: any) => moment(s.date));
|
||||||
const user = users.find((u) => u.id === assignee);
|
const user = users.find((u) => u.id === assignee);
|
||||||
return {
|
return {
|
||||||
userId: assignee,
|
userId: assignee,
|
||||||
// added some default values in case the user is not found
|
// added some default values in case the user is not found
|
||||||
// could it be possible to have an assigned user deleted from the database?
|
// could it be possible to have an assigned user deleted from the database?
|
||||||
user: user || {
|
user: user || {
|
||||||
name: "Unknown",
|
name: "Unknown",
|
||||||
email: "Unknown",
|
email: "Unknown",
|
||||||
demographicInformation: { passportId: "Unknown", gender: "Unknown" },
|
demographicInformation: { passportId: "Unknown", gender: "Unknown" },
|
||||||
},
|
},
|
||||||
...userStats.reduce(
|
...userStats.reduce(
|
||||||
(acc: any, curr: any) => {
|
(acc: any, curr: any) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
correct: acc.correct + curr.score.correct,
|
correct: acc.correct + curr.score.correct,
|
||||||
missing: acc.missing + curr.score.missing,
|
missing: acc.missing + curr.score.missing,
|
||||||
total: acc.total + curr.score.total,
|
total: acc.total + curr.score.total,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ correct: 0, missing: 0, total: 0 }
|
{ correct: 0, missing: 0, total: 0 }
|
||||||
),
|
),
|
||||||
firstDate: moment.min(...dates),
|
firstDate: moment.min(...dates),
|
||||||
lastDate: moment.max(...dates),
|
lastDate: moment.max(...dates),
|
||||||
stats: userStats,
|
stats: userStats,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.correct - a.correct);
|
.sort((a, b) => b.correct - a.correct);
|
||||||
|
|
||||||
const results = assigneesData.map((r: any) => r.correct);
|
const results = assigneesData.map((r: any) => r.correct);
|
||||||
const highestScore = Math.max(...results);
|
const highestScore = Math.max(...results);
|
||||||
const lowestScore = Math.min(...results);
|
const lowestScore = Math.min(...results);
|
||||||
const averageScore = results.reduce((a, b) => a + b, 0) / results.length;
|
const averageScore = results.reduce((a, b) => a + b, 0) / results.length;
|
||||||
const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate));
|
const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate));
|
||||||
const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate));
|
const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate));
|
||||||
|
|
||||||
const firstSectionData = [
|
const firstSectionData = [
|
||||||
{
|
{
|
||||||
label: sectionName,
|
label: sectionName,
|
||||||
value: userName,
|
value: userName,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Report Download date :",
|
label: "Report Download date :",
|
||||||
value: moment().format("DD/MM/YYYY"),
|
value: moment().format("DD/MM/YYYY"),
|
||||||
},
|
},
|
||||||
{ label: "Test Information :", value: data.name },
|
{ label: "Test Information :", value: data.name },
|
||||||
{
|
{
|
||||||
label: "Date of Test :",
|
label: "Date of Test :",
|
||||||
value: moment(data.startDate).format("DD/MM/YYYY"),
|
value: moment(data.startDate).format("DD/MM/YYYY"),
|
||||||
},
|
},
|
||||||
{ label: "Number of Candidates :", value: data.assignees.length },
|
{ label: "Number of Candidates :", value: data.assignees.length },
|
||||||
{ label: "Highest score :", value: highestScore },
|
{ label: "Highest score :", value: highestScore },
|
||||||
{ label: "Lowest score :", value: lowestScore },
|
{ label: "Lowest score :", value: lowestScore },
|
||||||
{ label: "Average score :", value: averageScore },
|
{ label: "Average score :", value: averageScore },
|
||||||
{ label: "", value: "" },
|
{ label: "", value: "" },
|
||||||
{
|
{
|
||||||
label: "Date and time of First submission :",
|
label: "Date and time of First submission :",
|
||||||
value: firstDate.format("DD/MM/YYYY"),
|
value: firstDate.format("DD/MM/YYYY"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Date and time of Last submission :",
|
label: "Date and time of Last submission :",
|
||||||
value: lastDate.format("DD/MM/YYYY"),
|
value: lastDate.format("DD/MM/YYYY"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create a new workbook and add a worksheet
|
// Create a new workbook and add a worksheet
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
const worksheet = workbook.addWorksheet("Report Data");
|
const worksheet = workbook.addWorksheet("Report Data");
|
||||||
|
|
||||||
// Populate the worksheet with the data
|
// Populate the worksheet with the data
|
||||||
firstSectionData.forEach(({ label, value }, index) => {
|
firstSectionData.forEach(({ label, value }, index) => {
|
||||||
worksheet.getCell(`A${index + 1}`).value = label; // First column (labels)
|
worksheet.getCell(`A${index + 1}`).value = label; // First column (labels)
|
||||||
worksheet.getCell(`B${index + 1}`).value = value; // Second column (values)
|
worksheet.getCell(`B${index + 1}`).value = value; // Second column (values)
|
||||||
});
|
});
|
||||||
|
|
||||||
// added empty arrays to force row spacings
|
// added empty arrays to force row spacings
|
||||||
const customTableAndLine = [[], ...customTable, []];
|
const customTableAndLine = [[], ...customTable, []];
|
||||||
customTableAndLine.forEach((row: string[], index) => {
|
customTableAndLine.forEach((row: string[], index) => {
|
||||||
worksheet.addRow(row);
|
worksheet.addRow(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define the static part of the headers (before "Test Sections")
|
// Define the static part of the headers (before "Test Sections")
|
||||||
const staticHeaders = [
|
const staticHeaders = [
|
||||||
"Sr N",
|
"Sr N",
|
||||||
"Candidate ID",
|
"Candidate ID",
|
||||||
"First and Last Name",
|
"First and Last Name",
|
||||||
"Passport/ID",
|
"Passport/ID",
|
||||||
"Email ID",
|
"Email ID",
|
||||||
"Gender",
|
"Gender",
|
||||||
...customTableHeaders,
|
...customTableHeaders,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define additional headers after "Test Sections"
|
// Define additional headers after "Test Sections"
|
||||||
const additionalHeaders = ["Time Spent", "Date", "Score"];
|
const additionalHeaders = ["Time Spent", "Date", "Score"];
|
||||||
|
|
||||||
// Calculate the dynamic columns based on the testSectionsArray
|
// Calculate the dynamic columns based on the testSectionsArray
|
||||||
const testSectionHeaders = uniqueExercises.map(
|
const testSectionHeaders = uniqueExercises.map(
|
||||||
(section, index) => `Part ${index + 1}`
|
(section, index) => `Part ${index + 1}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableColumnHeadersFirstPart = [
|
const tableColumnHeadersFirstPart = [
|
||||||
...staticHeaders,
|
...staticHeaders,
|
||||||
...uniqueExercises.map((a) => "Test Sections"),
|
...uniqueExercises.map((a) => "Test Sections"),
|
||||||
];
|
];
|
||||||
// Add the main header row, merging static columns and "Test Sections"
|
// Add the main header row, merging static columns and "Test Sections"
|
||||||
const tableColumnHeaders = [
|
const tableColumnHeaders = [
|
||||||
...tableColumnHeadersFirstPart,
|
...tableColumnHeadersFirstPart,
|
||||||
...additionalHeaders,
|
...additionalHeaders,
|
||||||
];
|
];
|
||||||
worksheet.addRow(tableColumnHeaders);
|
worksheet.addRow(tableColumnHeaders);
|
||||||
|
|
||||||
// 1 headers rows
|
// 1 headers rows
|
||||||
const startIndexTable =
|
const startIndexTable =
|
||||||
firstSectionData.length + customTableAndLine.length + 1;
|
firstSectionData.length + customTableAndLine.length + 1;
|
||||||
|
|
||||||
// // Merge "Test Sections" over dynamic number of columns
|
// // Merge "Test Sections" over dynamic number of columns
|
||||||
// const tableColumns = staticHeaders.length + numberOfTestSections;
|
// const tableColumns = staticHeaders.length + numberOfTestSections;
|
||||||
|
|
||||||
// K10:M12 = 10,11,12,13
|
// K10:M12 = 10,11,12,13
|
||||||
// horizontally group Test Sections
|
// horizontally group Test Sections
|
||||||
|
|
||||||
// if there are test section headers to even merge:
|
// if there are test section headers to even merge:
|
||||||
if (testSectionHeaders.length > 1) {
|
if (testSectionHeaders.length > 1) {
|
||||||
worksheet.mergeCells(
|
worksheet.mergeCells(
|
||||||
startIndexTable,
|
startIndexTable,
|
||||||
staticHeaders.length + 1,
|
staticHeaders.length + 1,
|
||||||
startIndexTable,
|
startIndexTable,
|
||||||
tableColumnHeadersFirstPart.length
|
tableColumnHeadersFirstPart.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the dynamic second and third header rows for test sections and sub-columns
|
// Add the dynamic second and third header rows for test sections and sub-columns
|
||||||
worksheet.addRow([
|
worksheet.addRow([
|
||||||
...Array(staticHeaders.length).fill(""),
|
...Array(staticHeaders.length).fill(""),
|
||||||
...testSectionHeaders,
|
...testSectionHeaders,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
]);
|
]);
|
||||||
worksheet.addRow([
|
worksheet.addRow([
|
||||||
...Array(staticHeaders.length).fill(""),
|
...Array(staticHeaders.length).fill(""),
|
||||||
...uniqueExercises.map(() => "Grammar & Vocabulary"),
|
...uniqueExercises.map(() => "Grammar & Vocabulary"),
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
]);
|
]);
|
||||||
worksheet.addRow([
|
worksheet.addRow([
|
||||||
...Array(staticHeaders.length).fill(""),
|
...Array(staticHeaders.length).fill(""),
|
||||||
...uniqueExercises.map(
|
...uniqueExercises.map(
|
||||||
(exercise) => allStats.find((s: any) => s.exercise === exercise).type
|
(exercise) => allStats.find((s: any) => s.exercise === exercise).type
|
||||||
),
|
),
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// vertically group based on the part, exercise and type
|
// vertically group based on the part, exercise and type
|
||||||
staticHeaders.forEach((header, index) => {
|
staticHeaders.forEach((header, index) => {
|
||||||
worksheet.mergeCells(
|
worksheet.mergeCells(
|
||||||
startIndexTable,
|
startIndexTable,
|
||||||
index + 1,
|
index + 1,
|
||||||
startIndexTable + 3,
|
startIndexTable + 3,
|
||||||
index + 1
|
index + 1
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
assigneesData.forEach((data, index) => {
|
assigneesData.forEach((data, index) => {
|
||||||
worksheet.addRow([
|
worksheet.addRow([
|
||||||
index + 1,
|
index + 1,
|
||||||
data.userId,
|
data.userId,
|
||||||
data.user.name,
|
data.user.name,
|
||||||
data.user.demographicInformation?.passportId,
|
data.user.demographicInformation?.passportId,
|
||||||
data.user.email,
|
data.user.email,
|
||||||
data.user.demographicInformation?.gender,
|
data.user.demographicInformation?.gender,
|
||||||
...renderCustomTableData(data),
|
...renderCustomTableData(data),
|
||||||
...uniqueExercises.map((exercise) => {
|
...uniqueExercises.map((exercise) => {
|
||||||
const score = data.stats.find(
|
const score = data.stats.find(
|
||||||
(s: any) => s.exercise === exercise && s.user === data.userId
|
(s: any) => s.exercise === exercise && s.user === data.userId
|
||||||
).score;
|
).score;
|
||||||
return `${score.correct}/${score.total}`;
|
return `${score.correct}/${score.total}`;
|
||||||
}),
|
}),
|
||||||
`${Math.ceil(
|
`${Math.ceil(
|
||||||
data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) /
|
data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) /
|
||||||
60
|
60
|
||||||
)} minutes`,
|
)} minutes`,
|
||||||
data.lastDate.format("DD/MM/YYYY HH:mm"),
|
data.lastDate.format("DD/MM/YYYY HH:mm"),
|
||||||
data.correct,
|
data.correct,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
worksheet.addRow([""]);
|
worksheet.addRow([""]);
|
||||||
worksheet.addRow([""]);
|
worksheet.addRow([""]);
|
||||||
|
|
||||||
for (let i = 0; i < tableColumnHeaders.length; i++) {
|
for (let i = 0; i < tableColumnHeaders.length; i++) {
|
||||||
worksheet.getColumn(i + 1).width = 30;
|
worksheet.getColumn(i + 1).width = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply styles to the headers
|
// Apply styles to the headers
|
||||||
[startIndexTable].forEach((rowNumber) => {
|
[startIndexTable].forEach((rowNumber) => {
|
||||||
worksheet.getRow(rowNumber).eachCell((cell) => {
|
worksheet.getRow(rowNumber).eachCell((cell) => {
|
||||||
if (cell.value) {
|
if (cell.value) {
|
||||||
cell.fill = {
|
cell.fill = {
|
||||||
type: "pattern",
|
type: "pattern",
|
||||||
pattern: "solid",
|
pattern: "solid",
|
||||||
fgColor: { argb: "FFBFBFBF" }, // Grey color for headers
|
fgColor: { argb: "FFBFBFBF" }, // Grey color for headers
|
||||||
};
|
};
|
||||||
cell.font = { bold: true };
|
cell.font = { bold: true };
|
||||||
cell.alignment = { vertical: "middle", horizontal: "center" };
|
cell.alignment = { vertical: "middle", horizontal: "center" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
worksheet.addRow(["Printed by: Confidential Information"]);
|
worksheet.addRow(["Printed by: Confidential Information"]);
|
||||||
worksheet.addRow(["info@encoach.com"]);
|
worksheet.addRow(["info@encoach.com"]);
|
||||||
|
|
||||||
// Convert workbook to Buffer (Node.js) or Blob (Browser)
|
// Convert workbook to Buffer (Node.js) or Blob (Browser)
|
||||||
return workbook.xlsx.writeBuffer();
|
return workbook.xlsx.writeBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function corporateAssignment(
|
function corporateAssignment(
|
||||||
user: CorporateUser,
|
user: CorporateUser,
|
||||||
data: AssignmentData,
|
data: AssignmentData,
|
||||||
users: User[]
|
users: User[]
|
||||||
) {
|
) {
|
||||||
return commonExcel({
|
return commonExcel({
|
||||||
data,
|
data,
|
||||||
userName: user.corporateInformation?.companyInformation?.name || "",
|
userName: user.name || "",
|
||||||
users,
|
users,
|
||||||
sectionName: "Corporate Name :",
|
sectionName: "Corporate Name :",
|
||||||
customTable: [],
|
customTable: [],
|
||||||
customTableHeaders: [],
|
customTableHeaders: [],
|
||||||
renderCustomTableData: () => [],
|
renderCustomTableData: () => [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mastercorporateAssignment(
|
async function mastercorporateAssignment(
|
||||||
user: MasterCorporateUser,
|
user: MasterCorporateUser,
|
||||||
data: AssignmentData,
|
data: AssignmentData,
|
||||||
users: User[]
|
users: User[]
|
||||||
) {
|
) {
|
||||||
const userGroups = await getStudentGroupsForUsersWithoutAdmin(
|
const userGroups = await getStudentGroupsForUsersWithoutAdmin(
|
||||||
user.id,
|
user.id,
|
||||||
data.assignees
|
data.assignees
|
||||||
);
|
);
|
||||||
const adminUsers = [...new Set(userGroups.map((g) => g.admin))];
|
const adminUsers = [...new Set(userGroups.map((g) => g.admin))];
|
||||||
|
|
||||||
const userGroupsParticipants = userGroups.flatMap((g) => g.participants);
|
const userGroupsParticipants = userGroups.flatMap((g) => g.participants);
|
||||||
const adminsData = await getSpecificUsers(adminUsers);
|
const adminsData = await getSpecificUsers(adminUsers);
|
||||||
const companiesData = adminsData.map((user) => {
|
const companiesData = adminsData.map((user) => {
|
||||||
const name = getUserName(user);
|
const name = getUserName(user);
|
||||||
const users = userGroupsParticipants.filter((p) =>
|
const users = userGroupsParticipants.filter((p) =>
|
||||||
data.assignees.includes(p)
|
data.assignees.includes(p)
|
||||||
);
|
);
|
||||||
|
|
||||||
const stats = data.results
|
const stats = data.results
|
||||||
.flatMap((r: any) => r.stats)
|
.flatMap((r: any) => r.stats)
|
||||||
.filter((s: any) => users.includes(s.user));
|
.filter((s: any) => users.includes(s.user));
|
||||||
const correct = stats.reduce(
|
const correct = stats.reduce(
|
||||||
(acc: number, s: any) => acc + s.score.correct,
|
(acc: number, s: any) => acc + s.score.correct,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const total = stats.reduce(
|
const total = stats.reduce(
|
||||||
(acc: number, curr: any) => acc + curr.score.total,
|
(acc: number, curr: any) => acc + curr.score.total,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
correct,
|
correct,
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const customTable = [
|
const customTable = [
|
||||||
...companiesData,
|
...companiesData,
|
||||||
{
|
{
|
||||||
name: "Total",
|
name: "Total",
|
||||||
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
|
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
|
||||||
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
|
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
|
||||||
},
|
},
|
||||||
].map((c) => [c.name, `${c.correct}/${c.total}`]);
|
].map((c) => [c.name, `${c.correct}/${c.total}`]);
|
||||||
|
|
||||||
const customTableHeaders = [
|
const customTableHeaders = [
|
||||||
{ name: "Corporate", helper: (data: any) => data.user.corporateName },
|
{ name: "Corporate", helper: (data: any) => data.user.corporateName },
|
||||||
];
|
];
|
||||||
return commonExcel({
|
return commonExcel({
|
||||||
data,
|
data,
|
||||||
userName: user.corporateInformation?.companyInformation?.name || "",
|
userName: user.name || "",
|
||||||
users: users.map((u) => {
|
users: users.map((u) => {
|
||||||
const userGroup = userGroups.find((g) => g.participants.includes(u.id));
|
const userGroup = userGroups.find((g) => g.participants.includes(u.id));
|
||||||
const admin = adminsData.find((a) => a.id === userGroup?.admin);
|
const admin = adminsData.find((a) => a.id === userGroup?.admin);
|
||||||
return {
|
return {
|
||||||
...u,
|
...u,
|
||||||
corporateName: getUserName(admin),
|
corporateName: getUserName(admin),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
sectionName: "Master Corporate Name :",
|
sectionName: "Master Corporate Name :",
|
||||||
customTable: [["Corporate Summary"], ...customTable],
|
customTable: [["Corporate Summary"], ...customTable],
|
||||||
customTableHeaders: customTableHeaders.map((h) => h.name),
|
customTableHeaders: customTableHeaders.map((h) => h.name),
|
||||||
renderCustomTableData: (data) =>
|
renderCustomTableData: (data) =>
|
||||||
customTableHeaders.map((h) => h.helper(data)),
|
customTableHeaders.map((h) => h.helper(data)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// verify if it's a logged user that is trying to export
|
// verify if it's a logged user that is trying to export
|
||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const { id } = req.query as { id: string };
|
const { id } = req.query as { id: string };
|
||||||
|
|
||||||
const assignment = await db.collection("assignments").findOne<AssignmentData>({ id: id });
|
const assignment = await db.collection("assignments").findOne<AssignmentData>({ id: id });
|
||||||
if (!assignment) {
|
if (!assignment) {
|
||||||
res.status(400).end();
|
res.status(400).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (
|
// if (
|
||||||
// data.excel &&
|
// data.excel &&
|
||||||
// data.excel.path &&
|
// data.excel.path &&
|
||||||
// data.excel.version === process.env.EXCEL_VERSION
|
// data.excel.version === process.env.EXCEL_VERSION
|
||||||
// ) {
|
// ) {
|
||||||
// // if it does, return the excel url
|
// // if it does, return the excel url
|
||||||
// const fileRef = ref(storage, data.excel.path);
|
// const fileRef = ref(storage, data.excel.path);
|
||||||
// const url = await getDownloadURL(fileRef);
|
// const url = await getDownloadURL(fileRef);
|
||||||
// res.status(200).end(url);
|
// res.status(200).end(url);
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const objectIds = assignment.assignees.map(id => id);
|
const objectIds = assignment.assignees.map(id => id);
|
||||||
|
|
||||||
const users = await db.collection("users").find<User>({
|
const users = await db.collection("users").find<User>({
|
||||||
id: { $in: objectIds }
|
id: { $in: objectIds }
|
||||||
}).toArray();
|
}).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);
|
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||||
if (user && users) {
|
if (user && users) {
|
||||||
// generate the file ref for storage
|
// generate the file ref for storage
|
||||||
const fileName = `${Date.now().toString()}.xlsx`;
|
const fileName = `${Date.now().toString()}.xlsx`;
|
||||||
const refName = `assignment_report/${fileName}`;
|
const refName = `assignment_report/${fileName}`;
|
||||||
const fileRef = ref(storage, refName);
|
const fileRef = ref(storage, refName);
|
||||||
|
|
||||||
const getExcelFn = () => {
|
const getExcelFn = () => {
|
||||||
switch (user.type) {
|
switch (user.type) {
|
||||||
case "teacher":
|
case "teacher":
|
||||||
case "corporate":
|
case "corporate":
|
||||||
return corporateAssignment(user as CorporateUser, assignment, users);
|
return corporateAssignment(user as CorporateUser, assignment, users);
|
||||||
case "mastercorporate":
|
case "mastercorporate":
|
||||||
return mastercorporateAssignment(
|
return mastercorporateAssignment(
|
||||||
user as MasterCorporateUser,
|
user as MasterCorporateUser,
|
||||||
assignment,
|
assignment,
|
||||||
users
|
users
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid user type");
|
throw new Error("Invalid user type");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const buffer = await getExcelFn();
|
const buffer = await getExcelFn();
|
||||||
|
|
||||||
// upload the pdf to storage
|
// upload the pdf to storage
|
||||||
await uploadBytes(fileRef, buffer, {
|
await uploadBytes(fileRef, buffer, {
|
||||||
contentType:
|
contentType:
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the stats entries with the pdf url to prevent duplication
|
// update the stats entries with the pdf url to prevent duplication
|
||||||
await db.collection("assignments").updateOne(
|
await db.collection("assignments").updateOne(
|
||||||
{ id: assignment.id },
|
{ id: assignment.id },
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
excel: {
|
excel: {
|
||||||
path: refName,
|
path: refName,
|
||||||
version: process.env.EXCEL_VERSION,
|
version: process.env.EXCEL_VERSION,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = await getDownloadURL(fileRef);
|
const url = await getDownloadURL(fileRef);
|
||||||
res.status(200).end(url);
|
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) {
|
if (groups.length > 0) {
|
||||||
const admins = await db.collection("users")
|
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();
|
.toArray();
|
||||||
|
|
||||||
const adminData = admins.find((a) => a.corporateInformation?.companyInformation?.name);
|
const adminData = admins.find((a) => a.name);
|
||||||
if (adminData) {
|
if (adminData) {
|
||||||
return adminData.corporateInformation.companyInformation.name;
|
return adminData.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assignerUser.type === "corporate" && assignerUser.corporateInformation?.companyInformation?.name) {
|
return assignerUser.type
|
||||||
return assignerUser.corporateInformation.companyInformation.name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -373,14 +371,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
await db.collection("assignments").updateOne(
|
await db.collection("assignments").updateOne(
|
||||||
{ id: data.id },
|
{ id: data.id },
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
pdf: {
|
pdf: {
|
||||||
path: refName,
|
path: refName,
|
||||||
version: process.env.PDF_VERSION,
|
version: process.env.PDF_VERSION,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = await getDownloadURL(fileRef);
|
const url = await getDownloadURL(fileRef);
|
||||||
res.status(200).end(url);
|
res.status(200).end(url);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
if (req.session.user.type === "corporate") {
|
if (req.session.user.type === "corporate") {
|
||||||
const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length;
|
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) {
|
if (totalCodes > allowedCodes) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
@@ -127,7 +127,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
// upsert: true -> if it doesnt exist insert
|
// upsert: true -> if it doesnt exist insert
|
||||||
await db.collection("codes").updateOne(
|
await db.collection("codes").updateOne(
|
||||||
{ id: code },
|
{ id: code },
|
||||||
{ $set: { id: code, ...codeInformation} },
|
{ $set: { id: code, ...codeInformation } },
|
||||||
{ upsert: true }
|
{ upsert: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ import { getEntityWithRoles } from "@/utils/entities.be";
|
|||||||
import { findBy } from "@/utils";
|
import { findBy } from "@/utils";
|
||||||
|
|
||||||
const DEFAULT_DESIRED_LEVELS = {
|
const DEFAULT_DESIRED_LEVELS = {
|
||||||
reading: 9,
|
reading: 9,
|
||||||
listening: 9,
|
listening: 9,
|
||||||
writing: 9,
|
writing: 9,
|
||||||
speaking: 9,
|
speaking: 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_LEVELS = {
|
const DEFAULT_LEVELS = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
speaking: 0,
|
speaking: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const auth = getAuth(app);
|
const auth = getAuth(app);
|
||||||
@@ -33,99 +33,94 @@ const db = client.db(process.env.MONGODB_DB);
|
|||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const maker = req.session.user;
|
const maker = req.session.user;
|
||||||
if (!maker) {
|
if (!maker) {
|
||||||
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
|
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 {
|
const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as {
|
||||||
email: string;
|
email: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
passport_id: string;
|
passport_id: string;
|
||||||
type: string;
|
type: string;
|
||||||
entity: string;
|
entity: string;
|
||||||
groupID?: string;
|
groupID?: string;
|
||||||
corporate?: string;
|
corporate?: string;
|
||||||
expiryDate: null | Date;
|
expiryDate: null | Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
// cleaning data
|
// cleaning data
|
||||||
delete req.body.passport_id;
|
delete req.body.passport_id;
|
||||||
delete req.body.groupID;
|
delete req.body.groupID;
|
||||||
delete req.body.expiryDate;
|
delete req.body.expiryDate;
|
||||||
delete req.body.password;
|
delete req.body.password;
|
||||||
delete req.body.corporate;
|
delete req.body.corporate;
|
||||||
delete req.body.entity
|
delete req.body.entity
|
||||||
|
|
||||||
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
|
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
|
||||||
.then(async (userCredentials) => {
|
.then(async (userCredentials) => {
|
||||||
const userId = userCredentials.user.uid;
|
const userId = userCredentials.user.uid;
|
||||||
|
|
||||||
const entityWithRole = await getEntityWithRoles(entity)
|
const entityWithRole = await getEntityWithRoles(entity)
|
||||||
const defaultRole = findBy(entityWithRole?.roles || [], "isDefault", true)
|
const defaultRole = findBy(entityWithRole?.roles || [], "isDefault", true)
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
...req.body,
|
...req.body,
|
||||||
bio: "",
|
bio: "",
|
||||||
id: userId,
|
id: userId,
|
||||||
type: type,
|
type: type,
|
||||||
focus: "academic",
|
focus: "academic",
|
||||||
status: "active",
|
status: "active",
|
||||||
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
||||||
profilePicture: "/defaultAvatar.png",
|
profilePicture: "/defaultAvatar.png",
|
||||||
levels: DEFAULT_LEVELS,
|
levels: DEFAULT_LEVELS,
|
||||||
isFirstLogin: false,
|
isFirstLogin: false,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
registrationDate: new Date(),
|
registrationDate: new Date(),
|
||||||
entities: [{ id: entity, role: defaultRole?.id || "" }],
|
entities: [{ id: entity, role: defaultRole?.id || "" }],
|
||||||
subscriptionExpirationDate: expiryDate || null,
|
subscriptionExpirationDate: expiryDate || null,
|
||||||
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
|
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
|
||||||
? {
|
? {
|
||||||
corporateInformation: {
|
corporateInformation: {},
|
||||||
companyInformation: {
|
}
|
||||||
name: maker.corporateInformation?.companyInformation?.name || "N/A",
|
: {}),
|
||||||
userAmount: 0,
|
};
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const uid = new ShortUniqueId();
|
const uid = new ShortUniqueId();
|
||||||
const code = uid.randomUUID(6);
|
const code = uid.randomUUID(6);
|
||||||
|
|
||||||
await db.collection("users").insertOne(user);
|
await db.collection("users").insertOne(user);
|
||||||
await db.collection("codes").insertOne({
|
await db.collection("codes").insertOne({
|
||||||
code,
|
code,
|
||||||
creator: maker.id,
|
creator: maker.id,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
type,
|
type,
|
||||||
creationDate: new Date(),
|
creationDate: new Date(),
|
||||||
userId,
|
userId,
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
...(!!passport_id ? { passport_id } : {}),
|
...(!!passport_id ? { passport_id } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!groupID) {
|
if (!!groupID) {
|
||||||
const group = await getGroup(groupID);
|
const group = await getGroup(groupID);
|
||||||
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
|
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Returning - ${email}`);
|
console.log(`Returning - ${email}`);
|
||||||
return res.status(200).json({ ok: true });
|
return res.status(200).json({ ok: true });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.code.includes("email-already-in-use")) return res.status(403).json({ error, message: "E-mail is already in the platform." });
|
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(`Failing - ${email}`);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return res.status(401).json({ error });
|
return res.status(401).json({ error });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (admin) {
|
if (admin) {
|
||||||
return {
|
return {
|
||||||
...d,
|
...d,
|
||||||
corporate: admin.corporateInformation?.companyInformation?.name,
|
corporate: admin.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import AssignmentCard from "@/dashboards/AssignmentCard";
|
import AssignmentCard from "@/components/AssignmentCard";
|
||||||
import AssignmentView from "@/dashboards/AssignmentView";
|
import AssignmentView from "@/components/AssignmentView";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
@@ -25,183 +25,183 @@ import Head from "next/head";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
BsClipboard2Data,
|
BsClipboard2Data,
|
||||||
BsClock,
|
BsClock,
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsPaperclip,
|
BsPaperclip,
|
||||||
BsPencilSquare,
|
BsPencilSquare,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
BsPeopleFill,
|
BsPeopleFill,
|
||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPersonFillGear,
|
BsPersonFillGear,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
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 entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
const entities = await getEntitiesWithRoles(entityIDS);
|
||||||
const users = await filterAllowedUsers(user, entities)
|
const users = await filterAllowedUsers(user, entities)
|
||||||
|
|
||||||
const assignments = await getEntitiesAssignments(entityIDS);
|
const assignments = await getEntitiesAssignments(entityIDS);
|
||||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||||
const groups = await getGroupsByEntities(entityIDS);
|
const groups = await getGroupsByEntities(entityIDS);
|
||||||
|
|
||||||
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
focus: students.find((u) => u.id === s.user)?.focus,
|
focus: students.find((u) => u.id === s.user)?.focus,
|
||||||
score: s.score,
|
score: s.score,
|
||||||
module: s.module,
|
module: s.module,
|
||||||
}))
|
}))
|
||||||
.filter((f) => !!f.focus);
|
.filter((f) => !!f.focus);
|
||||||
const bandScores = formattedStats.map((s) => ({
|
const bandScores = formattedStats.map((s) => ({
|
||||||
module: s.module,
|
module: s.module,
|
||||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: { [key in Module]: number } = {
|
const levels: { [key in Module]: number } = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
speaking: 0,
|
speaking: 0,
|
||||||
level: 0,
|
level: 0,
|
||||||
};
|
};
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
return calculateAverageLevel(levels);
|
return calculateAverageLevel(levels);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
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">
|
<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" />
|
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>EnCoach</title>
|
<title>EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/users?type=student")}
|
onClick={() => router.push("/users?type=student")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
label="Students"
|
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"
|
|
||||||
value={students.length}
|
value={students.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
onClick={() => router.push("/users?type=teacher")}
|
||||||
label="Expiration Date"
|
Icon={BsPencilSquare}
|
||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
label="Teachers"
|
||||||
color="rose"
|
value={teachers.length}
|
||||||
/>
|
color="purple"
|
||||||
<IconCard
|
/>
|
||||||
Icon={BsEnvelopePaper}
|
<IconCard
|
||||||
className="col-span-2"
|
onClick={() => router.push("/classrooms")}
|
||||||
onClick={() => router.push("/assignments")}
|
Icon={BsPeople}
|
||||||
label="Assignments"
|
label="Classrooms"
|
||||||
value={assignments.filter((a) => !a.archived).length}
|
value={groups.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
</section>
|
<IconCard Icon={BsPeopleFill}
|
||||||
</div>
|
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">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
title="Latest Students"
|
title="Latest Students"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
title="Latest Teachers"
|
title="Latest Teachers"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
title="Highest level students"
|
title="Highest level students"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={
|
users={
|
||||||
students
|
students
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
@@ -28,139 +28,139 @@ import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
|||||||
import { filterAllowedUsers } from "@/utils/users.be";
|
import { filterAllowedUsers } from "@/utils/users.be";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
const entities = await getEntitiesWithRoles(entityIDS);
|
||||||
const users = await filterAllowedUsers(user, entities)
|
const users = await filterAllowedUsers(user, entities)
|
||||||
|
|
||||||
const assignments = await getEntitiesAssignments(entityIDS);
|
const assignments = await getEntitiesAssignments(entityIDS);
|
||||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||||
const groups = await getGroupsByEntities(entityIDS);
|
const groups = await getGroupsByEntities(entityIDS);
|
||||||
|
|
||||||
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
focus: students.find((u) => u.id === s.user)?.focus,
|
focus: students.find((u) => u.id === s.user)?.focus,
|
||||||
score: s.score,
|
score: s.score,
|
||||||
module: s.module,
|
module: s.module,
|
||||||
}))
|
}))
|
||||||
.filter((f) => !!f.focus);
|
.filter((f) => !!f.focus);
|
||||||
const bandScores = formattedStats.map((s) => ({
|
const bandScores = formattedStats.map((s) => ({
|
||||||
module: s.module,
|
module: s.module,
|
||||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: { [key in Module]: number } = {
|
const levels: { [key in Module]: number } = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
speaking: 0,
|
speaking: 0,
|
||||||
level: 0,
|
level: 0,
|
||||||
};
|
};
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
return calculateAverageLevel(levels);
|
return calculateAverageLevel(levels);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
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">
|
<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" />
|
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>EnCoach</title>
|
<title>EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
onClick={() => router.push("/users?type=student")}
|
onClick={() => router.push("/users?type=student")}
|
||||||
label="Students"
|
label="Students"
|
||||||
value={students.length}
|
value={students.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/classrooms")}
|
onClick={() => router.push("/classrooms")}
|
||||||
Icon={BsPeople}
|
Icon={BsPeople}
|
||||||
label="Classrooms"
|
label="Classrooms"
|
||||||
value={groups.length}
|
value={groups.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").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={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsEnvelopePaper}
|
Icon={BsEnvelopePaper}
|
||||||
onClick={() => router.push("/assignments")}
|
onClick={() => router.push("/assignments")}
|
||||||
label="Assignments"
|
label="Assignments"
|
||||||
value={assignments.filter((a) => !a.archived).length}
|
value={assignments.filter((a) => !a.archived).length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
title="Latest Students"
|
title="Latest Students"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
title="Highest level students"
|
title="Highest level students"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={
|
users={
|
||||||
students
|
students
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import usePayments from "@/hooks/usePayments";
|
import usePayments from "@/hooks/usePayments";
|
||||||
import usePaypalPayments from "@/hooks/usePaypalPayments";
|
import usePaypalPayments from "@/hooks/usePaypalPayments";
|
||||||
import {Payment, PaypalPayment} from "@/interfaces/paypal";
|
import { Payment, PaypalPayment } from "@/interfaces/paypal";
|
||||||
import {CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable} from "@tanstack/react-table";
|
import { CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable } from "@tanstack/react-table";
|
||||||
import {CURRENCIES} from "@/resources/paypal";
|
import { CURRENCIES } from "@/resources/paypal";
|
||||||
import {BsTrash} from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useEffect, useState, useMemo} from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import {AgentUser, CorporateUser, User} from "@/interfaces/user";
|
import { AgentUser, CorporateUser, User } from "@/interfaces/user";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -26,15 +26,15 @@ import Input from "@/components/Low/Input";
|
|||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import PaymentAssetManager from "@/components/PaymentAssetManager";
|
import PaymentAssetManager from "@/components/PaymentAssetManager";
|
||||||
import {toFixedNumber} from "@/utils/number";
|
import { toFixedNumber } from "@/utils/number";
|
||||||
import {CSVLink} from "react-csv";
|
import { CSVLink } from "react-csv";
|
||||||
import {Tab} from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { redirect } from "@/utils";
|
import { redirect } from "@/utils";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
@@ -43,18 +43,18 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user},
|
props: { user },
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Payment>();
|
const columnHelper = createColumnHelper<Payment>();
|
||||||
const paypalColumnHelper = createColumnHelper<PaypalPaymentWithUserData>();
|
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 [corporate, setCorporate] = useState<CorporateUser>();
|
||||||
const [date, setDate] = useState<Date>(new Date());
|
const [date, setDate] = useState<Date>(new Date());
|
||||||
|
|
||||||
const {users} = useUsers();
|
const { users } = useUsers();
|
||||||
|
|
||||||
const price = corporate?.corporateInformation?.payment?.value || 0;
|
const price = corporate?.corporateInformation?.payment?.value || 0;
|
||||||
const commission = corporate?.corporateInformation?.payment?.commission || 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) => ({
|
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
||||||
value: user.id,
|
value: user.id,
|
||||||
meta: user,
|
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)}
|
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -129,10 +129,10 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Price *</label>
|
<label className="font-normal text-base text-mti-gray-dim">Price *</label>
|
||||||
<div className="w-full grid grid-cols-5 gap-2">
|
<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
|
<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"
|
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,
|
value: currency,
|
||||||
label,
|
label,
|
||||||
}))}
|
}))}
|
||||||
@@ -140,14 +140,14 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
value: currency || "EUR",
|
value: currency || "EUR",
|
||||||
label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
|
label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
|
||||||
}}
|
}}
|
||||||
onChange={() => {}}
|
onChange={() => { }}
|
||||||
value={{
|
value={{
|
||||||
value: currency || "EUR",
|
value: currency || "EUR",
|
||||||
label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
|
label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
|
||||||
}}
|
}}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -171,7 +171,7 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
<div className="flex gap-4 w-full">
|
<div className="flex gap-4 w-full">
|
||||||
<div className="flex flex-col w-full gap-3">
|
<div className="flex flex-col w-full gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Commission *</label>
|
<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>
|
||||||
<div className="flex flex-col w-full gap-3">
|
<div className="flex flex-col w-full gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Commission Value*</label>
|
<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 [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
|
||||||
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
|
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
|
||||||
const [isCreatingPayment, setIsCreatingPayment] = useState(false);
|
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 [displayPayments, setDisplayPayments] = useState<Payment[]>([]);
|
||||||
|
|
||||||
const [corporate, setCorporate] = useState<User>();
|
const [corporate, setCorporate] = useState<User>();
|
||||||
const [agent, setAgent] = useState<User>();
|
const [agent, setAgent] = useState<User>();
|
||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const { user } = useUser({ redirectTo: "/login" });
|
||||||
const {users, reload: reloadUsers} = useUsers();
|
const { users, reload: reloadUsers } = useUsers();
|
||||||
const {payments: originalPayments, reload: reloadPayment} = usePayments();
|
const { payments: originalPayments, reload: reloadPayment } = usePayments();
|
||||||
const {payments: paypalPayments, reload: reloadPaypalPayment} = usePaypalPayments();
|
const { payments: paypalPayments, reload: reloadPaypalPayment } = usePaypalPayments();
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
||||||
|
|
||||||
@@ -331,11 +331,11 @@ export default function PaymentRecord() {
|
|||||||
...(!agent
|
...(!agent
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "agent-filter",
|
id: "agent-filter",
|
||||||
filter: (p: Payment) => p.agent === agent.id,
|
filter: (p: Payment) => p.agent === agent.id,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}, [agent]);
|
}, [agent]);
|
||||||
|
|
||||||
@@ -345,18 +345,18 @@ export default function PaymentRecord() {
|
|||||||
...(!corporate
|
...(!corporate
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "corporate-filter",
|
id: "corporate-filter",
|
||||||
filter: (p: Payment) => p.corporate === corporate.id,
|
filter: (p: Payment) => p.corporate === corporate.id,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}, [corporate]);
|
}, [corporate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilters((prev) => [
|
setFilters((prev) => [
|
||||||
...prev.filter((x) => x.id !== "paid"),
|
...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]);
|
}, [paid]);
|
||||||
|
|
||||||
@@ -366,11 +366,11 @@ export default function PaymentRecord() {
|
|||||||
...(typeof commissionTransfer !== "boolean"
|
...(typeof commissionTransfer !== "boolean"
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "commissionTransfer",
|
id: "commissionTransfer",
|
||||||
filter: (p: Payment) => !p.commissionTransfer === commissionTransfer,
|
filter: (p: Payment) => !p.commissionTransfer === commissionTransfer,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}, [commissionTransfer]);
|
}, [commissionTransfer]);
|
||||||
|
|
||||||
@@ -380,11 +380,11 @@ export default function PaymentRecord() {
|
|||||||
...(typeof corporateTransfer !== "boolean"
|
...(typeof corporateTransfer !== "boolean"
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "corporateTransfer",
|
id: "corporateTransfer",
|
||||||
filter: (p: Payment) => !p.corporateTransfer === corporateTransfer,
|
filter: (p: Payment) => !p.corporateTransfer === corporateTransfer,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}, [corporateTransfer]);
|
}, [corporateTransfer]);
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ export default function PaymentRecord() {
|
|||||||
|
|
||||||
const updatePayment = (payment: Payment, key: string, value: any) => {
|
const updatePayment = (payment: Payment, key: string, value: any) => {
|
||||||
axios
|
axios
|
||||||
.patch(`api/payments/${payment.id}`, {...payment, [key]: value})
|
.patch(`api/payments/${payment.id}`, { ...payment, [key]: value })
|
||||||
.then(() => toast.success("Updated the payment"))
|
.then(() => toast.success("Updated the payment"))
|
||||||
.finally(reload);
|
.finally(reload);
|
||||||
};
|
};
|
||||||
@@ -540,7 +540,7 @@ export default function PaymentRecord() {
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case "agentCommission": {
|
case "agentCommission": {
|
||||||
const value = info.getValue();
|
const value = info.getValue();
|
||||||
return {value: `${value}%`};
|
return { value: `${value}%` };
|
||||||
}
|
}
|
||||||
case "agent": {
|
case "agent": {
|
||||||
const user = users.find((x) => x.id === info.row.original.agent) as AgentUser;
|
const user = users.find((x) => x.id === info.row.original.agent) as AgentUser;
|
||||||
@@ -553,18 +553,18 @@ export default function PaymentRecord() {
|
|||||||
case "amount": {
|
case "amount": {
|
||||||
const value = info.getValue();
|
const value = info.getValue();
|
||||||
const numberValue = toFixedNumber(value, 2);
|
const numberValue = toFixedNumber(value, 2);
|
||||||
return {value: numberValue};
|
return { value: numberValue };
|
||||||
}
|
}
|
||||||
case "date": {
|
case "date": {
|
||||||
const value = info.getValue();
|
const value = info.getValue();
|
||||||
return {value: moment(value).format("DD/MM/YYYY")};
|
return { value: moment(value).format("DD/MM/YYYY") };
|
||||||
}
|
}
|
||||||
case "corporate": {
|
case "corporate": {
|
||||||
const specificValue = info.row.original.corporate;
|
const specificValue = info.row.original.corporate;
|
||||||
const user = users.find((x) => x.id === specificValue) as CorporateUser;
|
const user = users.find((x) => x.id === specificValue) as CorporateUser;
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
value: user?.corporateInformation.companyInformation.name || user?.name,
|
value: user?.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "currency": {
|
case "currency": {
|
||||||
@@ -576,7 +576,7 @@ export default function PaymentRecord() {
|
|||||||
case "corporateId":
|
case "corporateId":
|
||||||
default: {
|
default: {
|
||||||
const value = info.getValue();
|
const value = info.getValue();
|
||||||
return {value};
|
return { value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -588,7 +588,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Country Manager",
|
header: "Country Manager",
|
||||||
id: "agent",
|
id: "agent",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {user, value} = columHelperValue(info.column.id, info);
|
const { user, value } = columHelperValue(info.column.id, info);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -604,7 +604,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Commission",
|
header: "Commission",
|
||||||
id: "agentCommission",
|
id: "agentCommission",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return <>{value}</>;
|
return <>{value}</>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -612,7 +612,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Commission Value",
|
header: "Commission Value",
|
||||||
id: "agentValue",
|
id: "agentValue",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
const finalValue = `${value} ${info.row.original.currency}`;
|
const finalValue = `${value} ${info.row.original.currency}`;
|
||||||
return <span>{finalValue}</span>;
|
return <span>{finalValue}</span>;
|
||||||
},
|
},
|
||||||
@@ -626,7 +626,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Corporate ID",
|
header: "Corporate ID",
|
||||||
id: "corporateId",
|
id: "corporateId",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -634,7 +634,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Corporate",
|
header: "Corporate",
|
||||||
id: "corporate",
|
id: "corporate",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {user, value} = columHelperValue(info.column.id, info);
|
const { user, value } = columHelperValue(info.column.id, info);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -650,7 +650,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Date",
|
header: "Date",
|
||||||
id: "date",
|
id: "date",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return <span>{value}</span>;
|
return <span>{value}</span>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -658,7 +658,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Amount",
|
header: "Amount",
|
||||||
id: "amount",
|
id: "amount",
|
||||||
cell: (info) => {
|
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 currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label;
|
||||||
const finalValue = `${value} ${currency}`;
|
const finalValue = `${value} ${currency}`;
|
||||||
return <span>{finalValue}</span>;
|
return <span>{finalValue}</span>;
|
||||||
@@ -669,7 +669,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Paid",
|
header: "Paid",
|
||||||
id: "isPaid",
|
id: "isPaid",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -691,7 +691,7 @@ export default function PaymentRecord() {
|
|||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({row}: {row: {original: Payment}}) => {
|
cell: ({ row }: { row: { original: Payment } }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{user?.type !== "agent" && (
|
{user?.type !== "agent" && (
|
||||||
@@ -720,7 +720,7 @@ export default function PaymentRecord() {
|
|||||||
})
|
})
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const user = users.find((x) => x.id === p.userId) as User;
|
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],
|
[paypalPayments, users, startDatePaymob, endDatePaymob],
|
||||||
);
|
);
|
||||||
@@ -730,7 +730,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Order ID",
|
header: "Order ID",
|
||||||
id: "orderId",
|
id: "orderId",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return <span>{value}</span>;
|
return <span>{value}</span>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -738,7 +738,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Status",
|
header: "Status",
|
||||||
id: "status",
|
id: "status",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return <span>{value}</span>;
|
return <span>{value}</span>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -746,7 +746,7 @@ export default function PaymentRecord() {
|
|||||||
header: "User Name",
|
header: "User Name",
|
||||||
id: "name",
|
id: "name",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return <span>{value}</span>;
|
return <span>{value}</span>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -754,7 +754,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Email",
|
header: "Email",
|
||||||
id: "email",
|
id: "email",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
return <span>{value}</span>;
|
return <span>{value}</span>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -762,7 +762,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Amount",
|
header: "Amount",
|
||||||
id: "value",
|
id: "value",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const {value} = columHelperValue(info.column.id, info);
|
const { value } = columHelperValue(info.column.id, info);
|
||||||
const finalValue = `${value} ${info.row.original.currency}`;
|
const finalValue = `${value} ${info.row.original.currency}`;
|
||||||
return <span>{finalValue}</span>;
|
return <span>{finalValue}</span>;
|
||||||
},
|
},
|
||||||
@@ -771,7 +771,7 @@ export default function PaymentRecord() {
|
|||||||
header: "Date",
|
header: "Date",
|
||||||
id: "createdAt",
|
id: "createdAt",
|
||||||
cell: (info) => {
|
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>;
|
return <span>{moment(value).format("DD/MM/YYYY")}</span>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -779,13 +779,13 @@ export default function PaymentRecord() {
|
|||||||
header: "Expiration Date",
|
header: "Expiration Date",
|
||||||
id: "subscriptionExpirationDate",
|
id: "subscriptionExpirationDate",
|
||||||
cell: (info) => {
|
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>;
|
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({
|
const paypalTable = useReactTable({
|
||||||
data: filteredRows.sort((a, b) => moment(b.createdAt).diff(moment(a.createdAt), "second")),
|
data: filteredRows.sort((a, b) => moment(b.createdAt).diff(moment(a.createdAt), "second")),
|
||||||
@@ -809,7 +809,7 @@ export default function PaymentRecord() {
|
|||||||
}}
|
}}
|
||||||
user={selectedCorporateUser}
|
user={selectedCorporateUser}
|
||||||
disabled
|
disabled
|
||||||
disabledFields={{countryManager: true}}
|
disabledFields={{ countryManager: true }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -859,7 +859,7 @@ export default function PaymentRecord() {
|
|||||||
return [...accm, ...data];
|
return [...accm, ...data];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {rows} = currentTable.getRowModel();
|
const { rows } = currentTable.getRowModel();
|
||||||
|
|
||||||
const finalColumns = [
|
const finalColumns = [
|
||||||
...columns,
|
...columns,
|
||||||
@@ -872,8 +872,8 @@ export default function PaymentRecord() {
|
|||||||
return {
|
return {
|
||||||
columns: finalColumns,
|
columns: finalColumns,
|
||||||
rows: rows.map((row) => {
|
rows: rows.map((row) => {
|
||||||
return finalColumns.reduce((accm, {key}) => {
|
return finalColumns.reduce((accm, { key }) => {
|
||||||
const {value} = columHelperValue(key, {
|
const { value } = columHelperValue(key, {
|
||||||
row,
|
row,
|
||||||
getValue: () => row.getValue(key),
|
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>) => (
|
const renderTable = (table: Table<any>) => (
|
||||||
<table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full">
|
<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.Group selectedIndex={selectedIndex} onChange={setSelectedIndex}>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({ selected }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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>
|
</Tab>
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
{checkAccess(user, ["developer", "admin"]) && (
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({ selected }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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) => ({
|
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
||||||
value: user.id,
|
value: user.id,
|
||||||
meta: user,
|
meta: user,
|
||||||
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`,
|
label: `${user.name} - ${user.email}`,
|
||||||
}))}
|
}))}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
user.type === "corporate"
|
user.type === "corporate"
|
||||||
? {
|
? {
|
||||||
value: user.id,
|
value: user.id,
|
||||||
meta: user,
|
meta: user,
|
||||||
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${
|
label: `${user.name} - ${user.email}`,
|
||||||
user.email
|
}
|
||||||
}`,
|
|
||||||
}
|
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
isDisabled={user.type === "corporate"}
|
isDisabled={user.type === "corporate"}
|
||||||
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -1049,15 +1047,15 @@ export default function PaymentRecord() {
|
|||||||
value={
|
value={
|
||||||
agent
|
agent
|
||||||
? {
|
? {
|
||||||
value: agent?.id,
|
value: agent?.id,
|
||||||
label: `${agent.name} - ${agent.email}`,
|
label: `${agent.name} - ${agent.email}`,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -1092,7 +1090,7 @@ export default function PaymentRecord() {
|
|||||||
}}
|
}}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -1149,7 +1147,7 @@ export default function PaymentRecord() {
|
|||||||
}}
|
}}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -1183,7 +1181,7 @@ export default function PaymentRecord() {
|
|||||||
}}
|
}}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react";
|
import { ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from "react";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {ErrorMessage} from "@/constants/errors";
|
import { ErrorMessage } from "@/constants/errors";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
CorporateUser,
|
CorporateUser,
|
||||||
@@ -23,32 +23,32 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
} from "@/interfaces/user";
|
} from "@/interfaces/user";
|
||||||
import CountrySelect from "@/components/Low/CountrySelect";
|
import CountrySelect from "@/components/Low/CountrySelect";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {BsCamera, BsQuestionCircleFill} from "react-icons/bs";
|
import { BsCamera, BsQuestionCircleFill } from "react-icons/bs";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {convertBase64, redirect} from "@/utils";
|
import { convertBase64, redirect } from "@/utils";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import GenderInput from "@/components/High/GenderInput";
|
import GenderInput from "@/components/High/GenderInput";
|
||||||
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
||||||
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import {InstructorGender} from "@/interfaces/exam";
|
import { InstructorGender } from "@/interfaces/exam";
|
||||||
import {capitalize} from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import TopicModal from "@/components/Medium/TopicModal";
|
import TopicModal from "@/components/Medium/TopicModal";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||||
import {getParticipantGroups, getUserCorporate} from "@/utils/groups.be";
|
import { getParticipantGroups, getUserCorporate } from "@/utils/groups.be";
|
||||||
import {InferGetServerSidePropsType} from "next";
|
import { InferGetServerSidePropsType } from "next";
|
||||||
import {getUsers} from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import { requestUser } from "@/utils/api";
|
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)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
@@ -72,9 +72,9 @@ interface Props {
|
|||||||
linkedCorporate?: CorporateUser | MasterCorporateUser;
|
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 [bio, setBio] = useState(user.bio || "");
|
||||||
const [name, setName] = useState(user.name || "");
|
const [name, setName] = useState(user.name || "");
|
||||||
const [email, setEmail] = useState(user.email || "");
|
const [email, setEmail] = useState(user.email || "");
|
||||||
@@ -182,21 +182,21 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
|
|||||||
passport_id,
|
passport_id,
|
||||||
timezone,
|
timezone,
|
||||||
},
|
},
|
||||||
...(user.type === "corporate" ? {corporateInformation} : {}),
|
...(user.type === "corporate" ? { corporateInformation } : {}),
|
||||||
...(user.type === "agent"
|
...(user.type === "agent"
|
||||||
? {
|
? {
|
||||||
agentInformation: {
|
agentInformation: {
|
||||||
companyName,
|
companyName,
|
||||||
commercialRegistration,
|
commercialRegistration,
|
||||||
arabName,
|
arabName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
toast.success("Your profile has been updated!");
|
toast.success("Your profile has been updated!");
|
||||||
mutateUser((response.data as {user: User}).user);
|
mutateUser((response.data as { user: User }).user);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
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>
|
<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()}>
|
<form className="flex flex-col items-center gap-6 w-full" onSubmit={(e) => e.preventDefault()}>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
{user.type !== "corporate" && user.type !== "mastercorporate" ? (
|
{user.type !== "corporate" && user.type !== "mastercorporate" && (
|
||||||
<Input
|
<Input
|
||||||
label={user.type === "agent" ? "English name" : "Name"}
|
label={user.type === "agent" ? "English name" : "Name"}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -257,25 +257,6 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
|
|||||||
defaultValue={name}
|
defaultValue={name}
|
||||||
required
|
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" && (
|
{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>
|
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
|
||||||
<ModuleLevelSelector
|
<ModuleLevelSelector
|
||||||
levels={desiredLevels}
|
levels={desiredLevels}
|
||||||
setLevels={setDesiredLevels as Dispatch<SetStateAction<{[key in Module]: number}>>}
|
setLevels={setDesiredLevels as Dispatch<SetStateAction<{ [key in Module]: number }>>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<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)}
|
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
|
||||||
options={[
|
options={[
|
||||||
{value: "male", label: "Male"},
|
{ value: "male", label: "Male" },
|
||||||
{value: "female", label: "Female"},
|
{ value: "female", label: "Female" },
|
||||||
{value: "varied", label: "Varied"},
|
{ value: "varied", label: "Varied" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -461,15 +442,6 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
|
|||||||
{user.type === "corporate" && (
|
{user.type === "corporate" && (
|
||||||
<>
|
<>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
name="companyUsers"
|
|
||||||
onChange={() => null}
|
|
||||||
label="Number of users"
|
|
||||||
defaultValue={user.corporateInformation?.companyInformation.userAmount}
|
|
||||||
disabled
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="pricing"
|
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[]}) {
|
export default function Home(props: { linkedCorporate?: CorporateUser | MasterCorporateUser; groups: Group[]; users: User[] }) {
|
||||||
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
const { user, mutateUser } = useUser({ redirectTo: "/login" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
|||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Modal from "@/components/Modal";
|
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 { BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill } from "react-icons/bs";
|
||||||
import UserCreator from "./(admin)/UserCreator";
|
import UserCreator from "./(admin)/UserCreator";
|
||||||
import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";
|
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",
|
student: "Student",
|
||||||
teacher: "Teacher",
|
teacher: "Teacher",
|
||||||
corporate: "Corporate",
|
corporate: "Corporate",
|
||||||
@@ -19,8 +19,8 @@ export function isAgentUser(user: User): user is AgentUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
|
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 (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 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));
|
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 "";
|
if (belongingGroupsAdmins.length === 0) return "";
|
||||||
|
|
||||||
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
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[]) {
|
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 db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
const addEntityToGroupPipeline = [
|
const addEntityToGroupPipeline = [
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: "entities",
|
from: "entities",
|
||||||
localField: "entity",
|
localField: "entity",
|
||||||
foreignField: "id",
|
foreignField: "id",
|
||||||
as: "entity"
|
as: "entity"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$addFields: {
|
$addFields: {
|
||||||
entity: { $arrayElemAt: ["$entity", 0] }
|
entity: { $arrayElemAt: ["$entity", 0] }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$addFields: {
|
$addFields: {
|
||||||
entity: {
|
entity: {
|
||||||
$cond: {
|
$cond: {
|
||||||
if: { $isArray: "$entity" },
|
if: { $isArray: "$entity" },
|
||||||
then: undefined,
|
then: undefined,
|
||||||
else: "$entity"
|
else: "$entity"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
|
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
|
||||||
const corporate = await db.collection("users").findOne<User>({ id: corporateID });
|
const corporate = await db.collection("users").findOne<User>({ id: corporateID });
|
||||||
const participant = await db.collection("users").findOne<User>({ id: participantID });
|
const participant = await db.collection("users").findOne<User>({ id: participantID });
|
||||||
|
|
||||||
if (!corporate || !participant) return;
|
if (!corporate || !participant) return;
|
||||||
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
|
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
|
||||||
|
|
||||||
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate)
|
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate)
|
||||||
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: null } });
|
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: null } });
|
||||||
|
|
||||||
const corporateDate = moment(corporate.subscriptionExpirationDate);
|
const corporateDate = moment(corporate.subscriptionExpirationDate);
|
||||||
const participantDate = moment(participant.subscriptionExpirationDate);
|
const participantDate = moment(participant.subscriptionExpirationDate);
|
||||||
|
|
||||||
if (corporateDate.isAfter(participantDate))
|
if (corporateDate.isAfter(participantDate))
|
||||||
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: corporateDate.toISOString() } });
|
return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: corporateDate.toISOString() } });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserCorporate = async (id: string) => {
|
export const getUserCorporate = async (id: string) => {
|
||||||
const user = await getUser(id);
|
const user = await getUser(id);
|
||||||
if (!user) return undefined;
|
if (!user) return undefined;
|
||||||
|
|
||||||
if (["admin", "developer"].includes(user.type)) return undefined;
|
if (["admin", "developer"].includes(user.type)) return undefined;
|
||||||
if (user.type === "mastercorporate") return user;
|
if (user.type === "mastercorporate") return user;
|
||||||
|
|
||||||
const groups = await getParticipantGroups(id);
|
const groups = await getParticipantGroups(id);
|
||||||
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
|
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
|
||||||
const corporates = admins
|
const corporates = admins
|
||||||
.filter((x) => (user.type === "corporate" ? x?.type === "mastercorporate" : x?.type === "corporate"))
|
.filter((x) => (user.type === "corporate" ? x?.type === "mastercorporate" : x?.type === "corporate"))
|
||||||
.filter((x) => !!x) as User[];
|
.filter((x) => !!x) as User[];
|
||||||
|
|
||||||
if (corporates.length === 0) return undefined;
|
if (corporates.length === 0) return undefined;
|
||||||
return corporates.shift() as CorporateUser | MasterCorporateUser;
|
return corporates.shift() as CorporateUser | MasterCorporateUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGroup = async (id: string) => {
|
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>[]> => {
|
export const getGroups = async (): Promise<WithEntity<Group>[]> => {
|
||||||
return await db.collection("groups")
|
return await db.collection("groups")
|
||||||
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
|
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParticipantGroups = async (id: string) => {
|
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[]) => {
|
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[]> => {
|
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) => {
|
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) => {
|
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
|
// @ts-expect-error
|
||||||
$pull: {
|
$pull: {
|
||||||
participants: user
|
participants: user
|
||||||
@@ -107,69 +107,69 @@ export const removeParticipantFromGroup = async (id: string, user: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getUsersGroups = async (ids: string[]) => {
|
export const getUsersGroups = async (ids: string[]) => {
|
||||||
return await db
|
return await db
|
||||||
.collection("groups")
|
.collection("groups")
|
||||||
.find<Group>({ admin: { $in: ids } })
|
.find<Group>({ admin: { $in: ids } })
|
||||||
.toArray();
|
.toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertToUsers = (group: Group, users: User[]): GroupWithUsers =>
|
export const convertToUsers = (group: Group, users: User[]): GroupWithUsers =>
|
||||||
Object.assign(group, {
|
Object.assign(group, {
|
||||||
admin: users.find((u) => u.id === group.admin),
|
admin: users.find((u) => u.id === group.admin),
|
||||||
participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[],
|
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[]> => {
|
export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise<string[]> => {
|
||||||
const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher");
|
const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher");
|
||||||
const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate");
|
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[]) =>
|
export const getGroupsForEntities = async (ids: string[]) =>
|
||||||
await db
|
await db
|
||||||
.collection("groups")
|
.collection("groups")
|
||||||
.find<Group>({ entity: { $in: ids } })
|
.find<Group>({ entity: { $in: ids } })
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
export const getGroupsForUser = async (admin?: string, participant?: string) => {
|
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 (admin) return await getUserGroups(admin);
|
||||||
if (participant) return await getParticipantGroups(participant);
|
if (participant) return await getParticipantGroups(participant);
|
||||||
|
|
||||||
return await getGroups();
|
return await getGroups();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => {
|
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => {
|
||||||
return await db
|
return await db
|
||||||
.collection("groups")
|
.collection("groups")
|
||||||
.find<Group>({ ...(admin ? { admin: { $ne: admin } } : {}), ...(participants ? { participants } : {}) })
|
.find<Group>({ ...(admin ? { admin: { $ne: admin } } : {}), ...(participants ? { participants } : {}) })
|
||||||
.toArray();
|
.toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCorporateNameForStudent = async (studentID: string) => {
|
export const getCorporateNameForStudent = async (studentID: string) => {
|
||||||
const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]);
|
const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]);
|
||||||
if (groups.length === 0) return "";
|
if (groups.length === 0) return "";
|
||||||
|
|
||||||
const adminUserIds = [...new Set(groups.map((g) => g.admin))];
|
const adminUserIds = [...new Set(groups.map((g) => g.admin))];
|
||||||
const adminUsersData = (await getSpecificUsers(adminUserIds)).filter((x) => !!x) as User[];
|
const adminUsersData = (await getSpecificUsers(adminUserIds)).filter((x) => !!x) as User[];
|
||||||
|
|
||||||
if (adminUsersData.length === 0) return "";
|
if (adminUsersData.length === 0) return "";
|
||||||
const admins = adminUsersData.filter((x) => x.type === "corporate");
|
const admins = adminUsersData.filter((x) => x.type === "corporate");
|
||||||
|
|
||||||
if (admins.length > 0) {
|
if (admins.length > 0) {
|
||||||
return (admins[0] as CorporateUser).corporateInformation.companyInformation.name;
|
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 getGroupsByEntity = async (id: string) => await db.collection("groups").find<Group>({ entity: id }).toArray();
|
||||||
|
|
||||||
export const getGroupsByEntities = async (ids: string[]): Promise<WithEntity<Group>[]> =>
|
export const getGroupsByEntities = async (ids: string[]): Promise<WithEntity<Group>[]> =>
|
||||||
await db.collection("groups")
|
await db.collection("groups")
|
||||||
.aggregate<WithEntity<Group>>([
|
.aggregate<WithEntity<Group>>([
|
||||||
{ $match: { entity: { $in: ids } } },
|
{ $match: { entity: { $in: ids } } },
|
||||||
...addEntityToGroupPipeline
|
...addEntityToGroupPipeline
|
||||||
]).toArray()
|
]).toArray()
|
||||||
|
|||||||
@@ -12,48 +12,48 @@ import { mapBy } from ".";
|
|||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
export async function getUsers(filter?: object) {
|
export async function getUsers(filter?: object) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>(filter || {}, { projection: { _id: 0 } })
|
.find<User>(filter || {}, { projection: { _id: 0 } })
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserWithEntity(id: string): Promise<WithEntities<User> | undefined> {
|
export async function getUserWithEntity(id: string): Promise<WithEntities<User> | undefined> {
|
||||||
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
||||||
if (!user) return undefined;
|
if (!user) return undefined;
|
||||||
|
|
||||||
const entities = await Promise.all(
|
const entities = await Promise.all(
|
||||||
user.entities.map(async (e) => {
|
user.entities.map(async (e) => {
|
||||||
const entity = await getEntity(e.id);
|
const entity = await getEntity(e.id);
|
||||||
const role = await getRole(e.role);
|
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> {
|
export async function getUser(id: string): Promise<User | undefined> {
|
||||||
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
||||||
return !!user ? user : undefined;
|
return !!user ? user : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSpecificUsers(ids: string[]) {
|
export async function getSpecificUsers(ids: string[]) {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ id: { $in: ids } }, { projection: { _id: 0 } })
|
.find<User>({ id: { $in: ids } }, { projection: { _id: 0 } })
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntityUsers(id: string, limit?: number, filter?: object) {
|
export async function getEntityUsers(id: string, limit?: number, filter?: object) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ "entities.id": id, ...(filter || {}) })
|
.find<User>({ "entities.id": id, ...(filter || {}) })
|
||||||
.limit(limit || 0)
|
.limit(limit || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countEntityUsers(id: string, filter?: object) {
|
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) {
|
export async function getEntitiesUsers(ids: string[], filter?: object, limit?: number) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) })
|
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) })
|
||||||
.limit(limit || 0)
|
.limit(limit || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countEntitiesUsers(ids: string[]) {
|
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(
|
export async function getLinkedUsers(
|
||||||
userID?: string,
|
userID?: string,
|
||||||
userType?: Type,
|
userType?: Type,
|
||||||
type?: Type,
|
type?: Type,
|
||||||
page?: number,
|
page?: number,
|
||||||
size?: number,
|
size?: number,
|
||||||
sort?: string,
|
sort?: string,
|
||||||
direction?: "asc" | "desc",
|
direction?: "asc" | "desc",
|
||||||
) {
|
) {
|
||||||
const filters = {
|
const filters = {
|
||||||
...(!!type ? { type } : {}),
|
...(!!type ? { type } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!userID || userType === "admin" || userType === "developer") {
|
if (!userID || userType === "admin" || userType === "developer") {
|
||||||
const users = await db
|
const users = await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>(filters)
|
.find<User>(filters)
|
||||||
.sort(sort ? { [sort]: direction === "desc" ? -1 : 1 } : {})
|
.sort(sort ? { [sort]: direction === "desc" ? -1 : 1 } : {})
|
||||||
.skip(page && size ? page * size : 0)
|
.skip(page && size ? page * size : 0)
|
||||||
.limit(size || 0)
|
.limit(size || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const total = await db.collection("users").countDocuments(filters);
|
const total = await db.collection("users").countDocuments(filters);
|
||||||
return { users, total };
|
return { users, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminGroups = await getUserGroups(userID);
|
const adminGroups = await getUserGroups(userID);
|
||||||
const groups = await getUsersGroups(adminGroups.flatMap((x) => x.participants));
|
const groups = await getUsersGroups(adminGroups.flatMap((x) => x.participants));
|
||||||
const belongingGroups = await getParticipantGroups(userID);
|
const belongingGroups = await getParticipantGroups(userID);
|
||||||
|
|
||||||
const participants = uniq([
|
const participants = uniq([
|
||||||
...adminGroups.flatMap((x) => x.participants),
|
...adminGroups.flatMap((x) => x.participants),
|
||||||
...(userType === "mastercorporate" ? groups.flat().flatMap((x) => x.participants) : []),
|
...(userType === "mastercorporate" ? groups.flat().flatMap((x) => x.participants) : []),
|
||||||
...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []),
|
...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ⨯ [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] {
|
// ⨯ [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] {
|
||||||
if (participants.length === 0) return { users: [], total: 0 };
|
if (participants.length === 0) return { users: [], total: 0 };
|
||||||
|
|
||||||
const users = await db
|
const users = await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ ...filters, id: { $in: participants } })
|
.find<User>({ ...filters, id: { $in: participants } })
|
||||||
.skip(page && size ? page * size : 0)
|
.skip(page && size ? page * size : 0)
|
||||||
.limit(size || 0)
|
.limit(size || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
const total = await db.collection("users").countDocuments({ ...filters, id: { $in: participants } });
|
const total = await db.collection("users").countDocuments({ ...filters, id: { $in: participants } });
|
||||||
|
|
||||||
return { users, total };
|
return { users, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserBalance(user: User) {
|
export async function getUserBalance(user: User) {
|
||||||
const codes = await getUserCodes(user.id);
|
const codes = await getUserCodes(user.id);
|
||||||
if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length;
|
if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length;
|
||||||
|
|
||||||
const groups = await getGroupsForUser(user.id);
|
const groups = await getGroupsForUser(user.id);
|
||||||
const participants = uniq(groups.flatMap((x) => x.participants));
|
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 participantUsers = await Promise.all(participants.map(getUser));
|
||||||
const corporateUsers = participantUsers.filter((x) => x?.type === "corporate") as CorporateUser[];
|
const corporateUsers = participantUsers.filter((x) => x?.type === "corporate") as CorporateUser[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
corporateUsers.reduce((acc, curr) => acc + curr.corporateInformation?.companyInformation?.userAmount || 0, 0) +
|
corporateUsers.reduce((acc, curr) => acc + 0, 0) +
|
||||||
corporateUsers.length +
|
corporateUsers.length +
|
||||||
codes.filter((x) => !participants.includes(x.userId || "") && !corporateUsers.map((u) => u.id).includes(x.userId || "")).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[]) => {
|
export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
|
||||||
const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students')
|
const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students')
|
||||||
const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers')
|
const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers')
|
||||||
const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates')
|
const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates')
|
||||||
const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates')
|
const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates')
|
||||||
|
|
||||||
const students = await getEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), {type: "student"})
|
const students = await getEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" })
|
||||||
const teachers = await getEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), {type: "teacher"})
|
const teachers = await getEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), { type: "teacher" })
|
||||||
const corporates = await getEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), {type: "corporate"})
|
const corporates = await getEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), { type: "corporate" })
|
||||||
const masterCorporates = await getEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), {type: "mastercorporate"})
|
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";
|
import { getEntitiesUsers } from "./users.be";
|
||||||
|
|
||||||
export interface UserListRow {
|
export interface UserListRow {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
type: string;
|
type: string;
|
||||||
entities: string;
|
entities: string;
|
||||||
expiryDate: string;
|
expiryDate: string;
|
||||||
verified: string;
|
verified: string;
|
||||||
country: string;
|
country: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
employmentPosition: string;
|
employmentPosition: string;
|
||||||
gender: string;
|
gender: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
|
export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
|
||||||
const rows: UserListRow[] = rowUsers.map((user) => ({
|
const rows: UserListRow[] = rowUsers.map((user) => ({
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
type: USER_TYPE_LABELS[user.type],
|
type: USER_TYPE_LABELS[user.type],
|
||||||
entities: user.entities.map((e) => e.label).join(', '),
|
entities: user.entities.map((e) => e.label).join(', '),
|
||||||
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
||||||
country: user.demographicInformation?.country || "N/A",
|
country: user.demographicInformation?.country || "N/A",
|
||||||
phone: user.demographicInformation?.phone || "N/A",
|
phone: user.demographicInformation?.phone || "N/A",
|
||||||
employmentPosition:
|
employmentPosition:
|
||||||
(user.type === "corporate" || user.type === "mastercorporate"
|
(user.type === "corporate" || user.type === "mastercorporate"
|
||||||
? user.demographicInformation?.position
|
? user.demographicInformation?.position
|
||||||
: user.demographicInformation?.employment) || "N/A",
|
: user.demographicInformation?.employment) || "N/A",
|
||||||
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
|
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
|
||||||
verified: user.isVerified?.toString() || "FALSE",
|
verified: user.isVerified?.toString() || "FALSE",
|
||||||
}));
|
}));
|
||||||
const header = "Name,Email,Type,Entities,Expiry Date,Country,Phone,Employment/Department,Gender,Verification";
|
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 rowsString = rows.map((x) => Object.values(x).join(",")).join("\n");
|
||||||
|
|
||||||
return `${header}\n${rowsString}`;
|
return `${header}\n${rowsString}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserName = (user?: User) => {
|
export const getUserName = (user?: User) => {
|
||||||
if (!user) return "N/A";
|
if (!user) return "N/A";
|
||||||
if (user.type === "corporate" || user.type === "mastercorporate") return user.corporateInformation?.companyInformation?.name || user.name;
|
if (user.type === "corporate" || user.type === "mastercorporate") return user.name;
|
||||||
return user.name;
|
return user.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAdmin = (user: User) => ["admin", "developer"].includes(user.type)
|
export const isAdmin = (user: User) => ["admin", "developer"].includes(user.type)
|
||||||
|
|||||||
Reference in New Issue
Block a user