Merged in bug-fixing-19-Jun-24 (pull request #52)
Bug fixing 19 Jun 24 Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -16,7 +16,20 @@ 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 Badge from "./Low/Badge";
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -26,7 +39,13 @@ 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);
|
||||||
@@ -39,9 +58,12 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
|
|||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light";
|
if (today.add(1, "days").isAfter(momentDate))
|
||||||
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
|
return "!bg-mti-red-ultralight border-mti-red-light";
|
||||||
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
if (today.add(3, "days").isAfter(momentDate))
|
||||||
|
return "!bg-mti-rose-ultralight border-mti-rose-light";
|
||||||
|
if (today.add(7, "days").isAfter(momentDate))
|
||||||
|
return "!bg-mti-orange-ultralight border-mti-orange-light";
|
||||||
};
|
};
|
||||||
|
|
||||||
const showExpirationDate = () => {
|
const showExpirationDate = () => {
|
||||||
@@ -54,39 +76,102 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user.type !== "student" && user.type !== "teacher") return setDisablePaymentPage(false);
|
if (user.type !== "student" && user.type !== "teacher")
|
||||||
isUserFromCorporate(user.id).then((result) => setDisablePaymentPage(result));
|
return setDisablePaymentPage(false);
|
||||||
|
isUserFromCorporate(user.id).then((result) =>
|
||||||
|
setDisablePaymentPage(result)
|
||||||
|
);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
const badges = [
|
||||||
|
{
|
||||||
|
module: "reading",
|
||||||
|
icon: () => <BsBook className="h-4 w-4 text-white" />,
|
||||||
|
achieved: user.levels.reading >= user.desiredLevels.reading,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
module: "listening",
|
||||||
|
icon: () => <BsHeadphones className="h-4 w-4 text-white" />,
|
||||||
|
achieved: user.levels.listening >= user.desiredLevels.listening,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: "writing",
|
||||||
|
icon: () => <BsPen className="h-4 w-4 text-white" />,
|
||||||
|
achieved: user.levels.writing >= user.desiredLevels.writing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: "speaking",
|
||||||
|
icon: () => <BsMegaphone className="h-4 w-4 text-white" />,
|
||||||
|
achieved: user.levels.speaking >= user.desiredLevels.speaking,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: "level",
|
||||||
|
icon: () => <BsClipboard className="h-4 w-4 text-white" />,
|
||||||
|
achieved: user.levels.level >= user.desiredLevels.level,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={isTicketOpen} onClose={() => setIsTicketOpen(false)} title="Submit a ticket">
|
<Modal
|
||||||
<TicketSubmission user={user} page={router.asPath} onClose={() => setIsTicketOpen(false)} />
|
isOpen={isTicketOpen}
|
||||||
|
onClose={() => setIsTicketOpen(false)}
|
||||||
|
title="Submit a ticket"
|
||||||
|
>
|
||||||
|
<TicketSubmission
|
||||||
|
user={user}
|
||||||
|
page={router.asPath}
|
||||||
|
onClose={() => setIsTicketOpen(false)}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<MobileMenu disableNavigation={disableNavigation} path={path} isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)} user={user} />
|
<MobileMenu
|
||||||
|
disableNavigation={disableNavigation}
|
||||||
|
path={path}
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
onClose={() => setIsMenuOpen(false)}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<header className="-md:justify-between -md:px-4 relative flex w-full items-center bg-transparent py-2 md:gap-12 md:py-4">
|
<header className="-md:justify-between -md:px-4 relative flex w-full items-center bg-transparent py-2 md:gap-12 md:py-4">
|
||||||
<Link href={disableNavigation ? "" : "/"} className=" flex items-center gap-8 md:px-8">
|
<Link
|
||||||
|
href={disableNavigation ? "" : "/"}
|
||||||
|
className=" flex items-center gap-8 md:px-8"
|
||||||
|
>
|
||||||
<img src="/logo.png" alt="EnCoach's Logo" className="w-8 md:w-12" />
|
<img src="/logo.png" alt="EnCoach's Logo" className="w-8 md:w-12" />
|
||||||
<h1 className="-md:hidden w-1/6 text-2xl font-bold">EnCoach</h1>
|
<h1 className="-md:hidden w-1/6 text-2xl font-bold">EnCoach</h1>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center justify-end gap-4 md:mr-8 md:w-5/6">
|
<div className="flex items-center justify-end gap-4 md:mr-8 md:w-5/6">
|
||||||
|
{user.type === "student" &&
|
||||||
|
badges.map((badge) => (
|
||||||
|
<div
|
||||||
|
key={badge.module}
|
||||||
|
className={`${badge.achieved ? `bg-ielts-${badge.module}`: 'bg-mti-gray-anti-flash'} flex h-8 w-8 items-center justify-center rounded-full`}
|
||||||
|
>
|
||||||
|
{badge.icon()}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{/* OPEN TICKET SYSTEM */}
|
{/* OPEN TICKET SYSTEM */}
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-purple-light tooltip tooltip-bottom flex h-8 w-8 flex-col items-center justify-center rounded-full border p-1",
|
"border-mti-purple-light tooltip tooltip-bottom flex h-8 w-8 flex-col items-center justify-center rounded-full border p-1",
|
||||||
"hover:bg-mti-purple-light transition duration-300 ease-in-out hover:text-white z-20",
|
"hover:bg-mti-purple-light transition duration-300 ease-in-out hover:text-white z-20"
|
||||||
)}
|
)}
|
||||||
data-tip="Submit a help/feedback ticket"
|
data-tip="Submit a help/feedback ticket"
|
||||||
onClick={() => setIsTicketOpen(true)}>
|
onClick={() => setIsTicketOpen(true)}
|
||||||
|
>
|
||||||
<BsQuestionCircleFill />
|
<BsQuestionCircleFill />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showExpirationDate() && (
|
{showExpirationDate() && (
|
||||||
<Link
|
<Link
|
||||||
href={!!user.subscriptionExpirationDate && !disablePaymentPage ? "/payment" : ""}
|
href={
|
||||||
|
!!user.subscriptionExpirationDate && !disablePaymentPage
|
||||||
|
? "/payment"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
data-tip="Expiry date"
|
data-tip="Expiry date"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex w-fit cursor-pointer justify-center rounded-full border px-6 py-2 text-sm font-normal focus:outline-none",
|
"flex w-fit cursor-pointer justify-center rounded-full border px-6 py-2 text-sm font-normal focus:outline-none",
|
||||||
@@ -94,24 +179,40 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
|
|||||||
!user.subscriptionExpirationDate
|
!user.subscriptionExpirationDate
|
||||||
? "bg-mti-green-ultralight border-mti-green-light"
|
? "bg-mti-green-ultralight border-mti-green-light"
|
||||||
: expirationDateColor(user.subscriptionExpirationDate),
|
: expirationDateColor(user.subscriptionExpirationDate),
|
||||||
"border-mti-gray-platinum bg-white",
|
"border-mti-gray-platinum bg-white"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{!user.subscriptionExpirationDate && "Unlimited"}
|
{!user.subscriptionExpirationDate && "Unlimited"}
|
||||||
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
{user.subscriptionExpirationDate &&
|
||||||
|
moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link href={disableNavigation ? "" : "/profile"} className="-md:hidden flex items-center justify-end gap-6">
|
<Link
|
||||||
<img src={user.profilePicture} alt={user.name} className="h-10 w-10 rounded-full object-cover" />
|
href={disableNavigation ? "" : "/profile"}
|
||||||
|
className="-md:hidden flex items-center justify-end gap-6"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.profilePicture}
|
||||||
|
alt={user.name}
|
||||||
|
className="h-10 w-10 rounded-full object-cover"
|
||||||
|
/>
|
||||||
<span className="-md:hidden text-right">
|
<span className="-md:hidden text-right">
|
||||||
{user.type === "corporate" ? `${user.corporateInformation?.companyInformation.name} |` : ""} {user.name} |{" "}
|
{user.type === "corporate"
|
||||||
{USER_TYPE_LABELS[user.type]}
|
? `${user.corporateInformation?.companyInformation.name} |`
|
||||||
|
: ""}{" "}
|
||||||
|
{user.name} | {USER_TYPE_LABELS[user.type]}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="cursor-pointer md:hidden" onClick={() => setIsMenuOpen(true)}>
|
<div
|
||||||
|
className="cursor-pointer md:hidden"
|
||||||
|
onClick={() => setIsMenuOpen(true)}
|
||||||
|
>
|
||||||
<BsList className="text-mti-purple-light h-8 w-8" />
|
<BsList className="text-mti-purple-light h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{focusMode && <FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />}
|
{focusMode && (
|
||||||
|
<FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import IconCard from "./IconCard";
|
|||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
|
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
|
||||||
|
import CorporateStudentsLevels from "./CorporateStudentsLevels";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -51,17 +52,24 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
useEffect(reload, [page]);
|
useEffect(reload, [page]);
|
||||||
|
|
||||||
const inactiveCountryManagerFilter = (x: User) =>
|
const inactiveCountryManagerFilter = (x: User) =>
|
||||||
x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
x.type === "agent" &&
|
||||||
|
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
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">
|
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
||||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
>
|
||||||
|
<img
|
||||||
|
src={displayUser.profilePicture}
|
||||||
|
alt={displayUser.name}
|
||||||
|
className="rounded-full w-10 h-10"
|
||||||
|
/>
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>
|
<span>
|
||||||
{displayUser.type === "corporate"
|
{displayUser.type === "corporate"
|
||||||
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
|
? displayUser.corporateInformation?.companyInformation?.name ||
|
||||||
|
displayUser.name
|
||||||
: displayUser.name}
|
: displayUser.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -74,25 +82,32 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
(!!selectedUser
|
(!!selectedUser
|
||||||
? groups
|
? groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id)
|
.includes(x.id)
|
||||||
: true);
|
: true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Students ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,25 +116,32 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
x.type === "teacher" &&
|
x.type === "teacher" &&
|
||||||
(!!selectedUser
|
(!!selectedUser
|
||||||
? groups
|
? groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id) || false
|
.includes(x.id) || false
|
||||||
: true);
|
: true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Teachers ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,36 +149,44 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
const filter = (x: User) => x.type === "agent";
|
const filter = (x: User) => x.type === "agent";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Country Managers ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Country Managers ({total})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CorporateList = () => (
|
const CorporateList = () => (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "corporate"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Corporate ({users.filter((x) => x.type === "corporate").length})</h2>
|
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[(x) => x.type === "corporate"]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
|
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
|
||||||
@@ -164,77 +194,122 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
|
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">
|
||||||
{paid ? "Payment Done" : "Pending Payment"} ({list.length})
|
{paid ? "Payment Done" : "Pending Payment"} ({total})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<UserList user={user} filters={[filter]} />
|
)}
|
||||||
</>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InactiveCountryManagerList = () => {
|
const InactiveCountryManagerList = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[inactiveCountryManagerFilter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Inactive Country Managers ({users.filter(inactiveCountryManagerFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Inactive Country Managers ({total})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[inactiveCountryManagerFilter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InactiveStudentsList = () => {
|
const InactiveStudentsList = () => {
|
||||||
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
const filter = (x: User) =>
|
||||||
|
x.type === "student" &&
|
||||||
|
(x.status === "disabled" ||
|
||||||
|
moment().isAfter(x.subscriptionExpirationDate));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Inactive Students ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Inactive Students ({total})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InactiveCorporateList = () => {
|
const InactiveCorporateList = () => {
|
||||||
const filter = (x: User) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
const filter = (x: User) =>
|
||||||
|
x.type === "corporate" &&
|
||||||
|
(x.status === "disabled" ||
|
||||||
|
moment().isAfter(x.subscriptionExpirationDate));
|
||||||
|
|
||||||
|
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">
|
||||||
|
Inactive Corporate ({total})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CorporateStudentsLevelsHelper = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Inactive Corporate ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Corporate Students Levels
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<CorporateStudentsLevels />
|
||||||
<UserList user={user} filters={[filter]} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -273,7 +348,15 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsGlobeCentralSouthAsia}
|
Icon={BsGlobeCentralSouthAsia}
|
||||||
label="Countries"
|
label="Countries"
|
||||||
value={[...new Set(users.filter((x) => x.demographicInformation).map((x) => x.demographicInformation?.country))].length}
|
value={
|
||||||
|
[
|
||||||
|
...new Set(
|
||||||
|
users
|
||||||
|
.filter((x) => x.demographicInformation)
|
||||||
|
.map((x) => x.demographicInformation?.country)
|
||||||
|
),
|
||||||
|
].length
|
||||||
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -281,8 +364,12 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
label="Inactive Students"
|
label="Inactive Students"
|
||||||
value={
|
value={
|
||||||
users.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
|
users.filter(
|
||||||
.length
|
(x) =>
|
||||||
|
x.type === "student" &&
|
||||||
|
(x.status === "disabled" ||
|
||||||
|
moment().isAfter(x.subscriptionExpirationDate))
|
||||||
|
).length
|
||||||
}
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
@@ -298,12 +385,22 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
Icon={BsBank}
|
Icon={BsBank}
|
||||||
label="Inactive Corporate"
|
label="Inactive Corporate"
|
||||||
value={
|
value={
|
||||||
users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
|
users.filter(
|
||||||
.length
|
(x) =>
|
||||||
|
x.type === "corporate" &&
|
||||||
|
(x.status === "disabled" ||
|
||||||
|
moment().isAfter(x.subscriptionExpirationDate))
|
||||||
|
).length
|
||||||
}
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
|
<IconCard
|
||||||
|
onClick={() => setPage("paymentdone")}
|
||||||
|
Icon={BsCurrencyDollar}
|
||||||
|
label="Payment Done"
|
||||||
|
value={done.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("paymentpending")}
|
onClick={() => setPage("paymentpending")}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
@@ -317,6 +414,12 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
label="Content Management System (CMS)"
|
label="Content Management System (CMS)"
|
||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => setPage("corporatestudentslevels")}
|
||||||
|
Icon={BsPersonFill}
|
||||||
|
label="Corporate Students Levels"
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
|
||||||
@@ -361,7 +464,9 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
<span className="p-4">Unpaid Corporate</span>
|
<span className="p-4">Unpaid Corporate</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter((x) => x.type === "corporate" && x.status === "paymentDue")
|
.filter(
|
||||||
|
(x) => x.type === "corporate" && x.status === "paymentDue"
|
||||||
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -375,8 +480,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
moment().isAfter(
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
||||||
|
) &&
|
||||||
|
moment().isBefore(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -391,8 +498,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "teacher" &&
|
x.type === "teacher" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
moment().isAfter(
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
||||||
|
) &&
|
||||||
|
moment().isBefore(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -407,8 +516,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "agent" &&
|
x.type === "agent" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
moment().isAfter(
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
||||||
|
) &&
|
||||||
|
moment().isBefore(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -423,8 +534,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "corporate" &&
|
x.type === "corporate" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
moment().isAfter(
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
||||||
|
) &&
|
||||||
|
moment().isBefore(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -436,7 +549,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
(x) => x.type === "student" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
(x) =>
|
||||||
|
x.type === "student" &&
|
||||||
|
x.subscriptionExpirationDate &&
|
||||||
|
moment().isAfter(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -448,7 +564,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
(x) => x.type === "teacher" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
(x) =>
|
||||||
|
x.type === "teacher" &&
|
||||||
|
x.subscriptionExpirationDate &&
|
||||||
|
moment().isAfter(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -460,7 +579,10 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
(x) => x.type === "agent" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
(x) =>
|
||||||
|
x.type === "agent" &&
|
||||||
|
x.subscriptionExpirationDate &&
|
||||||
|
moment().isAfter(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -473,7 +595,9 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "corporate" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
x.type === "corporate" &&
|
||||||
|
x.subscriptionExpirationDate &&
|
||||||
|
moment().isAfter(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -497,7 +621,8 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -507,7 +632,11 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -517,7 +646,8 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "student"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -527,7 +657,11 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -537,7 +671,8 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewCorporate={
|
onViewCorporate={
|
||||||
selectedUser.type === "teacher" || selectedUser.type === "student"
|
selectedUser.type === "teacher" ||
|
||||||
|
selectedUser.type === "student"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-corporate",
|
id: "view-corporate",
|
||||||
@@ -547,7 +682,9 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.participants.includes(selectedUser.id))
|
.filter((g) =>
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => [g.admin, ...g.participants])
|
.flatMap((g) => [g.admin, ...g.participants])
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -571,6 +708,7 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
{page === "inactiveCountryManagers" && <InactiveCountryManagerList />}
|
{page === "inactiveCountryManagers" && <InactiveCountryManagerList />}
|
||||||
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
||||||
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
||||||
|
{page === "corporatestudentslevels" && <CorporateStudentsLevelsHelper />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ import UserList from "@/pages/(admin)/Lists/UserList";
|
|||||||
import { dateSorter } from "@/utils";
|
import { dateSorter } from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {BsArrowLeft, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs";
|
import {
|
||||||
|
BsArrowLeft,
|
||||||
|
BsPersonFill,
|
||||||
|
BsBank,
|
||||||
|
BsCurrencyDollar,
|
||||||
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
|
||||||
import IconCard from "./IconCard";
|
import IconCard from "./IconCard";
|
||||||
import usePaymentStatusUsers from '@/hooks/usePaymentStatusUsers';
|
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -34,19 +39,34 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
|
|
||||||
const corporateFilter = (user: User) => user.type === "corporate";
|
const corporateFilter = (user: User) => user.type === "corporate";
|
||||||
const referredCorporateFilter = (x: User) =>
|
const referredCorporateFilter = (x: User) =>
|
||||||
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
|
x.type === "corporate" &&
|
||||||
|
!!x.corporateInformation &&
|
||||||
|
x.corporateInformation.referralAgent === user.id;
|
||||||
const inactiveReferredCorporateFilter = (x: User) =>
|
const inactiveReferredCorporateFilter = (x: User) =>
|
||||||
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
referredCorporateFilter(x) &&
|
||||||
|
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
|
|
||||||
const UserDisplay = ({ displayUser, allowClick = true }: {displayUser: User, allowClick?: boolean}) => (
|
const UserDisplay = ({
|
||||||
|
displayUser,
|
||||||
|
allowClick = true,
|
||||||
|
}: {
|
||||||
|
displayUser: User;
|
||||||
|
allowClick?: boolean;
|
||||||
|
}) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => allowClick && setSelectedUser(displayUser)}
|
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">
|
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
||||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
>
|
||||||
|
<img
|
||||||
|
src={displayUser.profilePicture}
|
||||||
|
alt={displayUser.name}
|
||||||
|
className="rounded-full w-10 h-10"
|
||||||
|
/>
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>
|
<span>
|
||||||
{displayUser.type === "corporate"
|
{displayUser.type === "corporate"
|
||||||
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
|
? displayUser.corporateInformation?.companyInformation?.name ||
|
||||||
|
displayUser.name
|
||||||
: displayUser.name}
|
: displayUser.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -56,37 +76,47 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
|
|
||||||
const ReferredCorporateList = () => {
|
const ReferredCorporateList = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[referredCorporateFilter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Referred Corporate ({users.filter(referredCorporateFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Referred Corporate ({total})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[referredCorporateFilter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InactiveReferredCorporateList = () => {
|
const InactiveReferredCorporateList = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[inactiveReferredCorporateFilter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({users.filter(inactiveReferredCorporateFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Inactive Referred Corporate ({total})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[inactiveReferredCorporateFilter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,18 +124,22 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
const filter = (x: User) => x.type === "corporate";
|
const filter = (x: User) => x.type === "corporate";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Corporate ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
<UserList user={user} filters={[filter]} />
|
)}
|
||||||
</>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,18 +148,24 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
|
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">{paid ? 'Payment Done' : 'Pending Payment'} ({list.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
{paid ? "Payment Done" : "Pending Payment"} ({total})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<UserList user={user} filters={[filter]} />
|
)}
|
||||||
</>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,8 +239,10 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
referredCorporateFilter(x) &&
|
referredCorporateFilter(x) &&
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
moment().isAfter(
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
||||||
|
) &&
|
||||||
|
moment().isBefore(moment(x.subscriptionExpirationDate))
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} displayUser={x} />
|
<UserDisplay key={x.id} displayUser={x} />
|
||||||
@@ -224,9 +266,16 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
|
? () => setPage("students")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onViewTeachers={
|
||||||
|
selectedUser.type === "corporate"
|
||||||
|
? () => setPage("teachers")
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
|
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +284,9 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
{page === "referredCorporate" && <ReferredCorporateList />}
|
{page === "referredCorporate" && <ReferredCorporateList />}
|
||||||
{page === "corporate" && <CorporateList />}
|
{page === "corporate" && <CorporateList />}
|
||||||
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
|
{page === "inactiveReferredCorporate" && (
|
||||||
|
<InactiveReferredCorporateList />
|
||||||
|
)}
|
||||||
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
||||||
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
|
|||||||
@@ -57,16 +57,26 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
|
const studentFilter = (user: User) =>
|
||||||
const teacherFilter = (user: User) => user.type === "teacher" && groups.flatMap((g) => g.participants).includes(user.id);
|
user.type === "student" &&
|
||||||
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
|
const teacherFilter = (user: User) =>
|
||||||
|
user.type === "teacher" &&
|
||||||
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
|
|
||||||
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
const getStatsByStudent = (user: User) =>
|
||||||
|
stats.filter((s) => s.user === user.id);
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
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">
|
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
||||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
>
|
||||||
|
<img
|
||||||
|
src={displayUser.profilePicture}
|
||||||
|
alt={displayUser.name}
|
||||||
|
className="rounded-full w-10 h-10"
|
||||||
|
/>
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -85,19 +95,22 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
: groups.flatMap((g) => g.participants).includes(x.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Students ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,35 +125,42 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
: groups.flatMap((g) => g.participants).includes(x.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Teachers ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupsList = () => {
|
const GroupsList = () => {
|
||||||
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
|
const filter = (x: Group) =>
|
||||||
|
x.admin === user.id || x.participants.includes(user.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Groups ({groups.filter(filter).length})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -150,14 +170,29 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({focus: users.find((u) => u.id === s.user)?.focus, score: s.score, module: s.module}))
|
.map((s) => ({
|
||||||
|
focus: users.find((u) => u.id === s.user)?.focus,
|
||||||
|
score: s.score,
|
||||||
|
module: s.module,
|
||||||
|
}))
|
||||||
.filter((f) => !!f.focus);
|
.filter((f) => !!f.focus);
|
||||||
const bandScores = formattedStats.map((s) => ({
|
const bandScores = formattedStats.map((s) => ({
|
||||||
module: s.module,
|
module: s.module,
|
||||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(
|
||||||
|
s.score.correct,
|
||||||
|
s.score.total,
|
||||||
|
s.module,
|
||||||
|
s.focus!
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0};
|
const levels: { [key in Module]: number } = {
|
||||||
|
reading: 0,
|
||||||
|
listening: 0,
|
||||||
|
writing: 0,
|
||||||
|
speaking: 0,
|
||||||
|
level: 0,
|
||||||
|
};
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
return calculateAverageLevel(levels);
|
return calculateAverageLevel(levels);
|
||||||
@@ -183,26 +218,46 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
value={
|
||||||
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
).length
|
||||||
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPaperclip}
|
Icon={BsPaperclip}
|
||||||
label="Average Level"
|
label="Average Level"
|
||||||
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
value={averageLevelCalculator(
|
||||||
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
)
|
||||||
|
).toFixed(1)}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => setPage("groups")}
|
||||||
|
Icon={BsPeople}
|
||||||
|
label="Groups"
|
||||||
|
value={groups.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
|
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonCheck}
|
Icon={BsPersonCheck}
|
||||||
label="User Balance"
|
label="User Balance"
|
||||||
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
value={`${codes.length}/${
|
||||||
|
user.corporateInformation?.companyInformation?.userAmount || 0
|
||||||
|
}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
value={
|
||||||
|
user.subscriptionExpirationDate
|
||||||
|
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
||||||
|
: "Unlimited"
|
||||||
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -235,7 +290,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -248,7 +307,8 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
|
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(a))).length
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -272,7 +332,8 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -282,7 +343,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -292,7 +357,8 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "student"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -302,7 +368,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
|
|||||||
140
src/dashboards/CorporateStudentsLevels.tsx
Normal file
140
src/dashboards/CorporateStudentsLevels.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React from "react";
|
||||||
|
import useUsers 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 flex-col 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 { users } = useUsers();
|
||||||
|
const { groups } = useGroups();
|
||||||
|
|
||||||
|
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
|
||||||
|
const [corporateId, setCorporateId] = React.useState<string>("");
|
||||||
|
const corporate =
|
||||||
|
corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
|
||||||
|
|
||||||
|
const groupsFromCorporate = corporate
|
||||||
|
? groups.filter((g) => g.admin === corporate.id)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const groupsParticipants = groupsFromCorporate
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.reduce((accm: User[], p) => {
|
||||||
|
const user = users.find((u) => u.id === p) as User;
|
||||||
|
if (user) {
|
||||||
|
return [...accm, user];
|
||||||
|
}
|
||||||
|
return accm;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
options={corporateUsers.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,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{groupsParticipants.map((u) => (
|
||||||
|
<Card user={u} key={u.id} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CorporateStudentsLevels;
|
||||||
@@ -57,12 +57,17 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
const [corporateUserToShow, setCorporateUserToShow] =
|
||||||
|
useState<CorporateUser>();
|
||||||
|
|
||||||
const { stats } = useStats();
|
const { stats } = useStats();
|
||||||
const { users, reload } = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const { groups } = useGroups(user.id);
|
const { groups } = useGroups(user.id);
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
|
const {
|
||||||
|
assignments,
|
||||||
|
isLoading: isAssignmentsLoading,
|
||||||
|
reload: reloadAssignments,
|
||||||
|
} = useAssignments({ assigner: user.id });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
@@ -72,15 +77,23 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
getUserCorporate(user.id).then(setCorporateUserToShow);
|
getUserCorporate(user.id).then(setCorporateUserToShow);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
|
const studentFilter = (user: User) =>
|
||||||
|
user.type === "student" &&
|
||||||
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
|
|
||||||
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
const getStatsByStudent = (user: User) =>
|
||||||
|
stats.filter((s) => s.user === user.id);
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
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">
|
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
||||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
>
|
||||||
|
<img
|
||||||
|
src={displayUser.profilePicture}
|
||||||
|
alt={displayUser.name}
|
||||||
|
className="rounded-full w-10 h-10"
|
||||||
|
/>
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -99,35 +112,42 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
: groups.flatMap((g) => g.participants).includes(x.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Students ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={[filter]} />
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupsList = () => {
|
const GroupsList = () => {
|
||||||
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
|
const filter = (x: Group) =>
|
||||||
|
x.admin === user.id || x.participants.includes(user.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Groups ({groups.filter(filter).length})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -137,14 +157,29 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({focus: users.find((u) => u.id === s.user)?.focus, score: s.score, module: s.module}))
|
.map((s) => ({
|
||||||
|
focus: users.find((u) => u.id === s.user)?.focus,
|
||||||
|
score: s.score,
|
||||||
|
module: s.module,
|
||||||
|
}))
|
||||||
.filter((f) => !!f.focus);
|
.filter((f) => !!f.focus);
|
||||||
const bandScores = formattedStats.map((s) => ({
|
const bandScores = formattedStats.map((s) => ({
|
||||||
module: s.module,
|
module: s.module,
|
||||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(
|
||||||
|
s.score.correct,
|
||||||
|
s.score.total,
|
||||||
|
s.module,
|
||||||
|
s.focus!
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0};
|
const levels: { [key in Module]: number } = {
|
||||||
|
reading: 0,
|
||||||
|
listening: 0,
|
||||||
|
writing: 0,
|
||||||
|
speaking: 0,
|
||||||
|
level: 0,
|
||||||
|
};
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
return calculateAverageLevel(levels);
|
return calculateAverageLevel(levels);
|
||||||
@@ -152,10 +187,16 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) =>
|
const activeFilter = (a: Assignment) =>
|
||||||
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
moment(a.endDate).isAfter(moment()) &&
|
||||||
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
moment(a.startDate).isBefore(moment()) &&
|
||||||
|
a.assignees.length > a.results.length;
|
||||||
|
const pastFilter = (a: Assignment) =>
|
||||||
|
(moment(a.endDate).isBefore(moment()) ||
|
||||||
|
a.assignees.length === a.results.length) &&
|
||||||
|
!a.archived;
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
const archivedFilter = (a: Assignment) => a.archived;
|
||||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
const futureFilter = (a: Assignment) =>
|
||||||
|
moment(a.startDate).isAfter(moment());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -170,7 +211,9 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
/>
|
/>
|
||||||
<AssignmentCreator
|
<AssignmentCreator
|
||||||
assignment={selectedAssignment}
|
assignment={selectedAssignment}
|
||||||
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
groups={groups.filter(
|
||||||
|
(x) => x.admin === user.id || x.participants.includes(user.id)
|
||||||
|
)}
|
||||||
users={users.filter(
|
users={users.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
@@ -179,7 +222,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
.filter((g) => g.admin === selectedUser.id)
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id) || false
|
.includes(x.id) || false
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id)),
|
: groups.flatMap((g) => g.participants).includes(x.id))
|
||||||
)}
|
)}
|
||||||
assigner={user.id}
|
assigner={user.id}
|
||||||
isCreating={isCreatingAssignment}
|
isCreating={isCreatingAssignment}
|
||||||
@@ -192,31 +235,47 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<div className="w-full flex justify-between items-center">
|
<div className="w-full flex justify-between items-center">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={reloadAssignments}
|
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">
|
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>
|
<span>Reload</span>
|
||||||
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
<BsArrowRepeat
|
||||||
|
className={clsx(
|
||||||
|
"text-xl",
|
||||||
|
isAssignmentsLoading && "animate-spin"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Active Assignments ({assignments.filter(activeFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => setSelectedAssignment(a)}
|
||||||
|
key={a.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Planned Assignments ({assignments.filter(futureFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
onClick={() => setIsCreatingAssignment(true)}
|
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">
|
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" />
|
<BsPlus className="text-6xl" />
|
||||||
<span className="text-lg">New Assignment</span>
|
<span className="text-lg">New Assignment</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,7 +292,9 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Past Assignments ({assignments.filter(pastFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
@@ -248,7 +309,9 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Archived Assignments ({assignments.filter(archivedFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
@@ -270,14 +333,19 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<>
|
<>
|
||||||
{corporateUserToShow && (
|
{corporateUserToShow && (
|
||||||
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
|
Linked to:{" "}
|
||||||
|
<b>
|
||||||
|
{corporateUserToShow?.corporateInformation?.companyInformation
|
||||||
|
.name || corporateUserToShow.name}
|
||||||
|
</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section
|
<section
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
|
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
|
||||||
!!corporateUserToShow && "mt-12 xl:mt-6",
|
!!corporateUserToShow && "mt-12 xl:mt-6"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("students")}
|
onClick={() => setPage("students")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
@@ -288,23 +356,40 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
value={
|
||||||
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
).length
|
||||||
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPaperclip}
|
Icon={BsPaperclip}
|
||||||
label="Average Level"
|
label="Average Level"
|
||||||
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
value={averageLevelCalculator(
|
||||||
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
)
|
||||||
|
).toFixed(1)}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPeople} label="Groups" value={groups.length} color="purple" onClick={() => setPage("groups")} />
|
<IconCard
|
||||||
|
Icon={BsPeople}
|
||||||
|
label="Groups"
|
||||||
|
value={groups.length}
|
||||||
|
color="purple"
|
||||||
|
onClick={() => setPage("groups")}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("assignments")}
|
onClick={() => setPage("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">
|
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" />
|
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
||||||
<span className="flex flex-col gap-1 items-center text-xl">
|
<span className="flex flex-col gap-1 items-center text-xl">
|
||||||
<span className="text-lg">Assignments</span>
|
<span className="text-lg">Assignments</span>
|
||||||
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
|
<span className="font-semibold text-mti-purple-light">
|
||||||
|
{assignments.filter((a) => !a.archived).length}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -326,7 +411,11 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -339,7 +428,8 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
|
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(a))).length
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -363,9 +453,16 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
|
? () => setPage("students")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onViewTeachers={
|
||||||
|
selectedUser.type === "corporate"
|
||||||
|
? () => setPage("teachers")
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
|
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import ReactDatePicker from "react-datepicker";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Code>();
|
const columnHelper = createColumnHelper<Code>();
|
||||||
|
|
||||||
@@ -41,41 +43,51 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
||||||
|
|
||||||
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(
|
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(
|
||||||
user?.type === "corporate" ? user : undefined,
|
user?.type === "corporate" ? user : undefined
|
||||||
);
|
);
|
||||||
const [filterAvailability, setFilterAvailability] = useState<
|
const [filterAvailability, setFilterAvailability] = useState<
|
||||||
"in-use" | "unused"
|
"in-use" | "unused"
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
||||||
|
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const { codes, reload } = useCodes(
|
const { codes, reload } = useCodes(
|
||||||
user?.type === "corporate" ? user?.id : undefined,
|
user?.type === "corporate" ? user?.id : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
||||||
let result = [...codes];
|
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
||||||
if (filteredCorporate)
|
const filteredCodes = useMemo(() => {
|
||||||
result = result.filter((x) => x.creator === filteredCorporate.id);
|
return codes.filter((x) => {
|
||||||
if (filterAvailability)
|
// TODO: if the expiry date is missing, it does not make sense to filter by date
|
||||||
result = result.filter((x) =>
|
// so we need to find a way to handle this edge case
|
||||||
filterAvailability === "in-use" ? !!x.userId : !x.userId,
|
if(startDate && endDate && x.expiryDate) {
|
||||||
);
|
const date = moment(x.expiryDate);
|
||||||
|
if(date.isBefore(startDate) || date.isAfter(endDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filteredCorporate && x.creator !== filteredCorporate.id) return false;
|
||||||
|
if (filterAvailability) {
|
||||||
|
if (filterAvailability === "in-use" && !x.userId) return false;
|
||||||
|
if (filterAvailability === "unused" && x.userId) return false;
|
||||||
|
}
|
||||||
|
|
||||||
setFilteredCodes(result);
|
return true;
|
||||||
}, [codes, filteredCorporate, filterAvailability]);
|
});
|
||||||
|
}, [codes, startDate, endDate, filteredCorporate, filterAvailability]);
|
||||||
|
|
||||||
const toggleCode = (id: string) => {
|
const toggleCode = (id: string) => {
|
||||||
setSelectedCodes((prev) =>
|
setSelectedCodes((prev) =>
|
||||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
|
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAllCodes = (checked: boolean) => {
|
const toggleAllCodes = (checked: boolean) => {
|
||||||
if (checked)
|
if (checked)
|
||||||
return setSelectedCodes(
|
return setSelectedCodes(
|
||||||
filteredCodes.filter((x) => !x.userId).map((x) => x.code),
|
filteredCodes.filter((x) => !x.userId).map((x) => x.code)
|
||||||
);
|
);
|
||||||
|
|
||||||
return setSelectedCodes([]);
|
return setSelectedCodes([]);
|
||||||
@@ -137,7 +149,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
|
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("code", {
|
columnHelper.accessor("code", {
|
||||||
id: "code",
|
id: "codeCheckbox",
|
||||||
header: () => (
|
header: () => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
||||||
@@ -242,18 +254,20 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
}
|
}
|
||||||
options={users
|
options={users
|
||||||
.filter((x) =>
|
.filter((x) =>
|
||||||
["admin", "developer", "corporate"].includes(x.type),
|
["admin", "developer", "corporate"].includes(x.type)
|
||||||
)
|
)
|
||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${
|
label: `${
|
||||||
USER_TYPE_LABELS[x.type]
|
x.type === "corporate"
|
||||||
})`,
|
? x.corporateInformation?.companyInformation?.name || x.name
|
||||||
|
: x.name
|
||||||
|
} (${USER_TYPE_LABELS[x.type]})`,
|
||||||
value: x.id,
|
value: x.id,
|
||||||
user: x,
|
user: x,
|
||||||
}))}
|
}))}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setFilteredCorporate(
|
setFilteredCorporate(
|
||||||
value ? users.find((x) => x.id === value?.value) : undefined,
|
value ? users.find((x) => x.id === value?.value) : undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -267,10 +281,32 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
]}
|
]}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setFilterAvailability(
|
setFilterAvailability(
|
||||||
value ? (value.value as typeof filterAvailability) : undefined,
|
value ? (value.value as typeof filterAvailability) : undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ReactDatePicker
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
className="px-4 py-6 w-full 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
|
||||||
|
filterDate={(date: Date) =>
|
||||||
|
moment(date).isSameOrBefore(moment(new Date()))
|
||||||
|
}
|
||||||
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||||
|
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
||||||
|
if (finalDate) {
|
||||||
|
// basicly selecting a final day works as if I'm selecting the first
|
||||||
|
// minute of that day. this way it covers the whole day
|
||||||
|
setEndDate(moment(finalDate).endOf("day").toDate());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEndDate(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<span>{selectedCodes.length} code(s) selected</span>
|
<span>{selectedCodes.length} code(s) selected</span>
|
||||||
@@ -295,7 +331,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext(),
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,19 +4,38 @@ import useGroups from "@/hooks/useGroups";
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Type, User, userTypes, CorporateUser, Group } from "@/interfaces/user";
|
import { Type, User, userTypes, CorporateUser, Group } from "@/interfaces/user";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize, reverse } from "lodash";
|
import { capitalize, reverse } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs";
|
import {
|
||||||
|
BsArrowDown,
|
||||||
|
BsArrowDownUp,
|
||||||
|
BsArrowUp,
|
||||||
|
BsCheck,
|
||||||
|
BsCheckCircle,
|
||||||
|
BsEye,
|
||||||
|
BsFillExclamationOctagonFill,
|
||||||
|
BsPerson,
|
||||||
|
BsTrash,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { countries, TCountries } from "countries-list";
|
import { countries, TCountries } from "countries-list";
|
||||||
import countryCodes from "country-codes-list";
|
import countryCodes from "country-codes-list";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import {getUserCompanyName, isAgentUser, USER_TYPE_LABELS} from "@/resources/user";
|
import {
|
||||||
|
getUserCompanyName,
|
||||||
|
isAgentUser,
|
||||||
|
USER_TYPE_LABELS,
|
||||||
|
} from "@/resources/user";
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { isCorporateUser } from "@/resources/user";
|
import { isCorporateUser } from "@/resources/user";
|
||||||
@@ -26,9 +45,21 @@ import {asyncSorter} from "@/utils";
|
|||||||
import { exportListToExcel, UserListRow } from "@/utils/users";
|
import { exportListToExcel, UserListRow } from "@/utils/users";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
|
const searchFields = [
|
||||||
|
["name"],
|
||||||
|
["email"],
|
||||||
|
["corporateInformation", "companyInformation", "name"],
|
||||||
|
];
|
||||||
|
|
||||||
const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => {
|
const CompanyNameCell = ({
|
||||||
|
users,
|
||||||
|
user,
|
||||||
|
groups,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
users: User[];
|
||||||
|
groups: Group[];
|
||||||
|
}) => {
|
||||||
const [companyName, setCompanyName] = useState("");
|
const [companyName, setCompanyName] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -37,17 +68,34 @@ const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; grou
|
|||||||
setCompanyName(name);
|
setCompanyName(name);
|
||||||
}, [user, users, groups]);
|
}, [user, users, groups]);
|
||||||
|
|
||||||
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
return isLoading ? (
|
||||||
|
<span className="animate-pulse">Loading...</span>
|
||||||
|
) : (
|
||||||
|
<>{companyName}</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserList({user, filters = []}: {user: User; filters?: ((user: User) => boolean)[]}) {
|
export default function UserList({
|
||||||
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
user,
|
||||||
|
filters = [],
|
||||||
|
renderHeader,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
filters?: ((user: User) => boolean)[];
|
||||||
|
renderHeader?: (total: number) => JSX.Element;
|
||||||
|
}) {
|
||||||
|
const [showDemographicInformation, setShowDemographicInformation] =
|
||||||
|
useState(false);
|
||||||
const [sorter, setSorter] = useState<string>();
|
const [sorter, setSorter] = useState<string>();
|
||||||
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
|
|
||||||
const { users, reload } = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const {groups} = useGroups(user && (user?.type === "corporate" || user?.type === "teacher") ? user.id : undefined);
|
const { groups } = useGroups(
|
||||||
|
user && (user?.type === "corporate" || user?.type === "teacher")
|
||||||
|
? user.id
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -56,10 +104,13 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through";
|
if (today.isAfter(momentDate))
|
||||||
|
return "!text-mti-red-light font-bold line-through";
|
||||||
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
||||||
if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light";
|
if (today.add(2, "weeks").isAfter(momentDate))
|
||||||
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
return "!text-mti-rose-light";
|
||||||
|
if (today.add(1, "months").isAfter(momentDate))
|
||||||
|
return "!text-mti-orange-light";
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -67,11 +118,19 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
if (user && users) {
|
if (user && users) {
|
||||||
const filterUsers =
|
const filterUsers =
|
||||||
user.type === "corporate" || user.type === "teacher"
|
user.type === "corporate" || user.type === "teacher"
|
||||||
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
|
? users.filter((u) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(u.id)
|
||||||
|
)
|
||||||
: users;
|
: users;
|
||||||
|
|
||||||
const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers);
|
const filteredUsers = filters.reduce(
|
||||||
const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
|
(d, f) => d.filter(f),
|
||||||
|
filterUsers
|
||||||
|
);
|
||||||
|
const sortedUsers = await asyncSorter<User>(
|
||||||
|
filteredUsers,
|
||||||
|
sortFunction
|
||||||
|
);
|
||||||
console.log(sortedUsers);
|
console.log(sortedUsers);
|
||||||
|
|
||||||
setDisplayUsers([...sortedUsers]);
|
setDisplayUsers([...sortedUsers]);
|
||||||
@@ -81,7 +140,8 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
}, [user, users, sorter, groups]);
|
}, [user, users, sorter, groups]);
|
||||||
|
|
||||||
const deleteAccount = (user: User) => {
|
const deleteAccount = (user: User) => {
|
||||||
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`))
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
||||||
@@ -96,10 +156,20 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateAccountType = (user: User, type: Type) => {
|
const updateAccountType = (user: User, type: Type) => {
|
||||||
if (!confirm(`Are you sure you want to update ${user.name}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to update ${
|
||||||
|
user.name
|
||||||
|
}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {...user, type})
|
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
||||||
|
...user,
|
||||||
|
type,
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User type updated successfully!");
|
toast.success("User type updated successfully!");
|
||||||
reload();
|
reload();
|
||||||
@@ -111,7 +181,10 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
|
|
||||||
const verifyAccount = (user: User) => {
|
const verifyAccount = (user: User) => {
|
||||||
axios
|
axios
|
||||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {...user, isVerified: true})
|
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
||||||
|
...user,
|
||||||
|
isVerified: true,
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User verified successfully!");
|
toast.success("User verified successfully!");
|
||||||
reload();
|
reload();
|
||||||
@@ -124,9 +197,11 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
const toggleDisableAccount = (user: User) => {
|
const toggleDisableAccount = (user: User) => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${
|
`Are you sure you want to ${
|
||||||
|
user.status === "disabled" ? "enable" : "disable"
|
||||||
|
} ${
|
||||||
user.name
|
user.name
|
||||||
}'s account? This change is usually related to their payment state.`,
|
}'s account? This change is usually related to their payment state.`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
@@ -137,7 +212,11 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
status: user.status === "disabled" ? "active" : "disabled",
|
status: user.status === "disabled" ? "active" : "disabled",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`User ${user.status === "disabled" ? "enabled" : "disabled"} successfully!`);
|
toast.success(
|
||||||
|
`User ${
|
||||||
|
user.status === "disabled" ? "enabled" : "disabled"
|
||||||
|
} successfully!`
|
||||||
|
);
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -169,31 +248,48 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="transition ease-in duration-150"
|
leave="transition ease-in duration-150"
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1">
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
<Popover.Panel className="absolute z-10 w-screen right-1/2 translate-x-1/3 max-w-sm">
|
<Popover.Panel className="absolute z-10 w-screen right-1/2 translate-x-1/3 max-w-sm">
|
||||||
<div className="bg-white p-4 rounded-lg grid grid-cols-2 gap-2 w-full drop-shadow-xl">
|
<div className="bg-white p-4 rounded-lg grid grid-cols-2 gap-2 w-full drop-shadow-xl">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "student")}
|
onClick={() => updateAccountType(row.original, "student")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "student" || !PERMISSIONS.generateCode["student"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "student" ||
|
||||||
|
!PERMISSIONS.generateCode["student"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Student
|
Student
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "teacher")}
|
onClick={() => updateAccountType(row.original, "teacher")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "teacher" || !PERMISSIONS.generateCode["teacher"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "teacher" ||
|
||||||
|
!PERMISSIONS.generateCode["teacher"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Teacher
|
Teacher
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "corporate")}
|
onClick={() => updateAccountType(row.original, "corporate")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "corporate" || !PERMISSIONS.generateCode["corporate"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "corporate" ||
|
||||||
|
!PERMISSIONS.generateCode["corporate"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Corporate
|
Corporate
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "admin")}
|
onClick={() => updateAccountType(row.original, "admin")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "admin" || !PERMISSIONS.generateCode["admin"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "admin" ||
|
||||||
|
!PERMISSIONS.generateCode["admin"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Admin
|
Admin
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,16 +297,26 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
{!row.original.isVerified && PERMISSIONS.updateUser[row.original.type].includes(user.type) && (
|
{!row.original.isVerified &&
|
||||||
<div data-tip="Verify User" className="cursor-pointer tooltip" onClick={() => verifyAccount(row.original)}>
|
PERMISSIONS.updateUser[row.original.type].includes(user.type) && (
|
||||||
|
<div
|
||||||
|
data-tip="Verify User"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => verifyAccount(row.original)}
|
||||||
|
>
|
||||||
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{PERMISSIONS.updateUser[row.original.type].includes(user.type) && (
|
{PERMISSIONS.updateUser[row.original.type].includes(user.type) && (
|
||||||
<div
|
<div
|
||||||
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
|
data-tip={
|
||||||
|
row.original.status === "disabled"
|
||||||
|
? "Enable User"
|
||||||
|
: "Disable User"
|
||||||
|
}
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={() => toggleDisableAccount(row.original)}>
|
onClick={() => toggleDisableAccount(row.original)}
|
||||||
|
>
|
||||||
{row.original.status === "disabled" ? (
|
{row.original.status === "disabled" ? (
|
||||||
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
) : (
|
) : (
|
||||||
@@ -219,7 +325,11 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{PERMISSIONS.deleteUser[row.original.type].includes(user.type) && (
|
{PERMISSIONS.deleteUser[row.original.type].includes(user.type) && (
|
||||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteAccount(row.original)}>
|
<div
|
||||||
|
data-tip="Delete"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => deleteAccount(row.original)}
|
||||||
|
>
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -230,7 +340,10 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
const demographicColumns = [
|
const demographicColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "name"))}
|
||||||
|
>
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<SorterArrow name="name" />
|
<SorterArrow name="name" />
|
||||||
</button>
|
</button>
|
||||||
@@ -238,31 +351,49 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type) &&
|
PERMISSIONS.updateExpiryDate[row.original.type].includes(
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
user.type
|
||||||
|
) &&
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
)}
|
)}
|
||||||
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type) ? setSelectedUser(row.original) : null)}>
|
onClick={() =>
|
||||||
|
PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type)
|
||||||
|
? setSelectedUser(row.original)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.country", {
|
columnHelper.accessor("demographicInformation.country", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "country"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "country"))}
|
||||||
|
>
|
||||||
<span>Country</span>
|
<span>Country</span>
|
||||||
<SorterArrow name="country" />
|
<SorterArrow name="country" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
info.getValue()
|
info.getValue()
|
||||||
? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${
|
? `${
|
||||||
|
countryCodes.findOne("countryCode" as any, info.getValue()).flag
|
||||||
|
} ${
|
||||||
countries[info.getValue() as unknown as keyof TCountries].name
|
countries[info.getValue() as unknown as keyof TCountries].name
|
||||||
} (+${countryCodes.findOne("countryCode" as any, info.getValue()).countryCallingCode})`
|
} (+${
|
||||||
|
countryCodes.findOne("countryCode" as any, info.getValue())
|
||||||
|
.countryCallingCode
|
||||||
|
})`
|
||||||
: "Not available",
|
: "Not available",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
columnHelper.accessor("demographicInformation.phone", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}
|
||||||
|
>
|
||||||
<span>Phone</span>
|
<span>Phone</span>
|
||||||
<SorterArrow name="phone" />
|
<SorterArrow name="phone" />
|
||||||
</button>
|
</button>
|
||||||
@@ -270,20 +401,37 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
cell: (info) => info.getValue() || "Not available",
|
cell: (info) => info.getValue() || "Not available",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor((x) => (x.type === "corporate" ? x.demographicInformation?.position : x.demographicInformation?.employment), {
|
columnHelper.accessor(
|
||||||
|
(x) =>
|
||||||
|
x.type === "corporate"
|
||||||
|
? x.demographicInformation?.position
|
||||||
|
: x.demographicInformation?.employment,
|
||||||
|
{
|
||||||
id: "employment",
|
id: "employment",
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() =>
|
||||||
|
setSorter((prev) => selectSorter(prev, "employment"))
|
||||||
|
}
|
||||||
|
>
|
||||||
<span>Employment/Position</span>
|
<span>Employment/Position</span>
|
||||||
<SorterArrow name="employment" />
|
<SorterArrow name="employment" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "Not available",
|
cell: (info) =>
|
||||||
|
(info.row.original.type === "corporate"
|
||||||
|
? info.getValue()
|
||||||
|
: capitalize(info.getValue())) || "Not available",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
columnHelper.accessor("demographicInformation.gender", {
|
columnHelper.accessor("demographicInformation.gender", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}
|
||||||
|
>
|
||||||
<span>Gender</span>
|
<span>Gender</span>
|
||||||
<SorterArrow name="gender" />
|
<SorterArrow name="gender" />
|
||||||
</button>
|
</button>
|
||||||
@@ -293,7 +441,10 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setShowDemographicInformation((prev) => !prev)}
|
||||||
|
>
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -305,7 +456,10 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "name"))}
|
||||||
|
>
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<SorterArrow name="name" />
|
<SorterArrow name="name" />
|
||||||
</button>
|
</button>
|
||||||
@@ -313,17 +467,30 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type) &&
|
PERMISSIONS.updateExpiryDate[row.original.type].includes(
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
user.type
|
||||||
|
) &&
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
)}
|
)}
|
||||||
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type) ? setSelectedUser(row.original) : null)}>
|
onClick={() =>
|
||||||
{row.original.type === "corporate" ? row.original.corporateInformation?.companyInformation?.name || getValue() : getValue()}
|
PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type)
|
||||||
|
? setSelectedUser(row.original)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.original.type === "corporate"
|
||||||
|
? row.original.corporateInformation?.companyInformation?.name ||
|
||||||
|
getValue()
|
||||||
|
: getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "email"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "email"))}
|
||||||
|
>
|
||||||
<span>E-mail</span>
|
<span>E-mail</span>
|
||||||
<SorterArrow name="email" />
|
<SorterArrow name="email" />
|
||||||
</button>
|
</button>
|
||||||
@@ -331,17 +498,27 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type) &&
|
PERMISSIONS.updateExpiryDate[row.original.type].includes(
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
user.type
|
||||||
|
) &&
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
)}
|
)}
|
||||||
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type) ? setSelectedUser(row.original) : null)}>
|
onClick={() =>
|
||||||
|
PERMISSIONS.updateExpiryDate[row.original.type].includes(user.type)
|
||||||
|
? setSelectedUser(row.original)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("type", {
|
columnHelper.accessor("type", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "type"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "type"))}
|
||||||
|
>
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<SorterArrow name="type" />
|
<SorterArrow name="type" />
|
||||||
</button>
|
</button>
|
||||||
@@ -350,29 +527,54 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}
|
||||||
|
>
|
||||||
<span>Company Name</span>
|
<span>Company Name</span>
|
||||||
<SorterArrow name="companyName" />
|
<SorterArrow name="companyName" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => <CompanyNameCell user={info.row.original} users={users} groups={groups} />,
|
cell: (info) => (
|
||||||
|
<CompanyNameCell
|
||||||
|
user={info.row.original}
|
||||||
|
users={users}
|
||||||
|
groups={groups}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}
|
||||||
|
>
|
||||||
<span>Expiry Date</span>
|
<span>Expiry Date</span>
|
||||||
<SorterArrow name="expiryDate" />
|
<SorterArrow name="expiryDate" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
<span
|
||||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
className={clsx(
|
||||||
|
info.getValue()
|
||||||
|
? expirationDateColor(moment(info.getValue()).toDate())
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!info.getValue()
|
||||||
|
? "No expiry date"
|
||||||
|
: moment(info.getValue()).format("DD/MM/YYYY")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("isVerified", {
|
columnHelper.accessor("isVerified", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() =>
|
||||||
|
setSorter((prev) => selectSorter(prev, "verification"))
|
||||||
|
}
|
||||||
|
>
|
||||||
<span>Verification</span>
|
<span>Verification</span>
|
||||||
<SorterArrow name="verification" />
|
<SorterArrow name="verification" />
|
||||||
</button>
|
</button>
|
||||||
@@ -383,8 +585,9 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
info.getValue() && "!bg-mti-purple-light ",
|
info.getValue() && "!bg-mti-purple-light "
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -392,7 +595,10 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setShowDemographicInformation((prev) => !prev)}
|
||||||
|
>
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -412,15 +618,21 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
|
|
||||||
const sortFunction = async (a: User, b: User) => {
|
const sortFunction = async (a: User, b: User) => {
|
||||||
if (sorter === "name" || sorter === reverseString("name"))
|
if (sorter === "name" || sorter === reverseString("name"))
|
||||||
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
return sorter === "name"
|
||||||
|
? a.name.localeCompare(b.name)
|
||||||
|
: b.name.localeCompare(a.name);
|
||||||
|
|
||||||
if (sorter === "email" || sorter === reverseString("email"))
|
if (sorter === "email" || sorter === reverseString("email"))
|
||||||
return sorter === "email" ? a.email.localeCompare(b.email) : b.email.localeCompare(a.email);
|
return sorter === "email"
|
||||||
|
? a.email.localeCompare(b.email)
|
||||||
|
: b.email.localeCompare(a.email);
|
||||||
|
|
||||||
if (sorter === "type" || sorter === reverseString("type"))
|
if (sorter === "type" || sorter === reverseString("type"))
|
||||||
return sorter === "type"
|
return sorter === "type"
|
||||||
? userTypes.findIndex((t) => a.type === t) - userTypes.findIndex((t) => b.type === t)
|
? userTypes.findIndex((t) => a.type === t) -
|
||||||
: userTypes.findIndex((t) => b.type === t) - userTypes.findIndex((t) => a.type === t);
|
userTypes.findIndex((t) => b.type === t)
|
||||||
|
: userTypes.findIndex((t) => b.type === t) -
|
||||||
|
userTypes.findIndex((t) => a.type === t);
|
||||||
|
|
||||||
if (sorter === "verification" || sorter === reverseString("verification"))
|
if (sorter === "verification" || sorter === reverseString("verification"))
|
||||||
return sorter === "verification"
|
return sorter === "verification"
|
||||||
@@ -428,73 +640,138 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
||||||
|
|
||||||
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
||||||
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate) return sorter === "expiryDate" ? -1 : 1;
|
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate)
|
||||||
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return sorter === "expiryDate" ? 1 : -1;
|
return sorter === "expiryDate" ? -1 : 1;
|
||||||
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return 0;
|
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate)
|
||||||
if (moment(a.subscriptionExpirationDate).isAfter(b.subscriptionExpirationDate)) return sorter === "expiryDate" ? -1 : 1;
|
return sorter === "expiryDate" ? 1 : -1;
|
||||||
if (moment(b.subscriptionExpirationDate).isAfter(a.subscriptionExpirationDate)) return sorter === "expiryDate" ? 1 : -1;
|
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate)
|
||||||
|
return 0;
|
||||||
|
if (
|
||||||
|
moment(a.subscriptionExpirationDate).isAfter(
|
||||||
|
b.subscriptionExpirationDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorter === "expiryDate" ? -1 : 1;
|
||||||
|
if (
|
||||||
|
moment(b.subscriptionExpirationDate).isAfter(
|
||||||
|
a.subscriptionExpirationDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorter === "expiryDate" ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "country" || sorter === reverseString("country")) {
|
if (sorter === "country" || sorter === reverseString("country")) {
|
||||||
if (!a.demographicInformation?.country && b.demographicInformation?.country) return sorter === "country" ? -1 : 1;
|
if (
|
||||||
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;
|
!a.demographicInformation?.country &&
|
||||||
if (!a.demographicInformation?.country && !b.demographicInformation?.country) return 0;
|
b.demographicInformation?.country
|
||||||
|
)
|
||||||
|
return sorter === "country" ? -1 : 1;
|
||||||
|
if (
|
||||||
|
a.demographicInformation?.country &&
|
||||||
|
!b.demographicInformation?.country
|
||||||
|
)
|
||||||
|
return sorter === "country" ? 1 : -1;
|
||||||
|
if (
|
||||||
|
!a.demographicInformation?.country &&
|
||||||
|
!b.demographicInformation?.country
|
||||||
|
)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return sorter === "country"
|
return sorter === "country"
|
||||||
? a.demographicInformation!.country.localeCompare(b.demographicInformation!.country)
|
? a.demographicInformation!.country.localeCompare(
|
||||||
: b.demographicInformation!.country.localeCompare(a.demographicInformation!.country);
|
b.demographicInformation!.country
|
||||||
|
)
|
||||||
|
: b.demographicInformation!.country.localeCompare(
|
||||||
|
a.demographicInformation!.country
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "phone" || sorter === reverseString("phone")) {
|
if (sorter === "phone" || sorter === reverseString("phone")) {
|
||||||
if (!a.demographicInformation?.phone && b.demographicInformation?.phone) return sorter === "phone" ? -1 : 1;
|
if (!a.demographicInformation?.phone && b.demographicInformation?.phone)
|
||||||
if (a.demographicInformation?.phone && !b.demographicInformation?.phone) return sorter === "phone" ? 1 : -1;
|
return sorter === "phone" ? -1 : 1;
|
||||||
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone) return 0;
|
if (a.demographicInformation?.phone && !b.demographicInformation?.phone)
|
||||||
|
return sorter === "phone" ? 1 : -1;
|
||||||
|
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return sorter === "phone"
|
return sorter === "phone"
|
||||||
? a.demographicInformation!.phone.localeCompare(b.demographicInformation!.phone)
|
? a.demographicInformation!.phone.localeCompare(
|
||||||
: b.demographicInformation!.phone.localeCompare(a.demographicInformation!.phone);
|
b.demographicInformation!.phone
|
||||||
|
)
|
||||||
|
: b.demographicInformation!.phone.localeCompare(
|
||||||
|
a.demographicInformation!.phone
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "employment" || sorter === reverseString("employment")) {
|
if (sorter === "employment" || sorter === reverseString("employment")) {
|
||||||
const aSortingItem = a.type === "corporate" ? a.demographicInformation?.position : a.demographicInformation?.employment;
|
const aSortingItem =
|
||||||
const bSortingItem = b.type === "corporate" ? b.demographicInformation?.position : b.demographicInformation?.employment;
|
a.type === "corporate"
|
||||||
|
? a.demographicInformation?.position
|
||||||
|
: a.demographicInformation?.employment;
|
||||||
|
const bSortingItem =
|
||||||
|
b.type === "corporate"
|
||||||
|
? b.demographicInformation?.position
|
||||||
|
: b.demographicInformation?.employment;
|
||||||
|
|
||||||
if (!aSortingItem && bSortingItem) return sorter === "employment" ? -1 : 1;
|
if (!aSortingItem && bSortingItem)
|
||||||
if (aSortingItem && !bSortingItem) return sorter === "employment" ? 1 : -1;
|
return sorter === "employment" ? -1 : 1;
|
||||||
|
if (aSortingItem && !bSortingItem)
|
||||||
|
return sorter === "employment" ? 1 : -1;
|
||||||
if (!aSortingItem && !bSortingItem) return 0;
|
if (!aSortingItem && !bSortingItem) return 0;
|
||||||
|
|
||||||
return sorter === "employment" ? aSortingItem!.localeCompare(bSortingItem!) : bSortingItem!.localeCompare(aSortingItem!);
|
return sorter === "employment"
|
||||||
|
? aSortingItem!.localeCompare(bSortingItem!)
|
||||||
|
: bSortingItem!.localeCompare(aSortingItem!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "gender" || sorter === reverseString("gender")) {
|
if (sorter === "gender" || sorter === reverseString("gender")) {
|
||||||
if (!a.demographicInformation?.gender && b.demographicInformation?.gender) return sorter === "employment" ? -1 : 1;
|
if (!a.demographicInformation?.gender && b.demographicInformation?.gender)
|
||||||
if (a.demographicInformation?.gender && !b.demographicInformation?.gender) return sorter === "employment" ? 1 : -1;
|
return sorter === "employment" ? -1 : 1;
|
||||||
if (!a.demographicInformation?.gender && !b.demographicInformation?.gender) return 0;
|
if (a.demographicInformation?.gender && !b.demographicInformation?.gender)
|
||||||
|
return sorter === "employment" ? 1 : -1;
|
||||||
|
if (
|
||||||
|
!a.demographicInformation?.gender &&
|
||||||
|
!b.demographicInformation?.gender
|
||||||
|
)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return sorter === "gender"
|
return sorter === "gender"
|
||||||
? a.demographicInformation!.gender.localeCompare(b.demographicInformation!.gender)
|
? a.demographicInformation!.gender.localeCompare(
|
||||||
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
|
b.demographicInformation!.gender
|
||||||
|
)
|
||||||
|
: b.demographicInformation!.gender.localeCompare(
|
||||||
|
a.demographicInformation!.gender
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
||||||
const aCorporateName = getUserCompanyName(a, users, groups);
|
const aCorporateName = getUserCompanyName(a, users, groups);
|
||||||
const bCorporateName = getUserCompanyName(b, users, groups);
|
const bCorporateName = getUserCompanyName(b, users, groups);
|
||||||
if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1;
|
if (!aCorporateName && bCorporateName)
|
||||||
if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1;
|
return sorter === "companyName" ? -1 : 1;
|
||||||
|
if (aCorporateName && !bCorporateName)
|
||||||
|
return sorter === "companyName" ? 1 : -1;
|
||||||
if (!aCorporateName && !bCorporateName) return 0;
|
if (!aCorporateName && !bCorporateName) return 0;
|
||||||
|
|
||||||
return sorter === "companyName" ? aCorporateName.localeCompare(bCorporateName) : bCorporateName.localeCompare(aCorporateName);
|
return sorter === "companyName"
|
||||||
|
? aCorporateName.localeCompare(bCorporateName)
|
||||||
|
: bCorporateName.localeCompare(aCorporateName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {rows: filteredRows, renderSearch} = useListSearch<User>(searchFields, displayUsers);
|
const { rows: filteredRows, renderSearch } = useListSearch<User>(
|
||||||
|
searchFields,
|
||||||
|
displayUsers
|
||||||
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredRows,
|
data: filteredRows,
|
||||||
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
|
columns: (!showDemographicInformation
|
||||||
|
? defaultColumns
|
||||||
|
: demographicColumns) as any,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -502,36 +779,52 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
const csv = exportListToExcel(filteredRows, users, groups);
|
const csv = exportListToExcel(filteredRows, users, groups);
|
||||||
|
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
const file = new Blob([csv], {type: "text/plain"});
|
const file = new Blob([csv], { type: "text/csv" });
|
||||||
element.href = URL.createObjectURL(file);
|
element.href = URL.createObjectURL(file);
|
||||||
element.download = "users.xlsx";
|
element.download = "users.csv";
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
element.click();
|
element.click();
|
||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewStudentFilter = (x: User) => x.type === "student";
|
||||||
|
const viewTeacherFilter = (x: User) => x.type === "teacher";
|
||||||
|
const belongsToAdminFilter = (x: User) => {
|
||||||
|
if (!selectedUser) return false;
|
||||||
|
return groups
|
||||||
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewStudentFilterBelongsToAdmin = (x: User) =>
|
||||||
|
x.type === "student" && belongsToAdminFilter(x);
|
||||||
|
const viewTeacherFilterBelongsToAdmin = (x: User) =>
|
||||||
|
x.type === "teacher" && belongsToAdminFilter(x);
|
||||||
|
|
||||||
|
const renderUserCard = (selectedUser: User) => {
|
||||||
|
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
||||||
|
const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin);
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
|
||||||
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
|
|
||||||
<>
|
|
||||||
{selectedUser && (
|
|
||||||
<div className="w-full flex flex-col gap-8">
|
<div className="w-full flex flex-col gap-8">
|
||||||
<UserCard
|
<UserCard
|
||||||
loggedInUser={user}
|
loggedInUser={user}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
(selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher") &&
|
||||||
|
studentsFromAdmin.length > 0
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
filter: (x: User) => x.type === "student",
|
filter: viewStudentFilter,
|
||||||
});
|
});
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: belongsToAdminFilter,
|
||||||
groups
|
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(x.id),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push("/list/users");
|
router.push("/list/users");
|
||||||
@@ -539,19 +832,17 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "student"
|
(selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student") &&
|
||||||
|
teachersFromAdmin.length > 0
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
filter: (x: User) => x.type === "teacher",
|
filter: viewTeacherFilter,
|
||||||
});
|
});
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: belongsToAdminFilter,
|
||||||
groups
|
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(x.id),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push("/list/users");
|
router.push("/list/users");
|
||||||
@@ -585,13 +876,27 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</>
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderHeader && renderHeader(displayUsers.length)}
|
||||||
|
<div className="w-full">
|
||||||
|
<Modal
|
||||||
|
isOpen={!!selectedUser}
|
||||||
|
onClose={() => setSelectedUser(undefined)}
|
||||||
|
>
|
||||||
|
{selectedUser && renderUserCard(selectedUser)}
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<div className="w-full flex gap-2 items-end">
|
<div className="w-full flex gap-2 items-end">
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={downloadExcel}>
|
<Button
|
||||||
|
className="w-full max-w-[200px] mb-1"
|
||||||
|
variant="outline"
|
||||||
|
onClick={downloadExcel}
|
||||||
|
>
|
||||||
Download List
|
Download List
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -601,7 +906,12 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="py-4 px-4 text-left" key={header.id}>
|
<th className="py-4 px-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -609,10 +919,16 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
<tr
|
||||||
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||||
|
key={row.id}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -621,5 +937,6 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
redirect: {
|
redirect: {
|
||||||
destination: "/login",
|
destination: "/login",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,10 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
export default function UsersListPage() {
|
export default function UsersListPage() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]);
|
const [filters, clearFilters] = useFilterStore((state) => [
|
||||||
|
state.userFilters,
|
||||||
|
state.clearUserFilters,
|
||||||
|
]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,20 +59,25 @@ export default function UsersListPage() {
|
|||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={filters.map((f) => f.filter)}
|
||||||
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearFilters();
|
clearFilters();
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
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" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Users ({filters.map((f) => f.filter).reduce((d, f) => d.filter(f), users).length})</h2>
|
<h2 className="text-2xl font-semibold">Users ({total})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<UserList user={user} filters={filters.map((f) => f.filter)} />
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,14 +18,24 @@ import {sortByModule} from "@/utils/moduleUtils";
|
|||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { calculateBandScore } from "@/utils/score";
|
import { calculateBandScore } from "@/utils/score";
|
||||||
import {BsBook, BsClipboard, BsClock, BsHeadphones, BsMegaphone, BsPen, BsPersonDash, BsPersonFillX, BsXCircle} from "react-icons/bs";
|
import {
|
||||||
|
BsBook,
|
||||||
|
BsClipboard,
|
||||||
|
BsClock,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
BsPersonDash,
|
||||||
|
BsPersonFillX,
|
||||||
|
BsXCircle,
|
||||||
|
} from "react-icons/bs";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import { uuidv4 } from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||||
|
import useRecordStore from "@/stores/recordStore";
|
||||||
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
@@ -52,15 +62,28 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
|
const defaultSelectableCorporate = {
|
||||||
|
value: "",
|
||||||
|
label: "All",
|
||||||
|
};
|
||||||
|
|
||||||
export default function History({ user }: { user: User }) {
|
export default function History({ user }: { user: User }) {
|
||||||
const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
const [statsUserId, setStatsUserId] = useRecordStore((state) => [
|
||||||
|
state.selectedUser,
|
||||||
|
state.setSelectedUser,
|
||||||
|
]);
|
||||||
|
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||||
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
|
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
const [filter, setFilter] = useState<
|
||||||
|
"months" | "weeks" | "days" | "assignments"
|
||||||
|
>();
|
||||||
const { assignments } = useAssignments({});
|
const { assignments } = useAssignments({});
|
||||||
|
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
|
const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
|
||||||
const {groups} = useGroups(user.id);
|
const { groups: allGroups } = useGroups();
|
||||||
|
|
||||||
|
const groups = allGroups.filter((x) => x.admin === user.id);
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||||
@@ -83,12 +106,17 @@ export default function History({user}: {user: User}) {
|
|||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}),
|
})
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [stats, isStatsLoading]);
|
}, [stats, isStatsLoading]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// // just set this initially
|
||||||
|
// if (!statsUserId) setStatsUserId(user.id);
|
||||||
|
// }, []);
|
||||||
|
|
||||||
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
|
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
|
||||||
setFilter((prev) => (prev === value ? undefined : value));
|
setFilter((prev) => (prev === value ? undefined : value));
|
||||||
};
|
};
|
||||||
@@ -101,7 +129,8 @@ export default function History({user}: {user: User}) {
|
|||||||
const filteredStats: { [key: string]: Stat[] } = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
if (timestamp >= filterDate)
|
||||||
|
filteredStats[timestamp] = stats[timestamp];
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredStats;
|
return filteredStats;
|
||||||
@@ -111,8 +140,14 @@ export default function History({user}: {user: User}) {
|
|||||||
const filteredStats: { [key: string]: Stat[] } = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
if (
|
||||||
filteredStats[timestamp] = [...stats[timestamp].filter((s) => !!s.assignment)];
|
stats[timestamp]
|
||||||
|
.map((s) => s.assignment === undefined)
|
||||||
|
.includes(false)
|
||||||
|
)
|
||||||
|
filteredStats[timestamp] = [
|
||||||
|
...stats[timestamp].filter((s) => !!s.assignment),
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredStats;
|
return filteredStats;
|
||||||
@@ -128,8 +163,12 @@ export default function History({user}: {user: User}) {
|
|||||||
return date.format(formatter);
|
return date.format(formatter);
|
||||||
};
|
};
|
||||||
|
|
||||||
const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => {
|
const aggregateScoresByModule = (
|
||||||
const scores: {[key in Module]: {total: number; missing: number; correct: number}} = {
|
stats: Stat[]
|
||||||
|
): { module: Module; total: number; missing: number; correct: number }[] => {
|
||||||
|
const scores: {
|
||||||
|
[key in Module]: { total: number; missing: number; correct: number };
|
||||||
|
} = {
|
||||||
reading: {
|
reading: {
|
||||||
total: 0,
|
total: 0,
|
||||||
correct: 0,
|
correct: 0,
|
||||||
@@ -174,10 +213,21 @@ export default function History({user}: {user: User}) {
|
|||||||
if (!groupedStats) return <></>;
|
if (!groupedStats) return <></>;
|
||||||
|
|
||||||
const dateStats = groupedStats[timestamp];
|
const dateStats = groupedStats[timestamp];
|
||||||
const correct = dateStats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
const correct = dateStats.reduce(
|
||||||
const total = dateStats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
(accumulator, current) => accumulator + current.score.correct,
|
||||||
const aggregatedScores = aggregateScoresByModule(dateStats).filter((x) => x.total > 0);
|
0
|
||||||
const assignmentID = dateStats.reduce((_, current) => current.assignment as any, "");
|
);
|
||||||
|
const total = dateStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.score.total,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const aggregatedScores = aggregateScoresByModule(dateStats).filter(
|
||||||
|
(x) => x.total > 0
|
||||||
|
);
|
||||||
|
const assignmentID = dateStats.reduce(
|
||||||
|
(_, current) => current.assignment as any,
|
||||||
|
""
|
||||||
|
);
|
||||||
const assignment = assignments.find((a) => a.id === assignmentID);
|
const assignment = assignments.find((a) => a.id === assignmentID);
|
||||||
const isDisabled = dateStats.some((x) => x.isDisabled);
|
const isDisabled = dateStats.some((x) => x.isDisabled);
|
||||||
|
|
||||||
@@ -206,7 +256,7 @@ export default function History({user}: {user: User}) {
|
|||||||
exams
|
exams
|
||||||
.map((x) => x!)
|
.map((x) => x!)
|
||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map((x) => x!.module),
|
.map((x) => x!.module)
|
||||||
);
|
);
|
||||||
router.push("/exercises");
|
router.push("/exercises");
|
||||||
}
|
}
|
||||||
@@ -216,7 +266,7 @@ export default function History({user}: {user: User}) {
|
|||||||
const textColor = clsx(
|
const textColor = clsx(
|
||||||
correct / total >= 0.7 && "text-mti-purple",
|
correct / total >= 0.7 && "text-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
|
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
|
||||||
correct / total < 0.3 && "text-mti-rose",
|
correct / total < 0.3 && "text-mti-rose"
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
@@ -226,12 +276,18 @@ export default function History({user}: {user: User}) {
|
|||||||
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!!timeSpent && (
|
{!!timeSpent && (
|
||||||
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Time Spent">
|
<span
|
||||||
|
className="text-sm flex gap-2 items-center tooltip"
|
||||||
|
data-tip="Time Spent"
|
||||||
|
>
|
||||||
<BsClock /> {Math.floor(timeSpent / 60)} minutes
|
<BsClock /> {Math.floor(timeSpent / 60)} minutes
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!!inactivity && (
|
{!!inactivity && (
|
||||||
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Inactivity">
|
<span
|
||||||
|
className="text-sm flex gap-2 items-center tooltip"
|
||||||
|
data-tip="Inactivity"
|
||||||
|
>
|
||||||
<BsXCircle /> {Math.floor(inactivity / 60)} minutes
|
<BsXCircle /> {Math.floor(inactivity / 60)} minutes
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -240,7 +296,12 @@ export default function History({user}: {user: User}) {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<span className={textColor}>
|
<span className={textColor}>
|
||||||
Level{" "}
|
Level{" "}
|
||||||
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
|
{(
|
||||||
|
aggregatedLevels.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.level,
|
||||||
|
0
|
||||||
|
) / aggregatedLevels.length
|
||||||
|
).toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
{renderPdfIcon(session, textColor, textColor)}
|
{renderPdfIcon(session, textColor, textColor)}
|
||||||
</div>
|
</div>
|
||||||
@@ -257,8 +318,9 @@ export default function History({user}: {user: User}) {
|
|||||||
module === "listening" && "bg-ielts-listening",
|
module === "listening" && "bg-ielts-listening",
|
||||||
module === "writing" && "bg-ielts-writing",
|
module === "writing" && "bg-ielts-writing",
|
||||||
module === "speaking" && "bg-ielts-speaking",
|
module === "speaking" && "bg-ielts-speaking",
|
||||||
module === "level" && "bg-ielts-level",
|
module === "level" && "bg-ielts-level"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||||
@@ -271,7 +333,8 @@ export default function History({user}: {user: User}) {
|
|||||||
|
|
||||||
{assignment && (
|
{assignment && (
|
||||||
<span className="font-light text-sm">
|
<span className="font-light text-sm">
|
||||||
Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name}
|
Assignment: {assignment.name}, Teacher:{" "}
|
||||||
|
{users.find((u) => u.id === assignment.assigner)?.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -286,12 +349,15 @@ export default function History({user}: {user: User}) {
|
|||||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
||||||
isDisabled && "grayscale tooltip",
|
isDisabled && "grayscale tooltip",
|
||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 &&
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.7 &&
|
||||||
|
"hover:border-mti-red",
|
||||||
|
correct / total < 0.3 && "hover:border-mti-rose"
|
||||||
)}
|
)}
|
||||||
onClick={isDisabled ? () => null : selectExam}
|
onClick={isDisabled ? () => null : selectExam}
|
||||||
data-tip="This exam is still being evaluated..."
|
data-tip="This exam is still being evaluated..."
|
||||||
role="button">
|
role="button"
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -299,17 +365,86 @@ export default function History({user}: {user: User}) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:tooltip md:hidden",
|
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:tooltip md:hidden",
|
||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 &&
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.7 &&
|
||||||
|
"hover:border-mti-red",
|
||||||
|
correct / total < 0.3 && "hover:border-mti-rose"
|
||||||
)}
|
)}
|
||||||
data-tip="Your screen size is too small to view previous exams."
|
data-tip="Your screen size is too small to view previous exams."
|
||||||
role="button">
|
role="button"
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectableCorporates = [
|
||||||
|
defaultSelectableCorporate,
|
||||||
|
...users
|
||||||
|
.filter((x) => x.type === "corporate")
|
||||||
|
.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [selectedCorporate, setSelectedCorporate] = useState<string>(
|
||||||
|
defaultSelectableCorporate.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const getUsersList = (): User[] => {
|
||||||
|
if (selectedCorporate) {
|
||||||
|
// get groups for that corporate
|
||||||
|
const selectedCorporateGroups = allGroups.filter(
|
||||||
|
(x) => x.admin === selectedCorporate
|
||||||
|
);
|
||||||
|
|
||||||
|
// get the teacher ids for that group
|
||||||
|
const selectedCorporateGroupsParticipants =
|
||||||
|
selectedCorporateGroups.flatMap((x) => x.participants);
|
||||||
|
|
||||||
|
// // search for groups for these teachers
|
||||||
|
// const teacherGroups = allGroups.filter((x) => {
|
||||||
|
// return selectedCorporateGroupsParticipants.includes(x.admin);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const usersList = [
|
||||||
|
// ...selectedCorporateGroupsParticipants,
|
||||||
|
// ...teacherGroups.flatMap((x) => x.participants),
|
||||||
|
// ];
|
||||||
|
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) =>
|
||||||
|
users.find((y) => y.id === x)
|
||||||
|
) as User[];
|
||||||
|
return userListWithUsers.filter((x) => x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const corporateFilteredUserList = getUsersList();
|
||||||
|
|
||||||
|
const getSelectedUser = () => {
|
||||||
|
if (selectedCorporate) {
|
||||||
|
const userInCorporate = corporateFilteredUserList.find(
|
||||||
|
(x) => x.id === statsUserId
|
||||||
|
);
|
||||||
|
return userInCorporate || corporateFilteredUserList[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return users.find((x) => x.id === statsUserId) || user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedUser = getSelectedUser();
|
||||||
|
const selectedUserSelectValue = selectedUser
|
||||||
|
? {
|
||||||
|
value: selectedUser.id,
|
||||||
|
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
value: "",
|
||||||
|
label: "",
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -327,36 +462,90 @@ export default function History({user}: {user: User}) {
|
|||||||
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
||||||
<div className="xl:w-3/4">
|
<div className="xl:w-3/4">
|
||||||
{(user.type === "developer" || user.type === "admin") && (
|
{(user.type === "developer" || user.type === "admin") && (
|
||||||
|
<>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Corporate
|
||||||
|
</label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
options={selectableCorporates}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
value={selectableCorporates.find(
|
||||||
|
(x) => x.value === selectedCorporate
|
||||||
|
)}
|
||||||
|
onChange={(value) =>
|
||||||
|
setSelectedCorporate(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,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
></Select>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
User
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
options={corporateFilteredUserList.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
}))}
|
||||||
|
value={selectedUserSelectValue}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value)}
|
||||||
styles={{
|
styles={{
|
||||||
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",
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && (
|
{(user.type === "corporate" || user.type === "teacher") &&
|
||||||
|
groups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
User
|
||||||
|
</label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
options={users
|
options={users
|
||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
.filter((x) =>
|
||||||
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
groups.flatMap((y) => y.participants).includes(x.id)
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
)
|
||||||
|
.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
}))}
|
||||||
|
value={selectedUserSelectValue}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value)}
|
||||||
styles={{
|
styles={{
|
||||||
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",
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||||
@@ -364,49 +553,59 @@ export default function History({user}: {user: User}) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
filter === "assignments" && "!bg-mti-purple-light !text-white",
|
filter === "assignments" && "!bg-mti-purple-light !text-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleFilter("assignments")}>
|
onClick={() => toggleFilter("assignments")}
|
||||||
|
>
|
||||||
Assignments
|
Assignments
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
filter === "months" && "!bg-mti-purple-light !text-white",
|
filter === "months" && "!bg-mti-purple-light !text-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleFilter("months")}>
|
onClick={() => toggleFilter("months")}
|
||||||
|
>
|
||||||
Last month
|
Last month
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
filter === "weeks" && "!bg-mti-purple-light !text-white",
|
filter === "weeks" && "!bg-mti-purple-light !text-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleFilter("weeks")}>
|
onClick={() => toggleFilter("weeks")}
|
||||||
|
>
|
||||||
Last week
|
Last week
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
filter === "days" && "!bg-mti-purple-light !text-white",
|
filter === "days" && "!bg-mti-purple-light !text-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleFilter("days")}>
|
onClick={() => toggleFilter("days")}
|
||||||
|
>
|
||||||
Last day
|
Last day
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && (
|
{groupedStats &&
|
||||||
|
Object.keys(groupedStats).length > 0 &&
|
||||||
|
!isStatsLoading && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||||
{Object.keys(filterStatsByDate(groupedStats))
|
{Object.keys(filterStatsByDate(groupedStats))
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
.map(customContent)}
|
.map(customContent)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && (
|
{groupedStats &&
|
||||||
<span className="font-semibold ml-1">No record to display...</span>
|
Object.keys(groupedStats).length === 0 &&
|
||||||
|
!isStatsLoading && (
|
||||||
|
<span className="font-semibold ml-1">
|
||||||
|
No record to display...
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -68,16 +68,40 @@ const SOURCE_OPTIONS = [
|
|||||||
{value: "platform", label: "Platform"},
|
{value: "platform", label: "Platform"},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type CustomStatus = TicketStatus | "all" | "pending";
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = [{
|
||||||
|
label: 'Pending',
|
||||||
|
value: 'pending',
|
||||||
|
filter: (x: Ticket) => x.status !== 'completed',
|
||||||
|
}, {
|
||||||
|
label: 'All',
|
||||||
|
value: 'all',
|
||||||
|
filter: (x: Ticket) => true,
|
||||||
|
}, {
|
||||||
|
label: 'Completed',
|
||||||
|
value: 'completed',
|
||||||
|
filter: (x: Ticket) => x.status === 'completed',
|
||||||
|
}, {
|
||||||
|
label: 'In Progress',
|
||||||
|
value: 'in-progress',
|
||||||
|
filter: (x: Ticket) => x.status === 'in-progress',
|
||||||
|
}, {
|
||||||
|
label: 'Submitted',
|
||||||
|
value: 'submitted',
|
||||||
|
filter: (x: Ticket) => x.status === 'submitted',
|
||||||
|
}]
|
||||||
|
|
||||||
export default function Tickets() {
|
export default function Tickets() {
|
||||||
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
|
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
|
||||||
const [selectedTicket, setSelectedTicket] = useState<Ticket>();
|
const [selectedTicket, setSelectedTicket] = useState<Ticket>();
|
||||||
const [assigneeFilter, setAssigneeFilter] = useState<string>();
|
const [assigneeFilter, setAssigneeFilter] = useState<string>();
|
||||||
const [sourceFilter, setSourceFilter] = useState<Source>("");
|
const [sourceFilter, setSourceFilter] = useState<Source>("");
|
||||||
|
// const [statusFilter, setStatusFilter] = useState<CustomStatus>('pending');
|
||||||
const [dateSorting, setDateSorting] = useState<"asc" | "desc">("desc");
|
const [dateSorting, setDateSorting] = useState<"asc" | "desc">("desc");
|
||||||
|
|
||||||
const [typeFilter, setTypeFilter] = useState<TicketType>();
|
const [typeFilter, setTypeFilter] = useState<TicketType>();
|
||||||
const [statusFilter, setStatusFilter] = useState<TicketStatus>();
|
const [statusFilter, setStatusFilter] = useState<CustomStatus>('pending');
|
||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
@@ -91,7 +115,10 @@ export default function Tickets() {
|
|||||||
const filters = [];
|
const filters = [];
|
||||||
if (user?.type === "agent") filters.push((x: Ticket) => x.assignedTo === user.id);
|
if (user?.type === "agent") filters.push((x: Ticket) => x.assignedTo === user.id);
|
||||||
if (typeFilter) filters.push((x: Ticket) => x.type === typeFilter);
|
if (typeFilter) filters.push((x: Ticket) => x.type === typeFilter);
|
||||||
if (statusFilter) filters.push((x: Ticket) => x.status === statusFilter);
|
if (statusFilter) {
|
||||||
|
const filter = STATUS_OPTIONS.find(x => x.value === statusFilter)?.filter;
|
||||||
|
if (filter) filters.push(filter);
|
||||||
|
}
|
||||||
if (assigneeFilter) filters.push((x: Ticket) => x.assignedTo === assigneeFilter);
|
if (assigneeFilter) filters.push((x: Ticket) => x.assignedTo === assigneeFilter);
|
||||||
if (sourceFilter) {
|
if (sourceFilter) {
|
||||||
if (sourceFilter === "webpage") filters.push((x: Ticket) => fromHomepage.some((r) => r.test(x.reportedFrom)));
|
if (sourceFilter === "webpage") filters.push((x: Ticket) => fromHomepage.some((r) => r.test(x.reportedFrom)));
|
||||||
@@ -214,17 +241,9 @@ export default function Tickets() {
|
|||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">Status</label>
|
<label className="text-mti-gray-dim text-base font-normal">Status</label>
|
||||||
<Select
|
<Select
|
||||||
options={Object.keys(TicketStatusLabel).map((x) => ({
|
options={STATUS_OPTIONS}
|
||||||
value: x,
|
|
||||||
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
|
|
||||||
}))}
|
|
||||||
value={
|
value={
|
||||||
statusFilter
|
STATUS_OPTIONS.find((x) => x.value === statusFilter)
|
||||||
? {
|
|
||||||
value: statusFilter,
|
|
||||||
label: TicketStatusLabel[statusFilter],
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
onChange={(value) => setStatusFilter((value?.value as TicketStatus) ?? undefined)}
|
onChange={(value) => setStatusFilter((value?.value as TicketStatus) ?? undefined)}
|
||||||
isClearable
|
isClearable
|
||||||
|
|||||||
18
src/stores/recordStore.ts
Normal file
18
src/stores/recordStore.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import {create} from "zustand";
|
||||||
|
|
||||||
|
export interface RecordState {
|
||||||
|
selectedUser?: string;
|
||||||
|
setSelectedUser: (selectedUser: string | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState = {
|
||||||
|
selectedUser: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordStore = create<RecordState>((set) => ({
|
||||||
|
...initialState,
|
||||||
|
setSelectedUser: (selectedUser: string | undefined) => set(() => ({selectedUser})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default recordStore;
|
||||||
Reference in New Issue
Block a user