Merged develop into feature/ExamGenRework
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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -15,11 +15,7 @@ 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;
|
||||||
|
|||||||
@@ -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,8 +109,7 @@ 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,
|
||||||
@@ -176,15 +168,10 @@ const UserCard = ({
|
|||||||
type === "corporate" || type === "mastercorporate"
|
type === "corporate" || type === "mastercorporate"
|
||||||
? {
|
? {
|
||||||
referralAgent,
|
referralAgent,
|
||||||
monthlyDuration,
|
|
||||||
companyInformation: {
|
|
||||||
name: companyName,
|
|
||||||
userAmount,
|
|
||||||
},
|
|
||||||
payment: {
|
payment: {
|
||||||
value: paymentValue,
|
value: paymentValue,
|
||||||
currency: paymentCurrency,
|
currency: paymentCurrency,
|
||||||
...(referralAgent === "" ? {} : {commission: commissionValue}),
|
...(referralAgent === "" ? {} : { commission: commissionValue }),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -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",
|
||||||
@@ -378,7 +308,7 @@ const UserCard = ({
|
|||||||
"!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",
|
||||||
@@ -667,7 +597,7 @@ const UserCard = ({
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
@@ -716,7 +646,7 @@ const UserCard = ({
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import { RolePermission } from "@/resources/entityPermissions";
|
|||||||
export interface Entity {
|
export interface Entity {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
licenses: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
|
|||||||
@@ -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,
|
||||||
}))}
|
}))}
|
||||||
|
|||||||
@@ -366,9 +366,7 @@ export default function UserList({
|
|||||||
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}
|
loggedInUser={user}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
|
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function RegisterCorporate({
|
|||||||
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();
|
||||||
|
|
||||||
@@ -77,10 +77,6 @@ export default function RegisterCorporate({
|
|||||||
profilePicture: "/defaultAvatar.png",
|
profilePicture: "/defaultAvatar.png",
|
||||||
subscriptionExpirationDate: moment().subtract(1, "days").toISOString(),
|
subscriptionExpirationDate: moment().subtract(1, "days").toISOString(),
|
||||||
corporateInformation: {
|
corporateInformation: {
|
||||||
companyInformation: {
|
|
||||||
name: companyName,
|
|
||||||
userAmount: companyUsers,
|
|
||||||
},
|
|
||||||
monthlyDuration: subscriptionDuration,
|
monthlyDuration: subscriptionDuration,
|
||||||
referralAgent,
|
referralAgent,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ function corporateAssignment(
|
|||||||
) {
|
) {
|
||||||
return commonExcel({
|
return commonExcel({
|
||||||
data,
|
data,
|
||||||
userName: user.corporateInformation?.companyInformation?.name || "",
|
userName: user.name || "",
|
||||||
users,
|
users,
|
||||||
sectionName: "Corporate Name :",
|
sectionName: "Corporate Name :",
|
||||||
customTable: [],
|
customTable: [],
|
||||||
@@ -361,7 +361,7 @@ async function mastercorporateAssignment(
|
|||||||
];
|
];
|
||||||
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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {deleteEntity, getEntity, getEntityWithRoles} from "@/utils/entities.be";
|
import { deleteEntity, getEntity, getEntityWithRoles } from "@/utils/entities.be";
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import {Entity} from "@/interfaces/entity";
|
import { Entity } from "@/interfaces/entity";
|
||||||
import { doesEntityAllow } from "@/utils/permissions";
|
import { doesEntityAllow } from "@/utils/permissions";
|
||||||
import { getUser } from "@/utils/users.be";
|
import { getUser } from "@/utils/users.be";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
import { isAdmin } from "@/utils/users";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
const {id, showRoles} = req.query as {id: string; showRoles: string};
|
const { id, showRoles } = req.query as { id: string; showRoles: string };
|
||||||
|
|
||||||
const entity = await (!!showRoles ? getEntityWithRoles : getEntity)(id);
|
const entity = await (!!showRoles ? getEntityWithRoles : getEntity)(id);
|
||||||
res.status(200).json(entity);
|
res.status(200).json(entity);
|
||||||
@@ -39,23 +40,31 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!entity) return res.status(404).json({ ok: false })
|
if (!entity) return res.status(404).json({ ok: false })
|
||||||
|
|
||||||
if (!doesEntityAllow(user, entity, "delete_entity") && !["admin", "developer"].includes(user.type))
|
if (!doesEntityAllow(user, entity, "delete_entity") && !["admin", "developer"].includes(user.type))
|
||||||
return res.status(403).json({ok: false})
|
return res.status(403).json({ ok: false })
|
||||||
|
|
||||||
await deleteEntity(entity)
|
await deleteEntity(entity)
|
||||||
return res.status(200).json({ok: true});
|
return res.status(200).json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
const {id} = req.query as {id: string};
|
const { id } = req.query as { id: string };
|
||||||
|
|
||||||
if (!user.entities.map((x) => x.id).includes(id)) {
|
if (!user.entities.map((x) => x.id).includes(id) && !isAdmin(user)) {
|
||||||
return res.status(403).json({ok: false});
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity = await db.collection<Entity>("entities").updateOne({id}, {$set: {label: req.body.label}});
|
if (req.body.label) {
|
||||||
|
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { label: req.body.label } });
|
||||||
|
return res.status(200).json({ ok: entity.acknowledged });
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({ok: entity.acknowledged});
|
if (req.body.licenses) {
|
||||||
|
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { licenses: req.body.licenses } });
|
||||||
|
return res.status(200).json({ ok: entity.acknowledged });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {addUsersToEntity, addUserToEntity, createEntity, getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
|
import { addUsersToEntity, addUserToEntity, createEntity, getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import {Entity} from "@/interfaces/entity";
|
import { Entity } from "@/interfaces/entity";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
@@ -18,7 +18,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
const {showRoles} = req.query as {showRoles: string};
|
const { showRoles } = req.query as { showRoles: string };
|
||||||
|
|
||||||
const getFn = showRoles ? getEntitiesWithRoles : getEntities;
|
const getFn = showRoles ? getEntitiesWithRoles : getEntities;
|
||||||
|
|
||||||
@@ -31,12 +31,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer"].includes(user.type)) {
|
if (!["admin", "developer"].includes(user.type)) {
|
||||||
return res.status(403).json({ok: false});
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity: Entity = {
|
const entity: Entity = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
label: req.body.label,
|
label: req.body.label,
|
||||||
|
licenses: req.body.licenses
|
||||||
};
|
};
|
||||||
|
|
||||||
const members = req.body.members as string[] | undefined || []
|
const members = req.body.members as string[] | undefined || []
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
@@ -9,8 +9,5 @@ type Data = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||||
// await db.collection("users").updateMany({}, {$set: {entities: []}});
|
res.status(200).json({ name: "John Doe" });
|
||||||
await db.collection("invites").deleteMany({});
|
|
||||||
|
|
||||||
res.status(200).json({name: "John Doe"});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,12 +87,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -4,34 +4,35 @@ import Layout from "@/components/High/Layout";
|
|||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import {Entity, EntityWithRoles, Role} from "@/interfaces/entity";
|
import { Entity, EntityWithRoles, Role } from "@/interfaces/entity";
|
||||||
import {GroupWithUsers, User} from "@/interfaces/user";
|
import { GroupWithUsers, User } from "@/interfaces/user";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import {getEntityWithRoles} from "@/utils/entities.be";
|
import { getEntityWithRoles } from "@/utils/entities.be";
|
||||||
import {convertToUsers, getGroup} from "@/utils/groups.be";
|
import { convertToUsers, getGroup } from "@/utils/groups.be";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import {checkAccess, doesEntityAllow, getTypesOfUser} from "@/utils/permissions";
|
import { checkAccess, doesEntityAllow, getTypesOfUser } from "@/utils/permissions";
|
||||||
import {getUserName, isAdmin} from "@/utils/users";
|
import { getUserName, isAdmin } from "@/utils/users";
|
||||||
import {filterAllowedUsers, getEntitiesUsers, getEntityUsers, getLinkedUsers, getSpecificUsers, getUsers} from "@/utils/users.be";
|
import { filterAllowedUsers, getEntitiesUsers, getEntityUsers, getLinkedUsers, getSpecificUsers, getUsers } from "@/utils/users.be";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
BsChevronLeft,
|
BsChevronLeft,
|
||||||
BsClockFill,
|
BsClockFill,
|
||||||
BsEnvelopeFill,
|
BsEnvelopeFill,
|
||||||
BsFillPersonVcardFill,
|
BsFillPersonVcardFill,
|
||||||
|
BsHash,
|
||||||
BsPerson,
|
BsPerson,
|
||||||
BsPlus,
|
BsPlus,
|
||||||
BsSquare,
|
BsSquare,
|
||||||
@@ -40,15 +41,15 @@ import {
|
|||||||
BsTrash,
|
BsTrash,
|
||||||
BsX,
|
BsX,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, params}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, params }) => {
|
||||||
const user = req.session.user as User;
|
const user = req.session.user as User;
|
||||||
|
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/")
|
||||||
|
|
||||||
const {id} = params as {id: string};
|
const { id } = params as { id: string };
|
||||||
|
|
||||||
const entity = await getEntityWithRoles(id);
|
const entity = await getEntityWithRoles(id);
|
||||||
if (!entity) return redirect("/entities")
|
if (!entity) return redirect("/entities")
|
||||||
@@ -56,12 +57,12 @@ export const getServerSideProps = withIronSessionSsr(async ({req, params}) => {
|
|||||||
if (!doesEntityAllow(user, entity, "view_entities")) return redirect(`/entities`)
|
if (!doesEntityAllow(user, entity, "view_entities")) return redirect(`/entities`)
|
||||||
|
|
||||||
const linkedUsers = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(user.entities, 'id'),
|
const linkedUsers = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(user.entities, 'id'),
|
||||||
{$and: [{type: {$ne: "developer"}}, {type: {$ne: "admin"}}]}))
|
{ $and: [{ type: { $ne: "developer" } }, { type: { $ne: "admin" } }] }))
|
||||||
const entityUsers = await (isAdmin(user) ? getEntityUsers(id) : filterAllowedUsers(user, [entity]));
|
const entityUsers = await (isAdmin(user) ? getEntityUsers(id) : filterAllowedUsers(user, [entity]));
|
||||||
|
|
||||||
const usersWithRole = entityUsers.map((u) => {
|
const usersWithRole = entityUsers.map((u) => {
|
||||||
const e = u.entities.find((e) => e.id === id);
|
const e = u.entities.find((e) => e.id === id);
|
||||||
return {...u, role: findBy(entity.roles, 'id', e?.role)};
|
return { ...u, role: findBy(entity.roles, 'id', e?.role) };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -74,7 +75,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, params}) => {
|
|||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
type UserWithRole = User & {role?: Role};
|
type UserWithRole = User & { role?: Role };
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -83,7 +84,7 @@ interface Props {
|
|||||||
linkedUsers: User[];
|
linkedUsers: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({user, entity, users, linkedUsers}: Props) {
|
export default function Home({ user, entity, users, linkedUsers }: Props) {
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
@@ -110,7 +111,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/entities/${entity.id}/users`, {add: false, members: selectedUsers})
|
.patch(`/api/entities/${entity.id}/users`, { add: false, members: selectedUsers })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The entity has been updated successfully!");
|
toast.success("The entity has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -132,7 +133,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
const defaultRole = findBy(entity.roles, 'isDefault', true)!
|
const defaultRole = findBy(entity.roles, 'isDefault', true)!
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/entities/${entity.id}/users`, {add: true, members: selectedUsers, role: defaultRole.id})
|
.patch(`/api/entities/${entity.id}/users`, { add: true, members: selectedUsers, role: defaultRole.id })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The entity has been updated successfully!");
|
toast.success("The entity has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -153,7 +154,28 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.patch(`/api/entities/${entity.id}`, {label})
|
.patch(`/api/entities/${entity.id}`, { label })
|
||||||
|
.then(() => {
|
||||||
|
toast.success("The entity has been updated successfully!");
|
||||||
|
router.replace(router.asPath);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Something went wrong!");
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const editLicenses = () => {
|
||||||
|
if (!isAdmin(user)) return;
|
||||||
|
|
||||||
|
const licenses = prompt("Update the number of licenses:", (entity.licenses || 0).toString());
|
||||||
|
if (!licenses) return;
|
||||||
|
if (!parseInt(licenses) || parseInt(licenses) <= 0) return toast.error("Write a valid number of licenses!")
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.patch(`/api/entities/${entity.id}`, { licenses })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The entity has been updated successfully!");
|
toast.success("The entity has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -190,7 +212,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.post(`/api/roles/${role}/users`, {users: selectedUsers})
|
.post(`/api/roles/${role}/users`, { users: selectedUsers })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The role has been assigned successfully!");
|
toast.success("The role has been assigned successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -274,7 +296,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">{entity.label}</h2>
|
<h2 className="font-bold text-2xl">{entity.label} {isAdmin(user) && `- ${entity.licenses || 0} licenses`}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -285,6 +307,15 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
<BsTag />
|
<BsTag />
|
||||||
<span className="text-xs">Rename Entity</span>
|
<span className="text-xs">Rename Entity</span>
|
||||||
</button>
|
</button>
|
||||||
|
{isAdmin(user) && (
|
||||||
|
<button
|
||||||
|
onClick={editLicenses}
|
||||||
|
disabled={isLoading || !isAdmin(user)}
|
||||||
|
className="flex items-center gap-1 px-2 py-2 border rounded-full hover:bg-neutral-100 disabled:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
||||||
|
<BsHash />
|
||||||
|
<span className="text-xs">Edit Licenses</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push(`/entities/${entity.id}/roles`)}
|
onClick={() => router.push(`/entities/${entity.id}/roles`)}
|
||||||
disabled={isLoading || !canViewRoles}
|
disabled={isLoading || !canViewRoles}
|
||||||
@@ -325,7 +356,7 @@ export default function Home({user, entity, users, linkedUsers}: Props) {
|
|||||||
{entity.roles.map((role) => (
|
{entity.roles.map((role) => (
|
||||||
<MenuItem key={role.id}>
|
<MenuItem key={role.id}>
|
||||||
<button onClick={() => assignUsersToRole(role.id)} className="p-4 hover:bg-neutral-100 w-32">
|
<button onClick={() => assignUsersToRole(role.id)} className="p-4 hover:bg-neutral-100 w-32">
|
||||||
{ role.label }
|
{role.label}
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -3,32 +3,32 @@ import Layout from "@/components/High/Layout";
|
|||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import {Entity, EntityWithRoles} from "@/interfaces/entity";
|
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import {mapBy, redirect, serialize} from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
|
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import {getUserName} from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
import {getLinkedUsers, getUsers} from "@/utils/users.be";
|
import { getLinkedUsers, getUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill} from "react-icons/bs";
|
import { BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill } from "react-icons/bs";
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { findAllowedEntities } from "@/utils/permissions";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|||||||
const users = await getUsers()
|
const users = await getUsers()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({user, users: users.filter((x) => x.id !== user.id)}),
|
props: serialize({ user, users: users.filter((x) => x.id !== user.id) }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -47,13 +47,14 @@ interface Props {
|
|||||||
users: User[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({user, users}: Props) {
|
export default function Home({ user, users }: Props) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
|
const [licenses, setLicenses] = useState(0);
|
||||||
|
|
||||||
const {rows, renderSearch} = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], users);
|
const { rows, renderSearch } = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], users);
|
||||||
const {items, renderMinimal} = usePagination<User>(rows, 16);
|
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export default function Home({user, users}: Props) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<Entity>(`/api/entities`, {label, members: selectedUsers})
|
.post<Entity>(`/api/entities`, { label, licenses, members: selectedUsers })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
toast.success("Your entity has been created successfully!");
|
toast.success("Your entity has been created successfully!");
|
||||||
router.replace(`/entities/${result.data.id}`);
|
router.replace(`/entities/${result.data.id}`);
|
||||||
@@ -104,7 +105,7 @@ export default function Home({user, users}: Props) {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={createGroup}
|
onClick={createGroup}
|
||||||
disabled={!label.trim() || isLoading}
|
disabled={!label.trim() || licenses <= 0 || isLoading}
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
||||||
<BsCheck />
|
<BsCheck />
|
||||||
<span className="text-xs">Create Entity</span>
|
<span className="text-xs">Create Entity</span>
|
||||||
@@ -112,10 +113,17 @@ export default function Home({user, users}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<div className="w-full grid grid-cols-2 gap-4">
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<span className="font-semibold text-xl">Entity Label:</span>
|
<span className="font-semibold text-xl">Entity Label:</span>
|
||||||
<Input name="name" onChange={setLabel} type="text" placeholder="Entity A" />
|
<Input name="name" onChange={setLabel} type="text" placeholder="Entity A" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<span className="font-semibold text-xl">Licenses:</span>
|
||||||
|
<Input name="licenses" min={0} onChange={(v) => setLicenses(parseInt(v))} type="number" placeholder="12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<span className="font-semibold text-xl">Members ({selectedUsers.length} selected):</span>
|
<span className="font-semibold text-xl">Members ({selectedUsers.length} selected):</span>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ToastContainer } from "react-toastify";
|
|||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import { GroupWithUsers, User } from "@/interfaces/user";
|
import { GroupWithUsers, User } from "@/interfaces/user";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { getUserName } from "@/utils/users";
|
import { getUserName, isAdmin } from "@/utils/users";
|
||||||
import { convertToUsers, getGroupsForUser } from "@/utils/groups.be";
|
import { convertToUsers, getGroupsForUser } from "@/utils/groups.be";
|
||||||
import { countEntityUsers, getEntityUsers, getSpecificUsers, getUsers } from "@/utils/users.be";
|
import { countEntityUsers, getEntityUsers, getSpecificUsers, getUsers } from "@/utils/users.be";
|
||||||
import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions";
|
||||||
@@ -64,7 +64,7 @@ export default function Home({ user, entities }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Members</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">Members</span>
|
||||||
<span className="bg-mti-purple-light/50 px-2">{count}</span>
|
<span className="bg-mti-purple-light/50 px-2">{count}{isAdmin(user) && ` / ${entity.licenses || 0}`}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{users.map(getUserName).join(", ")}{' '}
|
{users.map(getUserName).join(", ")}{' '}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
@@ -356,7 +356,7 @@ export default function PaymentRecord() {
|
|||||||
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]);
|
||||||
|
|
||||||
@@ -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,16 +996,14 @@ 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
|
||||||
}
|
}
|
||||||
@@ -1013,7 +1011,7 @@ export default function PaymentRecord() {
|
|||||||
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",
|
||||||
@@ -1057,7 +1055,7 @@ export default function PaymentRecord() {
|
|||||||
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,7 +182,7 @@ 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: {
|
||||||
@@ -196,7 +196,7 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
|
|||||||
.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[]) {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const getUserNamedGroup = async (id: string, name: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
@@ -159,7 +159,7 @@ export const getCorporateNameForStudent = async (studentID: string) => {
|
|||||||
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 "";
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export async function getUserBalance(user: User) {
|
|||||||
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
|
||||||
);
|
);
|
||||||
@@ -147,10 +147,10 @@ export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]
|
|||||||
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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
|
|||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user