Merged in bug-fixing-19-Jun-24 (pull request #52)

Bug fixing 19 Jun 24

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-06-25 11:47:24 +00:00
committed by Tiago Ribeiro
12 changed files with 3684 additions and 2490 deletions

View File

@@ -1,118 +1,219 @@
import {User} from "@/interfaces/user"; import { User } from "@/interfaces/user";
import Link from "next/link"; import Link from "next/link";
import FocusLayer from "@/components/FocusLayer"; import FocusLayer from "@/components/FocusLayer";
import {preventNavigation} from "@/utils/navigation.disabled"; import { preventNavigation } from "@/utils/navigation.disabled";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {BsList, BsQuestionCircle, BsQuestionCircleFill} from "react-icons/bs"; import { BsList, BsQuestionCircle, BsQuestionCircleFill } from "react-icons/bs";
import clsx from "clsx"; import clsx from "clsx";
import moment from "moment"; import moment from "moment";
import MobileMenu from "./MobileMenu"; import MobileMenu from "./MobileMenu";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import {Type} from "@/interfaces/user"; import { Type } from "@/interfaces/user";
import {USER_TYPE_LABELS} from "@/resources/user"; import { USER_TYPE_LABELS } from "@/resources/user";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import {isUserFromCorporate} from "@/utils/groups"; import { isUserFromCorporate } from "@/utils/groups";
import Button from "./Low/Button"; import Button from "./Low/Button";
import Modal from "./Modal"; import Modal from "./Modal";
import Input from "./Low/Input"; import Input from "./Low/Input";
import TicketSubmission from "./High/TicketSubmission"; import TicketSubmission from "./High/TicketSubmission";
import { Module } from "@/interfaces";
import 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;
focusMode?: boolean; focusMode?: boolean;
onFocusLayerMouseEnter?: () => void; onFocusLayerMouseEnter?: () => void;
path: string; path: string;
} }
/* 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({
const [isMenuOpen, setIsMenuOpen] = useState(false); user,
const [disablePaymentPage, setDisablePaymentPage] = useState(true); path,
const [isTicketOpen, setIsTicketOpen] = useState(false); navDisabled = false,
focusMode = false,
onFocusLayerMouseEnter,
}: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [disablePaymentPage, setDisablePaymentPage] = useState(true);
const [isTicketOpen, setIsTicketOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const disableNavigation = preventNavigation(navDisabled, focusMode); const disableNavigation = preventNavigation(navDisabled, focusMode);
const expirationDateColor = (date: Date) => { const expirationDateColor = (date: Date) => {
const momentDate = moment(date); const momentDate = moment(date);
const today = moment(new Date()); const today = moment(new Date());
if (today.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 = () => {
if (!user.subscriptionExpirationDate) return false; if (!user.subscriptionExpirationDate) return false;
const momentDate = moment(user.subscriptionExpirationDate); const momentDate = moment(user.subscriptionExpirationDate);
const today = moment(new Date()); const today = moment(new Date());
return today.add(7, "days").isAfter(momentDate); return today.add(7, "days").isAfter(momentDate);
}; };
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);
}, [user]); isUserFromCorporate(user.id).then((result) =>
setDisablePaymentPage(result)
);
}, [user]);
return ( const badges = [
<> {
<Modal isOpen={isTicketOpen} onClose={() => setIsTicketOpen(false)} title="Submit a ticket"> module: "reading",
<TicketSubmission user={user} page={router.asPath} onClose={() => setIsTicketOpen(false)} /> icon: () => <BsBook className="h-4 w-4 text-white" />,
</Modal> achieved: user.levels.reading >= user.desiredLevels.reading,
},
{user && ( {
<MobileMenu disableNavigation={disableNavigation} path={path} isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)} user={user} /> module: "listening",
)} icon: () => <BsHeadphones className="h-4 w-4 text-white" />,
<header className="-md:justify-between -md:px-4 relative flex w-full items-center bg-transparent py-2 md:gap-12 md:py-4"> achieved: user.levels.listening >= user.desiredLevels.listening,
<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" /> {
<h1 className="-md:hidden w-1/6 text-2xl font-bold">EnCoach</h1> module: "writing",
</Link> icon: () => <BsPen className="h-4 w-4 text-white" />,
<div className="flex items-center justify-end gap-4 md:mr-8 md:w-5/6"> achieved: user.levels.writing >= user.desiredLevels.writing,
{/* OPEN TICKET SYSTEM */} },
<button {
className={clsx( module: "speaking",
"border-mti-purple-light tooltip tooltip-bottom flex h-8 w-8 flex-col items-center justify-center rounded-full border p-1", icon: () => <BsMegaphone className="h-4 w-4 text-white" />,
"hover:bg-mti-purple-light transition duration-300 ease-in-out hover:text-white z-20", achieved: user.levels.speaking >= user.desiredLevels.speaking,
)} },
data-tip="Submit a help/feedback ticket" {
onClick={() => setIsTicketOpen(true)}> module: "level",
<BsQuestionCircleFill /> icon: () => <BsClipboard className="h-4 w-4 text-white" />,
</button> achieved: user.levels.level >= user.desiredLevels.level,
},
];
{showExpirationDate() && ( return (
<Link <>
href={!!user.subscriptionExpirationDate && !disablePaymentPage ? "/payment" : ""} <Modal
data-tip="Expiry date" isOpen={isTicketOpen}
className={clsx( onClose={() => setIsTicketOpen(false)}
"flex w-fit cursor-pointer justify-center rounded-full border px-6 py-2 text-sm font-normal focus:outline-none", title="Submit a ticket"
"tooltip tooltip-bottom transition duration-300 ease-in-out", >
!user.subscriptionExpirationDate <TicketSubmission
? "bg-mti-green-ultralight border-mti-green-light" user={user}
: expirationDateColor(user.subscriptionExpirationDate), page={router.asPath}
"border-mti-gray-platinum bg-white", onClose={() => setIsTicketOpen(false)}
)}> />
{!user.subscriptionExpirationDate && "Unlimited"} </Modal>
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
</Link> {user && (
)} <MobileMenu
<Link href={disableNavigation ? "" : "/profile"} className="-md:hidden flex items-center justify-end gap-6"> disableNavigation={disableNavigation}
<img src={user.profilePicture} alt={user.name} className="h-10 w-10 rounded-full object-cover" /> path={path}
<span className="-md:hidden text-right"> isOpen={isMenuOpen}
{user.type === "corporate" ? `${user.corporateInformation?.companyInformation.name} |` : ""} {user.name} |{" "} onClose={() => setIsMenuOpen(false)}
{USER_TYPE_LABELS[user.type]} user={user}
</span> />
</Link> )}
<div className="cursor-pointer md:hidden" onClick={() => setIsMenuOpen(true)}> <header className="-md:justify-between -md:px-4 relative flex w-full items-center bg-transparent py-2 md:gap-12 md:py-4">
<BsList className="text-mti-purple-light h-8 w-8" /> <Link
</div> href={disableNavigation ? "" : "/"}
</div> className=" flex items-center gap-8 md:px-8"
{focusMode && <FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />} >
</header> <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>
); </Link>
<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 */}
<button
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",
"hover:bg-mti-purple-light transition duration-300 ease-in-out hover:text-white z-20"
)}
data-tip="Submit a help/feedback ticket"
onClick={() => setIsTicketOpen(true)}
>
<BsQuestionCircleFill />
</button>
{showExpirationDate() && (
<Link
href={
!!user.subscriptionExpirationDate && !disablePaymentPage
? "/payment"
: ""
}
data-tip="Expiry date"
className={clsx(
"flex w-fit cursor-pointer justify-center rounded-full border px-6 py-2 text-sm font-normal focus:outline-none",
"tooltip tooltip-bottom transition duration-300 ease-in-out",
!user.subscriptionExpirationDate
? "bg-mti-green-ultralight border-mti-green-light"
: expirationDateColor(user.subscriptionExpirationDate),
"border-mti-gray-platinum bg-white"
)}
>
{!user.subscriptionExpirationDate && "Unlimited"}
{user.subscriptionExpirationDate &&
moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
</Link>
)}
<Link
href={disableNavigation ? "" : "/profile"}
className="-md:hidden flex items-center justify-end gap-6"
>
<img
src={user.profilePicture}
alt={user.name}
className="h-10 w-10 rounded-full object-cover"
/>
<span className="-md:hidden text-right">
{user.type === "corporate"
? `${user.corporateInformation?.companyInformation.name} |`
: ""}{" "}
{user.name} | {USER_TYPE_LABELS[user.type]}
</span>
</Link>
<div
className="cursor-pointer md:hidden"
onClick={() => setIsMenuOpen(true)}
>
<BsList className="text-mti-purple-light h-8 w-8" />
</div>
</div>
{focusMode && (
<FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />
)}
</header>
</>
);
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,325 +2,395 @@
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {CorporateUser, Group, Stat, User} from "@/interfaces/user"; import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList"; 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 { import {
BsArrowLeft, BsArrowLeft,
BsClipboard2Data, BsClipboard2Data,
BsClipboard2DataFill, BsClipboard2DataFill,
BsClock, BsClock,
BsGlobeCentralSouthAsia, BsGlobeCentralSouthAsia,
BsPaperclip, BsPaperclip,
BsPerson, BsPerson,
BsPersonAdd, BsPersonAdd,
BsPersonFill, BsPersonFill,
BsPersonFillGear, BsPersonFillGear,
BsPersonGear, BsPersonGear,
BsPencilSquare, BsPencilSquare,
BsPersonBadge, BsPersonBadge,
BsPersonCheck, BsPersonCheck,
BsPeople, BsPeople,
} from "react-icons/bs"; } 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 {calculateAverageLevel, calculateBandScore} from "@/utils/score"; import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils"; import { MODULE_ARRAY } from "@/utils/moduleUtils";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {groupByExam} from "@/utils/stats"; import { groupByExam } from "@/utils/stats";
import IconCard from "./IconCard"; import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList"; import GroupList from "@/pages/(admin)/Lists/GroupList";
import useFilterStore from "@/stores/listFilterStore"; import useFilterStore from "@/stores/listFilterStore";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import useCodes from "@/hooks/useCodes"; import useCodes from "@/hooks/useCodes";
interface Props { interface Props {
user: CorporateUser; user: CorporateUser;
} }
export default function CorporateDashboard({user}: Props) { export default function CorporateDashboard({ user }: Props) {
const [page, setPage] = useState(""); const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>(); const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const {stats} = useStats(); const { stats } = useStats();
const {users, reload} = useUsers(); const { users, reload } = useUsers();
const {codes} = useCodes(user.id); const { codes } = useCodes(user.id);
const {groups} = useGroups(user.id); const { groups } = useGroups(user.id);
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
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" /> >
<div className="flex flex-col gap-1 items-start"> <img
<span>{displayUser.name}</span> src={displayUser.profilePicture}
<span className="text-sm opacity-75">{displayUser.email}</span> alt={displayUser.name}
</div> className="rounded-full w-10 h-10"
</div> />
); <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && x.type === "student" &&
(!!selectedUser (!!selectedUser
? groups ? groups
.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));
return ( return (
<> <UserList
<div className="flex flex-col gap-4"> user={user}
<div filters={[filter]}
onClick={() => setPage("")} renderHeader={(total) => (
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> <div className="flex flex-col gap-4">
<BsArrowLeft className="text-xl" /> <div
<span>Back</span> onClick={() => setPage("")}
</div> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
<h2 className="text-2xl font-semibold">Students ({users.filter(filter).length})</h2> >
</div> <BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
};
<UserList user={user} filters={[filter]} /> const TeachersList = () => {
</> const filter = (x: User) =>
); x.type === "teacher" &&
}; (!!selectedUser
? groups
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id) || false
: groups.flatMap((g) => g.participants).includes(x.id));
const TeachersList = () => { return (
const filter = (x: User) => <UserList
x.type === "teacher" && user={user}
(!!selectedUser filters={[filter]}
? groups renderHeader={(total) => (
.filter((g) => g.admin === selectedUser.id) <div className="flex flex-col gap-4">
.flatMap((g) => g.participants) <div
.includes(x.id) || false onClick={() => setPage("")}
: groups.flatMap((g) => g.participants).includes(x.id)); className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
</div>
)}
/>
);
};
return ( const GroupsList = () => {
<> const filter = (x: Group) =>
<div className="flex flex-col gap-4"> x.admin === user.id || x.participants.includes(user.id);
<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">Teachers ({users.filter(filter).length})</h2>
</div>
<UserList user={user} filters={[filter]} /> return (
</> <>
); <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">
Groups ({groups.filter(filter).length})
</h2>
</div>
const GroupsList = () => { <GroupList user={user} />
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id); </>
);
};
return ( const averageLevelCalculator = (studentStats: Stat[]) => {
<> const formattedStats = studentStats
<div className="flex flex-col gap-4"> .map((s) => ({
<div focus: users.find((u) => u.id === s.user)?.focus,
onClick={() => setPage("")} score: s.score,
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> module: s.module,
<BsArrowLeft className="text-xl" /> }))
<span>Back</span> .filter((f) => !!f.focus);
</div> const bandScores = formattedStats.map((s) => ({
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2> module: s.module,
</div> level: calculateBandScore(
s.score.correct,
s.score.total,
s.module,
s.focus!
),
}));
<GroupList user={user} /> const levels: { [key in Module]: number } = {
</> reading: 0,
); listening: 0,
}; writing: 0,
speaking: 0,
level: 0,
};
bandScores.forEach((b) => (levels[b.module] += b.level));
const averageLevelCalculator = (studentStats: Stat[]) => { return calculateAverageLevel(levels);
const formattedStats = studentStats };
.map((s) => ({focus: users.find((u) => u.id === s.user)?.focus, score: s.score, module: s.module}))
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
module: s.module,
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
}));
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0}; const DefaultDashboard = () => (
bandScores.forEach((b) => (levels[b.module] += b.level)); <>
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
<IconCard
onClick={() => setPage("students")}
Icon={BsPersonFill}
label="Students"
value={users.filter(studentFilter).length}
color="purple"
/>
<IconCard
onClick={() => setPage("teachers")}
Icon={BsPencilSquare}
label="Teachers"
value={users.filter(teacherFilter).length}
color="purple"
/>
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
value={averageLevelCalculator(
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"
/>
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${codes.length}/${
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
color="rose"
/>
</section>
return calculateAverageLevel(levels); <section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
}; <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(studentFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest teachers</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(teacherFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(studentFilter)
.sort(
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(studentFilter)
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length -
Object.keys(groupByExam(getStatsByStudent(a))).length
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
</section>
</>
);
const DefaultDashboard = () => ( return (
<> <>
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center"> <Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<IconCard <>
onClick={() => setPage("students")} {selectedUser && (
Icon={BsPersonFill} <div className="w-full flex flex-col gap-8">
label="Students" <UserCard
value={users.filter(studentFilter).length} loggedInUser={user}
color="purple" onClose={(shouldReload) => {
/> setSelectedUser(undefined);
<IconCard if (shouldReload) reload();
onClick={() => setPage("teachers")} }}
Icon={BsPencilSquare} onViewStudents={
label="Teachers" selectedUser.type === "corporate" ||
value={users.filter(teacherFilter).length} selectedUser.type === "teacher"
color="purple" ? () => {
/> appendUserFilters({
<IconCard id: "view-students",
Icon={BsClipboard2Data} filter: (x: User) => x.type === "student",
label="Exams Performed" });
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length} appendUserFilters({
color="purple" id: "belongs-to-admin",
/> filter: (x: User) =>
<IconCard groups
Icon={BsPaperclip} .filter(
label="Average Level" (g) =>
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} g.admin === selectedUser.id ||
color="purple" g.participants.includes(selectedUser.id)
/> )
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" /> .flatMap((g) => g.participants)
<IconCard .includes(x.id),
Icon={BsPersonCheck} });
label="User Balance"
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between"> router.push("/list/users");
<div className="bg-white shadow flex flex-col rounded-xl w-full"> }
<span className="p-4">Latest students</span> : undefined
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> }
{users onViewTeachers={
.filter(studentFilter) selectedUser.type === "corporate" ||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) selectedUser.type === "student"
.map((x) => ( ? () => {
<UserDisplay key={x.id} {...x} /> appendUserFilters({
))} id: "view-teachers",
</div> filter: (x: User) => x.type === "teacher",
</div> });
<div className="bg-white shadow flex flex-col rounded-xl w-full"> appendUserFilters({
<span className="p-4">Latest teachers</span> id: "belongs-to-admin",
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> filter: (x: User) =>
{users groups
.filter(teacherFilter) .filter(
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) (g) =>
.map((x) => ( g.admin === selectedUser.id ||
<UserDisplay key={x.id} {...x} /> g.participants.includes(selectedUser.id)
))} )
</div> .flatMap((g) => g.participants)
</div> .includes(x.id),
<div className="bg-white shadow flex flex-col rounded-xl w-full"> });
<span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(studentFilter)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
<div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(studentFilter)
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>
</section>
</>
);
return ( router.push("/list/users");
<> }
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}> : undefined
<> }
{selectedUser && ( user={selectedUser}
<div className="w-full flex flex-col gap-8"> />
<UserCard </div>
loggedInUser={user} )}
onClose={(shouldReload) => { </>
setSelectedUser(undefined); </Modal>
if (shouldReload) reload(); {page === "students" && <StudentsList />}
}} {page === "teachers" && <TeachersList />}
onViewStudents={ {page === "groups" && <GroupsList />}
selectedUser.type === "corporate" || selectedUser.type === "teacher" {page === "" && <DefaultDashboard />}
? () => { </>
appendUserFilters({ );
id: "view-students",
filter: (x: User) => x.type === "student",
});
appendUserFilters({
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
router.push("/list/users");
}
: undefined
}
onViewTeachers={
selectedUser.type === "corporate" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-teachers",
filter: (x: User) => x.type === "teacher",
});
appendUserFilters({
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
router.push("/list/users");
}
: undefined
}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
{page === "students" && <StudentsList />}
{page === "teachers" && <TeachersList />}
{page === "groups" && <GroupsList />}
{page === "" && <DefaultDashboard />}
</>
);
} }

View 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;

View File

@@ -2,380 +2,477 @@
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {CorporateUser, Group, Stat, User} from "@/interfaces/user"; import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList"; 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 { import {
BsArrowLeft, BsArrowLeft,
BsArrowRepeat, BsArrowRepeat,
BsClipboard2Data, BsClipboard2Data,
BsClipboard2DataFill, BsClipboard2DataFill,
BsClipboard2Heart, BsClipboard2Heart,
BsClipboard2X, BsClipboard2X,
BsClipboardPulse, BsClipboardPulse,
BsClock, BsClock,
BsEnvelopePaper, BsEnvelopePaper,
BsGlobeCentralSouthAsia, BsGlobeCentralSouthAsia,
BsPaperclip, BsPaperclip,
BsPeople, BsPeople,
BsPerson, BsPerson,
BsPersonAdd, BsPersonAdd,
BsPersonFill, BsPersonFill,
BsPersonFillGear, BsPersonFillGear,
BsPersonGear, BsPersonGear,
BsPlus, BsPlus,
BsRepeat, BsRepeat,
BsRepeat1, BsRepeat1,
} from "react-icons/bs"; } 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 {calculateAverageLevel, calculateBandScore} from "@/utils/score"; import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils"; import { MODULE_ARRAY } from "@/utils/moduleUtils";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {groupByExam} from "@/utils/stats"; import { groupByExam } from "@/utils/stats";
import IconCard from "./IconCard"; import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList"; import GroupList from "@/pages/(admin)/Lists/GroupList";
import useAssignments from "@/hooks/useAssignments"; import useAssignments from "@/hooks/useAssignments";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import AssignmentCard from "./AssignmentCard"; import AssignmentCard from "./AssignmentCard";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import clsx from "clsx"; import clsx from "clsx";
import ProgressBar from "@/components/Low/ProgressBar"; import ProgressBar from "@/components/Low/ProgressBar";
import AssignmentCreator from "./AssignmentCreator"; import AssignmentCreator from "./AssignmentCreator";
import AssignmentView from "./AssignmentView"; import AssignmentView from "./AssignmentView";
import {getUserCorporate} from "@/utils/groups"; import { getUserCorporate } from "@/utils/groups";
interface Props { interface Props {
user: User; user: User;
} }
export default function TeacherDashboard({user}: Props) { export default function TeacherDashboard({ user }: Props) {
const [page, setPage] = useState(""); const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>(); const [selectedUser, setSelectedUser] = useState<User>();
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 === "");
}, [selectedUser, page]); }, [selectedUser, page]);
useEffect(() => { useEffect(() => {
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" /> >
<div className="flex flex-col gap-1 items-start"> <img
<span>{displayUser.name}</span> src={displayUser.profilePicture}
<span className="text-sm opacity-75">{displayUser.email}</span> alt={displayUser.name}
</div> className="rounded-full w-10 h-10"
</div> />
); <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && x.type === "student" &&
(!!selectedUser (!!selectedUser
? groups ? groups
.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));
return ( return (
<> <UserList
<div className="flex flex-col gap-4"> user={user}
<div filters={[filter]}
onClick={() => setPage("")} renderHeader={(total) => (
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> <div className="flex flex-col gap-4">
<BsArrowLeft className="text-xl" /> <div
<span>Back</span> onClick={() => setPage("")}
</div> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
<h2 className="text-2xl font-semibold">Students ({users.filter(filter).length})</h2> >
</div> <BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Students ({total})</h2>
</div>
)}
/>
);
};
<UserList user={user} filters={[filter]} /> const GroupsList = () => {
</> const filter = (x: Group) =>
); x.admin === user.id || x.participants.includes(user.id);
};
const GroupsList = () => { return (
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id); <>
<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">
Groups ({groups.filter(filter).length})
</h2>
</div>
return ( <GroupList user={user} />
<> </>
<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">Groups ({groups.filter(filter).length})</h2>
</div>
<GroupList user={user} /> const averageLevelCalculator = (studentStats: Stat[]) => {
</> const formattedStats = studentStats
); .map((s) => ({
}; focus: users.find((u) => u.id === s.user)?.focus,
score: s.score,
module: s.module,
}))
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
module: s.module,
level: calculateBandScore(
s.score.correct,
s.score.total,
s.module,
s.focus!
),
}));
const averageLevelCalculator = (studentStats: Stat[]) => { const levels: { [key in Module]: number } = {
const formattedStats = studentStats reading: 0,
.map((s) => ({focus: users.find((u) => u.id === s.user)?.focus, score: s.score, module: s.module})) listening: 0,
.filter((f) => !!f.focus); writing: 0,
const bandScores = formattedStats.map((s) => ({ speaking: 0,
module: s.module, level: 0,
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), };
})); bandScores.forEach((b) => (levels[b.module] += b.level));
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0}; return calculateAverageLevel(levels);
bandScores.forEach((b) => (levels[b.module] += b.level)); };
return calculateAverageLevel(levels); const AssignmentsPage = () => {
}; const activeFilter = (a: Assignment) =>
moment(a.endDate).isAfter(moment()) &&
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 futureFilter = (a: Assignment) =>
moment(a.startDate).isAfter(moment());
const AssignmentsPage = () => { return (
const activeFilter = (a: Assignment) => <>
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; <AssignmentView
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived; isOpen={!!selectedAssignment && !isCreatingAssignment}
const archivedFilter = (a: Assignment) => a.archived; onClose={() => {
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); setSelectedAssignment(undefined);
setIsCreatingAssignment(false);
reloadAssignments();
}}
assignment={selectedAssignment}
/>
<AssignmentCreator
assignment={selectedAssignment}
groups={groups.filter(
(x) => x.admin === user.id || x.participants.includes(user.id)
)}
users={users.filter(
(x) =>
x.type === "student" &&
(!!selectedUser
? groups
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id) || false
: groups.flatMap((g) => g.participants).includes(x.id))
)}
assigner={user.id}
isCreating={isCreatingAssignment}
cancelCreation={() => {
setIsCreatingAssignment(false);
setSelectedAssignment(undefined);
reloadAssignments();
}}
/>
<div className="w-full flex justify-between items-center">
<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>
<div
onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<span>Reload</span>
<BsArrowRepeat
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div>
</div>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Active Assignments ({assignments.filter(activeFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(activeFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Planned Assignments ({assignments.filter(futureFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
<div
onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"
>
<BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span>
</div>
{assignments.filter(futureFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
}}
key={a.id}
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Past Assignments ({assignments.filter(pastFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(pastFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowArchive
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Archived Assignments ({assignments.filter(archivedFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(archivedFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowUnarchive
/>
))}
</div>
</section>
</>
);
};
return ( const DefaultDashboard = () => (
<> <>
<AssignmentView {corporateUserToShow && (
isOpen={!!selectedAssignment && !isCreatingAssignment} <div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
onClose={() => { Linked to:{" "}
setSelectedAssignment(undefined); <b>
setIsCreatingAssignment(false); {corporateUserToShow?.corporateInformation?.companyInformation
reloadAssignments(); .name || corporateUserToShow.name}
}} </b>
assignment={selectedAssignment} </div>
/> )}
<AssignmentCreator <section
assignment={selectedAssignment} className={clsx(
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))} "flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
users={users.filter( !!corporateUserToShow && "mt-12 xl:mt-6"
(x) => )}
x.type === "student" && >
(!!selectedUser <IconCard
? groups onClick={() => setPage("students")}
.filter((g) => g.admin === selectedUser.id) Icon={BsPersonFill}
.flatMap((g) => g.participants) label="Students"
.includes(x.id) || false value={users.filter(studentFilter).length}
: groups.flatMap((g) => g.participants).includes(x.id)), color="purple"
)} />
assigner={user.id} <IconCard
isCreating={isCreatingAssignment} Icon={BsClipboard2Data}
cancelCreation={() => { label="Exams Performed"
setIsCreatingAssignment(false); value={
setSelectedAssignment(undefined); stats.filter((s) =>
reloadAssignments(); groups.flatMap((g) => g.participants).includes(s.user)
}} ).length
/> }
<div className="w-full flex justify-between items-center"> color="purple"
<div />
onClick={() => setPage("")} <IconCard
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> Icon={BsPaperclip}
<BsArrowLeft className="text-xl" /> label="Average Level"
<span>Back</span> value={averageLevelCalculator(
</div> stats.filter((s) =>
<div groups.flatMap((g) => g.participants).includes(s.user)
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"> ).toFixed(1)}
<span>Reload</span> color="purple"
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} /> />
</div> <IconCard
</div> Icon={BsPeople}
<section className="flex flex-col gap-4"> label="Groups"
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2> value={groups.length}
<div className="flex flex-wrap gap-2"> color="purple"
{assignments.filter(activeFilter).map((a) => ( onClick={() => setPage("groups")}
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} /> />
))} <div
</div> onClick={() => setPage("assignments")}
</section> 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"
<section className="flex flex-col gap-4"> >
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2> <BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<div className="flex flex-wrap gap-2"> <span className="flex flex-col gap-1 items-center text-xl">
<div <span className="text-lg">Assignments</span>
onClick={() => setIsCreatingAssignment(true)} <span className="font-semibold text-mti-purple-light">
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"> {assignments.filter((a) => !a.archived).length}
<BsPlus className="text-6xl" /> </span>
<span className="text-lg">New Assignment</span> </span>
</div> </div>
{assignments.filter(futureFilter).map((a) => ( </section>
<AssignmentCard
{...a}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
}}
key={a.id}
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(pastFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowArchive
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(archivedFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowUnarchive
/>
))}
</div>
</section>
</>
);
};
const DefaultDashboard = () => ( <section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
<> <div className="bg-white shadow flex flex-col rounded-xl w-full">
{corporateUserToShow && ( <span className="p-4">Latest students</span>
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b> {users
</div> .filter(studentFilter)
)} .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
<section .map((x) => (
className={clsx( <UserDisplay key={x.id} {...x} />
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center", ))}
!!corporateUserToShow && "mt-12 xl:mt-6", </div>
)}> </div>
<IconCard <div className="bg-white shadow flex flex-col rounded-xl w-full">
onClick={() => setPage("students")} <span className="p-4">Highest level students</span>
Icon={BsPersonFill} <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
label="Students" {users
value={users.filter(studentFilter).length} .filter(studentFilter)
color="purple" .sort(
/> (a, b) =>
<IconCard calculateAverageLevel(b.levels) -
Icon={BsClipboard2Data} calculateAverageLevel(a.levels)
label="Exams Performed" )
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length} .map((x) => (
color="purple" <UserDisplay key={x.id} {...x} />
/> ))}
<IconCard </div>
Icon={BsPaperclip} </div>
label="Average Level" <div className="bg-white shadow flex flex-col rounded-xl w-full">
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} <span className="p-4">Highest exam count students</span>
color="purple" <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
/> {users
<IconCard Icon={BsPeople} label="Groups" value={groups.length} color="purple" onClick={() => setPage("groups")} /> .filter(studentFilter)
<div .sort(
onClick={() => setPage("assignments")} (a, b) =>
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"> Object.keys(groupByExam(getStatsByStudent(b))).length -
<BsEnvelopePaper className="text-6xl text-mti-purple-light" /> Object.keys(groupByExam(getStatsByStudent(a))).length
<span className="flex flex-col gap-1 items-center text-xl"> )
<span className="text-lg">Assignments</span> .map((x) => (
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span> <UserDisplay key={x.id} {...x} />
</span> ))}
</div> </div>
</section> </div>
</section>
</>
);
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between"> return (
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <>
<span className="p-4">Latest students</span> <Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <>
{users {selectedUser && (
.filter(studentFilter) <div className="w-full flex flex-col gap-8">
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) <UserCard
.map((x) => ( loggedInUser={user}
<UserDisplay key={x.id} {...x} /> onClose={(shouldReload) => {
))} setSelectedUser(undefined);
</div> if (shouldReload) reload();
</div> }}
<div className="bg-white shadow flex flex-col rounded-xl w-full"> onViewStudents={
<span className="p-4">Highest level students</span> selectedUser.type === "corporate" ||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> selectedUser.type === "teacher"
{users ? () => setPage("students")
.filter(studentFilter) : undefined
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) }
.map((x) => ( onViewTeachers={
<UserDisplay key={x.id} {...x} /> selectedUser.type === "corporate"
))} ? () => setPage("teachers")
</div> : undefined
</div> }
<div className="bg-white shadow flex flex-col rounded-xl w-full"> user={selectedUser}
<span className="p-4">Highest exam count students</span> />
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> </div>
{users )}
.filter(studentFilter) </>
.sort( </Modal>
(a, b) => {page === "students" && <StudentsList />}
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length, {page === "groups" && <GroupsList />}
) {page === "assignments" && <AssignmentsPage />}
.map((x) => ( {page === "" && <DefaultDashboard />}
<UserDisplay key={x.id} {...x} /> </>
))} );
</div>
</div>
</section>
</>
);
return (
<>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
<>
{selectedUser && (
<div className="w-full flex flex-col gap-8">
<UserCard
loggedInUser={user}
onClose={(shouldReload) => {
setSelectedUser(undefined);
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
}
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
user={selectedUser}
/>
</div>
)}
</>
</Modal>
{page === "students" && <StudentsList />}
{page === "groups" && <GroupsList />}
{page === "assignments" && <AssignmentsPage />}
{page === "" && <DefaultDashboard />}
</>
);
} }

View File

@@ -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>
))} ))}

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,85 @@
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {sessionOptions} from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import useFilterStore from "@/stores/listFilterStore"; import useFilterStore from "@/stores/listFilterStore";
import {withIronSessionSsr} from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import Head from "next/head"; import Head from "next/head";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {useEffect} from "react"; import { useEffect } from "react";
import {BsArrowLeft} from "react-icons/bs"; import { BsArrowLeft } from "react-icons/bs";
import {ToastContainer} from "react-toastify"; import { ToastContainer } from "react-toastify";
import UserList from "../(admin)/Lists/UserList"; import UserList from "../(admin)/Lists/UserList";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user; const user = req.session.user;
const envVariables: {[key: string]: string} = {}; const envVariables: { [key: string]: string } = {};
Object.keys(process.env) Object.keys(process.env)
.filter((x) => x.startsWith("NEXT_PUBLIC")) .filter((x) => x.startsWith("NEXT_PUBLIC"))
.forEach((x: string) => { .forEach((x: string) => {
envVariables[x] = process.env[x]!; envVariables[x] = process.env[x]!;
}); });
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
return { return {
redirect: { redirect: {
destination: "/login", destination: "/login",
permanent: false, permanent: false,
} },
}; };
} }
return { return {
props: {user: req.session.user, envVariables}, props: { user: req.session.user, envVariables },
}; };
}, sessionOptions); }, sessionOptions);
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) => [
const router = useRouter(); state.userFilters,
state.clearUserFilters,
]);
const router = useRouter();
return ( return (
<> <>
<Head> <Head>
<title>EnCoach</title> <title>EnCoach</title>
<meta <meta
name="description" name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop." content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/> />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<ToastContainer /> <ToastContainer />
{user && ( {user && (
<Layout user={user}> <Layout user={user}>
<div className="flex flex-col gap-4"> <UserList
<div user={user}
onClick={() => { filters={filters.map((f) => f.filter)}
clearFilters(); renderHeader={(total) => (
router.back(); <div className="flex flex-col gap-4">
}} <div
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> onClick={() => {
<BsArrowLeft className="text-xl" /> clearFilters();
<span>Back</span> router.back();
</div> }}
<h2 className="text-2xl font-semibold">Users ({filters.map((f) => f.filter).reduce((d, f) => d.filter(f), users).length})</h2> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
</div> >
<BsArrowLeft className="text-xl" />
<UserList user={user} filters={filters.map((f) => f.filter)} /> <span>Back</span>
</Layout> </div>
)} <h2 className="text-2xl font-semibold">Users ({total})</h2>
</> </div>
); )}
/>
</Layout>
)}
</>
);
} }

View File

@@ -1,415 +1,614 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import Head from "next/head"; import Head from "next/head";
import {withIronSessionSsr} from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import {Stat, User} from "@/interfaces/user"; import { Stat, User } from "@/interfaces/user";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import {convertToUserSolutions, groupByDate} from "@/utils/stats"; import { convertToUserSolutions, groupByDate } from "@/utils/stats";
import moment from "moment"; import moment from "moment";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {ToastContainer} from "react-toastify"; import { ToastContainer } from "react-toastify";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {uniqBy} from "lodash"; import { uniqBy } from "lodash";
import {getExamById} from "@/utils/exams"; import { getExamById } from "@/utils/exams";
import {sortByModule} from "@/utils/moduleUtils"; 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 }) => {
const user = req.session.user;
export const getServerSideProps = withIronSessionSsr(({req, res}) => { if (!user || !user.isVerified) {
const user = req.session.user; return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
if (!user || !user.isVerified) { if (shouldRedirectHome(user)) {
return { return {
redirect: { redirect: {
destination: "/login", destination: "/",
permanent: false, permanent: false,
}, },
}; };
} }
if (shouldRedirectHome(user)) { return {
return { props: { user: req.session.user },
redirect: { };
destination: "/",
permanent: false,
},
};
}
return {
props: {user: req.session.user},
};
}, sessionOptions); }, sessionOptions);
export default function History({user}: {user: User}) { const defaultSelectableCorporate = {
const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id); value: "",
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>(); label: "All",
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">(); };
const {assignments} = useAssignments({});
const {users} = useUsers(); export default function History({ user }: { user: User }) {
const {stats, isLoading: isStatsLoading} = useStats(statsUserId); const [statsUserId, setStatsUserId] = useRecordStore((state) => [
const {groups} = useGroups(user.id); state.selectedUser,
state.setSelectedUser,
]);
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
const [filter, setFilter] = useState<
"months" | "weeks" | "days" | "assignments"
>();
const { assignments } = useAssignments({});
const setExams = useExamStore((state) => state.setExams); const { users } = useUsers();
const setShowSolutions = useExamStore((state) => state.setShowSolutions); const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
const setUserSolutions = useExamStore((state) => state.setUserSolutions); const { groups: allGroups } = useGroups();
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setInactivity = useExamStore((state) => state.setInactivity);
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
const router = useRouter();
const renderPdfIcon = usePDFDownload("stats");
useEffect(() => { const groups = allGroups.filter((x) => x.admin === user.id);
if (stats && !isStatsLoading) {
setGroupedStats(
groupByDate(
stats.filter((x) => {
if (
(x.module === "writing" || x.module === "speaking") &&
!x.isDisabled &&
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
)
return false;
return true;
}),
),
);
}
}, [stats, isStatsLoading]);
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => { const setExams = useExamStore((state) => state.setExams);
setFilter((prev) => (prev === value ? undefined : value)); const setShowSolutions = useExamStore((state) => state.setShowSolutions);
}; const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setInactivity = useExamStore((state) => state.setInactivity);
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
const router = useRouter();
const renderPdfIcon = usePDFDownload("stats");
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => { useEffect(() => {
if (filter && filter !== "assignments") { if (stats && !isStatsLoading) {
const filterDate = moment() setGroupedStats(
.subtract({[filter as string]: 1}) groupByDate(
.format("x"); stats.filter((x) => {
const filteredStats: {[key: string]: Stat[]} = {}; if (
(x.module === "writing" || x.module === "speaking") &&
!x.isDisabled &&
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
)
return false;
return true;
})
)
);
}
}, [stats, isStatsLoading]);
Object.keys(stats).forEach((timestamp) => { // useEffect(() => {
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp]; // // just set this initially
}); // if (!statsUserId) setStatsUserId(user.id);
// }, []);
return filteredStats; const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
} setFilter((prev) => (prev === value ? undefined : value));
};
if (filter && filter === "assignments") { const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
const filteredStats: {[key: string]: Stat[]} = {}; if (filter && filter !== "assignments") {
const filterDate = moment()
.subtract({ [filter as string]: 1 })
.format("x");
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 (timestamp >= filterDate)
filteredStats[timestamp] = [...stats[timestamp].filter((s) => !!s.assignment)]; filteredStats[timestamp] = stats[timestamp];
}); });
return filteredStats; return filteredStats;
} }
return stats; if (filter && filter === "assignments") {
}; const filteredStats: { [key: string]: Stat[] } = {};
const formatTimestamp = (timestamp: string) => { Object.keys(stats).forEach((timestamp) => {
const date = moment(parseInt(timestamp)); if (
const formatter = "YYYY/MM/DD - HH:mm"; stats[timestamp]
.map((s) => s.assignment === undefined)
.includes(false)
)
filteredStats[timestamp] = [
...stats[timestamp].filter((s) => !!s.assignment),
];
});
return date.format(formatter); return filteredStats;
}; }
const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => { return stats;
const scores: {[key in Module]: {total: number; missing: number; correct: number}} = { };
reading: {
total: 0,
correct: 0,
missing: 0,
},
listening: {
total: 0,
correct: 0,
missing: 0,
},
writing: {
total: 0,
correct: 0,
missing: 0,
},
speaking: {
total: 0,
correct: 0,
missing: 0,
},
level: {
total: 0,
correct: 0,
missing: 0,
},
};
stats.forEach((x) => { const formatTimestamp = (timestamp: string) => {
scores[x.module!] = { const date = moment(parseInt(timestamp));
total: scores[x.module!].total + x.score.total, const formatter = "YYYY/MM/DD - HH:mm";
correct: scores[x.module!].correct + x.score.correct,
missing: scores[x.module!].missing + x.score.missing,
};
});
return Object.keys(scores) return date.format(formatter);
.filter((x) => scores[x as Module].total > 0) };
.map((x) => ({module: x as Module, ...scores[x as Module]}));
};
const customContent = (timestamp: string) => { const aggregateScoresByModule = (
if (!groupedStats) return <></>; stats: Stat[]
): { module: Module; total: number; missing: number; correct: number }[] => {
const scores: {
[key in Module]: { total: number; missing: number; correct: number };
} = {
reading: {
total: 0,
correct: 0,
missing: 0,
},
listening: {
total: 0,
correct: 0,
missing: 0,
},
writing: {
total: 0,
correct: 0,
missing: 0,
},
speaking: {
total: 0,
correct: 0,
missing: 0,
},
level: {
total: 0,
correct: 0,
missing: 0,
},
};
const dateStats = groupedStats[timestamp]; stats.forEach((x) => {
const correct = dateStats.reduce((accumulator, current) => accumulator + current.score.correct, 0); scores[x.module!] = {
const total = dateStats.reduce((accumulator, current) => accumulator + current.score.total, 0); total: scores[x.module!].total + x.score.total,
const aggregatedScores = aggregateScoresByModule(dateStats).filter((x) => x.total > 0); correct: scores[x.module!].correct + x.score.correct,
const assignmentID = dateStats.reduce((_, current) => current.assignment as any, ""); missing: scores[x.module!].missing + x.score.missing,
const assignment = assignments.find((a) => a.id === assignmentID); };
const isDisabled = dateStats.some((x) => x.isDisabled); });
const aggregatedLevels = aggregatedScores.map((x) => ({ return Object.keys(scores)
module: x.module, .filter((x) => scores[x as Module].total > 0)
level: calculateBandScore(x.correct, x.total, x.module, user.focus), .map((x) => ({ module: x as Module, ...scores[x as Module] }));
})); };
const {timeSpent, inactivity, session} = dateStats[0]; const customContent = (timestamp: string) => {
if (!groupedStats) return <></>;
const selectExam = () => { const dateStats = groupedStats[timestamp];
const examPromises = uniqBy(dateStats, "exam").map((stat) => { const correct = dateStats.reduce(
console.log({stat}); (accumulator, current) => accumulator + current.score.correct,
return getExamById(stat.module, stat.exam); 0
}); );
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 isDisabled = dateStats.some((x) => x.isDisabled);
Promise.all(examPromises).then((exams) => { const aggregatedLevels = aggregatedScores.map((x) => ({
if (exams.every((x) => !!x)) { module: x.module,
if (!!timeSpent) setTimeSpent(timeSpent); level: calculateBandScore(x.correct, x.total, x.module, user.focus),
if (!!inactivity) setInactivity(inactivity); }));
setUserSolutions(convertToUserSolutions(dateStats)); const { timeSpent, inactivity, session } = dateStats[0];
setShowSolutions(true);
setExams(exams.map((x) => x!).sort(sortByModule));
setSelectedModules(
exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
);
router.push("/exercises");
}
});
};
const textColor = clsx( const selectExam = () => {
correct / total >= 0.7 && "text-mti-purple", const examPromises = uniqBy(dateStats, "exam").map((stat) => {
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", console.log({ stat });
correct / total < 0.3 && "text-mti-rose", return getExamById(stat.module, stat.exam);
); });
const content = ( Promise.all(examPromises).then((exams) => {
<> if (exams.every((x) => !!x)) {
<div className="w-full flex justify-between -md:items-center 2xl:items-center"> if (!!timeSpent) setTimeSpent(timeSpent);
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2"> if (!!inactivity) setInactivity(inactivity);
<span className="font-medium">{formatTimestamp(timestamp)}</span>
<div className="flex items-center gap-2">
{!!timeSpent && (
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Time Spent">
<BsClock /> {Math.floor(timeSpent / 60)} minutes
</span>
)}
{!!inactivity && (
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Inactivity">
<BsXCircle /> {Math.floor(inactivity / 60)} minutes
</span>
)}
</div>
</div>
<div className="flex flex-row gap-2">
<span className={textColor}>
Level{" "}
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
</span>
{renderPdfIcon(session, textColor, textColor)}
</div>
</div>
<div className="w-full flex flex-col gap-1"> setUserSolutions(convertToUserSolutions(dateStats));
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2"> setShowSolutions(true);
{aggregatedLevels.map(({module, level}) => ( setExams(exams.map((x) => x!).sort(sortByModule));
<div setSelectedModules(
key={module} exams
className={clsx( .map((x) => x!)
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl", .sort(sortByModule)
module === "reading" && "bg-ielts-reading", .map((x) => x!.module)
module === "listening" && "bg-ielts-listening", );
module === "writing" && "bg-ielts-writing", router.push("/exercises");
module === "speaking" && "bg-ielts-speaking", }
module === "level" && "bg-ielts-level", });
)}> };
{module === "reading" && <BsBook className="w-4 h-4" />}
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
{module === "writing" && <BsPen className="w-4 h-4" />}
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
{module === "level" && <BsClipboard className="w-4 h-4" />}
<span className="text-sm">{level.toFixed(1)}</span>
</div>
))}
</div>
{assignment && ( const textColor = clsx(
<span className="font-light text-sm"> correct / total >= 0.7 && "text-mti-purple",
Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name} correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
</span> correct / total < 0.3 && "text-mti-rose"
)} );
</div>
</>
);
return ( const content = (
<> <>
<div <div className="w-full flex justify-between -md:items-center 2xl:items-center">
key={uuidv4()} <div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
className={clsx( <span className="font-medium">{formatTimestamp(timestamp)}</span>
"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", <div className="flex items-center gap-2">
isDisabled && "grayscale tooltip", {!!timeSpent && (
correct / total >= 0.7 && "hover:border-mti-purple", <span
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", className="text-sm flex gap-2 items-center tooltip"
correct / total < 0.3 && "hover:border-mti-rose", data-tip="Time Spent"
)} >
onClick={isDisabled ? () => null : selectExam} <BsClock /> {Math.floor(timeSpent / 60)} minutes
data-tip="This exam is still being evaluated..." </span>
role="button"> )}
{content} {!!inactivity && (
</div> <span
<div className="text-sm flex gap-2 items-center tooltip"
key={uuidv4()} data-tip="Inactivity"
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", <BsXCircle /> {Math.floor(inactivity / 60)} minutes
correct / total >= 0.7 && "hover:border-mti-purple", </span>
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", )}
correct / total < 0.3 && "hover:border-mti-rose", </div>
)} </div>
data-tip="Your screen size is too small to view previous exams." <div className="flex flex-row gap-2">
role="button"> <span className={textColor}>
{content} Level{" "}
</div> {(
</> aggregatedLevels.reduce(
); (accumulator, current) => accumulator + current.level,
}; 0
) / aggregatedLevels.length
).toFixed(1)}
</span>
{renderPdfIcon(session, textColor, textColor)}
</div>
</div>
return ( <div className="w-full flex flex-col gap-1">
<> <div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
<Head> {aggregatedLevels.map(({ module, level }) => (
<title>Record | EnCoach</title> <div
<meta key={module}
name="description" className={clsx(
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop." "flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
/> module === "reading" && "bg-ielts-reading",
<meta name="viewport" content="width=device-width, initial-scale=1" /> module === "listening" && "bg-ielts-listening",
<link rel="icon" href="/favicon.ico" /> module === "writing" && "bg-ielts-writing",
</Head> module === "speaking" && "bg-ielts-speaking",
<ToastContainer /> module === "level" && "bg-ielts-level"
{user && ( )}
<Layout user={user}> >
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center"> {module === "reading" && <BsBook className="w-4 h-4" />}
<div className="xl:w-3/4"> {module === "listening" && <BsHeadphones className="w-4 h-4" />}
{(user.type === "developer" || user.type === "admin") && ( {module === "writing" && <BsPen className="w-4 h-4" />}
<Select {module === "speaking" && <BsMegaphone className="w-4 h-4" />}
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))} {module === "level" && <BsClipboard className="w-4 h-4" />}
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} <span className="text-sm">{level.toFixed(1)}</span>
onChange={(value) => setStatsUserId(value?.value)} </div>
styles={{ ))}
menuPortal: (base) => ({...base, zIndex: 9999}), </div>
option: (styles, state) => ({
...styles, {assignment && (
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", <span className="font-light text-sm">
color: state.isFocused ? "black" : styles.color, Assignment: {assignment.name}, Teacher:{" "}
}), {users.find((u) => u.id === assignment.assigner)?.name}
}} </span>
/> )}
)} </div>
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && ( </>
<Select );
options={users
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id)) return (
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))} <>
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} <div
onChange={(value) => setStatsUserId(value?.value)} key={uuidv4()}
styles={{ className={clsx(
menuPortal: (base) => ({...base, zIndex: 9999}), "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",
option: (styles, state) => ({ isDisabled && "grayscale tooltip",
...styles, correct / total >= 0.7 && "hover:border-mti-purple",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", correct / total >= 0.3 &&
color: state.isFocused ? "black" : styles.color, correct / total < 0.7 &&
}), "hover:border-mti-red",
}} correct / total < 0.3 && "hover:border-mti-rose"
/> )}
)} onClick={isDisabled ? () => null : selectExam}
</div> data-tip="This exam is still being evaluated..."
<div className="flex gap-4 w-full justify-center xl:justify-end"> role="button"
<button >
className={clsx( {content}
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", </div>
"transition duration-300 ease-in-out", <div
filter === "assignments" && "!bg-mti-purple-light !text-white", key={uuidv4()}
)} className={clsx(
onClick={() => toggleFilter("assignments")}> "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",
Assignments correct / total >= 0.7 && "hover:border-mti-purple",
</button> correct / total >= 0.3 &&
<button correct / total < 0.7 &&
className={clsx( "hover:border-mti-red",
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", correct / total < 0.3 && "hover:border-mti-rose"
"transition duration-300 ease-in-out", )}
filter === "months" && "!bg-mti-purple-light !text-white", data-tip="Your screen size is too small to view previous exams."
)} role="button"
onClick={() => toggleFilter("months")}> >
Last month {content}
</button> </div>
<button </>
className={clsx( );
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", };
"transition duration-300 ease-in-out",
filter === "weeks" && "!bg-mti-purple-light !text-white", const selectableCorporates = [
)} defaultSelectableCorporate,
onClick={() => toggleFilter("weeks")}> ...users
Last week .filter((x) => x.type === "corporate")
</button> .map((x) => ({
<button value: x.id,
className={clsx( label: `${x.name} - ${x.email}`,
"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", ];
filter === "days" && "!bg-mti-purple-light !text-white",
)} const [selectedCorporate, setSelectedCorporate] = useState<string>(
onClick={() => toggleFilter("days")}> defaultSelectableCorporate.value
Last day );
</button>
</div> const getUsersList = (): User[] => {
</div> if (selectedCorporate) {
{groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && ( // get groups for that corporate
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6"> const selectedCorporateGroups = allGroups.filter(
{Object.keys(filterStatsByDate(groupedStats)) (x) => x.admin === selectedCorporate
.sort((a, b) => parseInt(b) - parseInt(a)) );
.map(customContent)}
</div> // get the teacher ids for that group
)} const selectedCorporateGroupsParticipants =
{groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && ( selectedCorporateGroups.flatMap((x) => x.participants);
<span className="font-semibold ml-1">No record to display...</span>
)} // // search for groups for these teachers
</Layout> // 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 (
<>
<Head>
<title>Record | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user}>
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
<div className="xl:w-3/4">
{(user.type === "developer" || user.type === "admin") && (
<>
<label className="font-normal text-base text-mti-gray-dim">
Corporate
</label>
<Select
options={selectableCorporates}
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)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color,
}),
}}
/>
</>
)}
{(user.type === "corporate" || user.type === "teacher") &&
groups.length > 0 && (
<>
<label className="font-normal text-base text-mti-gray-dim">
User
</label>
<Select
options={users
.filter((x) =>
groups.flatMap((y) => y.participants).includes(x.id)
)
.map((x) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
}))}
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(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,
}),
}}
/>
</>
)}
</div>
<div className="flex gap-4 w-full justify-center xl:justify-end">
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
filter === "assignments" && "!bg-mti-purple-light !text-white"
)}
onClick={() => toggleFilter("assignments")}
>
Assignments
</button>
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
filter === "months" && "!bg-mti-purple-light !text-white"
)}
onClick={() => toggleFilter("months")}
>
Last month
</button>
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
filter === "weeks" && "!bg-mti-purple-light !text-white"
)}
onClick={() => toggleFilter("weeks")}
>
Last week
</button>
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
filter === "days" && "!bg-mti-purple-light !text-white"
)}
onClick={() => toggleFilter("days")}
>
Last day
</button>
</div>
</div>
{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">
{Object.keys(filterStatsByDate(groupedStats))
.sort((a, b) => parseInt(b) - parseInt(a))
.map(customContent)}
</div>
)}
{groupedStats &&
Object.keys(groupedStats).length === 0 &&
!isStatsLoading && (
<span className="font-semibold ml-1">
No record to display...
</span>
)}
</Layout>
)}
</>
);
} }

View File

@@ -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
View 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;